@giamme/forge 0.11.1 → 0.12.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 (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/SKILL.md +4 -2
  3. package/VERSION +1 -1
  4. package/docs/artifacts.md +44 -11
  5. package/docs/commands.md +45 -7
  6. package/docs/decomposed-runs.md +43 -2
  7. package/docs/forge-ui.md +27 -6
  8. package/docs/reviews.md +31 -6
  9. package/package.json +1 -1
  10. package/references/dataset.md +3 -1
  11. package/references/decompose.md +159 -17
  12. package/references/harnesses.md +13 -1
  13. package/references/ui.md +14 -4
  14. package/scripts/forge-control.py +268 -25
  15. package/scripts/forge-dispatch.sh +80 -3
  16. package/scripts/forge-guard.py +156 -15
  17. package/scripts/forge-lib-plans.sh +9 -3
  18. package/scripts/forge-lib-review.sh +22 -3
  19. package/scripts/forge-lib-task.sh +51 -4
  20. package/scripts/forge-lib-verify.sh +137 -6
  21. package/scripts/forge-parallel-options.sh +59 -0
  22. package/scripts/forge-parallel.sh +212 -28
  23. package/scripts/forge-ripwire.py +1 -1
  24. package/scripts/forge-route.py +157 -5
  25. package/scripts/forge-schedule.py +687 -50
  26. package/scripts/forge-verify.py +878 -0
  27. package/scripts/forge_dataset/forecast.py +29 -4
  28. package/scripts/forge_dataset/harvest.py +10 -4
  29. package/scripts/forge_verify_reports.py +225 -0
  30. package/ui/README.md +18 -3
  31. package/ui/docs/api.md +126 -17
  32. package/ui/forge_ui/control.py +12 -2
  33. package/ui/forge_ui/export.py +20 -7
  34. package/ui/forge_ui/reader.py +307 -11
  35. package/ui/forge_ui/server.py +16 -0
  36. package/ui/forge_ui/static/app.js +2 -1
  37. package/ui/forge_ui/static/controls.js +217 -33
  38. package/ui/forge_ui/static/gantt.js +14 -9
  39. package/ui/forge_ui/static/index.html +1 -0
  40. package/ui/forge_ui/static/panel.js +2 -1
  41. package/ui/forge_ui/static/runmodel.js +92 -11
  42. package/ui/forge_ui/static/scene3d.js +9 -5
  43. package/ui/forge_ui/static/style.css +60 -0
  44. package/ui/forge_ui/static/verifyview.js +324 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and versions follow the format `{major}.{minor}[.{patch}]` ([...] parts appear only when non-zero).
7
7
 
8
+ ## [0.12] - 2026-10-27
9
+
10
+ Decomposed runs can now be steered while they run and stop cleanly when interrupted: drain, stop, retry or resize a live run from forge control or forge-ui, an infrastructure stop pauses only the model or CLI it hit, and verification can run as parallel lanes that rerun only what failed and name the failing tests.
11
+
12
+ ### Highlights
13
+
14
+ - A running decomposed plan can be steered: `forge control drain` lets running tasks finish and starts nothing new until `forge control resume`; `forge control stop` ends the run once running tasks are done (exit 130 without verifying, unless every task merged; not-started tasks stay pending for the next `run`), and `stop --now` interrupts them; `forge control set <plan> max-parallel N` changes how many tasks run at once.
15
+ - Verification can run as lanes: declare them in `.forge/verify.d/` (one file per lane) or with `run --verify-lane 'name[options]=command'`, and forge runs them side by side (`--verify-parallel`, 3 by default), reruns only a lane that failed, and calls a lane that passed on rerun flaky. Lanes can read JUnit or TAP reports to name the failing tests, and a baseline of known failures keeps those from failing the run. The run summary and the whole-run review name each lane and its state.
16
+ - An infrastructure stop no longer pauses the whole decomposed run: a rate limit pauses only that model and a quota or sign-in stop only that CLI, each with its own wait and --infra-retries budget, while tasks routed elsewhere keep running. `forge control show` lists the active pauses.
17
+
18
+ ### Added
19
+
20
+ - `run --infra-backoff` sets how long forge waits between infrastructure retries: `exp:<base>[,<cap>]` (the default, `exp:60`), `linear:<start>,<step>[,<cap>]` or a list of waits such as `10,20,30`. `forge control set <plan> infra-backoff` changes it while the run goes on, and each wait message names the shape it used.
21
+ - A running decomposed plan can retry a stopped or failed task, optionally on another model (`forge control retry <plan> <task> [--dwarf <spec>]`), and pick up a row you added to tasks.tsv (`forge control add <plan> <task>`) without ending the run. The scheduler applies or refuses each request at its next pass and records the outcome in control.log.
22
+ - `run --infra-fallback`: when an implementer's model is at capacity, the task moves at once to the next model of its own tier pool instead of waiting, and the move is recorded like an escalation. The busy model is still paused and counted, so a pool that is entirely at capacity still ends the run.
23
+ - forge-ui now shows a verification while it runs: its current step, last output line and idle time, and, for a lanes verification, each lane's state, runs, test counts and failing tests, with known failures marked.
24
+ - forge-ui serves every verification log, each lane's and each rerun's, as a numbered tail or in 400-line pages, so the cause of a failed verification can be read without opening the run dir.
25
+ - The verification panel in forge-ui now follows a running verification live (current step, last line, elapsed time, a warning after five silent minutes, the log's tail) and, when it fails, opens on the failing tests, marked new or known, each linking to its line in the log.
26
+ - forge-ui's Execution view shows verification lanes as their own rows, with every run and rerun at its real time, and the 3D finish node shows one light per lane; a verification that is still running reads RUNNING instead of its previous result.
27
+ - `--escalate-effort` (on `run` and `retry`, or `forge control set <plan> escalate-effort on`): when a task has failed its escalation limit on the top tier's model, forge retries it once more at the next effort that model accepts before stopping it with a split suggestion. Turning it on also releases tasks the policy already stopped.
28
+ - forge-ui's Controls panel can now drain, resume and stop a live run (gracefully or at once), change max parallel, the infrastructure backoff and the effort step, and retry a stopped task now with a chosen model. It also shows each infrastructure pause with a countdown, and whether a queued retry was applied or refused.
29
+
30
+ ### Changed
31
+
32
+ - The forecast `run` prints before dispatching lists the tasks that don't have enough history yet on one line instead of a row each, and is a single line when no task has enough history, so a new setup no longer opens every run with a table of "not enough history" rows. `forge dataset forecast <plan>` still shows every task.
33
+
34
+ ### Fixed
35
+
36
+ - When the plan forecast that `run` prints runs out of its time budget while saving, it no longer leaves a stray `.forecast.json.<pid>` file in the plan.
37
+ - Interrupting a decomposed run no longer leaves an implementer or reviewer CLI running: forge waits up to 10 s (FORGE_INTERRUPT_GRACE) for each task to stop its harness, and a harness left behind by a killed run is stopped when the next run starts.
38
+ - Interrupting `forge-parallel.sh run` (Ctrl-C or SIGTERM) no longer goes on to verify a half-merged integration branch: the run stops, frees the plan lock within seconds, exits 130 and prints the command that continues each interrupted task. A signal during verification stops the whole verify command and skips its rerun.
39
+ - A task that finished just as a decomposed run was interrupted is now reaped first: its passing work still merges and its result is kept, and only the tasks still running are marked INTERRUPTED.
40
+ - On macOS, a Ripwire context call whose output was too long or that ran too long no longer fails with a PermissionError; it is skipped with its reason, as intended.
41
+
8
42
  ## [0.11.1] - 2026-10-26
9
43
 
10
44
  A bug-fix release for forge-ui and forge issues: the Settings and Open findings views keep your edits and selections honest, forge ui open handles a read-only server with another configuration, and refused labels and warnings say what's wrong.
package/SKILL.md CHANGED
@@ -57,8 +57,10 @@ For explanation requests, explain the workflow without spending model quota.
57
57
  overrides a reviewer, so run it (with the user's `--reason` and
58
58
  `--approved`) only on the user's explicit instruction; never self-accept.
59
59
  - When you report a run, read `<plan>/control.log` and mention any human change made mid-run
60
- (a threshold, a budget, a model pin; `forge control show <plan>` lists them). Never set a
61
- control, pin or delete a run with `forge control` / `forge runs rm` unless the user asks.
60
+ (a threshold, a budget, a model pin, a drain, stop or queued retry; `forge control show <plan>`
61
+ lists them). Never set a control, pin, `drain|stop|retry` or delete a run with `forge control` /
62
+ `forge runs rm` unless the user asks. Exit `130` / `INTERRUPTED`: the run was signalled or
63
+ stopped; relay the `retry` lines it printed.
62
64
  - Exit `8` / status `INFRA` is a quota, auth or network stop, not a task failure and no
63
65
  attempt was spent: report it, say what to fix, and re-run only when told. For a multi-task
64
66
  plan, offer the opt-in whole-run review (`review`, or `integrate --final-review <qa-spec>`);
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.1
1
+ 0.12
package/docs/artifacts.md CHANGED
@@ -15,7 +15,7 @@ as if the dwarf had written it.
15
15
  <role>.cmd the exact command line executed
16
16
  <role>.log full transcript
17
17
  <role>.last the final message — read this first
18
- <role>.infra only after an infrastructure failure: class, retry_after, detail
18
+ <role>.infra only after an infrastructure failure: class, retry_after, detail, harness, model
19
19
  <role>.orphans only when processes were left running after the harness exited
20
20
  <role>.session session id reported by claude, openclaude or codex; absent when the dispatch published none
21
21
  <role>.continue only when the dispatch was asked to continue a reviewer session: requested=, mode=continued|failed, reason=
@@ -49,9 +49,10 @@ A decomposed run adds:
49
49
  tasks/<id>/attempt, attempts.tsv attempts spent; one audit row per entry (incl. refunded stops)
50
50
  tasks/<id>/known_issues.md, qa.gate findings tolerated by --qa-threshold, and the gate's decision
51
51
  tasks/<id>/guard.txt what forge observed about the implementer (self-commit, orphans, promises)
52
- tasks/<id>/infra.txt, resume, noretry infrastructure stop record; QA-only resume marker; no-progress marker
52
+ tasks/<id>/infra.txt, resume, noretry infrastructure stop record (<role>.infra plus stage=); QA-only resume marker; no-progress marker
53
+ tasks/<id>/<role>.harness only while a guarded dispatch runs: its harness's pid and start time, for forge-guard.py stop
53
54
  tasks/<id>/qa_threshold optional per-task blocking threshold: P0-P3, or none (exempt from the plan's)
54
- tasks/<id>/escalations.tsv epoch, attempt, from-spec, to-spec, source (policy|retry|pin); older rows have 4 columns
55
+ tasks/<id>/escalations.tsv epoch, attempt, from-spec, to-spec, source (policy|retry|pin|fallback); older rows have 4 columns
55
56
  tasks/<id>/escalation.pin a human's one-shot model for the next full retry: one dwarf spec on one line
56
57
  tasks/<id>/escalation.pin.applying the pin while a retry has claimed it; removed once the retry is recorded
57
58
  tasks/<id>/retry_findings.commit commit the carried review judged
@@ -64,18 +65,40 @@ A decomposed run adds:
64
65
  tasks/<id>/retry_budget, retries.used live per-task retry budget (a number); retries spent (kept after the run)
65
66
  results.tsv id status branch
66
67
  timeout, qa_threshold, verify_retries options remembered from run/retry flags (also escalate_after)
68
+ escalate_effort on: the retry policy's effort step (--escalate-effort; absent = off)
69
+ infra_backoff, infra_fallback --infra-backoff's shape; on while --infra-fallback is set
70
+ max_parallel the concurrency limit: written by each run from --max-parallel, changed live by forge control
67
71
  reviewer_session holds on; present while plan --reviewer-session is on
68
72
  <final-review-dir>/review.meta final-review-<epoch>-<pid>/ or integrate-*/final-review/; base=, tip=, source=, spec=, threshold=, integration=, previous=, since=; session= and session_reason= while reviewer_session is set
69
- control.log one JSON line per human change made with forge control
73
+ control.log one JSON line per human change made with forge control, and per request outcome
74
+ control.requests/ retry and add requests queued by forge control: one JSON file each, removed once settled
70
75
  scheduler.live only while a scheduler runs: {"pid", "started"}
76
+ drain, stop only while a scheduler runs: forge control drain (empty) and stop (graceful|now)
77
+ infra.paused only while something is paused on infrastructure: {"scopes": [...]}
71
78
  control.lock the lock that serialises changes to the plan's control files, created by whichever takes it first (forge control, the scheduler or an escalation record) and left in place
72
79
  retry_budget, infra_budget only while a scheduler runs: its live budgets (a number each)
73
80
  integration.base commit the integration branch started from
74
- verification.{status,exit,log,command,fingerprint} the combined verification
75
- verification.{flaky,retry.log} only when a failing command passed on rerun
81
+ verification.{status,exit,log,command,fingerprint} the combined verification; status PASS, FAIL or INTERRUPTED (a signal stopped it)
82
+ verification.{flaky,retry.log} only when a failing command passed on rerun (lanes: flaky alone, per lane)
83
+ verification.commit the integration commit that was verified
84
+ verification.runs.tsv one row per run of the verify command or of a lane (below)
85
+ verification.run-<n>.log single command: each run's own copy of its log
86
+ verification.lanes.json lanes only: every lane's state, runs, step and tests, rewritten as they change
87
+ verification.lane.<name>.log lanes only: the lane's run in progress
88
+ verification.lane.<name>.run-<k>.log lanes only: each finished run of the lane
76
89
  verification.coverage.txt suites the verify command does not run (advisory)
90
+ verify_cmd, verify.d/ the plan's --verify command, or its --verify-lane lanes (one file per lane)
91
+ verify_parallel lanes run at once (--verify-parallel; default 3)
77
92
  ```
78
93
 
94
+ `verification.runs.tsv` is tab-separated, one row per run: `n`, `kind` (`initial`, `rerun`, or
95
+ `jev_retry` for a single command), `exit`, `tree_changed` (`0`/`1`), `duration_s` and `log`. With
96
+ lanes each row has two more columns, `lane` and `started` (epoch seconds); lanes run side by
97
+ side, so `tree_changed` is `1` only when the whole verification was a single lane run that
98
+ changed the tree (`verification.lanes.json`'s `mutated` says whether it changed at all). In lanes
99
+ mode `verification.flaky` holds `lanes=<names>` and, per flaky lane, `<lane>.first_exit=` and
100
+ `<lane>.retries_used=`.
101
+
79
102
  with worktrees and branches at:
80
103
 
81
104
  ```
@@ -86,11 +109,21 @@ forge/<run-id>-integration integration branch
86
109
 
87
110
  `control.log` lines look like
88
111
  `{"at": 1761200000.5, "actor": "ui", "action": "set", "task": null, "key": "qa-threshold", "value": "P2", "previous": "P1"}`
89
- (`actor` is `cli` or `ui`; `action` is `set`, `pin` or `unpin`; `key` is the control's name, or `pin`; `task` is null for a plan-wide
90
- control). `scheduler.live`, `retry_budget`, `infra_budget` and the per-task `retry_budget` exist
91
- only while the scheduler is live: it publishes the flags it was given when it starts, re-reads them
92
- at every decision and removes them when it stops, which is why `forge control` refuses a budget
93
- change outside a live run. See [live controls](decomposed-runs.md#changing-a-run-while-it-works).
112
+ (`actor` is `cli` or `ui`; `action` is `set`, `pin`, `unpin`, `drain`, `resume`, `stop`, `retry` or
113
+ `add`; `key` is the control's name, or `pin`; `task` is null for a plan-wide control). A queued
114
+ `retry` or `add` also carries its `request` id, and the scheduler later appends the outcome under
115
+ the same id:
116
+ `{"at": 1761200030.1, "actor": "scheduler", "action": "retry", "task": "docs", "key": null, "value": "sol:high", "previous": null, "request": "…", "outcome": "refused", "reason": "the task is running"}`
117
+ (`outcome` is `applied` or `refused`). `scheduler.live`, `retry_budget`, `infra_budget`, the
118
+ per-task `retry_budget`, `drain`, `stop` and `infra.paused` exist only while the scheduler is live:
119
+ it publishes the flags it was given when it starts, re-reads them at every decision and removes
120
+ them when it stops, which is why `forge control` refuses a budget change, a drain or a stop outside
121
+ a live run. A request still queued then is refused (`run ended`, or `left by an earlier run` at the
122
+ next start). See [live controls](decomposed-runs.md#changing-a-run-while-it-works).
123
+
124
+ `infra.paused` holds one entry per paused scope: `scope` (`all`, `harness:<h>` or
125
+ `model:<h>/<m>`), `class`, `harness`, `model`, the `tasks` it holds, `cycle` (which wait this is),
126
+ `until` (epoch seconds, null while not waiting) and `exhausted` (its waits are used up).
94
127
 
95
128
  The worktree root is a **sibling of the repo, never `TMPDIR`**. Isolated worktrees are never
96
129
  pushed, so a temp root that gets cleaned takes the only copy of that work with it.
package/docs/commands.md CHANGED
@@ -16,7 +16,7 @@ harness (`/forge`); this command installs forge and drives its optional tools.
16
16
  | `forge version [--check]` | the installed version, and whether a newer release is out |
17
17
  | `forge fractal …` | opt-in Fractal execution; see [the command group](fractal.md#forge-fractal-command-group) |
18
18
  | `forge runs [--repo DIR \| --all] [--limit N] [--json]` | this repo's recent runs, newest first: run id, kind, age, task states, verification and final review, goal, and the run dir. Read-only; the same summaries forge-ui shows |
19
- | `forge control show\|set\|pin\|unpin PLAN_DIR …` | change a live plan: `set` takes `qa-threshold`, `timeout`, `escalate-after`, `verify-retries`, `retry-budget` or `infra-retries` (the last two only while a scheduler runs; `--task ID` for `qa-threshold` and `retry-budget`, value `inherit` drops the override); `pin PLAN_DIR TASK SPEC` sets the model the task's next full retry uses once, `unpin` drops it; `--json`. Every change is logged in `control.log`. See [live controls](decomposed-runs.md#changing-a-run-while-it-works) |
19
+ | `forge control show\|set\|pin\|unpin\|drain\|resume\|stop\|retry\|add PLAN_DIR …` | change a live plan: `set` takes `qa-threshold`, `timeout`, `escalate-after`, `escalate-effort` (`on`/`off`), `verify-retries`, `infra-backoff` (a backoff shape; `none` drops it), `retry-budget`, `infra-retries` or `max-parallel` (1-64) (the last three only while a scheduler runs; `--task ID` for `qa-threshold` and `retry-budget`, value `inherit` drops the override); `pin PLAN_DIR TASK SPEC` sets the model the task's next full retry uses once, `unpin` drops it. While a scheduler runs: `drain` starts nothing new until `resume` (a run with nothing left to start ends as usual); `stop [--now]` ends the run once the running tasks finish (`--now` interrupts them; `run` exits 130 without verifying, unless every task had merged by then: it then verifies and exits 0 as usual); `retry PLAN_DIR TASK [--dwarf SPEC]` and `add PLAN_DIR TASK` (a row appended to `tasks.tsv`) queue a request the scheduler applies or refuses. `show` (`--json` for JSON) also says whether the run is drained or stopping and lists its infrastructure pauses. Every change and every request's outcome is logged in `control.log`. Exit 2 invalid input, 3 refused. See [live controls](decomposed-runs.md#changing-a-run-while-it-works) |
20
20
  | `forge config show [--repo DIR] [--json]` / `forge config set <key> <value> --repo DIR [--json]` / `forge config unset <key> --repo DIR [--json]` | one view of every config source (registry, Jev, the repo's `.forge/verify`, `setup` and `verify-subset`, dataset, `FORGE_*` variables with secrets hidden); `set KEY VALUE` / `unset KEY` edit `repo.verify`, `repo.setup`, `repo.verify-subset` (must contain `{files}`) and, for `set`, `jev.enabled true\|false` (that one does not need `--repo`). `-` as a value reads stdin. Exit 2 on invalid input |
21
21
  | `forge runs rm RUN_DIR [--dry-run] [--yes] [--no-archive] [--worktrees] [--branches] [--json]` | delete a run directory but keep what it taught the dataset; see [below](#forge-runs-rm) |
22
22
  | `forge issues RUN [--apply] …` | file the review findings a run left open as GitHub issues; see [below](#forge-issues) |
@@ -191,8 +191,13 @@ forge-dispatch.sh dwarf <spec> --prompt-file <f> [--repo <dir>] [--run-dir <d>
191
191
  forge-dispatch.sh qa <spec> --prompt-file <f> [--repo <dir>] [--run-dir <d>] [--yolo] [--dry-run]
192
192
  [--native-review [--review-base <ref>]]
193
193
  forge-dispatch.sh planner <spec> --prompt-file <f> [--repo <dir>] [--run-dir <d>] [--dry-run]
194
+ forge-dispatch.sh efforts --spec <spec>
194
195
  ```
195
196
 
197
+ `efforts` prints the efforts the spec's pairing accepts, weakest first, one per line (nothing
198
+ when the pairing has no effort dial); a literal model id with a harness takes that harness's
199
+ whole ladder. It never dispatches and spends no quota. The retry policy's effort step reads it.
200
+
196
201
  | Option | Meaning |
197
202
  |---|---|
198
203
  | `--prompt-file <f>` | file holding the prompt (required for `dwarf`/`qa`) |
@@ -210,7 +215,7 @@ forge-dispatch.sh planner <spec> --prompt-file <f> [--repo <dir>] [--run-dir <d>
210
215
  `8` an **infrastructure failure** (quota or usage limit, auth, rate limit, network, or an
211
216
  empty answer) — nothing about the work was judged, so callers pause instead of counting a
212
217
  failure. On `8`, `<run-dir>/<role>.infra` holds `class=`, `retry_after=`, `detail=`, `rc=`,
213
- `role=`.
218
+ `role=`, `harness=`, `model=`.
214
219
 
215
220
  **Timeout precedence.** An explicit `--timeout`, then `FORGE_TIMEOUT` (an explicit `0` wins
216
221
  and disables the watchdog), then the optional fifth `timeout` column of the model's
@@ -328,15 +333,21 @@ forge-parallel.sh plan <plan-dir> --repo <dir> [routing flags] [--planner <
328
333
  [--reviewer-session | --no-reviewer-session]
329
334
  [--explore E] [--ui | --no-ui] [--fail-first | --no-fail-first]
330
335
  forge-parallel.sh run <plan-dir> [--max-parallel N] [--yolo-dwarf] [--yolo-qa] [--dry-run]
331
- [--verify <cmd>] [--setup <cmd>] [--timeout S]
332
- [--retry-failed N] [--infra-retries M]
336
+ [--verify <cmd> | --verify-lane 'name[opts]=cmd' ...]
337
+ [--verify-parallel N] [--setup <cmd>] [--timeout S]
338
+ [--retry-failed N] [--infra-retries M] [--infra-backoff SHAPE]
339
+ [--infra-fallback | --no-infra-fallback]
333
340
  [--qa-threshold P0|P1|P2|P3|none] [--verify-retries N]
341
+ [--escalate-after N] [--escalate-effort | --no-escalate-effort]
334
342
  [--fractal | --no-fractal] [--fractal-<limit> <n>]
335
343
  forge-parallel.sh retry <plan-dir> <task-id> [--dwarf <spec>] [--timeout S]
336
344
  [--qa-threshold Pn|none] [--verify-retries N]
345
+ [--verify <cmd> | --verify-lane 'name[opts]=cmd' ...] [--verify-parallel N]
346
+ [--escalate-after N] [--escalate-effort | --no-escalate-effort]
337
347
  forge-parallel.sh accept <plan-dir> <task-id> --reason "<why>" --approved
338
348
  forge-parallel.sh review <plan-dir> [--qa <spec>] [--yolo-qa] [--qa-threshold Pn]
339
349
  forge-parallel.sh integrate <plan-dir> --approved [--final-review <qa-spec>] [--verify-retries N]
350
+ [--verify-parallel N]
340
351
  forge-parallel.sh split <plan-dir> <new-plan-dir> --tasks <id,id> [--dry-run]
341
352
  forge-parallel.sh combine <new-plan-dir> <plan-dir> <plan-dir>... [--verify <cmd>] [--dry-run]
342
353
  ```
@@ -355,17 +366,44 @@ forge-parallel.sh combine <new-plan-dir> <plan-dir> <plan-dir>... [--verify <c
355
366
  |---|---|---|---|
356
367
  | `--timeout S` | run, retry | `timeout` | per-dispatch timeout in seconds (`0` disables); wins over `FORGE_TIMEOUT` |
357
368
  | `--retry-failed N` | run | no — this run only | re-queue a task the reviewer rejected up to `N` more times inside the run |
358
- | `--infra-retries M` | run | no — this run only | wait out up to `M` infrastructure stops instead of pausing |
369
+ | `--infra-retries M` | run | no — this run only | wait out up to `M` infrastructure stops per paused scope (a model, a harness, or everything) instead of ending the run |
370
+ | `--infra-backoff SHAPE` | run | `infra_backoff` | the waits between those stops when the provider gave no retry-after: `exp:B[,C]`, `linear:S,T[,C]` or `list:a,b,...` in seconds (default `exp:60`) |
371
+ | `--infra-fallback` | run | `infra_fallback` | a dwarf stopped by a rate limit moves to the next healthy model of its tier pool instead of waiting (off by default; `--no-infra-fallback` turns it off) |
359
372
  | `--qa-threshold Pn` | run, retry | `qa_threshold` | least severe finding that still blocks; milder ones become known issues; `none` clears |
360
- | `--verify-retries N` | run, retry, integrate | `verify_retries` | reruns of a failed verify command (default `1`, `0` = strict single run) |
373
+ | `--verify-retries N` | run, retry, integrate | `verify_retries` | reruns of a failed verify command, or of each failed lane (default `1`, `0` = strict single run) |
374
+ | `--verify-lane 'name[opts]=cmd'` | run, retry | `verify.d/<name>` | one verification lane; repeatable. Options as in a lane file, comma-separated (`group=g`, `first`, `retries=n`, `reports=dir`, `baseline=file`). The first one of an invocation replaces the plan's earlier lanes and `--verify`; see [verification lanes](../references/decompose.md#execution-checks-and-integration-verification) |
375
+ | `--verify-parallel N` | run, retry, integrate | `verify_parallel` | lanes run at once, 1 to 32 (default `3`) |
376
+ | `--escalate-after N` | run, retry | `escalate_after` | failed attempts on one model before the next goes a tier up (default `3`, `0` = off) |
377
+ | `--escalate-effort` | run, retry | `escalate_effort` | at the top tier, retry the same model once more at its next accepted effort before stopping (off by default; `--no-escalate-effort` turns it off) |
361
378
 
362
379
  **Exit codes:** `0` ok · `2` usage or validation · `3` precondition (not a git repo, …) ·
363
380
  `5` some task failed QA, or the combined verification failed · `6` integration conflict ·
364
381
  `8` **paused on an infrastructure failure** (quota, auth, rate limit, network): no attempt was
365
- spent and nothing was judged — fix the cause and run the same command again.
382
+ spent and nothing was judged — fix the cause and run the same command again ·
383
+ `130` **interrupted or stopped** (a signal, or `forge control stop`): the running tasks are
384
+ `INTERRUPTED` and verification was skipped or stopped; the summary prints the `retry` for each.
366
385
 
367
386
  The full plan → run → integrate walkthrough is in [decomposed runs](decomposed-runs.md#plan-run-integrate). `retry`, `accept` and `review` are covered in [reviews, retries and recovery](reviews.md); resuming, `split`/`combine`, scope drift and `tasks.tsv` in [decomposed runs](decomposed-runs.md).
368
387
 
388
+ ## `scripts/forge-verify.py`
389
+
390
+ The verification lane runner `run`, `retry` and `integrate` use when lanes are declared. You can
391
+ run it by hand to check or try a lanes directory:
392
+
393
+ ```
394
+ forge-verify.py lanes --lanes-dir DIR [--checkout DIR] [--json]
395
+ forge-verify.py run --checkout DIR --out DIR --lanes-dir DIR [--parallel N] [--retries N]
396
+ [--fingerprint-cmd CMD]
397
+ ```
398
+
399
+ `lanes` lists the lanes with their options and checks them (with `--checkout`, also that
400
+ `reports=` and `baseline=` stay inside it and the baseline exists). `run` runs them in the
401
+ checkout as a forge run does and writes `verification.lanes.json`, `verification.runs.tsv` and
402
+ the per-lane logs into `--out` (`--parallel` default 3, `--retries` default 1).
403
+
404
+ **Exit codes:** `0` every lane passed and the tree is unchanged · `1` a lane failed, or the tree
405
+ changed · `2` configuration error (nothing ran) · `130` interrupted.
406
+
369
407
  ## `scripts/forge-memory.sh` and `scripts/forge-install.sh`
370
408
 
371
409
  See [project memory](memory.md#scriptsforge-memorysh) and [install](install.md#scriptsforge-installsh).
@@ -193,6 +193,14 @@ all their child invocations and QA. See [decomposed Fractal execution](../refere
193
193
 
194
194
  ## Resuming an interrupted run
195
195
 
196
+ Ctrl-C (or a SIGTERM) on `run` stops the running tasks' process groups, waiting up to
197
+ `FORGE_INTERRUPT_GRACE` seconds (10 by default) before killing what is left, and stops any
198
+ harness that outlived its dispatch. A task that had already finished is reaped first (a PASS
199
+ still merges); only the tasks still running become `INTERRUPTED`, with their partial work kept
200
+ in the worktree. Verification is skipped, or stopped if it had started (`verification.status`
201
+ `INTERRUPTED`, not rerun). The summary lists each interrupted task with its `retry` command,
202
+ the plan lock is released, and `run` exits 130.
203
+
196
204
  Run `run` again to skip MERGED tasks, merge saved PASS results after fingerprint checks,
197
205
  and dispatch eligible pending tasks. Tasks paused on an infrastructure failure (`INFRA`) are
198
206
  dispatched again without any flag, since nothing was wrong with the work. Failed and
@@ -200,6 +208,9 @@ interrupted tasks need an explicit `retry` (or `run --retry-failed N` for reject
200
208
  Their worktrees and pinned baselines remain available. Concurrent run/retry/integrate
201
209
  operations for the same plan are excluded by an advisory lock.
202
210
 
211
+ To pause a run on purpose, drain it instead of signalling it (see below): nothing is
212
+ interrupted, and `forge control resume` continues the same run.
213
+
203
214
  For a Fractal-backed run, use `forge fractal resume RUN_ID` to recover recorded execution
204
215
  and pipeline checkpoints. A task/subtree selector resumes only that execution scope.
205
216
  Explicit `retry` retains the backend and frozen pools; a replacement dwarf must already
@@ -213,12 +224,35 @@ belong to that pool. See [recovery](../references/fractal.md#limits-and-lifecycl
213
224
  forge control show <plan-dir> [--json] # every control, per-task overrides, pins, recent changes
214
225
  forge control set <plan-dir> qa-threshold P2 # also --task <id>; P0-P3, none, or inherit (task only)
215
226
  forge control set <plan-dir> retry-budget 3 [--task <id>] # automatic retries; live scheduler only
216
- forge control set <plan-dir> infra-retries 2 # infrastructure waits; live scheduler only
227
+ forge control set <plan-dir> infra-retries 2 # infrastructure waits per scope; live scheduler only
228
+ forge control set <plan-dir> max-parallel 2 # tasks at once, 1-64; live scheduler only
229
+ forge control set <plan-dir> infra-backoff linear:60,60 # the next infrastructure wait; none = exp:60
230
+ forge control set <plan-dir> escalate-effort on # the effort step before a policy stop
217
231
  forge control set <plan-dir> timeout|escalate-after|verify-retries <n>
218
232
  forge control pin <plan-dir> <task> sol:high:codex # the model the next full retry uses, once
219
233
  forge control unpin <plan-dir> <task>
220
234
  ```
221
235
 
236
+ And to steer the scheduler itself (each needs a live run, exit 3 otherwise):
237
+
238
+ ```bash
239
+ forge control drain <plan-dir> # start nothing new; running tasks finish, the run waits for resume
240
+ forge control resume <plan-dir> # end the drain
241
+ forge control stop <plan-dir> # finish what runs, start nothing, end the run (exit 130)
242
+ forge control stop <plan-dir> --now # interrupt the running tasks too, like Ctrl-C
243
+ forge control retry <plan-dir> docs --dwarf sol:high # queue a retry of a stopped or failed task
244
+ forge control add <plan-dir> docs-fix # queue a row you appended to tasks.tsv (with its prompt.md)
245
+ ```
246
+
247
+ A stop skips verification and exits 130, unless every task had merged by then, and a drain
248
+ with nothing left to start lets the run end as usual: both act on work still to do.
249
+ `retry` and `add` only queue a request in `control.requests/`; the scheduler applies or refuses
250
+ it at its next pass and logs the outcome in `control.log` under the same request id (`forge
251
+ control show` lists the log). A retry is prepared exactly as `forge-parallel.sh retry` prepares one,
252
+ so a task the policy stopped needs `--dwarf`; it spends no automatic retry budget. An added row
253
+ may depend only on tasks the run already knows. Neither outlives the run that was live when it
254
+ was queued, and both are refused once the run is stopping.
255
+
222
256
  Each change is validated, written under `control.lock`, and says when it applies:
223
257
 
224
258
  - **The QA threshold is read once per attempt**, at the start of the task's next attempt; the
@@ -244,8 +278,15 @@ Each change is validated, written under `control.lock`, and says when it applies
244
278
  worktree. A marker the next retry will discard is reported as `pinned`.
245
279
  Fractal plans never apply pins, and a MERGED task refuses one.
246
280
  - **Who decided an escalation.** The last column of `tasks/<id>/escalations.tsv` is `policy`
247
- (`--escalate-after`), `retry` (a typed `retry --dwarf`) or `pin`; rows from before it was added
281
+ (`--escalate-after`), `retry` (a typed `retry --dwarf`), `pin` or `fallback`
282
+ (`--infra-fallback` moved a task off a model at capacity); rows from before it was added
248
283
  have four columns and read as `policy`.
284
+ - **Max parallel and the backoff apply at the next decision.** A lower `max-parallel` lets the
285
+ running tasks finish and never stops one; `infra-backoff` shapes the next infrastructure wait,
286
+ not the one already counting down.
287
+ - **The effort step re-decides stopped tasks.** Turning `escalate-effort` on clears the policy's
288
+ stop markers, so a task stopped at the top tier is tried at its model's next effort on its
289
+ next retry.
249
290
 
250
291
  Every change is appended to `control.log` (one JSON line: `at`, `actor` `cli` or `ui`, `action`,
251
292
  `task`, `key`, `value`, `previous`) and recorded in the dataset as a `control` event, so a run's
package/docs/forge-ui.md CHANGED
@@ -55,6 +55,20 @@ following its log as it grows. It is read from the agent CLI's own log, whicheve
55
55
  wrote it (as plain text for those that log no events yet), or for an older claude run from
56
56
  Claude's session file on this machine.
57
57
 
58
+ The *Verification* node opens the run's verification in the inspector. While it runs you see
59
+ for how long, the step a lane announced (`::forge step`, see
60
+ [verification lanes](../references/decompose.md#execution-checks-and-integration-verification))
61
+ and its last printed line, a warning after five minutes with no output, and the live output of
62
+ the lane that printed last (or of the one you choose). Once it fails, the failing tests come
63
+ first, by lane and marked new or known (from the lanes' JUnit or TAP reports), each with its
64
+ message, an excerpt and a link that opens the lane's log at that line; a lane without reports
65
+ shows the end of its log and the step it ended in. Below, every lane with its state (flaky and
66
+ known called out), time, runs and test counts; a lane's name opens its output. A single verify
67
+ command shows the newest of its logs. On the *Execution*
68
+ view the verification has rows of its own after the tasks, one per lane with a bar per run and
69
+ reruns marked (a single command's runs sit back to back, dashed, because only their durations
70
+ are known), and its finish node counts the lanes and reruns.
71
+
58
72
  ### Controls
59
73
 
60
74
  forge-ui reads everything, and it changes things only by calling forge's own commands: it
@@ -64,12 +78,19 @@ indistinguishable from the same command typed in a terminal, except that `contro
64
78
  `ui` instead of `cli`. Every change asks you to confirm and says when it takes effect.
65
79
 
66
80
  - **Run controls** (a plan's inspector): the QA threshold, the per-attempt timeout, how many
67
- failures before escalating, verification retries, and, while a scheduler is live, the retry
68
- and infrastructure-retry budgets. A task has its own panel for a threshold override
69
- (including *None*, which exempts it from the run's threshold), its own retry budget, and a
70
- **model pin**: the model its next full retry uses, once. Each is `forge control`; see
71
- [decomposed runs](decomposed-runs.md#changing-a-run-while-it-works). A control that cannot
72
- apply (a budget with no live scheduler, a merged task) is disabled and says why.
81
+ failures before escalating, the effort step, verification retries, the infrastructure backoff,
82
+ and, while a scheduler is live, the retry and infrastructure-retry budgets and max parallel.
83
+ A live run also gets **Drain** (start nothing new; *Resume* ends it), **Stop** (finish what
84
+ runs, then end the run) and **Stop now** (interrupt the running tasks), and one line per
85
+ infrastructure pause: the model or harness it holds, the class, its tasks, and a countdown to
86
+ its next try, or *budget used up*. A task has its own panel for a threshold override
87
+ (including *None*, which exempts it from the run's threshold), its own retry budget, a
88
+ **model pin**: the model its next full retry uses, once, and, on a live run, **Retry now**
89
+ for a task that stopped or failed, on the policy's choice or a model you name. A retry is
90
+ queued; the panel says *queued*, then *applied* or *refused* with the scheduler's reason.
91
+ Each is `forge control`; see [decomposed runs](decomposed-runs.md#changing-a-run-while-it-works).
92
+ A control that cannot apply (a budget with no live scheduler, a merged task) is disabled and
93
+ says why.
73
94
  - **Open findings:** the review findings a run left open (tolerated by `--qa-threshold`,
74
95
  PLAUSIBLE, or left by the whole-run review), for this run or every run of its repository.
75
96
  Findings of an outdated review carry an "outdated review" marker, and findings a person
package/docs/reviews.md CHANGED
@@ -103,15 +103,30 @@ decomposed task then ends as **`INFRA`**, not `ERROR`/`FAIL`, and:
103
103
  - **no attempt is spent** — a dwarf-stage stop refunds the counter; a QA-stage stop keeps the
104
104
  dwarf's committed work and re-runs **QA only** next time (`tasks/<id>/resume`, revalidated
105
105
  against the branch tip and fingerprint before it is trusted);
106
- - the scheduler stops dispatching, lets running tasks drain, leaves unstarted ones `PENDING`
107
- (never `BLOCKED`) and `run` exits **8**: fix the cause, then run the same command again;
108
- - `run --infra-retries M` waits instead — up to `M` times, using the provider's retry-after
109
- hint when it gave one (capped at 6 hours) and otherwise 60 s doubling to 30 min
110
- (`FORGE_INFRA_BACKOFF` sets the base) — and resumes in the same run;
106
+ - the scheduler pauses only what the stop is about: a **rate limit** pauses that model on that
107
+ harness, a **quota** or **auth** stop pauses that harness, and anything else (network, an
108
+ empty answer) pauses everything, letting running tasks drain. A task whose next dispatch
109
+ needs a paused model or harness waits; the others keep running. Unstarted tasks stay
110
+ `PENDING` (never `BLOCKED`), and once nothing else can run `run` exits **8**: fix the cause,
111
+ then run the same command again;
112
+ - `run --infra-retries M` waits instead — up to `M` waits per paused scope, using the provider's
113
+ retry-after hint when it gave one (capped at 6 hours) and otherwise the backoff shape — and
114
+ resumes that scope's tasks in the same run. `--infra-backoff` picks the shape: `exp:60` by
115
+ default (60 s doubling to 30 min; `FORGE_INFRA_BACKOFF` sets that base), `exp:<base>,<cap>`,
116
+ `linear:<start>,<step>[,<cap>]` or `list:<a>,<b>,...`. `forge control set infra-backoff`
117
+ changes it for the next wait, and `forge control show` (and forge-ui) list each pause with
118
+ the time left;
119
+ - `run --infra-fallback` moves an implementer stopped by a rate limit to the next model of its
120
+ own tier pool that nobody paused, at once and with no attempt spent (recorded in
121
+ `escalations.tsv` with source `fallback`); a pinned task, or a pool with no healthy member,
122
+ waits as above;
111
123
  - verification is skipped for a paused run (it is unfinished); the next `run` verifies at its end.
112
124
 
125
+ For a planned outage, `forge control drain <plan>` lets the running tasks finish and starts
126
+ nothing new until `forge control resume <plan>`, inside the same run.
127
+
113
128
  `.infra` (`dwarf.infra`/`qa.infra`) holds `class=`, `retry_after=`, `detail=`, `rc=`,
114
- `role=`. Detection is deliberately strict: strong phrases only, and ordinary text that merely
129
+ `role=`, `harness=`, `model=`. Detection is deliberately strict: strong phrases only, and ordinary text that merely
115
130
  mentions "rate limit" in a reply that otherwise succeeded is never classified. A false
116
131
  positive costs a pause, not a spent attempt. Under Fractal a dwarf-side infrastructure stop
117
132
  still collapses to Fractal's own "failed" handling; QA-side stops propagate as above.
@@ -297,6 +312,16 @@ intermittent failure can be a real bug, so it is never passed off as a clean pas
297
312
  that changes the tree is never retried. This deterministic rerun comes before Jev's optional
298
313
  model-assisted triage, which then does not run.
299
314
 
315
+ With [verification lanes](../references/decompose.md#execution-checks-and-integration-verification)
316
+ the rerun is per lane: only a lane that failed runs again, alone, and a lane that passes on its
317
+ rerun is `flaky`. The run is then a pass, but `run`/`retry`/`integrate` print
318
+ `FLAKY: lane <name> failed once and passed on rerun`, and `verification.flaky` records each
319
+ such lane (`lanes=`, `<lane>.first_exit=`, `<lane>.retries_used=`); a lane whose every failing
320
+ test is in its baseline prints `known failures only: lane <name>`. A whole-run review's
321
+ verification summary lists every lane with its state (`unit: pass`, `db: flaky`, ...) and says
322
+ again which lane failed, which passed only on a rerun ("so its pass is not clean") and which
323
+ failed only on known tests. In lanes mode there is no whole-command rerun and no Jev triage.
324
+
300
325
  ## Native review
301
326
 
302
327
  `--native-review` switches a codex reviewer to its purpose-built reviewer, for a diff too large
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giamme/forge",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Multi-model coding with a reviewer that reads the code: plan, build in parallel worktrees, review every real diff, merge only what passes.",
5
5
  "keywords": ["claude-code", "codex", "opencode", "agent-skill", "code-review", "multi-model"],
6
6
  "license": "MIT",
@@ -350,7 +350,9 @@ run.
350
350
  wave), and the expected number of first-try passes.
351
351
  - It never creates the store, and prints nothing when the store is missing or has no history.
352
352
 
353
- `forge-parallel.sh run` prints the same block before it dispatches. It runs with a time budget
353
+ `forge-parallel.sh run` prints the same block before it dispatches, shortened: the tasks without
354
+ enough history share one line instead of a row each, and when no task has enough, the whole block
355
+ is one line pointing at `forge dataset forecast <plan>`. It runs with a time budget
354
356
  (`FORGE_FORECAST_BUDGET_S`, default 3 seconds), prints one line and moves on if reading the
355
357
  history takes longer, never prints errors and never changes the exit code. `--no-forecast` or
356
358
  `FORGE_FORECAST=off` turn it off. The block is written to the plan as `forecast.json` and