@giamme/forge 0.0.0-stage → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -2
  4. package/SKILL.md +123 -0
  5. package/VERSION +1 -0
  6. package/docs/README.md +26 -0
  7. package/docs/add-ons.md +33 -0
  8. package/docs/artifacts.md +69 -0
  9. package/docs/commands.md +315 -0
  10. package/docs/dataset.md +20 -0
  11. package/docs/decomposed-runs.md +268 -0
  12. package/docs/development.md +95 -0
  13. package/docs/forge-ui.md +92 -0
  14. package/docs/fractal.md +181 -0
  15. package/docs/install.md +160 -0
  16. package/docs/memory.md +156 -0
  17. package/docs/models.md +118 -0
  18. package/docs/reviews.md +231 -0
  19. package/docs/safety.md +55 -0
  20. package/docs/troubleshooting.md +54 -0
  21. package/forge +79 -0
  22. package/package.json +27 -4
  23. package/references/dataset.md +329 -0
  24. package/references/decompose.md +667 -0
  25. package/references/fractal-dashboard.md +21 -0
  26. package/references/fractal.md +307 -0
  27. package/references/harnesses.md +365 -0
  28. package/references/jev.md +865 -0
  29. package/references/memory.md +228 -0
  30. package/references/planning.md +42 -0
  31. package/references/ripwire.md +47 -0
  32. package/references/runtime.md +64 -0
  33. package/references/solo.md +113 -0
  34. package/references/ui.md +112 -0
  35. package/registry.tsv +81 -0
  36. package/scripts/forge-artifact.sh +78 -0
  37. package/scripts/forge-contract.py +430 -0
  38. package/scripts/forge-dataset.py +6 -0
  39. package/scripts/forge-dispatch.sh +671 -0
  40. package/scripts/forge-fail-first.py +195 -0
  41. package/scripts/forge-fractal-dispatch.sh +4 -0
  42. package/scripts/forge-fractal-options.sh +54 -0
  43. package/scripts/forge-fractal.py +6 -0
  44. package/scripts/forge-guard.py +266 -0
  45. package/scripts/forge-install-fractal.sh +4 -0
  46. package/scripts/forge-install-ripwire.sh +52 -0
  47. package/scripts/forge-install.sh +135 -0
  48. package/scripts/forge-jev-options.sh +259 -0
  49. package/scripts/forge-jev.py +6 -0
  50. package/scripts/forge-lib-plans.sh +785 -0
  51. package/scripts/forge-lib-review.sh +494 -0
  52. package/scripts/forge-lib-task.sh +302 -0
  53. package/scripts/forge-lib-verify.sh +30 -0
  54. package/scripts/forge-memory.sh +575 -0
  55. package/scripts/forge-metrics.sh +39 -0
  56. package/scripts/forge-parallel-options.sh +76 -0
  57. package/scripts/forge-parallel.sh +1717 -0
  58. package/scripts/forge-plan-check.py +118 -0
  59. package/scripts/forge-prompt.py +45 -0
  60. package/scripts/forge-ripwire.py +136 -0
  61. package/scripts/forge-route.py +344 -0
  62. package/scripts/forge-runs.py +121 -0
  63. package/scripts/forge-runtime.py +586 -0
  64. package/scripts/forge-schedule.py +312 -0
  65. package/scripts/forge-shadow.py +305 -0
  66. package/scripts/forge-solo.sh +347 -0
  67. package/scripts/forge-ui-options.sh +42 -0
  68. package/scripts/forge-ui.py +435 -0
  69. package/scripts/forge-update.sh +71 -0
  70. package/scripts/forge-verify-coverage.py +956 -0
  71. package/scripts/forge-version.py +240 -0
  72. package/scripts/forge_dataset/__init__.py +439 -0
  73. package/scripts/forge_dataset/capture.py +330 -0
  74. package/scripts/forge_dataset/checks.py +109 -0
  75. package/scripts/forge_dataset/cli.py +233 -0
  76. package/scripts/forge_dataset/export.py +1009 -0
  77. package/scripts/forge_dataset/harvest.py +716 -0
  78. package/scripts/forge_dataset/importer.py +412 -0
  79. package/scripts/forge_dataset/outcomes.py +275 -0
  80. package/scripts/forge_dataset/routing.py +278 -0
  81. package/scripts/forge_fractal/__init__.py +84 -0
  82. package/scripts/forge_fractal/bridge.py +46 -0
  83. package/scripts/forge_fractal/cli.py +202 -0
  84. package/scripts/forge_fractal/dashboard.css +138 -0
  85. package/scripts/forge_fractal/dashboard.html +41 -0
  86. package/scripts/forge_fractal/dashboard.js +687 -0
  87. package/scripts/forge_fractal/decomposition.py +262 -0
  88. package/scripts/forge_fractal/execution.py +659 -0
  89. package/scripts/forge_fractal/inspection.py +261 -0
  90. package/scripts/forge_fractal/install.py +158 -0
  91. package/scripts/forge_fractal/invocation.py +94 -0
  92. package/scripts/forge_fractal/pipeline.py +124 -0
  93. package/scripts/forge_fractal/selection.py +179 -0
  94. package/scripts/forge_jev/__init__.py +376 -0
  95. package/scripts/forge_jev/backtest.py +322 -0
  96. package/scripts/forge_jev/cli.py +980 -0
  97. package/scripts/forge_jev/client.py +418 -0
  98. package/scripts/forge_jev/corpus.py +156 -0
  99. package/scripts/forge_jev/coupling.py +244 -0
  100. package/scripts/forge_jev/features.py +24 -0
  101. package/scripts/forge_jev/gates.py +100 -0
  102. package/scripts/forge_jev/memory.py +163 -0
  103. package/scripts/forge_jev/outcome.py +49 -0
  104. package/scripts/forge_jev/predict.py +204 -0
  105. package/scripts/forge_jev/questions.py +661 -0
  106. package/scripts/forge_jev/review.py +73 -0
  107. package/scripts/forge_jev/routing.py +271 -0
  108. package/scripts/forge_jev/subset.py +123 -0
  109. package/scripts/forge_jev/verify.py +453 -0
  110. package/scripts/forge_registry.py +140 -0
  111. package/ui/README.md +347 -0
  112. package/ui/THIRD_PARTY_NOTICES.md +177 -0
  113. package/ui/docs/api.md +970 -0
  114. package/ui/forge_ui/__init__.py +20 -0
  115. package/ui/forge_ui/__main__.py +135 -0
  116. package/ui/forge_ui/diffs.py +454 -0
  117. package/ui/forge_ui/executions.py +436 -0
  118. package/ui/forge_ui/export.py +214 -0
  119. package/ui/forge_ui/findings.py +132 -0
  120. package/ui/forge_ui/fractal.py +582 -0
  121. package/ui/forge_ui/models.py +216 -0
  122. package/ui/forge_ui/reader.py +1424 -0
  123. package/ui/forge_ui/server.py +312 -0
  124. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  125. package/ui/forge_ui/static/app.js +1079 -0
  126. package/ui/forge_ui/static/cloud3d.js +647 -0
  127. package/ui/forge_ui/static/cloudmodel.js +375 -0
  128. package/ui/forge_ui/static/cloudui.js +397 -0
  129. package/ui/forge_ui/static/convo.js +295 -0
  130. package/ui/forge_ui/static/datatable.js +232 -0
  131. package/ui/forge_ui/static/diffview.js +235 -0
  132. package/ui/forge_ui/static/favicon.svg +1 -0
  133. package/ui/forge_ui/static/files.js +776 -0
  134. package/ui/forge_ui/static/findings3d.js +134 -0
  135. package/ui/forge_ui/static/gantt.js +97 -0
  136. package/ui/forge_ui/static/graph.js +266 -0
  137. package/ui/forge_ui/static/icons.js +94 -0
  138. package/ui/forge_ui/static/index.html +67 -0
  139. package/ui/forge_ui/static/iso.js +816 -0
  140. package/ui/forge_ui/static/leaderboard.js +311 -0
  141. package/ui/forge_ui/static/map3d.js +45 -0
  142. package/ui/forge_ui/static/modelstats.js +249 -0
  143. package/ui/forge_ui/static/notify.js +179 -0
  144. package/ui/forge_ui/static/panel.js +562 -0
  145. package/ui/forge_ui/static/runmodel.js +667 -0
  146. package/ui/forge_ui/static/scene3d.js +1471 -0
  147. package/ui/forge_ui/static/search.js +120 -0
  148. package/ui/forge_ui/static/shell.js +137 -0
  149. package/ui/forge_ui/static/stagectl.js +150 -0
  150. package/ui/forge_ui/static/style.css +1073 -0
  151. package/ui/forge_ui/static/theme.js +25 -0
  152. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,667 @@
1
+ # Decomposed runs
2
+
3
+ Mechanics behind `--decompose-level`: how a goal becomes tasks, how tasks are routed to
4
+ dwarves by difficulty, how waves are computed, and what happens when something fails.
5
+
6
+ Read this when writing a decomposition, when routing does something unexpected, or when a
7
+ run leaves branches behind.
8
+
9
+ ## Contents
10
+
11
+ - [The two axes: decompose-level and difficulty](#the-two-axes-decompose-level-and-difficulty)
12
+ - [tasks.tsv](#taskstsv)
13
+ - [Routing](#routing)
14
+ - [Fractal within decomposed tasks](#fractal-within-decomposed-tasks)
15
+ - [Waves and disjointness](#waves-and-disjointness)
16
+ - [Task size and acceptance criteria](#task-size-and-acceptance-criteria)
17
+ - [The capsule](#the-capsule)
18
+ - [Planning](#planning)
19
+ - [QA verdicts](#qa-verdicts)
20
+ - [Severity, thresholds and known issues](#severity-thresholds-and-known-issues)
21
+ - [Retrying, and resuming an interrupted run](#retrying-and-resuming-an-interrupted-run)
22
+ - [Infrastructure failures](#infrastructure-failures)
23
+ - [Implementer guards](#implementer-guards)
24
+ - [Accepting, splitting, combining and the whole-run review](#accepting-splitting-combining-and-the-whole-run-review)
25
+ - [Branches, worktrees, cleanup](#branches-worktrees-cleanup)
26
+ - [Failure modes](#failure-modes)
27
+
28
+ ## The two axes: decompose-level and difficulty
29
+
30
+ These are independent and easy to conflate.
31
+
32
+ **`--decompose-level`** controls how finely the goal is split:
33
+
34
+ | Level | Split |
35
+ |-------|-------|
36
+ | `low` | only where the goal contains obviously independent pieces; 2–3 coarse tasks |
37
+ | `medium` | one task per coherent unit (module, feature slice); typically 4–6 |
38
+ | `high` | finest split where each task is still independently reviewable *and* independently correct; typically 7–12 |
39
+
40
+ At every level, two rules override the target count: never split so fine that a task cannot
41
+ be verified on its own, and never emit two tasks that must edit the same file — make it one
42
+ task, or add a dep. The counts are guidance, not quotas; a goal with three natural seams
43
+ produces three tasks at `high` too.
44
+
45
+ **`difficulty`** is rated per task, by what the task demands of the model — **not** by how
46
+ much code it produces:
47
+
48
+ | Difficulty | Means |
49
+ |------------|-------|
50
+ | `low` | mechanical and local: rename, move, docs, a test for behaviour that already exists. Little design judgment; a mistake is obvious. |
51
+ | `medium` | self-contained implementation against a clear spec; some design choices, bounded blast radius. |
52
+ | `high` | needs design judgment, touches cross-cutting behaviour, has subtle edge cases or state/concurrency, or the right approach is not obvious from the goal. |
53
+
54
+ A five-hundred-line mechanical rename is `low`. A ten-line concurrency fix is `high`. Sizing
55
+ by diff length instead of by judgment required is the main way difficulty routing stops
56
+ paying off — it sends the cheap model at the subtle problem and the expensive one at the
57
+ boilerplate, which is worse than not routing at all.
58
+
59
+ ## tasks.tsv
60
+
61
+ Tab-separated, one row per task, written by whoever decomposes the goal:
62
+
63
+ ```
64
+ # id deps difficulty files dwarf qa title
65
+ mul - high src/calc.py - - Add multiply() to the calculator
66
+ docs - low README.md - - Document the calculator API
67
+ tests mul medium tests/test_calc.py - - Tests for multiply()
68
+ ```
69
+
70
+ | Column | Meaning |
71
+ |--------|---------|
72
+ | `id` | short slug, unique; also the worktree and branch name |
73
+ | `deps` | comma-list of task ids that must land first, or `-` |
74
+ | `difficulty` | `low` \| `medium` \| `high` |
75
+ | `files` | expected touch set, comma-separated. Drives disjointness — see below |
76
+ | `dwarf` / `qa` | `-` to resolve from the routing rules; an explicit spec always wins |
77
+ | `title` | one line, shown in the approval table and the capsule |
78
+
79
+ Each task also needs `tasks/<id>/prompt.md` — the implementation instruction, written the
80
+ same way a single-task forge goal is. `tasks/<id>/approach.md` is optional: a few lines of
81
+ intended approach, written by the orchestrator or by `--planner`. When present it is shown
82
+ in the approval table, prepended to the dwarf's prompt, and given to qa as the intent to
83
+ review against — so a review can say "this works but abandons the planned shape", which
84
+ matters when a sibling task was planned against that shape.
85
+
86
+ `files` is a promise, not a prediction: it is what the wave planner trusts when deciding what
87
+ may run concurrently, and it is what the capsule tells other dwarves not to touch. An
88
+ understated `files` is how two dwarves end up in the same file and produce a conflict.
89
+
90
+ The promise is checked. After each dwarf finishes, the runner compares what the task
91
+ *declared* against what it actually touched (a declared directory covers the paths beneath
92
+ it, so `src/routes` covers `src/routes/api.py`). Undeclared paths go into
93
+ `tasks/<id>/drift.txt`, into the run summary, and into the QA prompt — which already asks
94
+ whether the diff went outside the task's scope and previously had no way to know. It is a
95
+ warning and never a failure: a dwarf that genuinely had to touch one more file did the right
96
+ thing. What it buys is that a merge `CONFLICT` two waves later arrives with its cause
97
+ already named, instead of leaving someone to diff two branches to find out.
98
+
99
+ `plan` resolves difficulty into concrete specs and writes them back into the `dwarf`/`qa`
100
+ columns, so `run` never consults the routing rules. This is also what makes a gate override
101
+ durable: an explicit value is preserved across a re-plan, while `UNASSIGNED` is treated as a
102
+ leftover marker and re-resolved.
103
+
104
+ ## Routing
105
+
106
+ ```
107
+ --dwarf <spec> fallback for any tier without its own rule
108
+ --dwarf-high <spec> used for high-difficulty tasks
109
+ --dwarf-medium <spec>
110
+ --dwarf-low <spec>
111
+ --qa, --qa-high, --qa-medium, --qa-low identical grammar
112
+ ```
113
+
114
+ Each flag takes an ordinary forge spec (`alias[:effort[:harness]]`), so a tier can name a
115
+ model *and* the harness it runs through. A comma-list pools several models within one tier
116
+ and round-robins across them — useful when a tier has more tasks than one provider should
117
+ carry, since distinct harnesses have independent quotas.
118
+
119
+ Resolution order per task: explicit column value → the tier's rule → plain `--dwarf` → and if
120
+ none of those exist, `UNASSIGNED`. An `UNASSIGNED` task makes `plan` exit 2 and `run` refuse
121
+ outright. Forge does not invent a model when the user has not said which one should spend
122
+ their quota.
123
+
124
+ QA has no tier rules by default and falls back to `opus`, matching single-task forge.
125
+
126
+ ### Exploratory routing (on by default)
127
+
128
+ Every task in the run history ran on the model its tier was routed to, so the history
129
+ says how that model did and nothing about how another would have done on the same task
130
+ (only 9 tasks ever ran on more than one spec). Choosing models from measured pass rates
131
+ needs some randomized picks, so `plan` makes a few: `--explore E`, with E between 0 and
132
+ 0.5, defaults to **0.2**.
133
+
134
+ - **Which rows:** only rows whose dwarf column is `-` (left to forge). The pool's own turn
135
+ is still taken, so the round robin of every other row is unchanged.
136
+ - **Two candidates:** the pool's pick and its *neighbour*, the model one tier away in the
137
+ pools given to this plan: the next cheaper tier for medium and high rows ("is a cheaper
138
+ model enough?"), the next stronger for low rows ("did it need more?"). Within a tier the
139
+ round robin already alternates between pool members; across tiers nothing else ever
140
+ produces data. The neighbour keeps the pick's position in its pool, so
141
+ `--dwarf-low luna,haiku --dwarf-medium sol,sonnet` pairs luna with sol and haiku with
142
+ sonnet. A different effort is a different candidate, so pools that differ only in
143
+ effort (`--dwarf-low sol --dwarf-high sol:xhigh`) compare the two efforts. A row whose
144
+ tier has no neighbour is not explored.
145
+ - **How often:** with probability E the row is drawn uniformly between the two, so it
146
+ actually changes model with probability E/2 (10% at the default).
147
+ - **Never a model you did not name.**
148
+ - **The table:** an explored row is marked `*` in the approval table, and a footer states
149
+ the active setting, so the spend is visible before anything runs. To pin another model,
150
+ put its spec in the dwarf column and re-plan.
151
+ - **The record:** each decision goes to `tasks/<id>/route.jsonl` (append-only) with its
152
+ two candidates, the pool's own pick, the choice, and its probability:
153
+ `(1 - E)·[choice = pool's pick] + E / 2`.
154
+ - **Turning it off:** `--explore 0`. The choice is kept in the plan dir (`explore`), so a
155
+ later re-plan neither turns it back on nor changes rows already resolved. Precedence:
156
+ the flag, then the plan's earlier choice, then `FORGE_ROUTE_EXPLORE`, then 0.2.
157
+ - **Later changes:** a re-plan draws nothing for rows that are already resolved. An
158
+ approval-gate edit or a `retry --dwarf` simply runs a different model; the analysis
159
+ keeps such a task in the arm it was assigned and reports it as not followed.
160
+
161
+ `forge dataset routing` reads the draws back: per (tier, pool pick, neighbour) pair, each
162
+ arm's first-pass rate (inverse-propensity weighted, every task in the arm it was assigned),
163
+ attrition, runtime and a run-clustered interval, judged at the first of 30, 60 or 120
164
+ neighbour assignments it reaches. See [Dataset](dataset.md#routing). At the default rate
165
+ that is weeks of normal use per pair; a pair that matters can be given more with
166
+ `--explore 0.5` on its plans.
167
+
168
+ ## Fractal within decomposed tasks
169
+
170
+ Select optional Fractal execution on `run`; planning the task table does not activate it.
171
+ `--decompose-level` defines the top-level Forge tasks and approval table. Each such task
172
+ can then use a Fractal implementation node with bounded children inside its declared `files`.
173
+ Each top-level task still returns its combined candidate to Forge's independent QA and
174
+ existing integration checks. Child completion never marks the Forge task accepted.
175
+
176
+ Use `--fractal-auto-decompose` on `run` to require recursive split-or-atomic decisions
177
+ automatically. Add `--fractal-planner <spec>` to use a stronger decision model for all
178
+ nodes; otherwise the saved top-level planner or each task's initial root model is reused.
179
+ Show that choice and the bounds in the approval table. See the
180
+ [decision contract](fractal.md#automatic-decomposition-decisions) for recovery and limits.
181
+
182
+ ```bash
183
+ # With tasks.tsv, goal.txt, request.md and per-task prompts already prepared:
184
+ bash <skill_dir>/scripts/forge-parallel.sh plan "$PLAN" --repo "$REPO" --decompose-level medium \
185
+ --dwarf sol:high --dwarf-low luna:medium --dwarf-high sol:xhigh,terra:xhigh --qa opus
186
+
187
+ # Preview the selected backend, pools, bounds and workspace without provider calls.
188
+ bash <skill_dir>/scripts/forge-parallel.sh run "$PLAN" --fractal --dry-run
189
+
190
+ # After approval of the task/model table:
191
+ bash <skill_dir>/scripts/forge-parallel.sh run "$PLAN" --fractal \
192
+ --max-parallel 3 --fractal-concurrency 3 --fractal-depth 2
193
+ ```
194
+
195
+ Add `--ui` to `plan` to open the plan in [forge-ui](ui.md): the `forge-ui:` URL is printed
196
+ under the task table, so the user can watch the later `run`. Ask once when the plan is made,
197
+ not on `run`, `retry` or resume; it is fail-open and not remembered.
198
+
199
+ `plan` saves tier pools in `fractal-routing.json`. The first enabled execution freezes
200
+ eligible specs, canonical resolutions and limits with the run and preflights every
201
+ eligible model/harness. Children route by difficulty, then the configured general dwarf,
202
+ then their parent's dwarf. A retry cannot introduce a model outside that frozen pool.
203
+
204
+ `--max-parallel` bounds top-level task pipelines. `--fractal-concurrency` separately bounds
205
+ all active model invocations across their trees, including QA. Parents release their model
206
+ slot while waiting; nesting cannot multiply that limit. Child dependencies must be satisfied,
207
+ ownership must remain inside the parent, and overlapping children are serialized.
208
+
209
+ The choice is persisted in `fractal-selection.json`. Explicit `--fractal` or `--no-fractal`
210
+ skips the once-per-interactive-run question; unattended execution without either is off.
211
+ Installation never activates the backend. The [Fractal reference](fractal.md) documents
212
+ all limits, prerequisites and the child-request protocol.
213
+
214
+ ## Waves and disjointness
215
+
216
+ A wave is the largest set of tasks whose deps are all satisfied by earlier waves **and** whose
217
+ `files` sets are pairwise disjoint. Everything else waits.
218
+
219
+ Disjointness — not the task count — is what actually bounds parallelism. Two dwarves editing
220
+ one file produce a conflict no reviewer can untangle and no merge can resolve honestly, so
221
+ they are placed in different waves however the goal was decomposed. When this happens the
222
+ deferral is reported in the approval table:
223
+
224
+ ```
225
+ deferrals (serialized to protect the merge):
226
+ wave 1: lint deferred (file overlap with a task already in this wave)
227
+ ```
228
+
229
+ That line matters: a decomposition that looks parallel but serializes at run time should say
230
+ so, rather than leaving the user to wonder why eight tasks took eight rounds.
231
+
232
+ Waves remain a planning preview. At execution time a Python 3 standard-library coordinator
233
+ starts a task as soon as every prerequisite is MERGED, capacity is available, and its paths
234
+ do not overlap an active task. Each passing task merges serially through the existing
235
+ fingerprint checks. A slow unrelated task no longer holds up eligible dependents.
236
+
237
+ Each task's baseline is pinned immediately before dispatch and stays fixed across retries.
238
+
239
+ ## Task size and acceptance criteria
240
+
241
+ Size predicts a failed first attempt better than anything forge asks a model. In the run
242
+ history (269 first attempts, 2026-10-17), the share a reviewer passed:
243
+
244
+ | prompt.md | passed | | declared files | passed |
245
+ |---|---|---|---|---|
246
+ | under 3K chars | 62% (n=50) | | 1 | 83% (n=12) |
247
+ | 3–5K | 56% (n=48) | | 2 | 68% (n=25) |
248
+ | 5–7K | 51% (n=49) | | 3–5 | 52% (n=75) |
249
+ | 7–10K | 33% (n=49) | | 6–8 | 40% (n=50) |
250
+ | 10–14K | 28% (n=29) | | 9 or more | 29% (n=107) |
251
+ | 14K and over | 23% (n=44) | | | |
252
+
253
+ Size and difficulty are confounded (high tasks are longer), but splitting reduces both.
254
+ So:
255
+
256
+ - Split along independently verifiable behaviour and explicit interfaces, not to make more
257
+ parallel tasks. A task should still be reviewable and correct on its own.
258
+ - End every `prompt.md` with an `## Acceptance criteria` section: observable checks the
259
+ reviewer can verify (a behaviour, a command that passes, a file that exists), not
260
+ restated goals.
261
+ - `plan` prints an advisory block under the table naming tasks at 7,000 characters or 9
262
+ declared files and over, with the history for their size, and tasks with no acceptance
263
+ criteria. A heading inside a code fence, or with nothing under it, does not count. The
264
+ block never changes the table or the exit code.
265
+
266
+ ## The capsule
267
+
268
+ Every dispatch is a clean slate — forge never passes `--continue`, `--resume`, or
269
+ `codex exec resume`. That keeps one task's confused turn from poisoning another and keeps
270
+ each task's context small, but it also leaves each dwarf blind to the wider run.
271
+
272
+ `tasks/<id>/capsule.md` records ownership and baseline context. `baseline.capsule` preserves
273
+ the implementation-time capsule for QA. MERGED means the reviewed commit is an ancestor of
274
+ this task's pinned baseline, not merely that another task finished while QA was preparing.
275
+ Complete requirements, approach, retry findings and distinct memory facts are assembled once
276
+ per role; implementation and review instructions are separate.
277
+
278
+ It exists to prevent two specific failures that clean slates create:
279
+
280
+ - a dwarf reimplementing a helper an earlier task already merged, because nothing told it
281
+ that task was done;
282
+ - a dwarf editing a file another dwarf currently owns — producing exactly the conflict the
283
+ wave planner exists to avoid.
284
+
285
+ For QA the capsule additionally scopes the review: code from `MERGED` tasks is already in the
286
+ base and is not this task's bug. Without that, a wave-2 reviewer reports wave-1 code as
287
+ defects in the diff it was handed.
288
+
289
+ Keep it short. It is paid for on all 2N dispatches, so it is a status table and a few rules,
290
+ not a design document.
291
+
292
+ ## Planning
293
+
294
+ The capsule stops two dwarves editing one *file*. It does nothing about two dwarves
295
+ inventing incompatible *interfaces* at a seam they share: both stay inside their own
296
+ `files`, both pass their own QA, and the mismatch only appears at integration.
297
+
298
+ Writing the approach down before dispatch is what closes that. By default the orchestrator
299
+ does it while decomposing, which costs nothing. `--planner <spec>` dispatches it instead —
300
+ **one dispatch for the whole run, never one per task**, because the value is that a single
301
+ mind designed both sides of every seam. N independent planners would recreate the problem
302
+ they were meant to solve.
303
+
304
+ The planner runs with qa's permission profile: it reads the repo and writes nothing to it.
305
+ Its output becomes `tasks.tsv` plus each `tasks/<id>/approach.md`, and the approval gate
306
+ renders both — the last moment the plan can be changed for free.
307
+
308
+ ## QA verdicts
309
+
310
+ Decompose-mode QA prompts require a final line:
311
+
312
+ ```
313
+ FORGE_VERDICT: PASS no confirmed correctness bug (style nits are not failures)
314
+ FORGE_VERDICT: FAIL at least one CONFIRMED correctness bug
315
+ ```
316
+
317
+ The runner accepts only a standalone verdict on the final nonempty line (the shared parser
318
+ also tolerates the dash gloss the prompt prints after each verdict, trailing whitespace and
319
+ CRLF). A missing verdict is `UNKNOWN` and is treated exactly
320
+ like a failure — excluded from integration and flagged. Merging a diff whose reviewer never
321
+ reached a conclusion would defeat the point of reviewing it, so the ambiguous case fails
322
+ safe rather than optimistically.
323
+
324
+ A failing task is excluded and its branch preserved; the rest of the run proceeds. There is
325
+ no automatic repair pass unless the run was started with `--retry-failed N`, consistent with
326
+ forge's rule that a dwarf → QA cycle does not loop on its own initiative.
327
+
328
+ The prompt contract (preamble, findings format, severity definitions and the verdict lines)
329
+ comes from one place, `scripts/forge-contract.py`, shared by solo, decomposed and the final
330
+ review; the implementer rules appended to every dwarf prompt come from the same module.
331
+
332
+ ### Fail-first evidence (opt-in)
333
+
334
+ `plan --fail-first` (and `forge-solo.sh --fail-first`) checks that the tests a change adds
335
+ or edits can fail. Before each review, forge exports the reviewed commit and, separately,
336
+ the task's base plus only the change's test-path files (tests, their fixtures and helpers),
337
+ then runs `.forge/verify-subset` (the per-file test command with `{files}`, see
338
+ [Jev](jev.md)) once per new or edited test file in each. Every file gets one result in
339
+ `tasks/<id>/failfirst.json` (solo: `<run>/failfirst.json`):
340
+
341
+ - `proven`: passes with the change, fails without it;
342
+ - `passes_without_change`: passes both ways, so it does not prove the change;
343
+ - `inconclusive`: fails even with the change (a broken test or a missing dependency);
344
+ - `error`: could not run (export failure, or `FORGE_FAIL_FIRST_TIMEOUT`, default 600 s, per run).
345
+
346
+ The reviewer's prompt lists the results and asks for a `[P2][CONFIRMED]` finding for each
347
+ test that passes without the change, unless the reviewer explains why it need not fail
348
+ (a refactor kept green). It is advice: forge never changes a status or verdict from it, and
349
+ a P2 only blocks through the reviewer and the usual `--qa-threshold` gate. The exports are
350
+ throwaway directories outside the review snapshot, so no fingerprint changes. forge-ui
351
+ shows the evidence under the task's review findings, and each check is recorded in the
352
+ run dataset as a `fail_first` event with the JSON attached. Without
353
+ `.forge/verify-subset` the check records `skipped` and the prompt says nothing.
354
+ `plan --no-fail-first` turns it off again.
355
+
356
+ ### Shadow reviews (opt-in)
357
+
358
+ `plan --shadow-qa <spec>[,<spec>...] [--shadow-rate R]` sends a sampled share of
359
+ reviewed attempts (R, default 0.1) to a second, independent reviewer. Its verdict is
360
+ recorded in the run dataset and never read by the run. It exists to measure how often two
361
+ reviewers agree on one diff, which is the noise in every review-based label. The approval
362
+ table shows the reviewers, the rate and the expected extra reviews, and `--shadow-qa none`
363
+ turns shadow reviews off again.
364
+
365
+ - **Which reviewer:** the first listed spec whose model family (Claude, GPT, Gemini, ...)
366
+ differs from the task's reviewer, so the two do not share one blind spot. If none
367
+ differs, the first listed spec. Forge only ever uses the specs you list. A spec that
368
+ cannot run here (`doctor` fails) is dropped with a note and never fails the run.
369
+ - **Which attempts:** a deterministic sample per (run, task, attempt), so a QA-only
370
+ resume decides the same way.
371
+ - **Same evidence, separate everything else.** The shadow reviews a byte-identical copy of
372
+ the primary's prompt, in its own snapshot, from its own dir
373
+ (`<plan>/shadow/<task>/<attempt>-<k>/`). Nothing reads that dir. It writes no status,
374
+ memory, ledger, gate or retry findings, and a reviewer that edits its snapshot is
375
+ recorded as `invalid`.
376
+ - **Bounded:** it runs beside the primary, and the task waits for it before writing any
377
+ status. Once the primary is done, the shadow gets as long again as the primary took
378
+ (at least 5 minutes), then it is stopped. It dies with its task, including on Ctrl-C of
379
+ a manual `retry`.
380
+ - **Scope:** parallel mode only, and never under Fractal.
381
+
382
+ `./forge dataset status` reports the agreement rate, overall and per model pair. The export
383
+ adds `shadow` and `consensus` to the matching review cases' metadata.
384
+
385
+ ## Severity, thresholds and known issues
386
+
387
+ QA labels each finding `- [P0..P3][CONFIRMED|PLAUSIBLE] file:line — what breaks (input)`:
388
+ P0 data loss, security hole, crash or wrong result on the main path; P1 wrong behaviour on
389
+ realistic input or a broken existing contract; P2 edge-case bug, unusual-timing race, missing
390
+ hardening; P3 minor. Severity is impact; CONFIRMED/PLAUSIBLE is confidence.
391
+
392
+ `run`/`retry --qa-threshold Pn` (remembered in `<plan>/qa_threshold`; `none` clears it) sets
393
+ the **least severe level that still blocks**. `tasks/<id>/qa_threshold` overrides it per task —
394
+ the way to give a dev-tooling or investigation task a looser bar than production code,
395
+ which otherwise gets a new finding in a new corner of a non-default option every round and
396
+ only a human decision ends it. The orchestrator decides *deterministically* (`forge-contract.py gate`),
397
+ it does not take the reviewer's word: with a threshold set, a `FAIL` becomes `PASS` with known
398
+ issues only when there is at least one CONFIRMED finding, every CONFIRMED finding carries a
399
+ `P0..P3` label, and all of them are strictly milder than the threshold. Anything unlabelled,
400
+ any blocking finding, or a `FAIL` with no CONFIRMED finding stays a failure. The task's status is
401
+ then the literal `PASS`; `known_issues.md`, `qa.gate` and the summary say what was tolerated.
402
+ Without a threshold none of this applies.
403
+
404
+ ## Retrying, and resuming an interrupted run
405
+
406
+ A failed task keeps its branch and its worktree. `retry` is what acts on them:
407
+
408
+ ```bash
409
+ forge-parallel.sh retry <plan-dir> <task-id> [--dwarf <spec>]
410
+ ```
411
+
412
+ It re-dispatches that one task **in the worktree it already has**, with the previous
413
+ reviewer's findings prepended to the prompt — that carry-forward is the whole difference
414
+ between a retry and simply running the task again. It then re-reviews, and merges if the
415
+ verdict is now `PASS`. `--dwarf` escalates to a stronger model and is written back into
416
+ `tasks.tsv`, so the table shows the model that will actually be spent and the escalation
417
+ survives a re-plan.
418
+
419
+ Two details that are load-bearing rather than incidental:
420
+
421
+ - **The base is pinned per task, not per run.** A retry diffs against the commit the task
422
+ was originally branched from, so its reviewer sees the task's *cumulative* work. Reviewing
423
+ only the fix would let the first attempt's code through unread, and re-basing onto the
424
+ integration branch as it now stands would put other tasks' merged code into this task's
425
+ diff.
426
+ - **An identical retry is not re-reviewed.** If the diff comes back byte-identical to the
427
+ previous attempt, the task fails again without a QA dispatch. Paying a reviewer to read
428
+ the same code twice buys nothing.
429
+
430
+ `retry` is a command a human types. Forge does not loop a dwarf against its own reviewer on
431
+ its own initiative; the one exception is the opt-in `run --retry-failed N` below.
432
+
433
+ **`UNKNOWN` and `INFRA` resume at the review stage.** If the reviewer never gave a verdict (or
434
+ its dispatch stopped on an infrastructure failure after the dwarf finished), the work was not
435
+ rejected, so `retry` / `--retry-failed` re-runs **QA only** on the unchanged, already-committed
436
+ work: no dwarf, no new commit, no attempt spent. A `tasks/<id>/resume` marker records this and is
437
+ revalidated against the branch tip and the saved source fingerprint before it is trusted; if the
438
+ worktree changed, the full task runs. Findings are carried only from a real review.
439
+
440
+ **In-run retries.** `run <plan> --retry-failed N` re-queues a task whose reviewer said
441
+ `FAIL`/`UNKNOWN` as soon as it ends, up to `N` more times in that run, preparing the retry exactly
442
+ as `retry` does (same findings carry-forward, same pinned base, same cumulative review). Other
443
+ tasks keep running; dependents wait for the task to merge. It applies per invocation (not
444
+ remembered), never to `TIMEOUT`/`ERROR`/`INVALIDATED`/`CONFLICT`, and never after a retry that
445
+ changed nothing (`tasks/<id>/noretry`). Every entry is logged in `tasks/<id>/attempts.tsv`.
446
+
447
+ **Escalation after repeated failures.** In forge's history the first three attempts at a task
448
+ pass review at about the same rate (43%, 42%, 47%), and the fourth and later at 29%, whether
449
+ or not the model changed. So a retry does not repeat a dispatch that has already failed three
450
+ times on one model (`--escalate-after N` on `run` or `retry`, kept in the plan, default 3, `0`
451
+ turns it off):
452
+
453
+ - **Counting:** the count is the task's failed attempts (FAIL, or an implementer TIMEOUT) on its current model,
454
+ from the fifth column of `attempts.tsv`. Infrastructure stops, missing verdicts and
455
+ forge's own errors judge nothing and are skipped; a pass or a change of model resets it.
456
+ - **Escalation:** at the threshold, the next attempt goes to a different model in the next tier
457
+ up of the pools given to `plan` (same position in the pool, as with exploration),
458
+ recorded in `escalations.tsv` and `tasks.tsv` like a `retry --dwarf`.
459
+ - **Stop:** when nothing is above (the model is in the top tier, or in no tier pool at
460
+ all), the task stops with `tasks/<id>/split.suggested`: split it into smaller tasks, or
461
+ name a model. `run --retry-failed` does not retry it, and a plain `retry` refuses (exit 2).
462
+ - **Overrides:** an explicit `retry --dwarf <spec>` is the human's choice and skips the policy, even
463
+ with the same model. The dwarf column itself cannot tell a pin from a pool pick, so it does
464
+ not; `--escalate-after 0` does.
465
+ - **Fractal plans** keep their frozen eligible set and are not escalated; neither is a
466
+ QA-only resume, which spends no implementer attempt.
467
+ - **Reporting:** the policy's state is decided after every attempt, so a run whose retry budget ran out
468
+ still reports `its next retry goes to <spec>` or the split suggestion under the results.
469
+
470
+ History cannot yet say whether escalating rescues a task (31% of the 29 later attempts on a
471
+ changed spec passed, mostly effort bumps); the attempts this policy makes are what will.
472
+
473
+ **Ordinary execution resume:** run `run` again. Tasks already `MERGED` are skipped. A saved PASS is fingerprint-checked and merged without
474
+ redispatch. PENDING, `INFRA` (paused by an infrastructure failure) and newly unblocked tasks run
475
+ when eligible. Failed or interrupted tasks
476
+ require an explicit `retry` (or `--retry-failed N`); `run` never silently pays for them again. Worktrees and pinned
477
+ baselines are retained. A per-plan advisory lock excludes concurrent run/retry/integrate
478
+ operations. Interrupting the coordinator terminates its active process groups and preserves
479
+ their work as INTERRUPTED.
480
+
481
+ **Fractal resume:** discover the managed ID with `<skill_dir>/forge fractal runs --repo
482
+ "$REPO" --json`, then use `<skill_dir>/forge fractal resume RUN_ID`. Run-level resume
483
+ recovers execution and the recorded Forge pipeline, retaining accepted stages and pinned
484
+ baselines. A control scoped with `--task` or `--task ... --node ...` only resumes that
485
+ execution scope; it does not relaunch the full pipeline. Use `retry "$PLAN" TASK_ID` for
486
+ an explicitly requested new attempt, with the existing backend, bounds and eligible pool.
487
+ Pause, stop and inspection preserve both the work and diagnostics.
488
+
489
+ ## Infrastructure failures
490
+
491
+ Quota or usage limits, auth problems, rate limits, network errors and empty answers say nothing
492
+ about the work. `forge-dispatch.sh` classifies them from the dispatch log (strong phrases and
493
+ structured `is_error`/`turn.failed` results only; a reply that otherwise succeeded is never
494
+ reclassified) and exits **8**, writing `<role>.infra` (`class=`, `retry_after=`, `detail=`).
495
+ `do_task` then ends the task as **`INFRA`**:
496
+
497
+ - a **dwarf-stage** stop refunds the attempt counter;
498
+ - a **QA-stage** stop keeps the attempt (the dwarf's work is real and committed) and writes the
499
+ `resume=qa` marker, so the next entry re-reviews without running the dwarf;
500
+ - the scheduler stops dispatching, drains what is running, leaves unstarted tasks `PENDING`
501
+ (never `BLOCKED`), and `run` exits `8` after printing what to fix. Verification is skipped.
502
+ Running the same command again resumes; `run --infra-retries M` instead waits (the provider's
503
+ retry-after if parsed, capped at 6 h, else 60 s doubling to 30 min; `FORGE_INFRA_BACKOFF` sets the
504
+ base) and resumes in the same run, up to `M` times.
505
+
506
+ A dwarf that exits 0 with an empty final message *and* an empty diff is the same class
507
+ (`class=empty`): it never really ran. Under Fractal a dwarf-side stop still ends as Fractal's own
508
+ `failed`; QA-side stops behave as above, and `pipeline.py` resets `INFRA` to `PENDING` on resume.
509
+
510
+ ## Implementer guards
511
+
512
+ forge records what it observed about how each dwarf behaved, never what the dwarf claimed:
513
+ HEAD or the checked-out ref moved (**self-commit / branch switch**), processes left alive in the
514
+ harness's session after it exited (**orphans**, listed in `dwarf.orphans` and stopped; disabled by
515
+ `FORGE_ORPHAN_GUARD=off` or under Fractal), and a final message that **promises later work**.
516
+ They are written to `tasks/<id>/guard.txt`, added to the QA prompt as "Implementer guard notes" and
517
+ shown in the run summary. They never change a status. The dwarf prompt itself tells the
518
+ implementer not to commit, not to start background processes, to finish every measurement before
519
+ its last message, and to say plainly what it could not run.
520
+
521
+ ## Accepting, splitting, combining and the whole-run review
522
+
523
+ Four subcommands act on plans after tasks have run. Full usage and refusal conditions are in the
524
+ [README](../docs/reviews.md#accepting-a-task-despite-its-reviewer); the mechanics:
525
+
526
+ - **`accept <plan> <task> --reason … --approved`** overrides a `FAIL`/`UNKNOWN` reviewer only
527
+ when the reviewed commit is still the branch tip and the worktree still matches
528
+ `source.fingerprint`. It records `known_issues.md` and `accepted.by`, merges through `merge_task`,
529
+ and restores the prior state if the merge fails. The orchestrator never runs it unprompted.
530
+ - **`review <plan> [--qa <spec>]`** and **`integrate --final-review <spec>`** run one QA dispatch
531
+ over the combined result (`integration.base..tip`, or `parent..candidate`) in a disposable
532
+ checkout with the goal, every task's requirements, known issues, drift, guard notes and the
533
+ verification result in front of the reviewer. Same verdict/severity contract and
534
+ `--qa-threshold` gate as per-task QA; a mutation during the review is `INVALIDATED`; a `FAIL`
535
+ or missing verdict blocks `integrate` with exit `5`, an infrastructure stop with `8`, and the
536
+ user's branch is never touched. Nothing runs unless one of them is invoked.
537
+ - **`split <plan> <new-plan> --tasks a,b`** copies the selected tasks' saved state to a new plan,
538
+ renames their branches and worktrees to the new run id, starts a new integration branch at the
539
+ source's accepted tip and removes them from the source (refuses Fractal-backed plans, `MERGED`
540
+ or `RUNNING` tasks, and selections that strand a dependent).
541
+ - **`combine <new-plan> <plan>…`** merges the integration branches of fully merged plans into one
542
+ new plan whose `run` only verifies. Task branches keep their original names.
543
+
544
+ `split`/`combine` hold the plans' advisory locks for their whole duration, and every failure path
545
+ removes what it created.
546
+
547
+ ## Branches, worktrees, cleanup
548
+
549
+ ```
550
+ <plan-dir>/tasks/<id>/approach.md intended approach, if planned
551
+ <repo>/.forge/ project memory (survives the run)
552
+ <repo>/../.forge-worktrees/<run-id>/<task-id> task worktree
553
+ <repo>/../.forge-worktrees/<run-id>/_integration integration worktree
554
+ forge/<run-id>/<task-id> task branch
555
+ forge/<run-id> integration branch
556
+ ```
557
+
558
+ Fractal also keeps control repositories, isolated product candidates and per-step logs
559
+ under `${XDG_STATE_HOME:-$HOME/.local/state}/forge/fractal/runs/<managed-run-id>/tasks/`.
560
+ These are separate from the Forge task worktrees listed above. Candidates begin at each
561
+ task's pinned baseline and return to its ordinary review/integration pipeline; Fractal
562
+ initialization metadata stays outside the product diff. Managed run/task IDs are shown by
563
+ `fractal runs` and `fractal tree`, and differ from the human-readable `tasks.tsv` IDs.
564
+
565
+ ### Worktree setup
566
+
567
+ A fresh worktree is a bare checkout — no `node_modules`, no venv, no build output. Without help,
568
+ every dwarf in the run rediscovers that the same way (run the tests, watch them fail, work out the
569
+ install) and pays it again, which is both slow and a source of near-identical "this worktree has no
570
+ dependencies" entries in project memory.
571
+
572
+ Tell forge what a worktree needs and it runs it verbatim, once per worktree, from the worktree
573
+ root, at creation time only:
574
+
575
+ - `.forge/setup` in the repo — executable, or any file whose contents are a shell command;
576
+ - `--setup <command>` on `run` or `retry`, which overrides it.
577
+
578
+ A resumed run or a `retry` reuses the existing worktree and does not pay it again. The integration
579
+ worktree gets the same treatment, since that is where a merged result is validated — without it the
580
+ one checkout holding every task's merged work is the one checkout that cannot build it.
581
+
582
+ Failure is reported and non-fatal: the dwarf gets a worktree it can still install into itself,
583
+ which is the status quo this replaces. Output lands in `tasks/<id>/setup.out`.
584
+
585
+ Forge does not guess this step, and the obvious guess is worth naming because it looks right and
586
+ is not: symlinking the source repo's `node_modules` into the worktree. In a workspace monorepo the
587
+ workspace links inside it point back at the source checkout, so the worktree builds against the
588
+ source repo's packages rather than its own — producing type errors naming files the task never
589
+ touched. Copying is correct but can be gigabytes per task. The repo knows which of those it wants;
590
+ forge does not.
591
+
592
+ The worktree root is a sibling of the repo and **never** `TMPDIR`. Isolated worktrees are
593
+ never pushed, so a temp root that gets cleaned takes the only copy of that work with it —
594
+ this has already destroyed real work in this user's `linear-spanks` runs.
595
+
596
+ Cleanup rules:
597
+
598
+ - A successful task's worktree is removed only after `results.tsv` is written and read back.
599
+ - A failed, conflicted or unknown task keeps **both** its branch and its worktree, so there
600
+ is something to inspect and retry from.
601
+ - The integration branch is left in place. Merging it into the user's branch is a separate
602
+ `integrate --approved` step and never happens automatically.
603
+
604
+ The user's own branch and working tree are untouched for the whole run.
605
+
606
+ ## Failure modes
607
+
608
+ | Symptom | Cause |
609
+ |---------|-------|
610
+ | task status `ERROR` | worktree creation or a dispatch failed; see `tasks/<id>/dwarf.out` / `qa.out` |
611
+ | task status `TIMEOUT` | that role exceeded the dispatch timeout and was killed. It has usually left a partial edit in the worktree; `retry` continues from it |
612
+ | task status `FAIL` with "produced no changes" | the dwarf ended without editing anything — usually an ambiguous prompt it could not resolve headlessly |
613
+ | task status `UNKNOWN` | QA never emitted a verdict line; read `tasks/<id>/qa.last`. `retry` re-runs QA only, on the unchanged work |
614
+ | task status `INFRA`, `run` exits `8` | quota/auth/rate limit/network/empty output stopped a dispatch; no attempt spent. See `tasks/<id>/infra.txt`, fix the cause, run again (or `--infra-retries M`) |
615
+ | a `FAIL` task merged anyway | an explicit `--qa-threshold` tolerated its findings; see `known_issues.md` and `qa.gate` |
616
+ | task status `CONFLICT` | QA passed but the merge onto the integration branch conflicted — `files` was understated somewhere |
617
+ | every wave has one task | `files` sets overlap across most tasks; the decomposition is not actually parallel |
618
+ | dwarves ignore existing work | `goal.txt` or `files` missing, so the capsule carries no useful status |
619
+ | `.forge/` appears in a task's diff | the capture is missing `':(exclude).forge'` — see `memory.md` |
620
+ | dwarves build incompatible interfaces | no `approach.md`; the seam was never agreed. Plan it, or use `--planner` |
621
+ | "scope drift" in the summary | the task touched files it did not declare. Not a failure — but if a later task conflicts on one of those files, this is why |
622
+ | `retry` says "produced no new changes" | the dwarf returned a byte-identical diff. It has nothing to add; escalate with `--dwarf` or fix the prompt |
623
+ | a task will not re-run: "already exists" | a worktree directory was deleted by hand. `run`/`retry` prune stale registrations first, so re-run rather than deleting the branch |
624
+
625
+ A run where several tasks report "produced no changes" usually means the decomposition
626
+ produced tasks that were not independently actionable — the fix is a coarser
627
+ `--decompose-level`, not more retries.
628
+
629
+ ## Execution checks and integration verification
630
+
631
+ Before dispatch, `run` preflights every task's dwarf and QA plus a configured planner.
632
+ `retry` repeats preflight and dependency checks. A dependency must be MERGED, not merely
633
+ planned or QA-passing; otherwise its consumer is BLOCKED without spending a dispatch.
634
+ Directory/child ownership paths overlap and cannot run concurrently. A blocked task can be
635
+ retried after its prerequisites merge. A fresh task then branches from current integration.
636
+
637
+ QA receives complete implementation input, approach, capsule and cumulative binary diff.
638
+ It runs in a disposable repository. Both source and review fingerprints must remain unchanged,
639
+ and the task branch must still point at the reviewed commit when merged. INVALIDATED results
640
+ never merge. Disposable snapshots detect mutation; they do not confine unrestricted shell access.
641
+
642
+ Configure combined executable checks with `run <plan> --verify '<command>'` (also accepted by
643
+ `retry`), or a shell command file at `<repo>/.forge/verify`. Commands execute under Bash from
644
+ the integration checkout. Forge records `verification.command`, `.log`, `.exit`, `.fingerprint`
645
+ and `.status` in the plan directory. Failed checks or source changes fail the run. With no
646
+ command, the result is explicitly UNVERIFIED.
647
+
648
+ A failed command whose source fingerprint is unchanged is rerun (`--verify-retries N`, default
649
+ `1`, `0` = strict single run). A rerun that passes leaves status `PASS` but writes
650
+ `verification.flaky` (both exit codes), keeps the first run in `verification.log` and the second
651
+ in `verification.retry.log`, and prints a loud `FLAKY` line in `run`/`retry`/`integrate` output;
652
+ a command that changes the tree is never retried. This runs before Jev's optional model-assisted
653
+ triage, which then does not.
654
+
655
+ `scripts/forge-verify-coverage.py` reads the project's own test configuration and **warns**
656
+ (never fails) about suites the command does not run — at `run` start before any model is paid
657
+ for, again in `verify_result`, and at `integrate` — recording the lines in
658
+ `verification.coverage.txt`. Without a verify command it lists what the project defines.
659
+
660
+ `integrate <plan> --approved` prepares a separate candidate combining the current user branch
661
+ and integration revision, runs worktree setup and the configured check there, then updates the
662
+ user branch only if the candidate passed and the source branch stayed unchanged. Logs live in
663
+ `<plan>/integrate-*/`. A failed candidate remains available for inspection. With no verification
664
+ command, integration retains the existing approval boundary and reports UNVERIFIED.
665
+
666
+ Optional [Ripwire context](ripwire.md) is enabled by default. Use `--no-ripwire`
667
+ or `FORGE_RIPWIRE=off` to disable preparation and installation offers.