@giamme/forge 0.8.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 (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +22 -3
  4. package/VERSION +1 -1
  5. package/docs/README.md +26 -0
  6. package/docs/add-ons.md +33 -0
  7. package/docs/artifacts.md +84 -0
  8. package/docs/commands.md +365 -0
  9. package/docs/dataset.md +29 -0
  10. package/docs/decomposed-runs.md +310 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +128 -0
  13. package/docs/fractal.md +181 -0
  14. package/docs/install.md +160 -0
  15. package/docs/memory.md +156 -0
  16. package/docs/models.md +118 -0
  17. package/docs/reviews.md +245 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +20 -0
  21. package/package.json +3 -2
  22. package/references/dataset.md +18 -1
  23. package/references/decompose.md +20 -2
  24. package/references/fractal.md +1 -1
  25. package/references/harnesses.md +18 -6
  26. package/references/solo.md +1 -1
  27. package/references/ui.md +27 -5
  28. package/scripts/forge-config.py +347 -0
  29. package/scripts/forge-control.py +645 -0
  30. package/scripts/forge-dispatch.sh +7 -1
  31. package/scripts/forge-issues.py +806 -0
  32. package/scripts/forge-lib-task.sh +234 -35
  33. package/scripts/forge-parallel-options.sh +3 -1
  34. package/scripts/forge-parallel.sh +3 -2
  35. package/scripts/forge-route.py +184 -27
  36. package/scripts/forge-runs-rm.py +840 -0
  37. package/scripts/forge-runs.py +139 -0
  38. package/scripts/forge-runtime.py +59 -6
  39. package/scripts/forge-schedule.py +227 -11
  40. package/scripts/forge-ui.py +85 -19
  41. package/scripts/forge-version.py +54 -12
  42. package/scripts/forge_dataset/__init__.py +77 -6
  43. package/scripts/forge_dataset/capture.py +5 -2
  44. package/scripts/forge_dataset/harvest.py +23 -1
  45. package/scripts/forge_dataset/importer.py +420 -18
  46. package/scripts/forge_fractal/__init__.py +2 -1
  47. package/scripts/forge_registry.py +168 -16
  48. package/ui/README.md +116 -23
  49. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  50. package/ui/docs/api.md +283 -23
  51. package/ui/forge_ui/__main__.py +10 -2
  52. package/ui/forge_ui/control.py +575 -0
  53. package/ui/forge_ui/export.py +33 -9
  54. package/ui/forge_ui/fractal.py +1 -2
  55. package/ui/forge_ui/reader.py +121 -8
  56. package/ui/forge_ui/server.py +135 -5
  57. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  58. package/ui/forge_ui/static/app.js +358 -131
  59. package/ui/forge_ui/static/cloud3d.js +3 -2
  60. package/ui/forge_ui/static/cloudui.js +8 -5
  61. package/ui/forge_ui/static/controls.js +619 -0
  62. package/ui/forge_ui/static/convo.js +295 -0
  63. package/ui/forge_ui/static/datatable.js +232 -0
  64. package/ui/forge_ui/static/diffview.js +29 -10
  65. package/ui/forge_ui/static/findings3d.js +134 -0
  66. package/ui/forge_ui/static/findingsview.js +412 -0
  67. package/ui/forge_ui/static/graph.js +3 -3
  68. package/ui/forge_ui/static/icons.js +94 -0
  69. package/ui/forge_ui/static/index.html +31 -13
  70. package/ui/forge_ui/static/iso.js +70 -19
  71. package/ui/forge_ui/static/leaderboard.js +44 -48
  72. package/ui/forge_ui/static/panel.js +92 -36
  73. package/ui/forge_ui/static/rundelete.js +388 -0
  74. package/ui/forge_ui/static/runmodel.js +17 -1
  75. package/ui/forge_ui/static/scene3d.js +518 -43
  76. package/ui/forge_ui/static/search.js +2 -1
  77. package/ui/forge_ui/static/settings.js +399 -0
  78. package/ui/forge_ui/static/shell.js +137 -0
  79. package/ui/forge_ui/static/stagectl.js +150 -0
  80. package/ui/forge_ui/static/style.css +1099 -469
  81. package/ui/forge_ui/static/theme.js +25 -0
  82. package/ui/forge_ui/static/uikit.js +161 -0
  83. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,245 @@
1
+ # Reviews, retries and recovery
2
+
3
+ [← README](../README.md) · [All docs](README.md)
4
+
5
+ What a reviewer must answer, what forge does when it fails a task, how to retry, tolerate or accept findings, and the checks forge runs around the reviewer.
6
+
7
+ ## Verdicts and failure
8
+
9
+ Decompose-mode QA must end with a sentinel line:
10
+
11
+ ```
12
+ FORGE_VERDICT: PASS no confirmed correctness bug (style nits are not failures)
13
+ FORGE_VERDICT: FAIL at least one CONFIRMED correctness bug
14
+ ```
15
+
16
+ Only a standalone final line counts, and the parser is shared by every runner (solo,
17
+ decomposed, Fractal's QA checkpoint). The prompt shows each verdict with a dash and its
18
+ meaning, and some reviewers copy that whole line, so `FORGE_VERDICT: PASS — meaning` is
19
+ accepted, as are trailing whitespace and CRLF line endings. Prose or a quoted marker earlier
20
+ in the reply is never a verdict. A missing verdict is `UNKNOWN` and is treated exactly like a
21
+ failure — excluded from integration and flagged. Merging a diff whose reviewer never reached
22
+ a conclusion would defeat the point of reviewing it, so the ambiguous case fails safe.
23
+
24
+ A failing task is excluded and **its branch and worktree are preserved**; the rest of the run
25
+ proceeds. forge never loops a dwarf against its reviewer on its own: a retry happens when you
26
+ type `retry`, or when you opt in for a run with `--retry-failed N` ([below](#retrying-inside-a-run---retry-failed-n)).
27
+
28
+ An `UNKNOWN` task is different from a rejected one: the reviewer never judged the code, so
29
+ `retry` (or `--retry-failed`) asks **QA again on the unchanged work** instead of paying an
30
+ implementer to rewrite something nobody rejected. No attempt is spent and no dwarf runs. If
31
+ the worktree no longer matches what was reviewed, the full task runs instead.
32
+
33
+ ## Retrying a failed task
34
+
35
+ ```bash
36
+ # re-run one task with the findings that failed it, on a stronger model
37
+ bash scripts/forge-parallel.sh retry "$PLAN" middleware --dwarf sol:xhigh
38
+ ```
39
+
40
+ The findings are the point. `retry` prepends the previous reviewer's report to the dwarf's
41
+ prompt — "your last attempt was reviewed and these were the findings; fix them" — and reuses
42
+ the worktree, so the dwarf continues from its own code rather than starting over. If the
43
+ verdict comes back `PASS` the task merges like any other.
44
+
45
+ `--dwarf` is written back into `tasks.tsv`, so the table shows the model that will actually
46
+ be spent and the escalation survives a re-plan.
47
+
48
+ Two things it deliberately does:
49
+
50
+ - **Reviews the task's cumulative diff**, against the commit the task was originally branched
51
+ from — not just the fix. Reviewing the fix alone would let the first attempt's code through
52
+ unread.
53
+ - **Refuses to re-review identical code.** If the retry produces a byte-identical diff, the
54
+ task fails again without spending a QA dispatch.
55
+
56
+ `retry` is something you run. Forge never loops a dwarf against its own reviewer on its own
57
+ initiative — unless you ask a *run* to with `--retry-failed N`, see
58
+ [retrying inside a run](#retrying-inside-a-run---retry-failed-n). A task whose reviewer
59
+ never gave a verdict (`UNKNOWN`) or whose run stopped on an infrastructure failure (`INFRA`)
60
+ resumes at the review stage instead: QA runs again on the unchanged work, no dwarf runs, and
61
+ no attempt is spent.
62
+
63
+ ## Retrying inside a run: `--retry-failed N`
64
+
65
+ ```bash
66
+ forge-parallel.sh run <plan-dir> --retry-failed 2
67
+ ```
68
+
69
+ A task whose reviewer says `FAIL` (or `UNKNOWN`) is re-queued at once, **inside the same
70
+ run**, up to `N` more times — with the reviewer's findings carried into the next prompt exactly
71
+ as `retry` does. Other tasks keep running meanwhile; dependents still wait for the task to
72
+ *merge*, so nothing downstream starts early. Without this, a failed task waited for the whole
73
+ run (verification included) to finish before a human could retry it.
74
+
75
+ It is opt-in and per invocation: it is not remembered by the plan, and the orchestrator never
76
+ adds it unprompted. Only reviewer rejections are retried automatically — `TIMEOUT`, `ERROR`,
77
+ `INVALIDATED` and `CONFLICT` still need a human. A retry that changes nothing (empty diff, or
78
+ a diff byte-identical to the previous attempt) writes a `noretry` marker and is **not**
79
+ retried again, so a stuck task cannot burn its whole budget repeating itself.
80
+ `tasks/<id>/attempts.tsv` records every entry (epoch, attempt number, resulting status, infra
81
+ class).
82
+
83
+ ## Infrastructure failures are not task failures
84
+
85
+ A quota or usage limit, an auth problem, a rate limit, a network error, or a harness that
86
+ returns nothing at all says nothing about the work. forge classifies those from the dispatch
87
+ log (`forge-runtime.py classify`) and `forge-dispatch.sh` exits **8** instead of 4. A
88
+ decomposed task then ends as **`INFRA`**, not `ERROR`/`FAIL`, and:
89
+
90
+ - **no attempt is spent** — a dwarf-stage stop refunds the counter; a QA-stage stop keeps the
91
+ dwarf's committed work and re-runs **QA only** next time (`tasks/<id>/resume`, revalidated
92
+ against the branch tip and fingerprint before it is trusted);
93
+ - the scheduler stops dispatching, lets running tasks drain, leaves unstarted ones `PENDING`
94
+ (never `BLOCKED`) and `run` exits **8**: fix the cause, then run the same command again;
95
+ - `run --infra-retries M` waits instead — up to `M` times, using the provider's retry-after
96
+ hint when it gave one (capped at 6 hours) and otherwise 60 s doubling to 30 min
97
+ (`FORGE_INFRA_BACKOFF` sets the base) — and resumes in the same run;
98
+ - verification is skipped for a paused run (it is unfinished); the next `run` verifies at its end.
99
+
100
+ `.infra` (`dwarf.infra`/`qa.infra`) holds `class=`, `retry_after=`, `detail=`, `rc=`,
101
+ `role=`. Detection is deliberately strict: strong phrases only, and ordinary text that merely
102
+ mentions "rate limit" in a reply that otherwise succeeded is never classified. A false
103
+ positive costs a pause, not a spent attempt. Under Fractal a dwarf-side infrastructure stop
104
+ still collapses to Fractal's own "failed" handling; QA-side stops propagate as above.
105
+
106
+ ## Severity, thresholds and known issues
107
+
108
+ QA is asked to label every finding `- [P0..P3][CONFIRMED|PLAUSIBLE] file:line — what breaks`
109
+ (P0 data loss/security/crash on the main path, P1 wrong behaviour on realistic input, P2
110
+ edge-case bug, P3 minor). By default the verdict rule is unchanged — any CONFIRMED bug fails.
111
+
112
+ `--qa-threshold Pn` (on `run` or `retry`; remembered in the plan; `none` clears it) names the
113
+ **least severe level that still blocks**: `P2` means P0–P2 block and only P3 is tolerated.
114
+ A task-specific `tasks/<id>/qa_threshold` file beats the plan-level value, which is how a
115
+ dev-tooling or investigation task gets a looser bar than production code. With a threshold, a
116
+ reviewer `FAIL` made only of CONFIRMED findings that are all labelled and strictly milder than
117
+ the threshold is accepted as **PASS with known issues**: the task merges, `tasks/<id>/known_issues.md`
118
+ keeps the findings, `qa.gate` records the decision, and every run/retry/integrate summary
119
+ lists them. The literal status stays `PASS`, so nothing downstream needed a new vocabulary.
120
+ It fails safe: an unlabelled CONFIRMED finding, or a FAIL with no CONFIRMED finding at all,
121
+ is never tolerated, and without a threshold nothing changes.
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
+
137
+ ## Accepting a task despite its reviewer
138
+
139
+ ```bash
140
+ bash scripts/forge-parallel.sh accept "$PLAN" tooling \
141
+ --reason "only an unreachable option left; tracked in the backlog" --approved
142
+ ```
143
+
144
+ When a reviewer keeps finding new corners of something that should ship (one real
145
+ investigation harness got 12 findings over five rounds, and only a human decision ended it),
146
+ `accept` is that decision made explicit instead of done with git plumbing. It needs **both**
147
+ `--reason` and `--approved`, because it overrides a reviewer; an orchestrating model never runs
148
+ it on its own. It accepts only a `FAIL` or `UNKNOWN` task whose reviewed work is **intact** —
149
+ the branch is still at the reviewed commit and the worktree still has the saved fingerprint —
150
+ otherwise it exits `3` and says to `retry` for a fresh review (accepting would sign off on code
151
+ nobody reviewed). It writes `tasks/<id>/known_issues.md` (your reason, who and when, and the
152
+ reviewer's reply copied verbatim) and `accepted.by`, merges through the same checks as any other
153
+ task, and puts everything back if the merge conflicts (exit `6`). Dependents are not touched;
154
+ `run` continues them.
155
+
156
+ ## Whole-run review (opt-in)
157
+
158
+ Per-task QA only sees one slice. In one real run, a holistic review of the combined result still
159
+ found two confirmed P2 defects in code every per-task review had passed. It is opt-in because it
160
+ spends QA quota and `integrate` made no model calls before:
161
+
162
+ ```bash
163
+ bash scripts/forge-parallel.sh review "$PLAN" --qa opus # read-only look at the combined result
164
+ bash scripts/forge-parallel.sh integrate "$PLAN" --approved --final-review opus # and gate the merge on it
165
+ ```
166
+
167
+ The reviewer gets the overall goal, every task's complete requirements (bounded to about 80 KB,
168
+ truncated visibly), what forge observed that no per-task reviewer saw (accepted known issues,
169
+ scope drift, guard notes, the verification result including a `FLAKY` pass and coverage
170
+ warnings), the combined diff (inline up to 400 KB), and the same labelled-findings contract —
171
+ with an emphasis on cross-task interactions, interface mismatches and ordering races. `review`
172
+ reads `integration.base..integration tip` from a disposable checkout, writes
173
+ `<plan>/final-review-<epoch>-<pid>/` (`qa.input`, `qa.last`, `verdict`, `qa.gate`, …), keeps
174
+ `<plan>/final-review` and `final-review.verdict` pointing at the newest, and never touches your
175
+ branch or the integration branch. With `integrate --final-review`, it runs after verification
176
+ and before your branch can move: `PASS` (or a `FAIL` accepted by `--qa-threshold`) continues,
177
+ anything else stops with exit `5`; an infrastructure stop exits `8`. The QA spec is `--qa`, else
178
+ the one remembered in `final_qa`, else the single spec every task shares (otherwise it asks).
179
+ Tasks that are not merged are not in the result, and it says so. `integrate` also names unmerged
180
+ tasks even without a review.
181
+
182
+ ## Shadow reviews: how much of a verdict is the reviewer?
183
+
184
+ `plan --shadow-qa <spec>[,<spec>...] [--shadow-rate R]` (default R = 0.1) sends a sampled
185
+ share of reviewed attempts to a second reviewer, the first listed one of another model
186
+ family than the task's. Its verdict goes to the run dataset and nothing in the run reads it,
187
+ so it never changes a status, merge, memory or retry. `forge dataset status` then reports how
188
+ often the two reviewers agree, overall and per model pair. The table shows the extra reviews
189
+ before you approve. The shadow runs beside the main review and is stopped soon after that
190
+ finishes, so the most it costs is reviews and a little latency. See
191
+ [decomposition](../references/decompose.md#shadow-reviews-opt-in).
192
+
193
+ ## Implementer guards
194
+
195
+ The dwarf is told not to commit, switch branches, start background processes, or claim checks
196
+ it did not run. forge then **observes** whether it did, rather than trusting its report:
197
+
198
+ - **self-commit / branch switch** — HEAD or the checked-out ref moved during the dispatch;
199
+ - **orphan processes** — the harness runs in its own session; anything still alive in it after
200
+ the harness exits is listed in `<role>.orphans` and stopped (`FORGE_ORPHAN_GUARD=off`
201
+ disables this; it is also skipped under Fractal);
202
+ - **promised later work** — "running in the background", "will report back once it
203
+ finishes" and similar in the final message.
204
+
205
+ These are warnings recorded in `tasks/<id>/guard.txt`, shown in the QA prompt ("Implementer
206
+ guard notes") and in the run summary. They never change a status: the diff and the reviewer
207
+ still decide. In solo mode a self-commit prints the `git reset --soft <old HEAD>` that would
208
+ undo it; forge never resets anything itself.
209
+
210
+ ## Verification coverage and flaky reruns
211
+
212
+ `--verify` is whatever you hand it, and a gate that skips a suite the project defines lets a
213
+ regression there reach your branch silently. `forge-verify-coverage.py` reads the project's own
214
+ test configuration (`package.json` scripts, Makefile targets, pytest/tox, Cargo, Go, Ruby,
215
+ Elixir, `scripts/test*.sh`), follows aggregate scripts, and **warns** — before any model is
216
+ paid for, again in the run summary and at integrate — about suites the verify command never
217
+ runs, e.g. `suite 'test:contract' (npm run test:contract) is not run by the verify command`.
218
+ It is advisory: it never fails a run, and `verification.coverage.txt` keeps the lines. It is a
219
+ text heuristic (root of the repo only; a mentioned command counts as run).
220
+
221
+ A failing verify command is rerun **once** (`--verify-retries N`; `0` restores the single strict
222
+ run) when the source fingerprint is unchanged. If the rerun passes the status is `PASS` but
223
+ `verification.flaky` is written, `verification.log` keeps the first (failing) run,
224
+ `verification.retry.log` the second, and `run`/`integrate` print a loud `FLAKY` line — an
225
+ intermittent failure can be a real bug, so it is never passed off as a clean pass. A command
226
+ that changes the tree is never retried. This deterministic rerun comes before Jev's optional
227
+ model-assisted triage, which then does not run.
228
+
229
+ ## Native review
230
+
231
+ `--native-review` switches a codex reviewer to its purpose-built reviewer, for a diff too large
232
+ to inline. Know the trade: codex refuses a custom prompt alongside its scope flags, so the
233
+ reviewer never learns what the dwarf was *asked* to do. It can still judge whether the code is
234
+ correct, but not whether it's the right change, so its result is informational and the verdict
235
+ is `UNKNOWN`. See [solo runs](../references/solo.md) for how the baseline is passed.
236
+
237
+ ## Fail-first evidence (opt-in)
238
+
239
+ `--fail-first` (on a solo run or `plan`) checks that the tests a change adds or edits can
240
+ actually fail. Before each review, forge runs each new or edited test file against the change
241
+ and against the base without it, using the per-file command in `.forge/verify-subset`, and hands
242
+ the reviewer the result: `proven`, `passes_without_change`, `inconclusive` or `error`. The
243
+ reviewer is asked for a P2 finding for every test that passes without the change. It is advice
244
+ only: forge never changes a status or verdict from it. forge-ui shows the evidence under the
245
+ review findings. See [fail-first evidence](../references/decompose.md#fail-first-evidence-opt-in).
package/docs/safety.md ADDED
@@ -0,0 +1,55 @@
1
+ # Safety
2
+
3
+ [← README](../README.md) · [All docs](README.md)
4
+
5
+ What forge never does to your branch and working tree, what it refuses to run on, and how it keeps reviewed work and merged work the same.
6
+
7
+ ## Safety guarantees
8
+
9
+ - Solo runs leave source changes uncommitted. Forge creates private review commits, but
10
+ does not commit on the solo source branch, push, or reset the user tree.
11
+ - In a decomposed run, commits and merges happen **only** on the `forge/<run-id>/*` task
12
+ branches and the `forge/<run-id>-integration` branch — branches forge created for itself.
13
+ Never your branch, never a push. Your branch and working tree are untouched for the whole
14
+ run.
15
+ - Bypass flags are never added on forge's own initiative. `--yolo-dwarf` / `--yolo-qa` are the
16
+ only route to an unsandboxed run, so that choice is always explicit and visible.
17
+ - **One dwarf per working tree, always.** Two agents editing one tree interleave their edits
18
+ into a diff neither of them wrote and qa cannot untangle. Parallelism comes from separate
19
+ worktrees, which is why worktrees are mandatory there rather than an optimization.
20
+ - A failed task's branch and worktree are **never** deleted — they're the only record of what
21
+ went wrong and the only thing to retry from. A successful task's worktree is removed only
22
+ after `results.tsv` is written and read back.
23
+ - QA runs in a disposable repository. Mutations to the source or review snapshot invalidate
24
+ acceptance. Disabled editing tools alone do not prevent writes through Bash.
25
+
26
+ ## Preconditions
27
+
28
+ - Not a git repo → decomposed runs refuse. Worktrees are impossible and there's no honest
29
+ fallback.
30
+ - Uncommitted changes → **warned prominently.** Worktrees branch from a commit, so your local
31
+ edits are invisible to every dwarf. This is the likeliest route to a confusingly wrong
32
+ result.
33
+
34
+ ## Execution integrity
35
+
36
+ Forge now captures solo changes against a recorded starting snapshot, including staged edits,
37
+ without altering the user's index. Existing work is recorded separately in `existing.diff`.
38
+ QA receives the full implementation requirements and reviews a disposable repository; source
39
+ or snapshot changes invalidate the result. Only the recorded reviewed task commit can merge.
40
+
41
+ Parallel execution rechecks dependencies before each task and retry. Failed prerequisites leave
42
+ consumers BLOCKED. Directory and child file declarations are serialized.
43
+
44
+ `forge-dispatch.sh doctor --spec sol --role dwarf` validates a selected model recipe using local
45
+ CLI help. Solo and parallel runners preflight the whole selected pipeline before implementation.
46
+ These checks do not establish authentication, billing or live model availability.
47
+
48
+ Use `forge-parallel.sh run <plan> --verify 'your check command'` or put the command in
49
+ `.forge/verify` to verify the combined result. Output and exit status are saved with the plan.
50
+ Integration checks a candidate containing the current user branch before updating it. A missing
51
+ command is reported as UNVERIFIED; a failing configured check blocks integration. The project's
52
+ own test suites that the command does not run are named as warnings (advisory), and a failing
53
+ command is rerun once so a flaky lane is reported rather than failing the run — see
54
+ [verification coverage and flaky reruns](reviews.md#verification-coverage-and-flaky-reruns).
55
+ `integrate` also names any task that is not merged and therefore not part of what it delivers.
@@ -0,0 +1,54 @@
1
+ # Troubleshooting
2
+
3
+ [← README](../README.md) · [All docs](README.md)
4
+
5
+ Symptoms, causes and fixes, from a skill a harness cannot see to a task that will not merge.
6
+
7
+ | Symptom | Cause / fix |
8
+ |---|---|
9
+ | skill missing in **codex** only | frontmatter isn't strict YAML. Codex refuses to load and logs `invalid YAML`; the other four are lenient and load it fine. Note codex's line numbers exclude the opening `---`, so its "line 3" is the file's line 4 |
10
+ | skill missing in **antigravity** | `agy` ignores `~/.gemini/skills` entirely — skills load only inside a plugin. Re-run `forge-install.sh` |
11
+ | antigravity runs an **old version** | `agy plugin install` copies. Re-run `forge-install.sh` after every edit |
12
+ | duplicate entry in **opencode** | opencode auto-scans `~/.claude/`; a `~/.config/opencode/command/forge.md` on top is redundant |
13
+ | agy: `Find command timed out` | missing `--add-dir <repo>` — forge passes it; a hand-built command must too |
14
+ | agy dwarf ends with an error and an empty diff | it tried to run a shell command without yolo. A single denial aborts the run — use `--yolo-dwarf` |
15
+ | dwarf produced no changes | usually an ambiguous prompt it couldn't resolve headlessly. Nobody can answer a question mid-run |
16
+ | task status `ERROR` | worktree creation or a dispatch failed — read `tasks/<id>/dwarf.out` / `qa.out` |
17
+ | task status `UNKNOWN` | QA never emitted a verdict line; read `tasks/<id>/qa.last`. `retry` (or `run --retry-failed N`) asks QA again on the unchanged work — no dwarf runs, no attempt is spent |
18
+ | task status `INFRA`, `run` exits `8` | a quota/usage limit, auth problem, rate limit, network error or empty answer stopped a dispatch — not a task failure, and no attempt was spent. Read `tasks/<id>/infra.txt` (`class=`, `retry_after=`), fix the cause, run the same command again. `run --infra-retries M` waits it out instead. Dependents stay `PENDING`, never `BLOCKED` |
19
+ | a task merged although its reviewer said FAIL | `--qa-threshold` accepted it: every CONFIRMED finding was labelled and milder than the threshold. See `tasks/<id>/known_issues.md` and `qa.gate`; nothing is accepted without an explicit threshold |
20
+ | `accept` exits `3` "changed since it was reviewed" | the branch or worktree moved after QA, so accepting would sign off on unreviewed code. `retry` for a fresh review |
21
+ | `combine` / `split` refuses | the message names why: unmerged task left behind, duplicate ids, locked or dirty plan, a Fractal-backed plan, or a selection that strands a dependent. Nothing was changed |
22
+ | `integrate --final-review` stops with exit `5` | the whole-run reviewer found a blocking defect (or never reached a verdict); findings are in `<plan>/…/final-review/qa.last`. Your branch is untouched. Exit `8` means the reviewer hit a quota/auth stop: run it again |
23
+ | `FLAKY` in the summary, verification `PASS` | the verify command failed once and passed on rerun. Read `verification.log` (the first run) — an intermittent failure can be a real bug. `--verify-retries 0` restores the strict single run |
24
+ | `verify coverage: suite '…' is not run by the verify command` | the project defines a suite your `--verify` skips. Advisory only; add it to the command (or `.forge/verify`) if it should gate the merge |
25
+ | implementer guard notes in the summary | the dwarf committed, switched branch, left processes running, or promised later work. Recorded in `tasks/<id>/guard.txt`; the diff and the reviewer still decide. Leftover processes were stopped |
26
+ | task status `CONFLICT` | QA passed but the merge conflicted — `files` was understated somewhere. Check the run's scope-drift list: it usually names the file |
27
+ | task status `TIMEOUT` | that role exceeded the dispatch timeout and was killed. Raise `--timeout`, or `retry` — the partial edit is still in the worktree |
28
+ | a dispatch never returns | it can't any more: every backend is killed at its timeout (`--timeout`, else `FORGE_TIMEOUT`, else the registry's `timeout` column, else 45 min — 60 min for the default `xhigh` QA/planner effort) |
29
+ | a task won't re-run: "already exists" | a worktree directory was deleted by hand. `run` and `retry` prune stale registrations first, so re-run rather than deleting the branch |
30
+ | a failed task is expensive to redo | it isn't — `forge-parallel.sh retry <dir> <id> [--dwarf <spec>]` re-runs that one task with its reviewer's findings, in the worktree it already has |
31
+ | an interrupted run seems lost | run `run` again; merged tasks are skipped, not re-dispatched |
32
+ | every wave has exactly one task | `files` sets overlap across most tasks; the decomposition isn't actually parallel |
33
+ | dwarves reimplement already-merged work | `goal.txt` or `files` missing, so the capsule carries no useful status |
34
+ | several tasks report "produced no changes" | the decomposition made tasks that weren't independently actionable — use a coarser `--decompose-level`, not more retries |
35
+ | `retry` says "produced no new changes" | the dwarf returned a byte-identical diff, so no reviewer was paid to read the same code twice. Escalate with `--dwarf`, or fix the prompt |
36
+ | a new file's code never reached QA | fixed: untracked files are diffed against `/dev/null`. A hand-built capture using only `git status --porcelain` shows the reviewer a filename and no content |
37
+ | claude dwarf ends "I need your permission to edit" | a permission mode that prompts for Edit. forge uses `acceptEdits` plus an explicit Bash allowance, because `auto` allows shell but prompts for Edit, and `acceptEdits` alone allows Edit but prompts for real shell commands |
38
+ | `.forge/` turns up in a review | a diff capture is missing `':(exclude).forge'` |
39
+ | memory stays empty | nothing durable was learned — the common, correct case. The ledger still has a row per dispatch |
40
+ | a wrong fact keeps reappearing | `memory.md` is rebuilt from the ledger; delete the ledger rows, not the memory line |
41
+ | dwarves build incompatible interfaces | the seam was never agreed — plan the approach, or use `--planner` |
42
+ | Fractal was installed but a new unattended run uses ordinary execution | installation never activates it; pass `--fractal` explicitly |
43
+ | `Fractal selected but unavailable` | run `forge fractal doctor`, then the suggested managed install; resume retains the selected backend |
44
+ | existing run rejects a new backend, pool or limit | selection and bounds are recorded; create a new run directory for changed settings |
45
+ | Fractal reports completion but Forge has not accepted the work | inspect QA, verdict and verification; execution completion does not establish acceptance |
46
+ | Fractal import stops with `source_drift` | the source changed after its snapshot; preserve both versions and inspect the candidate instead of forcing import |
47
+ | Fractal work was paused or its coordinator exited | inspect `status` and `logs`, then use explicit `resume RUN_ID`; retry starts a new attempt |
48
+ | browser dashboard closed while work is running | execution is independent; reopen with `forge fractal open RUN_ID` |
49
+ | costs are unknown or a dollar cap is rejected | the bridge only reports available usage; v1 cannot enforce a spending cap |
50
+
51
+ For per-harness invocation details, effort ladders and known CLI failure modes, see
52
+ [`references/harnesses.md`](../references/harnesses.md). For decomposition mechanics, see
53
+ [`references/decompose.md`](../references/decompose.md). For memory internals, see
54
+ [`references/memory.md`](../references/memory.md).
package/forge CHANGED
@@ -21,7 +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' \
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' \
25
29
  ' update fast-forward this checkout and refresh every harness installation' \
26
30
  ' version the installed version, and whether a newer release is out' \
27
31
  '' \
@@ -46,10 +50,26 @@ case "${1:-}" in
46
50
  shift
47
51
  exec python3 "$ROOT/scripts/forge-dataset.py" "$@"
48
52
  ;;
53
+ issues)
54
+ shift
55
+ exec python3 "$ROOT/scripts/forge-issues.py" "$@"
56
+ ;;
49
57
  ui)
50
58
  shift
51
59
  exec python3 "$ROOT/scripts/forge-ui.py" "$@"
52
60
  ;;
61
+ runs)
62
+ shift
63
+ exec python3 "$ROOT/scripts/forge-runs.py" "$@"
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
+ ;;
53
73
  update)
54
74
  shift
55
75
  exec /bin/bash "$ROOT/scripts/forge-update.sh" "$@"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@giamme/forge",
3
- "version": "0.8.0",
4
- "description": "One model builds it. A different model reviews what it actually built.",
3
+ "version": "0.10.0",
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",
7
7
  "author": "Giamme",
@@ -14,6 +14,7 @@
14
14
  "VERSION",
15
15
  "CHANGELOG.md",
16
16
  "README.md",
17
+ "docs/",
17
18
  "LICENSE",
18
19
  "registry.tsv",
19
20
  "references/",
@@ -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
@@ -521,7 +539,7 @@ its last message, and to say plainly what it could not run.
521
539
  ## Accepting, splitting, combining and the whole-run review
522
540
 
523
541
  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:
542
+ [README](../docs/reviews.md#accepting-a-task-despite-its-reviewer); the mechanics:
525
543
 
526
544
  - **`accept <plan> <task> --reason … --approved`** overrides a `FAIL`/`UNKNOWN` reviewer only
527
545
  when the reviewed commit is still the branch tip and the worktree still matches
@@ -5,7 +5,7 @@ model routing, independent QA of the cumulative diff, verification and integrati
5
5
  Fractal execution completion and Forge acceptance are separate outcomes. Costs are
6
6
  observational, and this integration makes no speed or cost-saving claim.
7
7
 
8
- For a first run, follow the [README walkthrough](../README.md#fractal-execution).
8
+ For a first run, follow the [Fractal guide](../docs/fractal.md).
9
9
  This reference covers the exact selection, routing, lifecycle and inspection contracts.
10
10
 
11
11
  ## Contents
@@ -46,8 +46,8 @@ disables), else the optional `timeout` column of the model's registry row, else
46
46
 
47
47
  **Infrastructure failures exit `8`.** After the backend exits, `forge-runtime.py classify` reads
48
48
  the dispatch log (never `<role>.last`, which is only created afterwards): claude/openclaude/grok
49
- `result` events with `is_error`, codex `turn.failed`/`error` events that nothing followed,
50
- and for the raw-log harnesses (opencode, antigravity) the last 2 KB when the exit was nonzero —
49
+ `result` events with `is_error`, codex `turn.failed`/`error` and opencode `error` events that no
50
+ answer followed, and for the raw-log harness (antigravity) the last 2 KB when the exit was nonzero —
51
51
  looking for strong phrases (usage/session limit, quota, insufficient balance/credit, rate limit,
52
52
  429/529, unauthorized/401/403, invalid API key, not logged in, ECONNRESET/ENOTFOUND/…). An
53
53
  `empty` answer (exit 0, nothing said) is the fifth class. codex's non-fatal reconnect notices
@@ -90,8 +90,9 @@ reason forge validates efforts itself before dispatch.
90
90
 
91
91
  **Prompt delivery uses stdin.** Forge copies the input once (including FIFOs), checks it
92
92
  with a streaming whitespace character class, and runs `codex exec ... - < role.prompt`.
93
- Native review has no custom prompt and uses `/dev/null`. Codex JSONL events and Claude
94
- `--output-format json` provide native usage; final responses remain in `<role>.last`.
93
+ Native review has no custom prompt and uses `/dev/null`. Codex JSONL events, Claude and openclaude
94
+ `--output-format stream-json` and opencode `--format json` provide native usage; final responses
95
+ remain in `<role>.last`.
95
96
  See the official [Codex non-interactive guide](https://learn.chatgpt.com/docs/non-interactive-mode)
96
97
  and [Claude headless guide](https://code.claude.com/docs/en/headless).
97
98
 
@@ -109,12 +110,18 @@ and `--approve-for-me` additionally conflicts with an explicit `-s/--sandbox`.
109
110
  ## claude / openclaude
110
111
 
111
112
  ```
112
- <harness> -p --model <model> --add-dir <run-dir> --effort <effort> \
113
+ <harness> -p --model <model> --add-dir <run-dir> --output-format stream-json --verbose --effort <effort> \
113
114
  (--permission-mode acceptEdits --allowedTools Bash | --dangerously-skip-permissions)
114
115
  [--disallowed-tools "Edit,Write,NotebookEdit"]
115
116
  ```
116
117
  with the prompt delivered on **stdin**.
117
118
 
119
+ **The log is a stream.** `--output-format stream-json` (which `-p` accepts only with `--verbose`)
120
+ writes one JSON message per line: the session, every assistant message and tool call, every tool
121
+ result, and a closing `result` line with the final answer and the token counts, which is what
122
+ forge reads into `<role>.last` and the metrics. The rest is the run's conversation, which forge-ui
123
+ shows. Hook and rate-limit events in the stream are ignored.
124
+
118
125
  **The prompt must not go on argv when a variadic flag precedes it.** `--disallowed-tools`,
119
126
  `--allowed-tools` and friends are variadic, so a following positional is consumed as
120
127
  another value. In practice the prompt is parsed word by word into tool names — you get a
@@ -162,7 +169,7 @@ Claude-Code-shaped harness. That is the point of `sol:xhigh:openclaude`.
162
169
  ## opencode
163
170
 
164
171
  ```
165
- opencode run -m <provider>/<model> --dir <repo> --variant <effort> --auto [--agent plan] "<prompt>"
172
+ opencode run -m <provider>/<model> --dir <repo> --variant <effort> --auto [--agent plan] --format json "<prompt>"
166
173
  ```
167
174
 
168
175
  Models are always `provider/model`; `opencode models` lists what is configured.
@@ -195,6 +202,11 @@ agy --model <model> --print-timeout <t> [--effort <e>] --add-dir <repo> --add-di
195
202
  The harness is named `antigravity` in forge; the binary is `agy`. This is how Gemini is
196
203
  reached.
197
204
 
205
+ **Its log stays plain text.** agy has `--output-format stream-json`, but only its closing event
206
+ has been seen (`{"event": "result", "result": {"status", "response", "error", "usage", ...}}`, on a
207
+ lapsed login), not the stream of a working run, and the final answer would come from it. Until
208
+ then forge keeps agy's printed output, which is both its log and its answer.
209
+
198
210
  **`--print` must use the attached `=` form.** It is a Go flag with an optional value, so
199
211
  written detached it consumes whatever token follows as its prompt. `agy --print
200
212
  --print-timeout 120s --model X "real prompt"` fails with *"--print took --print-timeout as
@@ -48,7 +48,7 @@ nothing was judged — fix the cause and run again; a dwarf that returns nothing
48
48
  nothing is reported as 8, not 5). The default dispatch timeout is 2700 seconds, scaled up for
49
49
  `xhigh`/`max` effort (so the default QA/planner effort gets 3600), overridable per model in the
50
50
  registry; 0 disables that dispatch timeout, but an enabled Fractal task still has its own attempt
51
- deadline. See [dispatch timeouts](../README.md#scriptsforge-dispatchsh).
51
+ deadline. See [dispatch timeouts](../docs/commands.md#scriptsforge-dispatchsh).
52
52
 
53
53
  The dwarf prompt carries implementer rules (no commits, no background processes, finish every
54
54
  measurement before the last message). forge records what it observes: if the dwarf committed
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;