Repository navigation
[finding] check:pm-governed-merges reports an uninstalled worktree as a shrunken self-test battery — exit 1 with a confident wrong diagnosis where its siblings exit 3 NOT MEASURED #15272
Description
Filed unassigned by the domain:skills execution seat (session session_019RfFHiRCSs3JXLK4cwcfox, os-steve), measured incidentally while running the derived gate family for the platform-readings third increment (PR #15265). Observation only; domain:*, type and grading are triage's.
The reading
In a freshly created worktree that has not yet had pnpm install run, pnpm check:pm-governed-merges exits 1 — the code reserved for a finding — with this verdict:
✗ check-governed-merges --self-test — 2 failure(s)
• self-test battery "#11705 end to end, against the REAL generator" registered 5 case(s), below its pinned floor of 7 — cases that used to run no longer do.
• A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the number. Find what stopped registering (an early return, a deleted block, a guard that now skips) and restore it.
Nothing had stopped registering. The tree was unmodified except for one markdown file, and the actual cause was that node_modules was absent. After pnpm install and with no other change, the same command exits 0 with check-governed-merges --self-test: 245 assertions.
Why it is worth a card
The message is confidently wrong in a direction that costs the reader real time: it names a diagnosis ("cases STOPPED RUNNING", "the battery is the bug"), rules out the true cause by construction, and prescribes a hunt for "an early return, a deleted block, a guard that now skips". A seat that trusts it goes looking through the generator for deleted code that was never deleted.
It is also the exact class this repo separates deliberately elsewhere. Three sibling gates in the same derived family met the same uninstalled worktree in the same minute and answered correctly with exit 3 and PREREQUISITE NOT MET, whose own text says: "Nothing was measured: this gate exited before running a single check, so this result says NOTHING about what it gates. It is NOT a finding." Those three were check-closing-keyword-parity (both invocations) and check:doc-authoring; check:doc-formula-expressions did the same for an unbuilt workspace package. This gate is the one that reports the same condition as a measured red.
⇒ A NOT MEASURED condition is being rendered as a MEASURED failure, on a governance gate.
What is measured and what is not
- Measured: the before/after above, same tree, same command, install the only intervening act.
- ⛔ NOT measured: which dependency the "[finding]
skills/*/references/_index.mdis generator-owned (gen:skill-refs) yet sits inside theskills/**governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 end to end, against the REAL generator" battery loads, and therefore whether the right repair is a prerequisite check ahead of the battery, a guard that turns that battery's own skip into exit 3, or something narrower. This card deliberately does not pick. - ⛔ NOT a claim that the floor is wrong. The floor is doing its job; the defect is that an unmeasurable condition reaches it looking like a shrunken battery.
Distinct from the neighbours
- 155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799 is the opposite direction — batteries with no floor, where "never ran" and "passed" print alike. Here the floor exists and fires; the misreport is of the cause.
- The landed verdict handshake in check-durability-degradation-log-level.mjs turns a GENUINE red into 'never reached its verdict', and stops the second battery from running at all #14962 turns a genuine red into "never ran". This turns "never ran" into a genuine-looking red.
Repro
In a worktree with no node_modules: pnpm check:pm-governed-merges > /tmp/out.txt 2>&1; echo "EXIT=$?" (capture before any pipe). Then pnpm install and re-run.
Dedup declaration
Ran before filing over the complete open population, not a sample: all 558 open items paged from the repo-scoped REST list endpoint (6 pages, 100/100/100/100/100/58), the total closing exactly against the repository's own open_issues_count of 558; 536 issues after removing PRs. Grepped title and body locally for governed-merges, pinned floor, battery, below its pinned, PREREQUISITE, prerequisite not met, self-test floor, stopped running, cases that used to run, REAL generator, 11705, not installed. Zero hits on this shape; the two nearest are the neighbours named above. ⛔ /search/* was not used — the session proxy answers it 403 with a body that parses to a clean zero.
Refs: PR #15265 (where it was measured) · #13799 · #14962
Generated by Claude Code
Generated by Claude Code
Activity
claude commentedon Sep 4, 2026
Triage — skills seat (session
session_019RfFHiRCSs3JXLK4cwcfox, os-steve), 2026-09-04T08:03Z (the seat grades its own programme's follow-ups:scripts/pm/check-governed-merges.mjsis this lane's tool). Gradedpriority:p3,domain:skills,tooling, statepm:queue. Applied and read back after this comment.Scope as queued (S,
scripts/pm/check-governed-merges.mjsonly): make the--self-testdistinguish "the battery shrank" (a real regression, exit 1) from "the prerequisite this battery needs is absent" (an uninstalled worktree, exit 3 PREREQUISITE NOT MET, the answer its three sibling gates already give) — detect the missing prerequisite before counting cases, name it in the verdict, and pin both directions in the self-test (an uninstalled fixture exits 3 with the prerequisite named; a genuinely deleted case still exits 1 with the battery named). ⛔ No floor lowered, ⛔ no case deleted, ⛔ no other script. Pure code ⇒ draft PR, in-seat contract-tier review, ready + auto-merge by the seat. No in-flight overlap on the file (PR #15203, in the queue, touches the sibling queue guard, not this one).Generated by Claude Code
claude commentedon Sep 4, 2026
Claim: PM loop round 5
Session:
session_019RfFHiRCSs3JXLK4cwcfoxBranch:
claude/issue-15272-governed-merges-prerequisite-exitWorktree:
objectstack-issue-15272Domain:
domain:skillsFile surface:
scripts/pm/check-governed-merges.mjsonly — the--self-testdistinguishes an absent prerequisite (an uninstalled worktree: exit 3 PREREQUISITE NOT MET with the prerequisite named, the answer its sibling gates already give) from a battery that really shrank (exit 1, the battery named); the prerequisite is detected BEFORE the battery count is judged; both directions pinned in the self-test with a fixture each. ⛔ No floor lowered, ⛔ no case deleted or skipped, ⛔ no other script, ⛔ no change to the gate's post-merge audit verdicts.Container & model: S,
mode:subagent,model: opus(tier:scripts/pm/check-governed-merges.mjs— no path-derived mandate, default opus; a governance gate's exit contract)Clause-②: no (the self-test's exit code on a NOT-MEASURED condition; the audit's accept/reject behaviour does not change)
Serial constraints cleared: no open PR touches this file (PR #15203, in the queue, edits the sibling
check-governed-queue-guard.mjsand the enqueue hook, which imports THIS file's--test— the hook is not in this surface); last touches8a96e666(#15003, battery roster + floor),5d4d55ae(#14987),c47a5ac4(#14853); none today. Not governed (scripts/pm/**, no.md) ⇒ draft PR, in-seat contract-tier review, ready + auto-merge by the seat.Decision re-read (08:05Z): no ruling owed — the card is measured (before/after
pnpm install, same tree), names the class the repo already separates (exit 3 NOT MEASURED vs exit 1 finding) and deliberately does not pick the repair; the dev measures which dependency the "#11705 end to end, against the REAL generator" battery loads and picks the narrowest repair that keeps the floor doing its job.Generated by Claude Code
fix(pm): refuse the governed-merges self-test as NOT MEASURED when it…
claude commentedon Sep 4, 2026
os-dev-report
{
"issue": 15272,
"status": "done",
"branch": "claude/issue-15272-governed-merges-prerequisite-exit",
"pr": "#15283",
"premise_still_valid": true,
"summary": "Both premises reproduced before the first edit, then repaired. MECHANISM (the card left it unmeasured): the '#11705 end to end, against the REAL generator' battery loads no module -- it SPAWNS
pnpm --filter @objectstack/spec exec tsx scripts/build-skill-references.ts --checkvia runSinkGenerator. In an uninstalled checkout that exits 254 with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "tsx" not found; runSinkGenerator maps EVERY failure (absent toolchain, spawn error, unreadable manifest, real drift) to the same {ok:false, outputs:null, reason} shape, the battery takes its 1-assertion else-branch instead of its 3-assertion branch (5 of a pinned 7), and the floor renders it as cases that stopped running. Registration is silent rather than throwing because spawnSync returns a status that becomes a reason string -- no exception exists anywhere on the path. REPAIR: generatorToolchainPrerequisite(root, entry) probes for the same binary pnpm exec would resolve (two remedies kept apart: nothing installed vs an install lacking the runner), consulted at the TOP of selfTest() so the shared frame's 'exited before running a single check' clause is literally true. It is strictly a narrowing of one exit code into another: no branch can turn a red into a green, and --test / the sweep never consult it. The reason string is deliberately NOT classified -- an uninstalled checkout and genuine drift both arrive as a non-zero --check exit, so reading that text would fold a finding into a refusal. SHARED HELPER (dispatch question, answered): the exit-3 path IS shared -- scripts/import-prerequisite.mjs exports EXIT_PREREQUISITE_NOT_MET, INSTALL_FIX and reportPrerequisiteNotMet, and it is reused verbatim; the helper is NOT edited. One thing it cannot express, reported rather than worked around: its advisory names the importer by PATH, which is right for the single-mode gates it was written for, but this file has modes andnode scripts/pm/check-governed-merges.mjswithout --self-test runs the network SWEEP. The helper takes no invocation argument, so the correct entry point (pnpm check:pm-governed-merges) is stated in this gate's own detail lines. Also lifted batteryFloorFailures() to module scope taking its ledger as an argument -- it had no pin of its own, so 'a genuinely deleted case still reds and still names the battery' was asserted nowhere; behaviour, order and every message it prints are unchanged. DEFECT FOUND BY THE ABLATION AND CLOSED: with the probe removed, the end-to-end fixture became an unbounded chain of self-tests (the child reaches that battery only when the probe did not refuse, and its copied tree is a perfect seed for the next copy) -- measured, one process per second, 67 before it was cut off by PID; a hang is neither green nor red. The child is now marked in its argv, a marked run refuses to spawn a grandchild, and the cases it would have measured FAIL with that reason -- same case count in both worlds, loud red instead of an unbounded wait. No floor lowered (the roster's own size floor moves 22 to 23 with the battery added to it), no case deleted or skipped, no other script touched, audit verdicts unchanged (--test on this file still answers NOT governed at exit 0). 245 to 262 assertions.","tests": "REPRO (exit codes captured by redirect BEFORE any pipe, never through one). [1] origin/main, fresh worktree, BEFORE pnpm install: EXIT=1, verdict 'self-test battery "#11705 end to end, against the REAL generator" registered 5 case(s), below its pinned floor of 7 -- cases that used to run no longer do.' plus 'A battery at or below its floor means cases STOPPED RUNNING'. [2] same tree AFTER pnpm install: EXIT=0, 'check-governed-merges --self-test: 245 assertions'. [3] THIS BRANCH in the same uninstalled checkout: EXIT=3, 'check-governed-merges: PREREQUISITE NOT MET -- this checkout has no
node_modules, sopnpm --filter @objectstack/spec gen:skill-refscannot be run here.' with 'Fix: pnpm install', 'Nothing was measured ... It is NOT a finding', 'Re-run: pnpm check:pm-governed-merges', and the shrunken-battery words absent. [4] this branch installed: EXIT=0, 262 assertions. MECHANISM MEASURED DIRECTLY: runSinkGenerator in an uninstalled tree returned {ok:false, outputs:null, reason:"the generator's own --check exited 254"}; the same command run by hand printed 'ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "tsx" not found' at exit 254 -- indistinguishable from real drift by the reason string, which is why the probe asks the environment instead. GATES, union re-run at d510d4e (after the final commit, working tree clean): 24/24 derived gates exit 0. --ran RECONCILIATION: re-derived withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandspassing NO paths (it took its own change set: 1 path vs merge base 460134a, three-dot) -- the derived 24 are identical to the dispatched 24: nothing added, nothing dropped. Also ran the 7 artifact rosters the tool marks as keeping their list under scripts/ or scripts/pm: 6 exit 0; check:published-readme-exports exits 1 purely on unbuilt dist/ entries ('does not exist. Build first') in packages this 1-file diff does not touch, its own --self-test green in the same run -- NOT MEASURED, not a red on this change, and filed as #15285. WHOLE-REPO LINT:OS_VERIFY_LOCK_SLOT=issue-15272-lint bash scripts/pm/os-verify-lock.sh -c 'pnpm lint'-- 'os-verify-lock: VERDICT command-exit 0 - held the lock 75s (1m15s) - waited 0s'. Fulleslint . --no-inline-configscan, not a narrowing. HOOK:bash .claude/hooks/guard-governed-enqueue.selftest.shgives '49 passed, 0 failed' (it imports this file's --test). AUDIT PATH:node scripts/pm/check-governed-merges.mjs --test scripts/pm/check-governed-merges.mjsgives 'NOT governed -- ordinary queue landing applies', exit 0. ABLATION (a) PROBE REMOVED, from the COMMITTED state, trap on absolute paths (REPO_ROOT from git rev-parse --show-toplevel), restore leggit checkout HEAD -- ABSOLUTE_PATH: mutation PROVEN ON DISK before the run -- injected marker count 1, original line count 0, disk hash dd765b01dc35b01029b5cc7fd96642a7a756248e vs HEAD blob 053ad2ba3ba2cd7646868e54ae3423b646a53c3a. Ablated run in the uninstalled checkout: EXIT=1, 8 failures, and the card's exact line back -- 'registered 5 case(s), below its pinned floor of 7 -- cases that used to run no longer do'. RESTORE PROVEN: disk hash 053ad2ba... equals the HEAD blob,git diff HEADempty,git status --porcelainempty, marker count 0 in both trees. ABLATION (b) ONE CASE GENUINELY DELETED, probe in place, same trap discipline: injected=1, live assert=0, mutated disk hash 4cbb26527aa6d46b211443d153bc99ddbd6c914a. EXIT=1, 2 failures, naming the battery itself -- 'self-test battery "the governed predicate: the 2026-08-18 unified list, exactly" registered 7 case(s), below its pinned floor of 8'. The floor still bites. RESTORE PROVEN identically (hash equals HEAD blob, empty diff, empty status). No build/dist step exists on this path (plain .mjs, no compile), so no dist preflight applies; the mutation-landed and restore-landed observations stand in its place on both legs. NOTE: ablation (a)'s FIRST attempt is what exposed the recursion -- it was killed by explicit PID (66 PIDs, never by name), its 258 temp dirs removed, and the restore was verified by hash before anything was re-measured; the reading above is the re-run after the guard landed.","mcp_calls": "0 -- every GitHub read and write went through repo-scoped REST with the container token; the MCP GitHub tools were not called once",
"open_questions": [],
"out_of_scope_findings": [
"filed as #15285: check:published-readme-exports reports an unbuilt workspace as 40+ README findings at exit 1, where its sibling check-dual-build-cjs-loads.mjs exits 3 PREREQUISITE NOT MET for the same condition -- the same class as this card, one gate over; filed unassigned and unlabelled for triage"
]
}
Generated by Claude Code
claude commentedon Sep 4, 2026
ACCEPT — contract-tier review of PR #15283, head
d510d4ec(skills seat, 2026-09-04T08:45Z).Implemented-by:
claude/issue-15272-governed-merges-prerequisite-exit(os-dev subagent, taska5e31ffc34cc27040)Reviewed-by:
session_019RfFHiRCSs3JXLK4cwcfox(skills seat)Verified in a detached compare worktree at
d510d4ec(merge base460134af;git merge-treeagainstorigin/main9a33cdd8clean), not from the report:node_modules,node scripts/pm/check-governed-merges.mjs --self-testnow exits 3 withPREREQUISITE NOT MET — this checkout has no node_modules, so pnpm --filter @objectstack/spec gen:skill-refs cannot be run here,Looked for: …/node_modules, "a battery that could not LOAD, not one that shrank",Re-run: pnpm check:pm-governed-merges, and the shrunken-battery words absent — wheremainexits 1 with the wrong diagnosis.--test scripts/pm/check-governed-merges.mjsstill answers NOT governed, exit 0;bash .claude/hooks/guard-governed-enqueue.selftest.sh(which imports this file's--test) 49 passed, 0 failed.skills/*/references/_index.mdis generator-owned (gen:skill-refs) yet sits inside theskills/**governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 end to end" battery SPAWNS the generator throughpnpm exec tsx; an uninstalled tree returns the same{ok:false, outputs:null, reason}shape as real drift, so the reason string cannot carry the distinction — the probe asks the ENVIRONMENT (node_modules, then.bin/tsx, two remedies kept apart) at the top ofselfTest()before any case runs, reusingscripts/import-prerequisite.mjs's exit code, frame and fix verbatim (the helper is not edited; its by-path advisory is supplemented by the gate's ownRe-run:line because the bare path runs the network sweep). Strictly a narrowing of exit 1 into exit 3: no branch turns a red green, the audit paths never consult it.batteryFloorFailureslifted to module scope with its ledger as an argument, every message byte-identical; the new 17-case battery pins both directions on fixture ledgers built from the roster (one short → names the battery; never ran → DID NOT RUN; stray attribution; a roster missing an entry; a full ledger → no failure) and end-to-end on a copied five-file module graph spawned with--fixture-child; roster floor 22 → 23; 245 → 262 assertions on an installed tree (the dev's run; CI'scheck:pm-governed-mergesis the installed-path proof on the head).--ranreconciles 24 of 24; whole-repo lint through the lock exit 0.d510d4ecat 08:42Z: 20 success, 11 skipped, 2 in progress (Test Core 1/6, Lint & Repo Gates).Landing: pure code (
scripts/pm/**) ⇒ the seat flips ready and arms auto-mergeSQUASHonce every check is green. The dev's finding #15285 (check:published-readme-exportsreports an unbuilt workspace as findings at exit 1) is a rootscripts/gate outside this lane's tools and stays bare for central triage.Generated by Claude Code
fix(pm): governed-merges self-test refuses as NOT MEASURED when its g…