@giamme/forge 0.9.0 → 0.10.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 (46) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/SKILL.md +19 -1
  3. package/VERSION +1 -1
  4. package/docs/artifacts.md +17 -2
  5. package/docs/commands.md +50 -0
  6. package/docs/dataset.md +9 -0
  7. package/docs/decomposed-runs.md +42 -0
  8. package/docs/forge-ui.md +42 -6
  9. package/docs/reviews.md +14 -0
  10. package/forge +15 -0
  11. package/package.json +1 -1
  12. package/references/dataset.md +18 -1
  13. package/references/decompose.md +19 -1
  14. package/references/ui.md +27 -5
  15. package/scripts/forge-config.py +347 -0
  16. package/scripts/forge-control.py +645 -0
  17. package/scripts/forge-issues.py +806 -0
  18. package/scripts/forge-lib-task.sh +234 -35
  19. package/scripts/forge-parallel-options.sh +3 -1
  20. package/scripts/forge-parallel.sh +3 -2
  21. package/scripts/forge-route.py +184 -27
  22. package/scripts/forge-runs-rm.py +840 -0
  23. package/scripts/forge-runs.py +19 -1
  24. package/scripts/forge-schedule.py +227 -11
  25. package/scripts/forge-ui.py +85 -19
  26. package/scripts/forge_dataset/__init__.py +77 -6
  27. package/scripts/forge_dataset/capture.py +5 -2
  28. package/scripts/forge_dataset/harvest.py +22 -0
  29. package/scripts/forge_dataset/importer.py +419 -17
  30. package/scripts/forge_registry.py +168 -16
  31. package/ui/README.md +37 -4
  32. package/ui/docs/api.md +184 -6
  33. package/ui/forge_ui/__main__.py +10 -2
  34. package/ui/forge_ui/control.py +575 -0
  35. package/ui/forge_ui/export.py +17 -3
  36. package/ui/forge_ui/reader.py +26 -1
  37. package/ui/forge_ui/server.py +125 -4
  38. package/ui/forge_ui/static/app.js +80 -8
  39. package/ui/forge_ui/static/controls.js +619 -0
  40. package/ui/forge_ui/static/findingsview.js +412 -0
  41. package/ui/forge_ui/static/index.html +7 -1
  42. package/ui/forge_ui/static/panel.js +24 -4
  43. package/ui/forge_ui/static/rundelete.js +388 -0
  44. package/ui/forge_ui/static/settings.js +399 -0
  45. package/ui/forge_ui/static/style.css +177 -2
  46. package/ui/forge_ui/static/uikit.js +161 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.10] - 2026-10-23
9
+
10
+ forge-ui can now change a run: live QA threshold, retries and timeout, a one-shot model pin, open findings filed as GitHub issues, deleting runs while the dataset keeps them, and every config source in one view. Each control is also a forge CLI: forge control, forge runs rm, forge config and new forge issues modes.
11
+
12
+ ### Highlights
13
+
14
+ - After a run, review findings nobody would act on can become GitHub issues: forge offers once to file the findings `--qa-threshold` tolerated, the PLAUSIBLE ones and whatever a whole-run review left open. Where the gh-proman skill is set up for the repository they go on the project board through it; anywhere else forge files them itself with plain `gh` (`forge issues <plan-dir>`). A finding is never filed twice. `--file-issues auto` files them without asking, `--file-issues off` never offers.
15
+ - `forge control show|set|pin|unpin <plan-dir>` changes a plan while it runs: the QA threshold (read once per attempt, per task too, and `none` exempts a task), the timeout, `--escalate-after`, verify retries, and, while a scheduler is live, the retry and infrastructure-retry budgets. The budgets keep their per-invocation rule: a live scheduler publishes them for the length of the run and re-reads them at every decision. Every change is validated, logged in `control.log` and recorded in the dataset as a `control` event.
16
+ - forge-ui has an Open findings view: the findings a run, or every run of its repository, left open, with their filed state. Select some to file as GitHub issues or dismiss ones that need no work. `forge issues` gains `--offline`, `--only`, `--all-runs`, `--dismiss`/`--undismiss` (with `--reason`), `--include-dismissed` and a `findings` list in its JSON; the whole-run review's findings are now collected, and filing and dismissal take a repository lock (exit 3 when busy).
17
+ - `forge runs rm <run-dir>` deletes a run while the dataset keeps what it taught: a strict import, a self-describing archive (`FORGE_ARCHIVE.json`), and `run_delete_started` and `run_deleted` events, all under the run's own lock. `--worktrees` and `--branches` also remove the run's clean worktrees and its unchecked-out branches (foreign and dirty worktrees and UNMERGED branches are reported); `--dry-run` previews. forge-ui has a matching delete dialog. The dataset also imports `escalations.tsv` and `control.log`.
18
+ - forge-ui is no longer read-only: it gains write endpoints and Controls panels for a run and for each task (QA threshold, timeout, escalate-after, verify retries, budgets, threshold override, retry budget, model pin). It only ever calls forge's own CLIs, and every change must carry a per-launch token and pass the Host and Origin checks. `--read-only` (or `FORGE_UI_READONLY=1`) turns writing off, and `forge ui open` never reuses a server running in the other mode.
19
+
20
+ ### Added
21
+
22
+ - `forge control pin <plan-dir> <task> <spec>` sets the model a task's next full retry uses, once: the spec is checked with `forge-dispatch.sh doctor` (no quota), applied at the retry and written to `tasks.tsv`, and `escalations.tsv` gains a `source` column (`policy`, `retry` or `pin`) saying who decided. `forge control unpin` drops it; Fractal plans never apply pins.
23
+ - `forge config show|set|unset` shows every config source in one view (the registry, Jev, a repository's `.forge/verify`, `setup` and `verify-subset`, the dataset and `FORGE_*` variables with secrets hidden) and edits the per-repo files and `jev.enabled`. forge-ui shows the same view as Settings.
24
+
25
+ ### Changed
26
+
27
+ - A repeated `forge ui open` for a run that already has a browser tab no longer looks like it did nothing: it prints the link as before and says on stderr that no new tab was opened, so you know to use the link.
28
+
29
+ ### Fixed
30
+
31
+ - `forge ui open DIR` now reads only the first line of the run's `repo` file, as the runners do, so a run whose `repo` file has extra lines is listed under its real repo instead of under a path that does not exist.
32
+ - A forge-ui export no longer leaks a local path spelled another way: the run, repository and home paths are also scrubbed in their real form (on macOS, `/private/var/...` and `/var/...`).
33
+
8
34
  ## [0.9] - 2026-10-21
9
35
 
10
36
  forge-ui has a new look and layout, a 3D stage that marks every review finding on its task, and each dwarf's and reviewer's conversation, which runs on claude, openclaude and opencode now keep in full. Also new: `forge runs` and a first screen that says how to get runs; npm installations now hear about new releases.
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: forge
3
3
  description: Dispatch implementation and independent diff review to user-selected models through local agent CLIs. Use for /forge or explicit requests to delegate coding or review; explanation-only questions do not authorize dispatch.
4
- argument-hint: '"<goal>" --dwarf <alias>[:effort[:harness]] [--qa <alias>] [--planner <alias>] [--yolo-dwarf] [--yolo-qa] [--decompose-level low|medium|high] [--fractal|--no-fractal|--fractal-auto-decompose] [--fractal-planner <spec>] [--jev|--no-jev] [--jev-shadow] [--no-memory] [--no-ripwire] [--ui|--no-ui] [--timeout <seconds>] [--retry-failed <n>] [--escalate-after <n>] [--qa-threshold P0|P1|P2|P3] [--shadow-qa <spec>[,<spec>]] [--shadow-rate <0..1>] [--explore <0..0.5>] [--fail-first]'
4
+ argument-hint: '"<goal>" --dwarf <alias>[:effort[:harness]] [--qa <alias>] [--planner <alias>] [--yolo-dwarf] [--yolo-qa] [--decompose-level low|medium|high] [--fractal|--no-fractal|--fractal-auto-decompose] [--fractal-planner <spec>] [--jev|--no-jev] [--jev-shadow] [--no-memory] [--no-ripwire] [--ui|--no-ui] [--timeout <seconds>] [--retry-failed <n>] [--escalate-after <n>] [--qa-threshold P0|P1|P2|P3] [--file-issues ask|auto|off] [--shadow-qa <spec>[,<spec>]] [--shadow-rate <0..1>] [--explore <0..0.5>] [--fail-first]'
5
5
  allowed-tools: [Bash, Read]
6
6
  ---
7
7
 
@@ -55,10 +55,28 @@ For explanation requests, explain the workflow without spending model quota.
55
55
  (`--escalate-after N`, `0` = off); an explicit `retry --dwarf` overrides it. `accept`
56
56
  overrides a reviewer, so run it (with the user's `--reason` and
57
57
  `--approved`) only on the user's explicit instruction; never self-accept.
58
+ - When you report a run, read `<plan>/control.log` and mention any human change made mid-run
59
+ (a threshold, a budget, a model pin; `forge control show <plan>` lists them). Never set a
60
+ control, pin or delete a run with `forge control` / `forge runs rm` unless the user asks.
58
61
  - Exit `8` / status `INFRA` is a quota, auth or network stop, not a task failure and no
59
62
  attempt was spent: report it, say what to fix, and re-run only when told. For a multi-task
60
63
  plan, offer the opt-in whole-run review (`review`, or `integrate --final-review <qa-spec>`);
61
64
  it spends QA quota, so never run it unprompted.
65
+ - **Leftover findings become issues.** A run can finish with review findings nobody will act on:
66
+ - CONFIRMED findings that `--qa-threshold` tolerated (`tasks/<id>/known_issues.md`);
67
+ - PLAUSIBLE findings in a passed task's `qa.last`;
68
+ - whatever a whole-run review left open.
69
+
70
+ When there are any, act on the user's `--file-issues` choice with one filer:
71
+ - **gh-proman**, only when it is installed and set up for the repository: `gh-proman -C <repo> config show` exits 0. It also puts the issues on the team's board with a priority. Run `gh-proman findings --from <plan or run dir> --include-plausible --json`, then follow the gh-proman skill's review-findings steps.
72
+ - **forge's own filer** otherwise (gh-proman missing, or not configured for this repo; never configure it yourself): `python3 <skill_dir>/scripts/forge-issues.py <plan or run dir> --include-plausible --json`, plain `gh issue create`. Pass `--lang` matching the repo's existing issues, and `--label bug` when the repo has that label. If `gh` is missing, unauthenticated or the repo has no GitHub remote, list the findings in the report and say why none were filed.
73
+
74
+ The choice:
75
+ - `ask` (the default): show the dry-run summary (count by severity, titles, anything already filed) and ask once: **"File these N findings as GitHub issues? [all / CONFIRMED only / none]"**. Then re-run with `--apply`, adding `--include-plausible` only for "all".
76
+ - `auto`: file them without asking (`--apply`, PLAUSIBLE included as "to verify" issues) and report what was created and skipped.
77
+ - `off`: never offer.
78
+
79
+ `--file-issues` is an instruction to you: never forward it to a runner. Skip INFRA, interrupted and still-failing runs. Both filers mark each issue with the same finding fingerprint and skip a finding either one already filed, so offering again after a retry only brings up new ones. A draft flagged as a possible duplicate is skipped on `--apply`: show it, and file it (`--confirm-new`) only if the user says it is different.
62
80
  - Jev is optional, off unless configured and enabled, and always advisory: it never
63
81
  changes a task's tier, never selects a model, and never substitutes for QA acceptance
64
82
  or repository verification (`--jev-act` is still accepted and changes nothing). Read
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9
1
+ 0.10
package/docs/artifacts.md CHANGED
@@ -48,9 +48,16 @@ A decomposed run adds:
48
48
  tasks/<id>/known_issues.md, qa.gate findings tolerated by --qa-threshold, and the gate's decision
49
49
  tasks/<id>/guard.txt what forge observed about the implementer (self-commit, orphans, promises)
50
50
  tasks/<id>/infra.txt, resume, noretry infrastructure stop record; QA-only resume marker; no-progress marker
51
- tasks/<id>/qa_threshold optional per-task blocking threshold
51
+ tasks/<id>/qa_threshold optional per-task blocking threshold: P0-P3, or none (exempt from the plan's)
52
+ tasks/<id>/escalations.tsv epoch, attempt, from-spec, to-spec, source (policy|retry|pin); older rows have 4 columns
53
+ tasks/<id>/escalation.pin a human's one-shot model for the next full retry: one dwarf spec on one line
54
+ tasks/<id>/escalation.pin.applying the pin while a retry has claimed it; removed once the retry is recorded
55
+ tasks/<id>/retry_budget, retries.used live per-task retry budget (a number); retries spent (kept after the run)
52
56
  results.tsv id status branch
53
- timeout, qa_threshold, verify_retries options remembered from run/retry flags
57
+ timeout, qa_threshold, verify_retries options remembered from run/retry flags (also escalate_after)
58
+ control.log one JSON line per human change made with forge control
59
+ scheduler.live, control.lock only while a scheduler runs: {"pid", "started"}; the lock serialises forge control
60
+ retry_budget, infra_budget only while a scheduler runs: its live budgets (a number each)
54
61
  integration.base commit the integration branch started from
55
62
  verification.{status,exit,log,command,fingerprint} the combined verification
56
63
  verification.{flaky,retry.log} only when a failing command passed on rerun
@@ -65,5 +72,13 @@ forge/<run-id>/<task-id> task branch
65
72
  forge/<run-id>-integration integration branch
66
73
  ```
67
74
 
75
+ `control.log` lines look like
76
+ `{"at": 1761200000.5, "actor": "ui", "action": "set", "task": null, "key": "qa-threshold", "value": "P2", "previous": "P1"}`
77
+ (`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
78
+ control). `scheduler.live`, `retry_budget`, `infra_budget` and the per-task `retry_budget` exist
79
+ only while the scheduler is live: it publishes the flags it was given when it starts, re-reads them
80
+ at every decision and removes them when it stops, which is why `forge control` refuses a budget
81
+ change outside a live run. See [live controls](decomposed-runs.md#changing-a-run-while-it-works).
82
+
68
83
  The worktree root is a **sibling of the repo, never `TMPDIR`**. Isolated worktrees are never
69
84
  pushed, so a temp root that gets cleaned takes the only copy of that work with it.
package/docs/commands.md CHANGED
@@ -16,11 +16,61 @@ 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) |
20
+ | `forge config show\|set\|unset [--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`. `-` as a value reads stdin. Exit 2 on invalid input |
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
+ | `forge issues RUN [--apply] …` | file the review findings a run left open as GitHub issues; see [below](#forge-issues) |
19
23
  | `forge ui open\|status\|stop` | forge-ui, the run viewer; see [the command group](forge-ui.md#forge-ui-command-group) |
20
24
  | `forge dataset …` | every run's inputs and outcomes; see [run dataset](dataset.md) |
21
25
  | `forge jev …` | optional Jev advice: setup, status, doctor, enable, disable, backtest; see [Jev](add-ons.md#jev) |
22
26
  | `forge --help` | list the commands; `forge <command> --help` shows each one's options |
23
27
 
28
+ ### `forge runs rm`
29
+
30
+ Deletes a run directory (a plan dir or a solo run dir) without losing its traces. It holds the
31
+ run's own lock from the first step to the last (`schedule.lock` of a plan, `pipeline.lock` of a
32
+ solo run), so a run started meanwhile fails to start instead of racing the delete. In order:
33
+
34
+ 1. **preserve**: a strict dataset import of the run; every event must be stored, or nothing is deleted;
35
+ 2. **archive**: a self-describing `.tar.gz` under `<data home>/archive/` (`--no-archive` skips it);
36
+ 3. **intent**: the dataset event `run_delete_started`;
37
+ 4. `--worktrees` removes the run's clean worktrees; `--branches` runs `git branch -D` on its
38
+ branches that no worktree has checked out;
39
+ 5. the run directory is removed, `run_deleted` is recorded and the run leaves the registry.
40
+
41
+ What counts as the run's is decided from the run id, not read from the run directory: a worktree
42
+ under `<repo>/../.forge-worktrees/<run id>/` on `forge/<run id>/<task>` or
43
+ `forge/<run id>-integration`. Any other worktree there is `foreign`, one with uncommitted changes
44
+ is `dirty`; neither is touched, and a dirty one keeps the run directory too. Branches are reported
45
+ `merged` or `UNMERGED`, so you see what deleting them would lose. `--dry-run` changes nothing and
46
+ reports; `--yes` skips the prompt for the run id (needed without a terminal); `--json` prints one
47
+ object, on failure too. Exit 0 ok, 2 refused input, 3 busy or the run is active, 4 failed during
48
+ deletion or the run directory was kept (the same command finishes the job).
49
+
50
+ ### `forge issues`
51
+
52
+ `forge issues <plan dir, task dir, review dir, known_issues.md or qa.last>` drafts one GitHub
53
+ issue per finding the run left open (`--group task|run` for fewer) and files them only with
54
+ `--apply`. The findings are the CONFIRMED ones `--qa-threshold` tolerated, PLAUSIBLE ones with
55
+ `--include-plausible`, and what the whole-run review left open. Each issue carries a hidden
56
+ fingerprint, so a finding is never filed twice, by this command or by gh-proman. Also:
57
+ `--min-severity`, `--repo`, `--lang`, `--label`, `--confirm-new`, `--max-issues` (default 10; pass
58
+ the size of a `--only` selection).
59
+
60
+ | Flag | Does |
61
+ |---|---|
62
+ | `--json` | machine-readable output; `findings` lists each one with its fingerprint, severity, origin, task, `filed` state and `dismissed` mark |
63
+ | `--offline` | never run `gh`: no filed state, commit publication judged from local refs |
64
+ | `--only FP` | keep only these 12-hex fingerprints (repeatable or comma-separated) |
65
+ | `--all-runs` | read `RUN` as a repository (or a plan dir naming one) and collect every run in its registry; the newest plan wins for a finding in several |
66
+ | `--dismiss FP`, `--undismiss FP`, `--reason TEXT` | mark findings as needing no work (or take it back), then exit; no network. The marks live outside the repo, in `${XDG_STATE_HOME:-~/.local/state}/forge/findings/<repo key>.json` |
67
+ | `--include-dismissed` | draft dismissed findings too |
68
+
69
+ Two filers for one repository at once would both see a finding as unfiled, so `--apply` takes a
70
+ repository lock from the issue lookup to the last `gh issue create`, and each dismissal takes it
71
+ around its write. A busy lock is retried for 10 s, then exit 3. Exit 0 ok, 2 usage or a `gh` or
72
+ dismissal-store error, 3 busy.
73
+
24
74
  ## The `/forge` slash command
25
75
 
26
76
  ```
package/docs/dataset.md CHANGED
@@ -16,5 +16,14 @@ forge dataset export # labeled decision cases for benchmarks and fine
16
16
  forge dataset outcomes # which integrated lines survived, read from git history
17
17
  ```
18
18
 
19
+ Deleting a run keeps what it taught the dataset. `forge runs rm <run-dir>` first imports the run
20
+ **strictly** (every event must be stored, or nothing is deleted), then writes a self-describing
21
+ archive under `<data home>/archive/`: a `.tar.gz` whose `FORGE_ARCHIVE.json` records the run
22
+ dir's original path, run id and dataset uid, so `forge dataset import` maps it back to the same
23
+ run later. It records `run_delete_started` and `run_deleted` events around the removal. The same
24
+ import also reads a plan's `escalations.tsv` (with who decided: `policy`, `retry` or `pin`) and
25
+ `control.log`, so human interventions made with `forge control` are in the dataset as
26
+ `control` events. See [forge runs rm](commands.md#forge-runs-rm).
27
+
19
28
  Capture is fail-open and never read by anything that builds a prompt. See
20
29
  [references/dataset.md](../references/dataset.md) for the event schema and its invariants.
@@ -200,6 +200,48 @@ and pipeline checkpoints. A task/subtree selector resumes only that execution sc
200
200
  Explicit `retry` retains the backend and frozen pools; a replacement dwarf must already
201
201
  belong to that pool. See [recovery](../references/fractal.md#limits-and-lifecycle).
202
202
 
203
+ ## Changing a run while it works
204
+
205
+ `forge control` changes a plan's knobs without stopping it (forge-ui calls the same command):
206
+
207
+ ```bash
208
+ forge control show <plan-dir> [--json] # every control, per-task overrides, pins, recent changes
209
+ forge control set <plan-dir> qa-threshold P2 # also --task <id>; P0-P3, none, or inherit (task only)
210
+ forge control set <plan-dir> retry-budget 3 [--task <id>] # automatic retries; live scheduler only
211
+ forge control set <plan-dir> infra-retries 2 # infrastructure waits; live scheduler only
212
+ forge control set <plan-dir> timeout|escalate-after|verify-retries <n>
213
+ forge control pin <plan-dir> <task> sol:high:codex # the model the next full retry uses, once
214
+ forge control unpin <plan-dir> <task>
215
+ ```
216
+
217
+ Each change is validated, written under `control.lock`, and says when it applies:
218
+
219
+ - **The QA threshold is read once per attempt**, at the start of the task's next attempt; the
220
+ attempt already running keeps the one it began with, so a verdict is never judged by a bar that
221
+ moved under it. A per-task `none` exempts that task from the plan's threshold; `inherit` drops
222
+ the override. A plan-level `none` clears the threshold.
223
+ - **Budgets stay "per invocation".** `run --retry-failed N` and `--infra-retries M` still apply
224
+ only to the invocation that was given them and are not remembered; the next `run` starts
225
+ from its own flags. A rule that came back from an old file would silently spend money nobody
226
+ asked for. While a scheduler is live it mirrors them into the plan (`retry_budget`,
227
+ `infra_budget`, `tasks/<id>/retry_budget`) and re-reads them at every decision, which is what
228
+ `forge control` edits. When the scheduler stops it removes them; so a budget can only be changed
229
+ while a run is live (exit 3 otherwise), and the change dies with the run.
230
+ `tasks/<id>/retries.used` counts the retries spent and stays after the run for forge-ui.
231
+ - **A pin is one-shot.** `pin` makes the task's next *full* retry use that spec (checked with
232
+ `forge-dispatch.sh doctor`, which spends no quota). A QA-only resume spends no implementer
233
+ attempt and leaves the pin for later. The retry applies it, writes it into `tasks.tsv`, records
234
+ an `escalations.tsv` row with `source` `pin`, clears any `split.suggested` stop and consumes
235
+ it. Fractal plans never apply pins, and a MERGED task refuses one.
236
+ - **Who decided an escalation.** The last column of `tasks/<id>/escalations.tsv` is `policy`
237
+ (`--escalate-after`), `retry` (a typed `retry --dwarf`) or `pin`; rows from before it was added
238
+ have four columns and read as `policy`.
239
+
240
+ Every change is appended to `control.log` (one JSON line: `at`, `actor` `cli` or `ui`, `action`,
241
+ `task`, `key`, `value`, `previous`) and recorded in the dataset as a `control` event, so a run's
242
+ human interventions are on the record next to what the policy did. Setting a control to the
243
+ value it already has changes and logs nothing.
244
+
203
245
  ## Splitting and combining plans
204
246
 
205
247
  `run --retry-failed N` removes most of the reason to split a plan (a failed task no longer holds
package/docs/forge-ui.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [← README](../README.md) · [All docs](README.md)
4
4
 
5
- forge-ui is a small read-only local web page that shows runs live, as a 3D isometric map
5
+ forge-ui is a small local web page that shows runs live and, when you ask, changes them, as a 3D isometric map
6
6
  ported from [archivision](https://github.com/Giamme/archivision). Its **run cloud** puts every
7
7
  run in one full-screen view, grouped by project, in five layouts (helix, sphere, galaxy,
8
8
  timeline, constellation), filtered down to exactly the runs you want; drag any run anywhere
@@ -52,6 +52,41 @@ following its log as it grows. It is read from the agent CLI's own log, whicheve
52
52
  wrote it (as plain text for those that log no events yet), or for an older claude run from
53
53
  Claude's session file on this machine.
54
54
 
55
+ ### Controls
56
+
57
+ forge-ui reads everything, and it changes things only by calling forge's own commands: it
58
+ never writes a run file, a repo or a config itself. That keeps one set of rules (validation,
59
+ locks, the audit log) whichever way you change something, and a change from the page is
60
+ indistinguishable from the same command typed in a terminal, except that `control.log` says
61
+ `ui` instead of `cli`. Every change asks you to confirm and says when it takes effect.
62
+
63
+ - **Run controls** (a plan's inspector): the QA threshold, the per-attempt timeout, how many
64
+ failures before escalating, verification retries, and, while a scheduler is live, the retry
65
+ and infrastructure-retry budgets. A task has its own panel for a threshold override
66
+ (including *None*, which exempts it from the run's threshold), its own retry budget, and a
67
+ **model pin**: the model its next full retry uses, once. Each is `forge control`; see
68
+ [decomposed runs](decomposed-runs.md#changing-a-run-while-it-works). A control that cannot
69
+ apply (a budget with no live scheduler, a merged task) is disabled and says why.
70
+ - **Open findings:** the review findings a run left open (tolerated by `--qa-threshold`,
71
+ PLAUSIBLE, or left by the whole-run review), for this run or every run of its repository.
72
+ Select some and file them as GitHub issues, or dismiss the ones that need no work (and take
73
+ that back). It runs `forge issues`, so a finding is never filed twice.
74
+ - **Delete a run:** a dialog that first shows a dry run, then deletes the run directory
75
+ through `forge runs rm`. The dataset keeps the run's traces, and by default a `.tar.gz` of
76
+ them too; you can also remove its clean worktrees and its branches that nothing has checked
77
+ out. Anything that is not the run's own, or has uncommitted changes, is listed and left alone.
78
+ - **Settings:** every config source in one view (`forge config show`) and edits for the
79
+ per-repo ones (`repo.verify`, `repo.setup`, `repo.verify-subset`, and Jev on or off). The
80
+ registry stays read-only: the skill directory is often a live symlink to a checkout.
81
+
82
+ Writing is protected, because anything that can reach the server could otherwise change your
83
+ runs. The server makes a random token each time it starts and puts it in the page it serves;
84
+ the page sends it with every change, and the server also checks the `Host` and `Origin`
85
+ headers, so another website open in your browser cannot make your browser change a run. Reading
86
+ needs no token. `--read-only` (or `FORGE_UI_READONLY=1`) turns writing off altogether: no
87
+ token is issued and every change is refused. `FORGE_UI_READONLY=1 forge ui open` starts such a
88
+ server, and the launcher never reuses a server running in the other mode; it replaces it.
89
+
55
90
  Also: replay, critical path, Fractal child-task trees, quick jump (Ctrl K, ⌘K on a Mac, or
56
91
  `/`), a bell for runs that finish or fail, failed tasks and final-review verdicts (with opt-in
57
92
  browser notifications), and a run exported as one HTML file that opens without forge-ui, with
@@ -68,8 +103,9 @@ forge ui open [DIR] # an existing run dir, or the run list
68
103
  ```
69
104
 
70
105
  All runs share one detached server on `127.0.0.1:8765` (`FORGE_UI_PORT`) that exits after
71
- four idle hours (`FORGE_UI_IDLE_S`). A UI that cannot start never fails the run. It has no
72
- token: any local process can read the prompts and diffs of the runs it shows. See
106
+ four idle hours (`FORGE_UI_IDLE_S`). A UI that cannot start never fails the run. Reading needs
107
+ no token: any local process can read the prompts and diffs of the runs it shows; only changes
108
+ need the token (see [Controls](#controls)). See
73
109
  [references/ui.md](../references/ui.md) and [ui/README.md](../ui/README.md).
74
110
 
75
111
  To look around before your first run, point it at made-up runs (the ones in these pictures), from a git checkout:
@@ -83,10 +119,10 @@ python3 ui/tools/demo_runs.py /tmp/forge-demo
83
119
 
84
120
  | Command | Purpose |
85
121
  | --- | --- |
86
- | `open [DIR] [--no-browser] [--port N]` | start or reuse the shared forge-ui server; print `forge-ui: <url>` for the plan or solo run dir `DIR` (the run list without it) and open a browser the first time |
87
- | `status [--json]` | exit 0 and describe the server when it runs, 1 when not |
122
+ | `open [DIR] [--no-browser] [--port N]` | start or reuse the shared forge-ui server (never one in the other mode); print `forge-ui: <url>` for the plan or solo run dir `DIR` (the run list without it) and open a browser the first time (a later `open` of the same run says on stderr that it opened none) |
123
+ | `status [--json]` | exit 0 and describe the server (and whether it is read-only) when it runs, 1 when not |
88
124
  | `stop` | stop the server |
89
125
 
90
126
  `FORGE_UI=off` makes `--ui` on a runner start nothing (it does not stop a running server or block `forge ui open`; use `forge ui stop`), `FORGE_UI_BROWSER=off` never opens a browser,
91
- `FORGE_UI_PORT` (8765) and `FORGE_UI_IDLE_S` (14400, `0` = never) tune the server. State and
127
+ `FORGE_UI_PORT` (8765) and `FORGE_UI_IDLE_S` (14400, `0` = never) tune the server, and `FORGE_UI_READONLY=1` starts it read-only. State and
92
128
  `server.log` are in `${XDG_STATE_HOME:-~/.local/state}/forge/ui/`.
package/docs/reviews.md CHANGED
@@ -120,6 +120,20 @@ lists them. The literal status stays `PASS`, so nothing downstream needed a new
120
120
  It fails safe: an unlabelled CONFIRMED finding, or a FAIL with no CONFIRMED finding at all,
121
121
  is never tolerated, and without a threshold nothing changes.
122
122
 
123
+ Tolerated findings are recorded, not fixed. After a run, the skill offers once to file them as
124
+ GitHub issues, together with PLAUSIBLE findings of passed tasks and whatever a whole-run review
125
+ left open (`--file-issues ask|auto|off`, `ask` by default). Where the
126
+ [gh-proman](https://github.com/Giamme/gh-proman) skill is set up for the repository, it files
127
+ them on the team's project board; anywhere else forge files them itself with plain `gh`:
128
+
129
+ ```sh
130
+ ./forge issues <plan-dir> # drafts only: titles, severity, already filed
131
+ ./forge issues <plan-dir> --include-plausible --apply --label bug
132
+ ```
133
+
134
+ Each issue gets the finding, its severity, the task, reviewer and a code link at the merged
135
+ commit. A hidden fingerprint, shared with gh-proman, keeps a finding from being filed twice.
136
+
123
137
  ## Accepting a task despite its reviewer
124
138
 
125
139
  ```bash
package/forge CHANGED
@@ -21,8 +21,11 @@ usage() {
21
21
  ' fractal opt-in Fractal execution: install, doctor, runs, status, tree, logs, open, report, ...' \
22
22
  ' jev optional Jev advice: setup, status, doctor, enable, disable, backtest' \
23
23
  ' dataset every run'"'"'s inputs and outcomes: status, dump, import, export, outcomes, ...' \
24
+ ' issues file the review findings a run left open as GitHub issues (plain gh)' \
24
25
  ' ui forge-ui, the run viewer: open [DIR], status, stop' \
25
26
  ' runs this repo'"'"'s recent runs in the terminal: state, verdicts and run dirs' \
27
+ ' control a plan'"'"'s live controls: show, set (QA threshold, retries, timeouts), pin, unpin' \
28
+ ' config every config source: show, and set/unset the per-repo .forge files and Jev on/off' \
26
29
  ' update fast-forward this checkout and refresh every harness installation' \
27
30
  ' version the installed version, and whether a newer release is out' \
28
31
  '' \
@@ -47,6 +50,10 @@ case "${1:-}" in
47
50
  shift
48
51
  exec python3 "$ROOT/scripts/forge-dataset.py" "$@"
49
52
  ;;
53
+ issues)
54
+ shift
55
+ exec python3 "$ROOT/scripts/forge-issues.py" "$@"
56
+ ;;
50
57
  ui)
51
58
  shift
52
59
  exec python3 "$ROOT/scripts/forge-ui.py" "$@"
@@ -55,6 +62,14 @@ case "${1:-}" in
55
62
  shift
56
63
  exec python3 "$ROOT/scripts/forge-runs.py" "$@"
57
64
  ;;
65
+ control)
66
+ shift
67
+ exec python3 "$ROOT/scripts/forge-control.py" "$@"
68
+ ;;
69
+ config)
70
+ shift
71
+ exec python3 "$ROOT/scripts/forge-config.py" "$@"
72
+ ;;
58
73
  update)
59
74
  shift
60
75
  exec /bin/bash "$ROOT/scripts/forge-update.sh" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giamme/forge",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -35,7 +35,9 @@ time order: an attempt that PASSed, then a merge that CONFLICTed, then a human `
35
35
  | `run_end` | `run` | results per task, the accepted integration commit, `memory.md` |
36
36
  | `invocation` | user-facing `forge-parallel.sh` subcommand | argv, exit, duration, plus final review, accept, integrate and split/combine payloads |
37
37
  | `event` | point event | `merge` (MERGED, CONFLICT with the files, INVALIDATED and why), `integrate_advanced`, `integrate_conflict`, `combine_conflict`, `sched_terminal`, `subset_run` (every outcome), `memory_record`, `memory_prune` |
38
- | `event` `escalation` | `retry --dwarf` | the model a retry escalated from and to, recorded at the rewrite, before the retried attempt begins |
38
+ | `event` `escalation` | retry that changes the model | the model a retry escalated from and to, `after_attempt`, and `source` (`policy`, `retry` for an explicit `--dwarf`, or `pin`), recorded at the rewrite, before the retried attempt begins |
39
+ | `event` `control` | `forge control` change | a human's live change: `at`, `actor` (`cli`/`ui`), `action` (`set`, `pin`, `unpin`), `key`, `value`, `previous`; task events carry the task id. Also read back from `control.log` by `import` |
40
+ | `event` `run_delete_started` / `run_deleted` | `forge runs rm` | written strictly around a run's deletion: `path`, `run_id`, `archive`, and for `run_deleted` the bytes freed |
39
41
  | `event` `shadow_review` | shadow review | how an opt-in second review ended (`done`, `invalid`, `overtime`, `orphaned`, ...), with the reviewed commit and both specs. Its dispatch is recorded with role `qa_shadow` |
40
42
  | `jev` | Jev request | site, answers, usage, latency, mode. Blob: the **full request** (state and questions). Its original sha is `jev.jsonl`'s `key` |
41
43
  | `fractal_step` / `fractal_node` | Fractal step, node state | decomposition decision, iteration, resolved model, cost, tokens, candidate diff |
@@ -297,6 +299,21 @@ so the importer will not guess which execution wrote a mutable file:
297
299
  - Archives are extracted to a temp dir and mapped back to their original path. The legacy
298
300
  run id hashes that original path, so the archive and the original dir agree on it.
299
301
 
302
+ - **Import is strict when a caller must know.** `importer.import_run` is strict by default and
303
+ `forge runs rm` relies on it: it stores through `record_strict`, which raises (and the deletion
304
+ stops) if any event, blob included, was not stored, or any part of the run could not be read,
305
+ instead of treating an unreadable file as absent. Live capture stays fail-open. Interventions import by content, not by time of capture:
306
+ `tasks/<id>/escalations.tsv` (four or five columns; a missing `source` reads as `policy`) and
307
+ `control.log` produce the same event ids as live capture, so a re-import fills gaps without
308
+ duplicating. A line that does not parse is reported as failed.
309
+ - **Archives describe themselves.** `archive_run` writes `FORGE_ARCHIVE.json` first
310
+ (`format`, `original_path`, `run_id`, `dataset_uid`, `created`), then `FORGE_ARCHIVE_EXCLUDED.txt`, then
311
+ the run dir under its basename. Symlinks, files over 50 MB, `review-*` snapshots and what the
312
+ importer never reads are left out and listed in the excluded file. Importing such an archive maps
313
+ it back to `original_path`, so its run id and uid match the original dir's; an unreadable path
314
+ during archiving is an error, not a silent omission. The archive is written to a temp name, fsynced
315
+ and renamed.
316
+
300
317
  `~/.local/share/forge/legacy/` holds archives of run dirs taken before live capture, with a
301
318
  `MANIFEST.tsv`. Passing that dir to `import` reads every archive, plus the ledger copies
302
319
  listed in `ledgers/REPOS.tsv`.
@@ -390,7 +390,7 @@ realistic input or a broken existing contract; P2 edge-case bug, unusual-timing
390
390
  hardening; P3 minor. Severity is impact; CONFIRMED/PLAUSIBLE is confidence.
391
391
 
392
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 —
393
+ the **least severe level that still blocks**. `tasks/<id>/qa_threshold` overrides it per task (the value `none` exempts the task from a plan threshold; `forge control set ... qa-threshold` writes both and takes effect at the next attempt) —
394
394
  the way to give a dev-tooling or investigation task a looser bar than production code,
395
395
  which otherwise gets a new finding in a new corner of a non-default option every round and
396
396
  only a human decision ends it. The orchestrator decides *deterministically* (`forge-contract.py gate`),
@@ -470,6 +470,24 @@ turns it off):
470
470
  History cannot yet say whether escalating rescues a task (31% of the 29 later attempts on a
471
471
  changed spec passed, mostly effort bumps); the attempts this policy makes are what will.
472
472
 
473
+ **Live controls.** `forge control show|set|pin|unpin <plan>` changes a run while it works:
474
+ `qa-threshold` (read once per attempt, so an attempt keeps the bar it started with; per task, `none`
475
+ exempts it and `inherit` drops the override), `timeout`, `escalate-after`, `verify-retries`, and,
476
+ only while a scheduler is live, `retry-budget` (plan or `--task`) and `infra-retries`. The budgets
477
+ keep the **per invocation** rule: the flags of `run` are not remembered, and a live scheduler only
478
+ mirrors them into `retry_budget`, `infra_budget` and `tasks/<id>/retry_budget` (re-read at every
479
+ decision, removed when it stops; `tasks/<id>/retries.used` stays), so a change dies with the
480
+ run. `pin <task> <spec>` sets the model the task's next full retry uses, once (checked with
481
+ `forge-dispatch.sh doctor`, no quota): `prepare_retry` claims `escalation.pin` as
482
+ `escalation.pin.applying`, rewrites `tasks.tsv`, appends the `escalations.tsv` row with
483
+ `source=pin`, clears `escalation.stop`/`split.suggested`, then consumes the pin. A QA-only resume
484
+ leaves it for the next full retry; Fractal plans never apply pins; a MERGED task refuses one.
485
+ The last column of `escalations.tsv` says who decided: `policy`, `retry` (an explicit
486
+ `--dwarf`) or `pin`; four-column rows read as `policy`. Every change goes to `control.log` (JSON lines:
487
+ `at`, `actor` `cli|ui`, `action`, `task`, `key`, `value`, `previous`) and to the dataset as a
488
+ `control` event. Exit 2 invalid input, 3 refused (not live, MERGED, Fractal, failing doctor,
489
+ lock busy over 5 s). Report them; never set one yourself unless asked.
490
+
473
491
  **Ordinary execution resume:** run `run` again. Tasks already `MERGED` are skipped. A saved PASS is fingerprint-checked and merged without
474
492
  redispatch. PENDING, `INFRA` (paused by an infrastructure failure) and newly unblocked tasks run
475
493
  when eligible. Failed or interrupted tasks
package/references/ui.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # forge-ui
2
2
 
3
- forge-ui is a read-only local web page that shows a forge run live: a decomposed plan or a
3
+ forge-ui is a local web page that shows a forge run live and can change it: a decomposed plan or a
4
4
  solo run. It is opt-in per run. The orchestrator asks "Open forge-ui to watch this run?
5
5
  [y/N]" once at the start of every new interactive run and passes the answer as `--ui` or
6
6
  `--no-ui`. Nothing about the run depends on it: a UI that cannot start prints a note and the
@@ -23,6 +23,15 @@ run goes on, with the same exit code.
23
23
  review's combined diff, with every P0–P3 finding shown under the line it names.
24
24
  - **Fractal child tasks:** the tree a Fractal task was split into, in the inspector and above
25
25
  the opened task in 3D, each node with its decision, cost, output and candidate diff.
26
+ - **Controls:** change a live plan from its inspector: QA threshold, timeout, escalate-after,
27
+ verify retries, and (while a scheduler is live) the retry and infra-retry budgets; per task, a
28
+ threshold override (`None` exempts it), a retry budget and a one-shot model pin. Each is one
29
+ `forge control` call; the change is confirmed first and logged in `control.log`.
30
+ - **Open findings and issues:** the findings a run (or all of its repository's runs) left open,
31
+ filed as GitHub issues or dismissed through `forge issues`.
32
+ - **Delete a run:** a dry run, then `forge runs rm`: the dataset keeps the traces, an archive is
33
+ written unless declined, and clean worktrees and unchecked-out branches can go with it.
34
+ - **Settings:** `forge config show` in one view, with edits for the per-repo files and Jev.
26
35
  - **Export a run:** one HTML file to share that opens without forge-ui; prompts, diffs, reports
27
36
  and local paths stay out unless chosen.
28
37
  - **Quick jump and notifications:** Ctrl K (⌘K) or `/` searches every run, task, project and
@@ -96,10 +105,23 @@ how the map shows all of a repo's runs. `forge-parallel.sh run` also records the
96
105
 
97
106
  ## Privacy
98
107
 
99
- The server binds only `127.0.0.1` and serves read-only. It has **no token**: any process on
100
- the machine, under any user who can reach loopback, can read every run it knows, including
101
- prompts, diffs and review text from private repos. A running server keeps serving them,
102
- including runs that were later rerun with `--no-ui`.
108
+ The server binds only `127.0.0.1`. **Reading needs no token**: any process on the machine,
109
+ under any user who can reach loopback, can read every run it knows, including prompts, diffs and
110
+ review text from private repos. A running server keeps serving them, including runs that were
111
+ later rerun with `--no-ui`.
112
+
113
+ **Changing needs one.** The server never writes a run file, a repo or a config itself: every
114
+ change is a call of forge's own CLI (`forge control`, `forge issues`, `forge runs rm`,
115
+ `forge config`), so validation, locks and the audit log are the CLI's. Each server start makes a
116
+ random token and puts it in the page it serves; every change must send it back, from a `Host` of
117
+ `127.0.0.1` or `localhost` and with an `Origin` equal to that host when the browser sends one, or
118
+ it is refused before any CLI runs. There are no CORS headers, so another site in your browser
119
+ can neither send nor read a change. Any process that can fetch the page can read the token, as
120
+ it can read the runs, so on a shared machine start the server **read-only**: `--read-only`
121
+ (`python3 -m forge_ui`) or `FORGE_UI_READONLY=1` (also for `forge ui open`). A read-only server
122
+ issues no token and answers every change with 405; its `/api/health` says `"controls": false`.
123
+ `forge ui open` never reuses a server in the other mode: it replaces it, as it does one running
124
+ old code, and `forge ui status` shows the mode.
103
125
 
104
126
  Every run is registered, so a running server shows every run on the machine, not only those
105
127
  opened with `--ui`. The 3D view fetches three.js from cdn.jsdelivr.net (no run data is sent;