You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Triage verdict — type:bug, p1, homed in the Worktree-Isolation Integrity campaign. Every source claim below was read first-hand against this checkout in this run and is cited file+line. Anything not verified here is labelled unverified, not softened. This desk wrote to no other issue; see Outstanding actions.
The question this ticket was filed to settle
Does #4544's fix also fix this? No. Same root-cause class, disjoint fix surface — #4544's scope and every one of its acceptance criteria enumerate exactly four skills, and write-code is not among them. Folding this into #4544 would silently drop the build path. Detail in Reconciliation, below.
What is wrong
The §SP contract promises that concurrent runs cannot name each other's scratch files. On the build path that promise is delivered by convention alone, in two places where it does not hold.
1. Every write-code scratch write hand-rolls the §SP path instead of allocating through the shipped allocator — so no owner stamp exists and there is no refusal to hit.
The allocator's protection is the exclusive-create owner stamp, not the path shape: openNamespace writes .kampus-run-owner with flag: "wx" (packages/pipeline-cli/src/tools/scratchpad/store.ts:83-85), and classifyOwnership (packages/pipeline-cli/src/tools/scratchpad/scratchpad.ts:194-199) refuses a same-session stamp carrying a different CLAUDE_PID as foreign. The shell entry points that reach it are kp_scratch_open / kp_scratch_path (claude-plugins/kampus-pipeline/lib/common.sh:380-381).
write-code calls neither. Eight committed sites interpolate the §SP path as a literal string and mkdir -p it, which creates no stamp and therefore can never return ForeignNamespace:
Site (under claude-plugins/kampus-pipeline/skills/write-code/scripts/)
Line
Slug
step6-progress-comment.sh
40
write-code-<N>
step7-epic-handoff.sh
39
write-code-<N>
stepR3-push-and-note.sh
53
write-code-<N>
stepR-frozen-ac.sh
28
write-code-repair-<PR>
stepR-round-count.sh
30
write-code-repair-<PR>
stepR1-verdicts.sh
42
write-code-repair-<PR>
stepR2-fail-body.sh
38
write-code-repair-<PR>
stepR2-inline-comments.sh
29
write-code-repair-<PR>
Three of those scripts carry an in-file comment conceding the bypass verbatim — "The hand-rolled RUN_SCRATCH derivation duplicates kp_scratch_path; collapsing it onto that helper is phase 2's call (#1929)" (step6-progress-comment.sh:14, step7-epic-handoff.sh:12, stepR3-push-and-note.sh:15). So the bypass is known and deferred, not accidental.
The slugs do carry the work item, so two lanes on two different issues resolve two different directories — that separator holds. What the missing stamp removes is the protection on the cases where the slug is not a separator: a second role on the same lane, a re-run, or a repair round over the same issue number. There the allocator would refuse; the hand-rolled path silently re-enters and clobbers.
2. Nothing binds the intermediates an agent invents mid-run to §SP at all. The three artifacts this report names — a mutation-harness script, a PR-body draft, a progress note — are exactly that class. Two of them have no skill-prescribed path anywhere:
The PR body is composed inline into gh pr create --body from a here-document (claude-plugins/kampus-pipeline/skills/write-code/SKILL.md:1476 ff). There is no file in the prescribed shape, so a lane that drafts the body to a file is improvising a path the skill never named.
A mutation-harness script has no prescribed shape at all.
The progress note is the one that is prescribed — progress.md inside the run scratch directory under slug write-code-<N> (SKILL.md:1550-1560, step6-progress-comment.sh:40).
write-code's only §SP instruction is scoped to one instance and says so: the branch-name block at SKILL.md:812-844 closes with "This is the local instance of the per-run scratchpad namespace, §SP...". There is no general rule in SKILL.md or repair.md binding every run-scoped intermediate the agent creates to §SP or to a rule-4 mktemp. That is precisely the rule #4544 requires of the four gate skills — and it is what would have prevented this report.
The banned surface an improvised write most likely lands on is the harness-provided session scratchpad directory, which §SP explicitly carves out: "A session-scoped scratchpad handed to an agent by the runtime is shared across the concurrent runs of that session, and generic leaf names under it ... collide" (claude-plugins/kampus-pipeline/skills/gh-issue-intake-formats.md:1985-1991). Six concurrent sibling lanes of one parent session share that directory by construction. The report does not name the concrete directory its three artifacts were written to, so which surface each collision landed on is not verifiable from the board — carried as unverified. The finding does not depend on it: both candidate surfaces are §SP violations at an improvised call site.
Why the price is what it is
Priced on the unmitigated failure mode, not the observed outcome. The clean result here was produced by write ordering, which is not a control. The class is silent by construction — a clobbered file reads back successfully, with plausible content, because it is a genuine artifact of a genuine lane. The two live outcomes are one lane overwriting another's state, or one lane adopting another lane's content and posting it: a PR body describing work the diff does not contain, and — because write-code PR bodies carry a closing keyword (Fixes #<N>, SKILL.md:1502) — a merge that closes the wrong issue.
p1 and not p0: the direction that makes #4544p0 is a gate reading corrupted content and emitting a well-formed false PASS. That direction is absent here — a build-lane artifact swap is misattribution, and the code gate still reads the diff. p1 because the fix is a small prose unit that rides an in-flight p0's fix at near-zero marginal cost, and the hazard fires in the factory's normal operating mode (six lanes).
Reconciliation — #4544 vs this issue: same class, non-overlapping surface
Both are the same root-cause class: run-scoped intermediates not bound to the §SP allocator. They cover different write sites, and neither fix reaches the other's:
review-code, review-plan, ship-it, review-skill — named in its Scope bullet and in its first acceptance criterion
write-code (including repair.md)
Write sites
Agent-authored gate intermediates (file lists, set-comparison inputs, working copies of a base SKILL.md)
Agent-authored build intermediates (mutation harness, PR-body draft) plus the eight committed hand-rolled sites tabled above
Allocator relation
Never routed
Path-shaped like §SP but stamp-free — allocator refusals unavailable
Worst case
A gate emits a confident false PASS
A lane posts another lane's PR body / progress note; a wrong Fixes #<N> closes a foreign issue
Recommendation: do not fold, and do not make this a child of #4544.#4544's triage explicitly verified its own scope against origin/main and fixed it to the four gate skills; write-code was never in it. The right shape is sibling tickets on one convention: whoever writes #4544's rule should write the identical paragraph into write-code/SKILL.md and repair.md, and this ticket is what makes that second half tracked rather than assumed. If an implementer takes both at once, this issue is the one that closes on the write-code half.
Relation to #4692 — closest neighbour, and a correction to its sweep
#4692 ("Unkeyed scratchpad filename let a lane commit another lane's commit message", type:bugp2axis:pipeline-hardening) is the same class at a third build-path call site, and its acceptance criteria already cover the stamp-free no-CLI fallback in lib/common.sh, three fixed-slug allocations, and a mechanical §SP-conformance guard. It is the nearest thing to a duplicate of this issue and an implementer should read it first. Two reasons this is not closed into it:
Unkeyed scratchpad filename let a lane commit another lane's commit message #4692's acceptance criteria name three write-code allocator-bypassing scripts (stepR-round-count.sh, stepR1-verdicts.sh, stepR2-fail-body.sh). The sweep run for this triage finds eight under skills/write-code/scripts/ (table above) — the five additional ones (step6-progress-comment.sh:40, step7-epic-handoff.sh:39, stepR3-push-and-note.sh:53, stepR-frozen-ac.sh:28, stepR2-inline-comments.sh:29) are not in Unkeyed scratchpad filename let a lane commit another lane's commit message #4692's list, and three of them sit on the non-repair main path. Closing this as a duplicate would carry that undercount forward.
Distinct. This refutes the "may share a root cause" hedge in the original report.#4864 is about the inside of the allocator; this issue is about writes that never reach it.
scratchpad open concurrency test fails only in merge-queue batch: loser exits 6, not 4 #4864's mechanism is the non-atomic owner stamp: the wx write at store.ts:83-85 makes the create exclusive but not the content write, so a loser hitting EEXIST can read a zero-byte stamp, parseOwner returns null (scratchpad.ts:171-183), and the loser takes the NamespaceUnavailable branch (store.ts:90-96, exit 6) instead of ForeignNamespace (exit 4).
This issue is the opposite direction: no refusal is reached at all, because the write sites either never call the allocator or never write a stamp. A fully atomic stamp would change nothing here.
#4516 ("Per-run scratch namespace's session key is shared across lanes and roles", type:bugp1) is the keying defect: the session id does not distinguish concurrent sibling lanes. It is upstream of this issue and does not subsume it — the write-code slugs already carry the work item (write-code-<N>), so lane separation on the build path survives a shared session id. What #4516 fixes is the residual same-lane / cross-role collision; what this issue fixes is the sites that have no stamp and the writes that have no rule. A fix here should adopt #4516's chosen lane identity rather than inventing a second one.
Scope note for the implementer
§CP surface.claude-plugins/kampus-pipeline/skills/write-code/ is control-plane, so the prose half rides the §CP review/merge path with a human approval at head. packages/pipeline-cli/src/tools/scratchpad/ is not touched by this ticket's fix.
Do not fix this by keying leaf filenames. §SP rule 2 puts uniqueness in the directory and names a randomized leaf as the anti-pattern (gh-issue-intake-formats.md:2063-2078). The original report's non-binding suggestion ("lane-unique filenames within the scratchpad namespace") is the fix the contract already rejects — recorded here so it is not re-bred.
§SP rule 1 first. Several of these intermediates need no file at all (the PR body is already a here-document; a harness script consumed inside one Bash call is a rule-4 mktemp).
Acceptance criteria
write-code's own body (claude-plugins/kampus-pipeline/skills/write-code/SKILL.md and repair.md) states that every run-scoped intermediate the agent creates — a harness script, a PR-body draft, a file list, a working copy — is allocated under §SP via kp_scratch_open / kp_scratch_path (or a rule-4 single-call mktemp), never a fixed leaf in the harness scratchpad and never a bare temp-root path. The rule is general, not a second branch-name-shaped local instance.
The eight hand-rolled §SP path interpolations under claude-plugins/kampus-pipeline/skills/write-code/scripts/ (tabled above) allocate through kp_scratch_open / kp_scratch_path, so an owner stamp exists and a same-slug concurrent peer is refused rather than silently re-entering.
The wrong-data direction is named where the rule lives: a clobbered draft reads back successfully and plausibly, which is why a lane can post another lane's PR body — including its Fixes #<N> — and nothing errors.
No criterion is satisfied by keying leaf filenames (§SP rule 2), and none is satisfied by a change confined to the one reported artifact.
The PR touching claude-plugins/kampus-pipeline/ carries a human approval at head (§CP).
Six build lanes ran concurrently today and shared one scratchpad namespace; one lane reported sibling lanes overwriting its just-written working files at three distinct moments during its run. No wrong artifact is known to have shipped this time, but the collision itself was real, and only timing — not any guard — kept the outcome clean.
What I was doing
Running as one of six build lanes executing concurrently, doing normal in-lane work: writing a mutation-harness script, drafting a PR body, and writing a progress note into the per-run scratchpad namespace.
What I observed
Across the run, this lane found three of its own scratchpad artifacts overwritten by what appears to be sibling lanes writing into the same namespace: a mutation-harness script, then a PR-body draft, then a progress note. Each of this lane's own writes happened to land after the prior clobber and before the next one, so by luck the artifact it read back and used (including the PR body it ultimately posted) was verified on GitHub to be its own. The writes themselves genuinely collided — this is not a near-miss, just a clean outcome produced by timing nobody controls.
Why it matters
If timing had gone the other way, a lane could read back a sibling's PR-body draft or progress note and post it to the wrong PR or issue, misattributing one lane's work to another — the same shape as the earlier incident where a lane wrote directly to a foreign lane's issue. With concurrency rising (six lanes today), the odds of an incidental clean outcome go down while the collision rate goes up.
Pointers
Related, filed 2026-07-30: Gate skills still write fixed-name scratchpad intermediates, bypassing the shipped allocator #4544 — "Gate skills still write fixed-name scratchpad intermediates, bypassing the shipped allocator" — same collision shape (fixed-name scratch writes clobbered under concurrency), but scoped to review-gate lanes (review-code/review-plan/ship-it), not build lanes. This report may be the same root-cause class occurring on the build-lane side, or a distinct occurrence — worth reconciling.
Related: scratchpad open concurrency test fails only in merge-queue batch: loser exits 6, not 4 #4864 — "scratchpad open concurrency test fails only in merge-queue batch: loser exits 6, not 4" — a separate report of a race in the scratchpad namespace-open path itself (a non-atomic owner-file claim can let a losing racer read a partial owner stamp). May share a root cause with this report's clobber, or may be independent — worth triage determining which rather than assuming.
packages/pipeline-cli/src/tools/scratchpad/ — the scratchpad allocator implementation referenced by both related issues above.
Suggested next step (non-binding)
Reported candidate fix (not evaluated, record only): lane-unique filenames within the scratchpad namespace, so concurrent lanes cannot target the same path even if they otherwise share a namespace. Triage may find this is exactly #4544's class applied to build lanes rather than gate lanes, in which case the fix may already be in flight there.
Filed by an agent · session fcd74bd3-8872-4023-b100-a81ac870eb43 · branch main · 2026-08-08T00:51:53Z
Triage verdict —
type:bug,p1, homed in the Worktree-Isolation Integrity campaign. Every source claim below was read first-hand against this checkout in this run and is cited file+line. Anything not verified here is labelled unverified, not softened. This desk wrote to no other issue; see Outstanding actions.The question this ticket was filed to settle
Does #4544's fix also fix this? No. Same root-cause class, disjoint fix surface — #4544's scope and every one of its acceptance criteria enumerate exactly four skills, and
write-codeis not among them. Folding this into #4544 would silently drop the build path. Detail in Reconciliation, below.What is wrong
The §SP contract promises that concurrent runs cannot name each other's scratch files. On the build path that promise is delivered by convention alone, in two places where it does not hold.
1. Every
write-codescratch write hand-rolls the §SP path instead of allocating through the shipped allocator — so no owner stamp exists and there is no refusal to hit.The allocator's protection is the exclusive-create owner stamp, not the path shape:
openNamespacewrites.kampus-run-ownerwithflag: "wx"(packages/pipeline-cli/src/tools/scratchpad/store.ts:83-85), andclassifyOwnership(packages/pipeline-cli/src/tools/scratchpad/scratchpad.ts:194-199) refuses a same-session stamp carrying a differentCLAUDE_PIDasforeign. The shell entry points that reach it arekp_scratch_open/kp_scratch_path(claude-plugins/kampus-pipeline/lib/common.sh:380-381).write-codecalls neither. Eight committed sites interpolate the §SP path as a literal string andmkdir -pit, which creates no stamp and therefore can never returnForeignNamespace:claude-plugins/kampus-pipeline/skills/write-code/scripts/)step6-progress-comment.shwrite-code-<N>step7-epic-handoff.shwrite-code-<N>stepR3-push-and-note.shwrite-code-<N>stepR-frozen-ac.shwrite-code-repair-<PR>stepR-round-count.shwrite-code-repair-<PR>stepR1-verdicts.shwrite-code-repair-<PR>stepR2-fail-body.shwrite-code-repair-<PR>stepR2-inline-comments.shwrite-code-repair-<PR>Three of those scripts carry an in-file comment conceding the bypass verbatim — "The hand-rolled RUN_SCRATCH derivation duplicates
kp_scratch_path; collapsing it onto that helper is phase 2's call (#1929)" (step6-progress-comment.sh:14,step7-epic-handoff.sh:12,stepR3-push-and-note.sh:15). So the bypass is known and deferred, not accidental.The slugs do carry the work item, so two lanes on two different issues resolve two different directories — that separator holds. What the missing stamp removes is the protection on the cases where the slug is not a separator: a second role on the same lane, a re-run, or a repair round over the same issue number. There the allocator would refuse; the hand-rolled path silently re-enters and clobbers.
2. Nothing binds the intermediates an agent invents mid-run to §SP at all. The three artifacts this report names — a mutation-harness script, a PR-body draft, a progress note — are exactly that class. Two of them have no skill-prescribed path anywhere:
gh pr create --bodyfrom a here-document (claude-plugins/kampus-pipeline/skills/write-code/SKILL.md:1476ff). There is no file in the prescribed shape, so a lane that drafts the body to a file is improvising a path the skill never named.progress.mdinside the run scratch directory under slugwrite-code-<N>(SKILL.md:1550-1560,step6-progress-comment.sh:40).write-code's only §SP instruction is scoped to one instance and says so: the branch-name block atSKILL.md:812-844closes with "This is the local instance of the per-run scratchpad namespace, §SP...". There is no general rule inSKILL.mdorrepair.mdbinding every run-scoped intermediate the agent creates to §SP or to a rule-4mktemp. That is precisely the rule #4544 requires of the four gate skills — and it is what would have prevented this report.The banned surface an improvised write most likely lands on is the harness-provided session scratchpad directory, which §SP explicitly carves out: "A session-scoped scratchpad handed to an agent by the runtime is shared across the concurrent runs of that session, and generic leaf names under it ... collide" (
claude-plugins/kampus-pipeline/skills/gh-issue-intake-formats.md:1985-1991). Six concurrent sibling lanes of one parent session share that directory by construction. The report does not name the concrete directory its three artifacts were written to, so which surface each collision landed on is not verifiable from the board — carried as unverified. The finding does not depend on it: both candidate surfaces are §SP violations at an improvised call site.Why the price is what it is
Priced on the unmitigated failure mode, not the observed outcome. The clean result here was produced by write ordering, which is not a control. The class is silent by construction — a clobbered file reads back successfully, with plausible content, because it is a genuine artifact of a genuine lane. The two live outcomes are one lane overwriting another's state, or one lane adopting another lane's content and posting it: a PR body describing work the diff does not contain, and — because
write-codePR bodies carry a closing keyword (Fixes #<N>,SKILL.md:1502) — a merge that closes the wrong issue.p1and notp0: the direction that makes #4544p0is a gate reading corrupted content and emitting a well-formed false PASS. That direction is absent here — a build-lane artifact swap is misattribution, and the code gate still reads the diff.p1because the fix is a small prose unit that rides an in-flightp0's fix at near-zero marginal cost, and the hazard fires in the factory's normal operating mode (six lanes).Reconciliation — #4544 vs this issue: same class, non-overlapping surface
Both are the same root-cause class: run-scoped intermediates not bound to the §SP allocator. They cover different write sites, and neither fix reaches the other's:
p0)review-code,review-plan,ship-it,review-skill— named in its Scope bullet and in its first acceptance criterionwrite-code(includingrepair.md)Fixes #<N>closes a foreign issueRecommendation: do not fold, and do not make this a child of #4544. #4544's triage explicitly verified its own scope against
origin/mainand fixed it to the four gate skills;write-codewas never in it. The right shape is sibling tickets on one convention: whoever writes #4544's rule should write the identical paragraph intowrite-code/SKILL.mdandrepair.md, and this ticket is what makes that second half tracked rather than assumed. If an implementer takes both at once, this issue is the one that closes on thewrite-codehalf.Relation to #4692 — closest neighbour, and a correction to its sweep
#4692 ("Unkeyed scratchpad filename let a lane commit another lane's commit message",
type:bugp2axis:pipeline-hardening) is the same class at a third build-path call site, and its acceptance criteria already cover the stamp-free no-CLI fallback inlib/common.sh, three fixed-slug allocations, and a mechanical §SP-conformance guard. It is the nearest thing to a duplicate of this issue and an implementer should read it first. Two reasons this is not closed into it:write-codeallocator-bypassing scripts (stepR-round-count.sh,stepR1-verdicts.sh,stepR2-fail-body.sh). The sweep run for this triage finds eight underskills/write-code/scripts/(table above) — the five additional ones (step6-progress-comment.sh:40,step7-epic-handoff.sh:39,stepR3-push-and-note.sh:53,stepR-frozen-ac.sh:28,stepR2-inline-comments.sh:29) are not in Unkeyed scratchpad filename let a lane commit another lane's commit message #4692's list, and three of them sit on the non-repair main path. Closing this as a duplicate would carry that undercount forward.Relation to #4864 — distinct mechanism, confirmed
Distinct. This refutes the "may share a root cause" hedge in the original report. #4864 is about the inside of the allocator; this issue is about writes that never reach it.
wxwrite atstore.ts:83-85makes the create exclusive but not the content write, so a loser hittingEEXISTcan read a zero-byte stamp,parseOwnerreturnsnull(scratchpad.ts:171-183), and the loser takes theNamespaceUnavailablebranch (store.ts:90-96, exit 6) instead ofForeignNamespace(exit 4).write-codesites ontokp_scratch_openincreases traffic through scratchpad open concurrency test fails only in merge-queue batch: loser exits 6, not 4 #4864's claim path. That is a sequencing note, not a shared cause.#4864 is claimed and under active build. This desk proposes no change to it and did not write to it.
Relation to #4516 — upstream, not the same defect
#4516 ("Per-run scratch namespace's session key is shared across lanes and roles",
type:bugp1) is the keying defect: the session id does not distinguish concurrent sibling lanes. It is upstream of this issue and does not subsume it — thewrite-codeslugs already carry the work item (write-code-<N>), so lane separation on the build path survives a shared session id. What #4516 fixes is the residual same-lane / cross-role collision; what this issue fixes is the sites that have no stamp and the writes that have no rule. A fix here should adopt #4516's chosen lane identity rather than inventing a second one.Scope note for the implementer
claude-plugins/kampus-pipeline/skills/write-code/is control-plane, so the prose half rides the §CP review/merge path with a human approval at head.packages/pipeline-cli/src/tools/scratchpad/is not touched by this ticket's fix.gh-issue-intake-formats.md:2063-2078). The original report's non-binding suggestion ("lane-unique filenames within the scratchpad namespace") is the fix the contract already rejects — recorded here so it is not re-bred.mktemp).Acceptance criteria
write-code's own body (claude-plugins/kampus-pipeline/skills/write-code/SKILL.mdandrepair.md) states that every run-scoped intermediate the agent creates — a harness script, a PR-body draft, a file list, a working copy — is allocated under §SP viakp_scratch_open/kp_scratch_path(or a rule-4 single-callmktemp), never a fixed leaf in the harness scratchpad and never a bare temp-root path. The rule is general, not a second branch-name-shaped local instance.claude-plugins/kampus-pipeline/skills/write-code/scripts/(tabled above) allocate throughkp_scratch_open/kp_scratch_path, so an owner stamp exists and a same-slug concurrent peer is refused rather than silently re-entering.Fixes #<N>— and nothing errors.claude-plugins/kampus-pipeline/carries a human approval at head (§CP).Outstanding actions this desk did not take
write-codehalf — five further allocator-bypassing sites are listed above; (c) scratchpad open concurrency test fails only in merge-queue batch: loser exits 6, not 4 #4864 needs a sequencing note only, and is under active build, so nothing is proposed for it.Original report (verbatim)
Summary
Six build lanes ran concurrently today and shared one scratchpad namespace; one lane reported sibling lanes overwriting its just-written working files at three distinct moments during its run. No wrong artifact is known to have shipped this time, but the collision itself was real, and only timing — not any guard — kept the outcome clean.
What I was doing
Running as one of six build lanes executing concurrently, doing normal in-lane work: writing a mutation-harness script, drafting a PR body, and writing a progress note into the per-run scratchpad namespace.
What I observed
Across the run, this lane found three of its own scratchpad artifacts overwritten by what appears to be sibling lanes writing into the same namespace: a mutation-harness script, then a PR-body draft, then a progress note. Each of this lane's own writes happened to land after the prior clobber and before the next one, so by luck the artifact it read back and used (including the PR body it ultimately posted) was verified on GitHub to be its own. The writes themselves genuinely collided — this is not a near-miss, just a clean outcome produced by timing nobody controls.
Why it matters
If timing had gone the other way, a lane could read back a sibling's PR-body draft or progress note and post it to the wrong PR or issue, misattributing one lane's work to another — the same shape as the earlier incident where a lane wrote directly to a foreign lane's issue. With concurrency rising (six lanes today), the odds of an incidental clean outcome go down while the collision rate goes up.
Pointers
packages/pipeline-cli/src/tools/scratchpad/— the scratchpad allocator implementation referenced by both related issues above.Suggested next step (non-binding)
Reported candidate fix (not evaluated, record only): lane-unique filenames within the scratchpad namespace, so concurrent lanes cannot target the same path even if they otherwise share a namespace. Triage may find this is exactly #4544's class applied to build lanes rather than gate lanes, in which case the fix may already be in flight there.
Filed by an agent · session
fcd74bd3-8872-4023-b100-a81ac870eb43· branchmain· 2026-08-08T00:51:53Z