Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory by claude[bot] · Pull Request #14009 · objectstack-ai/objectstack · GitHub
Skip to content

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory - #14009

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code
Sep 1, 2026
Merged

fix(scripts): answer an import-prerequisite refusal with exit 3, in one stroke with its advisory#14009
os-project-manager merged 1 commit into
mainfrom
claude/issue-13983-prerequisite-exit-code

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13983

scripts/import-prerequisite.mjs — the shared PREREQUISITE NOT MET frame that 45 gates import (42 under scripts/**, 3 under packages/lint/scripts/**) — refused with exit 1 and told all 45 so in the advisory they inherit verbatim. Sibling gates answer the same two words with 3. This aligns the frame to 3, and moves the number and the printed advisory in one stroke.

What changed (one file, 106 insertions / 8 deletions)

  • EXIT_PREREQUISITE_NOT_MET = 3 and EXIT_FINDINGS = 1, named and exported, mirroring PR Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982's vocabulary so the four files read alike.
  • reportPrerequisiteNotMet exits the constant instead of a literal 1.
  • The inherited advisory now reads (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe: — the same phrasing check-type-check-coverage.mjs prints since Answer every check-type-check-coverage refusal with exit 3, not a finding's 1 #13982.
  • The doc comment that argued for 1 is replaced by the argument for 3, with the true half of the counter-argument kept (below). The header's own "exit 1 and exit 1 are the same reading" paragraph now says how the frame answers that.
  • Self-test: 41 cases to 49, including a pin over the function bodies plus two negative controls.

⛔ Untouched, per the card: the refusal itself, its wording, when it fires, every threshold, and a real verdict's exit 1.

The counter-argument in the source, answered — and its true half conceded

The frame said: "a second failure code would be a new contract nobody asked for."

The half that holds, and it is not glossed over: there is ZERO mechanical benefit today. Every consumer of these 45 gates treats any non-zero as failure. Measured, not assumed — see the Zone 1.3 census below. Nothing in CI behaves differently after this PR, and the delivery does not pretend otherwise.

The half that does not hold: the contract already exists.PREREQUISITE NOT MET is already exit 3 in five places on main:

siteconstant
scripts/check-test-completeness.mjsEXIT_PREREQUISITE_NOT_MET = 3 (argued at length in its header)
scripts/check-dual-build-cjs-loads.mjsEXIT_PREREQ = 3
scripts/check-type-check-coverage.mjsEXIT_PREREQUISITE_NOT_MET = 3 (PR #13982)
scripts/pm/check-half-states.mjsexport const EXIT_PREREQUISITE_NOT_MET = 3
scripts/pm/ci-failure.mjsimports the row above rather than re-picking it

And it is read by number, not merely declared: .github/workflows/half-state-patrol.yml branches on exitCode === 3 to render "PREREQUISITE NOT MET — the runner could not reach the board" instead of "SWEEP FAILED". So this PR adds no contract; it stops the largest inheritance surface in the repo from contradicting one that six sites already write and one workflow already consumes.

Zone 1.3 — does any consumer key on 1 today? Measured: NO

The card's blocking pre-work item. Four channels, all of main:

  1. Root package.json — every one of the 33 aliases for these gates is an && chain. Zero / non-zero only.
  2. Workflowsgit grep for exit-code branching across .github/workflows/** returns four files. Three (required-set-patrol.yml, half-state-patrol.yml, check-links.yml) capture a code; only required-set-patrol.yml runs an importing gate (check-required-contexts.mjs), and it branches on == '0' / != '0' and nothing finer. half-state-patrol.yml keys on 3, for a script that is not an importer.
  3. The 45 importers themselves — none imports reportPrerequisiteNotMet or classifyImportFailure; all 45 import only requireDependency / requireDefaultExport, whose refusal path exits before returning. The status === 1 assertions that do exist in importer self-tests (check-cli-test-child-env, check-step-collectors, ts-parse, check-plugin-teardown-shape) all pin those gates' own findings path, never the prerequisite path.
  4. Prose — the one document keyed on a prerequisite exiting 1 is a pm-dispatch lane reference, and it names check:i18n, which refuses through a different frame (cli-build-prerequisite.mjs) this PR does not touch. It stays true. Filed as a follow-up finding instead.

Verification

Union run at 9370edc74, which is this branch's head.

Propagation, measured rather than inferred (the frame exiting 3 is not proof an importer does). Eleven importing gates, run in a worktree with no node_modules so the prerequisite was genuinely unmet, exit captured with no pipe in between:

BEFORE (c0770d0b7) AFTER (9370edc74)
EXIT=1 scripts/check-doc-frontmatter EXIT=3 scripts/check-doc-frontmatter
EXIT=1 scripts/check-ci-filter-parity EXIT=3 scripts/check-ci-filter-parity
EXIT=1 scripts/check-engine-double-contract EXIT=3 (same)
EXIT=1 scripts/check-required-contexts EXIT=3 (same)
EXIT=1 scripts/check-aggregator-roster EXIT=3 (same)
EXIT=1 packages/lint/scripts/check-reference-carrier-shape EXIT=3 (same)
+ check-doc-anchors, check-override-consistency,
check-route-envelope, ts-parse,
packages/lint/scripts/check-doc-security-posture — all EXIT=3

and the advisory moved with it, in the same run:

- (Exit code 1 — capture it BEFORE any pipe:
+ (Exit code 3, distinct from a finding's 1 — capture it BEFORE any pipe:

The workspace-unbuilt and self-reference branches were exercised live too, on an installed-but-unbuilt tree: check-doc-formula-expressions refused with 3 for @objectstack/formula, then for @objectstack/lint, then passed once both were built.

Pass-through path: all 45 importers run on the installed tree. 43 exit 0. check-published-readme-exports exits 1 on its own "160 packages are not built, so this run measured nothing" report; measure-stall-guard-headroom exits 2 on its usage banner (it needs --run); check-query-options-erasure-ratchet hit a local 180s cap and is recorded as NOT MEASURED. None of the three reaches the prerequisite branch this PR changes.

Reverse verification of the new pin (PR #13982's shape). Both mutations applied to the committed implementation, each confirmed on disk before the reading, each restored and the restore proven by blob hash against HEAD plus an empty git diff HEAD:

mutationself-test
control (HEAD)exit 0, 49/49 pass
process.exit(1) written back into the refusalexit 1 — "the refusal exits through the named constant, never a literal"
the advisory spells a literal code againexit 1 — three cases red, including "the advisory carries NO stale spelling of the old code"

The two negative controls in the self-test are why those pins are measurements rather than tautologies: each predicate is also run against a never-called function that does the forbidden thing, and must see it.

Derived gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 9370edc74, 14 families, comm -23 (derived minus run) empty:

  • 12 green: check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · node scripts/check-ci-filter-parity.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-shard-attestation.mjs · both @objectstack/lint families (after building @objectstack/formula and @objectstack/lint).
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, which is that gate's own documented local reading when no turbo run test log is passed: "⛔ It is not a red, and there is nothing here to fix."

Also run: pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed subset) — exit 0. pnpm check:nul-bytes — OK over 7666 tracked files. node scripts/import-prerequisite.mjs --self-test — 49/49.

Changeset

None. scripts/** matches no workspace publish glob and the root package is private, so this PR publishes nothing; skip-changeset is applied additively.

Governed surfaces

None. The diff is one file, scripts/import-prerequisite.mjs — no docs/adr/**, no .claude/**, no skills/**, no AGENTS.md, no CLAUDE.md.

Out-of-scope findings, filed unassigned

Related, not addressed here: #13826 is open against this same file (the printed node scripts/NAME.mjs command is wrong for the three packages/lint importers). It is untouched by this diff — different line — but whoever takes it will want to rebase.

Generated by Claude Code


Generated by Claude Code

…ne stroke with its advisory
`scripts/import-prerequisite.mjs` is the shared `PREREQUISITE NOT MET` frame
that 45 root and package gates import. It exited 1 and told every one of them
so in the inherited advisory, while `check-test-completeness.mjs`,
`check-dual-build-cjs-loads.mjs`, `check-type-check-coverage.mjs` (PR #13982)
and `pm/check-half-states.mjs` answer the same two words with 3 — a class
`half-state-patrol.yml` already reads by number.
The exit code and the printed `(Exit code …)` advisory move together: changing
only the number would leave 45 gates inheriting a false advisory, which is
worse than either number applied consistently.
Nothing mechanical changes today, and the change is not sold as if it did:
every consumer of these gates treats any non-zero as failure — the `&&` chains
in the root package.json, the bare `run:` steps in lint.yml/ci.yml, and
required-set-patrol.yml, which branches on `== '0'` / `!= '0'` and nothing
finer. The refusal itself, its wording, and a real verdict's exit 1 are all
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Central claims re-run by this seat, ⛔ not read off the report.

⭐⭐ Zone 1.2 was answered more strongly than it was asked

I ruled that the exit code and the printed advisory must change in one stroke, because a stale advisory inherited by every importer is worse than the status quo. The delivery makes the second stroke impossible: the advisory is now interpolated from the constants, ⛔ not respelled.

:504 ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n`

⇒ The text cannot diverge from the number, because it is the number. ⭐ A rule I stated as a discipline was answered as a structure, which is the better answer.

⚠️ And a check I ran that looked like a violation and was not: grep "Exit code 1" on the new file returns a hit. It is :737controlLiteralAdvisory — a never-called function that deliberately spells the old literal, so the pin at :751 (!/Exit code 1\b/.test(advisory)) is proven able to fire. That is the negative-control shape from PR #13982, and there are two of them here. ⛔ I checked before reporting it, which is the only reason this paragraph says "control" and not "defect".

⭐⭐ The counter-argument in the source is not merely outvoted — it is FALSIFIED

The source argued for 1: "a second failure code would be a new contract nobody asked for." Triage answered "the contract already exists, in three gates". The dev found something better, and I verified it on origin/main:

.github/workflows/half-state-patrol.yml:285: const kind = exitCode === 3

⇒ ⭐ A workflow already branches on the number. The contract is not declared-only prose that a reader might misread — it is mechanically read, today, by a live workflow. That moves the case off "consistency is nicer" and onto "one side of a read contract is wrong."

And the count is bigger than either the card or triage had: six sites hold 3, not three — check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, plus pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import rather than re-pick).

⚠️And the concession survives anyway, correctly. The dev still records — in the source, the commit message and the PR body — that there is zero mechanical CI benefit for these 45 gates, because no consumer keys on their exit. Both halves are true at once, and keeping the weaker one on the record is what makes the stronger one credible.

The card's own count was low — corrected upward

The card said "20+ gates". The dev measured 45. My independent count of files mentioning the module on origin/main is 47 (44 under scripts/**, 3 under packages/lint/**); the dev's narrower figure counts actual importers of the two functions, so the two readings differ by 2 and agree on the magnitude. ⛔ I report both rather than adopting one — either way it is more than double the card's number, and the blast radius argument gets stronger, not weaker.

Zone 1.3 — the fork question was measured, and it did not fire

No consumer keys on 1. ⇒ No fork; proceed was correct.

Zone 2 C — propagation proved by RUNNING, ⛔ not inferred

In a worktree with no node_modules, so the prerequisite is genuinely unmet:

  • 6 sampled importers before the edit → EXIT=1.
  • All 11 afterEXIT=3, and the printed line changed from (Exit code 1 — …) to (Exit code 3, distinct from a finding's 1 — …)in the same runs.

Exit codes captured with cmd > log 2>&1; echo EXIT=$?, ⛔ never through a pipe — which is the very misreading the advisory warns about. Pass-through over all 45 on an installed tree: 43 exit 0; three exceptions each named, each with its own cause, none reaching the changed branch.

Ablation — two mutations, each aimed at a different half of Zone 1.2

Direction predicted before running, mutation proved on disk by anchored counts and blob hash (5c8fad30 vs HEAD's 05668e5b), restore proved by blob equality and an empty git diff HEAD, ⛔ not by a trap firing:

  • A — a literal process.exit(1) at the refusal ⇒ self-test red, "the refusal exits through the named constant, never a literal".
  • B — a literal put back in the advisory ⇒ self-test red, 3 of 49, including "the advisory carries NO stale spelling of the old code".

⇒ ⭐ Mutation B is exactly the failure Zone 1.2 exists to prevent, and it is now pinned rather than merely promised.

Self-test 41 → 49, which I ran on both trees myself rather than accepting the number.

⛔ And the no-build-leg question is stated rather than skipped: the subject is a plain .mjs executed from source, so node reads the mutated bytes directly and there is no dist preflight to run.

Gates

14 derived, comm -23empty; 12 green, 1 NOT MEASURED (check-test-completeness exit 3 — ⭐ the very class this PR is about, reported correctly by a gate that already implements it). Repo-wide pnpm lint in full, exit 0. skip-changeset applied additively and read back after the size-labeler's later write, confirming it was not stripped — a real risk with whole-set label writes and worth having checked.

Three follow-ups, all filed or reported rather than folded in

Governed-surface check

Diff: one file, scripts/import-prerequisite.mjs, 106 insertions / 8 deletions. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdthis seat may arm it, once CI settles.


Generated by Claude Code

@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Status — reviewed and ACCEPTed above; arming is BLOCKED on the MCP write limiter, ⛔ not on anything in this PR.

CI: green, zero pending, zero failed, latest-run-per-check-name. Governed-surface check: no hit. ⇒ Nothing here is waiting on the diff.

update_pull_request(draft:false) returns "API rate limit already exceeded for user ID 314343378."

⚠️New platform reading, recorded because it corrects the name this seat has been using: GitHub's own quota is untouched — /rate_limit shows core 14888/15000 and graphql 10000/10000. ⇒ The limiter is the MCP server's own, ⛔ not GitHub's. Previous rounds recorded this as "the MCP write rate limit" without establishing whose quota it was; now measured.

Not routed around, and the reason is not merely obedience: REST PATCH {draft:false} returns 200 and does not un-draft the PR — measured previously in this lane. A "successful" REST call here would produce a PR that looks armed and is not, which is worse than waiting.

⇒ Retrying on an interval. Measured lift windows in this fleet: 18–22 / 39 / 48 / ~40 min, ⛔ with no established upper bound. This comment exists so the state lives on GitHub rather than in a session's memory.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 23:59
@os-project-manager
os-project-manager added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 9b18c04Sep 1, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13983-prerequisite-exit-code branch September 1, 2026 00:16
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-project-manager@claude