Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@

### Changed

- Replaced custom `forge-worktree` workspace adapter with opencode's builtin `worktree` workspace type to fix red-dot/disconnected status in the TUI. Old `forge-worktree` workspace rows in the local DB must be deleted manually.
- Renamed auto-generated git branches to `opencode/<loopName>` (with `-2`, `-3`, ... suffixes on conflict) at loop completion for better discoverability.
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Add to your `opencode.json` to enable Forge’s server-side hooks, tools, and ag
}
```

**Optional — workspace integration:** to let worktree loops appear as switchable OpenCode workspaces in the TUI, also export this in the environment that launches `opencode`:

```bash
export OPENCODE_EXPERIMENTAL_WORKSPACES=true
```

Requires OpenCode ≥ 1.15.0. Without it, loops still run normally — you just don't get workspace switching. See [Workspace Integration](#workspace-integration) for details.

## What Forge Adds

Forge ships two user-facing surfaces:
Expand Down Expand Up @@ -213,6 +221,7 @@ Enable `logging.enabled` to write logs to disk. To use the default log path, omi
"planArchiveTtlMs": 604800000, // TTL in ms for archived plans before pruning. 0 disables pruning.
"keybinds": { // Keyboard shortcut overrides
"viewPlan": "<leader>v", // View plan dialog
"showLoops": "<leader>w", // Show loops dialog
"loadPlan": "<leader>i" // Load archived plans dialog
}
},
Expand Down Expand Up @@ -278,6 +287,7 @@ When enabled, logs are written to the specified file with timestamps. The log fi
- `tui.autoSavePlans` - Auto-save captured plans to disk under `<dataDir>/plans/<projectId>/`. Default: `false`.
- `tui.planArchiveTtlMs` - TTL in ms for archived plans before pruning. 0 disables pruning. Default: `604800000` (7 days).
- `tui.keybinds.viewPlan` - View plan dialog keybind. Default: `<leader>v`.
- `tui.keybinds.showLoops` - Show loops dialog keybind. Default: `<leader>w`.
- `tui.keybinds.loadPlan` - Load archived plans dialog keybind. Default: `<leader>i`.

## TUI Plugin
Expand Down Expand Up @@ -432,7 +442,7 @@ After the architect presents a summary, the user chooses an execution mode from
| `Execute here` | When preserving current context matters |
| `Loop` | Safer autonomous iteration |

The dialog also lets you pick the execution model and auditor model at launch time. Those selections are remembered per project and pre-filled on later launches.
The dialog also lets you pick the execution model and auditor model at launch time. Those selections are remembered per project and pre-filled on later launches. Optional **variant selectors** accompany each model selector, letting you choose provider-specific reasoning or thinking-effort levels (e.g., `low`, `high`, `max`) when the model exposes them. Variant selections are also persisted per project.

Execution is immediate — there are no additional LLM calls between approval and execution. The system intercepts the user's approval answer, reads the cached plan, and dispatches it programmatically to the code agent. The architect never processes the approval response.

Expand Down Expand Up @@ -533,30 +543,44 @@ Loops always run in an isolated git worktree. Sandbox is optional: when Docker i

Worktree loops can optionally register as **OpenCode workspaces**, letting you switch between them (and your main project) from the same TUI session without restarting or re-opening anything.

### When it runs
### Requirements

Workspace integration requires the **experimental workspace runtime** to be enabled in OpenCode itself. The plugin API surface (`experimental_workspace.register`) is always present, but the underlying sync, session-scoping, and TUI dialogs are gated behind an environment variable. Without it, Forge's adapter registers fine but `workspace.create` silently no-ops and the TUI never shows worktree workspaces.

Set one of these in the environment that launches `opencode`:

```bash
export OPENCODE_EXPERIMENTAL_WORKSPACES=true
# or, to enable every experimental opencode feature at once:
export OPENCODE_EXPERIMENTAL=true
```

Accepted values are `true` or `1` (case-insensitive). Requires **OpenCode ≥ 1.15.0**.

> The `OPENCODE_EXPERIMENTAL_WORKSPACES` flag is not currently documented on opencode.ai. The authoritative source is `packages/core/src/flag/flag.ts` and `packages/opencode/src/effect/runtime-flags.ts` in the OpenCode repo.

Workspace integration is **host-gated, not config-gated**. Forge uses opencode's builtin `worktree` workspace type, which is always available on hosts that expose the experimental workspace API (`experimental_workspace` on the plugin input, `experimental.workspace` on the SDK client).
No forge config option enables or disables this — the toggle is purely on the OpenCode side.

- **Host exposes the API** → worktree loops become workspace-backed. The worktree directory appears as a switchable workspace in the TUI, and its sessions are bound to that workspace.
- **Host does not expose the API** → forge skips registration, logs a note, and worktree loops run exactly as before. Everything else (iteration, auditing, sandbox, status, cancel, restart) is unaffected.
### When workspace integration is active

No forge config option enables or disables this — the feature lights up automatically on supported hosts.
- **Env var set, OpenCode ≥ 1.15.0** → worktree loops become workspace-backed. The worktree directory appears as a switchable workspace in the TUI, and its sessions are bound to that workspace.
- **Env var unset or older OpenCode** → Forge's adapter still registers (the API surface is always present), but `workspace.create` no-ops and the loop runs as a plain worktree loop with no workspace switching. Everything else (iteration, auditing, sandbox, status, cancel, restart) is unaffected.

### What it does

When a worktree loop starts on a supported host, forge:
When a worktree loop starts with `OPENCODE_EXPERIMENTAL_WORKSPACES=true`, forge:

1. Creates the git worktree (as usual)
2. Creates a new Code session pointed at the worktree directory
3. Calls `experimental.workspace.create` with `type: "worktree"` and `branch: null` to create a builtin worktree workspace
1. Calls `experimental.workspace.create` with `type: "forge"`, `branch: null`, and `extra: { loopName, projectDirectory, workspaceCreatedAt }` to register the workspace through the `forge` adapter
2. The adapter's `create` hook creates the git worktree (reusing an orphaned branch when possible) and, when configured, provisions the Docker sandbox container
3. Creates a new Code session pointed at the worktree directory
4. Calls `experimental.workspace.warp` to bind the session to that workspace
5. Persists the workspace ID on the loop record (`loops.workspace_id`) so the TUI can route clicks on a loop into the correct workspace

The adaptor's `create` and `remove` hooks are intentional no-ops — forge's loop system owns worktree lifecycle, not the workspace system. The adaptor only surfaces existing worktrees to the workspace UI.
The adapter's `remove` hook commits in-flight changes (when teardown context allows), stops the sandbox container if any, and removes the worktree directory unless the loop is restartable. Branches are preserved for later restart or merge.

### Graceful degradation

If workspace creation or session binding fails at runtime (network error, API mismatch, unsupported host), the loop **does not abort**. Forge logs the failure, clears the workspace ID, and the loop continues as a regular (non-workspace) worktree loop. You lose workspace-based switching for that loop, but the loop itself runs to completion.
If workspace creation or session binding fails at runtime — env var unset, OpenCode version too old, network error, API mismatchthe loop **does not abort**. Forge logs the failure, clears the workspace ID, and the loop continues as a regular (non-workspace) worktree loop. You lose workspace-based switching for that loop, but iteration, auditing, sandbox, and restart all run to completion.

### From the TUI

Expand Down
51 changes: 40 additions & 11 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
# Keep in sync with vitest.config.ts:5-24 (vitest-only test includes)
# Files that import from 'vitest' must be ignored by `bun test`.
# Bun executes their top-level `vi.mock(...)` calls, which can replace
# real modules (e.g. `bun:sqlite`) and corrupt unrelated `bun:test` files
# that run in the same process.
# Keep in sync with the test files that use `import ... from 'vitest'`.
[test]
pathIgnorePatterns = [
"test/constants/loop.test.ts",
"test/deterministic-decomposer.test.ts",
"test/hooks/audit-rotate-ordering.test.ts",
"test/hooks/loop-decomposing-salvage.test.ts",
"test/hooks/loop-decomposing.test.ts",
"test/hooks/loop-section-audit-retry.test.ts",
"test/hooks/loop-idle-gate.test.ts",
"test/hooks/forge-session-attach.test.ts",
"test/hooks/host-side-effects-unwarp.test.ts",
"test/hooks/loop-event-gate.test.ts",
"test/section-capture-streaming-completion.test.ts",
"test/services/execution-decomposer.test.ts",
"test/services/orphan-sweep.test.ts",
"test/hooks/loop-idle-gate.test.ts",
"test/hooks/loop-section-audit-retry.test.ts",
"test/hooks/plan-approval-dedupe.test.ts",
"test/hooks/plan-approval-worktree-timing.test.ts",
"test/index/session-lookup.test.ts",
"test/loop-runtime-audit-permissions.test.ts",
"test/loop-status-tool.test.ts",
"test/loop/cancel.test.ts",
"test/loop/in-flight-guard.test.ts",
"test/loop/prompts.test.ts",
"test/loop/state-mapper.test.ts",
"test/loop/termination.test.ts",
"test/loop/transitions.test.ts",
"test/plan-approval.test.ts",
"test/plan-execution.test.ts",
"test/sandbox/context.test.ts",
"test/services/execution-attach-cleanup.test.ts",
"test/services/execution-in-flight-guard.test.ts",
"test/services/execution-restart.test.ts",
"test/services/execution.start-loop.test.ts",
"test/services/parse-section-summary.test.ts",
"test/utils/worktree-cleanup.test.ts",
"test/workspace/forge-worktree-list.test.ts",
"test/services/select-initial-worktree-session.test.ts",
"test/tui/execute-plan-panel-busy.test.ts",
"test/utils/tui-client-await-workspace-connected.test.ts",
"test/utils/tui-client-loop-inline-plan.test.ts",
"test/utils/tui-client-select-session.test.ts",
"test/utils/tui-client-variants.test.ts",
"test/utils/tui-client-warp-flow.test.ts",
"test/utils/tui-client-workspaces.test.ts",
"test/index/session-lookup.test.ts",
"test/utils/workspace-status-registry.test.ts",
"test/utils/worktree-cleanup.test.ts",
"test/workspace/classify-stale.test.ts",
"test/workspace/forge-adapter-e2e.test.ts",
"test/workspace/forge-adapter.test.ts",
"test/workspace/forge-worktree.test.ts",
"test/workspace/sweep-stale.test.ts",
]
Loading