@codedology/code-forge 0.7.7 → 0.9.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 (69) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +68 -2
  3. package/docs/README.md +2 -1
  4. package/docs/reference/blocks.json +390 -1
  5. package/docs/reference/config.md +26 -8
  6. package/package.json +1 -1
  7. package/schema/code-forge.schema.json +81 -4
  8. package/skill/SKILL.md +6 -6
  9. package/skill/references/code.md +45 -7
  10. package/skill/references/ledger.md +8 -4
  11. package/skill/references/proof.md +44 -3
  12. package/skill/references/review.md +25 -5
  13. package/skill/references/security.md +1 -1
  14. package/skill/scripts/forge +1 -1
  15. package/skill/templates/coder-brief.md +19 -2
  16. package/src/autopilot/binnacle.mjs +21 -3
  17. package/src/cli/block.mjs +427 -41
  18. package/src/cli/context.mjs +175 -0
  19. package/src/cli/gates.mjs +125 -15
  20. package/src/cli/ledger.mjs +46 -2
  21. package/src/cli/proof.mjs +62 -16
  22. package/src/cli/report.mjs +7 -4
  23. package/src/cli/review-file.mjs +324 -18
  24. package/src/cli/status.mjs +212 -0
  25. package/src/config/validate.mjs +45 -11
  26. package/src/context/pack.mjs +319 -0
  27. package/src/doctor/index.mjs +5 -4
  28. package/src/doctor/local.mjs +91 -3
  29. package/src/engines/sentinel.mjs +27 -6
  30. package/src/gates/detect.mjs +21 -8
  31. package/src/gates/expand.mjs +202 -0
  32. package/src/gates/owned-count.mjs +66 -0
  33. package/src/gates/run.mjs +49 -12
  34. package/src/install/wizard/answers.mjs +34 -5
  35. package/src/install/wizard/flags.mjs +8 -1
  36. package/src/install/wizard/project-file.mjs +2 -0
  37. package/src/install/wizard/run.mjs +6 -0
  38. package/src/install/wizard/steps.mjs +24 -4
  39. package/src/install/wizard/summary.mjs +13 -5
  40. package/src/ledger/export.mjs +1 -1
  41. package/src/ledger/outside-review.mjs +164 -0
  42. package/src/ledger/report.mjs +17 -5
  43. package/src/ledger/write.mjs +7 -1
  44. package/src/proof/export.mjs +93 -10
  45. package/src/proof/lock.mjs +28 -5
  46. package/src/proof/worktree.mjs +482 -0
  47. package/src/review/answers.mjs +445 -0
  48. package/src/review/deletion.mjs +106 -0
  49. package/src/review/engine.mjs +36 -2
  50. package/src/review/fixloop.mjs +25 -15
  51. package/src/review/gate-check.mjs +384 -83
  52. package/src/review/open-findings.mjs +59 -0
  53. package/src/review/reopen.mjs +258 -0
  54. package/src/review/stop-exits.mjs +15 -0
  55. package/src/state/block-end.mjs +290 -0
  56. package/src/state/block.mjs +103 -39
  57. package/src/state/brief-status.mjs +131 -0
  58. package/src/state/config-snapshot.mjs +51 -0
  59. package/src/state/registry.mjs +35 -4
  60. package/src/state/run.mjs +10 -1
  61. package/src/util/breadcrumbs.mjs +1 -0
  62. package/src/util/forbidden.mjs +46 -1
  63. package/src/util/gate-token.mjs +8 -0
  64. package/src/util/tmp.mjs +16 -4
  65. package/src/worker/engine.mjs +230 -24
  66. package/src/worker/loop.mjs +29 -8
  67. package/src/worker/queue.mjs +296 -15
  68. package/src/worker/review-state.mjs +346 -51
  69. package/src/worker/ticket.mjs +46 -6
@@ -130,22 +130,26 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
130
130
  | `caps` | object | | — | |
131
131
  | `caps.coders` | integer | | `2` | min 1 |
132
132
  | `gates` | object | | — | |
133
- | `gates.test` | array<string> \| null | | — | |
134
- | `gates.lint` | array<string> \| null | | — | |
135
- | `gates.types` | array<string> \| null | | — | |
136
- | `gates.format` | array<string> \| null | | — | |
133
+ | `gates.test` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. |
134
+ | `gates.lint` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. |
135
+ | `gates.types` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. |
136
+ | `gates.format` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. |
137
137
  | `gates.extra` | object | | — | |
138
138
  | `gates.extra.secret_scan` | boolean | | `true` | |
139
139
  | `gates.extra.safe_edit` | boolean | | `true` | |
140
140
  | `gates.extra.invariants` | string | | — | |
141
- | `gates.full_suite_threshold_files` | integer | | `10` | min 1 |
141
+ | `gates.full_suite_threshold_files` | integer | | `10` | min 1; A block that owns more files than this runs the full suite for a {test_files} gate (reason threshold), not its related tests. Owned files are counted this way. A glob or a directory counts the tracked and new (not ignored) files it covers. A path not created yet counts as 1. Outside git each entry counts 1. A git failure runs the full suite. block open --tests names the test files precisely. |
142
142
  | `proof` | object | | — | |
143
143
  | `proof.tiers` | object | | — | |
144
144
  | `proof.tiers.high` | object | | — | |
145
145
  | `proof.tiers.high.paths` | array<string> | | — | |
146
146
  | `proof.tiers.light` | object | | — | |
147
147
  | `proof.extra` | array<string> | | — | |
148
- | `proof.isolation` | enum: `export`, `lock` | | `"export"` | |
148
+ | `proof.isolation` | enum: `export`, `lock`, `worktree` | | `"export"` | export: proof runs in a temporary copy; lock: in the shared tree under the proof lock; worktree (B62a): block open gives each block its own git worktree and branch (proof.worktree). Fixed for the run: run reload refuses to change it. |
149
+ | `proof.worktree` | object | | — | |
150
+ | `proof.worktree.dir` | string | | `"../<repo-name>-worktrees"` | minLength 1; Where the trees go, relative to the repository top level (not the workspace). In the default, <repo-name> is replaced with the basename of the repository top level; the default text is never used as is. The default sits beside the repository, so the main tree never lists the trees as untracked. |
151
+ | `proof.worktree.branch` | string | | `"forge/{run}/{block}"` | minLength 1; The branch template: only {run} and {block} are substituted; the result must pass git check-ref-format --branch. |
152
+ | `proof.worktree.base` | string | | — | minLength 1; The commit-ish the tree starts from; default the block's base (block open --base, else HEAD). |
149
153
  | `proof.export` | object | | — | |
150
154
  | `proof.export.link_dirs` | array<string> | | `["vendor","node_modules"]` | |
151
155
  | `proof.export.copy_untracked` | array<string> | | `[".env",".env.testing"]` | |
@@ -157,8 +161,8 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
157
161
  | `budget.usd` | number | | — | > 0; Per-run spend stop in USD (R6). Before every session spawn the run's estimated spend is checked: one warning at 80%, new sessions refused at 100%. |
158
162
  | `budget.block_cases` | integer | | `80` | min 10 |
159
163
  | `autonomy` | object | | — | |
160
- | `autonomy.coder_may_open_draft_pr` | boolean | | `true` | |
161
- | `autonomy.coder_may_push` | boolean | | `true` | |
164
+ | `autonomy.coder_may_open_draft_pr` | boolean | | `false` | A coder may open a draft PR on its own, under the skill's five-condition rule. Has effect only when coder_may_push is true. False or absent: it never opens a PR. |
165
+ | `autonomy.coder_may_push` | boolean | | `false` | A coder may push its block branch on its own. False or absent: it hands back the branch and the orchestrator (or the human) pushes. |
162
166
  | `commit` | object | | — | |
163
167
  | `commit.trailers` | enum: `none`, `harness` | | `"none"` | |
164
168
  | `wip` | object | | — | |
@@ -174,6 +178,20 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
174
178
  | `keys.<key>` | string | | — | user-defined key |
175
179
  | `tmp` | object | | — | |
176
180
  | `tmp.root` | string \| null | | `null` | |
181
+ | `context` | object | | — | |
182
+ | `context.rules` | array<string> | | `[]` | Rules files (paths relative to the repository root), in order. Read from the main tree; a missing file or a path outside the repository is refused. |
183
+ | `context.lessons` | string | | — | minLength 1; One lessons-learned file (path relative to the repository root). Cut first when the pack is over max_lines. |
184
+ | `context.commands` | object | | — | |
185
+ | `context.commands.build` | array<string> | | — | minItems 1; The build command (argv), shown POSIX shell-quoted in the pack. |
186
+ | `context.commands.test` | array<string> | | — | minItems 1; The test command (argv), shown POSIX shell-quoted in the pack. |
187
+ | `context.commands.lint` | array<string> | | — | minItems 1; The lint command (argv), shown POSIX shell-quoted in the pack. |
188
+ | `context.commands.format` | array<string> | | — | minItems 1; The format command (argv), shown POSIX shell-quoted in the pack. |
189
+ | `context.max_lines` | integer | | `300` | min 20; The pack's line cap. Lessons are cut first, then the rules files from the last one backwards, each cut section ending in [cut: N lines dropped]; the header and the commands are never cut. |
190
+ | `context.require_coder_tests` | boolean | | `false` | block close refuses unless --report carries the block's COMPLETE sentinel with a `TESTS <command> exit=0` line before it, and writes a signed coder.tests row. Authority, unlike the rest of `context`: read from the run-start snapshot only, and `run reload` refuses a change to it. Reviews waived (--no-require-reviews or review.external) do not lift it. |
191
+ | `hooks` | object | | — | |
192
+ | `hooks.on_block_end` | object | | — | |
193
+ | `hooks.on_block_end.argv` | array<string> | yes | — | minItems 1; The command as an argv list (no shell): at least one item, and every item a non-empty string (an empty argument is refused). argv[0] is a program on PATH or a path. |
194
+ | `hooks.on_block_end.timeout_s` | integer | | `60` | min 1; max 600; Seconds before the hook's process group is killed (the hook.ran row then says timed_out). |
177
195
  | `autopilot` | object | | — | |
178
196
  | `autopilot.min_confidence` | number | | `0.7` | min 0; max 1; B46: the lowest confidence at which the autopilot delegate's answer is acted on; below it the question goes to the owner. |
179
197
  | `telemetry` | const `"off"` | | `"off"` | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codedology/code-forge",
3
- "version": "0.7.7",
3
+ "version": "0.9.0",
4
4
  "description": "Plan/harden/code/review orchestration CLI and Agent Skill for cross-model, parallel, evidence-gated feature delivery.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -205,7 +205,12 @@
205
205
  "invariants": { "type": "string" }
206
206
  }
207
207
  },
208
- "full_suite_threshold_files": { "type": "integer", "minimum": 1, "default": 10 }
208
+ "full_suite_threshold_files": {
209
+ "type": "integer",
210
+ "minimum": 1,
211
+ "default": 10,
212
+ "description": "A block that owns more files than this runs the full suite for a {test_files} gate (reason threshold), not its related tests. Owned files are counted this way. A glob or a directory counts the tracked and new (not ignored) files it covers. A path not created yet counts as 1. Outside git each entry counts 1. A git failure runs the full suite. block open --tests names the test files precisely."
213
+ }
209
214
  }
210
215
  },
211
216
  "proof": {
@@ -227,7 +232,17 @@
227
232
  }
228
233
  },
229
234
  "extra": { "type": "array", "items": { "type": "string" } },
230
- "isolation": { "type": "string", "enum": ["export", "lock"], "default": "export" },
235
+ "isolation": { "type": "string", "enum": ["export", "lock", "worktree"], "default": "export", "description": "export: proof runs in a temporary copy; lock: in the shared tree under the proof lock; worktree (B62a): block open gives each block its own git worktree and branch (proof.worktree). Fixed for the run: run reload refuses to change it." },
236
+ "worktree": {
237
+ "type": "object",
238
+ "additionalProperties": false,
239
+ "description": "B62a: used when proof.isolation is worktree. block open runs git worktree add <dir>/<block> -b <branch> <base>; nothing is pushed. Fixed for the run: run reload refuses to change it. Until B62b, gates, reviews and the close check run as under export (from the main checkout) and do not see changes made only in the block worktree.",
240
+ "properties": {
241
+ "dir": { "type": "string", "minLength": 1, "default": "../<repo-name>-worktrees", "description": "Where the trees go, relative to the repository top level (not the workspace). In the default, <repo-name> is replaced with the basename of the repository top level; the default text is never used as is. The default sits beside the repository, so the main tree never lists the trees as untracked." },
242
+ "branch": { "type": "string", "minLength": 1, "default": "forge/{run}/{block}", "description": "The branch template: only {run} and {block} are substituted; the result must pass git check-ref-format --branch." },
243
+ "base": { "type": "string", "minLength": 1, "description": "The commit-ish the tree starts from; default the block's base (block open --base, else HEAD)." }
244
+ }
245
+ },
231
246
  "export": {
232
247
  "type": "object",
233
248
  "additionalProperties": false,
@@ -266,8 +281,16 @@
266
281
  "type": "object",
267
282
  "additionalProperties": false,
268
283
  "properties": {
269
- "coder_may_open_draft_pr": { "type": "boolean", "default": true },
270
- "coder_may_push": { "type": "boolean", "default": true }
284
+ "coder_may_open_draft_pr": {
285
+ "type": "boolean",
286
+ "default": false,
287
+ "description": "A coder may open a draft PR on its own, under the skill's five-condition rule. Has effect only when coder_may_push is true. False or absent: it never opens a PR."
288
+ },
289
+ "coder_may_push": {
290
+ "type": "boolean",
291
+ "default": false,
292
+ "description": "A coder may push its block branch on its own. False or absent: it hands back the branch and the orchestrator (or the human) pushes."
293
+ }
271
294
  }
272
295
  },
273
296
  "commit": {
@@ -315,6 +338,45 @@
315
338
  }
316
339
  }
317
340
  },
341
+ "context": {
342
+ "type": "object",
343
+ "additionalProperties": false,
344
+ "description": "The coder context pack. `code-forge context pack --run <id> --block <id>` builds one pack per block from it (header, commands, rules, lessons; cut to max_lines), and every review packet carries the rules and lessons in its project-rules digest. The whole key is read from the live config, not the run-start snapshot: it is not authority, and `run reload` accepts a change to it — except require_coder_tests, which is authority, read from the snapshot only and fixed for the run. Without this key packets are unchanged.",
345
+ "properties": {
346
+ "rules": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true, "default": [], "description": "Rules files (paths relative to the repository root), in order. Read from the main tree; a missing file or a path outside the repository is refused." },
347
+ "lessons": { "type": "string", "minLength": 1, "description": "One lessons-learned file (path relative to the repository root). Cut first when the pack is over max_lines." },
348
+ "commands": {
349
+ "type": "object",
350
+ "additionalProperties": false,
351
+ "description": "The project's commands as argv lists, shown to the coder in the pack. code-forge never runs them.",
352
+ "properties": {
353
+ "build": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "The build command (argv), shown POSIX shell-quoted in the pack." },
354
+ "test": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "The test command (argv), shown POSIX shell-quoted in the pack." },
355
+ "lint": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "The lint command (argv), shown POSIX shell-quoted in the pack." },
356
+ "format": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "The format command (argv), shown POSIX shell-quoted in the pack." }
357
+ }
358
+ },
359
+ "max_lines": { "type": "integer", "minimum": 20, "default": 300, "description": "The pack's line cap. Lessons are cut first, then the rules files from the last one backwards, each cut section ending in [cut: N lines dropped]; the header and the commands are never cut." },
360
+ "require_coder_tests": { "type": "boolean", "default": false, "description": "block close refuses unless --report carries the block's COMPLETE sentinel with a `TESTS <command> exit=0` line before it, and writes a signed coder.tests row. Authority, unlike the rest of `context`: read from the run-start snapshot only, and `run reload` refuses a change to it. Reviews waived (--no-require-reviews or review.external) do not lift it." }
361
+ }
362
+ },
363
+ "hooks": {
364
+ "type": "object",
365
+ "additionalProperties": false,
366
+ "description": "Commands code-forge runs on the owner's machine. Read from the run-start snapshot only: `run reload` refuses a change. With hooks.on_block_end set, .code-forge.yml is non-ownable (the same guard as review.external.required): no block may own it, also when hooks.on_block_end is the only key that guards it.",
367
+ "properties": {
368
+ "on_block_end": {
369
+ "type": "object",
370
+ "additionalProperties": false,
371
+ "required": ["argv"],
372
+ "description": "Run once after every block end (block close closes or stops it, block stop), after the done file `<run root>/blocks/<id>.done.json` is written (its absolute path is in CODE_FORGE_DONE_FILE, the empty string when it was not written, and in done_file of close/stop --json) and outside the run lock. No shell; cwd is the repository top; stdin closed; output discarded; env adds CODE_FORGE_RUN, CODE_FORGE_BLOCK, CODE_FORGE_BLOCK_STATUS, CODE_FORGE_DONE_FILE. One signed hook.ran row (block, hook, argv0, exit, signal, timed_out, duration_ms) per block end, also when the hook fails. A failing, missing or timed-out hook only adds a stderr warning. A coder transcript that runs it refuses the close.",
373
+ "properties": {
374
+ "argv": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "The command as an argv list (no shell): at least one item, and every item a non-empty string (an empty argument is refused). argv[0] is a program on PATH or a path." },
375
+ "timeout_s": { "type": "integer", "minimum": 1, "maximum": 600, "default": 60, "description": "Seconds before the hook's process group is killed (the hook.ran row then says timed_out)." }
376
+ }
377
+ }
378
+ }
379
+ },
318
380
  "autopilot": {
319
381
  "type": "object",
320
382
  "additionalProperties": false,
@@ -360,8 +422,23 @@
360
422
  }
361
423
  },
362
424
  "argvOrNull": {
425
+ "description": "An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason.",
363
426
  "anyOf": [
364
427
  { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
428
+ {
429
+ "type": "object",
430
+ "additionalProperties": false,
431
+ "required": ["argv"],
432
+ "properties": {
433
+ "argv": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
434
+ "env": {
435
+ "type": "object",
436
+ "additionalProperties": false,
437
+ "patternProperties": { "^[A-Za-z_][A-Za-z0-9_]*$": { "type": "string" } },
438
+ "description": "Variables added to the gate's environment, NAME: value. code-forge never prints or records an env value. Gate env is not a secret store. Child output is masked only for values of 8 or more characters. Only the names are shown (env_keys); put secrets in the environment or a secret manager."
439
+ }
440
+ }
441
+ },
365
442
  { "type": "null" }
366
443
  ]
367
444
  }
package/skill/SKILL.md CHANGED
@@ -11,14 +11,14 @@ You are the **orchestrator**: the session that invoked this skill. You dispatch,
11
11
 
12
12
  1. `forge validate` — exit 0, or stop and show the refusal (it names the key, never the value).
13
13
  2. `forge doctor --quick` — config, links, key resolution. On the first run of a day run `forge doctor` in full and read its `engine` row and its `signer: same-user boundary only` row (`references/security.md`).
14
- 3. `forge run start` — prints `run <id> started · engine <e> · worker pid <n>`. The engine is `solo`, `harness` or `subprocess` (`engine: auto` picks; `subprocess` only by explicit config). No engine ⇒ the four-line stop text in `references/degraded.md` §1 is printed and you stop. Keep `<id>`: every later verb takes `--run <id>`. A small change with briefs and no plan starts with `forge run start --mode brief` instead (`references/plan.md` §6); the mode is fixed for the run.
14
+ 3. `forge run start` — prints `run <id> started · engine <e> · worker pid <n>`. The engine is `solo`, `harness` or `subprocess` (`engine: auto` picks; `subprocess` only by explicit config). No engine ⇒ the four-line stop text in `references/degraded.md` §1 is printed and you stop. Keep `<id>`: every later verb takes `--run <id>`. Pick the mode here; it is fixed for the run. **Brief mode** — `forge run start --mode brief` (`references/plan.md` §6) — is for one small task or a short list of tasks (a monorepo pass) whose blocks are clear from their briefs: no plan doc, no `forge plan check`, no plan approval; steps 1–3 of §3 below (facts, author, harden) are skipped and `block open` without `--level` takes the rules lane (a signed `decision` row, `source: rules`). **Plan mode** (the default) is for multi-block work that needs `forge plan check` and the owner's approval.
15
15
  4. When the engine is not `solo`, print the banner of `references/degraded.md` §2 once.
16
16
 
17
17
  ## §1 Invocation and state
18
18
 
19
19
  `/code-forge plan|harden|code|full [brief file | plan file]`. No job given ⇒ infer it: a brief ⇒ `plan`; a plan whose header says `status: planned` ⇒ `harden`; `status: hardened` ⇒ `code`. `full` runs the three in order with one human checkpoint after `harden`.
20
20
 
21
- State lives in the plan file's header — `facts → planned → hardened → coding → done` — and in the run record: `forge run status --run <id>` prints the active blocks, `forge ledger tail --slug <slug>` the last rows. Takeover after a lost session: `references/continuity.md`. A config change mid-run (a model swap, the review topology) is `forge run reload --run <id>` — never `block stop` + `run end` + `run start` (`references/continuity.md` §4).
21
+ State lives in the plan file's header — `facts → planned → hardened → coding → done` — and in the run record. To check progress run `forge status` — never read logs or poll transcripts for it. It is read-only and shows each active run (a run of this repository that is not ended; `no active run` when there is none) in ≤ 10 lines: the spend, then each open or in-attempt block's state, level, model, last gate and open findings, `… +<k> more blocks` when they do not fit, and the `closed <a> · stopped <b>` counts; `--run <id>` for one run, `--json` for one line; `forge run status --run <id>` prints the full record, `forge ledger tail --slug <slug>` the last rows. To know a block is done, wait for its done file `<run root>/blocks/<id>.done.json` — `<run root>` is `~/.code-forge/runs/<run>/`; written at every close or stop — or the owner's `hooks.on_block_end` command instead of polling (`references/code.md`). With `hooks.on_block_end` set, `close`/`stop --json` carry the file's absolute path as `done_file`, and the hook's own env (not yours) carries it as `CODE_FORGE_DONE_FILE`. A done file from an earlier end stays after a reopen: count it only when its `at` is later than your current dispatch. If no fresh done file appears by the time the coder agent reports (or stderr shows `warning: done file not written`), run `forge status` once. Takeover after a lost session: `references/continuity.md`. A config change mid-run (a model swap, the review topology) is `forge run reload --run <id>` — never `block stop` + `run end` + `run start` (`references/continuity.md` §4).
22
22
 
23
23
  ## §2 Roles by level (never by model name)
24
24
 
@@ -38,11 +38,11 @@ State lives in the plan file's header — `facts → planned → hardened → co
38
38
  2. `forge author --job plan --brief <brief> --facts <sheet> --out <plan>` — refuses without `--facts` (exit 2) and refuses a sheet older than the brief. The draft's §0 is the sheet, verbatim.
39
39
  3. Harden: `forge author --job harden --brief <brief> --facts <sheet> --draft <plan> --answers <file>` until `questions[]` is empty. The human answers; you relay, never answer for them (`references/harden.md`). Before the exit gate, record each block's lane: `forge jev ask lane --block <id> --plan <plan> --state <file>` (`--rules` without Jev); the plan's level must equal it. Exit gate: `forge plan check <plan>` green, with a lane for every block on its `lanes:` line (`references/plan.md`).
40
40
  Then show the plan to the owner for approval in the harness's plan mode, never as chat text (Claude Code: EnterPlanMode, write the plan, ExitPlanMode); a harness without a plan mode shows it and waits for an explicit yes. No block is dispatched before that approval (`references/plan.md` §4.1).
41
- 4. Per block, in dispatch order, never more than `caps.coders` alive: `forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> --brief <file> --lines <n> [--kind code|docs|contract]` prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>`. Send the pointer per the engine's adapter — `solo` → `references/adapters/solo.md`, `harness` → `references/adapters/claude-code.md`, `subprocess` → `references/adapters/subprocess.md` — and wait for `ACK <sha8> lines=<n>`.
41
+ 4. Per block, in dispatch order, never more than `caps.coders` alive: `forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> --brief <file> --lines <n> [--kind code|docs|contract] [--tests <paths…>]` prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>`. With a `context` key, build the pack right after `block open`, before the pointer goes out: `forge context pack --run <r> --block <id>`; send the pack's text with the pointer and with every later coder task (`references/code.md` §3). Send the pointer per the engine's adapter — `solo` → `references/adapters/solo.md`, `harness` → `references/adapters/claude-code.md`, `subprocess` → `references/adapters/subprocess.md` — and wait for `ACK <sha8> lines=<n>`.
42
42
  5. The coder's first message is its **facts diff**, then its forecast (cases and lines, `templates/block-record.md`), then code, file by file: after each file `forge review-file <path> --block <id> --run <r>`, then `forge review-file --wait <ticket>` before the next file. Rounds ≥ 2 re-check only the fix hunks (`references/review.md`).
43
- 6. `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` ends the coder's turn. Its report is a claim sheet: re-measure it (`references/code.md` §5).
44
- 7. `forge block close <id> --run <r> --report <coder report>` is the gate: a report without a `reviewed <path> ticket <id>` line per changed file is FAILED; a signed `review.approved` row per changed file that no later review of the same content overrode (`not_approved`), no `unowned_change`, no `no_changes` (the files are the owned diff plus every owned file reviewed for the block), gates green, every clause covered, forecast vs actual, no `rule_break`. Red ⇒ `references/decisions.md` (S1 `next`, escalation). Green ⇒ one commit for the block in the project's house style; `forge block rebase <id> --run <r>` first when HEAD moved.
45
- 8. `forge run end --run <r>` when every block is `done`; `forge report --slug <slug>` prints cost per block, escalations, review rounds and the spend per run (`budget.usd`, `references/ledger.md` §3.1).
43
+ 6. `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` ends the coder's turn. Its report is a claim sheet: re-measure it (`references/code.md` §5). Just before the sentinel it carries the `TESTS <command> exit=<n>` line (`references/code.md` §3).
44
+ 7. `forge block close <id> --run <r> --report <coder report>` is the gate: a report without a `reviewed <path> ticket <id>` line per changed file is FAILED; with `context.require_coder_tests` (set before `run start`) `--report` is required, and a report whose LAST `TESTS` line before its COMPLETE sentinel is missing or not `TESTS <command> exit=0` is FAILED too, waived or external reviews or not (`references/code.md` §3); a signed `review.approved` row per changed file that no later review of the same content overrode (`not_approved`), no `unowned_change`, no `no_changes` (the files are the owned diff plus every owned file reviewed for the block), gates green, every clause covered, forecast vs actual, no `rule_break`. Red ⇒ `references/decisions.md` (S1 `next`, escalation). Green ⇒ one commit for the block in the project's house style; `forge block rebase <id> --run <r>` first when HEAD moved.
45
+ 8. `forge run end --run <r>` when every block is `done`; `forge report --slug <slug>` prints cost per block, escalations, review rounds, the outside review rounds per PR next to their code-forge cost, and the spend per run (`budget.usd`, `references/ledger.md` §3.1); when a PR also got an outside review, record it first with `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]…` (`references/ledger.md` §4).
46
46
 
47
47
  ## §4 Hard rules (the headings of `references/rules-core.md`)
48
48
 
@@ -10,19 +10,40 @@ The coder then forecasts **cases** (tests) and **lines**. Compute the floor your
10
10
 
11
11
  ## §3 Dispatch — what the brief contains
12
12
 
13
- `forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] [--kind code|docs|contract] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused; an owned entry with no glob character — `*`, `?`, `{` — owns that path and every path below it, so `src/feature` owns `src/feature/a.swift` but not `src/featureX/a.swift`, and `src/feature` overlaps `src/feature/x.swift` or `src` in another block — unless the entry is a regular file in the workspace at `block open` (or `block claim`): then it owns only itself, recorded as `owned_kinds`, and every later check reads that answer; a glob owns what it matches, and a brace entry `src/{a,b}` is a glob, so `src/a` owns nothing below it; no trailing `/`), the block kind (declared, else detected — `decisions.md` §5), the level (a docs/contract block below `levels.coder_floor_docs`, default L1, is raised to it and the CLI prints the floor), the clauses with their test ids and the forecast, writes the ledger `dispatch` row and prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` (≤ 200 B). Call it before every spawn, in every engine. `--level` may be left out only in a brief-mode run (`plan.md` §6: the rules pick the lane from the owned entries, `--lines` and `proof.tiers.high.paths`, and a signed `decision` row with `source: rules` records it) or when autopilot recorded a level; otherwise its absence is a usage error. A docs block that owns a glob (`docs/**`) must declare `--kind docs`: a glob is judged by its literal ending, so undeclared it counts as `code`. The brief file carries, in this order:
13
+ `forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] [--kind code|docs|contract] [--tests <paths…>] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused; an owned entry with no glob character — `*`, `?`, `{` — owns that path and every path below it, so `src/feature` owns `src/feature/a.swift` but not `src/featureX/a.swift`, and `src/feature` overlaps `src/feature/x.swift` or `src` in another block — unless the entry is a regular file in the workspace at `block open` (or `block claim`): then it owns only itself, recorded as `owned_kinds`, and every later check reads that answer; a glob owns what it matches, and a brace entry `src/{a,b}` is a glob, so `src/a` owns nothing below it; no trailing `/`), the block kind (declared, else detected — `decisions.md` §5), the level (a docs/contract block below `levels.coder_floor_docs`, default L1, is raised to it and the CLI prints the floor), the clauses with their test ids and the forecast, writes the ledger `dispatch` row and prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` (≤ 200 B). Call it before every spawn, in every engine. `--level` may be left out only in a brief-mode run (`plan.md` §6: the rules pick the lane from the owned entries, `--lines` and `proof.tiers.high.paths`, and a signed `decision` row with `source: rules` records it) or when autopilot recorded a level; otherwise its absence is a usage error. A docs block that owns a glob (`docs/**`) must declare `--kind docs`: a glob is judged by its literal ending, so undeclared it counts as `code`. `--tests <paths…>` (same path rules as `--owned`, else `bad-tests`; exact paths only — a file or a directory; `*` and `?` are refused, and so is a path starting with `-`, both a usage error, exit 2; `[` and `{` are allowed, as in `app/[id]/page.test.tsx`) names the block's test files, stored as `test_files` on the block and the `dispatch` row; a `{test_files}` gate runs them (`proof.md` §3). Without it the owned entries that are concrete files (no glob) and look like tests are used; `--tests` is the precise way.
14
+
15
+ Under `proof.isolation: worktree` (B62a, `proof.md` §3), `block open` first makes the block's own worktree. The output then has a second line, `worktree <path> · branch <branch>` (with `· reused` when a second `block open` took the earlier attempt's tree), before the pointer. Give the coder that path as its working directory. Two worktree blocks may own the same path. A refused worktree refuses the open, and nothing is recorded. A second `block open` keeps the tree's base: do not pass a different `--base`. The coder works in that tree path: it edits there, commits on the block's branch, and calls `review-file` there. Run `gates run --run <r> --block <id>` without `--cwd` (another `--cwd` is refused, exit 2). Gates, reviews and the close all measure the tree (B62b, `proof.md` §3): a change made only in the main checkout is not the block's work, and a block whose tree was deleted cannot close until the tree is restored.
16
+
17
+ **`--json` on every block verb (B64a).** `block open|attempt|rebase|claim|close|stop|waive|reopen … --json` prints exactly one JSON line on success instead of the text: `{"block":<id>,"event":<open|attempt|rebased|claimed|closed|stopped|waived|reopened>, …}` with the facts the text prints — open: `level`, `asked`, `source` (`flag`/`rules`/`autopilot`), `kind`, `attempt`, the full `base_sha`, `tests` (count), `worktree` (`{path, branch, reused}` or null) and `brief` (the pointer or null); attempt: `attempt`; rebase: `old_base`, `new_base`; claim: `path`; waive: `finding`, `file`, `by`; reopen: `file`, `from`, `stop_reason`, `at_seq`, `from_hash`, `to_hash`, `ticket_cleared`, `by`; stop: nothing more; close: `status`, `reason`, `close_event` (or null), plus `worktree {removed, path, branch}` with `--remove-worktree` (on a block with no worktree that flag is a usage error, exit 2). On failure the exit code and stderr are as without the flag and stdout stays empty — except a close the gate refused, which also prints `{"block":<id>,"ok":false,"reason":<text>}`; every other close refusal (usage, not open, a cancelled `--no-require-reviews`, a failed worktree removal) leaves stdout empty. Without `--json` the output is unchanged.
18
+
19
+ **Block end: done file and `on_block_end` hook (B64b).** Every block end — `block close` that closes or stops it (a pin event or `ledger.tamper`) and `block stop` — writes `<run root>/blocks/<id>.done.json` (the run's state folder, `~/.code-forge/runs/<run>/`) atomically after the end's rows, overwritten on a later end; its absolute path is in the hook's `CODE_FORGE_DONE_FILE` and in `done_file` of `close`/`stop --json`. Content: `{block, run_id, status, event, reason, commit, spent_usd, unknown_usd_sessions, findings, at}` — `status` `closed`/`stopped`; `event` `block.close`, `block.stop` or the close's own stop event (e.g. `worker.replaced`, `ledger.tamper`); `reason` null for a plain close; `commit` the HEAD of the block's tree or the repository, null when unreadable; `spent_usd`/`unknown_usd_sessions` from the block's own rows as `forge status` computes them, both null when the ledger cannot be read; `findings` the open review findings, null when the ledger cannot be read or the count fails. It never holds transcript text, diff text or secrets. A failed write changes nothing else and prints `warning: done file not written: <reason>` on stderr. Wait for that file (or the hook) instead of polling `forge status` or the coder's transcript. A done file from an earlier end stays after a second `block open` of the block, so count it only when its `at` is later than the current dispatch; if no fresh done file appears by the time the coder agent reports, run `forge status` once. With `hooks.on_block_end: {argv, timeout_s}` set before `run start` (immutable; `run reload` refuses a change), the CLI runs `argv` once per end — also when a later step such as `--remove-worktree` fails — after the done file and outside the run lock: no shell, cwd = the repository top, stdin closed, output discarded, the process group killed at `timeout_s` (1–600, default 60), with `CODE_FORGE_RUN`, `CODE_FORGE_BLOCK`, `CODE_FORGE_BLOCK_STATUS` and `CODE_FORGE_DONE_FILE` (`""` when the done file was not written) added to its env; one signed `hook.ran` row per end (`ledger.md`). A hook that does not succeed (exit 0) only warns: `warning: on_block_end hook exit N` | `timed out` | `not found` | `signal <NAME>` | `failed to start (<code>)`; if the `hook.ran` row itself cannot be written, one more line `warning: on_block_end hook row not written (<code>)` follows (an error code only, never message text). With the hook set, `close --json` and `stop --json` add `done_file` (the absolute path, or null when it was not written); without it the output is byte-identical to before. The hook is the owner's command and runs outside coders: a coder never runs it (the close gate flags a transcript line that runs its argv[0] basename plus argv[1] when present and not blank — both trimmed, argv[1] one literal token even with spaces inside, which a transcript line split on spaces never matches — as `rule_break … code-forge-on-block-end-hook`), and `.code-forge.yml` is non-ownable (the same guard as `review.external.required`) with the hook set — also when `hooks.on_block_end` is the only key that guards it.
20
+
21
+ **The context pack (B63a).** With a `context` key in `.code-forge.yml` (`rules`, `lessons`, `commands`, `max_lines`), build the block's pack right after `block open`, before the pointer is sent, and again before every later coder task (fix rounds too): `forge context pack --run <r> --block <id>` writes `<run root>/context/<id>.md` and prints its path and `tokens ~<n>` (`--json`: `{"path":…,"tokens":…,"lines":…,"cut":…,"sha256":…}`). The pack file lives under the run root, which a brief never names (see below): paste the pack's **text** into the coder task with the pointer and with every later coder task (or copy the file to a location outside the run root and name that), and keep the run-root path out of the brief. The pack holds, in order: the block header (run, block, level, owned files, the test files when present, the worktree path and branch when present), the project's build, test, lint and format commands as argv text in POSIX shell quoting and redacted like the rules (code-forge never runs them; a control character in a token, a path or a header value is refused), each rules file, then the lessons file, each under its source path (an empty or whitespace-only file gives its heading alone). Over `max_lines` (default 300, minimum 20) the lessons are cut first, then the rules files from the last one backwards, each cut section ending in `[cut: N lines dropped]`; the header and the commands are never cut. Rules and lessons are read from the main tree. A missing file, or a path outside the repository (`..`, an absolute path, a symlink leading out — a broken symlink is judged by where it points), refuses the pack (exit 1, naming the key and the path, nothing written); so does a config without `context` or with a bad `context` value. Each new pack writes a signed `context.pack` row; the same pack again writes none and prints the same output. The key is read from the live config, not the run's snapshot: it is not authority, and `run reload` takes a change to it — except `context.require_coder_tests` (the close check for the TESTS line, below), which is fixed for the run. The coder may run the verb too, but a pack the coder builds (and its path) is for the coder's own use only, never what the orchestrator sends; `.code-forge.yml` stays non-ownable.
22
+
23
+ The brief file carries, in this order:
14
24
 
15
25
  1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
16
26
  2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
17
- 3. The file loop: *after you finish each file in `owned_files` (tests written and green) run `forge review-file <path> --block <id> --run <r>`; before the next file run `forge review-file --wait <ticket> [--max 90s]` and act on the packet.* A `worker_down` answer ⇒ print `===BLOCK <id> FAILED: worker down===` (never approval).
18
- 4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines), the forbidden list verbatim (`security.md` §2), and the sentinel contract: `===BLOCK <id> COMPLETE===` followed by the test summary, `git diff --stat` and one `reviewed <path> ticket <ticket-id>` line per changed file, or `===BLOCK <id> FAILED: <reason>===`.
27
+ 3. The file loop: *after you finish each file in `owned_files` (tests written and green) run `forge review-file <path> --block <id> --run <r>`; before the next file run `forge review-file --wait <ticket> [--max 90s]` and act on the packet.* A `worker_down` answer ⇒ print `===BLOCK <id> FAILED: worker down===` (never approval). A cached result that never counted (`stale`, `unavailable`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`) is run again with `forge review-file <path> --block <id> --run <r> --fresh` (B75); anything else (a counted round, `review-state-lost`, or an unknown result) answers `fresh-needs-reopen` (`fresh-tampered` for a tampered state), and the coder stops and reports. A finding whose fix belongs in another owned file (a missing test, say) is fixed there and sent with `forge review-file <that file> --block <id> --run <r> --answers <ids> --of <source>` (`review.md` §5).
28
+ 4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines) — left out with a `context` key, because the pasted pack carries the rules and lessons — the forbidden list verbatim (`security.md` §2), and the sentinel contract: the `TESTS` line of item 6, then `===BLOCK <id> COMPLETE===` followed by the test summary, `git diff --stat` and one `reviewed <path> ticket <ticket-id>` line per changed file, or `===BLOCK <id> FAILED: <reason>===`.
19
29
  5. **FAILED unless** (`templates/coder-brief.md`): a final report — first attempt or fix round — that does not list the `review-file` ticket id of every changed file counts as FAILED whatever its sentinel says. Pass the report to the gate with `forge block close <id> --run <r> --report <file>`; it refuses `coder report FAILED: no review-file ticket id for <files>`.
30
+ 6. **The green test run.** The coder runs the block's test command and prints one line that is exactly `TESTS <command> exit=<n>` just before `===BLOCK <id> COMPLETE===` — on the first attempt and again, freshly run, in every fix round. `<command>` is the command it ran (not empty); `<n>` is its exit code as a decimal 0–255 with no leading zeros. The line is matched after trimming; a line quoted inside prose, a non-numeric, signed or out-of-range `<n>`, or a leading zero does not count.
31
+
32
+ The brief never mentions `block waive`, `block reopen`, keys, or `~/.code-forge/runs/`. The coder process gets no Jev key (the System 1 model's key, `keys.jev`) and no signer key.
33
+
34
+ **The close check for the TESTS line (B63b).** The gate reads the LAST `TESTS` line in the segment of the block's last sentinel: after the previous sentinel line of any block (an earlier attempt, or another block in a shared log) and before this one; a line after the sentinel is ignored. Set `context.require_coder_tests: true` in `.code-forge.yml` **before `run start`** to make it a close check: it is read from the run's config snapshot only, never the live file, and `run reload` refuses a change to it (`context.require_coder_tests cannot change mid-run`). With it on, `--report` is required, and `block close` refuses (exit 1, the block stays open, no `block.close` row) with one of the messages below. This check runs before the other close checks and only reads the report, so a refusal here writes no row at all — not even the human waiver row of `--no-require-reviews`:
20
35
 
21
- The brief never mentions `block waive`, keys, or `~/.code-forge/runs/`. The coder process gets no Jev key (the System 1 model's key, `keys.jev`) and no signer key.
36
+ - `coder report FAILED: context.require_coder_tests needs --report` — no `--report`;
37
+ - `coder report FAILED: --report cannot be read`;
38
+ - `coder report FAILED: no ===BLOCK <id> COMPLETE=== sentinel` — none for the block, or its last one is FAILED;
39
+ - `coder report FAILED: no TESTS line` — none in that sentinel's segment;
40
+ - `coder report FAILED: TESTS exit=<n>` — `<n>` is not 0.
41
+
42
+ Waived or external reviews (`--no-require-reviews`, `review.external`) do not lift it: they waive reviews, not tests. A close that passes writes a signed `coder.tests {block, command, exit}` row (the command redacted) under the run lock with the close rows, only when every other check passed — after any `gate.reviews_waived {by: config}` row, just before `block.close`. With the key false or absent the brief still asks for the line, but the gate ignores it and writes no row.
22
43
 
23
44
  ## §4 The file loop, from your side
24
45
 
25
- Each `review-file` ticket is served by the worker `forge run start` launched: tools first (the project's gates on that file — red ⇒ `tools_red`, no model review), then S1 `risk`, then the planned depth (`review.md` §2). You watch the ledger, not the coder's output: `forge ledger tail --slug <slug>` shows `review.plan`, `review.round`, `review.done`. Idle is a wake, never proof; the sentinel is the only completion signal (`adapters/*.md`).
46
+ Each `review-file` ticket is served by the worker `forge run start` launched: tools first (the project's gates on that file — red ⇒ `tools_red`, no model review), then S1 `risk`, then the planned depth (`review.md` §2). You watch the run, not the coder's output: `forge status` (B64a) is the progress check — read-only (no ledger row, never the run lock), every run of this repository that is not ended, newest first, with a blank line between runs, at most 10 lines per run: `run <id> · <status> · <plan|brief> · spent $<x>[ of $<budget>]` (money with 2 decimals; `$<x>+?` when some session has no known price, `$?` when the ledger cannot be read), then per open or in-attempt block `<id> <open|attempt-<n>> <level> · <model or -> · gate <pass|fail|none>[ (<gate>)] · findings <n>` (the gate field is `gate pass`, `gate fail (<gate>)` with the failing gate's name, `gate fail` when no gate was named, or `gate none` when no gate ran; findings = the count of open review findings the close gate would refuse today), `… +<k> more blocks` as the last block line when they do not fit, and `closed <a> · stopped <b>` only when either is non-zero — printed LAST, but it keeps its place in the cap ahead of block lines (block lines are dropped first). No such run: `no active run`, exit 0. `--run <id>` shows one run (ended too); an unknown run exits 1, a usage error exits 2. `--json` prints one line, `{"runs":[…]}`, with every open block (no cap, so `more` is always 0), `unknown_usd_sessions`, and per block `gate: {result, name}` (`name` null for `pass`, for `none` and for an unnamed fail). Use it instead of reading logs or polling the coder's transcript: it never prints transcript or diff text. `forge ledger tail --slug <slug>` shows the rows themselves (`review.plan`, `review.round`, `review.done`). Idle is a wake, never proof; the sentinel is the only completion signal (`adapters/*.md`).
26
47
 
27
48
  ## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
28
49
 
@@ -39,7 +60,22 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
39
60
  | no rule break | the transcript grep for forbidden verbs and paths (`--transcript <file>`, else the run record's log); a missing transcript ⇒ a signed `gate.transcript_missing` row and a WARN, never silence |
40
61
  | high tier proven | every changed high-tier file (`proof.md` §1) has a signed, proven red→green row with `red_kind: assertion` for this block whose `covers` names it (light tier: optional), else `unproven <file>`; only the human's `block waive <id> proof --file <path>` clears it |
41
62
  | rows genuine | the MAC of every gate-relevant row verifies; the live worker matches the pinned pid |
42
- | nothing left open | no unruled `late_findings` (a ruling clears only a warning or nit ruled `nit`; a critical late finding needs the human's waiver); no file at `review_cap` without a signed `review.waived` row |
63
+ | nothing left open | no unruled `late_findings` (a ruling clears only a warning or nit ruled `nit`; a critical late finding needs the human's waiver); no stopped file: every stop (`review_cap`, `l3_patch_exhausted`, `split_required`, …) writes a `review.cap` row, and the file holds the close until a signed approval of its current content comes after that row, or every open finding is waived or answered (then it closes with a `gate.review_waived_all` row) |
64
+
65
+ `forge block close <id> --run <r> --remove-worktree` (B62a, worktree isolation only) removes the block's worktree after a successful close. It runs `git worktree remove` without `--force` and keeps the branch.
66
+ - Success prints `block <id> worktree removed: <path> (branch <branch> kept)` and writes a signed `worktree.removed` row.
67
+ - A tree with modified or untracked files prints `block <id> worktree NOT removed: <reason> …` and writes `worktree.remove_failed`. The command exits 1, but the block stays closed.
68
+
69
+ Without the flag the tree stays. On a block without a worktree the flag is a usage error (exit 2). The flag is forbidden to coders: only you remove a tree.
70
+
71
+ **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:
72
+
73
+ 1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then run `forge block reopen <id> --file <path> --run <r> --reason <text>` only after the human says so in chat, with their reason, exactly like `block waive`. Never run it on your own or 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. A `reopen-same-content` refusal means the content is still what the stop reviewed: offer the waive instead.
74
+ 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.
75
+ 3. **`block waive`.** Waive each open finding, after the human says so. A file whose findings are all waived or answered closes with a signed `gate.review_waived_all` row.
76
+ 4. **Re-decompose.** The human decides, and the architect plans new blocks.
77
+
78
+ `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`, 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.
43
79
 
44
80
  `--no-require-reviews` is the human's way out, written as its own audit row. The human runs it themselves at a terminal: it asks for a yes, has no `--yes`, exits 2 unless stdin and stdout are both terminals, lifts `no_changes` and is refused while an autopilot grant is active — so never run it for them; tell them the command when they say so in chat. The owner's other way out is `review.external` in the config before `run start` (`review.md` §1): the gate then skips the review rows and the report's ticket ids and writes `gate.reviews_waived {by: config}`; every other check, `no_changes` included, still applies. A finding counts only under the gate's own configuration; a tautology (an expected value computed by the code under test) is a finding. Reproduce disclosures: a coder that reports its own rule break did the hard part.
45
81
 
@@ -49,7 +85,9 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
49
85
 
50
86
  ## §7 The PR contract — a draft PR only after independent review
51
87
 
52
- With `autonomy.coder_may_open_draft_pr` and `autonomy.coder_may_push` on, a coder may commit, push and open a **draft** PR on its own when all five hold, each pasted as evidence in the PR body: (1) facts diff delivered and no STOP tripped; (2) the block filter and regression anchors green in one run; formatter, static analysis, safe-edit and invariants clean; (3) red→green rows for every new test (`proof.md` §2); (4) every changed file carries a signed `review.approved` row — a stub, a timeout or `review.unavailable` forbids the push; (5) the commit is path-scoped to owned files, in house style, with trailers per `commit.trailers`, and the PR body carries the acceptance verbatim plus the claim sheet. A coder may not mark the PR ready, merge, dismiss a finding, raise a cap or push after a failed review. A GO carries a checksum (plan file, revision, line count) that the coder echoes back; unsent text in a coder's input box is never an instruction. Re-measurement then runs on the pushed sha; the orchestrator marks ready; the human merges — at most `wip.ready_prs` at a time.
88
+ Both `autonomy` keys default to false. Whatever they say, a block's branch is handed back, pushed or put in a PR only when conditions (1)–(4) below hold; the orchestrator pushes only a branch that meets them. **Push off** (`autonomy.coder_may_push` false or absent): the coder never pushes and never opens a PR. It commits on its block branch and hands the branch back with the evidence of (1)–(4) and the claim sheet in its hand-back report; the orchestrator (or the human) pushes and opens the draft PR with the acceptance verbatim plus the claim sheet. **Push on, draft PR off** (`autonomy.coder_may_open_draft_pr` false or absent): when (1)–(4) hold and its commit meets the commit part of (5) the coder may push its block branch, and its evidence goes in the hand-back report (claim sheet), not in a PR body; it opens no PR. The orchestrator (or the human) opens the draft PR from the pushed branch with the acceptance verbatim plus the claim sheet. **Both on:** a coder may commit, push and open a **draft** PR on its own when all five hold, each pasted as evidence in the PR body: (1) facts diff delivered and no STOP tripped; (2) the block filter and regression anchors green in one run; formatter, static analysis, safe-edit and invariants clean; (3) red→green rows for every new test (`proof.md` §2); (4) every changed file carries a signed `review.approved` row — a stub, a timeout or `review.unavailable` forbids the push; (5) the commit is path-scoped to owned files, in house style, with trailers per `commit.trailers`, and the PR body carries the acceptance verbatim plus the claim sheet. A coder may not mark the PR ready, merge, dismiss a finding, raise a cap or push after a failed review. A GO carries a checksum (plan file, revision, line count) that the coder echoes back; unsent text in a coder's input box is never an instruction. Whoever pushed, re-measurement then runs on the pushed sha; the orchestrator marks ready; the human merges — at most `wip.ready_prs` at a time.
89
+
90
+ **An outside review of the PR (B66).** When a reviewer outside code-forge (the `review.external` reviewer, a person, a CI review bot) has finished with a PR, record its rounds and findings: `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...` — name every block the PR carries (`ledger.md` §4). A later call for the same PR replaces the earlier one in `forge report`, whose `outside_review` section sets the rounds next to code-forge's cost for those blocks.
53
91
 
54
92
  **A finding dispositioned out of the block:** only the architect may rule it, and only when severity is warning or lower, every fix site lies outside the owned paths, no shipped UI or API reaches the failure path after merge, and a **named successor block** carries the failure path as its opening clause. The verdict stays recorded; a disposition without a successor clause is a dismissal, and dismissals do not exist.
55
93
 
@@ -4,11 +4,11 @@
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`, `gate.red`, `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}`, `gate.reviews_waived {by: human | config}` (`by: config` from `review.external`), `decision {question: lane, source: rules}` (a brief-mode `block open` without `--level`), `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
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}`, `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}` (signed; B72: the human's `block reopen` of a stopped or lost file — `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, not `by: human` 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 | 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` only when every clearing waiver is by a human, else `autopilot`, and `by: null` only when every id was answered (`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
- 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, 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`. `--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.
11
+ 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, 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, last): 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`). `--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
12
 
13
13
  ## §3 Live views
14
14
 
@@ -24,10 +24,14 @@ Sessions that run at the same time (B41): each session first takes its provider
24
24
 
25
25
  Known limits, accepted: unknown-price sessions and `session.background` coders add **0** to the spent total — record their real cost with `forge ledger add coder --usd <n>`; a background coder's reservation is released at its start row, since its cost is never seen; a session refused because of running sessions' reservations is refused, not queued — run it again once they finish; `forge run status` shows `unknown_usd_sessions` and prints `N sessions have no known price; the budget does not count them` when N > 0. The lock and the reservations live in one process only: two separate code-forge processes starting a session of one run at the same moment can still each pass the check or each write a `budget.warning` row.
26
26
 
27
- ## §4 Calibration — `forge ledger calibration --slug <slug> [--question <id>]`
27
+ ## §4 Outside review rounds — `forge ledger outside-review`
28
+
29
+ When a PR got an outside review (the owner's reviewer, a review bot), record it once the review is done: `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...` — one signed `outside_review` row in the run's project ledger, stdout `outside_review pr <n> · rounds <r> · findings <f> · blocks <ids or ->`. `--pr` > 0, `--rounds` and `--findings` ≥ 0; `--pr`, `--rounds`, `--findings` decimal, no sign, no leading zeros except 0; `--link` an `https://`/`http://` URL, no whitespace, ≤ 2048 characters, redacted before it is stored; `--block` repeats, each a block of the run (unknown ⇒ exit 1 naming the id, nothing written), duplicates collapsed, order kept. Bad flags exit 2 and write nothing; an unknown run exits 1. A second call for the same PR replaces the first in `report` (both rows stay in the ledger). Orchestrator or owner only — never put it in a coder brief.
30
+
31
+ ## §5 Calibration — `forge ledger calibration --slug <slug> [--question <id>]`
28
32
 
29
33
  Per question id and `source`: reliability buckets × outcome, the count of `≥ 0.99` answers later marked wrong, and a threshold recommendation per question — **never applied automatically**; the human edits `thresholds.<question>`. Ground truth arrives from four callers: shadow sampling (`calibration.shadow_rate`; ships at 0 — the code path exists, turn it on by config once outcome rows flow), the block gate's `outcome` rows (`next = complete` then a red gate ⇒ `wrong`; an escalation after a `lane` pick ⇒ `lane.under`), `forge ledger outcome --slug <slug> --pr <n> --ci red|green|reverted` from the shipped CI template, and `forge ledger outcome --slug <slug> --scan-git [--cwd <dir>] [--days <n>]`, which walks `git log` for reverts (the `This reverts commit <sha>` chain, parity-aware) and marks `reverted` on the matching review rows — run by `run start` and on demand, no CI needed.
30
34
 
31
- ## §5 Durability
35
+ ## §6 Durability
32
36
 
33
37
  Rotated at 10 MB; rows are signed only where the gate relies on them; `report --export` snapshots are what anything downstream reads. The ledger is per project slug, and every verb takes the config and the slug from the **project root** (the nearest regular `.code-forge.yml` up to the git top level, else the git top level; outside git never above HOME, and a `.git` at HOME does not count), so a run from a sub-directory still lands in the same file.
@@ -23,12 +23,53 @@ The runner works in the export (§3) or under the lock with a journal: it refuse
23
23
 
24
24
  ## §3 The measurement export (R13) — `proof.isolation: export`, the default
25
25
 
26
- Parallel blocks share one working tree, so nothing is measured there. `forge proof export <block> --run <r>` builds `.code-forge/export/<block>/`: (a) `git archive <base_sha>` extracted; (b) every path the base's `.gitattributes` marks `export-ignore` restored from `git show` — the hazard is real: repositories mark `/.github` and `CHANGELOG.md` that way; (c) every path in `proof.export.copy_untracked` that exists in the main tree added (`.env`, `.env.testing` by default; never anything under `.code-forge/`); (d) the block's owned files at their **current** content — new files included, and an owned file that existed at the base but is deleted in the tree is **removed from the export** too (listed under `removed` in the export's result), so a deletion is measured as a deletion; (e) the dependency dirs in `proof.export.link_dirs` symlinked read-only. Gates, red→green and the test-count delta run there; `--remove` deletes it at block close. A gate command that fails inside the export with a missing-file error is reported as `export.incomplete: <path>` with the sentence *add it to `proof.export.copy_untracked` or set `proof.isolation: lock`* — never as a red test.
26
+ Parallel blocks share one working tree, so nothing is measured there. `forge proof export <block> --run <r>` builds `.code-forge/export/<block>/`: (a) `git archive <base_sha>` extracted; (b) every path the base's `.gitattributes` marks `export-ignore` restored from `git show` — the hazard is real: repositories mark `/.github` and `CHANGELOG.md` that way; (c) every path in `proof.export.copy_untracked` that exists in the main tree added (`.env`, `.env.testing` by default; never anything under `.code-forge/`); (d) the block's owned files at their **current** content — new files included, and an owned file that existed at the base but is deleted in the tree is **removed from the export** too (listed under `removed` in the export's result), so a deletion is measured as a deletion; (e) the dependency dirs in `proof.export.link_dirs` symlinked read-only — except a **Composer vendor dir** (one holding `composer/autoload_real.php`), which is **copied** (listed under `copied` in the export's result): PHP resolves `__DIR__` through symlinks, so a linked `vendor` makes Composer's autoloader load `App\…` and `Tests\…` from the main tree, where another block's half-written file would leak into the measurement. Gates, red→green and the test-count delta run there; `--remove` deletes it at block close. A gate command that fails inside the export with a missing-file error is reported as `export.incomplete: <path>` with the sentence *add it to `proof.export.copy_untracked` or set `proof.isolation: lock`* — never as a red test.
27
27
 
28
28
  **`proof.isolation: lock`** is the fallback for stacks whose tests cannot run from an export: `forge proof lock <block> --run <r>` takes a run-wide lock (Solo `lock_acquire` when present, else a lock file) that pauses other blocks' gate runs (`proof.busy`); the runner works in the shared tree with the journal; `forge proof unlock <block> --run <r>` releases it. The ledger flags `isolation: lock`.
29
29
 
30
- Test-count delta = the count on the block's filtered run in the export minus the same filter at `base_sha` (a second export, cached per base). The full suite runs only above `gates.full_suite_threshold_files`, also in the export.
30
+ **`proof.isolation: worktree` (B62a)** gives each block its own git worktree on its own branch. `forge block open` runs `git worktree add <dir>/<block> -b <branch> <base>` before anything is recorded. It stores `worktree {path, branch, base_sha}` on the block (the path is absolute), writes a signed `worktree.created {block, path, branch, base}` row before `dispatch`, and prints `worktree <path> · branch <branch>` between the open line and the pointer. The keys are under `proof.worktree`:
31
+ - `dir`: where the trees go, resolved from the repository top level (not from the workspace, even when the workspace is a subdirectory). The default is `../<repo-name>-worktrees`, where `<repo-name>` is replaced with the basename of the repository top level: outside the repository, so the main tree never shows the worktrees as untracked files. A `dir` through a symlink is recorded by its realpath.
32
+ - `branch`: a template, default `forge/{run}/{block}`. Only `{run}` and `{block}` are substituted, and the name must pass `git check-ref-format --branch`.
33
+ - `base`: the commit-ish the tree starts from. The default is the block's base (`--base`, else HEAD).
34
+
35
+ The open is refused (exit 1, `…; nothing was recorded`: no block, no row, no dispatch, no tree, no branch) for a block id that is not one plain path segment, an invalid branch name, a branch that already exists (checked again just before `git worktree add`, so a branch made after the plan is refused and kept, never deleted), something already at `<dir>/<block>`, a base that is not a commit, or a failing `git worktree add` (git's reason is named; a tree git registered anyway and a branch the failed add made are removed again, and anything that cannot be removed is named: `…; nothing was recorded, but left behind: <what> (<git's reason>) — remove it by hand`). If the open fails after the tree was made (a ledger row or the run record cannot be written), the tree is removed again (`git worktree remove --force`) with the branch the open made; the open's own error is shown, and anything that could not be removed is added to it: `Removing the worktree of the failed open left behind: the worktree <path> (<reason>); the branch <branch> (<reason>) — remove them by hand.`
36
+
37
+ A re-open of the same block (`--attempt`) reuses its worktree and branch as they are, printing `· reused` with no new `worktree.created` row. The block's base is then the tree's `base_sha`, not HEAD; a `--base` that names another commit is refused (exit 1, nothing recorded). `block rebase` is refused for a block with a worktree (exit 1): its base is the base of its tree. A tree still registered at the path but on another branch or a detached HEAD refuses the re-open (`worktree <path> is on <branch | a detached HEAD>, not <branch>`). A tree that is gone — removed by `--remove-worktree`, or its directory deleted by hand (git then lists it `prunable`; the open runs `git worktree prune`) — is added back at the same path on the same branch, with a new `worktree.created` row.
38
+
39
+ Overlap: two open blocks that each have their own worktree, at different paths, may own the same path (at `block open` and `block claim`). A block without a worktree is checked against every other open block exactly as under export, and `.code-forge.yml` is still never an owned file.
40
+
41
+ `forge block close <id> --run <r> --remove-worktree` (orchestrator only, forbidden to coders) runs `git worktree remove <path>` after a successful close. It never uses `--force` and always keeps the branch. A tree whose directory is gone refuses the close itself (see below); one that vanishes after that check (git lists it `prunable`) is pruned with `git worktree prune` and reported as removed (`worktree.removed`, exit 0).
42
+ - Success writes a signed `worktree.removed {block, path, branch}` row.
43
+ - A tree with modified or untracked files is kept. The command writes a signed `worktree.remove_failed {block, path, reason}` row and exits 1, and the block stays closed. It prints `block <id> closed`, then `block <id> worktree NOT removed: <git's reason> — the block stays closed; the tree stays at <path> (commit or clean it, then git worktree remove it)`.
44
+
45
+ Without the flag the tree stays. The flag on a block without a worktree is a usage error (exit 2).
46
+
47
+ `proof.isolation` and `proof.worktree` are fixed for the run: they are read from the run's config snapshot, and `run reload` refuses to change them. Nothing is ever pushed or merged, and no branch is deleted except one a failed open made itself.
48
+
49
+ **Where a worktree block's work is measured.** Gates, reviews and the close check of a block with a worktree run in its tree, never in the main checkout:
50
+ - `forge gates run --run <r> --block <id>` runs every gate with cwd = the workspace's place in the tree (leave `--cwd` out for a worktree block): the tree root when the workspace is the repository top, else the workspace's subdirectory inside the tree. `{test_files}` resolves the tests in the tree, and `gates.full_suite_threshold_files` counts the owned files in the tree. The gates themselves still come from the main tree's `.code-forge.yml`. The `gate.red` and `gate.done` rows and the JSON output carry `worktree: <path>`. The only `--cwd` accepted is that same place in the tree (compared by realpath), which is the same as no `--cwd`; any other `--cwd` is refused (exit 2): `gates run: block <id> of run <r> works in its worktree <path>, not in --cwd <dir> — drop --cwd (or give the worktree); no gate was run`. Any `--cwd=<dir>` (one argument with `=`, which is not read as `--cwd`) is refused too (exit 2), whatever it names: `gates run: block <id> of run <r> works in its worktree <path>, not in --cwd=<dir> — drop it (--cwd=<dir> is not read as --cwd); no gate was run`. For a block without a worktree, `--cwd` and `--cwd=<dir>` behave as before. A recorded tree that is gone, not a directory, lacks the workspace's subdirectory, or is not on the block's branch (another branch, a detached HEAD, or a plain directory), or is malformed (no absolute path, a branch that is empty or has control characters, a `base_sha` that is not 40 or 64 lowercase hex) refuses the run (exit 1): `gates run: block <id> of run <r>: <reason>; no gate was run`, e.g. `the worktree <path> is gone (removed or moved)`. `gates run` takes no count at `base_sha`: there is no test-count delta for a worktree block.
51
+ - `forge review-file`, from the main tree or from inside the tree, queues in the main tree's `.code-forge/queue`, where the worker runs. The file must lie inside the tree: a path with a `..` segment, an absolute path, or a symlink that leads out of the tree is refused (exit 1, nothing queued) with `{"status":"refused","reason":"bad-path","message":"…"}`. The content hash, the diff against the tree's `base_sha`, the peers' diffs and the fix hunks are read in the tree the run record names (never a path from the ticket). The ticket and the answer carry `tree: <path>` (the realpath), and the tree is part of the ticket id, so the same bytes in the main tree and in the tree are two tickets. A repeat request answers with the tree it was given, never with a path stored in the ticket. The worker reads the tree from the run record; a ticket whose `tree` is not that tree, or a ticket with no `tree` for a block that has one, is `review.unavailable` with `reason: worktree-mismatch` (paths compared by realpath). A block the run record gives no worktree is hashed in the main tree, wherever `review-file` is called (also from inside another block's tree), and its ticket carries no `tree`; only a run record that cannot be opened here (no such file, no permission) falls back to the tree `review-file` is called from. The packet's facts slot says `Worktree: the file is reviewed as it is in the block's worktree <path> (branch <branch>), against the block base.`. A file changed only in the main tree is never seen. A recorded tree that is gone, off its branch (`worktree-branch-mismatch`) or malformed, or a run record that exists but cannot be read (`run-record-unreadable`), refuses `review-file` (exit 1, nothing queued): `{"status":"refused","reason":"worktree-missing","message":"block <id>: the worktree <path> is gone (removed or moved); nothing was queued"}`. A tree that goes away after the ticket was queued makes it `review.unavailable` with `reason: worktree-missing` (`worktree-malformed` for a bad record, `worktree-branch-mismatch` off the branch), which is retryable.
52
+ - The `reason` for each refusal (gates run, block close, proof, `review-file`, and the worker's `review.unavailable`): `worktree-missing` the tree is gone; `worktree-not-dir` its path is a file; `worktree-malformed` the record has no absolute path, a path or branch with control characters, an empty branch, or a `base_sha` that is not 40 or 64 lowercase hex; `worktree-branch-mismatch` the tree is on another branch, detached, or a plain directory; `worktree-foreign` the path is a checkout of another repository (its git common dir differs from the workspace's), even on a branch of the same name; `worktree-workspace-missing` the tree lacks the workspace's subdirectory; `git-failed` git could not run (timed out, could not start, killed), with git's reason; `run-record-unreadable` the run record exists but cannot be parsed or names no workspace; `worktree-mismatch` (worker only) the ticket's `tree` is not the recorded one; `bad-path` (`review-file` only) the file leaves the tree.
53
+ - `forge block close` runs no gate (for any block), so no gate runs outside the tree at close: a worktree block's gates are `gates run --block <id>`, which runs in the tree. It measures the changed, unowned and reviewed files in the tree against its `base_sha`, committed on the branch and uncommitted alike. A change in the main tree or in another block's tree never counts. In the tree nothing is exempt: the main tree's dirty files at open (`tree_at_open`) excuse nothing, and a file a sibling open block owns is unowned too (`block <id> open: unowned_change <file>`, exit 1), since a sibling's work is in its own tree. When no owned file differs but owned files have review rows, the WARN says the work was likely `done outside the block's worktree (in the main checkout or another tree)`. A tree that is off its branch or malformed refuses the close too (exit 1, `block close: block <id>: <reason>, so its work cannot be measured; nothing was recorded`). A tree that is gone refuses the close (exit 1): `block <id>: its worktree <path> is gone (removed or moved), so its work cannot be measured — restore it on branch <branch> (git worktree prune, then git worktree add <path> <branch>) and close again; nothing was recorded`.
54
+ - `forge proof export <id>` and `forge proof red-green <id>` read the block's changed files, the `--test` file and the base in the tree (the workspace's place in it, against the tree's `base_sha`). The mutation proof runs in an export built FROM the tree, never in the tree itself: the export is written to the main tree's `.code-forge/export/<id>` (where `export --remove` and `proof restore` look), so the tree's files and `git status` are exactly as they were after the proof. This was chosen over measuring in the tree under the proof lock, because a crash mid-proof could leave a reverted source in the tree. The config (`proof.export.link_dirs`, `copy_untracked`) still comes from the main tree's `.code-forge.yml`; the link dirs and untracked files are taken from the tree. The `proof` rows are `isolation: export` and carry `worktree: <path>`, even if the main tree's `.code-forge.yml` was later edited to `isolation: lock` (no proof lock is asked for). A recorded tree that is gone, off its branch or malformed refuses (exit 1, no row): `proof <verb>: block <id> of run <r>: <reason>; no proof was run`. `proof lock <id>` on a worktree block is refused (exit 2, no lock, no row): `proof lock: block <id> is proven in an export built from its tree; proof lock does not apply`. A block without a worktree under `isolation: lock` still refuses `proof export` (exit 1, `proof export: proof.isolation is lock — use proof lock instead of an export`). `unlock`, `restore` and `export --remove` act only on the run's lock and the export directory, as before.
55
+
56
+ None of them pushes, merges or checks anything out: the work stays on the block's branch, and the main tree's HEAD and index never move. Blocks without a worktree (export, lock) are unchanged.
57
+
58
+ Test-count delta (blocks without a worktree) = the count on the block's filtered run in the export minus the same filter at `base_sha` (a second export, cached per base). The full suite runs only above `gates.full_suite_threshold_files`, also in the export. This delta belongs to the proof/export path; `gates run` never takes it.
59
+
60
+ **Related tests (B60).** A gate argv item that is exactly `{test_files}` (one whole item, never part of one, never the command, never the only item — `validate` refuses those — never a shell string) runs only the block's tests under `forge gates run --cwd <dir> --run <r> --block <id> [--slug <slug>]`: the paths given at `block open --tests` (the precise way), else the owned entries that are concrete files (no glob, no trailing `/`) and look like tests (a `test`/`tests`/`__tests__`/`spec` directory, or a name ending `.test.*`, `.spec.*`, `Test.php`, `_test.go`, or `test_*.py`). When the block owns more files than `gates.full_suite_threshold_files` (default 10; owned files: globs and directories count the tracked and new (not ignored) files they cover, a path not created yet counts as 1; outside a git repository each entry counts 1, and any other git failure runs the full suite with `reason: threshold`), the item is dropped with `reason: threshold`; when no test file is known, or `gates run` has no `--run/--block`, it is dropped with `reason: no_tests`. Dropped, the gate runs the full suite. The `gate.red` and `gate.done` rows say which: `scope: related` with `test_count`, or `scope: full` with `reason`; with `--run/--block` they also name `run` and `block`. A gate without the item runs exactly as before (no scope is worked out and its rows carry none). A test path that starts with `-` is refused, never passed as an option. Example: `test: [npx, vitest, run, "{test_files}"]`. For a worktree block (`proof.isolation: worktree`), leave `--cwd` out (`gates run --run <r> --block <id>`): the gate runs in the tree, `{test_files}` resolves the tests there, and the threshold counts the owned files in the tree, never the main tree's.
61
+
62
+ **Gate env (B60).** A gate may be `{argv: [...], env: {NAME: value}}`; the variables are added to that gate's environment (`env: {}` is no env). code-forge never prints or records an env value. Gate env is not a secret store. Child output is masked only for values of 8 or more characters. `gates detect`, results, ledger rows, `doctor` and `init` show only the names (`env_keys`); put secrets in the environment or a secret manager (`validate` refuses secret-looking values). A short value (`CI=1`, `MODE=test`) is not masked, so `npm test` stays readable. A block that edits `.code-forge.yml` shows the values in its own diff.
31
63
 
32
64
  ## §4 Verified and unverified stacks
33
65
 
34
- The export is proven on the package's own example repository (a Node library with a `node:test` suite, `/.github` and `CHANGELOG.md` restored from `export-ignore`). A PHP/Pest or Vitest stack is **UNVERIFIED** until a real second consumer runs it; `proof.isolation: lock` is the documented fallback for those until then. `doctor` says which gate commands are on PATH.
66
+ The export is proven on the package's own example repository (a Node library with a `node:test` suite, `/.github` and `CHANGELOG.md` restored from `export-ignore`).
67
+
68
+ **PHP (Pest and PHPUnit, Laravel) under export: VERIFIED** on 2026-10-11 with PHP 8.4.25, Composer 2.10.2 and Laravel Framework 13.35.0 — a fresh `laravel/laravel` skeleton once on PHPUnit 12.5.38 (as shipped) and once on Pest 4.7.8 (`pestphp/pest` + `pestphp/pest-plugin-laravel` added). Re-run it with `CODE_FORGE_LIVE_PHP=1 node --import ./test/helpers/isolate.mjs --test test/proof/laravel-live.test.mjs` (needs `php`, `composer` and the network; skipped without the flag, so `npm test` never needs them; POSIX only — it was run on macOS, not on Windows). Each check builds the skeleton, commits it, adds the block's owned files (a new `app/` class, a unit test, a feature test that writes to the database), breaks a file the block does not own in the main tree (the main tree goes red), builds the export with the default lists and runs the detected gate `php artisan test --compact` there: exit 0, 4 tests passed. What a Laravel export needs:
69
+ - `vendor` in `proof.export.link_dirs` (the default): it is **copied**, not linked, as a Composer vendor dir (§3 (e)). Run `composer install` in the main tree first; the export never installs anything. The copy has a cost on every export: about 5 s and 69 MB for the Laravel skeleton where the file system cannot clone; code-forge asks for a copy-on-write clone (`COPYFILE_FICLONE`), which is near-free where it is supported (APFS, Btrfs, XFS). An absolute symlink inside `vendor` that points into the main tree (a Composer path repository: `vendor/acme/pkg -> <main>/packages/pkg`) is pointed at the same path in the export; relative links and links outside the main tree are kept.
70
+ - `.env` (and `.env.testing` when the project has one) in `proof.export.copy_untracked` (the default): Laravel needs its `APP_KEY`.
71
+ - A database the export can own: SQLite in memory. The skeleton's `phpunit.xml` already sets `DB_CONNECTION=sqlite` and `DB_DATABASE=:memory:`; when yours does not, set them as gate env: `test: {argv: [php, artisan, test, --compact], env: {DB_CONNECTION: sqlite, DB_DATABASE: ":memory:"}}`. A suite that needs a running database server is not isolated by the export — use `proof.isolation: lock`.
72
+
73
+ Detection: `composer.json` + `vendor/bin/pest` is `php-pest`; `composer.json` + `vendor/bin/phpunit` without Pest is `php-phpunit`. With an `artisan` file either runs `php artisan test --compact`; without one, `vendor/bin/pest` or `vendor/bin/phpunit` (a plain PHP project; detected, not run live).
74
+
75
+ A Vitest stack is **UNVERIFIED** until a real second consumer runs it; `proof.isolation: lock` is the documented fallback until then. `doctor` says which gate commands are on PATH.