The loop system provides autonomous iterative development with automatic code auditing.
- Plugin boot does not mutate loop rows. Initialization loads storage and runtime services only.
- No loops are recovered, cancelled, restarted, or reconciled during plugin startup.
- Loop recovery and restart are explicit user actions via
loop-status restart=true.
- Any non-completed loop is restartable via explicit restart when the worktree is available.
- Restartable statuses:
running,cancelled,errored,stalled. - Completed loops are history-only and cannot be restarted.
- Missing worktree blocks restart — the worktree directory must exist for restart to proceed.
- Restart preserves loop identity, plan, worktree path, section progress, and review findings.
- Restart resets iteration count and error budget.
- Restart creates a fresh session and resumes from the persisted phase and section index.
- Stale workspace sweep is teardown cleanup-only, not boot-time recovery.
- Sweep removes workspace registrations for non-running restartable loops (
cancelled,errored,stalled) while preserving worktrees for manual restart. - Completed loops are fully removed (registration + worktree).
- Running loops are never touched by sweep.
stateDiagram-v2
[*] --> Coding: loop tool invoked
Coding --> Auditing: coding idle complete
Auditing --> Coding: section dirty or audit dirty
Auditing --> Auditing: next section
Auditing --> FinalAuditing: last section clean
Auditing --> [*]: audit clear
FinalAuditing --> Coding: final audit dirty (fix mode)
Coding --> FinalAuditing: final-audit fix complete
FinalAuditing --> PostAction: final audit clean
note right of PostAction: Only when loop.postAction.enabled
PostAction --> [*]: post-action complete
Coding --> [*]: max iterations / retry limit / stall timeout / cancellation
Auditing --> [*]: max iterations / retry limit / stall timeout / cancellation
FinalAuditing --> [*]: final audit clean (no post-action)
Each loop has a LoopState backed by the typed loops and loop_large_fields SQLite tables:
interface LoopState {
active: boolean // Whether loop is currently running
sessionId: string // Current OpenCode session ID
loopName: string // Unique loop identifier
worktreeDir: string // Worktree path
projectDir?: string // Project directory path
worktreeBranch?: string // Branch name if using worktree
iteration: number // Current iteration count
maxIterations: number // Maximum iterations (0 = unlimited)
startedAt: string // ISO timestamp
prompt?: string // Original task prompt
phase: 'coding' | 'auditing' | 'final_auditing' | 'post_action'
lastAuditResult?: string // Last audit output
errorCount: number // Consecutive error count
auditCount: number // Number of audits completed
terminationReason?: string // Reason for termination
completedAt?: string // ISO timestamp
worktree?: boolean // Whether using worktree isolation
modelFailed?: boolean // Whether model error occurred
sandbox?: boolean // Whether using Docker sandbox
sandboxContainer?: string // Container name if sandboxed
completionSummary?: string // Summary of loop completion
executionModel?: string // Model used for execution
auditorModel?: string // Model used for auditing
workspaceId?: string // OpenCode workspace ID
hostSessionId?: string // Host session ID for post-completion redirect
currentSectionIndex: number
totalSections: number
finalAuditDone: boolean
}Each iteration runs in a fresh session to keep context small and prioritize speed:
- Coding phase completes
- Current session is destroyed
- New session is created
- Continuation prompt is injected with:
- Original task prompt
- Current iteration number
- Audit findings (if any)
function buildContinuationPrompt(state: LoopState, auditFindings?: string): string {
let systemLine = `Loop iteration ${state.iteration}`
if (state.maxIterations > 0) {
systemLine += ` / ${state.maxIterations}`
} else {
systemLine += ` | No max iterations set - loop runs until auditor all-clear or cancelled`
}
let prompt = `[${systemLine}]\n\n${state.prompt ?? ''}`
if (auditFindings) {
prompt += `\n\n---\nThe code auditor reviewed your changes. You MUST address all bugs and convention violations.`
}
return prompt
}Loop usage is captured across rotated code and auditor sessions so loop-status can report cumulative cost and token totals after the original session has been replaced.
token-usage.tsextracts assistant message usage, normalizes token fields, and groups totals by model label.loop_session_usagepersists per-session, per-model rows keyed by project, loop name, session ID, and role.loop-status <name>merges persisted rows with the currently live session output while avoiding double-counting the active session.- When no loops are active,
loop-statuscan still show cumulative usage for completed loops that have persisted usage data.
Tracked token buckets are input, output, reasoning, cache read, and cache write, plus cost and assistant message count.
A watchdog monitors loop activity. If no progress is detected within stallTimeoutMs (default: 60 seconds), the current phase is re-triggered.
const STALL_TIMEOUT_MS = 60_000
const MAX_CONSECUTIVE_STALLS = 5After 5 consecutive stalls, the loop terminates with terminationReason: 'stall_timeout'.
Audit findings survive session rotation via the review store:
interface ReviewFinding {
projectId: string
file: string
line: number
severity: 'bug' | 'warning'
description: string
scenario: string | null
loopName: string | null
sectionIndex: number | null
createdAt: number
}At the start of each audit:
- Existing findings are retrieved via
review-read - Resolved findings are deleted via
review-delete - Unresolved findings are carried forward
Outstanding severity: 'bug' findings block loop completion — the loop terminates only when the auditor has run at least once and zero bug-severity findings remain.
Loops always run in an isolated git worktree. Sandbox is optional: when Docker is available and sandbox.mode = 'docker' is configured, a sandbox container is provisioned automatically; otherwise the loop runs in worktree-only mode.
Note: this applies to the
execute-plantool's defaultmode: loop. The same tool also acceptsmode: new-session, which bypasses the loop entirely and runs the plan in a fresh standalone session with no worktree or sandbox (see Tools Reference).
graph TD
A[loop tool invoked] --> B[Create worktree]
B --> C[Create new branch]
C --> D[Start coding session]
D --> E[Iterate until completion]
E --> F[Loop completes or is cancelled]
F --> G[Cleanup worktree]
G --> H[Branch preserved]
Benefits of worktree isolation:
- Isolation from ongoing development
- Safe to experiment without affecting main branch
- Branch preserved for later review/merge
- Per-loop customization via
loop.worktreeOpencodeConfig— inject MCP servers and other opencode config into each worktree without host config changes or commit pollution (see Configuration Reference)
Sandbox is optional. When Docker is available and configured, a sandbox container is provisioned automatically; otherwise loops run in worktree-only mode.
- Container created with worktree mounted at
/workspace bash,glob,greptools redirect into containerread/write/editoperate on host filesystem- Container stopped and removed on loop completion
See sandbox documentation for details.
In user-facing language, a plan is decomposed into milestones — ordered units of execution. In the code and database, these are called sections:
section_plansSQL table — one row per milestone, ordered bysectionIndexcurrentSectionIndex/totalSectionscolumns on the loop row<!-- forge-section -->markers in the architect plan outputsection-readtool reads the current or specified milestone
Decomposition is a one-shot preprocessing step at loop start (services/deterministic-decomposer.ts), not a runtime loop phase. Once milestones exist, the loop advances through them via advance-section transitions inside the auditing phase. When the final_auditing phase reports outstanding bug findings, the loop rotates to a coding session in "final-audit fix" mode — the code agent fixes the reported findings without rewinding to a specific section, and on idle the loop transitions straight back to final_auditing for re-verification.
A loop completes when the active phase emits a clean audit result (optionally followed by a post-completion action phase):
- Non-sectioned loops complete on
audit-clear. - Sectioned loops advance through clean section audits, then complete on
final-audit-clean. - Dirty section audits rotate back to coding for the same section so findings can be addressed.
- Dirty final audits rotate to coding in "final-audit fix" mode (no section rewind); when the fix coding pass goes idle, the loop returns straight to
final_auditing. - After a clean final audit, if
loop.postAction.enabledistrueand specifies askillorprompt, the loop enters apost_actionphase that runs inside the worktree before teardown. Completion occurs when the post-action session goes idle (post-action-completeevent).
After a clean final audit, before worktree teardown, the loop may run a post-completion action configured via loop.postAction in forge-config.jsonc. This phase is best-effort — it is not re-audited and relies only on safe, scoped fixes. The post-action runs as the code agent in a fresh session inside the worktree.
| Field | Type | Default | Description |
|---|---|---|---|
loop.postAction.enabled |
boolean |
false |
Enable the post-completion action phase. |
loop.postAction.skill |
string |
— | Name of a skill to load via the Skill tool at action time (e.g. "pr-review"). Must be installed host-side. |
loop.postAction.prompt |
string |
— | Optional extra instruction text appended to the action prompt. Used standalone when no skill is set. |
- Runs only after a clean final audit completes.
- Runs inside the worktree as the
codeagent, with access to the full worktree state (including uncommitted changes). - Best-effort: The post-action result is not re-audited; it applies only safe, scoped fixes. The question tool is blocked — any finding requiring clarification is auto-deferred.
- On idle (
post-action-complete), the loop terminates normally. - If the post-action session fails to create, the loop terminates as completed without retrying.
- Outcome capture: On
post-action-complete, the post-action session's raw final assistant message is stored verbatim in the loop'scompletion_summary(surfaced as Completion Summary in the dashboard). The loop status is alwayscompletedregardless of what the post-action reported — the plan itself was already cleared by the final audit; the summary only provides context (alternate-review verdict, CI result, etc.). Completion summary is captured only on the cleanpost-action-completepath; idle-exhausted, error, and abort-without-assistant terminations leave it empty.
Loops can be cancelled via:
loop-canceltool/loop-cancelslash command
Cancellation:
- Marks loop as inactive
- Sets
terminationReasonto'cancelled' - Stops sandbox container if applicable
- Optionally cleans up worktree (if
cleanupWorktree: true)
| Error Type | Behavior |
|---|---|
| Model error | Automatic fallback to default model, retry |
| Error retry limit | Loop terminates with terminationReason: 'error_max_retries' |
| Audit retry limit | Loop terminates with terminationReason: 'audit_retry_exhausted' |
| Final audit retry limit | Loop terminates with terminationReason: 'final_audit_retry_exhausted' |
| Stall timeout | Loop terminates with terminationReason: 'stall_timeout' after the configured consecutive stall limit |
Inside active loop sessions:
git pushis denied (permission hook)execute-planis blocked (tool hooks)questionis blocked (tool hooks)
{ "loop": { "postAction": { "enabled": false, // Enable the post-completion action phase "skill": "pr-review", // Skill to load via the Skill tool (e.g. "pr-review") "prompt": "..." // Extra instruction text; used standalone when no skill is set } } }