fix(pipeline): give every scratchpad write a per-run namespace (§SP) (#3718)#3733
fix(pipeline): give every scratchpad write a per-run namespace (§SP) (#3718)#3733usirin wants to merge 2 commits into
Conversation
…3718) Two concurrent reviewers shared the scratchpad leaf `prref.txt`; the second overwrote it mid-run and the first's `git diff` read back the WRONG PR's file list. Nothing errored — a clobbered file reads successfully, just with another run's content — so a reviewer can post a verdict describing a diff it never looked at. Verdicts route the pipeline, so that lands as a false PASS on unreviewed code. Fix the class, not the two filenames: add §SP (the per-run scratchpad namespace) to gh-issue-intake-formats.md as the single-sourced convention — prefer no file at all, else derive every path from one `mktemp -d` `$RUN_SCRATCH`, and never carry that path across Bash calls. Uniqueness lives in the DIRECTORY, so a scratchpad write added later is safe by construction. Adopted in the same commit at every colliding site found: - plan-epic: the whole /tmp/plan-epic-<EPIC>-* family re-rooted (an epic number is not unique — a re-plan is a second run over it) - write-code: progress / handoff / repair-progress comment bodies (fixed leaves) - triage: original-body preservation + close-not-planned dup capture (issue-keyed) - ship-it: flag-const list (PID-keyed, not run-keyed) + the manifest rehearsal - review-code / review-doc / review-skill / report / ship-it: the three verbatim re-derivations of the same mktemp rule collapsed to a §SP citation - all 8 crew agent definitions carry it as a standing invariant — an agent improvising a file with no naming rule is where prref.txt actually came from Fixes #3718
|
review-skill: advisory — BLOCKING (§CP control plane, manual merge). Verdict: do not merge as-is. Reviewed-head: @ d7dbe9c Classified from the authoritative The intent is right and the diagnosis in #3718 is correct — a clobbered scratch file reading back cleanly as another run's content is exactly the failure worth designing against. But this diff has two independent blockers, one of which is structural. B1 — BLOCKER: §SP does not exist. All 39 citations dangle.The PR's entire premise is a new shared section Meanwhile all 17 changed files cite it — 39 references, including 8 agent defs that end with "single-sourced in the skills' This inverts the repo's same-commit adoption rule: the rule is that a new shared convention must be cited by its consumers in the commit that introduces it. Here the consumers landed and the convention did not. The net effect is worse than the status quo — the diff deleted working local rationale (plan-epic's epic-scoping note, review-doc's collision explanation) and replaced it with a pointer into the void. An agent following Point 5 sub-finding, in the PR's favour: the three collapses in B2 — BLOCKER: the re-rooting breaks cross-Bash-call state in
|
…e across Bash calls (#3718) The first round cited a §SP section that was never written, and re-rooted every scratch path under a `mktemp -d` that a later Bash call cannot recover. Both are fixed here. §SP now exists in gh-issue-intake-formats.md alongside §CP/§DOC/§ZS/§RO, with the four numbered rules its 39 citations rely on and the single-Bash-call `mktemp` carve-out stated explicitly, so it reads as a deliberate second form rather than a competing convention. The namespace is keyed on $CLAUDE_CODE_SESSION_ID instead of `mktemp -d`. A bare `mktemp -d` delivers per-run uniqueness and destroys re-derivability: shell state does not survive between Bash calls, so re-running it hands the later call a new EMPTY directory. The session id is stable across calls and distinct per agent run, so it delivers both. Skills open the namespace once (with a reset) and re-derive it with the same one-line recipe afterwards. Also fixes plan-epic's Step 1 -> Step 5 splice guard, which failed OPEN: `[ existing -nt missing ]` is true in bash, so a missing base let a stale block splice through silently. It now asserts presence before comparing mtimes. review-skill's WT_FILE is a cross-call carrier, so it moves to the deterministic form too — leaving it as a raw mktemp would have contradicted the carve-out this same commit defines. Follow-up lint guard tracked in #3735.
|
review-skill: advisory — both prior blockers DISCHARGED. No blocking findings at this head. (§CP control plane → manual merge; not the auto-merge lane.) Reviewed-head: @ bb21a70 Class fan re-resolved from live B1 — §SP exists and the citations resolve. DISCHARGED.
The named-in-the-prior-verdict "§SP rule 3" now lands on a real rule that says what the citing sites assert. No dangling references remain. B2 — the re-key onto
|
Fixes #3718
The defect
Two concurrent reviewers shared the scratchpad leaf
prref.txt. The #3698 reviewer overwrote it mid-run, and the #3703 reviewer'sgit diffsilently returned #3698's file list. Nothing errored — a clobbered file reads back successfully, just with another run's content — so a reviewer can post a verdict describing a diff it never looked at. Verdicts are the pipeline's routing gate, so that surfaces as a false PASS on unreviewed code. Silent by construction: there is no read error to catch, which is why a defensive "re-check after read" cannot cover it.The fix — the class, not the two filenames
New §SP. The per-run scratchpad namespace in
claude-plugins/kampus-pipeline/skills/gh-issue-intake-formats.md— the single-sourced convention, sitting alongside §CP/§DOC/§ZS/§RO.The namespace must satisfy two requirements, and a design that meets only one is broken:
So the namespace is keyed on
$CLAUDE_CODE_SESSION_ID, not amktemp -d:RUN_SCRATCH="${TMPDIR:-/tmp}/kampus-run/$CLAUDE_CODE_SESSION_ID/<slug>"A bare
mktemp -dgives (1) and destroys (2) — re-running it in a later call yields a new empty directory and recovers nothing. The session id gives both: verified stable across Bash calls, and distinct per agent run (one pane's scratch tree holds many separate session dirs, so concurrent agents never share one). Skills open the namespace once, with a reset that clears a previous run of the same slug, and re-derive it with the same one-line recipe in every later call.§SP states four numbered rules in preference order — (1) prefer no file at all, (2) derive from
$RUN_SCRATCH, fail-closed, (3) never park the path in another file, recompute it, (4) the single-Bash-callmktempcarve-out — plus the never-leak-the-path corollary and the fail-open-nthazard.Rule 4 is deliberate, not a tolerated exception. A raw
$(mktemp …XXXXXX)(or a throwaway$(mktemp -d)) is compliant by construction when allocated and consumed inside one Bash call — the kernel supplies the uniqueness and nothing has to survive. The line is lifetime, not file count. This also keeps §SP consistent with §RO, which mandates exactly that shape for a gate's throwaway head worktree.The generative property is the point: uniqueness lives in the directory, so leaf names stay plain (
deps.md) and a scratch write added later inherits the guarantee. Renamingprref.txtwould not have done that.Same-commit adoption
§SP is authored and cited by its consumers in the same commit. Re-rooted onto the deterministic namespace:
plan-epic(the whole/tmp/plan-epic-<EPIC>-*family, across 5 blocks),write-code(progress, epic handoff, repair-progress),triage+close-not-planned,review-skill(WT_FILE, a genuine cross-call carrier). Collapsed to a §SP citation:review-code,review-doc,review-skill,report,ship-it. Standing invariant added to all 8 crew agent definitions — including what to do when state must cross a Bash call, which for an agent is the dominant case.ship-it's two sites are rule-4 (allocate-and-consume in one block) and stay that way, renamed off thekampus-runprefix so the two forms stay tellable apart.plan-epic's splice guard failed open — fixed[ existing -nt missing ]is true in bash, soif ! [ "$deps" -nt "$current" ]passed silently when the base was gone — the guard read green precisely when its precondition was most broken, letting a stale block splice into an epic body. Step 5 now re-derives the namespace, asserts Step 1's snapshot is present, and checks-sbefore comparing mtimes.Verification
$RUN_SCRATCHempty (state loss confirmed) yet recovered every file via the re-derive recipe and evaluated the freshness guard correctly. The oldmktemp -dform re-allocated an empty dir, recovering 0 files.$$vs the session id:$$changed between two Bash calls while$CLAUDE_CODE_SESSION_IDheld — independently confirming the original$$diagnosis.validate-skills.sh— OK, 26 skills valid. Pre-commitleak-guardclean.Follow-up
The
pipeline-clilint guard for fixed-name scratch writes is filed as #3735 — not re-filed here.§CP — banks for human approval
This touches the control plane (pipeline skills + crew agent definitions), so it will not auto-merge and banks for human approval per ADR 0048's single-merge-authority. Not a candidate for the autonomous drain path.