Skip to content

docs(backlog): file the shared-scratchpad commit-message hazard (BACKLOG #1440) - #849

Merged
wshallwshall merged 5 commits into
mainfrom
claude/dazzling-jang-df503f
Sep 5, 2026
Merged

wshallwshall merged 5 commits into
mainfrom
claude/dazzling-jang-df503f

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Files one backlog item and narrows one existing CLAUDE.md bullet. No engine code, no test file, no workflow change.

What went wrong

A session wrote a commit message to <scratchpad>/msg1.txt, then ran git commit -F <scratchpad>/msg1.txt. Between those two steps another agent overwrote that file with its own message, for a different backlog item. The commit would have carried another item's subject and body. The substitution is invisible in git commit -F output.

The ledger claim gate refused the commit, because the substituted subject cited a number this worktree did not hold. That gate exists for an unrelated reason, and it is the only reader in the chain that compares the message's content against something the session independently knows about itself.

The original framing was wrong, and the row corrects it

The finding was filed as "concurrent sessions in the same project resolve to one directory". They do not. The session-UUID segment partitions top-level sessions correctly. What it does not partition is a session from its own subagents and background tasks, all of which are handed the same path and told it is "session-specific, isolated".

Three measurements, in the row:

  • Direct. A marker was written into this session's scratchpad, then a subagent was spawned and asked to quote the scratchpad path from its own system prompt. It returned this session's exact path, listed the directory, found the marker, and wrote beside it. Had the directories differed, the parent's file could not have appeared in its listing.
  • The opposite control. If the UUID segment did nothing, every project slug would hold one child. The busiest holds 209.
  • Structural. The slug is a function of the working directory, so two seats in different worktrees cannot collide by construction.

A peer manager seat corroborated this independently at n=5 by listing its own scratchpad after four hours of ordinary work: 22 files, one of them its own, the rest deposited by five agents it had spawned. That is the reproduction most readers should use, because it needs ls rather than a deliberately spawned subagent.

The isolation is asymmetric, which is why the mistake is easy

A subagent gets its own git worktree, branch and .venv. Every cheap signal it can consult says isolated. The one directory that is actually shared is the one it is told is private.

Remedy

Write any file whose content is later fed to a command into your own worktree, under a name that cannot collide, and delete it after. The CLAUDE.md bullet already said "inside the project"; it now says which directory, names the scratchpad as the wrong place, and gives the reason.

The per-worktree git directory looks like the ideal home and is not: it sits under the primary checkout's path, so worktree_gate.ps1 refuses a Write there. That was found by trying it, after a first draft of both the row and the CLAUDE.md line recommended it.

The scratchpad mechanism itself is untouched. It is harness behaviour, not repository behaviour.

Checks run

Run here and passing: backlog_status_check (429 live items, each declaring exactly one status), forbidden-content leak scan, dangling_citation_check, backlog_citation_check, claude_section_check, ledger_check, plus the full pre-commit hook set on every commit. Targeted pytest: 143 passed across test_backlog_status_check, test_backlog_citation_check, test_ledger_check, test_link_resolution, test_cp1252_console_safety.

Not run here, and needing a reader: every hosted-runner leg. This worktree has no .venv, so the tests above ran on a sibling interpreter and the extras-gated modules (fhir, dicom, x12, xml, webauthn) were never collected. That is fine for a documentation change and is stated rather than implied.

verdict_divergence_check exits 1 on this branch. It also exits 1 on main: the three divergent items are #340, #1003 and #1266, all present on main and untouched here, and this item carries no prose re-score. The script is neither a pre-commit hook nor referenced by any workflow.

Conflict resolution

main took ten commits touching docs/BACKLOG.md while this branch was held, and both sides append at the tail. Resolved by taking main's file whole and re-appending the #1440 block after #1439. No existing item was edited; the diff against main is CLAUDE.md and the new item only.

Notes for the reviewer

  • The two colliding paths are deliberately not published in the row. They carry a host account name and worktree slugs; the forbidden-content gate blocked the first commit on exactly those lines, and its own message says a real host string must be removed rather than allowlisted. The row publishes the schematic form and says why.
  • One measured claim in the row is second-hand and marked as such: the reported write-then-clobber sequence. The residue on disk was read directly and confirms it.
  • BACKLOG #1440 was allocated with scripts/coord/alloc.ps1 and is claimed by this worktree.

🤖 Generated with Claude Code

wshallwshall and others added 4 commits September 3, 2026 19:00
…LOG #1440)

A session wrote a commit message to <scratchpad>/msg1.txt and ran `git commit -F`
against it. A concurrent agent overwrote the file in between, so the commit would
have carried another item's subject. The claim gate refused it, catching a defect
it was not built for.

The original framing said concurrent SESSIONS share the directory. They do not:
the UUID segment partitions top-level sessions, one slug carrying 209 of them.
What shares it is a session and every subagent it spawns -- confirmed by spawning
one, which quoted this session's exact path from its own system prompt and read
back a marker only the parent had written.

The isolation is asymmetric: subagents get their own worktree, branch and .venv,
so every cheap signal says isolated while the shared directory is the one
advertised as private.

CLAUDE.md sec. 5 now says to write the message inside your own worktree, names
the scratchpad as the wrong place, and gives the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A peer seat reviewing the row listed three candidate sharing conditions. Two were
already measured and named: same-worktree sessions cannot collide because the
UUID partitions them, and subagents inheriting the parent path is the measured
cause. The third, a resumed session reusing its UUID, was not measured and is now
recorded as an open candidate. It cannot produce the observed failure, since a
session is not concurrent with itself.

Also records how to reproduce, because the first framing did not. A reader who
lists their own project directory finds one UUID and correctly concludes nothing
is shared between seats; that is not a refutation, because the collision is inside
one session. The reproduction is to write a marker and spawn a subagent to list
the same directory.

States why the two colliding paths are not published: they carry a host account
name and worktree slugs, which the forbidden-content gate blocks and which its own
message says to remove rather than allowlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o #1440

A peer manager seat observed the same sharing from the parent side, without
running an experiment: it listed its own scratchpad after four hours of ordinary
work and found 22 files, one of which it had written. At least five agents it had
spawned deposited the rest.

Two things that adds. Those builders each ran in their own git worktree on their
own branch and still wrote into the parent's scratchpad, so the row's asymmetry
claim is now observed at n=5 rather than inferred at n=1. And a near miss on a
generic filename that is not a commit message: one fullsuite.log existed while at
least two builders reported running full suites. Whether one overwrote the other
is not established and is not claimed; the collision surface is.

Also records this as the reproduction most readers should use. Measurement A needs
a subagent spawned deliberately. This needs ls.

The peer's measured/inferred boundary is preserved: the listing and mtimes are
measured, the per-agent attribution is inferred from filenames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main took ten commits touching docs/BACKLOG.md while this branch was held, and both
sides append at the tail, so the file conflicted. Resolved by taking main's file
whole and re-appending the #1440 block after #1439. No existing item was edited and
no line above #1440 differs from main.

CLAUDE.md merged without conflict: main has not touched that file since this
branch's base.

Verified after resolution: no conflict markers remain, backlog_status_check reports
429 items in the live ledger each declaring exactly one status, and the leak,
dangling-citation and claude-section gates pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ci-red A required check went red. Attribute it before retrying. label Sep 5, 2026
Resolved by the Lander. The ledger tail serialising: this branch appends
its item, main appended another. Different items, so both sides stand,
with main's block in the position it holds on main.

Set-difference control over item numbers: nothing lost, nothing invented,
no duplicate headings. #1069 is absent because main archived it at
7f97202, not because this merge dropped it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the unread Green and mergeable, but not marked read. Read it, then add: reviewed label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

@wshallwshall

This pull request is green and nobody has marked it read.

Every check that has reported is passing and the reviewed label is absent, so
a reviewer has read this is the only thing between it and a merge. Nothing else reports that, which is
why this comment exists (BACKLOG #1413).

To clear it, IN THIS ORDER:

  1. If the branch is behind main, update it FIRST: gh pr update-branch --repo MEFORORG/MessageFoundry 849.
    That push fires synchronize, and synchronize REMOVES the reviewed label. Labelling
    before updating throws the label away and costs a round trip.
  2. Read the diff.
  3. gh pr edit 849 --repo MEFORORG/MessageFoundry --add-label reviewed

DO NOT TRIAGE THIS FROM mergeStateStatus. It returns one value with precedence, so BEHIND,
DIRTY and UNSTABLE each mask the missing check -- the requirement is invisible until you act on
something else. This check never reads that field; see scripts/ci/check_unread_prs.py.

A seat can find every pull request in this state in one call: gh pr list --label unread.
The label is withdrawn automatically once this pull request leaves the state.

The reviewed label is a PROCESS gate. It records that a step happened. It does not
establish that an independent party looked, and nothing here should be read as saying it does.

@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 5, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 5, 2026
@github-actions github-actions Bot removed the unread Green and mergeable, but not marked read. Read it, then add: reviewed label Sep 5, 2026
Merged via the queue into main with commit 00afe89 Sep 5, 2026
42 of 43 checks passed
@wshallwshall
wshallwshall deleted the claude/dazzling-jang-df503f branch September 5, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-red A required check went red. Attribute it before retrying. reviewed A reviewer has read this. Removed automatically when new commits arrive.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant