@giamme/forge 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,353 @@
1
+ # Harness reference
2
+
3
+ Per-CLI invocation details behind `scripts/forge-dispatch.sh`. Read this when a dispatch
4
+ fails, when adding a backend, or when you need to know what a role can actually do.
5
+
6
+ Everything below was verified by running the CLIs on this machine, not read from docs —
7
+ several of these behaviours are undocumented and a few contradict what the `--help` text
8
+ implies.
9
+
10
+ Fractal uses this same dispatcher for all six harnesses through its `agents.py` bridge.
11
+ It is an optional execution backend, not a seventh model harness or a replacement for
12
+ `alias:effort:harness`. The same effort and permission rules apply to each fresh nested
13
+ work step. Use `./forge fractal doctor --spec SPEC` for runtime and advertised capability
14
+ checks; authentication and live provider availability still require an explicitly requested
15
+ provider test. See [Fractal execution](fractal.md).
16
+
17
+ ## Contents
18
+
19
+ - [At a glance](#at-a-glance)
20
+ - [codex](#codex)
21
+ - [claude / openclaude](#claude--openclaude)
22
+ - [opencode](#opencode)
23
+ - [antigravity (agy)](#antigravity-agy)
24
+ - [grok](#grok)
25
+ - [Adding a harness](#adding-a-harness)
26
+
27
+ ## At a glance
28
+
29
+ | Harness | Non-interactive form | Model flag | Effort flag | Full bypass |
30
+ |---------|----------------------|------------|-------------|-------------|
31
+ | `codex` | `codex exec` | `-m` | `-c model_reasoning_effort=<e>` | `--dangerously-bypass-approvals-and-sandbox` |
32
+ | `claude` | `claude -p` | `--model` | `--effort` | `--dangerously-skip-permissions` |
33
+ | `openclaude` | `openclaude -p` | `--model` | `--effort` | `--dangerously-skip-permissions` |
34
+ | `opencode` | `opencode run` | `-m provider/model` | `--variant` | `--auto` |
35
+ | `antigravity` | `agy --print=...` | `--model` | `--effort` | `--dangerously-skip-permissions` |
36
+ | `grok` | `grok --prompt-file` | `--model` | `--reasoning-effort` | `--always-approve` |
37
+
38
+ **Only antigravity bounds its own runtime.** The other five will sit on a hung request
39
+ indefinitely, and stock macOS ships no `timeout` binary to wrap them in — so a hung dwarf
40
+ used to stall its whole wave under `forge-parallel.sh`'s `xargs -P` with no output and no
41
+ way to distinguish stuck from slow. `forge-dispatch.sh` therefore runs every backend in the
42
+ background and kills it after its timeout: `--timeout`, else `FORGE_TIMEOUT` (an explicit `0`
43
+ disables), else the optional `timeout` column of the model's registry row, else 2700 s — scaled
44
+ ×4/3 for `xhigh` and ×2 for `max`/`ultra` effort when not explicit (`<role>.resolved` records
45
+ `timeout=` and `timeout_source=`). A killed dispatch exits `7`.
46
+
47
+ **Infrastructure failures exit `8`.** After the backend exits, `forge-runtime.py classify` reads
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 —
51
+ looking for strong phrases (usage/session limit, quota, insufficient balance/credit, rate limit,
52
+ 429/529, unauthorized/401/403, invalid API key, not logged in, ECONNRESET/ENOTFOUND/…). An
53
+ `empty` answer (exit 0, nothing said) is the fifth class. codex's non-fatal reconnect notices
54
+ followed by a normal reply never classify. `<role>.infra` holds `class=`, `retry_after=` (parsed
55
+ from "resets 3pm", "try again in 2h", `|<epoch>`, capped at 24 h), `detail=`, `rc=`, `role=`.
56
+ A dwarf that exits 0 with nothing said writes the file but still exits 0; callers consult it
57
+ only when the diff is empty.
58
+
59
+ Implementation notes worth keeping, because they were arrived at the hard way: the backend is
60
+ started with `exec` inside its subshell, so the pid forge holds is the backend itself rather than
61
+ a wrapper it would be useless to kill. macOS has no `setsid` binary, so the harness is launched
62
+ through `forge-guard.py session` (python's `os.setsid()` then `exec`, same pid, pgid == pid)
63
+ unless `FORGE_ORPHAN_GUARD=off` or the run is Fractal. That gives a process group to signal as a
64
+ unit: the watchdog (TERM, then KILL for a CLI that traps TERM) and the exit trap signal the whole
65
+ group in addition to `pkill -P`, and after a normal exit `forge-guard.py reap` lists and stops
66
+ anything still alive in it (`<role>.orphans`). This is proven offline only; verify a new harness
67
+ with one live dispatch before relying on it, and use the kill switch if a harness deliberately
68
+ leaves a helper running.
69
+
70
+ Effort ladders, low to high:
71
+
72
+ - codex — `low medium high xhigh max ultra`
73
+ - claude — `low medium high xhigh max`
74
+ - openclaude — `low medium high xhigh max ultracode`
75
+ - antigravity — `low medium high`, and **per-model**: see below
76
+ - opencode — no fixed ladder; `--variant` is provider-specific and is forwarded verbatim
77
+ - grok — `low medium high xhigh max`; each model offers a subset (grok-4.7: up to `xhigh`)
78
+
79
+ ## codex
80
+
81
+ ```
82
+ codex exec -m <model> -C <repo> --skip-git-repo-check -o <last-message-file> \
83
+ -c model_reasoning_effort=<effort> (--approve-for-me | --dangerously-bypass-...) "<prompt>"
84
+ ```
85
+
86
+ **It does not validate the effort.** `-c model_reasoning_effort=bogus` starts a run and
87
+ prints `reasoning effort: bogus` in its own header without erroring. A typo therefore costs
88
+ a full run at some unintended level, with nothing in the output saying so. This is the main
89
+ reason forge validates efforts itself before dispatch.
90
+
91
+ **Prompt delivery uses stdin.** Forge copies the input once (including FIFOs), checks it
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`.
95
+ See the official [Codex non-interactive guide](https://learn.chatgpt.com/docs/non-interactive-mode)
96
+ and [Claude headless guide](https://code.claude.com/docs/en/headless).
97
+
98
+ `--approve-for-me` and `--dangerously-bypass-approvals-and-sandbox` are mutually exclusive,
99
+ and `--approve-for-me` additionally conflicts with an explicit `-s/--sandbox`.
100
+
101
+ **Native review** (`codex exec review`) has two constraints worth knowing:
102
+
103
+ - Its scope flags (`--uncommitted`, `--base`, `--commit`) are **mutually exclusive with a
104
+ custom PROMPT** — `error: the argument '--uncommitted' cannot be used with '[PROMPT]'`.
105
+ So native review cannot be told what the dwarf was asked to do. It judges whether the
106
+ code is correct, never whether it is the right change.
107
+ - It has **no `-C/--cd`**, unlike `codex exec`. It must be launched from inside the repo.
108
+
109
+ ## claude / openclaude
110
+
111
+ ```
112
+ <harness> -p --model <model> --add-dir <run-dir> --effort <effort> \
113
+ (--permission-mode acceptEdits --allowedTools Bash | --dangerously-skip-permissions)
114
+ [--disallowed-tools "Edit,Write,NotebookEdit"]
115
+ ```
116
+ with the prompt delivered on **stdin**.
117
+
118
+ **The prompt must not go on argv when a variadic flag precedes it.** `--disallowed-tools`,
119
+ `--allowed-tools` and friends are variadic, so a following positional is consumed as
120
+ another value. In practice the prompt is parsed word by word into tool names — you get a
121
+ pile of `Permission deny rule "the" matches no known tool` warnings and then
122
+ `Error: Input must be provided either through stdin or as a prompt argument`. Passing it on
123
+ stdin sidesteps this entirely and also lifts the argv length limit, which matters once a
124
+ diff is inlined into a review prompt.
125
+
126
+ **Permission modes are not interchangeable, and no single mode is enough.** Re-measured on
127
+ this machine with a prompt that asks the agent to edit a file *and* run a real command:
128
+
129
+ | Mode | Can edit files | Can run shell |
130
+ |------|----------------|---------------|
131
+ | `acceptEdits` | yes | trivial commands only — prompts for the rest |
132
+ | `dontAsk` | — | **no** |
133
+ | `auto` | **no** — prompts for Edit | yes |
134
+ | `bypassPermissions` | yes | yes |
135
+
136
+ Both single-mode choices fail, and both fail *silently*:
137
+
138
+ - under `auto`, a dwarf writes nothing and ends with "I need your permission to edit f.py".
139
+ The harness still exits 0, so forge sees a successful run that produced an empty diff;
140
+ - under `acceptEdits` alone, a dwarf writes the code and then stalls on "awaiting approval
141
+ to run pytest", shipping unverified work while reporting success.
142
+
143
+ Beware that `acceptEdits` auto-approves *some* shell — `echo` goes through — so a smoke test
144
+ using a trivial command will wrongly suggest shell access works.
145
+
146
+ Forge therefore uses **`--permission-mode acceptEdits` plus `--allowedTools "Bash"`** for
147
+ non-yolo roles, which gives both. QA additionally gets
148
+ `--disallowed-tools "Edit,Write,NotebookEdit"`, which still overrides the mode's edit
149
+ permission for those tools. Bash can still write files. The solo and parallel runners use
150
+ disposable review repositories and fingerprint checks to invalidate changed artifacts.
151
+
152
+ `--add-dir <run-dir>` is required because the run directory deliberately sits outside the
153
+ repo; without it the agent refuses to open the artifacts forge just wrote for it
154
+ ("outside the allowed working directory").
155
+
156
+ openclaude is a Claude Code fork and takes the same flags, plus `--provider` and an extra
157
+ `ultracode` effort tier. It routes to whatever provider it is authenticated against —
158
+ check with `openclaude auth status`, which on this machine reports
159
+ `{"authMethod": "third_party", "apiProvider": "codex"}`, meaning GPT models run through a
160
+ Claude-Code-shaped harness. That is the point of `sol:xhigh:openclaude`.
161
+
162
+ ## opencode
163
+
164
+ ```
165
+ opencode run -m <provider>/<model> --dir <repo> --variant <effort> --auto [--agent plan] "<prompt>"
166
+ ```
167
+
168
+ Models are always `provider/model`; `opencode models` lists what is configured.
169
+
170
+ `opencode run` exposes **no sandbox tier** — there is only `--auto`. Without it the run
171
+ blocks on a permission prompt that nothing can answer headlessly, so forge always passes
172
+ it. This means opencode's non-yolo mode is weaker than the other three harnesses': the
173
+ containment for a QA role comes from `--agent plan`, its built-in read-only agent, rather
174
+ than from a permission boundary.
175
+
176
+ **Credentials are the usual failure.** Being installed proves nothing. On this machine all
177
+ three configured routes currently fail, each differently:
178
+
179
+ - `github-copilot/*` → `Unauthorized: unauthorized: AuthenticateToken authentication failed`
180
+ - `opencode-go/*` → `Insufficient balance.`
181
+ - `opencode/*-free` → `UnknownError ... Unexpected server error`
182
+
183
+ `forge-dispatch.sh doctor` reports binaries on PATH; it cannot detect any of these, which
184
+ surface only mid-dispatch as an exit 4. When an opencode dispatch fails, show the user the
185
+ actual error — it is almost always auth or billing, and swapping to another backend would
186
+ hide a problem they need to fix.
187
+
188
+ ## antigravity (agy)
189
+
190
+ ```
191
+ agy --model <model> --print-timeout <t> [--effort <e>] --add-dir <repo> --add-dir <run-dir> \
192
+ (--mode accept-edits | --mode plan | --dangerously-skip-permissions) --print='<prompt>'
193
+ ```
194
+
195
+ The harness is named `antigravity` in forge; the binary is `agy`. This is how Gemini is
196
+ reached.
197
+
198
+ **`--print` must use the attached `=` form.** It is a Go flag with an optional value, so
199
+ written detached it consumes whatever token follows as its prompt. `agy --print
200
+ --print-timeout 120s --model X "real prompt"` fails with *"--print took --print-timeout as
201
+ its prompt, so the intended prompt was left as an argument and ignored"*. Forge puts
202
+ `--print=...` last so nothing can be captured after it.
203
+
204
+ **`--add-dir <repo>` is mandatory.** Antigravity is workspace-scoped, not cwd-scoped.
205
+ Without it the agent searches the wrong tree and the run dies on *"Find command timed out.
206
+ Use a more targeted search directory or pattern.: context deadline exceeded"* — which reads
207
+ like a performance problem and is actually a missing workspace.
208
+
209
+ **It cannot run shell commands without yolo.** Measured: `--mode accept-edits` and
210
+ `--sandbox` both deny every command, and the denial is *fatal* — the run aborts with
211
+ `permission check failed for command ... user denied permission`, leaving an empty diff
212
+ after consuming the quota. Only `--dangerously-skip-permissions` allows execution. Because
213
+ the standard dwarf prompt asks the model to run the project's tests, forge appends an
214
+ explicit "do not attempt commands" note to the prompt whenever antigravity runs without
215
+ yolo. This is the one harness where the non-yolo dwarf genuinely cannot verify its own work.
216
+
217
+ **Effort is validated, and varies per model** — unlike codex, which accepts anything:
218
+
219
+ | Model | Accepts |
220
+ |-------|---------|
221
+ | `gemini-3.7-flash`, `-3.6-flash`, `-3.5-flash` | `low`, `medium`, `high` |
222
+ | `gemini-3.1-pro` | `low`, `high` — **no medium** |
223
+ | `gpt-oss-120b` | `medium` only |
224
+ | `claude-sonnet-4-6`, `claude-opus-4-6-thinking` | none; passing `--effort` errors |
225
+
226
+ Two traps here. `agy models` lists effort-suffixed ids (`gemini-3.7-flash-low`), but a
227
+ suffixed id **conflicts** with `--effort`: *"--model gemini-3.7-flash-low conflicts with
228
+ --effort=high"*. Forge therefore registers the unsuffixed ids and sets effort via the flag,
229
+ which is what makes `gemini:high` mean the same thing here as on every other harness. And
230
+ `gemini-3.1-pro` has a hole in the middle rather than a low ceiling, which is why the
231
+ registry's effort column supports explicit sets and not just a `<=ceiling`.
232
+
233
+ QA runs under `--mode plan`, antigravity's read-only mode.
234
+
235
+ **Installing forge *into* antigravity works differently from the others.** agy ignores a
236
+ loose skill directory under `~/.gemini/skills` — it cannot see skills placed there at all,
237
+ including ones that were already sitting in that folder. Skills are only loaded as part of
238
+ a plugin: a directory containing `plugin.json` **at its root** (not in `.claude-plugin/`,
239
+ which is why `agy plugin validate` rejects a Claude plugin directly) plus a `skills/`
240
+ subdirectory. `forge-install.sh` generates that wrapper at `~/.gemini/forge-plugin` and runs
241
+ `agy plugin install`.
242
+
243
+ The catch: `agy plugin install` **copies** the files into `~/.gemini/config/plugins/forge/`
244
+ rather than referencing them. Every other harness tracks this directory live through a
245
+ symlink or a shim, so editing `SKILL.md` updates them all at once — antigravity is the
246
+ exception and keeps running whatever it copied. Re-run `forge-install.sh` after changing
247
+ the skill, or agy will silently run a stale version.
248
+
249
+ `--print-timeout` defaults to 5m in agy, which is short for real implementation work; forge
250
+ passes 30m and exposes `--agy-timeout` to change it.
251
+
252
+ ## grok
253
+
254
+ ```
255
+ grok --model <model> --prompt-file <prompt> --output-format streaming-messages-json \
256
+ --reasoning-effort <effort> (--permission-mode acceptEdits --allow Bash [--deny Edit --deny Write] | --always-approve)
257
+ ```
258
+
259
+ The xAI Grok Build CLI, signed in with `grok login` (or `XAI_API_KEY`); `grok models` lists
260
+ what the account can use and each model's efforts live in `~/.grok/models_cache.json`. The
261
+ `grok` alias runs `grok-4.7` here and keeps its opencode route (`grok:high:opencode`, 4.6 via
262
+ GitHub Copilot) as a second row.
263
+
264
+ - **A headless run cannot ask, so it refuses and exits 0.** In the default mode an edit that
265
+ needs approval is skipped with `permission required: Edit calc.py was not run` on stderr:
266
+ the same silent empty diff claude gives. The dwarf therefore runs in `acceptEdits` with
267
+ `--allow Bash` (shell for tests). A reviewer gets the same plus `--deny Edit --deny Write`;
268
+ deny beats the mode and every allow, and Bash can still write files, as with claude.
269
+ - **Output is the claude stream.** `streaming-messages-json` ends in a `result` event with
270
+ `result`, `is_error` and the three disjoint token buckets, so `forge-runtime.py` reads the
271
+ final message, the tokens and an `is_error` failure exactly as it does for claude. An error
272
+ subtype can leave `result` empty and put the reason in `errors[]`, which is read instead.
273
+ - **A tool call that still needs approval cancels the whole session.** `--allow Bash` does not
274
+ cover every command. Seen in a real run: a `mkdir -p /tmp/… && cp …` into a directory outside
275
+ the worktree, and a multi-line `cd …/ui && \` command. Each printed `permission required: …
276
+ was not run`, and the session then ended with an `error_during_execution` result (`stop_reason`
277
+ `cancelled`), still exiting 0.
278
+ - **What forge records:** class `cancelled` in `<role>.infra`, with the refused command, and
279
+ a note in the run output.
280
+ - **It is not an infrastructure stop:** the dwarf's diff so far still goes to review, and a
281
+ reviewer cut off before its verdict is UNKNOWN, which re-runs only the review.
282
+ - **Untested:** whether `--permission-mode dontAsk` (deny without cancelling) together with
283
+ explicit Edit/Write/Bash allows would avoid the cancel. Nothing changes until that is
284
+ proven live.
285
+ - The prompt goes by `--prompt-file`, never argv, and stdin is `/dev/null`.
286
+
287
+ Verified live on 2026-10-19 with a `FORGE_OK` dispatch: exit 0, `permissionMode` acceptEdits,
288
+ the answer extracted from the `result` line, tokens recorded, nothing left running. The same
289
+ day, grok-4.7 dwarfs implemented and passed review on real tasks (the forge-ui integration run).
290
+ A grok reviewer has not been proven live yet.
291
+
292
+ ## Installing forge into a harness
293
+
294
+ Each harness discovers skills differently, and the differences are not guessable — verify
295
+ with the listed check rather than trusting file placement.
296
+
297
+ | Harness | Mechanism | Verify with |
298
+ |---------|-----------|-------------|
299
+ | claude | `~/.claude/skills/forge/` (the source) | — |
300
+ | openclaude | symlink in `~/.openclaude/skills/` | `openclaude skills list --json` |
301
+ | codex | symlink in **`~/.codex/skills/`** | `codex exec "do you have a skill named forge?"` |
302
+ | opencode | **nothing** — it scans `~/.claude/` itself | `opencode debug skill` |
303
+ | antigravity | `agy plugin install` of a wrapper | `agy --print='/forge'` |
304
+ | grok | **nothing** — it lists `~/.claude/skills` itself | `skills` in a headless `init` line |
305
+
306
+ Three traps here:
307
+
308
+ - **Codex reads `~/.codex/skills`, not `~/.codex/prompts`.** A file in `prompts/` gives you
309
+ a slash command that expands as text; it does not register a skill.
310
+ - **opencode needs no install.** It scans `~/.claude/` and `~/.agents/` for skills unless
311
+ `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1` is set, so it finds this directory on its own.
312
+ Adding a `~/.config/opencode/command/forge.md` on top produces a duplicate entry.
313
+ - **Antigravity ignores `~/.gemini/skills` entirely** — it cannot see skills placed there,
314
+ including any already in that folder. Skills load only inside a plugin: a directory with
315
+ `plugin.json` **at its root** (not in `.claude-plugin/`, which is why `agy plugin validate`
316
+ rejects a Claude plugin as-is) plus a `skills/` subdirectory. `forge-install.sh` generates
317
+ that wrapper at `~/.gemini/forge-plugin`.
318
+
319
+ And one ongoing caveat: `agy plugin install` **copies** into `~/.gemini/config/plugins/forge/`
320
+ rather than referencing the source. The other four track this directory live, so an edit to
321
+ `SKILL.md` reaches them immediately — antigravity keeps running whatever it copied. Re-run
322
+ `forge-install.sh` after any change, or agy silently runs a stale version.
323
+
324
+ ### Frontmatter must be strict YAML
325
+
326
+ Codex parses skill frontmatter strictly and **refuses to load the skill** on any error,
327
+ logging `failed to load skill ...: invalid YAML` and then behaving as if it does not exist.
328
+ Claude Code and OpenClaude are lenient and load the same file fine, so a frontmatter bug can
329
+ sit unnoticed in four harnesses and break the fifth.
330
+
331
+ The one that bit this skill: a value that *starts* with a quoted scalar and then continues
332
+ unquoted is invalid YAML —
333
+
334
+ ```yaml
335
+ argument-hint: "<goal>" --dwarf <alias> # invalid: text after the closing quote
336
+ argument-hint: '"<goal>" --dwarf <alias>' # valid: whole value quoted
337
+ ```
338
+
339
+ Note codex reports line numbers relative to the frontmatter body, i.e. excluding the opening
340
+ `---`, so its "line 3" is the file's line 4.
341
+
342
+ ## Adding a harness
343
+
344
+ 1. Add rows to `registry.tsv` mapping aliases to that harness's model ids.
345
+ 2. Add its effort ladder to `ladder_for()` in `scripts/forge-dispatch.sh`, or leave it
346
+ empty for verbatim forwarding.
347
+ 3. Add a `case` branch building its argv, covering: non-interactive flag, model, effort,
348
+ working directory, the non-yolo containment for each role, the yolo bypass, and how the
349
+ prompt is delivered.
350
+ 4. Add it to `known_harness()` and to `doctor()`.
351
+ 5. Prove it end to end before trusting it — a dwarf that really edits a scratch repo and a
352
+ QA that really finds a planted bug. Every trap documented above was found this way and
353
+ none of them were visible from `--help`.