@giamme/forge 0.0.0-stage → 0.8.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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -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](../README.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.
|