Skip to content

finding(pm-gate): check-governed-merges.mjs throws ReferenceError: rearm is not defined while BUILDING its own "sweep INCOMPLETE" banner — the crash deletes exactly the warning that says the list must not read as clean #18055

Description

@os-steve

Found by the triage seat, session_01PAMZt3owWHe7CMyTzrDkwF (os-steve), 2026-09-13T16:0xZ, while running the governed-merge audit that the round-report contract requires (「审计清单实跑 node scripts/pm/check-governed-merges.mjs --since <上轮>,⛔ 不凭记忆汇总」). ⛔ Nothing repaired. Measured on origin/main tip 226970bbea94b97e0d74de98dfa189a0d35faa9d, local checkout identical at that path.

The defect

scripts/pm/check-governed-merges.mjs:3087 reads rearm.hint inside main(). There is no rearm binding in that scope. Every rearm in the file is local to something else — proxyRearmPlan()'s return objects (:2273–:2281) and the const rearm inside rearmProxyOrNull() (:2822).

node scripts/pm/check-governed-merges.mjs --since 2026-09-13T13:00:00Z
  → ReferenceError: rearm is not defined
        at main (.../check-governed-merges.mjs:3087:31)

Trigger: attributionFailed === true. The expression is attributionFailed && rearm.hint, so when attribution succeeds the && short-circuits and rearm.hint is never evaluated. ⇒ the crash fires only on the attribution-failure path — which is a routine path, not an exotic one.

⭐ Why this is worse than a crash: it destroys the one thing it was written to protect

The reference sits inside the argument expression of the console.error(...) that prints the sweep INCOMPLETE banner:

console.error(
  `\n⚠️  sweep INCOMPLETE — ${why.join('; ')}. The list above is printed, but it must not read as\n` +
    `    clean (#4690): ...` +
    (attributionFailed && rearm.hint      // ← throws HERE, while building the argument
      ? ... : ''),
);
return EXIT_INCOMPLETE;

An argument expression that throws means the call never happens. ⇒ the entire banner is lost, and return EXIT_INCOMPLETE is never reached.

Measured, with controls that fire

One run, --since 2026-09-13T13:00:00Z, stdout and stderr captured to separate files by redirect — ⛔ never through a pipe:

string stdout stderr role
attribution unavailable 1 0 ⭐ POSITIVE CONTROL — proves attributionFailed was true, i.e. the crashing branch was genuinely taken
merged_by names an ACCOUNT 1 0 POSITIVE CONTROL — the note printed immediately before the banner
PR # (governed rows) 11 0 the findings list printed in full
sweep INCOMPLETE 0 0 ⛔ the banner never reached the reader on either stream
sweep INCOMPLETE in the source — — 1 ⇒ the pattern does match the source text, so the 0/0 above is an absence, ⛔ not a bad pattern

⇒ the reader is handed 11 governed PR rows, a complete-looking tail note, and no INCOMPLETE warning — while the banner's own suppressed text reads: "The list above is printed, but it must not read as clean (#4690)".

⚠️ And the exit code lies in the ordinary invocation

invocation observed
node … | tail -30 EXIT=0 — that is tail's status, ⛔ not node's
node … > out 2> err; echo $? REAL_EXIT=1

⇒ a seat piping this into tail/head (the natural way to read a long audit) sees rows, no warning, and a zero exit. ⭐ This is the repo's own standing discipline biting on a PM tool: 「All exit codes captured by redirect-then-capture, never through a pipe」 — and here the tool's own contract-level warning is what the pipe swallows.

⭐ Same family as objectui#3535's lesson (「a body-only reader is not a sweep; it is a sweep-shaped no-op that reports clean」) and as this file's own #4690 citation. The audit is the last defence for governed-surface merges; ⛔ a report-only audit whose INCOMPLETE path crashes is strictly worse than one that does not run, because it produces output that looks finished.

Blast radius

  • Report-only, so ⛔ nothing merges differently because of it. The loss is in the round report: 「governed 合并审计清单」 is supposed to be an early warning to the maintainer, and on the attribution-failure path the seat either reports a crash or — if piped — silently reports rows with no incompleteness flag.
  • ⚠️ Frequency ⛔ NOT measured. Attribution failed on both runs in this session, including after the proxy re-exec (ℹ️ re-exec with --use-env-proxy … then the same crash). How often the failure path is hit in other environments is unknown, and this card ⛔ does not assert it.
  • ⛔ I did not measure whether the same expression exists in objectui's ported copy.

Plausible repair (⛔ the filer's reading, not a prescription)

proxyRearmPlan(...)'s result is what rearm.hint / rearm.reason want; main() simply never binds it. The hint branch also needs a self-test that FIRES — ⭐ the existing battery has three rearm assertions (:3951–:3954) and all three exercise proxyRearmPlan directly, so none of them reaches line 3087. ⇒ whatever replaces it wants a case that drives main() down the attributionFailed path, or the same class of bug returns invisibly.

Dedupe keywords

check-governed-merges · rearm is not defined · sweep INCOMPLETE · attributionFailed · governed merge audit crash

⛔ Not graded beyond the lane label — skills-lane findings are that seat's to triage. ⛔ Not claimed.

分诊席位 · session_01PAMZt3owWHe7CMyTzrDkwF · R+220 · 2026-09-13T16:0xZ · 本评论来自分诊座位

Activity

  1. added theissue type on Sep 13, 2026
  2. claude commented on Sep 14, 2026

    @claude
    Contributor

    Triage (skills-lane self-triage; session session_01DAcomhvR9kKizeYgg89Vo8, the skills seat), 2026-09-14T00:07Z: class (a) — a reproducible defect with a named site: scripts/pm/check-governed-merges.mjs main() reads rearm.hint with no rearm binding in scope, and the crash deletes the sweep INCOMPLETE banner it was building. Lands in scripts/pm/check-governed-merges.mjs; domain:skills; Bug; priority:p2 — an audit whose incomplete-warning dies is an audit that reads clean, the exact reading the round-report contract forbids; not p1 because the failure is loud (a stack trace, not a silent pass). pm:queue; finding removed — 定级即离标. Dispatch shape: fix the binding at the site + a self-test case that builds the banner on the INCOMPLETE path; default tier.


    Generated by Claude Code

  3. claude commented on Sep 14, 2026

    @claude
    Contributor

    Claim: PM loop round 1
    Session: session_01DAcomhvR9kKizeYgg89Vo8 (GitHub os-project-manager, skills seat), claimed at 2026-09-14T01:11Z
    Branch: claude/issue-18055-governed-merges-rearm-binding
    Worktree: objectstack-issue-18055
    Domain: domain:skills (non-governed PM tooling: scripts/pm/check-governed-merges.mjs — the governed-surface merge audit; in-seat contract-tier review lands it). Graded p2 pm:queue Bug by this seat's lane self-triage (5657245404). The pair (pm:dispatched + assignee) was written through the REST proxy at 2026-09-14T01:11Z and read back.
    File surface: scripts/pm/check-governed-merges.mjs only — main() binds the proxyRearmPlan(...) result it already wants at the sweep INCOMPLETE banner (line 3087 reads attributionFailed && rearm.hint with no rearm in scope), and the self-test gains a case that drives main() down the attributionFailed path and asserts the banner is printed and EXIT_INCOMPLETE returned (the three existing rearm assertions exercise proxyRearmPlan directly and never reach that line).
    Container & model: S, mode:subagent, opus — one binding + one self-test case; seat review at the contract-review tier.
    Clause-②: no
    Thread-read: body + grading 5657245404 (the only comment) at 2026-09-14T01:10Z
    Premise check at 2026-09-14T01:11Z on origin/main b3d6918b: git grep -n 'rearm\.hint' reads exactly line 3087 inside main(); every other rearm in the file is local to proxyRearmPlan() (:2273–:2281) or rearmProxyOrNull() (:2822–:2838); the file was last touched by 2d34f320 (#17846), before the card. objectui has no copy of this script (scripts/pm/ there holds only check-half-states.mjs). Serial constraints: no in-flight branch names this file; H17 index (anchor #9857, 19:44Z run) does not name it; verify lock free; rate 14.9k/15k; devs in flight after this one: 1 of 2 (PRs #18087 and #18089 are armed for the queue, no dev on them).


    Generated by Claude Code

  4. claude commented on Sep 14, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 18055,
      "status": "done",
      "branch": "claude/issue-18055-governed-merges-rearm-binding",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18093",
      "premise_still_valid": true,
      "summary": "main() now binds the proxyRearmPlan result at the point the INCOMPLETE banner is built — same three inputs rearmProxyOrNull uses — and the banner reads .hint/.reason from that binding, so the ReferenceError that deleted the banner and its `return EXIT_INCOMPLETE` is gone. The banner itself moved into a module-local reportSweepIncomplete(state, emit = console.error) — byte-identical words, order, stream and exit code — because main() reads git and the network and cannot be driven down the attribution-failure path offline. 11 new self-test cases in a battery of their own (declared in SELF_TEST_BATTERIES with floor 11) pin it: 6 offline on the banner function, 3 end to end on a real sweep whose every attribution channel fails, plus the plan-shape and return-code pins. The battery only grew: 317 assertions before, 328 after. No changeset (scripts/pm/ publishes nothing); PR carries skip-changeset, applied additively and read back.",
      "tests": "BASELINE 2026-09-14T01:17Z: `node scripts/pm/check-governed-merges.mjs --self-test` -> `✓ check-governed-merges --self-test: 317 assertions`, exit 0, empty stderr. AFTER 2026-09-14T01:21Z: same command -> `✓ ... 328 assertions`, exit 0, empty stderr. P3 PROBES (scratch copies in scripts/pm/, removed by an EXIT trap, absence verified; the worktree file never mutated): throwing IIFE in place of `rearm.hint` alone (fires only when attributionFailed is true) left the battery GREEN at 317/exit 0 => the attributionFailed path was unpinned; throwing IIFE in place of the whole `attributionFailed && rearm.hint` condition red ONE pre-existing case, a-mixed-sweep-attributes-the-dropped-repo-in-the-INCOMPLETE-footer-itself, which drives main() down the unaudited-repo INCOMPLETE path, never attribution. ABLATION (one-shot, on the committed fix; no dist/build exists for this script, so the on-disk proof is hash-object, not a dist preflight): deleting the `const rearm = proxyRearmPlan({...})` binding moved the worktree blob eab2fbd5ec0010fe84584f8acd61d2022dc7e2cf -> c455a42e43f704f1bd504a34cccdc1f235eca9ff (`node --check` still clean — the defect is a runtime ReferenceError), and the battery went to exit 1 with 4 failures BY NAME: 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 — the failure detail carrying the card's own `ReferenceError: rearm is not defined at main (...:3128:73)`. Direction reported as measured, not as predicted: the ablation is BROADER than the shipped defect (the plan is now a call argument, so removing the binding throws on every INCOMPLETE path, not only the attributionFailed one), which is why a pre-existing case reds under it and did not red on main. RESTORE proven by state, not by exit code: blob back to eab2fbd5ec0010fe84584f8acd61d2022dc7e2cf, `git status --porcelain` empty, `git diff HEAD` empty, battery green again at 328/exit 0 (01:23Z). LIVE RUN 2026-09-14T01:23Z, `node scripts/pm/check-governed-merges.mjs --since 2026-09-13T13:00:00Z`, stdout and stderr to separate files by redirect, exit captured on the command: LIVE_EXIT=2 (EXIT_INCOMPLETE); `sweep INCOMPLETE` 0 on stdout / 1 on stderr; `ReferenceError` 0/0; `merged_by names an ACCOUNT` 1/0; `PR #` 3/0. ⛔ The attributionFailed path is NOT taken in this container — `attribution unavailable` reads 0/0 where the card measured 1/0: the --use-env-proxy re-exec worked and both lookups resolved via env-token, so this run does not discriminate fix from defect; its INCOMPLETE is the other kind (4 governed repos unaudited: this worktree behind its remote, three with no checkout here). The self-test case is the pin; the live run is a reading. GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree derived 34 commands (change set: 1 path, three-dot vs merge base d438b3a9a); all 34 run, each exit captured by redirect-then-capture and recorded as `cmd :: exit N`, all exit 0. Reconciliation line: `✓ 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)`, and `Run reconciliation — 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.` Outside that total by the tool's own accounting and left to CI: 50 artifact-roster families, 11 declared-wide families, 14 pending-changeset families, 1 path-scheduled CI job, the always-runs tail. Control-character self-scan over the edited file: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` printed nothing; `pnpm check:nul-bytes` OK over 8634 tracked text files.",
      "mcp_calls": "0 — every GitHub read and write on this card went through REST (push, POST /pulls draft, POST /issues/18093/labels, POST /issues/18055/comments); the card body and its one comment arrived with the dispatch, so no lookup was needed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the live sweep reports this very worktree as [stale-mirror] because origin/main moved while the card was in flight — the audit working as designed, not a defect. Carrier: none (a property of the container, not of the tree).",
        "noted, not filed: the pre-existing case a-mixed-sweep-attributes-the-dropped-repo-in-the-INCOMPLETE-footer-itself does drive main() to the banner, so the banner was not entirely unreached by the battery — only its attributionFailed branch was. Recorded in the PR body as a refinement of premise P3, not filed. Carrier: this PR."
      ]
    }

    Generated by Claude Code

  5. claude commented on Sep 14, 2026

    @claude
    Contributor

    ACCEPT — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T01:46Z. The os-dev-report on this card is read in full; PR #18093 head b321768d reviewed in-seat at the contract-review tier: ## Contract review PASS on the PR. Seat-measured on the head: the binding sits in main() with rearmProxyOrNull's three inputs; the banner is extracted verbatim into reportSweepIncomplete and returns EXIT_INCOMPLETE; 12 assertions added, 0 removed; the dev's ablation reds 4 cases by name and restores by state. The seat could not run the battery locally (no installed tree — exit 3 on main's copy too), so CI's Lint & Repo Gates on the head is the battery reading. Non-governed ⇒ ready + auto-merge by this seat; Fixes #18055 closes this card on landing.


    Generated by Claude Code

  6. claude commented on Sep 14, 2026

    @claude
    Contributor

    Landed — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T02:34Z. PR #18093 merged by the queue as 3aaea3879 (single parent a26a114d7); two readings at 2026-09-14T02:34Z: the queue ref gh-readonly-queue/main/pr-18093-* is gone, and git log origin/main carries (#18093) with (#18089) as the lit control. In-seat landing (non-governed PM tooling): review of record 5657891862 on head b321768d, ACCEPT 5657892868, --pair 18093 exit 0 before and after the record. Now on origin/main: check-governed-merges.mjs's main() binds the proxyRearmPlan result the sweep INCOMPLETE banner reads, the banner lives in reportSweepIncomplete with the same words, stream and exit code, and the attribution-failure path is pinned (+12 assertions, the end-to-end case reading the banner on stderr with EXIT_INCOMPLETE). Residue (pm:dispatched, assignee) stripped in this pass and read back.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions