Skip to content

fix(pm): check-governed-merges binds the rearm plan its INCOMPLETE banner reads, and pins the attribution-failure path (#18055) - #18093

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-18055-governed-merges-rearm-binding
Sep 14, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-18055-governed-merges-rearm-binding

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #18055

scripts/pm/check-governed-merges.mjs main() read rearm.hint out of a scope that never bound
rearm, and it did so inside the argument of the console.error that prints the sweep's
sweep INCOMPLETE banner. An argument that throws means the call never happens: the
ReferenceError deleted the one warning saying the printed list must not read as clean, and the
return EXIT_INCOMPLETE behind it.

What changed (one file, scripts/pm/check-governed-merges.mjs)

  1. The binding, in the scope that reads it. main() now builds the re-arm plan at the point
    the banner is built, from the same three inputs rearmProxyOrNull builds its own from
    (process.env, process.execArgv, process.allowedNodeEnvironmentFlags.has(PROXY_FLAG)), and
    the banner reads .hint / .reason from that binding. One binding; nothing widened.
  2. The banner owns a function, reportSweepIncomplete(state, emit = console.error) — the same
    words, the same order, the same stream, the same EXIT_INCOMPLETE. It is extracted for exactly
    one reason: main() reads git and the network and cannot be driven down the
    attribution-failure path offline, and a banner that dies while being built is pinnable only by
    something that builds it.
  3. A battery of its own, 11 cases, declared in SELF_TEST_BATTERIES with its floor, so a case
    that stops registering names itself instead of going quiet.

Premise readings — measured before writing, worktree at base d438b3a9a

premise reading when (UTC)
P1 — line 3087 reads attributionFailed && rearm.hint in main() and no rearm exists in that scope HOLDS. git grep -n rearm over the file returns hits only at :2273-:2281 (the proxyRearmPlan return objects), :2821-:2838 (rearmProxyOrNull's own const rearm), :3087-:3088 (the banner) and :3951-:3958 (self-test, all calling proxyRearmPlan directly). Nothing binds rearm in main(). 2026-09-14T01:13Z
P2 — the expression sits inside the console.error(...) argument, so a throw loses the banner AND the return EXIT_INCOMPLETE after it HOLDS. Read at :3084-:3093: the ternary is the last operand of the string concatenation passed as the sole argument, and return EXIT_INCOMPLETE; is the next statement. 2026-09-14T01:13Z
P3 — no existing self-test case drives main() down the attributionFailed path HOLDS, with one refinement worth recording. Probe on a scratch copy (never the worktree file; copy removed by an EXIT trap, verified absent): replacing only rearm.hint with an unconditionally throwing IIFE — so the throw fires exactly when attributionFailed is true — left the battery green: 317 assertions, exit 0. Refinement: replacing the whole condition attributionFailed && rearm.hint with a throwing IIFE reds one pre-existing case, a-mixed-sweep-attributes-the-dropped-repo-in-the-INCOMPLETE-footer-itself, which drives main() down the INCOMPLETE path for an unaudited repo, never for attribution. So the banner was reached by the battery; the attributionFailed branch of it was not. 2026-09-14T01:14Z (whole condition) and 01:17Z (hint only)

Baseline for both probes: node scripts/pm/check-governed-merges.mjs --self-test on the unmodified
tree, ✓ ... 317 assertions, exit 0, empty stderr (2026-09-14T01:17Z).

The battery, and the falsifiability pair

node scripts/pm/check-governed-merges.mjs --self-test
✓ check-governed-merges --self-test: 328 assertions, exit 0, empty stderr (2026-09-14T01:21Z).
317 before, 328 after: the 11 new cases, battery only grows, nothing deleted or weakened.

Ablation (one-shot, on the committed fix, restore proven by state): delete the const rearm = proxyRearmPlan({...}) binding main() now carries. On-disk proof, not an editor exit code —
git hash-object before eab2fbd5ec0010fe84584f8acd61d2022dc7e2cf (equal to the HEAD blob),
after c455a42e43f704f1bd504a34cccdc1f235eca9ff; the file still parses (node --check clean), as
it did before the fix, because the defect is a runtime ReferenceError.

Ablated run: exit 1, 4 failures, by name

✗ check-governed-merges --self-test — 4 failure(s)
  • a-mixed-sweep-attributes-the-dropped-repo-in-the-INCOMPLETE-footer-itself
  • a-sweep-whose-every-attribution-channel-fails-exits-EXIT_INCOMPLETE-not-a-crash
  • ⭐ and-the-INCOMPLETE-banner-REACHES-STDERR-the-one-thing-the-crash-deleted
  • and-nothing-on-either-stream-is-a-ReferenceError

with the card's own stack reproduced inside the failure detail:
ReferenceError: rearm is not defined at main (.../check-governed-merges.mjs:3128:73).

⚠️ Direction stated as measured, not as predicted: the ablation is broader than the original
defect. Removing the binding makes the reference throw on every INCOMPLETE path (the plan is
now passed as a call argument), where the shipped defect threw only when attributionFailed was
true — which is why the pre-existing unaudited-path case reds here too and did not red on main.
The three new end-to-end cases are the ones that answer for this card. The six offline cases stay
green under the ablation by construction: they call the banner function directly and do not go
through main(), which is exactly why the end-to-end case exists.

Restored (git checkout HEAD -- scripts/pm/check-governed-merges.mjs): worktree blob back to
eab2fbd5ec0010fe84584f8acd61d2022dc7e2cf, git status --porcelain empty, git diff HEAD empty,
and the battery green again at 328 assertions, exit 0 (2026-09-14T01:23Z). No ablation artefact is
left in the tree.

The live invocation the card measured — a reading, not the pin

node scripts/pm/check-governed-merges.mjs --since 2026-09-13T13:00:00Z, stdout and stderr
captured to separate files by redirect, exit captured on the command itself (2026-09-14T01:23Z):

string stdout stderr
attribution unavailable 0 0
merged_by names an ACCOUNT 1 0
PR # 3 0
sweep INCOMPLETE 0 1
ReferenceError 0 0

LIVE_EXIT=2EXIT_INCOMPLETE.

The attributionFailed path is NOT taken in this container, so this run does not
discriminate the fix from the defect: the re-exec worked (ℹ️ re-exec with --use-env-proxy ...),
both lookups resolved through the env token, and the card's positive control
attribution unavailable reads 0/0 here where the card measured 1/0. The INCOMPLETE this
run reports is the other kind — 4 governed repos unaudited (one stale mirror, three with no
checkout in this container). What the run does show is that the banner now reaches stderr and the
exit is 2 on a path that reaches the same statement. The pin is the self-test case, which
drives the attribution-failure path deliberately: a fixture governed merge in the window and
GITHUB_API_URL pointed at a port nothing listens on, so every channel fails at the transport,
offline, in milliseconds.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the
worktree derived 34 commands; every one was run, each exit captured by redirect-then-capture,
each recorded as command :: exit N, all exit 0. Reconciliation:

Run reconciliation — 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED
  (a DERIVED zero — all 34 recorded an exit code and none of them is 3).

Included in that 34 and green: pnpm check:pm-governed-merges (this script's own self-test),
pnpm check:pm-dispatch-gates, pnpm check:nul-bytes, pnpm check:entry-guard,
pnpm check:parse-guard, node scripts/check-self-test-wired.mjs,
node scripts/check-scripts-symbol-anchors.mjs, node scripts/check-declaration-mirrors.mjs.
The tool's own accounting names what stays outside that total (50 artifact-roster families, 11
declared-wide families, 14 pending-changeset families, 1 path-scheduled CI job and the always-runs
tail); those are CI's run, not a local claim.

Beyond the derived set, the repo-wide grep -naP control-character scan over the edited file
returned nothing and pnpm check:nul-bytes passed over 8634 tracked text files.

Changeset

No changeset; this PR takes skip-changeset. What the check reads: the changeset gate is
pr-automation.yml's changeset-check job, and it has no path exemption — its only two
exemptions are the skip-changeset label (read live from the PR, not from the frozen event
payload) and the changesets release branch. The diff is one file under scripts/pm/, repo tooling
that sits outside every published package directory and therefore outside every package's
files[]; nothing published moves. Same declaration shape as PR #18089.

Acceptance notes

  • Extraction scope. reportSweepIncomplete is module-local, not exported: the self-test lives
    in the same module, and the exported surface (which scripts/pm/ci-failure.mjs imports) is
    unchanged.
  • The banner text is untouched, deliberately, including its #4690 citation and the
    NODE_OPTIONS prescription. The only textual additions are the new battery name in
    SELF_TEST_BATTERIES, its floor (11), and one clause appended to the self-test's verdict
    enumeration so that line stays an honest inventory.
  • rearm.hint is still read strictly (no ?.): a defensive optional-chain would hide the next
    missing binding instead of failing, and the new end-to-end case is what catches it by name.
  • Noted, not filed: the live sweep reports this worktree's own checkout as [stale-mirror] because
    origin/main moved while this card was in flight. That is the audit working as designed, not a
    defect. Carrier: none — it is a property of the container, not of the tree.

Clause-②: no — no contract accept/reject behaviour changes and no public surface widens; the
edit is repo tooling.


Generated by Claude Code

… attribution-failure path

`main()` read `rearm.hint` out of a scope that never bound `rearm`, inside the
argument of the `console.error` that prints the sweep's `sweep INCOMPLETE`
banner. An argument that throws means the call never happens, so the
ReferenceError deleted the one warning saying the printed list must not read as
clean — and the `return EXIT_INCOMPLETE` behind it.

The plan is now bound in the scope that reads it, out of the same inputs
`rearmProxyOrNull` builds its own from. The banner moves into a function of its
own, byte-identical in what it says and what it returns, so `--self-test` can
drive the attribution-failure path offline: `main()` reads git and the network
and cannot be.

Pinned by 11 new cases in a battery of their own: the banner is built and
emitted on the attribution-failure path, it returns EXIT_INCOMPLETE, the proxy
hint renders from a hinting plan and stays empty otherwise (including on an
incompleteness that is not about attribution), and — end to end — a real sweep
over a governed fixture merge whose every attribution channel fails prints the
banner on stderr and exits 2.

Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 14, 2026
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • Served-tier: 1718/1718 CONTRACT_REVIEW_TIER — harness model stamp counted over this seat's own transcript (non-sidechain assistant messages a model served; <synthetic> harness notices excluded) at 2026-09-14T01:46Z and compared to the constant's value outside the repository; get_session external_metadata.last_served_model read equal to the constant at 2026-09-14T00:20Z.

Contract review

Head: b321768d (PR #18093, card #18055) — reviewed at 2026-09-14T01:46Z by the skills seat at the contract-review tier. Non-governed path (scripts/pm/check-governed-merges.mjs only) ⇒ the in-seat review lands it: ready + auto-merge through the queue.

① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):

  1. The binding is in the scope that reads it: main() now calls proxyRearmPlan({ env: process.env, execArgv: process.execArgv, flagSupported: process.allowedNodeEnvironmentFlags.has(PROXY_FLAG) }) — the same three inputs rearmProxyOrNull builds its own plan from — and passes the result to the banner; the unbound rearm.hint read at the old line 3087 is gone (git grep rearm.hint on the head: only inside the new function, where rearm is a parameter).
  2. The banner moved, not changed: reportSweepIncomplete({ unaudited, edged, attributionFailed, rearm }, emit = console.error) carries the same strings in the same order on the same stream and returns EXIT_INCOMPLETE — the diff removes the inline block and re-adds it verbatim inside the function (seat-read, hunk by hunk); the extraction exists so the attribution-failure path can be driven offline.
  3. Pinned, and the pin fails without the fix: 12 assert( lines added, 0 removed (static count 300 → 312; the dev's runtime count 317 → 328), including the end-to-end case where every attribution channel fails and the banner REACHES STDERR with EXIT_INCOMPLETE and no ReferenceError on either stream; the dev's ablation (delete the binding) reds 4 cases by name carrying the card's own ReferenceError: rearm is not defined, restore proven by blob hash + empty git diff HEAD. The seat could not run the battery in its own worktree — the self-test needs an installed tree (PREREQUISITE NOT MET, exit 3, the same reading on main's copy as the control) — so the battery reading is the dev's plus CI's Lint & Repo Gates on the head.
  4. Live reading, honestly bounded: in this container the attribution-failure path is NOT taken (attribution unavailable 0/0 where the card read 1/0), so the live run's sweep INCOMPLETE on stderr with exit 2 is the unaudited-repos kind — the self-test case is the pin, the live run a reading; stated in the PR body, not glossed.
  5. Gates: --ran 34 derived / 34 run / 0 NOT-MEASURED / 0 UNRUN with --repo asserted; check:nul-bytes OK. Checks on the head at 2026-09-14T01:46Z: see the seat's reading below; 0 red at read time.
  6. Scope held: one file; skip-changeset is the declaration; Clause-②: no holds — no contract path. --pair 18093 on origin/main's reader (post-fix(pm): the Served-tier line names the tier constant, never a model identifier — AGENTS.md's comment rule (#18060) #18087) → exit 0 at 2026-09-14T01:46Z before this record.

② semver: unchanged — PM tooling, nothing published.

③ boundary flags: none. The dev's refinement of premise P3 (one pre-existing case already reaches the banner via the unaudited-repo path; only the attribution branch was unpinned) is recorded in the PR body.

Implemented-by: claude/issue-18055-governed-merges-rearm-binding
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

Verdict: PASS — ready + auto-merge by this seat.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants