Skip to content

A subagent wrote into another ticket's live worktree, and its recorded cwd said otherwise #2334

Description

@hdkiller

Problem & Context

A worker subagent was dispatched for CLNT-2981 with cwd set to /home/hdkiller/Develop/.worktrees/legalease/CLNT-2981 and an explicit instruction block:

Do NOT touch any other path — not the main checkout at ~/Develop/legalease, not any other ~/Develop/.worktrees/legalease/* worktree. You write exactly one file (see Owned Paths).

Owned Paths — exactly one file

  • legalease/main/tests_e2e/test_case_research.py

It did none of that work. Its tool history (recentTools in the run's status.json) is entirely inside /home/hdkiller/Develop/.worktrees/legalease/CLNT-2979 — a different ticket's worktree, concurrently owned by the parent session:

bash cd ~/Develop/.worktrees/legalease/CLNT-2979 && sed -n '16...
bash cd ~/Develop/.worktrees/legalease/CLNT-2979 && rg -n "Use...
read /home/hdkiller/Develop/.worktrees/legalease/CLNT-2979/doc...
read /home/hdkiller/Develop/.worktrees/legalease/CLNT-2979/bin...
write /home/hdkiller/Develop/.worktrees/legalease/CLNT-2979/bin/test.sh
chmod +x bin/test.sh
bash cd ~/Develop/.worktrees/legalease/CLNT-2979 && for i in 1 2; do ...

Concretely it overwrote bin/test.sh in CLNT-2979's worktree with its own implementation, at a moment when the parent session was mid-verification of its own version of that file. The parent only noticed because the script it then executed behaved differently from the script it had written — the bash -x trace showed variables (VENV_PY, CORES, .testdb/) that do not exist in the parent's version.

Nothing was lost this time: the parent re-read the file, took the two ideas worth taking, and rewrote it. But the failure mode is exactly the one the worktree rules exist to prevent — two writers on one worktree, with the loser being whichever wrote first and said nothing.

Notable details:

  • The child's own status showed Running, cwd /home/hdkiller/Develop/.worktrees/legalease/CLNT-2981 — i.e. the reported cwd disagreed with every path in its tool calls, so nothing in the run metadata surfaced the drift.
  • The child never touched its actual ticket's worktree; git status there was clean at the end. So the failure was total non-delivery plus foreign mutation, not partial work.
  • It had been running ~25 minutes and had spent most of it on the wrong ticket before being stopped. There is no signal in the subagent status output that would have caught this earlier; the parent found out by diffing the artifact it produced.
  • Recovery required stopping the run (stop, since interrupt is unsupported for async workflows), auditing both worktrees, and rebuilding the file from the parent's version.

Why it matters

The floor requires "one writer for the same cwd/worktree" and "stay inside Owned Paths". Both are load-bearing for parallel dispatch, and both were silently violated by a child that reported a correct cwd. A write that lands in another live worktree is indistinguishable from the owner of that worktree writing it — nothing downstream can tell them apart, and the parent has no way to detect the clobber other than noticing its own file changed.

What would have caught it

  • Refusing a child write/edit whose resolved path is outside the worktree the child was launched in, or outside a declared Owned Paths set.
  • A child tool-call assertion on every mutating call: does this path's worktree match my launch cwd?
  • Surfacing "child wrote outside its launch worktree" in the run's status/events rather than only in raw recentTools history.

Acceptance Criteria

  • A child subagent cannot write/edit outside the worktree it was launched in without an explicit opt-in, or such a call is recorded as a visible run event.
  • The run's status surface reports when a child's mutating tool calls resolve outside its launch cwd/worktree, so a supervising parent does not have to diff its own artifacts to find out.
  • A regression test covers a child launched with cwd: <worktree A> attempting to write a path under <worktree B> — asserting the call is refused or flagged, not silently applied.

Source File Pointers

  • pi-subagents run metadata: status.json steps[].recentTools[] is where the foreign paths were visible; steps[].cwd reported the correct worktree throughout.
  • The subagent tool's execution contract (cwd, worktree, acceptance) — the enforcement point.
  • factory/tools/ticket.mjs claim performs the read-back that is the inter-ticket concurrency control; this is the intra-session equivalent that is missing.

Owned Paths

  • pi/pi-subagents/**

Verification Command

Manual, or a test in the pi-subagents package: launch a child with cwd in one worktree and a task instructing it to write into another, and assert the write is refused or surfaced as an out-of-scope event.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    source:agentDiscovered by an agenttype:bugSomething is broken

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions