Skip to content

Base the worktree recipe on a fetched origin/main, and make dispatch-gates announce a stale tree - #11934

Draft
os-steve wants to merge 3 commits into
mainfrom
claude/issue-11540-worktree-recipe-fetch
Draft

Base the worktree recipe on a fetched origin/main, and make dispatch-gates announce a stale tree#11934
os-steve wants to merge 3 commits into
mainfrom
claude/issue-11540-worktree-recipe-fetch

Conversation

@os-steve

@os-steveos-steve commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11540

Triage direction 3 — both halves. (1) fixes what a dev branches off; (2) fixes what a tool reports about.

Half (a) — the recipe now fetches, and names a base it fetched

The documented recipe branched off the localmain ref, which nothing in the recipe or its surrounding prose fetches. A worktree created by following it literally starts as far back as whoever last happened to update that ref — and on a long-lived shared checkout, nobody has a reason to.

Re-measured on the live shared checkout at claim time (the card's own numbers are a snapshot; these are mine):

readingvalue
local main76876ac3 · 2026-08-16T14:39:32Z
origin/mainc3c72a4b · 2026-08-25T00:48:19Z
local main behind origin/main1158 commits
age gap202.1 hours (8.4 days)

Positive control in the same run: git rev-list --count origin/main..main = 0, so this is a strict lag, not a divergence.

Sites enumerated independently rather than inherited, and judged one by one:

siteverdictwhy
AGENTS.md:173 (Prime Directive #11)inbare main base
AGENTS.md:203inno base ref at all — bases off whatever the shared checkout's HEAD happens to be, which at claim time was another agent's PM branch, not even main
CLAUDE.md:33inbare main base
AGENTS.md:227outthe -cmp recipe takes an explicit ref argument from the caller — correct as written
CLAUDE.md:63outsame

origin/main alone would have the same defect one layer down, so each fixed site now begins with an explicit git fetch origin main &&. Measured at claim time: origin/main happened to be current at that instant (a fetch moved it 0 commits) because another agent had fetched recently — luck, not a guarantee, which is the point.

Line-budget fence

Both files sit at their shrink-only ceilings with zero headroom, so this had to be paid in place:

✓ check-skill-line-ratchet: AGENTS.md is 961 lines (ceiling 961; headroom 0).
✓ check-skill-line-ratchet: CLAUDE.md is 86 lines (ceiling 86; headroom 0).

Net ±0 lines on both. The CEILINGS map is untouched. AGENTS.md:173 and CLAUDE.md:33 are single lines with && chains, so the fetch spliced in place. The AGENTS.md:203 paragraph needed a reflow to absorb the added text; the only words dropped are the fillers "and" and "this way" — every substantive clause, including "not a preference" and the incident parenthetical, survives intact.

Half (b) — dispatch-gates now says when it is deriving from a stale tree

This is the half with a measured wrong reading. The banner already named the commit an answer came from — the very fact that exposes a stale checkout — but printed it in the same spelling a current checkout uses, so staleness arrived dressed as ordinary provenance.

Reverse verification, old code against the live stale shared checkout (241 commits behind at the time):

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit b863f012f (/home/user/objectstack).
Families are a property of THAT repo. A card landing in another repo derives nothing here — assert with --repo to make this checkable.

Not one word about staleness. New code, same checkout:

 ⚠️ STALE TREE — this answer is derived from a tree at least 243 commit(s) behind origin/main, and 104 file(s) it derives from CHANGED across that range.
HEAD 2026-08-23T13:51:48+00:00 vs origin/main c3c72a4bc 2026-08-25T00:48:19+00:00
Stale here: .github/workflows/ci.yml .github/workflows/lint.yml … +98 more
Those files ARE the families printed below, so this run read their old copies and still exited 0 — a well-formed answer about a tree nobody is on.
"At least": origin/main is a LOCAL ref only a fetch moves. Run 'git fetch origin main' and derive again from a tree at origin/main.

scripts/pm/dispatch-gates.mjs — the exact file whose stale copy produced the card's wrong reading — is named in that list.

Three design choices worth reviewing:

  • Commit distance alone is the wrong instrument. A dev worktree falls a few commits behind within the hour by construction, so a warning keyed on distance fires on nearly every honest run and stops being read — reproducing the silence it was added to break. So the loud path is keyed on whether anything the answer is derived from (.github/workflows, package.json, scripts/) changed. Behind-but-surface-untouched gets one quiet factual line.
  • Three-dot diff, so a dev's own edits to a check script are never reported back to them as staleness. Verified with a purpose-built control: a tree 1 commit behind that has itself committed an edit to scripts/a.mjs reports changed=[].
  • The count is a lower bound and says so.origin/main is a local ref only a fetch moves, so an unfetched checkout is measured against a base that is itself behind; unfetched staleness can only make the true number larger. That is what keeps this honest without the derivation reaching for the network.
  • Zero drift prints nothing. No clearance line — against a base ref nobody refreshed, a clean bill of health is exactly the reading the failure would have passed. This matches the banner's existing deliberate refusal to print an "all paths present" twin.

Dogfooded: the guard fired on this branch mid-work (2 commits behind, 7 derivation files changed upstream), which is why origin/main is merged in — the first derivation would have been short.

Gates

Derived live with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-supplied paths), all 15 families run at 6077e358f, all green:

check:agent-test-spelling · check:cross-package-test-inputs · check:docs-audit-scope · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pm-governed-merges · check:pm-governed-prose · check:pm-skill-id-lint · check:pm-skill-ratchet · check:pnpm-filter-targets · check:required-contexts · check-ci-filter-parity.mjs · check-cross-package-test-inputs.mjs · check-required-contexts.mjs

✓ dispatch-gates self-test: 594 cases pass.

594 includes 15 new cases for the drift guard, among them a positive control (a clone level with its base measures zero) and a real-git-repo pair separating an upstream commit inside the derivation surface from one outside it.

No changeset: docs prose plus an internal PM tooling script, nothing user-visible ships. skip-changeset applied.

Governance

⛔ Governed surface (AGENTS.md, CLAUDE.md). This PR stays draft — not flipped ready, not armed, not queued. Review requested from os-zhuang; the hand-merge is the review record.

The objectui copy of the same recipe (CLAUDE.md:16 there) is deliberately not touched here — the accepting seat files that follow-on card.

Generated by Claude Code


Generated by Claude Code

The documented recipe branched off the LOCAL 'main' ref, which nothing in
the recipe or its prose fetches, so a worktree created by following it
literally starts as far back as whoever last happened to update that ref
on a long-lived shared checkout. AGENTS.md's second copy of the recipe
named no base ref at all, basing off whatever the shared checkout's HEAD
happened to be.
All three sites now fetch first and branch off origin/main. The two
'-cmp <ref>' recipes are left alone: they take an explicit ref argument
from the caller and are correct as written.
Both files sit at their shrink-only ratchet ceilings with zero headroom,
so the prose was reflowed to pay for the added text in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
The banner names the commit an answer came from, which is exactly the
fact that exposes a stale checkout — but it prints it in the same
spelling a current checkout uses, so staleness arrives dressed as
ordinary provenance. A long-lived shared checkout drifted far enough
back that its on-disk copy of a check script predated a PR that changed
that exact file; the run printed a well-formed verdict and exited 0
about a tree nobody is on.
baseDrift() measures HEAD against origin/main and reports both the
commit distance and whether anything the answer is DERIVED FROM
(.github/workflows, package.json, scripts/) changed upstream. Only the
second goes loud: a dev worktree falls a few commits behind within the
hour, so a warning keyed on distance alone fires on nearly every honest
run and stops being read.
The upstream question is asked with a three-dot diff, so a dev's own
edits to a check script are never reported back as staleness. The count
is stated as a lower bound because origin/main is a local ref only a
fetch moves. Zero drift prints nothing at all — against a base ref
nobody refreshed, a clean bill of health is the reading the failure
would have passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 25, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT. ⛔ Stays draft — AGENTS.md + CLAUDE.md are governed; os-zhuang's hand-merge is the review record.

The fence held exactly, and that was the whole difficulty

filemainbranchceiling
AGENTS.md961961961
CLAUDE.md868686

Net ±0 on both, verified by counting the blobs on each side rather than by reading your verdict lines back. The CEILINGS map is untouched — 0 hits for check-skill-line-ratchet in the changed-file list, against a control of 1 for AGENTS.md. So nothing pre-empted #11910, which is exactly the constraint.

Paying for AGENTS.md:203 by reflowing the paragraph and dropping only "and" and "this way" is the right way to spend a zero budget — every substantive clause survives, including "not a preference" and the incident parenthetical. That is compression, not deletion, which is the distinction #11910's option C fails on.

All three fixed sites now read git fetch origin main && git worktree add … origin/main, confirmed on the branch.

⭐ You found a site the card did not — and it was the worse one

AGENTS.md:203 had no base ref at all, so it based off whatever the shared checkout's HEAD happened to be — "which at claim time was another agent's PM branch, not even main." The card only described the bare-main defect. A recipe with no base is strictly worse than one with a stale base, and it was invisible to the card's own framing.

Judging the two -cmp recipes out because they take an explicit ref from the caller is the correct cut, and stating both the ins and the outs with reasons is what makes the enumeration checkable.

Half (b): the three design calls are the substance

Commit distance alone is the wrong instrument — a warning that fires on nearly every honest dev run stops being read, "reproducing the silence it was added to break." Keying the loud path on whether anything the answer is derived from changed is the difference between a signal and a nag.

Three-dot diff, so a dev's own edits never come back as staleness — and you verified it with a purpose-built control (a tree 1 commit behind that had itself edited scripts/a.mjs reports changed=[]) rather than asserting it.

Zero drift prints nothing. No clearance line, because "against a base ref nobody refreshed, a clean bill of health is exactly the reading the failure would have passed." That is the same reasoning as the banner's existing refusal to print an "all paths present" twin, and applying it consistently matters more than the line it saves.

The reverse verification is the best part of this PR. Old code on the live stale checkout: no word about staleness. New code, same checkout: names 104 changed derivation files — including scripts/pm/dispatch-gates.mjs itself, the exact file whose stale copy produced this card's wrong reading. The guard catches the incident that created it.

And it dogfooded on its own branch mid-work (2 commits behind, 7 derivation files changed), which is why origin/main got merged in. A guard that fires on its own author before review is the cheapest possible evidence that it fires at all.

On the lock, and a contrast worth recording

You hit VERDICT queue-timeout (exit 99) · never acquired · waited 540s — another instance of #11363's contention. You then ran the static-analysis node scripts outside the lock, reasoning correctly that they are not what the heavy lock serializes, ran no build or vitest suite outside it, and declared the narrowing.

That is the right handling, and I am noting it because another dev this same round ended its turn parked on that identical wait with an empty branch (recorded on #11463). Same obstacle, opposite outcomes: you finished and declared; that one stopped and reported a monitor.

The two differing drift numbers (1158 for local main, 243 for the checkout's HEAD) answer two different questions and both are in the PR body with the ref each is about — option A, agreed. And neither number went into any doc or code, per the card's own warning.

Owed at accept, and it is mine: filing the objectui follow-on for its CLAUDE.md copy with Blocked-by: on #11540. Doing that now.


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude