Skip to content

fix(scripts): anchor the shared CLI-build probe at the repo root - #11650

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11394-cli-prerequisite-root-anchor
Aug 24, 2026
Merged

fix(scripts): anchor the shared CLI-build probe at the repo root#11650
os-steve merged 1 commit into
mainfrom
claude/issue-11394-cli-prerequisite-root-anchor

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#11394

scripts/cli-build-prerequisite.mjs is the one shared answer to "is the workspace CLI built?", used by check-i18n-coverage and check-i18n-bundles. It read packages/cli/package.json against the cwd, so from any cwd but the repo root the probe ENOENTed and deferred — not because the declaration was unreadable, but because it had been looked for in the wrong place.

The read is now anchored to a root derived from import.meta.url, behind one exported seam (atRepoRoot) — the shape #10907 put on check-i18n-coverage.mjs one file over.

The vocabulary is untouched

CLI, CLI_PKG, and the path oclifCommandFileFor returns all stay repo-relative. They are the baseline keys, the text in every error message, the rerun commands, and — for CLI — an argv path spawned against a cwd of the repo root. Only the reads are anchored. Pinned by a self-test assertion (#11394 …spelled repo-relative).

Non-vacuity, both directions, on one fully built tree

check-i18n-coverage run from the repo root and from /tmp:

root/tmp
before (origin/main3637731, unbuilt)verdictcould not read packages/cli/package.json (ENOENT …) — build prerequisite not pre-checked, then 12 CLI spawns
after (this branch, fully built)OK (12 config(s), 657 baselined untranslated string(s), none new). exit 0byte-identical, exit 0, no deferral line

The two runs were diffed, not eyeballed: diff -q reports them identical. The card's residue is gone in the direction it was measured, and the root run is unchanged.

Reverting only the anchor (atRepoRoot = (rel) => join(rel)) on that same built tree brings the deferral line back verbatim, so the "after" column is not measuring an unrelated tree. Both ablation legs ran under trap … EXIT INT TERM, the mutation was confirmed on disk by marker counts and a sha change, and the restore was verified byte-identical by sha (077e3349d942abe9…), not merely by "it looked fine".

A second line changed, and why it is not scope creep

check-i18n-bundles.mjs asked existsSync(resolved.file) on that repo-relative answer unanchored. Anchoring only the read would have been strictly worse than the defect it fixes: the probe now resolves off-root, so the bare existsSync would return false and the gate would report

check-i18n-bundles: PREREQUISITE NOT MET — the workspace CLI is not built
packages/cli/dist/commands/i18n/extract.js

…about a file that exists. That is measured, not predicted — LEG B of a simulated-built-tree experiment produced exactly that output. It is also the #5862 defect (a confident diagnosis pointing somewhere innocent), and the correct shape was already declared in the sibling: check-i18n-coverage.mjs carries existsSync(at(resolved.file)) with a comment naming this precise failure. So the two lines are one change.

The instrument question

#11395's dev deliberately left this defect in place, because the deferring probe was the state that made the twelve-way cause split observable. That reason has discharged, verified on this base rather than assumed: groupFailuresByCause keys on [reason, fix] where fix is drawn from a closed set of constants (SHARED_REMEDIES), and rerunCommand(configPath) is rendered per config by reportUnmeasuredConfigs. The origin/main off-root run above confirms it live — 12 failing configs report 1 distinct cause, not twelve.

Nothing else depends on the deferral: resolveCliCommandFile has exactly two callers, and both consumers' --self-test reach the unknown branch through the pure oclifCommandFileFor({ oclif: {} }, …), which this change does not touch.

#11557 — an unmet prerequisite says so in words

This change does not add a new silent exit 1; it makes the worded path reachable off-root. Where an off-root reader previously got a deferral line and a pile of downstream noise, they now get the prerequisite verdict up front, which already states Nothing was measured: … so this result says NOTHING about whether any declared label went untranslated and warns that piping the gate hides its status. Zero CLI spawns instead of twelve.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (which reads the change set from git itself), and the union was run at 7a390dfb0 — the final commit — with every exit code captured before any pipe.

gateresult
check:i18n-coverageself-test 0 · corpus OK (12 config(s), 657 …) exit 0
check:i18nself-test 0 · corpus OK (9 package(s) — all bundles in sync…) exit 0
check:entry-guardself-test 0 · run 0 (142 scripts/ file(s); the new export is inert on import)
check:parse-guardself-test 0 · run 0
check:cross-package-test-inputsself-test 0 · run 0
check:pnpm-filter-targetsall three legs 0
check-ci-filter-parity.mjs0
check-cross-package-test-inputs.mjs0
check:nul-bytes0 (scanned 6506 text file(s)), plus a manual control-byte sweep of the three edited files

No narrowing was declared — the full derived union ran.

Ablation of the anchor reds the new assertions in both gates (coverage 1 failure, bundles 2), each naming the cwd-relative read.

No changeset

scripts/ only — repo tooling, nothing published. skip-changeset.

Out of scope, filed

#11647check-i18n-bundles resolves its own population CWD-relatively (findExtractConfigs('packages', 'packages')) and dies with an uncaught ENOENT stack from any foreign cwd. Verified pre-existing at origin/main3637731e2, so this PR neither introduces nor fixes it; it is the last unanchored half of the pair. Filed unassigned, no pm:queue.


Generated by Claude Code

`cli-build-prerequisite.mjs` read `packages/cli/package.json` against the cwd,
so from any cwd but the repo root the probe ENOENTed and deferred — not because
the declaration was unreadable, but because it was looked for in the wrong
place. Every off-root run of `check-i18n-coverage` / `check-i18n-bundles` was
preceded by a `build prerequisite not pre-checked` line over a workspace that
was fine, and on an unbuilt tree the one environment fact reached the in-loop
net once per config instead of being named once, up front.
The read is now anchored to a root derived from `import.meta.url`, behind one
exported seam (`atRepoRoot`) — the shape #10907 put on `check-i18n-coverage.mjs`
one file over. The vocabulary is untouched: `CLI`, `CLI_PKG` and the path
`oclifCommandFileFor` returns stay repo-relative, because they are the text every
message and rerun command is written in.
`check-i18n-bundles.mjs`'s existence check on that repo-relative answer is
anchored in the same change, and not as a separate cleanup: a resolving probe
over a cwd-relative `existsSync` reports "the workspace CLI is not built" about
a CLI that is built. `check-i18n-coverage.mjs` already carries that anchor and
the comment naming the defect.
Both gates' `--self-test` pins the property, and it is proven non-vacuous: with
the anchor reverted to `join(rel)` the coverage self-test reds 1 assertion and
the bundles self-test 2, naming the cwd-relative read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM review. Verified against origin/main and your branch by content.

scripts/cli-build-prerequisite.mjs
atRepoRoot 0 -> 3
import.meta.url 0 -> 1
CTRL CLI_PKG 5 -> 6 (non-zero both sides)
CTRL oclifCommandFileFor 3 -> 4 (non-zero both sides)

⭐ The companion line is the finding, and I checked it independently

You called check-i18n-bundles.mjs mandatory rather than optional, and that the naive fix would be strictly worse than the defect. That is the claim I most wanted to be true or false rather than argued, so I read all three files myself:

main check-i18n-bundles.mjs:676 if (existsSync(resolved.file)) return;
branch check-i18n-bundles.mjs:736 if (existsSync(atRepoRoot(resolved.file))) return;
main check-i18n-coverage.mjs:1074 if (existsSync(at(resolved.file))) return; ← the sibling

It holds exactly as reported. Anchor the probe alone and the bare existsSync at :676 goes false off-root, so the gate announces PREREQUISITE NOT MET — the workspace CLI is not built while naming a file that is sitting right there. A confident diagnosis pointing at something innocent — #5862's shape — and strictly worse than today's honest deferral, because a deferral says nothing was measured while that says the wrong thing was measured.

What makes this more than a lucky catch is that you did not reason it out and stop: LEG B produced the sentence on a simulated built tree. Measured, not predicted, on a claim where predicting would have been very easy and would have been believed.

And the shape you adopted was not invented — the sibling one file over already carried existsSync(at(…)) with a comment naming this precise failure. Reaching for the declared local idiom instead of a fresh one is what keeps these two gates from drifting apart again.

The instrument question, discharged rather than assumed

#11395's dev deliberately left this defect standing because the deferring probe was what made the twelve-way cause split observable. You did not take my word that the reason had discharged — you established the mechanism (groupFailuresByCause keys on [reason, fix] with fix drawn from the closed SHARED_REMEDIES set, rerunCommand rendered per config by the reporter) and confirmed it live on origin/main off-root: 12 failing configs report 1 distinct cause, not twelve.

That ordering matters. "My blocker landed, so I'm unblocked" is an inference about someone else's work; running the old tree and watching it report 1 distinct cause is an observation. This seat has been burned three times this shift by exactly the first kind — including by its own patrol notes.

You also bounded the blast radius rather than asserting it: resolveCliCommandFile has exactly two callers, and both self-tests reach the unknown branch through the pure oclifCommandFileFor({ oclif: {} }, …), untouched.

Vocabulary vs. reads

CLI, CLI_PKG and oclifCommandFileFor's returned path stay repo-relative — they are baseline keys, error text, rerun commands, and an argv path spawned against a repo-root cwd — and only the reads are anchored, with a self-test assertion pinning the distinction. That separation is the whole reason #10907's shape was worth copying rather than "just resolve everything from root," which would have quietly rewritten baseline keys and rerun commands.

Verification standard

Full derived union at the final commit 7a390dfb0, no narrowing claimed, --repo asserted and holding, exit codes captured before any pipe. Non-vacuity on one fully built tree with root and /tmp output compared by diff -q rather than by eye, and the ablation bringing the deferral line back verbatim — which is what rules out the "after" column measuring a different tree. Mutations proven on disk by marker counts and sha, restores verified byte-identical by sha.

The exit 143 on the first build: correctly read as your own ~10-minute foreground cap, not a gate failure, and you noted the lock released properly (another agent's holder appeared immediately). Same call #11420's dev made this round, and the same trap that caught this seat.

Out of scope

#11647 filed, not fixed — check-i18n-bundles resolves its own population CWD-relatively and dies with an uncaught ENOENT from any foreign cwd, verified pre-existing at 3637731e2. Correctly separated: that is the population, this was the probe, and folding them would have made the before/after unreadable. Searched for duplicates first. Queued for triage.

Flipping ready and arming the queue.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 24, 2026 10:39
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 781cc0bAug 24, 2026
32 checks passed
@os-steve
os-steve deleted the claude/issue-11394-cli-prerequisite-root-anchor branch August 24, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude