OpenProgram Docs

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 by git worktree add (absolute path)
  • branch_name: the branch name corresponding to the worktree (defaults to op/wt/<id>)
  • base_ref: the baseline ref at creation time (defaults to HEAD, can be set to origin/main / commit sha)
  • created_at: unix timestamp
  • status: active / committing / merged / discarded / kept
  • parent_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 via worktree_create's pr parameter (D5b); lets a later pr-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:

  1. Runtime-spawned subprocess (Codex CLI / Claude CLI, which take a --cd argument) controlled via runtime.set_workdir(path).
  2. In-process @function tools (bash / edit / write / read, etc.) bash goes through get_active_backend().run(...); currently LocalBackend.run accepts a cwd argument 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_path as 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's workdir/.

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 merge succeeded, the worktree directory has been git worktree removed.
  • discarded: git worktree remove --force succeeded, the branch was deleted too.
  • kept: the user went through worktree_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:

  1. Explicitly passed by the agent: the source_repo parameter of the worktree_create tool (absolute path). Suitable when a plan agent already knows the target repository while listing tasks.
  2. The fn-form "Working in a folder": the _work_dir the 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).
  3. 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):

  1. gh pr view <n> --json headRefName,headRepositoryOwner,isCrossRepository reads the PR's head branch and whether it lives on a fork.
  2. Same-repo PR (isCrossRepository=false): git fetch origin <headRefName>:<local_branch> — the branch already exists on origin.
  3. Fork PR (isCrossRepository=true): git fetch origin pull/<n>/head:<local_branch> — GitHub's synthetic per-PR ref, the same mechanism gh pr checkout uses, so no remote has to be registered for the contributor's fork.
  4. The fetched commit becomes base_ref for a normal git worktree add; the worktree's own branch is named op/wt/pr-<n>-<id6> (same op/wt/<slug>-<id6> convention as every other worktree, with the PR number standing in for the label). The throwaway fetch ref is deleted once -b has 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_path that 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 commit via 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_dirty and 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 error worktree_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 repo
  • worktree_dirty: the worktree has uncommitted changes
  • not_fast_forward: cannot ff during merge
  • merge_conflict: conflict during merge
  • worktree_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_repo
  • pr_ref_error: pr doesn't parse, or gh is 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_repo is not a git repo → not_a_git_repo error, prompting the user to git init first.
  • source_repo has 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_path already exists → worktree_exists error. The user is allowed to pass a name and retry.
  • source_repo is inside the sessions tree → worktree_in_sessions_dir rejection (D4).
  • base_ref does 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=discarded and 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 ", so the agent later looking at ContextCommit knows this happened
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#

  1. worktree_path is never inside the ~/.openprogram/sessions/ subtree (isolating OpenProgram's own git; if violated, worktree_create rejects).

  2. Zero changes to the main repo after discard git worktree remove --force + git branch -D do not touch source_repo's HEAD or working tree. Check: git rev-parse HEAD is identical before and after discard.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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 checkout to 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.json matches both a.local.json and sub/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-standard so already-gitignored files like .env still show up) — tracked files are already checked out by git worktree add itself.
  • 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 Worktree record (include_synced / include_failed) and echoed in the worktree_create tool's return string.
  • No .worktreeinclude file in source_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
Last updated · 2026-08-13