Skip to content

Commit 20a1062

Browse files
chriswritescode-devForge
andauthored
feat: add loop FSM transitions, plan amendments, and audit rewind (#72)
* feat: add loop FSM transitions, plan amendments, and audit rewind * refactor: consolidate loop transition logging and persist final_audit_fix phase - Renumber migrations 138-140 to 141-143 to avoid collision with feat/loop-metrics - Extract transitionSectionIndex and recordTerminalTransition single-source helpers - Dedupe loops-repo bind params and unify setState/restoreState via persistState - Persist final_audit_fix as a real phase; restart resumes the final-audit fix prompt - Move plan-adjust guards into adjustRemainingSections under immediateTransaction - Cache dashboard repos and strip amendment section content from poll payload * fix: render dashboard machine graph SVG elements in correct namespace * style: improve dashboard edge label contrast with yellow fill and stroke outline --------- Co-authored-by: Forge <forge@example.com>
1 parent 2b7e01f commit 20a1062

70 files changed

Lines changed: 7313 additions & 303 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ Model and variant selection follows this priority order:
369369

370370
## Loop
371371

372-
The loop is an iterative development system with four phases, ending with an optional post-completion action:
372+
The loop is an iterative development system with five persisted phases (`coding`, `auditing`, `final_auditing`, `final_audit_fix`, `post_action`), ending with an optional post-completion action:
373373

374374
1. **Coding phase** — A Code session works on the task
375375
2. **Auditing phase** — The Auditor agent reviews changes against project conventions and stored review findings

‎docs/api/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ Model and variant selection follows this priority order:
371371

372372
## Loop
373373

374-
The loop is an iterative development system with four phases, ending with an optional post-completion action:
374+
The loop is an iterative development system with five persisted phases (`coding`, `auditing`, `final_auditing`, `final_audit_fix`, `post_action`), ending with an optional post-completion action:
375375

376376
1. **Coding phase** — A Code session works on the task
377377
2. **Auditing phase** — The Auditor agent reviews changes against project conventions and stored review findings

‎docs/api/_media/architecture.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ OpenCode Forge uses `bun:sqlite` for all data persistence. The storage layer is
167167
- `initializeDatabase(dataDir, options)` - Creates SQLite DB in the data directory
168168
- `closeDatabase()` - Closes database connections on shutdown
169169
- `resolveDataDir()` - Resolves platform-appropriate data directory (`~/.local/share/opencode/forge`)
170-
- Migrations are sequential SQL files (numbered 100-131) tracked in a `migrations` table
170+
- Migrations are registered explicitly in execution order (ids 100-143; not every id ships a SQL file) and tracked in a `migrations` table
171171

172172
### Repository Pattern
173173

@@ -179,6 +179,8 @@ All data access goes through typed repository interfaces created via factory fun
179179
| `PlansRepo` | CRUD for plans | `PlanRow`, `PlansRepo` |
180180
| `ReviewFindingsRepo` | CRUD for review findings | `ReviewFindingRow`, `ReviewFindingsRepo` |
181181
| `SectionPlansRepo` | CRUD for milestone (section) plans used in decomposed loops | `SectionPlanRow`, `SectionPlansRepo` |
182+
| `LoopTransitionsRepo` | Append-only loop phase-transition log | `LoopTransitionRow` |
183+
| `PlanAmendmentsRepo` | Append-only audit trail of mid-loop plan amendments | `PlanAmendmentRow` |
182184
| `LoopSessionUsageRepo` | Per-session token/cost usage across rotated loop sessions | `LoopSessionUsageRow`, `LoopUsageAggregate` |
183185
| `TuiPrefsRepo` | TUI preferences persistence | `TuiPrefsRepo` |
184186

‎docs/api/_media/loop-system.md‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ stateDiagram-v2
4040
Auditing --> Auditing: next section
4141
Auditing --> FinalAuditing: last section clean
4242
Auditing --> [*]: audit clear
43-
FinalAuditing --> Coding: final audit dirty (fix mode)
44-
Coding --> FinalAuditing: final-audit fix complete
43+
FinalAuditing --> FinalAuditFix: final audit dirty
44+
FinalAuditFix --> FinalAuditing: fix pass idle complete
45+
FinalAuditing --> Auditing: plan amendment appended sections
4546
FinalAuditing --> PostAction: final audit clean
4647
note right of PostAction: Only when loop.postAction.enabled
4748
PostAction --> [*]: post-action complete
4849
Coding --> [*]: max iterations / retry limit / stall timeout / cancellation
4950
Auditing --> [*]: max iterations / retry limit / stall timeout / cancellation
51+
FinalAuditFix --> [*]: max iterations / retry limit / stall timeout / cancellation
5052
FinalAuditing --> [*]: final audit clean (no post-action)
5153
```
5254

@@ -66,7 +68,7 @@ interface LoopState {
6668
maxIterations: number // Maximum iterations (0 = unlimited)
6769
startedAt: string // ISO timestamp
6870
prompt?: string // Original task prompt
69-
phase: 'coding' | 'auditing' | 'final_auditing' | 'post_action'
71+
phase: 'coding' | 'auditing' | 'final_auditing' | 'final_audit_fix' | 'post_action'
7072
lastAuditResult?: string // Last audit output
7173
errorCount: number // Consecutive error count
7274
auditCount: number // Number of audits completed
@@ -187,7 +189,7 @@ graph TD
187189
G --> H[Branch preserved]
188190
```
189191

190-
When a workspace carries a SHA pin (`extra.startRef`, set by remote loop launches), the new branch is created from that exact commit instead of the clone's current `HEAD`. If the commit is not present locally, the adapter fetches the sync ref (`extra.syncRef`, default `refs/forge/<loopName>`) from the configured git remote first, and fails with a descriptive error when the SHA still cannot be resolved. Existing branches always win — the pin is ignored when the loop branch already exists. On final teardown the sync ref is deleted from the shared git remote. See [Configuration → Remotes](configuration.md#remotes).
192+
When a workspace carries a SHA pin (`extra.startRef`, set by remote loop launches), the new branch is created from that exact commit instead of the clone's current `HEAD`. If the commit is not present locally, the adapter fetches the sync ref (`extra.syncRef`, default `refs/forge/<loopName>`) from the configured git remote first, and fails with a descriptive error when the SHA still cannot be resolved. If the loop branch already exists, its tip must match the pinned SHA — a leftover same-named branch at a different commit fails creation with an actionable error instead of silently running old code (unpinned workspaces still reuse existing branches). On final teardown the sync ref is deleted from the shared git remote. See [Configuration → Remotes](configuration.md#remotes).
191193

192194
Benefits of worktree isolation:
193195
- Isolation from ongoing development
@@ -215,7 +217,15 @@ In user-facing language, a plan is decomposed into **milestones** — ordered un
215217
- `<!-- forge-section -->` markers in the architect plan output
216218
- `section-read` tool reads the current or specified milestone
217219

218-
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.
220+
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 the persisted `final_audit_fix` phase — 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 stopped mid-fix restarts as a coding pass that re-sends the final-audit fix prompt (rebuilt from the persisted `lastAuditResult`).
221+
222+
### Plan Amendments
223+
224+
After decomposition, the *remaining* (not yet started) milestones can still be amended mid-loop: during a section audit, the auditor may call the `plan-adjust` tool to replace the pending section suffix when completed work makes the remaining sections unable to achieve the plan objective as written. The objective and verification criteria are immutable, completed/current sections cannot be changed, goal loops are excluded, and the resulting total is capped at 24 sections. Every amendment is recorded in the `plan_amendments` table with before/after snapshots and a rationale. If an amendment appends sections while the loop is already in `final_auditing`, the loop reverts to `auditing` to execute them.
225+
226+
### Transition Log
227+
228+
Every persisted phase change appends exactly one row to the `loop_transitions` table (event type, transition kind, from/to phase, iteration, section index, and terminal status/reason for terminate rows). The dashboard renders this log as a live state-machine graph with per-edge traversal counts. Transition history survives loop restarts (loop rows are restored in place, never delete+reinserted) but is removed with the loop row by the terminal-loop sweep.
219229

220230
## Completion Conditions
221231

@@ -224,7 +234,7 @@ A loop completes when the active phase emits a clean audit result (optionally fo
224234
- Non-sectioned loops complete on `audit-clear`.
225235
- Sectioned loops advance through clean section audits, then complete on `final-audit-clean`.
226236
- Dirty section audits rotate back to coding for the same section so findings can be addressed.
227-
- 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`.
237+
- Dirty final audits rotate to a coding session in the `final_audit_fix` phase (no section rewind); when the fix coding pass goes idle, the loop returns straight to `final_auditing`.
228238
- After a clean final audit, if `loop.postAction.enabled` is `true` and specifies a `skill` or `prompt`, the loop enters a `post_action` phase that runs inside the worktree before teardown. Completion occurs when the post-action session goes idle (`post-action-complete` event).
229239

230240
## Post-Completion Action Phase

‎docs/api/_media/tools.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ See also: [Agents and Slash Commands](agents-and-commands.md), [Configuration](c
1010
|---|---|---|
1111
| `plan-read` | Read the current session or loop plan, or list/search recent project plans. | [`src/tools/plan-kv.ts`](../src/tools/plan-kv.ts) |
1212
| `section-read` | Read a section plan and status for the active loop session. | [`src/tools/section-read.ts`](../src/tools/section-read.ts) |
13+
| `plan-adjust` | Replace the remaining (not yet started) sections of the active loop plan; auditor-only, logged as a plan amendment. | [`src/tools/plan-adjust.ts`](../src/tools/plan-adjust.ts) |
1314
| `review-write` | Store a review finding. | [`src/tools/review.ts`](../src/tools/review.ts) |
1415
| `review-read` | Read review findings. | [`src/tools/review.ts`](../src/tools/review.ts) |
1516
| `review-delete` | Delete a review finding. | [`src/tools/review.ts`](../src/tools/review.ts) |
@@ -46,6 +47,17 @@ Arguments:
4647
|---|---|
4748
| `section_index` | Optional 0-based section index. If omitted, returns the lowest-index incomplete section. |
4849

50+
### `plan-adjust`
51+
52+
Only callable by the current auditor session of a sectioned plan loop during the `auditing` phase (rejected in goal loops and during the final audit). Replaces the pending section suffix (from the current section + 1 onward); the plan objective and verification are immutable. The resulting total may not exceed 24 sections. Every adjustment is recorded in the `plan_amendments` table with before/after snapshots.
53+
54+
Arguments:
55+
56+
| Argument | Description |
57+
|---|---|
58+
| `sections` | Replacement list of `{ title, content }` for the remaining sections. An empty list removes the entire pending suffix. |
59+
| `rationale` | Why the plan needs adjustment. |
60+
4961
## Review Tools
5062

5163
Review findings are scoped to the current loop when invoked from a loop session. Sectioned loops automatically scope findings to the current section unless overridden.

‎docs/api/functions/createForgePlugin.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **createForgePlugin**(`config`): `Plugin`
1010
11-
Defined in: [index.ts:199](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L199)
11+
Defined in: [index.ts:199](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L199)
1212

1313
Creates an OpenCode plugin instance with loop management and sandboxing.
1414

‎docs/api/functions/createParentSessionLookup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **createParentSessionLookup**(`__namedParameters`): (`sessionId`) => `Promise`\<`string` \| `null`\>
1010
11-
Defined in: [index.ts:53](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L53)
11+
Defined in: [index.ts:53](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L53)
1212

1313
## Parameters
1414

‎docs/api/functions/createSessionDirectoryLookup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **createSessionDirectoryLookup**(`__namedParameters`): (`sessionId`) => `Promise`\<`string` \| `null`\>
1010
11-
Defined in: [index.ts:134](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L134)
11+
Defined in: [index.ts:134](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L134)
1212

1313
## Parameters
1414

‎docs/api/interfaces/CompactionConfig.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Interface: CompactionConfig
88

9-
Defined in: [types.ts:152](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/types.ts#L152)
9+
Defined in: [types.ts:152](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/types.ts#L152)
1010

1111
Configuration for session compaction behavior.
1212

@@ -16,7 +16,7 @@ Configuration for session compaction behavior.
1616

1717
> `optional` **customPrompt?**: `boolean`
1818
19-
Defined in: [types.ts:154](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/types.ts#L154)
19+
Defined in: [types.ts:154](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/types.ts#L154)
2020

2121
Use a custom compaction prompt.
2222

@@ -26,6 +26,6 @@ Use a custom compaction prompt.
2626

2727
> `optional` **maxContextTokens?**: `number`
2828
29-
Defined in: [types.ts:156](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/types.ts#L156)
29+
Defined in: [types.ts:156](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/types.ts#L156)
3030

3131
Maximum context tokens for compaction.

‎docs/api/interfaces/CreateParentSessionLookupOptions.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@
66

77
# Interface: CreateParentSessionLookupOptions
88

9-
Defined in: [index.ts:43](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L43)
9+
Defined in: [index.ts:43](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L43)
1010

1111
## Properties
1212

1313
### client
1414

1515
> **client**: `ForgeClient`
1616
17-
Defined in: [index.ts:44](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L44)
17+
Defined in: [index.ts:44](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L44)
1818

1919
***
2020

2121
### directory
2222

2323
> **directory**: `string`
2424
25-
Defined in: [index.ts:45](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L45)
25+
Defined in: [index.ts:45](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L45)
2626

2727
***
2828

2929
### logger
3030

3131
> **logger**: `object`
3232
33-
Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L47)
33+
Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L47)
3434

3535
#### debug
3636

@@ -92,12 +92,12 @@ Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/
9292

9393
> **loop**: `Loop`
9494
95-
Defined in: [index.ts:46](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L46)
95+
Defined in: [index.ts:46](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L46)
9696

9797
***
9898

9999
### negativeTtlMs?
100100

101101
> `optional` **negativeTtlMs?**: `number`
102102
103-
Defined in: [index.ts:48](https://github.com/chriswritescode-dev/opencode-forge/blob/f95e7c2c73f775c4d7d3d5c8c63957e7a3039691/src/index.ts#L48)
103+
Defined in: [index.ts:48](https://github.com/chriswritescode-dev/opencode-forge/blob/a221960b2db97da192fa8ae878f660ac7cfa0d90/src/index.ts#L48)

0 commit comments

Comments
 (0)