@codedology/code-forge 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +12 -2
- package/docs/reference/blocks.json +153 -1
- package/docs/reference/config.md +2 -0
- package/package.json +1 -1
- package/schema/code-forge.schema.json +8 -0
- package/skill/references/adapters/claude-code.md +1 -1
- package/skill/references/adapters/solo.md +1 -1
- package/skill/references/code.md +3 -3
- package/skill/references/ledger.md +4 -2
- package/skill/references/proof.md +1 -1
- package/skill/references/review.md +13 -6
- package/skill/references/security.md +5 -3
- package/skill/scripts/forge +1 -1
- package/src/cli/block.mjs +103 -31
- package/src/cli/report.mjs +2 -2
- package/src/cli/review-file.mjs +47 -35
- package/src/ledger/export.mjs +1 -1
- package/src/ledger/report.mjs +9 -3
- package/src/review/accept-hit.mjs +78 -0
- package/src/review/agent-waiver.mjs +201 -0
- package/src/review/gate-check.mjs +33 -24
- package/src/review/reopen.mjs +35 -14
- package/src/state/config-snapshot.mjs +1 -0
- package/src/util/forbidden.mjs +22 -0
- package/src/worker/queue.mjs +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.14.0] — 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `review-file --wait-block`: when the current content's ticket is done with a result that never counted (`stale`, `unavailable`, round-1 `split_required`), the join drops that done marker with the `--fresh` claim and the worker reviews the content once; one drop per ticket at most, kept on disk; a counted result is never dropped (B125)
|
|
14
|
+
- `block waive --yes`: a warning or nit finding can be waived without a terminal when the reason cites evidence (path:line, a test file, or an acceptance clause) and a fresh L3 judge agrees; one signed `review.waived {by: agent}` row with the verdict; refusals `waive-yes-critical`, `waive-yes-no-evidence`, `waive-yes-disagreed`; `block close` lists agent waivers and `--json` adds `agent_waivers`; `block reopen` stays human-only (B124a)
|
|
15
|
+
- `block accept-hit <id> --run --rule --line --transcript --reason`: human-only; refuses `accept-hit-no-such-hit` before any prompt; writes a signed `rule_break.accepted {block, rule, line, line_sha256, reason, by: human}` row; `block close` drops a transcript hit only on the same rule and the sha256 of that line, prints each accepted hit, and `--json` adds `accepted_hits`; the verb is on the coder forbidden list (B127)
|
|
16
|
+
- `block unwaive <id> <finding> --run --file --reason`: human-only; refuses `unwaive-no-agent-waiver` before any prompt; a signed `review.unwaived` row cancels the earlier agent waivers of that finding at the gate and the review-file pre-check; `code-forge report` gets a 15th section `agent_waivers`; new key `review.agent_waivers.require_ack` (frozen at run start): `block close` refuses `agent-waivers-not-acked` until a human `block ack-waivers <id> --run` row matches the current agent waivers (`ack-waivers-none` when there are none); both verbs are on the coder forbidden list (B124b)
|
|
17
|
+
- `block reopen <id> --file --reason --yes`: agent reopen; refuses `reopen-yes-no-evidence` without cited evidence, `reopen-yes-disagreed` when a fresh judge disagrees, and `reopen-yes-limit` after one agent reopen per block and file in a run; the row is `by: agent`; an agreed, verified agent reopen supersedes the review cap like a human reopen; `block close --json` lists `agent_reopens` (B124c)
|
|
18
|
+
- Docs: the README command table lists `--yes` on `block waive` and `block reopen` and adds `block accept-hit`, `block unwaive` and `block ack-waivers`; `docs/getting-started.md` and `skill/references/review.md` no longer say reopen has no `--yes`
|
|
19
|
+
|
|
20
|
+
|
|
9
21
|
## [0.13.0] — 2026-10-14
|
|
10
22
|
|
|
11
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -8,10 +8,20 @@ for cross-model, parallel, evidence-gated feature delivery — on Claude Code, C
|
|
|
8
8
|
> `examples/` have all landed (`docs/reference/blocks.json` lists every build block). What changed
|
|
9
9
|
> in each version is in [CHANGELOG.md](CHANGELOG.md).
|
|
10
10
|
|
|
11
|
-
## What's new in 0.
|
|
11
|
+
## What's new in 0.14
|
|
12
12
|
|
|
13
13
|
In the next release:
|
|
14
14
|
|
|
15
|
+
In 0.14.0:
|
|
16
|
+
|
|
17
|
+
- **A stale review no longer counts as done (B125).** When a file changed during its review and came back to the same content, `review-file --wait-block` now reviews it once more by itself. No `--fresh` by hand.
|
|
18
|
+
- **Agent waivers with proof (B124a).** `block waive --yes` now works for a warning or nit finding. The reason must name evidence: a `path:line`, a test file, or an acceptance clause. A fresh judge must agree. The row is marked `by: agent`, and `block close` lists it. A critical finding and `block reopen` stay human-only.
|
|
19
|
+
- **Accept a false rule hit (B127).** `block accept-hit <id> --run <r> --rule <rule-id> --line <n> --transcript <path> --reason <text>` lets a human accept one false transcript hit, after a yes in their own terminal. The close then skips that hit only when the rule and the exact line text match. The close lists each accepted hit, and `--json` adds `accepted_hits`. A coder can never run this verb.
|
|
20
|
+
- **Undo and check agent waivers (B124b).** `block unwaive <id> <finding> --run <r> --file <path> --reason <text>` lets a human undo one agent waiver. The finding is then open again. `code-forge report` has a new `agent_waivers` section. Set `review.agent_waivers.require_ack: true`, and `block close` waits until a human runs `block ack-waivers <id> --run <r>` and says yes to the list. Both verbs are human-only.
|
|
21
|
+
- **Agent reopen with evidence (B124c).** `block reopen <id> --file <path> --reason <text> --yes` lets the orchestrator reopen a stopped file without you. The reason must cite evidence, a fresh judge must agree, and only one agent reopen per block and file is allowed in a run. The row says `by: agent`. `block close --json` lists them in `agent_reopens`. Without `--yes`, a human still confirms.
|
|
22
|
+
|
|
23
|
+
## What's new in 0.13
|
|
24
|
+
|
|
15
25
|
In 0.13.0:
|
|
16
26
|
|
|
17
27
|
- **Every block gets the proof rule, with or without a `context` key (B118).** `code-forge context pack` no longer refuses a config with no `context` key. The pack then holds the block header and the proof rule.
|
|
@@ -355,7 +365,7 @@ usage line.
|
|
|
355
365
|
| `validate` | `code-forge validate [--file <path>]` |
|
|
356
366
|
| `resolve` | `code-forge resolve <L0\|L1\|L2\|L3>` |
|
|
357
367
|
| `run` | `code-forge run start [--cwd <dir>] [--run <id>] [--engine <e>] [--worker-pid <pid>]` · `run start --reattach --run <id> [--worker-pid <pid>]` · `run status --run <id>` (with spent and budget USD) · `run reload --run <id>` (re-read `.code-forge.yml` mid-run) · `run end --run <id>` |
|
|
358
|
-
| `block` | `code-forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> [--brief <file>] [--attempt <n>] [--base <sha>] [--lines <n>] [--kind code\|docs\|contract]` · `block attempt\|rebase <id> --run <r>` · `block close <id> --run <r> [--transcript <file>] [--report <file>] [--no-require-reviews]` · `block claim <id> <path> --run <r>` · `block stop <id> --run <r> --reason <text>` · `block waive <id> <finding-id> --run <r> --file <path> --reason <text
|
|
368
|
+
| `block` | `code-forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> [--brief <file>] [--attempt <n>] [--base <sha>] [--lines <n>] [--kind code\|docs\|contract]` · `block attempt\|rebase <id> --run <r>` · `block close <id> --run <r> [--transcript <file>] [--report <file>] [--no-require-reviews]` · `block claim <id> <path> --run <r>` · `block stop <id> --run <r> --reason <text>` · `block waive <id> <finding-id> --run <r> --file <path> --reason <text> [--yes]` · `block reopen <id> --run <r> --file <path> --reason <text> [--yes]` (without `--yes` you run them in your own terminal and they ask for a yes; with `--yes` the reason must cite evidence, a fresh judge must agree, and the row is `by: agent`) · `block accept-hit <id> --run <r> --rule <rule-id> --line <n> --transcript <path> --reason <text>` · `block unwaive <id> <finding-id> --run <r> --file <path> --reason <text>` · `block ack-waivers <id> --run <r>` (these three are human-only) |
|
|
359
369
|
| `worker` | `code-forge worker --run <id> [--cwd <dir>] [--poll-ms <n>] [--once]` — started detached by `run start`, never by a coder |
|
|
360
370
|
| `review` | `code-forge review [--base <ref>] [--files <path…>] [--acceptance <file> \| --intent "<text>"] [--run <id>] [--max <seconds>] [--json] [--keep-run]` — review only: no coder, no proof, never closes a block |
|
|
361
371
|
| `review-file` | `code-forge review-file <path> --block <id> [--run <id>]` (enqueue) · `review-file --wait <ticket> [--max <seconds>s]` (poll, default 90s; still running at `--max` prints `status: pending, reason: wait_timeout`) |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"plans/code-forge-plan-v1.3.md §10.4",
|
|
5
5
|
"plans/NIGHT-LOG.md"
|
|
6
6
|
],
|
|
7
|
-
"block_count":
|
|
7
|
+
"block_count": 133,
|
|
8
8
|
"baseline_ids": ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
|
|
9
9
|
"baseline_note": "Wave 0-1 blocks, landed before v1.3; depends_on may name them, but they have no entry under blocks.",
|
|
10
10
|
"blocks": {
|
|
@@ -2183,6 +2183,158 @@
|
|
|
2183
2183
|
"B113"
|
|
2184
2184
|
],
|
|
2185
2185
|
"note": "The final-check test in orphan.test.mjs forged an empty baseline, so files already in the repo's .code-forge folder (normal when code-forge builds itself) showed up as leaks and failed npm test. The test now records the folder's current contents as the baseline and still checks the exact planted leak list."
|
|
2186
|
+
},
|
|
2187
|
+
"B125": {
|
|
2188
|
+
"title": "The join reruns a stale review of the current content once",
|
|
2189
|
+
"wave": 14,
|
|
2190
|
+
"level": "L2",
|
|
2191
|
+
"status": "landed",
|
|
2192
|
+
"owned": [
|
|
2193
|
+
"docs/how-it-works.md",
|
|
2194
|
+
"skill/references/review.md",
|
|
2195
|
+
"src/cli/review-file.mjs",
|
|
2196
|
+
"src/worker/queue.mjs",
|
|
2197
|
+
"test/worker/wait-block.test.mjs"
|
|
2198
|
+
],
|
|
2199
|
+
"depends_on": [
|
|
2200
|
+
"B122"
|
|
2201
|
+
],
|
|
2202
|
+
"note": "review-file --wait-block: when the ticket of a file's current content is done with a result that never counted (stale, unavailable, round-1 split_required), the join drops that done marker with the --fresh claim and the worker reviews the content once; at most one drop per ticket, kept on disk. Counted results are never dropped."
|
|
2203
|
+
},
|
|
2204
|
+
"B124a": {
|
|
2205
|
+
"title": "block waive --yes: an evidence-backed agent waiver a fresh judge must agree with",
|
|
2206
|
+
"wave": 14,
|
|
2207
|
+
"level": "L2",
|
|
2208
|
+
"status": "landed",
|
|
2209
|
+
"owned": [
|
|
2210
|
+
"docs/concepts.md",
|
|
2211
|
+
"docs/how-it-works.md",
|
|
2212
|
+
"skill/references/adapters/claude-code.md",
|
|
2213
|
+
"skill/references/adapters/solo.md",
|
|
2214
|
+
"skill/references/code.md",
|
|
2215
|
+
"skill/references/ledger.md",
|
|
2216
|
+
"skill/references/proof.md",
|
|
2217
|
+
"skill/references/review.md",
|
|
2218
|
+
"skill/references/security.md",
|
|
2219
|
+
"src/cli/block.mjs",
|
|
2220
|
+
"src/review/agent-waiver.mjs",
|
|
2221
|
+
"src/review/gate-check.mjs",
|
|
2222
|
+
"test/cli/block-human-confirm.test.mjs",
|
|
2223
|
+
"test/cli/block-waive-yes.test.mjs",
|
|
2224
|
+
"test/skill/structure.test.mjs"
|
|
2225
|
+
],
|
|
2226
|
+
"depends_on": [
|
|
2227
|
+
"B80",
|
|
2228
|
+
"B62a",
|
|
2229
|
+
"B90",
|
|
2230
|
+
"B122",
|
|
2231
|
+
"B93",
|
|
2232
|
+
"B47"
|
|
2233
|
+
],
|
|
2234
|
+
"note": "block waive --yes skips the terminal confirm only for a finding whose signed recorded severity is warning or nit; the reason must cite evidence (path:line, a test file, or an acceptance clause); a fresh L3 judge must agree; then one signed review.waived row by: agent with the verdict. Critical or unknown severity is refused waive-yes-critical; no evidence waive-yes-no-evidence (judge not called); disagree waive-yes-disagreed. The gate counts an agreed agent waiver; block close lists agent waivers and --json adds agent_waivers. block reopen stays human-only with no --yes."
|
|
2235
|
+
},
|
|
2236
|
+
"B127": {
|
|
2237
|
+
"title": "block accept-hit: the human accepts one named false transcript hit",
|
|
2238
|
+
"wave": 14,
|
|
2239
|
+
"level": "L2",
|
|
2240
|
+
"status": "landed",
|
|
2241
|
+
"owned": [
|
|
2242
|
+
"docs/how-it-works.md",
|
|
2243
|
+
"skill/references/review.md",
|
|
2244
|
+
"skill/references/security.md",
|
|
2245
|
+
"src/cli/block.mjs",
|
|
2246
|
+
"src/review/accept-hit.mjs",
|
|
2247
|
+
"src/review/gate-check.mjs",
|
|
2248
|
+
"src/util/forbidden.mjs",
|
|
2249
|
+
"test/cli/block-accept-hit.test.mjs",
|
|
2250
|
+
"test/engines/builders/argv-snapshots.test.mjs",
|
|
2251
|
+
"test/engines/builders/forbidden-rendering.test.mjs",
|
|
2252
|
+
"test/engines/codex-home.test.mjs",
|
|
2253
|
+
"test/util/forbidden.test.mjs"
|
|
2254
|
+
],
|
|
2255
|
+
"depends_on": [
|
|
2256
|
+
"B124a",
|
|
2257
|
+
"B80",
|
|
2258
|
+
"B5"
|
|
2259
|
+
],
|
|
2260
|
+
"note": "block accept-hit <id> --run --rule --line --transcript --reason: human-only (terminal yes, no --yes); pre-checks refuse accept-hit-no-such-hit before any prompt when the transcript scan does not report that (rule, line); on yes one signed rule_break.accepted row {block, rule, line, line_sha256, reason, by: human}. block close drops a transcript hit only when a verified accepted row of the same run and block has the same rule and the sha256 of that exact line text; a rule_break ledger row is never accepted. close prints each accepted hit; --json adds accepted_hits. The verb is coder-only forbidden."
|
|
2261
|
+
},
|
|
2262
|
+
"B124b": {
|
|
2263
|
+
"title": "undo and audit agent waivers: block unwaive, report agent_waivers, require_ack + block ack-waivers",
|
|
2264
|
+
"wave": 14,
|
|
2265
|
+
"level": "L2",
|
|
2266
|
+
"status": "landed",
|
|
2267
|
+
"owned": [
|
|
2268
|
+
"docs/concepts.md",
|
|
2269
|
+
"docs/how-it-works.md",
|
|
2270
|
+
"docs/reference/config.md",
|
|
2271
|
+
"docs/tutorial-first-test.md",
|
|
2272
|
+
"schema/code-forge.schema.json",
|
|
2273
|
+
"skill/references/ledger.md",
|
|
2274
|
+
"skill/references/review.md",
|
|
2275
|
+
"skill/references/security.md",
|
|
2276
|
+
"src/cli/block.mjs",
|
|
2277
|
+
"src/cli/report.mjs",
|
|
2278
|
+
"src/ledger/export.mjs",
|
|
2279
|
+
"src/ledger/report.mjs",
|
|
2280
|
+
"src/review/agent-waiver.mjs",
|
|
2281
|
+
"src/review/gate-check.mjs",
|
|
2282
|
+
"src/state/config-snapshot.mjs",
|
|
2283
|
+
"src/util/forbidden.mjs",
|
|
2284
|
+
"test/cli/block-ack-waivers.test.mjs",
|
|
2285
|
+
"test/cli/block-unwaive.test.mjs",
|
|
2286
|
+
"test/config/schema-coverage.test.mjs",
|
|
2287
|
+
"test/engines/builders/argv-snapshots.test.mjs",
|
|
2288
|
+
"test/engines/builders/forbidden-rendering.test.mjs",
|
|
2289
|
+
"test/engines/codex-home.test.mjs",
|
|
2290
|
+
"test/first-use.test.mjs",
|
|
2291
|
+
"test/ledger/cli.test.mjs",
|
|
2292
|
+
"test/ledger/export.test.mjs",
|
|
2293
|
+
"test/ledger/report.test.mjs",
|
|
2294
|
+
"test/skill/structure.test.mjs",
|
|
2295
|
+
"test/state/brief-mode.test.mjs",
|
|
2296
|
+
"test/util/forbidden.test.mjs"
|
|
2297
|
+
],
|
|
2298
|
+
"depends_on": [
|
|
2299
|
+
"B124a",
|
|
2300
|
+
"B127",
|
|
2301
|
+
"B66",
|
|
2302
|
+
"B58"
|
|
2303
|
+
],
|
|
2304
|
+
"note": "block unwaive <id> <finding> --run --file --reason: human-only; refuses unwaive-no-agent-waiver before any prompt unless a verified by: agent waiver exists; writes a signed review.unwaived row that cancels earlier agent waivers of that (block, file, finding) on the gate and the review-file pre-check. report gets a 15th section agent_waivers {block, file, finding, severity, reason, verdict, unwaived}. review.agent_waivers.require_ack (frozen at run start): close refuses agent-waivers-not-acked until a human block ack-waivers row matches the digest of the counted agent waivers; ack-waivers-none with none. Both verbs are coder-only forbidden."
|
|
2305
|
+
},
|
|
2306
|
+
"B124c": {
|
|
2307
|
+
"title": "agent reopen: block reopen --yes with evidence, fresh judge, one per block and file per run; close audit agent_reopens",
|
|
2308
|
+
"wave": 14,
|
|
2309
|
+
"level": "L2",
|
|
2310
|
+
"status": "landed",
|
|
2311
|
+
"owned": [
|
|
2312
|
+
"docs/concepts.md",
|
|
2313
|
+
"docs/how-it-works.md",
|
|
2314
|
+
"skill/references/adapters/claude-code.md",
|
|
2315
|
+
"skill/references/adapters/solo.md",
|
|
2316
|
+
"skill/references/code.md",
|
|
2317
|
+
"skill/references/ledger.md",
|
|
2318
|
+
"skill/references/review.md",
|
|
2319
|
+
"skill/references/security.md",
|
|
2320
|
+
"src/cli/block.mjs",
|
|
2321
|
+
"src/review/agent-waiver.mjs",
|
|
2322
|
+
"src/review/gate-check.mjs",
|
|
2323
|
+
"src/review/reopen.mjs",
|
|
2324
|
+
"test/cli/block-human-confirm.test.mjs",
|
|
2325
|
+
"test/cli/block-reopen-yes.test.mjs",
|
|
2326
|
+
"test/review/gate-check-reopen-cap.test.mjs",
|
|
2327
|
+
"test/review/reopen.test.mjs",
|
|
2328
|
+
"test/skill/structure.test.mjs"
|
|
2329
|
+
],
|
|
2330
|
+
"depends_on": [
|
|
2331
|
+
"B124b",
|
|
2332
|
+
"B124a",
|
|
2333
|
+
"B72",
|
|
2334
|
+
"B80",
|
|
2335
|
+
"B123"
|
|
2336
|
+
],
|
|
2337
|
+
"note": "block reopen <id> --file --reason --yes: the reason must cite evidence (reopen-yes-no-evidence), a fresh judge must agree (reopen-yes-disagreed), at most one agent reopen per block and file per run (reopen-yes-limit); the row is by: agent. An agreed, verified agent reopen supersedes the B123 cap like a human reopen. block close --json lists agent_reopens. Without --yes the verb still needs human confirmation."
|
|
2186
2338
|
}
|
|
2187
2339
|
}
|
|
2188
2340
|
}
|
package/docs/reference/config.md
CHANGED
|
@@ -138,6 +138,8 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
138
138
|
| `review.parallel_tickets` | integer | | `3` | min 1; max 16; B42: how many review tickets the worker runs at once (oldest first); 1 = one at a time. Tickets for the same file never overlap. |
|
|
139
139
|
| `review.fair_share` | boolean | | `false` | Give each free ticket slot to the block with the fewest running tickets; ties stay oldest first. |
|
|
140
140
|
| `review.file_loop` | enum: `wait`, `overlap`, `watch` | | `"wait"` | After each green file, enqueue its review. wait (default): wait for its ticket before the next file. overlap: continue coding, then run review-file --wait-block <id> --run <run> before the completion sentinel; inspect findings with --wait <ticket>. watch: commit each green file instead of running review-file. A file's first commit is queued at once. Later commits of a file that already has a ticket of this block are held (a watch.held row) until --wait-block, which queues the latest content once before the sentinel. Older pending tickets are still superseded. |
|
|
141
|
+
| `review.agent_waivers` | object | | — | |
|
|
142
|
+
| `review.agent_waivers.require_ack` | boolean | | `false` | Require a verified human acknowledgement of the current counted agent waiver set before block close. Read only from the run config snapshot. New or removed waivers need a new acknowledgement. |
|
|
141
143
|
| `review.external` | object | | — | |
|
|
142
144
|
| `review.external.required` | boolean | | `false` | B58: true ⇒ block close skips the local per-file review checks (and the --report ticket check), with no terminal and under autopilot, and writes a signed gate.reviews_waived {by: config} row. Unowned changes, proof, no_changes and the transcript grep still refuse. No block may own .code-forge.yml. |
|
|
143
145
|
| `review.external.name` | string | | — | minLength 1; B58: the outside reviewer, written in the review.external and gate.reviews_waived rows. Required when required is true: run start refuses a missing or blank name. |
|
package/package.json
CHANGED
|
@@ -187,6 +187,14 @@
|
|
|
187
187
|
"parallel_tickets": { "type": "integer", "minimum": 1, "maximum": 16, "default": 3, "description": "B42: how many review tickets the worker runs at once (oldest first); 1 = one at a time. Tickets for the same file never overlap." },
|
|
188
188
|
"fair_share": { "type": "boolean", "default": false, "description": "Give each free ticket slot to the block with the fewest running tickets; ties stay oldest first." },
|
|
189
189
|
"file_loop": { "type": "string", "enum": ["wait", "overlap", "watch"], "default": "wait", "description": "After each green file, enqueue its review. wait (default): wait for its ticket before the next file. overlap: continue coding, then run review-file --wait-block <id> --run <run> before the completion sentinel; inspect findings with --wait <ticket>. watch: commit each green file instead of running review-file. A file's first commit is queued at once. Later commits of a file that already has a ticket of this block are held (a watch.held row) until --wait-block, which queues the latest content once before the sentinel. Older pending tickets are still superseded." },
|
|
190
|
+
"agent_waivers": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"additionalProperties": false,
|
|
193
|
+
"description": "Human acknowledgement of counted agent waivers. Set before run start; run reload refuses to change require_ack.",
|
|
194
|
+
"properties": {
|
|
195
|
+
"require_ack": { "type": "boolean", "default": false, "description": "Require a verified human acknowledgement of the current counted agent waiver set before block close. Read only from the run config snapshot. New or removed waivers need a new acknowledgement." }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
190
198
|
"external": {
|
|
191
199
|
"type": "object",
|
|
192
200
|
"additionalProperties": false,
|
|
@@ -12,7 +12,7 @@ The harness's `Agent` tool runs the coder as a subagent; everything else — the
|
|
|
12
12
|
|
|
13
13
|
The tool return is the completion signal; the report must begin with `ACK <sha8>` and end with `===BLOCK <id> COMPLETE===` (+ test summary + `git diff --stat`) or `===BLOCK <id> FAILED: <reason>===`. A report without the sentinel is a failed block, never a partial success.
|
|
14
14
|
|
|
15
|
-
**Between completion and close come the review rounds.** The subagent ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket inside its turn, so most rounds happen before the sentinel; when the tool returns, first drain every `pending` ticket with `forge review-file --wait <ticket>` yourself — a pending ticket is a review in flight, not an open finding, and waiting on it costs no round. Then the open `fix_now` findings, the failing tests and the files the coder never submitted go into one fix round. Fix rounds are one per block: wait for every `review-file` ticket of the block and its tests, then send ONE fix message with every failing test and every open finding; a finding that arrives later waits for the next round (`code.md` §4). Dispatch that message as one new `Agent` call at the level the ladder says (`review.md` §5 — the coder is gone with its tool return, so the fix brief carries every failing test, every open finding and the fix history). Its brief says: in that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. One block round is one round for each file it touched. Rounds are bounded: four rounds per file, then the L3 rung once, then `stopped: review_cap` for the human. Only when every changed file has a signed `review.approved` row do you run the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --report <file>` (the subagent's final report, saved to a file) — the transcript grep reads `.code-forge/runs/<run>/<block>.log`; pass `--transcript <file>` when the harness saved the subagent's transcript elsewhere. Closing with a file still unreviewed
|
|
15
|
+
**Between completion and close come the review rounds.** The subagent ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket inside its turn, so most rounds happen before the sentinel; when the tool returns, first drain every `pending` ticket with `forge review-file --wait <ticket>` yourself — a pending ticket is a review in flight, not an open finding, and waiting on it costs no round. Then the open `fix_now` findings, the failing tests and the files the coder never submitted go into one fix round. Fix rounds are one per block: wait for every `review-file` ticket of the block and its tests, then send ONE fix message with every failing test and every open finding; a finding that arrives later waits for the next round (`code.md` §4). Dispatch that message as one new `Agent` call at the level the ladder says (`review.md` §5 — the coder is gone with its tool return, so the fix brief carries every failing test, every open finding and the fix history). Its brief says: in that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. One block round is one round for each file it touched. Rounds are bounded: four rounds per file, then the L3 rung once, then `stopped: review_cap` for the human. Only when every changed file has a signed `review.approved` row do you run the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --report <file>` (the subagent's final report, saved to a file) — the transcript grep reads `.code-forge/runs/<run>/<block>.log`; pass `--transcript <file>` when the harness saved the subagent's transcript elsewhere. Closing with a file still unreviewed needs an audited waiver. Use `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding. The human can also use `forge block close … --no-require-reviews` at a terminal. That path asks for a yes. An active autopilot grant refuses it. `forge block reopen <id> --file <path> --run <r> --reason "<why>"` does not close anything: it sends a fixed stopped file back to review. Without `--yes` it asks for a yes at a terminal. The orchestrator prints the command for the human paths of `block reopen` and `block waive`. The orchestrator may run `block reopen --yes` with evidence and agreement from a fresh judge, at most once per block and file in a run. The orchestrator may run `block waive --yes` only for a recorded warning or nit finding with evidence and agreement from a fresh judge. A coder never runs either verb. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
16
16
|
|
|
17
17
|
## §2.1 Plan approval
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ Solo adds spawn, watch, timers, scratchpads, locks and `kv` on top of the CLI; i
|
|
|
22
22
|
5. **Wake channels, event-driven, never fixed polls:** record your own pid once (`whoami` → `kv_set("forge:orchestrator_pid")`); coders that can call Solo carry the push contract (post-sentinel `kv_set("block:<id>", …)` + `timer_set(1s, body, delivery_process_id=<your pid>)`); for every group arm `timer_fire_when_idle_any([coders], max_wait≈20–25 min, delivery_process_id=<your pid>)` right after the launch gate (idle-any ignores already-idle processes, so arm while they are provably running); keep one long dead-man fallback per 30–40 min with an L0 triage first. On wake: sentinel ⇒ gate; no sentinel and active again ⇒ re-arm; no sentinel and idle ⇒ parked, intervene now.
|
|
23
23
|
6. Completion = `search_output(pattern="===BLOCK <id> COMPLETE===")`, last occurrence (the `kv` key is the truncation backup). Completion is not closure.
|
|
24
24
|
7. **Review rounds, with the coder still alive.** The coder ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket; read the ledger (`forge ledger tail --slug <slug>`) for what is still open — a `fix_now` finding, a `pending` ticket, a file never submitted. After the coder's first COMPLETE, wait until every `review-file` ticket of the block is answered and the block's tests ran; then send ONE fix message with every failing test (name and first error line) and every open finding (by file and id). Never one finding or one file at a time. A finding that arrives after that message waits for the next round. In that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. The message goes to the **warm coder** by `send_input`, ending in `<<<EOM>>>`; when it is over the input limit (§1), write it to a file and send its path. Round ≥ 2 re-checks only the fix hunks (`review.md` §5), and one block round is one round for each file it touched. **One ladder** (`decisions.md` §5): a stall or exhausted rounds *below L2* climbs one level — close this coder and respawn a fresh one at the higher level with the open findings and the fix history in its brief; **the respawn keeps the per-file round count**, the cap does not reset. L2 is the highest running level: a trigger at L2, or the four-round cap on a file, goes to the **L3 rung once** — a fresh L3 patch session, never this coder and never a coder at L3 — and if findings stay open after the `patch_check` round the file is `stopped: review_cap` for the human. No other path exists.
|
|
25
|
-
8. **Close** only when every changed file has a signed `review.approved` row: the re-measurement of `code.md` §5, then `forge block close <id> --run <r> --report <file>` (the coder's final report) — the transcript grep reads the Solo output. Closing with a file still unreviewed
|
|
25
|
+
8. **Close** only when every changed file has a signed `review.approved` row: the re-measurement of `code.md` §5, then `forge block close <id> --run <r> --report <file>` (the coder's final report) — the transcript grep reads the Solo output. Closing with a file still unreviewed needs an audited waiver. Use `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding. The human can also use `forge block close … --no-require-reviews` at a terminal. That path asks for a yes. An active autopilot grant refuses it. `forge block reopen <id> --file <path> --run <r> --reason "<why>"` does not close anything: it sends a fixed stopped file back to review. Without `--yes` it asks for a yes at a terminal. Under an active autopilot grant the human paths still prompt. Autopilot's own waive path, `forge autopilot waive` (`by: autopilot`), needs no terminal. The orchestrator prints the command for the human paths of `block reopen` and `block waive`. The orchestrator may run `block reopen --yes` with evidence and agreement from a fresh judge, at most once per block and file in a run. The orchestrator may run `block waive --yes` only for a recorded warning or nit finding with evidence and agreement from a fresh judge. A coder never runs either verb. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
26
26
|
9. **Reap after the close**: `close_process` the coder and confirm `lock_status` shows no lease before the next group. Locks: a coder that can call Solo holds `lock_acquire` on its file area while editing; coders that cannot never overlap another block.
|
|
27
27
|
|
|
28
28
|
## §4 Latency overlaps (standing)
|
package/skill/references/code.md
CHANGED
|
@@ -87,12 +87,12 @@ Without the flag the tree stays. On a block without a worktree the flag is a usa
|
|
|
87
87
|
|
|
88
88
|
**When a file stops** (`review_cap`, `l3_patch_exhausted`, `split_required`, …), the block cannot close. The stop's `exits` name the ways out. Take them in this order; `review.md` §5 gives each one's rules and refusals:
|
|
89
89
|
|
|
90
|
-
1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then print the exact command, `forge block reopen <id> --file <path> --run <r> --reason <text>` with the human's reason, and the human runs it in their own terminal, exactly like `block waive`. Never run it yourself, and never for a coder (it is on the coder forbidden list). After it, send the coder's `review-file` again: the next round is a full round 1 when the content differs from what the stop reviewed; on the stop's own content the stop repeats. For known stop content, `reopen-same-content` means the content is still what the stop reviewed: offer the waive instead. A legacy stop with unknown content accepts a current hash without a verified `review.round` of this run/block/file and records `from_hash: null`. If that hash already has such a round, change the file and reopen, or the owner can use `block close --no-require-reviews`; waivers at an old hash cannot clear the current one.
|
|
90
|
+
1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then print the exact command, `forge block reopen <id> --file <path> --run <r> --reason <text>` with the human's reason, and the human runs it in their own terminal, exactly like `block waive`. For this comparison, `block waive` means its human path. Never run it yourself, and never for a coder (it is on the coder forbidden list). After it, send the coder's `review-file` again: the next round is a full round 1 when the content differs from what the stop reviewed; on the stop's own content the stop repeats. For known stop content, `reopen-same-content` means the content is still what the stop reviewed: offer the waive instead. A legacy stop with unknown content accepts a current hash without a verified `review.round` of this run/block/file and records `from_hash: null`. If that hash already has such a round, change the file and reopen, or the owner can use `block close --no-require-reviews`; waivers at an old hash cannot clear the current one.
|
|
91
91
|
2. **`review-file --answers`.** Use it when the fix belongs in another owned file (a missing test, say). The coder fixes it there and runs `forge review-file <that file> --block <id> --run <r> --answers <ids> --of <source>`. A fresh reviewer judges the fix. It needs no human step.
|
|
92
|
-
3. **`block waive`.**
|
|
92
|
+
3. **`block waive`.** Use `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` per finding. The rule below sets who may run it and when `--yes` is allowed. A file whose findings are all waived or answered closes with a signed `gate.review_waived_all` row.
|
|
93
93
|
4. **Re-decompose.** The human decides, and the architect plans new blocks.
|
|
94
94
|
|
|
95
|
-
**`block waive` and `block reopen`
|
|
95
|
+
**`block waive` and `block reopen` (B80).** The orchestrator prints the command for the human paths of `block reopen` and `block waive`. The orchestrator may run `block reopen --yes` with evidence and agreement from a fresh judge, at most once per block and file in a run. The orchestrator may run `block waive --yes` only for a recorded warning or nit finding with evidence and agreement from a fresh judge. A coder never runs either verb. The orchestrator never runs the human paths and never answers their prompt (not through a pseudo-terminal either); the human runs them in their own terminal. The human paths ask for a yes at a terminal. Without a terminal they exit 2 and record nothing. For `block reopen` without `--yes`, the message is `block reopen needs the human at a terminal to confirm; nothing was recorded (--yes works only with evidence and agreement from a fresh judge)`. Without `--yes` and with no terminal, `block waive` prints `block waive needs the human at a terminal to confirm; nothing was recorded (--yes works only for a recorded warning or nit finding, with evidence)`. An autopilot grant does not change the human paths. The delegate waives through `forge autopilot waive`. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
96
96
|
|
|
97
97
|
`block attempt` never resets review state: it only raises the attempt counter, and a stopped file stays stopped. Use `review-file --fresh` only after a result that never counted (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); it never re-runs a counted round. A missing state file needs no exit: the worker first rebuilds it from its ledger anchor (`review.state_restored`). Only a state whose anchor has no `state_body` is `lost` (`review-state-lost`); that one needs the human's `block reopen`, on its current content. A `review-state-tampered` file has none of these exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
98
98
|
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
Every row has the run, the block, the event and a timestamp; the ones the block gate relies on carry `mac` (`security.md` §3). Useful fields: `level`, `provider`, `model`, `effort`, `fallback_step` (every spawn), `role ∈ {coder, reviewer, judge, s2, author, facts}`, `trigger` (escalations), `source ∈ {jev, rules, s2, s2-fallback, shadow}` (decisions), `depth_unconstrained` / `depth_chosen` / `degrade_step` and `context_mode ∈ {whole, hunks, minimal, recheck}` (reviews), `round` and `kind ∈ {full, recheck, patch_check, sweep}` (rounds), `isolation ∈ {export, lock}` (proof), `l3_patch`, `l3_fallback`, `overspend`. **Tokens are facts, dollars are estimates**: `tokens_source: reported` when the CLI's JSON carries usage, else `estimated`. File paths in rows are relative to the repository root.
|
|
6
6
|
|
|
7
|
-
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch` (`test_files` when `block open --tests` named them), `gate.red` and `gate.done` (with `scope: related` + `test_count` or `scope: full` + `reason: threshold | no_tests` when a gate has the `{test_files}` item, `no_tests` also without `--run/--block`; with `--run/--block` they name `run` and `block`, and for a block with a worktree also `worktree: <path>` (the gates ran there); a gate with env names only `env_keys` in its result, `proof.md` §3), `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}` (
|
|
7
|
+
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch` (`test_files` when `block open --tests` named them), `gate.red` and `gate.done` (with `scope: related` + `test_count` or `scope: full` + `reason: threshold | no_tests` when a gate has the `{test_files}` item, `no_tests` also without `--run/--block`; with `--run/--block` they name `run` and `block`, and for a block with a worktree also `worktree: <path>` (the gates ran there); a gate with env names only `env_keys` in its result, `proof.md` §3), `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human | agent}` (`block waive` writes no row on a "no", a cancel, no terminal without `--yes`, a `--yes` refusal, or a pre-check refusal. The `--yes` refusal codes are `waive-yes-critical`, `waive-yes-no-evidence`, and `waive-yes-disagreed`. Pre-checks reject a malformed or control-character finding id or `--file`, an unknown block, an unowned file, or an unknown finding. An agreed `block waive --yes` writes one signed `review.waived` row with `by: agent`. The human path writes `by: human` after a yes in their own terminal (B80). The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).), `review.state {block, file, seq, state_sha, l3_rung_used, state_body}` (signed; the fix-loop state anchor, `review.md` §5 step 8; `state_body` carries `reviewed_content_sha` and `reviewed_content_blob`, the git blob id of the reviewed text in the reviewed tree's object store, or null when there was no reviewed text or no content root, or the write failed; when the signed body is over 64 KB, `state_omitted: "too_large"` replaces `state_body`, and a lost state file then refuses as `review-state-lost`), `review.state_restored {block, file, seq, from_seq}` (signed; a lost state file was rebuilt from the anchor at `from_seq` and saved as `seq`), `review.reopened {block, file, from: stopped | lost, stop_reason, at_seq, from_hash, to_hash, reason, by: human | agent}` (signed; a reopen of a stopped or lost file. An agreed `block reopen --yes` writes one signed `review.reopened` row with `by: agent`, the reason and `verdict: {verdict, reason}`. It needs evidence and a fresh judge, at most once per block and file in a run. The refusal codes are `reopen-yes-no-evidence`, `reopen-yes-limit` and `reopen-yes-disagreed` (exit 1, no row). Without `--yes` the human confirms at a terminal; no terminal, no, cancel or a pre-check refusal writes nothing — `at_seq` the file's latest `review.state` anchor seq, `from_hash` the content the stop reviewed (null for `lost`), `to_hash` the current content, `stop_reason` null for `lost`; at most one row per `at_seq`, the verb refuses a second one for other content as `reopen-already-recorded`; the worker starts a fresh round-1 state for it once, saved at `at_seq + 1` — for `from: stopped` only when the current content still differs from the stop's content (the state's `stop_content_hash`) and from `from_hash`, else the stop repeats; round 1 is a full review (`reviewed_content` null) and `l3_rung_used` is kept, so the file gets up to `review.max_rounds_per_file` rounds at L2 again; the worker ignores a row that does not verify, of another run, neither `by: human` nor `by: agent`, or at another seq), `review.answered {block, file, content_hash, finding, by_file, by_hash, ticket}` (signed; B71: an answers ticket's reviewer found that `by_file` at `by_hash` resolves the open finding `finding` of `file` at `content_hash` — exactly one row per (finding, `by_hash`, ticket): a retried ticket writes no duplicate, only the rows a failed write left out; the close counts it only while both hashes are current, and ignores a row without a valid run-key MAC or from another run; an answer may clear a critical and never approves `by_file`, `review.md` §5 step 5), `gate.reviews_waived {by: human | config}` (`by: config` from `review.external`), `gate.review_waived_all {block, file, content_hash, findings, answered?, by: human | autopilot | agent | null}` (signed; a file with no approval closed as `reviewed_by_waiver`: a signed `review.round` at its current hash, and every finding of the latest hash-bound refusal and every open `review.cap` id cleared by an accepted waiver or a still-current signed `review.answered` row (B71) — a file cleared only by answers is `reviewed_by_waiver` too. `findings` are the sorted WAIVED ids and `answered` (B71, only when there is one) the sorted ids `review.answered` rows cleared; `by: human` means every clearing waiver is human. Otherwise, any agent waiver sets `by: agent`; the rest use `by: autopilot`. Only answers set `by: null` (`findings: []`); a reader must handle null. One row per such file just before `block.close`, none on a refused close, `review.md` §5 step 5), `coder.tests {block, command, exit}` (signed; under `context.require_coder_tests` the close read the coder's `TESTS <command> exit=0` line from `--report`, written with the close rows just before `block.close` and only when the block closes, `code.md` §3), `decision {question: lane, source: rules}` (a brief-mode `block open` without `--level`), `worktree.created {block, path, branch, base}` (`block open` under `proof.isolation: worktree` made the block's tree, written before `dispatch`; a re-open that reuses it writes none), `worktree.removed {block, path, branch}` and `worktree.remove_failed {block, path, reason}` (after `block close --remove-worktree`; the branch is always kept, and on a failure the tree stays, `proof.md` §3), `context.pack {block, path, sha256, tokens, lines, cut}` (signed; `forge context pack` wrote a new pack for the block — the same pack again, same sha256 as the block's last row, writes none; `code.md` §3), `hook.ran {block, hook: on_block_end, argv0, exit, signal, timed_out, duration_ms}` (signed; one row per block end with `hooks.on_block_end` set — also when the hook fails, times out or is not found; none when the key is absent; `argv0` is the basename only. Exited: `exit` its code, `signal` null. Killed by a signal: `exit` null, `signal` its name. Timed out: `exit` null, `signal` `SIGKILL`, `timed_out` true. Not found (argv[0] ENOENT) or any other spawn error (a missing cwd, EACCES): `exit` null, `signal` null, `timed_out` false; `code.md` §3), `outside_review {run, pr, rounds, findings, link (or null), blocks}` (signed; `forge ledger outside-review`, §4 — the latest row per PR wins in every reader), `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
|
|
8
8
|
|
|
9
9
|
## §2 `forge report --slug <slug> [--json] [--export <dir>]`
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
`review.unwaived {block, file, finding, reason, by: human}` is a signed human undo of earlier agent waivers. `agent_waivers.acked {block, digest, count, by: human}` is a signed human acknowledgement of the current counted set. The gate verifies both rows. The digest is SHA-256 of the sorted file, finding and row signature set. New or removed waivers need a new acknowledgement when the run snapshot requires one.
|
|
12
|
+
|
|
13
|
+
Terminal output in this version. Sections: cost per block — completed and still open, with `completed` and the count of unknown-price sessions (per level and lane, split review / coder / S1+S2 / facts / proof time) · lane distribution · escalations per block with `trigger` · S1 calls per block · S2 rate and overrule rate per question · the review budget table with `missed_after_degrade` per degrade step · review depth and context-mode distribution, findings by category and severity · fix rounds per file (one block round is one round for each file it touched, `review.md` §5), rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine` · `outside_review` (14th): per PR sorted by number, `{pr, rounds, findings, link, blocks, code_forge_usd, unknown_usd_sessions}` — `code_forge_usd` sums the `cost_per_block` totals of the PR's blocks in its run, rounded as `cost_per_block` rounds; unknown-price sessions count 0 and are counted; a PR with no blocks shows 0 and 0 unknown; `[]` with no row; in the terminal the lines print under `== outside_review ==`: `outside review: none`, or one line per PR (`PR #12 · 3 outside rounds · 7 findings · code-forge $3.75 (+1 unknown) · B1, B2`). `agent_waivers` is the 15th and last section. It lists every agent waiver as `{block, file, finding, severity, reason, verdict, unwaived}`. A later human unwaive marks earlier matching rows as `unwaived: true`. A later agent waiver is a separate entry. An empty run gives `[]`. `--export <dir>` writes one JSON file per section — the durable dataset a future UI reads. After the sections the terminal output prints the spend per run (open blocks included) and the total. `report` checks no row's signature in any section (an unsigned row counts like a signed one); shadow rows (`source: shadow`) are excluded from every section.
|
|
12
14
|
|
|
13
15
|
## §3 Live views
|
|
14
16
|
|
|
@@ -9,7 +9,7 @@ Tier per file = `high` when `risk ≥ 2`, or a `proof.tiers.high.paths` match, o
|
|
|
9
9
|
| light | red→green required for every new or changed test; **optional at the gate** | the coder or orchestrator runs the runner per test (§2) |
|
|
10
10
|
| high | red→green **required at the gate** for every changed high-tier file, plus the deeper **review** depth of `review.md` §2 (two lenses and a judge) | as above; `block close` refuses `unproven <file>` |
|
|
11
11
|
|
|
12
|
-
**`forge block close` enforces the high tier:** tier inputs are `proof.tiers.high.paths` (the union of the config at the block's base and the current one), the recorded `risk`, and `security_sensitive`; a row naming the file whose MAC fails makes it high. Each changed high-tier file needs a signed `proof` row of this run and block with `step: red-green`, `proven: true`, `red_kind: assertion` whose `covers` list names the file (`revert` covers the sources it put back; `assertion-deletion` covers only its test), else `unproven <file>`; only the human's `forge block waive <id> proof --file <path>` clears it
|
|
12
|
+
**`forge block close` enforces the high tier:** tier inputs are `proof.tiers.high.paths` (the union of the config at the block's base and the current one), the recorded `risk`, and `security_sensitive`; a row naming the file whose MAC fails makes it high. Each changed high-tier file needs a signed `proof` row of this run and block with `step: red-green`, `proven: true`, `red_kind: assertion` whose `covers` list names the file (`revert` covers the sources it put back; `assertion-deletion` covers only its test), else `unproven <file>`; only the human's `forge block waive <id> proof --file <path>` clears it. The proof waiver has unknown severity and no `--yes` path. The orchestrator prints the exact command and the human runs it in their own terminal (`review.md` §5).
|
|
13
13
|
|
|
14
14
|
**Mutation testing is not part of this product.** The question whether tool-made mutants on changed lines (Infection, Stryker) should be a user-facing proof tier was decided: **cut**. There is no `proof mutate`, no nightly mutation run, no `min_msi` floor, and no mutation-tool key in `.code-forge.yml`; a consumer that runs a mutation tool does so outside code-forge and never beside a coder's test run in the same tree. What replaces it as proof is the rule itself: one exact test per acceptance clause plus one negative or boundary where the clause has a failure mode — assertions that say **which** element and **how many**, never merely whether something exists — and the isolated per-file review with the convergence rule, which on this package's own build found the defects green suites had missed. The same holds for hand mutations by a coder: they cost about one test run each and a coder tends to pick mutants its own tests already kill, so a coder proves each new or changed test red→green (§2) instead; when a review finds a weak assertion, the fix is a stronger assertion, not a mutant.
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The same content answers the cached result; `review-file <path> --block <id> --fresh` (B75) drops that cached result and queues the ticket again only when it never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`). Use it after such a transient answer to get a real review; a coder may run it, and with no cached result it is a plain `review-file`. A cached `review-state-tampered` is refused `fresh-tampered` (only the owner's `block close --no-require-reviews` gets past it); any other cached result is refused `fresh-needs-reopen` (the human's exits are `block reopen` and `block waive`). A refused marker is read in place and never moved; only a re-runnable one is claimed (an atomic rename, then a byte compare) and dropped. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. Without commit watch, a coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
|
|
6
6
|
|
|
7
|
-
With `review.file_loop: watch` (B104), commit each owned file after its tests are green and continue coding. Watch replaces per-file enqueueing. Before the completion sentinel, including fix rounds, run `forge review-file --wait-block <id> --run <r> [--max 90s]`, inspect finished tickets with `--wait <ticket>`, and fix findings. The worker watches the block worktree HEAD (otherwise repository HEAD) and reads owned content from git objects. A file's first commit is queued at once with the same ticket id as a manual request. Later commits of a file that already has a ticket of this block are held until `--wait-block`. If the last review found problems and the file changed blind, the join does not recheck it. It answers `status: findings` with that ticket and records delivery once. Read the findings with `review-file --wait <ticket>`, fix them, and the next join rechecks the latest content once. A file whose last review approved still queues its latest content at the join. A stopped file is not requeued. Each held file writes one `watch.held {block, file, content_hash}` row per new head. Older pending tickets are still marked `refused`, `reason: superseded`. A running ticket finishes for its old hash, which cannot approve newer bytes; `--wait-block` joins the current ticket per file. `--fresh` refuses a superseded marker; enqueueing that content again removes only its superseded marker so it can run, while a real result stays cached. `queue/watch/<block>.json` stores the last seen commit; missing or unreadable state restarts at `base_sha` with idempotent tickets. A rewound tick queues nothing and drops the last seen commit so the next tick restarts at `base_sha`. A `watch.skipped {block, reason}` row is written once per reason until a healthy head clears the skip reasons: `unreadable_head` (head, tree or objects cannot be read), `not_descendant` (head does not descend from the block base), `rewound` (head no longer descends from the last seen commit), or `unowned_path` (unowned paths are ignored; owned paths still queue). A skip never fails the block. After a `watch.skipped` row, use `forge review-file <path> --block <id> --run <r>` when the file needs a manual review; use `forge review-file <fix file> --block <id> --run <r> --answers <ids> --of <source>` for a fix in another owned file that answers findings. Set watch before starting the worker. Without the `review.file_loop: watch` setting, the default `wait` loop performs no watch work or git calls.
|
|
7
|
+
With `review.file_loop: watch` (B104), commit each owned file after its tests are green and continue coding. Watch replaces per-file enqueueing. Before the completion sentinel, including fix rounds, run `forge review-file --wait-block <id> --run <r> [--max 90s]`, inspect finished tickets with `--wait <ticket>`, and fix findings. The worker watches the block worktree HEAD (otherwise repository HEAD) and reads owned content from git objects. A file's first commit is queued at once with the same ticket id as a manual request. Later commits of a file that already has a ticket of this block are held until `--wait-block`. If the last review found problems and the file changed blind, the join does not recheck it. It answers `status: findings` with that ticket and records delivery once. Read the findings with `review-file --wait <ticket>`, fix them, and the next join rechecks the latest content once. A file whose last review approved still queues its latest content at the join. A stopped file is not requeued. Each held file writes one `watch.held {block, file, content_hash}` row per new head. Older pending tickets are still marked `refused`, `reason: superseded`. A running ticket finishes for its old hash, which cannot approve newer bytes; `--wait-block` joins the current ticket per file. The join reruns a stale, unavailable or round-1 `split_required` review of the current content once. The file stays `pending` until that rerun finishes. No manual `--fresh` is needed. The retry is recorded per ticket across joins. If the rerun is stale again, later joins report `done`; `--wait <ticket>` shows that stale status. `--fresh` refuses a superseded marker; enqueueing that content again removes only its superseded marker so it can run, while a real result stays cached. `queue/watch/<block>.json` stores the last seen commit; missing or unreadable state restarts at `base_sha` with idempotent tickets. A rewound tick queues nothing and drops the last seen commit so the next tick restarts at `base_sha`. A `watch.skipped {block, reason}` row is written once per reason until a healthy head clears the skip reasons: `unreadable_head` (head, tree or objects cannot be read), `not_descendant` (head does not descend from the block base), `rewound` (head no longer descends from the last seen commit), or `unowned_path` (unowned paths are ignored; owned paths still queue). A skip never fails the block. After a `watch.skipped` row, use `forge review-file <path> --block <id> --run <r>` when the file needs a manual review; use `forge review-file <fix file> --block <id> --run <r> --answers <ids> --of <source>` for a fix in another owned file that answers findings. Set watch before starting the worker. Without the `review.file_loop: watch` setting, the default `wait` loop performs no watch work or git calls.
|
|
8
8
|
|
|
9
9
|
A cached round-1 `split_required` ran no session and counted no round. Plain `review-file` drops its marker and re-queues the same ticket with `retry: true`; `--fresh` also re-queues it (B110). After the human raises the budget and runs `run reload`, the worker re-plans with the live config; the ticket id stays the same. If it still does not fit, it returns `split_required` again with no session or cost. A recheck stop (`status: stopped`, `stopped: split_required`) has a `review.cap` row and still needs `block reopen` or `block waive`; plain review keeps it cached and `--fresh` refuses `fresh-needs-reopen`.
|
|
10
10
|
|
|
@@ -70,15 +70,22 @@ Only real rechecks count toward the round cap, per-level ladder and stall rules
|
|
|
70
70
|
3a. **Heavy rounds climb at once** (`review_warnings`, `decisions.md` §5 rule 3a): below L2, a round — round 1 included — that leaves ≥ `escalation.warning_threshold` (2) warnings or any `critical` open, `escalation.after_rounds_with_warnings` (1) times at the current level, climbs one level for the next fix. One round with a stronger model is faster and cheaper than three with a cheap one. The per-file cap (step 5) stays the outer limit.
|
|
71
71
|
4. **Ladder**: the running ceiling is `min(L2, escalation.stop_at)` — L3 is never a *running* level. A trigger — rule 1 (`retries`), rule 2 (`escalation.review_rounds_per_level` exhausted with an open `fix_now`), rule 2b (`review_stall`) or step 3a (`review_warnings`) — below the ceiling climbs one level, coded by a fresh session with the open findings and the fix history in its brief (an L1 block's rounds 3–4 run at L2). At the ceiling — an L2 block, or an L1 block that has climbed to L2 — it takes the **L3 rung**: a patch ≤ 80 lines, owned files only, reviewed as a `patch_check` round outside the cap. Under `escalation.l3_mode: code` (opt-in) the rung is instead one L3 coding turn. Either way the rung fires **once per block** and the same post-patch order follows it. After it, in order: `patch_check` clean ⇒ complete; the patch closed no finding that was open before it (ids compared, not counts) ⇒ `stopped: l3_patch_exhausted` (`review_stall`); round ≥ `review.max_rounds_per_file` ⇒ `stopped: review_cap`; otherwise the fixes continue at L2 with `rounds_at_level` reset and the patch in the brief. A patch that closed at least one old finding is progress, not a stall, even when new findings in its fix hunks keep the open set from shrinking (B73). Any later trigger in that block stops with its named reason (`l3_patch_exhausted`, `review_cap`) and never patches again.
|
|
72
72
|
5. **Cap — four rounds per file** (`review.max_rounds_per_file`, default 4, range 2–6). The per-file cap still counts per file: the orchestrator sends one fix message per block (`code.md` §4), and one block round is one round for each file it touched. Round 4 still open ⇒ the L3 rung if the block has not used it (same post-patch order as step 4), else `stopped: review_cap`. A stopped file (`review_cap`, `l3_patch_exhausted`, `split_required`, …) keeps the block from closing; the ledger lists its open findings. The stop names its ways out: a `stopped` answer from `review-file` carries `exits: ["block reopen", "block waive", "review-file --answers", "re-decompose"]`, and the close's `review_cap` detail and `unreviewed` detail for a stopped file end with `exits: block reopen, block waive, review-file --answers, re-decompose` (B73). That printed list is the code's list, not an order; follow Exit 1–4 below. **The ways out of a stop, in this order** (take the first that fits):
|
|
73
|
-
- **Exit 1 — fix, then `block reopen` (B72).** The file is fixed by hand, then the human restarts its review: `forge block reopen <id> --file <path> --run <r> --reason <text>`. It also restarts a `lost` state (step 8) on its current content.
|
|
73
|
+
- **Exit 1 — fix, then `block reopen` (B72).** The file is fixed by hand, then the human restarts its review: `forge block reopen <id> --file <path> --run <r> --reason <text>`. It also restarts a `lost` state (step 8) on its current content. Without `--yes` it needs human confirmation; see **Human confirmation** below. `block reopen` and `block waive` both have evidence-backed agent paths through `--yes` with a fresh judge. A stopped file must differ from the content its stop reviewed. Every stop records that content as `stop_content_hash` in the signed state; a state written before B72, or before B114 by a fix-loop stop, has none, and then only the `review.cap` row of that stop (after the file's latest reopen, same round and reason) names it — otherwise the content is unknown. With unknown stop content, reopen accepts only when no verified `review.round` of this run, block and file names the current hash (B114). It records `from_hash: null`; the worker enforces the same check. If that hash was reviewed, change the file and reopen, or the owner can use `block close --no-require-reviews`. Waiving findings at an old hash cannot clear unreviewed current bytes. The verb first removes the done marker of the current content's ticket, so the next `review-file` runs instead of returning the cached stop. Then it writes one signed `review.reopened {block, file, from: stopped|lost, stop_reason, at_seq, from_hash, to_hash, reason, by: human | agent}` row (`at_seq` is the file's latest `review.state` anchor seq; `from_hash` the content the stop reviewed, null for `lost` or unknown stop content; `to_hash` the current content). There is at most one row per `at_seq`: a retry on the same content finds its row and writes no second one, and other content at the same `at_seq` is refused. The worker starts a new state for a row whose MAC verifies, of this run, `by: human` or `by: agent`, whose `at_seq` is the latest anchor seq: round 1, a full review (no fix hunks), the level from the run record, up to `review.max_rounds_per_file` rounds again. It saves at `at_seq + 1`, so a row works once. For a stop the worker checks again that the content changed; on the stop's own content the stop repeats. The block's one L3 rung stays spent when it was used, so a reopened file that reaches the cap stops at `review_cap`. After a verified human or agreed agent reopen and a fresh signed review of the current bytes, the old cap ids are cleared. The fresh review’s own findings still count. An approval written after the `review.cap` row also clears the cap. Refusals (exit 1, no row): `reopen-same-content` (a stopped file still at the content its stop reviewed, or unknown stop content whose current hash has a verified round: for unknown content, change the file and reopen, or the owner can use `block close --no-require-reviews`), `reopen-tampered` (an integrity failure stays a security event), `reopen-not-stopped` (no state, or an open or complete one), `reopen-block-not-open` (the block is stopped or closed), `reopen-not-owned`, `unknown_block` (the block lookup code `block waive` uses; it and `reopen-block-not-open` are checked before anything else is read), `reopen-config` (the run's config snapshot or `tmp.root` cannot be read), `reopen-already-recorded` (a row for this `at_seq` already names other content: run `review-file`, or the content changed since that reopen), `reopen-write-failed` (the row was not written). A missing `--file` or `--reason`, or a `--file` that is not a plain repo-root-relative path (no `..`, `./`, `//` or `/./`, and no control character), is `usage` (exit 2).
|
|
74
74
|
- **Exit 2 — `review-file --answers` (B71).** A finding fixed in ANOTHER file of the block (a missing test added in the test file, say) is answered with `forge review-file <answering file> --block <id> --answers <id,id,…> --of <source> --run <r>` (1 to 10 ids, none twice; `--of` names the source because finding ids are numbered per file). A coder may run it.
|
|
75
|
-
- **Refusals.** The checks run when the ticket is queued and again in the worker, whose check is the one that counts. The block must own both files (`answers-not-owned`), they must be two files (`answers-same-file`), and every id must be open on the source's CURRENT hash (`answers-not-open`). Open means named by the latest signed refusal row whose `content_hash` is the current hash, or by ANY signed `review.cap` row whose `content_hash` is the current hash, not waived or answered yet, and one the close could count as answered: never the proof finding (a proof finding is cleared by its proof, not by a reviewer) and never an id read at an unknown severity (a cap id no signed refusal at that hash records, or a value outside critical/warning/nit). A cap id from an older hash (or a cap row with no `content_hash`) is never open for an answer. The close still requires a human waiver unless the cap row is superseded. A verified human `review.reopened` row after the cap and a signed `review.round` of the current hash after the reopen supersede it. A missing `--of`, no ids, more than 10 ids, an id given twice or a malformed id is `usage` (exit 2). The other refusals exit 1 and queue nothing. In the worker, an answering file that is gone or cannot be read is `answers-file-missing`.
|
|
75
|
+
- **Refusals.** The checks run when the ticket is queued and again in the worker, whose check is the one that counts. The block must own both files (`answers-not-owned`), they must be two files (`answers-same-file`), and every id must be open on the source's CURRENT hash (`answers-not-open`). Open means named by the latest signed refusal row whose `content_hash` is the current hash, or by ANY signed `review.cap` row whose `content_hash` is the current hash, not waived or answered yet, and one the close could count as answered: never the proof finding (a proof finding is cleared by its proof, not by a reviewer) and never an id read at an unknown severity (a cap id no signed refusal at that hash records, or a value outside critical/warning/nit). A cap id from an older hash (or a cap row with no `content_hash`) is never open for an answer. The close still requires a human waiver unless the cap row is superseded. A verified human or agreed agent `review.reopened` row after the cap and a signed `review.round` of the current hash after the reopen supersede it. An agent row counts only with `verdict: {verdict: agree, reason}`. A missing `--of`, no ids, more than 10 ids, an id given twice or a malformed id is `usage` (exit 2). The other refusals exit 1 and queue nothing. In the worker, an answering file that is gone or cannot be read is `answers-file-missing`.
|
|
76
76
|
- **The review.** The ticket is its own kind (`kind: answers`); its id also covers the source, the source hash and the sorted ids, so the same request is the same ticket. The worker sends ONE fresh L2 reviewer session (lens `answers`) a packet with the findings, the source lines around each, and the answering file's diff against the block base (its full content when the base has no such file). Each file is read ONCE: those bytes are hashed and the packet is built from them, so the row's `by_hash` is the hash of what the reviewer saw. The packet file is removed after the session; a packet that cannot be built is `unavailable` (`packet-unreadable`), one that cannot be written `unavailable` (`packet-write-failed`). The reviewer answers `resolved` or `not_resolved` per id. A missing, repeated or broken verdict is `not_resolved`. A failed session (no result, not `ok`, a non-zero exit, or a spawn that throws: `session-error`) is `unavailable` (retry it; it is not a verdict) and writes no row. An answering file changed since the ticket is `stale` (`answering-file-changed`); a source changed since the ticket is `stale` (`source-changed`). All verdicts are built first, then each resolved id gets one signed `review.answered {block, file, content_hash, finding, by_file, by_hash, ticket}` row. If a write fails the ticket is `unavailable` (`ledger-write-failed`); the same request again retries the same ticket, the ids it already answered stay open for it, and it writes only the missing rows, so it ends with one row per id. `review-file --wait` prints `{answers: [{id, verdict, reason}]}`.
|
|
77
77
|
- **Never an approval.** The answering file still needs its own `review.approved`, and the source's fix-loop state is unchanged.
|
|
78
|
-
- **Exit 3 — `block waive`.** The human waives each open finding: `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` (a signed `review.waived` row, `by: human`). The coder never sees a `waive` verb; it is on the forbidden list.
|
|
79
|
-
- **
|
|
78
|
+
- **Exit 3 — `block waive`.** The human waives each open finding: `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` (a signed `review.waived` row, `by: human`). The coder never sees a `waive` verb; it is on the forbidden list. For the human path, see **Human confirmation** below. **Autopilot:** under a grant, a warning or nit finding may be waived by the delegate (`forge autopilot waive … --decision <id>`, written `by: autopilot`) and a file at the cap may get one extra round (`forge autopilot round`); a critical or proof waiver is never delegated (`autopilot.md`).
|
|
79
|
+
- **Undo and acknowledge agent waivers.** The human runs `forge block unwaive <id> <finding-id> --run <r> --file <path> --reason <text>` to reopen a finding. It requires a verified agent waiver for an owned file. Unknown blocks, unowned files and missing verified agent waivers refuse with `unwaive-no-agent-waiver` (exit 1) before any prompt. A yes writes one signed `review.unwaived {block, file, finding, reason, by: human}` row. It cancels all earlier agent waivers for that finding and file in that block. Human waivers remain in force. A later agreed agent waiver counts again. Unsigned or tampered unwaives never cancel a waiver at the gate.
|
|
80
|
+
Set `review.agent_waivers.require_ack: true` before run start to require human acknowledgement before close. The key is read only from the run snapshot and cannot change through run reload. The human runs `forge block ack-waivers <id> --run <r>`. It prints each counted waiver's file, finding, reason and verdict before asking. No counted waivers refuses with `ack-waivers-none` (exit 1), without a prompt. A yes writes one signed `agent_waivers.acked {block, digest, count, by: human}` row. The digest hashes the sorted file, finding and signed row identity set. A new or removed waiver changes it. Close refuses with `agent-waivers-not-acked` until a verified human ack matches the current set. With the key absent or false, close is unchanged.
|
|
81
|
+
Both verbs are human-only and coder-forbidden. They ask at a terminal and have no `--yes`. No terminal exits 2. A no or cancel exits 1. These paths record nothing.
|
|
82
|
+
- **Agent waivers.** Use `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text> --yes`. The orchestrator may run `block waive --yes` only for a recorded warning or nit finding, with evidence, and a fresh judge must agree. Critical, unknown and unrecorded findings refuse with `waive-yes-critical` (exit 1). The reason must cite an existing repo-relative path with `:<line>`, an existing test file, or a numbered acceptance clause of this block. The path must exist in the block's tree. A clause number must be within its acceptance clause count. Missing evidence refuses with `waive-yes-no-evidence` (exit 1), before the judge runs. One fresh L3 judge gets the recorded finding, reason and current file bytes. It never resumes the file's reviewer session. An `agree` writes one signed `review.waived` row with `by: agent`, the recorded severity, reason, `verdict: agree` and `judge_reason`. A disagreement or broken answer refuses with `waive-yes-disagreed` (exit 1). The judge's reason is printed. No refusal writes a waiver. The finding stays open.
|
|
83
|
+
- **Agent reopens.** Use `block reopen <id> --run <r> --file <path> --reason <text> --yes`. It runs without a terminal or prompt. Every ordinary reopen check runs first. Evidence uses the same rule as agent waivers: an existing repo path with `:line`, an existing test file, or a numbered acceptance clause. No evidence refuses `reopen-yes-no-evidence` (exit 1). Only one verified agent reopen per block and file is allowed in a run; another refuses `reopen-yes-limit` (exit 1). Neither refusal calls the judge. One fresh L3 judge gets the stop reason, last findings, reason and current file bytes. A disagreement or invalid answer refuses `reopen-yes-disagreed` (exit 1) and prints the judge reason. The file stays stopped and nothing is written. Agreement uses the same done-marker removal and signed reopen row as the human path, with `by: agent` and `verdict: {verdict, reason}`. There is no severity restriction. The human path remains available after the agent limit. Close prints `agent reopen <file>: <reason> (verdict: agree)` for each agent reopen. Its JSON adds `agent_reopens: [{file, reason, verdict}]` only when nonempty.
|
|
84
|
+
- **Human confirmation.** Without `--yes`, `block waive` keeps its human confirmation and writes `by: human`. Critical and unknown-severity findings need the human terminal path. With no terminal it exits 2: `block waive needs the human at a terminal to confirm; nothing was recorded (--yes works only for a recorded warning or nit finding, with evidence)`. `block reopen --yes` needs evidence and agreement from a fresh judge, at most once per block and file in a run. With no terminal it exits 2: `block reopen needs the human at a terminal to confirm; nothing was recorded (--yes works only with evidence and agreement from a fresh judge)`. Without `--yes`, the orchestrator prints the exact command with the human's reason; the human runs it in their own terminal. The orchestrator never answers a human confirmation prompt. A coder never runs either verb. Both verbs validate the file and block before asking. A waive also checks ownership and that the finding is recorded. A no or cancel records nothing. Under an active autopilot grant these human paths still prompt. The delegate's existing `autopilot waive` path still writes `by: autopilot`. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
85
|
+
- **False transcript hits.** The human runs `forge block accept-hit <id> --run <r> --rule <rule-id> --line <n> --transcript <path> --reason <text>` in their terminal. It uses the same human confirmation and has no `--yes`. It checks the run, block, readable transcript and scanned rule/line pair before asking. A missing hit refuses `accept-hit-no-such-hit` (exit 1). Bad forms are exit 2. No terminal is exit 2. A no or cancel writes nothing. A yes writes one signed `rule_break.accepted {block, rule, line, line_sha256, reason, by: human}` row. Close matches its run, block, rule and exact line content hash, even if the line moves in a joined log. Other hits and ledger `rule_break` rows still refuse. Close prints each acceptance it used with the current line and reason. Its JSON adds `accepted_hits: [{rule, line, reason}]`. A coder never runs this verb.
|
|
86
|
+
- **Agent waivers at close.** A signed `by: agent` waiver with `verdict: agree` clears a warning or nit finding for `reviewed_by_waiver`. Unsigned or tampered rows never count. Its `gate.review_waived_all` row uses `by: agent`. `block close` prints one line per signed agent waiver with file, finding, reason and verdict. `--json` adds `agent_waivers: [{file, finding, severity, reason, verdict}]` when there are agent waivers.
|
|
80
87
|
- **Exit 4 — re-decompose.** The human decides, and the architect plans the work again as new blocks (`decisions.md` §6, `redecompose`). Each new block reviews its files from round 1.
|
|
81
|
-
- **A fully waived or answered file closes with a `gate.review_waived_all` row.** A file with no approval for its current hash counts as reviewed — `reviewed_by_waiver`, never approved — when a signed `review.round` exists for that hash (a repeated stop or an `unavailable` result never counts), the latest refusal row for the hash is signed, carries `content_hash` equal to that hash (a refusal row with no `content_hash` still refuses) and names at least one finding, and every finding it names and every open id of the file's unsuperseded `review.cap` rows is waived or answered. A cap row adds no ids when a verified human `review.reopened` row follows it and a signed `review.round` of the current hash follows the reopen. A critical waiver counts only `by: human`; an autopilot waiver counts only when `autopilotWaiverProblem` finds nothing, and only for a finding a signed refusal row at that hash records as warning or nit (or records with no severity at all). A cap id no refusal row records a severity for, or a cap row whose `open` is not a list of ids, is the human's alone. A signed, still-current answer clears its finding there, and in the late-finding and cap checks only at the severity the rows RECORD for that id (a late finding's own severity; a cap id's from the signed refusals at the current hash) — a missing or unknown one is never cleared by an answer. An answer counts only while BOTH the source and the answering file have the row's hashes (a change to either makes it stale), and it may clear a critical. Otherwise the file stays `unreviewed`. The close writes one signed `gate.review_waived_all {block, file, content_hash, findings, by}` row per such file just before `block.close`: `findings` are the sorted waived ids, `by: human` when every clearing waiver is the human's, else `autopilot`; `answered` lists the answered ids (only when there is one); with every id answered, `findings` is `[]` and `by` is null. When an id is both waived and answered, a human waiver is reported as the waiver; otherwise the answer is listed under `answered` instead of an autopilot waiver. The close line prints only the parts that apply: `block <id>: <file> reviewed_by_waiver (<ids>; by <human|autopilot>)`, `reviewed_by_waiver (W1; by human; answered C1)`, or `reviewed_by_waiver (answered C1, C2)` (ids sorted and joined by `, `). A refused close writes no such row, and a close where no file needs it writes none.
|
|
88
|
+
- **A fully waived or answered file closes with a `gate.review_waived_all` row.** A file with no approval for its current hash counts as reviewed — `reviewed_by_waiver`, never approved — when a signed `review.round` exists for that hash (a repeated stop or an `unavailable` result never counts), the latest refusal row for the hash is signed, carries `content_hash` equal to that hash (a refusal row with no `content_hash` still refuses) and names at least one finding, and every finding it names and every open id of the file's unsuperseded `review.cap` rows is waived or answered. A cap row adds no ids when a verified human or agreed agent `review.reopened` row follows it and a signed `review.round` of the current hash follows the reopen. A critical waiver counts only `by: human`; an autopilot waiver counts only when `autopilotWaiverProblem` finds nothing, and only for a finding a signed refusal row at that hash records as warning or nit (or records with no severity at all). A cap id no refusal row records a severity for, or a cap row whose `open` is not a list of ids, is the human's alone. A signed, still-current answer clears its finding there, and in the late-finding and cap checks only at the severity the rows RECORD for that id (a late finding's own severity; a cap id's from the signed refusals at the current hash) — a missing or unknown one is never cleared by an answer. An answer counts only while BOTH the source and the answering file have the row's hashes (a change to either makes it stale), and it may clear a critical. Otherwise the file stays `unreviewed`. The close writes one signed `gate.review_waived_all {block, file, content_hash, findings, by}` row per such file just before `block.close`: `findings` are the sorted waived ids, `by: human` when every clearing waiver is the human's, else `agent` when an agent waiver cleared a finding, else `autopilot`; `answered` lists the answered ids (only when there is one); with every id answered, `findings` is `[]` and `by` is null. When an id is both waived and answered, a human waiver is reported as the waiver; otherwise the answer is listed under `answered` instead of an autopilot waiver. The close line prints only the parts that apply: `block <id>: <file> reviewed_by_waiver (<ids>; by <human|autopilot|agent>)`, `reviewed_by_waiver (W1; by human; answered C1)`, or `reviewed_by_waiver (answered C1, C2)` (ids sorted and joined by `, `). A refused close writes no such row, and a close where no file needs it writes none.
|
|
82
89
|
- **Not exits.** `block attempt` never resets review state: it only raises the attempt counter (a coder may run it), and a stopped file stays stopped, with no session. New content without a reopen row still gets the cached stop, with no session. `review-file --fresh` (§1) re-runs only a result that never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); on a recheck stop it refuses `fresh-needs-reopen`. A `tampered` state (step 8) is not a stop and gets no exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
83
90
|
6. **Block-close sweep**: `forge block close` batches the `late_findings` per file to one L3 judge ruling (`fix_now | nit`); a `fix_now` opens a round on that file at the block's current level and counts toward its cap. The gate counts a ruling only when it is `nit` on a warning or nit finding; a critical late finding stays open until the human waives it.
|
|
84
91
|
7. Ledger: one `review.round` row per round with `open_before`, `closed`, `new_in_hunks`, `late`, `open_after`; `forge report` prints rounds per file and rounds that stalled.
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
## §2 The forbidden list — one list, rendered per CLI at spawn, grepped from every transcript
|
|
8
8
|
|
|
9
|
-
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload`, `block waive` and `block
|
|
9
|
+
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload`, `block waive`, `block reopen`, `block accept-hit`, `block unwaive` and `block ack-waivers` from a coder (also via `npx`) · `--remove-worktree` from a coder (B62a) · any write under `.code-forge/reviews/` or `.code-forge/queue/*.done`.
|
|
10
10
|
|
|
11
|
-
Rendered into every coder spawn as the CLI's own deny rules (path rules widen a directory to `<dir>/**`; a read-write path denies read, edit and write); carried verbatim in the brief; refused by the package's own process spawner; and at block close the transcript (captured stdout, the harness log, or the Solo output) is grepped for the forbidden verbs **and paths
|
|
11
|
+
Rendered into every coder spawn as the CLI's own deny rules (path rules widen a directory to `<dir>/**`; a read-write path denies read, edit and write); carried verbatim in the brief; refused by the package's own process spawner; and at block close the transcript (captured stdout, the harness log, or the Solo output) is grepped for the forbidden verbs **and paths**. A hit fails the gate as `rule_break`. Only the human can accept a false hit with `block accept-hit`, after terminal confirmation. That verb is also coder-forbidden. It writes one signed `rule_break.accepted` row for the run, block, rule and SHA-256 of the exact transcript line text. Close verifies the signature and matches by line content, so moving the line does not invalidate it. Unsigned, tampered or other-run or other-block rows never count. A ledger `rule_break` row still refuses. Where a CLI cannot load a rules file, `doctor` says so and the transcript grep remains the detector.
|
|
12
12
|
|
|
13
13
|
## §3 Signed ledger rows — the threat model, stated honestly
|
|
14
14
|
|
|
15
|
+
`block unwaive` and `block ack-waivers` are human-only and coder-forbidden with both `forge` and `code-forge` prefixes. Both require a yes at a terminal and have no `--yes`. No terminal exits 2 with no row. A no, cancel or pre-check refusal writes nothing. Only verified human undo and acknowledgement rows count at close.
|
|
16
|
+
|
|
15
17
|
**Mechanism.** A per-run HMAC key (32 random bytes) lives in `~/.code-forge/runs/<run>.key`, mode 0600, outside the workspace; `forge run start` generates it, the worker and `forge block close` read it, and it is never placed in a coder's environment or workspace. Gate-relevant rows carry `mac`; a verification failure is a `ledger.tamper` row and the block is `stopped`. Key rotation = a new run.
|
|
16
18
|
|
|
17
|
-
**What it defends against.** A coder that never called `review-file` and appends or edits rows or result files with its ordinary tools — no valid MAC, refused. A coder that edits a review result after the worker wrote it — the MAC no longer matches. A stale approval reused after the file changed — rows are keyed by content hash. A coder that starts its own worker to sign rows — the pinned pid and start time in the run record no longer match, `worker.replaced`, block stopped. A coder that waives its own finding — `block waive` is forbidden
|
|
19
|
+
**What it defends against.** A coder that never called `review-file` and appends or edits rows or result files with its ordinary tools — no valid MAC, refused. A coder that edits a review result after the worker wrote it — the MAC no longer matches. A stale approval reused after the file changed — rows are keyed by content hash. A coder that starts its own worker to sign rows — the pinned pid and start time in the run record no longer match, `worker.replaced`, block stopped. A coder that waives its own finding — `block waive` is forbidden. Waivers need a valid signature from the run key. The orchestrator prints the command for the human paths of `block reopen` and `block waive`. The orchestrator may run `block reopen --yes` with evidence and agreement from a fresh judge, at most once per block and file in a run. The orchestrator may run `block waive --yes` only for a recorded warning or nit finding with evidence and agreement from a fresh judge. A coder never runs either verb. The human paths ask for a yes at a terminal. Without a terminal they exit 2 and record nothing (B80). For `block reopen` without `--yes`, the message is `block reopen needs the human at a terminal to confirm; nothing was recorded (--yes works only with evidence and agreement from a fresh judge)`. Under an active autopilot grant the human paths still prompt. Only `block close --no-require-reviews` is refused under a grant. Autopilot's own waive path, `forge autopilot waive` (written `by: autopilot`), needs no terminal.
|
|
18
20
|
|
|
19
21
|
**What it does not defend against, and the product does not claim.** A coder running as the same OS user with a shell can read the key file and mint a MAC, or edit the run record. **The honest limit of the `block waive` / `block reopen` prompt:** the prompt checks only that stdin and stdout are terminals. It does not prove a human: a process that runs the verb in a pseudo-terminal (`script`, `expect`, a pty library, or an agent shell that is itself a pty, as Solo processes may be) passes the check and can answer yes, and a same-user process can read the run key and sign a row. The signer is a same-user boundary only. Within one OS user there is no boundary that a determined bypass-permissions process cannot cross; the deny rules make it a deliberate, named rule break rather than an accident, and the transcript grep makes the attempt visible (`rule_break: key_path`). `doctor` prints `signer: same-user boundary only` on every full run so nobody reads "signed" as "unforgeable".
|
|
20
22
|
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.
|
|
13
|
+
PINNED_VERSION="0.14.0"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|