Files
msd-core/tests
Tom Boucher e48eb44003 fix(#1856): give the executor-worktree refusal a handoff instead of a dead end (#2727)
* test(#1856): failing-first contract for the orchestrator cwd-drift guard handoff

The #48 guard correctly refuses to execute waves from an agent worktree, but the
refusal is a dead end: that worktree can hold committed fixes AND uncommitted
work, and "re-run from the orchestrator's own worktree" silently means
abandoning them. The reporter was left choosing between continuing from a
blocked worktree and losing the work.

The guard is shell embedded in execute-phase.md, so these tests extract the
block by a stable marker and EXECUTE it against real git fixtures — the shipped
text is the runtime contract. Covers the stranded-commit and dirty-tree report,
the integration commands, both agent- namespaces, commit-count boundaries 0/1/2,
and the constraints the guard's own comment records: it must NOT fire on an
ordinary branch, on 'agentic-refactor', or on a legitimate feature worktree under
.claude/worktrees/, and must degrade cleanly with no resolvable base or a
detached HEAD.

RED expected: the marker does not exist, so extraction fails and every case errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* fix(#1856): give the executor-worktree refusal a handoff instead of a dead end

The #48 cwd-drift guard correctly refuses to execute waves from an agent
worktree — its comment records why ("this is how a wrong-base merge nearly
shipped ~1000 files"), and that refusal is untouched here. The defect is that it
was a dead end.

At the moment it fires, the worktree can hold committed product work, uncommitted
product and planning changes, and the live gap-planning context. Telling the user
to "re-run from the orchestrator's own worktree" silently means abandoning all of
it, because the orchestrator worktree cannot see commits that live only on the
agent branch. The reporter was left choosing between continuing from a blocked
worktree and losing five commits plus uncommitted work.

The refusal now reports what is actually stranded — the commit count and log
against the resolved base, and the uncommitted files — followed by the concrete
integration sequence (commit here, switch to an orchestrator-safe checkout,
merge or cherry-pick, re-run) and a verify command. Nothing is claimed that is
not there: a clean worktree with no commits ahead prints the plain refusal with
no empty sections.

Every added command is diagnostic and `|| true`-guarded, so a failure degrades to
the original refusal rather than crashing before the message prints. Verified: an
unresolvable base still refuses cleanly.

Deliberately NOT done: auto-merging or auto-cherry-picking the agent branch. That
is precisely the operation #48 exists to stop the orchestrator performing from a
drifted cwd, at the moment it has least confidence about which tree is which.
Reporting beats acting here.

The guard block carries a `gsd:guard=orchestrator-cwd-drift` marker so the new
contract test can extract and EXECUTE the shipped shell against real git
fixtures rather than asserting on its characters.

Fixes #1856

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* fix(#1856): report true counts, add the changeset, and document the seam

Review findings, all from the isolated adversarial pass:

- The dirty-file list was capped at 20 with no indication, so a worktree with 27
  uncommitted files reported 20 — under-informing the user about exactly what is
  stranded, which is the entire point of this change. Both lists now count BEFORE
  truncating and print "… and N more". Verified with 25 commits / 27 dirty files.
- The has-commits condition was written out twice and could drift on a future
  edit. Collapsed to a single _WT_HAS_COMMITS flag.
- The changeset fragment existed but was untracked, so it was in neither commit
  on this branch and the PR gate would have failed against real history.
- CONTEXT.md:122 documents this exact seam ("the orchestrator runs a cwd-drift
  guard at execute_waves entry…") and was not extended. Now records the handoff
  report, that the refusal condition and exit code are unchanged, and that every
  added command is diagnostic and || true-guarded.

Verified NOT a defect, correcting the review's premise: the guard block does break
when its line endings are CRLF, but .gitattributes:2 is `* text=auto eol=lf`,
which OVERRIDES core.autocrlf and forces LF on checkout on every platform
including Windows — so the shipped file is LF there too, and the installer copies
it through Node without translating endings. The reproduction (mine and the
reviewer's) required injecting CRLF by hand. It is also not fixable from inside
the script: a \r breaks the shell parse at the block's first line, before any
#1856 code runs. Neither introduced nor amplified by this change.

Also noted and left as-is by design: the review flagged that #1856's "offer an
explicit recovery option" could be read as requiring an interactive/automated
integration rather than printed instructions. Deliberate — see the commit that
added the block: auto-merging is the exact operation #48 exists to prevent the
orchestrator performing from a drifted cwd. Called out in the PR body for a
maintainer decision rather than silently chosen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* chore(#1856): backfill changeset PR number

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:32:00 -04:00
..