Repository navigation
A green gate union proves nothing about the head it is reported against — nothing ties the run to a commit, so a mid-round union goes stale silently #8550
Description
Activity
Triage:
pm:queue+domain:skills, type Task. Lands in the dev-seat contract / gate-runner tooling surface (os-dev definition + dispatch runbook territory → skills lane). — triage seat Routine.Route guidance (PM-suggested route, dev may re-cut against measurement): shape 2 (mechanical sha stamp on the union run) is the only one that doesn't rely on anyone remembering, but shape 1's contract line ("run the union after the final commit; quote
git rev-parse --short HEADfrom that run") is the cheap immediate cover and 3 (ratchet-family-only re-run) is its targeted subset. A sensible single card: land 1+3 as contract text now, and 2 as the mechanical successor if the wrapper is cheap enough to include.Guardrail note for dispatch/review: if the fix edits
.claude/skills/**, the skills-update guardrail applies (human merge, terminal three-piece);.claude/agents/os-dev.mdis agent-definition text — either way the PR must flag its path surface so review takes the maintainer-merge fork deliberately, not by noticing late.Related: #8545 (wrong invocation) and #8553 (blind detector) are sibling failure modes in the same family — deliberately not packaged; no shared acceptance criterion.
Size/model suggestion: S if contract-text only, M with the wrapper; fable-mandatory only if
.claude/skills/pm-dispatch/**is touched.
Generated by Claude Code
Claim: PM loop round 9
Session:session_018WuTtyckQa1VcXwgd52JpN
Branch:claude/issue-8550-union-head-binding
Worktree:objectstack-issue-8550
Domain:domain:skills
File surface:.claude/agents/os-dev.md(the gate-union contract line) +references/review-checklist.md(the reviewer's half) (stop on breach; explain in the report)
Container & model: S,mode:subagent,model: opus(role-file + checklist text; not under the fable-mandatory roots)
Serial constraints cleared: os-dev.md free (last holder's PR merged yesterday); review-checklist.md freed by the sweep PR (MERGED). Round-9 batch file-disjoint by construction.Seat ruling recorded (shapes 1+3 composed; shape 2 deferred): the dev-seat contract becomes "run the union after the final commit and quote
git rev-parse --short HEADfrom that run in the report/PR body"; the ratchet emphasis from shape 3 lands as the stated reason (a stale ratchet run is a claim about a ledger measured against different code); the reviewer's checklist line reads the quoted HEAD against the PR head — the same head-pinning discipline the checklist already applies to CI runs, now applied to local unions. ⛔ Shape 2 (mechanical sha-stamping wrapper) deferred: new machinery for what one contract sentence catches — revisit only if the contract line measurably fails. The observed fixture-remedy note (override the existing double instead of pinning a second) may land as one sentence beside the gate-union line if it fits the file's idiom — dev's call.
Generated by Claude Code
{ "issue": 8550, "status": "done", "branch": "claude/issue-8550-union-head-binding", "pr": "https://github.com/objectstack-ai/objectstack/pull/8635", "premise_still_valid": true, "summary": "Shapes 1+3 composed as contract text per the recorded seat ruling; shape 2 not built. os-dev.md's 'Local verification scope' section now carries the timing half of the union contract — run the union AFTER the final commit, quote `git rev-parse --short HEAD` from that run in the report's tests field and the PR body, and re-run the union (or at minimum the ratchet family) at the new head after any post-review push before the report/body is updated — with shape 3's ratchet reasoning as the stated why. review-checklist.md gains the reviewer's half placed immediately after the existing CI head-pinning clause (PM hypothesis confirmed: those clauses are at lines 49-56 and the new bullet sits between the pinning clause and the never-re-run clause, so the two read as one discipline): compare the quoted HEAD against the PR's current head.sha, a mismatch counts in NEITHER direction, and an unquoted HEAD is no reading at all rather than a green one. Fixture-remedy observation landed as one sentence, but in the engine-double trap bullet rather than beside the union line — reported as a deliberate deviation from the ruling's suggested placement, on the reasoning that the seat who needs it is the one adding a fake engine, not the one running the union. Premise verified against current origin/main before implementing: the union contract had moved with today's merges (it is now the 'Local verification scope' section, and the flock discipline around it had also been rewritten to `-E 99 -w 540` since the dispatch prompt was cut) — the gap itself was intact, the contract said which gates and never when, and the checklist had CI head-pinning with nothing about locally-run unions. One surprise for the PM to resolve, also flagged in the PR body: the claim comment recorded this card as 'not under the fable-mandatory roots', but `references/review-checklist.md` does live under `.claude/skills/pm-dispatch/**`, which the triage comment named as the fable-mandatory trigger — the practical posture is unchanged (draft, human merge, not armed) but the model/guardrail read deserves a deliberate look rather than being inherited.", "tests": "Union run AFTER the final commit at HEAD=99cc617bfe (practicing the clause being written; PR head.sha reads back as 99cc617bfe807d75e8231cf343aff018d25a217d, so the quoted HEAD is current). All eight named families exit 0: `=== UNION RUN AT HEAD=99cc617bfe ===` / `[exit=0] pnpm check:agent-model-declared` (1 agent definition, os-dev.md -> opus) / `[exit=0] pnpm check:doc-authoring` (377 files clean) / `[exit=0] pnpm --filter @objectstack/lint run check:doc-formula-expressions` / `[exit=0] pnpm check:nul-bytes` (7770 text files, no raw control bytes) / `[exit=0] pnpm check:pm-skill-id-lint` (9 files clean, pattern /#[0-9]{3,}/g) / `[exit=0] pnpm check:pm-skill-ratchet` (SKILL.md 686 lines, ceiling 686, untouched by this diff) / `[exit=0] pnpm check:skill-frame-sync` (4 copies isomorphic, 3 axes) / `[exit=0] node scripts/check-skill-frame-freshness.mjs` / `=== UNION AT HEAD=99cc617bfe overall_fail=0 ===`. Everything ran in the foreground under `flock -E 99 -w 540 /tmp/os-heavy-verify.lock` with NODE_OPTIONS=--max-old-space-size=4096; no queue timeout was hit. Gate lists: prompt named 8; `node scripts/pm/dispatch-gates.mjs .claude/agents/os-dev.md .claude/skills/pm-dispatch/references/review-checklist.md` derived 6 (agent-model-declared, doc-authoring, doc-formula-expressions, nul-bytes, pm-skill-id-lint, skill-frame-sync) — a strict SUBSET, so the derivation added nothing the prompt missed; pm-skill-ratchet and check-skill-frame-freshness.mjs were run because the prompt named them. First pass had one red that was environmental, not a defect: check:doc-formula-expressions died on ERR_MODULE_NOT_FOUND for @objectstack/formula/dist/index.mjs — the fresh-worktree build-closure trap the role file already names — fixed with `pnpm --filter '@objectstack/formula...' build` (upstream/suffix direction, deliberately) and the logged union above is the post-build run. Self-scans beyond the gates on both edited files: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` and `grep -nE '#[0-9]{3,}'` both returned no matches. No ablation and no reverse verification in this card — the deliverable is contract prose with no executable behaviour to mutate, so the template's artifact-rebuild field has nothing to report rather than a green to claim.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 14, 2026
Filed by the dev seat on #8470 (PR #8537), where this cost a red CI lap. Unassigned and unlabeled for triage. Surface: dev-seat contract / PM dispatch tooling.
check:engine-double-contractbehaved correctly — it caught a real second unguarded engine double. This is about the fact that a PR body can truthfully report a green union over a tree that is no longer the head, and nothing anywhere notices.What happened
The lane's contract says: derive the gate families from
lint.ymland re-derive withscripts/pm/dispatch-gates.mjs, run the union, report. I did. Every gate exited 0 and the PR body listed them.Then review feedback landed and I pushed two more commits, both adding test code. One of them grew a second unguarded engine double in a file whose shrink-only baseline records one. The gate that catches exactly that had been run — before the code that broke it existed. CI went red on a PR whose body accurately reported a green union.
Why this is not #8545
#8545 is the same lane, one lap earlier, and it is worth stating the difference because the remedies do not overlap:
check:type-check-coveragewhere the ratchet ischeck:type-check-debtdispatch-gates.mjsemit the invocation CI runsFixing #8545's emitted name would not have prevented this, and vice versa. They are neighbours, not duplicates.
Why the existing defences do not catch it
The contract is written as a checklist of which gates to run. It says nothing about when, and the natural workflow — implement, verify, then respond to review — puts the verification before the last commits by construction. The failure is therefore most likely on exactly the PRs that get the most review attention, which is the wrong way round.
It is also invisible after the fact: a gate log carries no commit sha, so neither the dev seat re-reading its own transcript nor the PM reading the PR body can tell whether a reported green run covers the head. The report and the tree can disagree with nothing in between to notice.
Note the ratchet gates make this sharper than ordinary lint. A stale ordinary-lint run is usually still true. A stale ratchet run is a claim about a ledger measured against different code, and the ratchets are precisely the gates whose whole purpose is to notice code that was just added.
Suggested shapes (not decided here)
git rev-parse --short HEADfrom that run". Costs one re-run per review round; catches this whole class.HEAD=<sha>and a reviewer — or a bot — can compare it to the PR head. This is the version that does not rely on anyone remembering.check:engine-double-contract,check:type-check-debt,check:query-options-erasure, and the other shrink-only ledgers), since those are the ones a late commit actually moves.Worth noting one thing observed while fixing this: the gate's own message says "pin the new one rather than raising it", but a third option was better here — build the fixture by overriding the file's existing double instead of declaring a second one, so there is no new double to pin or ledger. Not a defect in the message; just a remedy the message does not mention and that a seat under ratchet pressure may not think of.
Related
#8470 / PR #8537 (where it surfaced) · #8545 (sibling failure mode, same lane, different cause) · #5278 (shrink-only ratchet rule)
Generated by Claude Code