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
Fork a Claude conversation into a new worktree (#237)
## Summary
Lets a new worktree be seeded with an **existing Claude conversation**
instead of a hand-written summary prompt, so the new agent resumes
actually holding what was said rather than being briefed about it.
Two entry points:
- **UI** — a "fork this conversation" option on the new-worktree screen,
launched from a Chat tab.
- **MCP** — `create_worktree` gains `forkConversation: true`. Always
self-scoped: for Chat tabs the bridge's terminal id *is* the session id,
so both fork inputs are derived from the request header. Rejected up
front (before any git work) for terminal tabs, callers with no
transcript, and any combination with `prNumber`.
### The relocation preamble
The load-bearing piece. A forked transcript is full of edits to a
worktree the new agent isn't in, and the new branch is cut from the base
ref — so it carries neither the uncommitted work nor, usually, the
commits. `fork-relocation.ts` prepends a note stating where the agent is
now, where the history happened, and **what actually survived** —
computed from git, not templated. When git can't answer, it says so
explicitly rather than guessing.
## Verification
Forked a real conversation into a real worktree over both paths and read
what the resumed agent did.
The forking agent's `initialPrompt` asserted a plan file *would* be
present ("the commit is on main, so it should be there"). The forked
agent didn't take it on faith — it checked, found the file missing, and
diagnosed it exactly: *"this branch was cut from `7937032`, one commit
behind main, so commit `41da774` didn't come along."* It then verified
with `merge-base --is-ancestor`, fast-forwarded, re-read the plan, and
got to work. The preamble's "check before you build on anything you
edited earlier" did its job — and caught a wrong assumption the agent's
own past self had written.
The first end-to-end run **failed on the tool description, not the
plumbing**: given an explicit continuity request the agent still
hand-wrote a briefing, reasoning that a forked transcript "would mostly
be noise." The old decision test asked whether it *could* write a
sufficient briefing — a diligent agent always answers yes, so that test
only ever resolved to "don't fork." Replaced with "ask what the briefing
would have to *contain*" (`d32ce04`); the agent then forked on the
identical prompt.
## Test plan
- [x] `npm run typecheck`
- [x] `npx electron-vite build`
- [x] `npx vitest run` — new coverage in `fork-transcript.test.ts`,
`fork-relocation.test.ts`, `control-server.test.ts`,
`worktrees-fsm.test.ts` (pre-existing unrelated failures in `path-fix` /
`git-ops-state` only)
- [x] End-to-end fork over the UI path
- [x] End-to-end fork over the MCP path — matching `fork wrote` /
`spawn` session ids
- [x] Rejection paths: terminal tab, no transcript, `prNumber`
combination
## Notes for review
- API is `forkConversation: true` (self-scoped boolean) rather than a
caller-supplied session id — an agent can't reliably observe its own
session id, and cross-worktree forking would need a session→worktree
index nothing currently asks for. Easy to widen later.
- The MCP `POST /worktrees` branch path bypasses `WorktreesFSM` and
calls `addWorktree` directly, so it has no pending-list entry to guard
the async seeding window. `seedingWorktreePaths` claims the path instead
— without it the pane sweep re-opens the race fixed in `4dcf8b1`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: resources/mcp-bridge.js
+57-11Lines changed: 57 additions & 11 deletions
Original file line number
Diff line number
Diff line change
@@ -123,11 +123,18 @@ function callControl(method, path, body) {
123
123
})
124
124
}
125
125
126
+
// Appended to create_worktree's description, and removed again by
127
+
// stripForkAffordance when the feature is off. Kept as its own constant so the
128
+
// two stay in sync — a literal that drifts would silently stop being stripped.
129
+
constFORK_DESCRIPTION_SENTENCE=
130
+
' The new tab normally starts as a blank conversation seeded with initialPrompt; set forkConversation to instead hand it a copy of THIS conversation to continue from.'
131
+
126
132
constTOOLS=[
127
133
{
128
134
name: 'create_worktree',
129
135
description:
130
-
"Create a new git worktree in a Harness-managed repo. Either create a brand-new branch (set branchName) OR check out an existing GitHub PR for review (set prNumber). Harness will open a new agent chat tab inside the new worktree automatically. Defaults to the caller's current repo when repoRoot is omitted.",
136
+
"Create a new git worktree in a Harness-managed repo. Either create a brand-new branch (set branchName) OR check out an existing GitHub PR for review (set prNumber). Harness will open a new agent chat tab inside the new worktree automatically. Defaults to the caller's current repo when repoRoot is omitted."+
137
+
FORK_DESCRIPTION_SENTENCE,
131
138
inputSchema: {
132
139
type: 'object',
133
140
properties: {
@@ -172,6 +179,11 @@ const TOOLS = [
172
179
type: 'string',
173
180
description:
174
181
'Optional display alias applied to the new worktree once creation succeeds. Same semantics as set_worktree_alias — trimmed and clamped to 80 chars, empty string is ignored. Useful when the user gave the task a memorable label ("call this one auth-refactor") so the sidebar/window title show that instead of the branch name.'
182
+
},
183
+
forkConversation: {
184
+
type: 'boolean',
185
+
description:
186
+
'Copy YOUR current conversation into the new worktree, so its agent resumes holding everything said here instead of starting blank. Only works when you are a Harness Chat tab with existing history; otherwise the call is rejected and you should retry without it. Cannot be combined with prNumber.\n\nFORK when the new worktree continues THIS thread of work. Strongest signal: the user asks for continuity — "pick up where we left off", "they should already know what we discussed", "carry on from here". Take that at face value; it is a request to fork, and answering it with a hand-written briefing instead is the wrong call. Also fork when the work leans on things that only exist in this conversation: what you already read and ruled out, why the user rejected an earlier approach, a design the two of you converged on over several turns.\n\nDO NOT FORK for a task that merely sits next to this one ("also fix the flaky test", "do the same on the other service"), for a clean retry after an approach failed, or for reviewing someone else\'s code. There the history is noise the new agent must read past.\n\nDeciding: do not ask yourself whether you COULD write a sufficient briefing — you almost always can, so that question always answers "no fork" and is useless. Ask instead what the briefing would have to contain. If it needs to relay specific findings, discarded options, or user decisions from this conversation, fork: the transcript already holds those, faithfully, and your summary of them will be lossier than you expect. If it would just be a task description someone could have written before this conversation started, do not fork.\n\nCost of forking, so you can weigh it: the transcript is full of your earlier file edits, but the new branch is cut from the base ref, so it does NOT contain your uncommitted work and may not contain your commits. Harness prepends a note telling the new agent where it now is and which of those changes actually survived, and it will spend a little effort re-verifying before it builds.\n\nWhen you fork, still pass initialPrompt — it lands right after that note and is what actually directs the new agent. Write it as a continuation ("now build the page we just planned, here") and do not re-explain what the conversation already contains.'
175
187
}
176
188
}
177
189
}
@@ -523,16 +535,29 @@ const FULL_CONTROL_BROWSER_TOOLS = new Set([
523
535
'show_cursor'
524
536
])
525
537
526
-
letcachedBrowserPerms=null
527
-
asyncfunctiongetBrowserPerms(){
528
-
if(cachedBrowserPerms)returncachedBrowserPerms
538
+
// One /scope fetch backs every capability gate below. Defaults on failure are
539
+
// permissive for browser tools (pre-existing behaviour) but the fork gate
540
+
// defaults off — the setting is opt-in, and a server that doesn't report it
541
+
// would reject the call anyway, so advertising it would only waste a turn.
? ' It resumes a copy of this conversation, and has been told where it is and which of your earlier changes came along.'
625
+
: ''
582
626
returnprNumber
583
627
? `Created worktree ${r.path} on branch ${r.branch} for PR #${prNumber}${aliasSuffix}. Harness will open a new ${agentLabel} chat tab in it${modelSuffix}.`
584
-
: `Created worktree ${r.path} on branch ${r.branch}${aliasSuffix}. Harness will open a new ${agentLabel} chat tab in it${modelSuffix}.`
628
+
: `Created worktree ${r.path} on branch ${r.branch}${aliasSuffix}. Harness will open a new ${agentLabel} chat tab in it${modelSuffix}.${forkSuffix}`
0 commit comments