# 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_job_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/programs/_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 `set`s 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 remove`d.
- `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

Two 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 selected Project**: the dispatcher binds the session Project directory as cwd;
   `git rev-parse --show-toplevel` resolves its ancestor git root.

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/<id>.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-job-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.
- **Function form**: contains only function parameters. The selected Project remains
  the source repository and the worktree is not surfaced in the 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 selected Project 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 <path>", 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

The worktree subsystem is partially implemented. The current state is:

| Capability | Current evidence | Status |
|---|---|---|
| Worktree entity and state machine | openprogram/worktree/types.py: Worktree, WorktreeStatus | Implemented |
| Persistence and lifecycle manager | openprogram/worktree/store.py; openprogram/worktree/manager.py: create_worktree, merge_worktree, discard_worktree, keep_worktree, list_worktrees | Implemented |
| Include-file synchronization | openprogram/worktree/include_sync.py, called from WorktreeManager.create_worktree | Implemented |
| Agent-facing operations | openprogram/programs/tools/files/worktree/: worktree_create, worktree_merge, worktree_discard, worktree_keep, worktree_list | Implemented |
| Turn/workdir binding | openprogram/worktree/context.py; openprogram/agent/dispatcher/turn_context.py | Implemented for the current context bridge; boundary behavior needs integration verification |
| WebSocket operations | apps/server/openprogram_server/_webui/ws_actions/worktree.py: list, get, merge, discard, keep | Implemented |
| Worktree × Job lifecycle | openprogram/agent/job/types.py carries worktree_id; openprogram/agent/job/runner.py binds worktree context | Partially integrated; automatic cancel/discard policy remains a separate acceptance item |
| Edit/read/write boundary enforcement | Worktree-aware context and path helpers exist | Requires verification for every file tool and escape path |
| UI worktree chip and metadata rendering | No matching current UI implementation was found in the inspected paths | Not implemented / remains design work |
| End-to-end tests | Manager and tool coverage exists, but the complete create → agent edit → merge/discard flow remains an acceptance obligation | Not a claim of full acceptance |

The design is neither entirely absent nor fully accepted. The implemented manager, persistence, tools, context bridge, and WebSocket actions should be maintained as current behavior. The UI chip, full file-tool boundary matrix, automatic cancellation policy, and complete integration tests remain explicit gaps.

The following design boundaries remain unchanged: remote push, cherry-pick/rebase, conflict-resolution UI, cross-repository worktrees, discard backups, namespace isolation, and automatic cleanup after session close.
