Agent Worktree Tool#
When an agent runs high-risk changes inside the user's real code repository, it works in an isolated temporary directory: good changes merge back into the mainline, bad ones are discarded in one wipe, and the main repository is left untouched. Underneath this is a wrapper around
git worktree add/git worktree remove, kept strictly separate from OpenProgram's own session-git.
The skeleton — switch cwd, run a state machine, keep or discard on exit — follows Claude Code's
EnterWorktreeTool / ExitWorktreeTool
(references/claude-code-leaked/src/tools/EnterWorktreeTool/), adapted to OpenProgram's
runtime / session model.
Part 1. Design Dimensions#
D1. What a Worktree Entity Stores#
Each active worktree is one record, with fields:
id: short worktree id (hex, same style as commit ids)source_repo: the user's real repository root (absolute path)worktree_path: the directory created bygit worktree add(absolute path)branch_name: the branch name corresponding to the worktree (defaults toop/wt/<id>)base_ref: the baseline ref at creation time (defaults toHEAD, can be set to origin/main / commit sha)created_at: unix timestampstatus:active/committing/merged/discarded/keptparent_session_id: the associated OpenProgram session (one-to-one or one-to-many)parent_task_id: the associated async task (if any)created_by_agent: agent id (records which agent opened it, so it is visible in the UI)pr_number: set when the worktree was opened viaworktree_create'sprparameter (D5b); lets a laterpr-mode call detect a duplicate instead of opening a second worktree for the same PR
Records are persisted in the session-git repository at worktrees/<id>.json, stored alongside ContextCommit.
They are not cleaned up automatically when the session closes; they wait for the agent or user to explicitly merge / discard.
D2. cwd Switching Mechanism#
OpenProgram's tools fall into two categories:
- Runtime-spawned subprocess (Codex CLI / Claude CLI, which take a
--cdargument) controlled viaruntime.set_workdir(path). - In-process
@functiontools (bash / edit / write / read, etc.) bash goes throughget_active_backend().run(...); currentlyLocalBackend.runaccepts acwdargument but callers don't pass it; edit / write / read require absolute paths.
A ContextVar _current_worktree_path: Optional[str] in openprogram/functions/_runtime.py carries the
active path. Each time the dispatcher enters a turn, if the session currently has an active worktree
(read from session meta), it sets this var. Tool implementations consume it as needed:
- bash:
LocalBackend.run(cmd, cwd=_current_worktree_path.get()) - edit / write / read: resolve relative paths against
_current_worktree_pathas the root; absolute paths must be under the worktree (D6 security check). - runtime subprocess:
apply_default_workdir(runtime, session_id)prefers the worktree path (if any), otherwise session-git'sworkdir/.
There is no explicit cwd argument. The worktree is session-level context; tools are unaware of it.
D3. State Machine#
create
│
▼
┌─ active ─┐
│ │
merge │ │ discard
│ │
▼ ▼
merged discarded
│ │
└────┬─────┘
│ keep
▼
kept (user decides to keep the branch but neither merge nor delete it)
active: the agent is using it. Tools like bash / edit default their cwd here.committing: a brief state, holding a lock during the merge operation to prevent concurrent file edits.merged:git mergesucceeded, the worktree directory has beengit worktree removed.discarded:git worktree remove --forcesucceeded, the branch was deleted too.kept: the user went throughworktree_keep— the worktree directory is kept, OpenProgram unbinds this record but does not touch git. The user takes over from there.
D4. Isolation from OpenProgram session-git#
OpenProgram has its own ~/.openprogram/sessions/<sid>/ (one git repo per session),
storing the conversation memory's history / context / workdir. The agent worktree is never
created inside this directory tree:
- worktree_path must not be under any
~/.openprogram/sessions/*(D14 check). - source_repo must not equal a session-git repository path.
- session-git commits and worktree commits are managed independently; in the UI the ContextCommit timeline only looks at session-git, while the worktree timeline is shown in a separate panel.
An earlier sub-agent worktree mechanism created worktrees under <session-repo>/_worktrees/<branch>/
and was later replaced by "sub-agent = peer session + attach". This design does not reuse that path —
that one opened a branch inside session-git to run a sub-agent, whereas this one opens a worktree
in the user's real code repository for the agent to run changes. The purposes are unrelated.
D5. Where source_repo Comes From#
Three entry points, in descending priority:
- Explicitly passed by the agent: the
source_repoparameter of the worktree_create tool (absolute path). Suitable when a plan agent already knows the target repository while listing tasks. - The fn-form "Working in a folder": the
_work_dirthe user supplies to the program in the web UI. Before entering a turn, if the dispatcher finds that this path is a git repo root, it treats it as the default source_repo (used when worktree_create is called without source_repo). - The ancestor git root of the session's current cwd: resolved via
git rev-parse --show-toplevel. Usually the directory from which the user launched OpenProgram.
If all entry points fail → worktree_create reports the error source_repo_not_a_git_repo.
It will not automatically git init a repository for the user (too destructive).
D5b. Opening a Worktree Directly From a PR#
worktree_create's pr parameter opens a worktree on a pull request's branch instead of
base_ref — accepts a bare number (123), a #-prefixed number (#123), or a full GitHub PR
URL (https://github.com/<owner>/<repo>/pull/<number>). Parsing lives in
openprogram/worktree/pr_ref.py::parse_pr_ref. This reuses the same worktree_create tool and
the same WorktreeManager.create_worktree code path as every other worktree — pr is a mode
switch on that one entry point, not a second tool.
Resolution, via the gh CLI (openprogram/worktree/pr_ref.py):
gh pr view <n> --json headRefName,headRepositoryOwner,isCrossRepositoryreads the PR's head branch and whether it lives on a fork.- Same-repo PR (
isCrossRepository=false):git fetch origin <headRefName>:<local_branch>— the branch already exists onorigin. - Fork PR (
isCrossRepository=true):git fetch origin pull/<n>/head:<local_branch>— GitHub's synthetic per-PR ref, the same mechanismgh pr checkoutuses, so no remote has to be registered for the contributor's fork. - The fetched commit becomes
base_reffor a normalgit worktree add; the worktree's own branch is namedop/wt/pr-<n>-<id6>(sameop/wt/<slug>-<id6>convention as every other worktree, with the PR number standing in for the label). The throwaway fetch ref is deleted once-bhas copied its tip onto the new branch.
gh missing or unauthenticated raises pr_ref_error: gh_not_found / gh_not_authenticated —
never a silent fallback to base_ref. A non-terminal worktree already open on the same PR number
(Worktree.pr_number) raises pr_worktree_exists, naming the existing worktree's id and path,
instead of creating a duplicate.
D6. Security / Permissions#
The core security constraint for agent tools inside a worktree: the bash command's cwd is locked, but the cmd
itself can cd .. to run outside the worktree. This is not a true sandbox; it sets the default location.
Two mitigations:
- Absolute path check: when edit / write / read receive a
file_paththat falls outside worktree_path, log a warning into the ContextCommit metadata (outside_worktree=true), but do not block — the user may genuinely want to read a system config. - bash's cwd is always worktree_path: even if the LLM writes
cd /tmp && rm -rf X, the starting point is worktree_path, the shell session is not persistent (each bash is a fresh subprocess), and the next bash returns to worktree_path.
Out of scope: chroot / namespace isolation for bash commands. OpenProgram already supports a docker backend; for a hard sandbox, go that route.
D7. Commits Inside a Worktree#
The agent writes files in the worktree → the worktree directory is dirty. Two semantics:
- Auto commit: after an agent tool call (bash running git add / edit / write),
the worktree tool does not auto-commit. The agent runs
git add -A && git commitvia bash itself. This way the commit message is decided by the agent, consistent with git conventions. - Forced commit at merge time: when worktree_merge runs, if the worktree has uncommitted
changes, it first reports the error
worktree_dirtyand lets the agent handle it explicitly (commit it / stash it / discard it). It does not auto-commit-and-merge.
D8. Merge Strategy#
worktree_merge(worktree_id, mode="ff-only" | "squash" | "no-ff"), defaults to ff-only.
- ff-only: source_repo's HEAD is an ancestor of the worktree branch → fast-forward.
Otherwise reports the error
not_fast_forward, letting the agent decide whether to rebase or switch to squash. - squash:
git merge --squash <branch>→ multiple worktree commits squashed into one; suitable when the worktree contains many exploratory small commits. - no-ff: always creates a merge commit, preserving the worktree's commit history.
After a merge, by default git worktree remove <path> deletes the worktree directory, but
the branch is kept (so the user can git log to see the history of this change).
Handling conflicts: on merge failure it does not auto-reset; the worktree status stays committing
(effectively rolling back to active), letting the agent or user enter the worktree to resolve conflicts manually.
D9. Discard Semantics#
worktree_discard(worktree_id, force=False):
force=False(default): the worktree must be clean (no uncommitted / untracked). Otherwise reports the errorworktree_dirty, and the agent can decide whether to stash or force.force=True:git worktree remove --force <path>+git branch -D <branch>. Uncommitted changes are dropped directly.- The record in worktrees/
.json has its status changed to discarded+ a timestamp. The file is not deleted, for auditing convenience — but worktree_path no longer exists.
There is no automatic backup before discard. Archiving the discarded content as a tarball under
~/.openprogram/discarded/ is cheap enough to add later, and is listed in Part 6.
D10. Relationship Between Worktree and Task#
The async task system (see async-task-lifecycle.md):
- A task can exclusively create and use a worktree (task_create → worktree_create).
- On task cancel, the worktree held by the task defaults to
discard force=True. On task complete it does not auto-merge — after the task completes, the plan agent / user decides explicitly (the plan agent picks one to merge after looking at the output of 3 tasks). - A task is not required to open a worktree. Lightweight tasks (reading files, running grep) just run directly on source_repo without opening a worktree.
In implementation, the task lifecycle calls worktree_manager.discard_for_task(task_id) in the cancel hook.
D11. Relationship Between Worktree and ContextCommit#
When the agent runs tools in a worktree, the tool results (bash stdout / edit confirmation) go into the ContextCommit items normally. The file diff inside the worktree does not go directly into ContextCommit content — file diffs are git's business; ContextCommit only records event-level facts like "tool call X modified file Y".
Each tool item's metadata carries a lightweight field worktree_id: Optional[str], indicating which
worktree this tool call happened in (None means it ran directly in source_repo). When the UI renders,
it adds a badge to tool calls inside a worktree.
The worktree merge / discard operations themselves are also written into ContextCommit as system nodes (similar to an attach pointer marker), with content like "Merged worktree wt_abc1234 into source_repo (ff-only, 3 files changed)".
D12. Agent Tool Surface#
Four tools:
| Tool | Parameters | Returns |
|---|---|---|
worktree_create |
source_repo: str? branch_name: str? base_ref: str? label: str? pr: str? (D5b — number / #number / GitHub PR URL, ignores branch_name/base_ref when set) |
{id, path, branch, base_sha} |
worktree_merge |
worktree_id: str mode: str = "ff-only" delete_branch: bool = False |
{merged_sha, files_changed: int, summary: str} |
worktree_discard |
worktree_id: str force: bool = False |
{status: "discarded"} |
worktree_list |
status_filter: str? |
[{id, path, branch, status, source_repo, age_seconds}] |
Error codes (prefix of the returned error string):
not_a_git_repo: source_repo is not a git repoworktree_dirty: the worktree has uncommitted changesnot_fast_forward: cannot ff during mergemerge_conflict: conflict during mergeworktree_in_sessions_dir: source_repo falls inside the sessions tree (D4 isolation violation)worktree_exists: a worktree with the same branch name already exists under the same source_repopr_ref_error:prdoesn't parse, orghis missing/unauthenticated/fails (D5b)pr_worktree_exists: a non-terminal worktree already exists for this PR number (D5b)
worktree_create / worktree_merge / worktree_discard default to
requires_approval=True; only permission_mode=auto skips the approval prompt.
There is no worktree_switch tool. A session has only one active worktree at a time
(D2's ContextVar is single-valued), and switching raises questions the benefit does not justify
(whether to write a switch marker, what becomes of the old worktree). Multiple
worktrees are achieved through async tasks, one worktree per task.
D13. UI Representation#
- Composer toolbar: when the current session has an active worktree, a chip
worktree: wt_abc1234 (3 files changed)is shown above the PromptInput; hovering pops a panel showing worktree_path / branch / the list of changed files / Merge / Discard / Keep buttons. - The fn-form "Working in a folder": unchanged, showing only the source_repo path. The worktree is an internal detail and is not surfaced in the fn-form.
- DAG timeline: worktree create / merge / discard marker nodes are rendered in a distinguishing color (same style as the attach marker).
- Out of scope: inline preview of worktree file diffs (the user can click "open in editor" / use their own git GUI to view).
D14. Errors / Edge Cases#
source_repois not a git repo →not_a_git_repoerror, prompting the user togit initfirst.source_repohas uncommitted changes but the worktree is a new branch → OK, the worktree is created from base_ref (defaults to HEAD), unaffected by the source_repo working tree state.worktree_pathalready exists →worktree_existserror. The user is allowed to pass a name and retry.source_repois inside the sessions tree →worktree_in_sessions_dirrejection (D4).base_refdoes not exist → git reports the error itself, the tool passes through stderr.- the agent accidentally deletes worktree_path (bypassing worktree_discard with a direct rm -rf) →
next time worktree_list detects the path no longer exists, it automatically marks
status=discardedand writes an "auto-cleaned" record.
D15. Integration with Async Task#
worktree_create / merge / discard are themselves synchronous tools (git subprocesses) and are not wrapped into async tasks. But long-running work inside a worktree (the agent running tests, running a build) is usually the work content of an async task:
- when an async task starts it can specify
worktree_id(the task's cwd is locked to this worktree). - the bash / edit run inside the task also goes through the D2 ContextVar path, with cwd being worktree_path.
- task cancel hook → calls
worktree_manager.on_task_cancel(task_id), which defaults to discard. - task complete does not auto-merge (D10).
Part 2. Scenario × Dimension#
Scenario A: Single agent, single worktree (basic flow)#
The agent receives the task "modify foo.py to add logging", opens a worktree → modifies → runs tests → merges.
| Dimension | Design |
|---|---|
| D1 entity | one worktree record, status=active, bound to the current session |
| D2 cwd | when the dispatcher enters a turn it reads session.meta.active_worktree_id → sets the _current_worktree_path ContextVar; bash/edit/write/read all default cwd here |
| D3 status | active → committing (during merge) → merged |
| D4 isolation | worktree_path is not in the sessions tree: defaults to ~/.openprogram/worktrees/<id>-<slug>/ (an independent directory, sibling to source_repo) |
| D5 source | the _work_dir passed by the fn-form is the source_repo; the agent can also pass it explicitly |
| D6 security | bash's cwd starts at worktree_path; edit/write receiving an absolute path outside the worktree writes a warning but does not block |
| D7 commit | the agent runs git add . && git commit -m "..." via bash itself; worktree_merge requires the worktree to be clean beforehand |
| D8 merge | ff-only default; ff succeeds since source_repo HEAD has not moved |
| D9 discard | not used |
| D10 task | no task (runs directly in the main turn) |
| D11 commit log | the bash/edit tool items are all tagged with worktree_id; merge writes a system marker |
| D12 tools | worktree_create → do the work → worktree_merge |
| D13 UI | composer shows chip "wt_abc1234 (2 files changed)", the chip disappears after merge, the DAG adds a marker |
| D14 edge | worktree_create reports an error when source_repo is not a git repo; the user git inits first |
| D15 task | N/A |
Scenario B: Single agent, multiple worktrees (failed exploration)#
The agent tries approach A, tests fail → discard → tries approach B → passes → merge.
| Dimension | Design |
|---|---|
| D1 entity | two worktree records (different id / branch / path). The first has status=discarded, the second has status=merged |
| D2 cwd | only one is active at any moment: the second can be created only after discarding the first; the ContextVar switch is done by the dispatcher at the turn boundary |
| D3 status | wt1: active → discarded; wt2: active → merged |
| D4 isolation | the two worktrees each have an independent directory |
| D5 source | the same source_repo, reused twice |
| D6 security | same as A |
| D7 commit | in wt1 the agent may have run a few commits, deleted along with the branch on discard; wt2's commits go through a normal merge |
| D8 merge | wt2 goes through ff-only; if source_repo did not move during wt1 (only the worktree itself changed), ff succeeds |
| D9 discard | wt1 force=True (the agent decides to drop this line, including uncommitted experiments) |
| D10 task | not used |
| D11 commit log | on the DAG, wt1 markers (create + discard) + wt2 markers (create + merge) |
| D12 tools | create → discard → create → merge |
| D13 UI | the chip switches twice: wt1 shows then disappears, wt2 shows then disappears |
| D14 edge | when discarding wt1, force=True skips the dirty check |
| D15 task | N/A |
Scenario C: Concurrent worktrees (plan agent dispatches 3 tasks)#
The plan agent lists 3 independent changes → 3 async tasks, one worktree per task (independent copies of source_repo) → all complete → the plan agent looks at the results, picks one to merge, discards the rest.
| Dimension | Design |
|---|---|
| D1 entity | 3 worktree records, each bound to a task_id; status evolves in sync |
| D2 cwd | when running inside each task, the task runtime's ContextVar independently sets _current_worktree_path=task.worktree_path; the main session's plan agent itself does not activate any worktree (the plan agent does not touch files) |
| D3 status | 3 parallel active → all tasks complete → 2 discarded + 1 merged |
| D4 isolation | each worktree has an independent directory; source_repo all point to the same one, but git worktree add inherently supports multiple worktrees at once (different branches) |
| D5 source | all the same source_repo |
| D6 security | each task isolates its cwd, not affecting one another |
| D7 commit | each task commits itself |
| D8 merge | the chosen one goes through ff-only; if none of the other tasks merged, source_repo HEAD has not moved, ff succeeds |
| D9 discard | the remaining 2 go through force=True (the plan agent picked 1, the rest are no longer wanted) |
| D10 task | each task is assigned a worktree at creation; task complete does not auto-merge (D10), waiting for the plan agent's decision |
| D11 commit log | all 3 worktrees each produce markers; the plan agent writes an assistant explanation "adopting approach 2" |
| D12 tools | the plan agent calls worktree_list to see the 3; calls worktree_merge wt2 + worktree_discard wt1 wt3 |
| D13 UI | the composer chip is the plan agent's own session, not showing sub-worktrees; in the task panel each task card shows its own worktree chip |
| D14 edge | when 3 worktrees are created at once, git worktree add mutex (git has its own lockfile) |
| D15 task | fully integrated: task creation → worktree assignment; task cancel → discard; task complete → wait for decision |
Scenario D: Long-running worktree / user takeover#
The agent is halfway through (5 patches committed in the worktree), the user decides to take over themselves.
| Dimension | Design |
|---|---|
| D1 entity | status goes from active → kept |
| D2 cwd | after the user clicks "Keep & detach", the session's active_worktree_id is cleared, the ContextVar is no longer set; subsequent agent turns return to source_repo as cwd |
| D3 status | active → kept |
| D4 isolation | unchanged |
| D5 source | unchanged |
| D6 security | the worktree is still on disk, but OpenProgram no longer writes to it; the user opens worktree_path in their own terminal / IDE to continue |
| D7 commit | the agent's earlier commits are all kept on the branch |
| D8 merge | not used (the user decides merge / rebase themselves) |
| D9 discard | not used |
| D10 task | if it is a worktree held by a task, the task also moves into the kept state in sync (the task no longer writes logs, but the record is kept) |
| D11 commit log | writes a system marker "Worktree wt_xxx kept for manual handover at |
| D12 tools | the UI directly calls the ws action (not an agent tool) worktree_keep(worktree_id); the agent tool can also expose worktree_keep, but low priority |
| D13 UI | the chip changes to "kept — open in editor", clicking copies the path |
| D14 edge | the user later deletes the worktree directory manually → next OpenProgram startup, list_worktrees detects the path no longer exists → marks discarded (D14) |
| D15 task | the task also enters a detached state, not affecting new tasks |
Part 3. Key Invariants#
-
worktree_path is never inside the
~/.openprogram/sessions/subtree (isolating OpenProgram's own git; if violated, worktree_create rejects). -
Zero changes to the main repo after discard
git worktree remove --force+git branch -Ddo not touch source_repo's HEAD or working tree. Check:git rev-parse HEADis identical before and after discard. -
The worktree does not disappear automatically when a merge fails after a merge_conflict / not_fast_forward error, the worktree status returns to
active, the directory is kept, letting the agent or user inspect it manually. -
At most one active worktree per session at any moment session.meta.active_worktree_id is single-valued; if there is already an active worktree at worktree_create time, it reports the error
already_active, prompting to merge/discard/keep first. -
The starting cwd of a bash command is always the active worktree_path (if it exists) rather than session-git workdir/; shell state is not persistent across bash calls, each one is a fresh subprocess, with cwd reset back to worktree_path.
-
worktree_id appearing in a tool item's metadata = this tool call executed inside that worktree when ContextCommit reads it, it can distinguish "where the change was made" accordingly.
-
The branch of a kept worktree is not deleted only OpenProgram's reference is unbound; in the user's git repository they can still
git checkoutto this branch.
Part 4. .worktreeinclude#
git worktree add only ever checks out tracked content. Files a repo deliberately keeps
untracked and gitignored — .env, per-machine TLS certs, *.local.json overrides — never
appear in a fresh worktree, even though the agent's tools need them immediately (a bash command
reading .env fails silently different from how it fails in the main tree).
If source_repo/.worktreeinclude exists, each line is a gitignore-style pattern:
#at the start of a (trimmed) line is a comment; blank lines are ignored.*,?,[...]— standard glob wildcards.- A pattern with no
/matches the basename anywhere in the tree (*.local.jsonmatches botha.local.jsonandsub/a.local.json). - A pattern containing
/is anchored to the repo root. - A trailing
/marks a directory pattern (matches the directory and everything under it).
Not supported: ! negation, ** double-star. Split those cases into multiple plain patterns.
At the end of create_worktree, right after git worktree add succeeds, WorktreeManager
calls sync_include_files(source_repo, worktree_path)
(openprogram/worktree/include_sync.py). This is the single choke point every creation path
shares — the worktree_create tool, and any future task/plan-mode fan-out — so the hook lives
in the manager, not in one caller.
Semantics:
- Only untracked files are ever candidates (
git ls-files --others, deliberately without--exclude-standardso already-gitignored files like.envstill show up) — tracked files are already checked out bygit worktree additself. - A destination path that already exists is left untouched (no clobber).
- Symlinks are copied as links, not dereferenced.
- File permissions are preserved (
shutil.copy2). - One file's copy failure is recorded and does not abort the rest; failures and successes are
both attached to the
Worktreerecord (include_synced/include_failed) and echoed in theworktree_createtool's return string. - No
.worktreeincludefile insource_repo→ zero git calls, zero behavior change.
Pattern matching is hand-rolled Python (fnmatch + pathlib), not git's own exclude engine.
git check-ignore / ls-files --exclude-from only ever exclude matches from a listing — there
is no plumbing form that treats a pattern file as an include filter — and pointing git at an
arbitrary pattern file otherwise requires core.excludesFile config injection, which this
sandbox refuses. Reusing git's own untracked-file enumeration (git ls-files --others) plus a
small pattern matcher over the manifest gets git's semantics for "what's untracked" without
needing config injection or a new dependency (pathspec is not in openprogram's own base
dependency set — it only ships transitively via semble, an MCP dev tool).
Part 5. Out of Scope#
- Remote push: the worktree is local-only; to push the worktree branch to origin, the agent runs
git push -u origin <branch>via bash itself. worktree_merge does not push either. - cherry-pick / rebase between worktrees: complex semantics, left for the agent to handle via bash itself.
- conflict resolution UI: on merge conflicts OpenProgram does not provide a visual mergetool; the agent uses edit to modify files / bash to run git mergetool.
- worktrees across source_repos: one worktree necessarily corresponds to one source_repo; merging worktree changes into another repository is not supported (do it via a bash git patch flow if needed).
- auto-backup before discard: the
~/.openprogram/discarded/archiving mentioned in D9, left as a future enhancement. - chroot / namespace true sandbox: D6 locks the default cwd rather than sandboxing; for hard isolation, go through the docker backend.
- auto-cleanup of the active worktree when the session closes: the active worktree is kept across session restarts (after restart, list_worktrees probes, and ones still active are marked kept for the user to handle manually).
Appendix: Implementation Status#
None of this design has landed. The pieces it needs, and where they stand:
| Capability | Present behavior |
|---|---|
| Worktree isolation in the user's real repo | absent; create/merge/discard is entirely new |
| Agent cwd bound to worktree | absent; the runtime uses session-git workdir/ |
| Bash tool passing cwd | LocalBackend.run accepts cwd, the bash function does not pass it |
| Edit/Write/Read checking worktree boundary | absent; only absolute paths are checked |
| Worktree state machine persistence | absent; worktrees/<id>.json in session-git is new |
| UI worktree chip | absent |
| Worktree × Task integration | absent; depends on the async task system, itself still in design |
| Sub-agent worktree mechanism | removed when sub-agents became peer sessions; not reused here |
.worktreeinclude untracked-file sync |
landed — openprogram/worktree/include_sync.py, called from WorktreeManager.create_worktree |
The work, in dependency order:
| Step | File | Main change |
|---|---|---|
| 1 | new openprogram/worktree/types.py |
Worktree dataclass + WorktreeStatus Enum + serialization |
| 2 | new openprogram/worktree/manager.py |
WorktreeManager: create / merge / discard / list / keep; underlying subprocess.run(["git", "worktree", ...]); persists to <session-repo>/worktrees/<id>.json |
| 3 | new openprogram/worktree/_paths.py |
worktree path policy: ~/.openprogram/worktrees/<id>-<slug>/; isolation check (D4) |
| 4 | edit openprogram/agent/internals/_workdir.py |
apply_default_workdir prefers returning the active worktree path |
| 5 | edit openprogram/agent/dispatcher.py |
at the start of a turn, read session.meta.active_worktree_id → set the _current_worktree_path ContextVar |
| 6 | edit openprogram/functions/tools/bash/bash.py |
call backend.run(cmd, cwd=_current_worktree_path.get()) |
| 7 | edit openprogram/functions/tools/edit/edit.py + write/read |
warning when path outside worktree (D6) |
| 8 | new openprogram/functions/tools/worktree/ |
4 @function tools: worktree_create / worktree_merge / worktree_discard / worktree_list; go through WorktreeManager |
| 9 | edit openprogram/store/session/session_store.py |
add an active_worktree_id field to session.meta; helpers set_active_worktree / get_active_worktree |
| 10 | new openprogram/webui/ws_actions/worktree.py |
list_worktrees / keep_worktree / discard_worktree (user manual UI operations) |
| 11 | new web/components/chat/composer/worktree-chip.tsx |
chip component + hover panel + Merge/Discard/Keep buttons |
| 12 | edit web/components/chat/composer/composer.tsx |
bring in the chip |
| 13 | edit ContextCommit item metadata rendering | tool call items show a worktree_id badge |
| 14 | edit openprogram/agent/dispatcher.py to write markers |
worktree_create / merge / discard write system nodes into ContextCommit |
| 15 | (depends on async-task) hook into openprogram/tasks/lifecycle.py |
task cancel → WorktreeManager.on_task_cancel; task create can optionally attach a worktree |
| 16 | Tests | unit: WorktreeManager (create/merge/discard path checks, isolation check); integration: agent in worktree → merge full flow |