Skip to content

[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

Closed

Description

@claude

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

Distinct from the neighbours

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

  1. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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.mjs is this lane's tool). Graded priority:p3, domain:skills, tooling, state pm:queue. Applied and read back after this comment.

    Scope as queued (S, scripts/pm/check-governed-merges.mjs only): make the --self-test distinguish "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

  2. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round 5
    Session: session_019RfFHiRCSs3JXLK4cwcfox
    Branch: claude/issue-15272-governed-merges-prerequisite-exit
    Worktree: objectstack-issue-15272
    Domain: domain:skills
    File surface: scripts/pm/check-governed-merges.mjs only — the --self-test distinguishes 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.mjs and the enqueue hook, which imports THIS file's --test — the hook is not in this surface); last touches 8a96e666 (#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

  3. added a commit that references this issue on Sep 4, 2026
    cc7757a
  4. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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 --check via 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 and node scripts/pm/check-governed-merges.mjs without --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, so pnpm --filter @objectstack/spec gen:skill-refs cannot 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 with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands passing 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'. Full eslint . --no-inline-config scan, not a narrowing. HOOK: bash .claude/hooks/guard-governed-enqueue.selftest.sh gives '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.mjs gives '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 leg git 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 HEAD empty, git status --porcelain empty, 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

  5. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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, task a5e31ffc34cc27040)
    Reviewed-by: session_019RfFHiRCSs3JXLK4cwcfox (skills seat)

    Verified in a detached compare worktree at d510d4ec (merge base 460134af; git merge-tree against origin/main 9a33cdd8 clean), not from the report:

    • The card's repro, reproduced by the seat on the head: in a worktree with no node_modules, node scripts/pm/check-governed-merges.mjs --self-test now exits 3 with PREREQUISITE 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 — where main exits 1 with the wrong diagnosis. --test scripts/pm/check-governed-merges.mjs still answers NOT governed, exit 0; bash .claude/hooks/guard-governed-enqueue.selftest.sh (which imports this file's --test) 49 passed, 0 failed.
    • Mechanism measured, repair narrow: the live "[finding] skills/*/references/_index.md is generator-owned (gen:skill-refs) yet sits inside the skills/** 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 through pnpm 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 of selfTest() before any case runs, reusing scripts/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 own Re-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.
    • The floor stays the floor: batteryFloorFailures lifted 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's check:pm-governed-merges is the installed-path proof on the head).
    • A defect the ablation exposed is closed in the same PR: with the probe removed the end-to-end fixture recursed (a copied tree is a seed for the next copy; 67 processes measured, killed by PID) — the child is now marked and a marked run refuses to spawn a grandchild, failing those cases loudly instead of hanging. The dev's two ablation legs (probe removed → the card's exact wrong line returns; one case deleted with the probe in place → exit 1 naming the battery) are quoted with hashes and restores; --ran reconciles 24 of 24; whole-repo lint through the lock exit 0.
    • CI on d510d4ec at 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-merge SQUASH once every check is green. The dev's finding #15285 (check:published-readme-exports reports an unbuilt workspace as findings at exit 1) is a root scripts/ gate outside this lane's tools and stays bare for central triage.


    Generated by Claude Code

  6. added a commit that references this issue on Sep 9, 2026
    5b09356
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions