Repository navigation
Releases: phantomghost2023/manual-cli
Release list
v0.11.4 — crates drill, multi-machine bounds, Node-free fallback
Four threads from the field, one release.
The canary triaged
The v0.11.3 wild run came back 8/11. All three red cells were legible:
- npm/cli — upstream drift. Their committed
package-lock.jsonis out of sync at HEAD;npm cifails before any verifier runs. The matrix cell moves to microsoft/TypeScript (npm, synced lockfile, suite runs in their CI). - yarn-1 on react-router — upstream migration. Their HEAD now declares
packageManager: yarn@pnpm@11.7.0; the tree the cell installs no longer exists. The cell pins the yarn-1 tree the local drill validated. - Python lifecycle on httpx — honest report. Discovery proposed
tests.pythonfrom a layout that passes only through their no-dependency test helper; a plain-pytest claim cannot hold there. The lifecycle cell moves to pallets/flask, whose[tool.pytest.ini_options]the canary can read.
Two maintenance rows and one honest report — nothing reverted, exactly the split the workflow header promises.
The crates drill (sharkdp/bat, 316 locked crates)
Two bugs, both found only on the wild repo:
- False positives (5 + 2 stale, 2.2%). Cargo registry dirs are
name-version, and build metadata liketoml-1.1.2+spec-1.1.0contains dashes; last-dash splitting parsed such crates as nametoml-1.1.2+spec, version1.1.0. The split now resolves like cargo itself does: the earliest dash-remainder that parses as a full semver is the version. Healthy verdict after:ok: true — 316 crate(s) present, matching Cargo.lockin 17 ms, zero false positives; damage inregistry/src+registry/cacheflagged by name and repaired bycargo fetch. - A false negative. With a populated
$CARGO_HOMEand avendor/tree, deleting a vendored crate still verifiedok: true— the shared registry cache masked the gap (the twin of the Gomodules.txthole). The vendored tree is checked on its own now (both cargo layouts), and the verdict follows the wiring rule: when.cargo/configreplaces crates.io with the vendored sources, a gap is a build failure and answersno; an unwired vendor — bat ships a rustflags-only config — is spare evidence, the gap named without judging. The unwiredcargo vendordamage test (deleted crate, no config) answersok: truewith the gap named; wired, the same damage answersok: false.
observe aggregates across machines
Raise and tighten proposals now merge the committed ledger's per-machine history with the local tail, so a bound reflects the slowest machine's story: the comparator is the slowest machine's p90 against every other machine's p90, and tightening is refused when the local tail would flake the machine the ledger knows about. Ledger runs killed at the bound — which carry no duration — count as raise evidence instead of vanishing as no-data, and a raise must clear the bound that failed. One machine? Behavior unchanged.
A manual outlives Node
scripts/fallback/verify.py and scripts/fallback/brief.py: the smallest honest slice of manual verify for hosts with no Node — same manual/v1 frontmatter, command and expression checks (exit-code expectations, exists(), manifest(), lockActive()), npm/yarn-1/pnpm/venv/gomod lockfile comparisons with the same report-not-judge answers (other-platform entries skipped, uv-lock refusal honored), prerequisite satisfaction, and state.json stamps marked "tool": "python-fallback". No sandbox, no digests, no ledger — the banner says so every run. Proven on the demo repo (5/5 fresh, including the trap's exit-7 expectation), an npm fixture, and a Go checkout running its real 27-second suite under bash.
283 tests green. Full changelog in CHANGELOG.md; the drills and what they found in docs/WILD-REPOS.md.
v0.11.3 — bounds that age with the suite, and canaries that watch the wild
observe recalibrates bounds from history — and rewrites the measurement with them
A bound calibrated once at init is a snapshot; the suite it describes keeps moving. raise is the new middle direction between tighten and relax: when the worst run meets the bound or the 90th percentile crowds it (75%), observe proposes raising the bound while the claim is still fresh — before a busy machine turns a suite that drifted from 2s to 24s under a 30s bound into a false alarm. All three directions share one formula (3× p50, 1.5× p90, 1.1× worst), because two formulas can disagree about one series.
Every proposal — tighten, raise, relax — now also patches observation.measured_ms to the median of the accumulated history, with observation.samples and observation.measured_from: "verify history" as provenance. The claim stops advertising the single number init's probe took the day it was written.
The wild drills are release canaries
.github/workflows/wild.yml fires on release publish, monthly, and on demand — 8 builtin cells (pnpm/npm/yarn/bun on vuejs/core, npm/cli, react-router, oven-sh/bun; venv on httpx and flask; gomod on cobra and caddy — each cloned and installed by the ecosystem's own tooling) plus 3 lifecycle cells running the full init → probe → accept → verify path with the install performed by verify's prerequisite machinery. The one rule: a healthy tree must never be reported unsatisfied — and ok: null, correctly withheld, is a pass. test/wild.mjs is the checker; docs/WILD-REPOS.md documents every drill's false-positive rate and what it changed.
Discovery beyond package.json
manual init on spf13/cobra printed "0 candidate(s)" on a repo with a test suite — the tool shipped verifiers for eight ecosystems and proposed no claim about seven of them. Discovery now reads go.mod + _test.go globs (a testless module's go test ./... exits 0 having tested nothing, so it is not proposed), vendored-Go facts, pyproject/requirements/uv.lock and declared pytest. The Python install is chosen from the lockfile: uv sync --frozen, poetry install, or venv+pip. A repository-level go mod download prerequisite is declared when the module cache is empty — the one shared-location write discovery proposes, because a module cache is additive and re-derivable. The lifecycle canary now fails on "0 candidates": "proposed nothing" and "had nothing to propose" must not look identical.
Fixed: two bugs only a lifecycle could catch
- A vendored Go tree was trusted on its manifest. Deleting
vendor/github.com/spf13/pflagstill verifiedok: true— the vendor branch compared go.mod againstvendor/modules.txtand never looked at the disk. Proven at caddy's scale (787 packages promised, one deleted, still "ok") before fixing: the manifest's package lines are checked as directories now (healthy caddy: 379 ms; damage: named). Same hole yarn-1's integrity check has; the lesson generalizes: a lockfile is evidence about an install, only the tree can confirm it. - A repo's own virtualenv never reached PATH on Windows.
localBins()listed only.venv/bin; on encode/httpx a fresh venv was built, a 64 s install succeeded, andpython -m pytestwas still answered by the system interpreter. Both layouts are on PATH now.
277 tests / 52 suites green (11 new), own manual 5/5 fresh.
v0.11.2 — the pnpm builtin tells the truth on pnpm 12, and bounds that measure themselves
[0.11.2] - 2026-09-22
Fixed
- The pnpm builtin reported up to 165 false "missing" packages on a freshly synced pnpm-12 tree. Measured end to end on vuejs/core (660 lockfile entries,
pnpm install --frozen-lockfileexit 0 immediately before the check). Four independent causes, each found on the wild repo and each fixed against it:- Peer-suffix directory names. pnpm 11 named store dirs
name@version; pnpm 12 names them after the lockfile's snapshot key — the peer-resolved form, peers joined by_— and past 60 characters storesfirst 27 chars + '_' + sha256(key)[:32]. The recipe was proven before use (19/19 hash directories matched, zero store directories uncovered) and matching is exact again, derived from the lockfile's own spelling rather than a heuristic. - A nested YAML key hijacked the parser.
snapshots:entries indentdependencies:maps; the section-key regex let four-space lines match, so the current entry flipped to a junk key and theoptional: truethat followed landed nowhere —@emnapi/*counted missing though its snapshot marks it optional. Regexes that count spaces now count them exactly. - The package manager records itself.
packageManager: pnpm@12.4.2produces a lockfile entry for pnpm that never materializes in the project's virtual store. Same rule as platform binaries: a package not supposed to be here is not missing. - pnpm 12 filters the recorded lockfile copy.
node_modules/.pnpm/lock.yamlholds 645 of 660 entries — exactly the other-OS binaries dropped — so neither full nor machine-expected set equality holds. The check is now subset in both directions: every copied package must be in the lockfile (a foreign resolve adds packages), and every package this machine must have must be in the copy (a copy from another machine lacks them).
After the fixes the wild verdict isok: true — 489 package(s) present, matching pnpm-lock.yaml (152 platform-specific or optional skipped, 1 packageManager self-reference skipped)in 17 ms, with damage detection re-proven both ways (deletedvite@8.3.0→ flagged;pnpm install --frozen-lockfile→ restored →ok: true).
- Peer-suffix directory names. pnpm 11 named store dirs
- A check stopped by its time bound stamped
broken exit ?. Broken means the check ran and said no; a killed run never ran to a verdict. It isblocked — untested, not false, with a note naming the bound and suggestingmax_ms. Found on the same wild repo, whose suite needs ~5 minutes against the 120 s default. (The bareexit nullnote was made legible asno exit within Nsearlier in the same pass.) initdiscovered nothing on apnpm-workspace.yamlmonorepo. The earlier workspaces fix readpackage.json'sworkspaceskey; vuejs/core declares its packages in pnpm's own workspace file, so the same zero-candidates breakdown recurred on the very ecosystem this drill targets.pnpm-workspace.yamlnow counts as evidence that packages with code exist.- Sandbox teardown could crash a verify that already had its results. On Windows, an
EBUSYwhile removing the temp worktree killed the run after the check but beforestate.save()— stamp lost, worktree leaked, observed live on the wild repo. Teardown is best-effort: retries briefly, then warns and moves on, so a verify's verdict survives a dirty teardown. - Three more package-manager builtins met their wild repos; each had its own false-positive class.
- npm on npm/cli: a lockfile lists every platform's variants (
@typescript/typescript-aix-*…) butnpm cimaterializes only this machine's — 20 of 1181 "missing", every one foreign-platform. The platform rule pnpm and bun already had is npm's too now:ok: true — 1161 present, 20 platform skipped. - bun on oven-sh/bun (isolated linker): bun 1.2 suffixes a store directory with
+<hex>when the package resolves peers (@types+react-dom@18.3.7+52f32cb6c6aeed77), which exactname@versionmatching can never hit — a freshly installed healthy store read 3/23 missing. Matching folds the suffix off, raw name first so semver build metadata still matches only itself; damage/repair re-proven live (deletedtypescript@6.0.2→ named; fresh install →ok: true). - yarn 1 held up: healthy tree zero false positives, root-package damage reported rather than judged,
--check-filesrepair restoredok: true.
- npm on npm/cli: a lockfile lists every platform's variants (
inittrustednode_modulesexisting — but npm/cli commits most of itsnode_modulesto git (1367 files, no.bin), so a 122-package partial tree wired no prerequisite and the proposed suite claim ran against an install that was never made. When the tree exists, the ecosystem builtin gets the last word: a definitiveok: falsewiresrequires,ok: nullleaves discovery's judgment alone.- The git journal could silently lose an entry on a fast machine. Entry ids were second-resolution, so an accept and its immediate revert wrote the same filename — the revert overwrote the accept and the journal showed one entry where there were two. Windows passed by latency luck; the ubuntu CI runner caught it. Ids are collision-proof now.
- A vendored Go build reported "no module cache". The gate demanded
GOMODCACHEeven when the tree is vendored — correct on a dev machine, wrong on a fresh ubuntu runner. A vendored build verifies without a cache.
Added
- The repo's own manual runs in GitHub Actions on every push and PR (
.github/workflows/manual.yml), with the badge in the README — the same verify a contributor gets from the pre-commit gate, executed where nobody's laptop is involved. CONTRIBUTING.mddocuments regenerating the hero and social-preview images and keeping the two cards in sync.initcalibrates a suite's time bound instead of inventing one. A discovered command check no longer ships a hardcodedmax_ms: 120000; the probe measures the first honest run and writesmax(120000, measured × 3)rounded to seconds, withobservation.measured_msas provenance — a five-minute suite gets a bound that can hold it (vuejs/core's ~293 s → 879000) while fast suites keep today's default formanual observeto tighten later on accumulated evidence. A check that never finishes gets one 600 s calibration window, then an honestblocked. Along the way the probe adopted verify's own doctrine — a run stopped by a bound isblocked, untested not false (it used to file thembroken) — and its note stopped printing max_ms as if it were seconds (120000s).verifywaits for a claim's own bound (checkTimeoutMs): the configdefault_timeout_swas passed as an explicit timeout, which the runner prefers over every claim'smax_ms— calibrated bounds would have been decorative, a 15-minute suite still killed at 60 s. The config default now applies only to claims that declare no bound.- Regression tests for every fix above: the hash-truncated pnpm-12 directory (fixture uses the real pnpm-computed hash, not a reimplementation), the nested
optional: true, thepackageManagerself-reference, the timeout-blocked stamp, and the EBUSY-tolerant teardown (platform-aware: a live process parked in the worktree on Windows, clean removal on POSIX).
See docs/FIELD-NOTES.md, "Eighth pass" (the pnpm-12 monorepo) and "Ninth pass" (npm, yarn 1, bun, and the bound that measures itself), for the full drill narratives.
v0.11.0 — yarn (classic + berry) and bun, measured before judged
Every Node package manager a repo can use
verify: { builtin: yarn } and verify: { builtin: bun } complete the Node set, so a cached install is checkable in 2–5 ms whichever manager wrote it — and each verifier was measured against the real manager before it was allowed a verdict.
- yarn, both generations. Classic yarn 1 compares the pattern → resolved-URL map in
node_modules/.yarn-integritywithyarn.lockand checks that every directory the install claims to have linked is on disk. Berry compares every location innode_modules/.yarn-state.ymlwith the locatorsyarn.lockresolves; a resolved package with no location here (optional, platform-specific) is reported, never judged missing. PnP stays unverifiable and says so. - bun, both linkers. Reads
bun.lock(trailing commas and all); in a hoisted tree the lockfile key path undernode_modulesis the install, in an isolated one each resolved package has its store directorynode_modules/.bun/<name>@<version>— scoped names spelled@scope+name, other-platform/optional entries skipped and counted. - The rule, enforced by the command itself. Classic yarn 1 trusts
.yarn-integrityand nothing else: measured on a real 1.22.22 clone, deletingnode_modules/is-oddleft both the integrity file andyarn install --frozen-lockfilesaying "Already up-to-date" (0.2 s) with the package still gone — while--check-filesre-links it in the same 0.2 s. So a classic repo's discovered prerequisite isyarn install --frozen-lockfile --check-files, and a missing linked directory is a verdict only under a command that repairs it. Berry rejects that flag outright; bun's isolated linker is the third case of the same rule and reports like pnpm.
The damage table (each cell measured):
| manager | damage | does its install repair it? | verdict allowed |
|---|---|---|---|
| yarn 1.22.22 | rm -rf node_modules/is-odd |
plain: no ("Already up-to-date") · --check-files/--force: yes (0.2 s) |
only under the repairing flag |
| yarn 4.9.0 | a .yarn-state.yml location |
yes (155 ms) | yes |
| bun 1.2 hoisted | a top-level package | yes (30–42 ms) | yes |
| bun 1.2 isolated | node_modules/.bun/<pkg>@<ver> |
no ("Checked 6 installs across 32 packages (no changes)") | reported, not judged |
Full details: CHANGELOG · field notes
v0.10.0 — Six ecosystem verifiers behind one word
Six ecosystems, one question
verify: { builtin: … } grows from npm-only to npm, pnpm, venv, gems, gomod, crates — plus auto, which picks from the setup's own evidence (or the one lockfile present) and says which it picked. Every builtin compares an installed tree against its own lockfile by reading directory listings, not by running the package manager's check.
| builtin | compares | answers in |
|---|---|---|
npm |
package-lock / npm-shrinkwrap (v1 + v3) vs node_modules |
~64 ms on 403 packages |
pnpm |
resolved set + pnpm's lockfile copy vs node_modules/.pnpm |
ms |
venv |
requirements/poetry/Pipfile/uv vs dist-info dirs (both Windows and POSIX layouts) |
ms |
gems |
Gemfile.lock specs vs vendor/bundle (BUNDLE_PATH honoured) |
ms |
gomod |
go.mod vs $GOMODCACHE (Go's !-escaped layout) or vendor/modules.txt |
ms |
crates |
Cargo.lock vs $CARGO_HOME/registry, vendor/ |
ms |
The rule the real repos forced on us
"No" is only said when the declared command can repair it — measured, not assumed: npm ci, pip install -r, bundle install and go mod download all restore damaged trees. pnpm 11 does not (a satisfied install leaves a deleted node_modules/.pnpm/<pkg> and a hand-modified .pnpm/lock.yaml alone, even with --force), and Cargo.lock covers dev-deps a plain build never fetches. There the builtin reports instead of judging, because a verdict it can never change is a rebuild loop forever:
⚙ setup: pnpm install --frozen-lockfile — reused, not re-confirmed: 1/2 package(s) the lockfile
lists are not in node_modules/.pnpm, e.g. is-number@6.0.0 — not a verdict: a satisfied
`pnpm install` re-imports only when the lockfile itself changes
Also fixed: a lockfileVersion: 1 npm lock used to declare nothing — which read as a yes for an empty tree — and init's virtualenv command was POSIX-only, so the first verify on Windows wrote a prerequisite that could not run.
Proven against real trees on one machine: npm 11, pnpm 11, a python -m venv, bundler 2.6.9, go 1.25, cargo 1.90 — each installed, damaged, and re-verified.
Full details: CHANGELOG · field notes
v0.9.0 — Prerequisites: declared once, verified before trusted
Prerequisites become a first-class object
A fresh clone is not a broken repository — but npm test without an install exits 127, and a claim used to read broken, blaming the repo for the checkout. v0.9.0 makes the install a prerequisite: declared once, deduplicated by (command, evidence content), and verified before it is trusted.
Highlights
- Prerequisites can depend on each other.
build: { run: make build, requires: [node], cache: ["dist"] }— a claim listing[build, node]in the wrong order still installs before it builds. Cycles are load errors;manual setup --planprints the deduped, ordered sequence and exits 1 while anything is unsatisfied (a CI preflight). - A satisfied prerequisite is verified, not merely remembered.
verify:re-checks a cached install: a command, or{ builtin: lockfile }, which stats the tree againstpackage-lock.json— 64 ms against 10.9 s fornpm ls --depth=0on a 403-package express tree. That 170× gap is why the cheap answer was never being run. share: truelets another checkout borrow a verified install. The store records where an install was verified (not a copy), re-verified at the moment of borrowing. A second express clone went from 43.0 s (install for itself) to 16.3 s end-to-end.
The honest findings
- Express ships
.npmrcwithpackage-lock=false, so "nothing to compare against" had been answered as "no" — reinstalling on every verify of a perfectly fine repo. A verifier now answers yes / no / cannot-tell, and the gap is reported where the person who can act on it is looking. - A normalized spec re-derived its builtin from a marker string, so the verifier ran as
bash -c 'builtin:lockfile'— exit 127 — and every verify distrusted its cache. Found on a real repo, not by the suite.
Verified on: a fresh expressjs/express clone (403 packages, 1261 tests) — damaged trees caught and repaired, borrowing proven across clones.
Full details: CHANGELOG · field notes