@codedology/code-forge 0.9.0 → 0.11.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 (62) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +49 -9
  3. package/docs/reference/blocks.json +587 -1
  4. package/docs/reference/config.md +36 -0
  5. package/package.json +2 -2
  6. package/schema/code-forge.schema.json +89 -0
  7. package/skill/SKILL.md +1 -1
  8. package/skill/references/adapters/claude-code.md +1 -1
  9. package/skill/references/adapters/solo.md +2 -2
  10. package/skill/references/adapters/subprocess.md +12 -2
  11. package/skill/references/code.md +14 -7
  12. package/skill/references/ledger.md +2 -2
  13. package/skill/references/proof.md +77 -2
  14. package/skill/references/review.md +39 -7
  15. package/skill/references/security.md +4 -4
  16. package/skill/scripts/forge +1 -1
  17. package/skill/templates/coder-brief.md +6 -6
  18. package/src/autopilot/binnacle.mjs +9 -3
  19. package/src/cli/block.mjs +262 -31
  20. package/src/cli/jev.mjs +22 -3
  21. package/src/cli/keys.mjs +156 -21
  22. package/src/cli/proof.mjs +35 -7
  23. package/src/cli/review-file.mjs +17 -11
  24. package/src/cli/spawn.mjs +44 -3
  25. package/src/config/validate.mjs +320 -5
  26. package/src/doctor/index.mjs +2 -1
  27. package/src/doctor/local.mjs +27 -6
  28. package/src/doctor/probes.mjs +38 -0
  29. package/src/engines/builders/codex.mjs +44 -4
  30. package/src/engines/sandbox-dirs.mjs +257 -0
  31. package/src/keys/store.mjs +128 -21
  32. package/src/proof/junit.mjs +530 -0
  33. package/src/proof/red-green.mjs +57 -9
  34. package/src/proof/red-parse.mjs +2 -0
  35. package/src/proof/runners.mjs +472 -0
  36. package/src/proof/xcresult.mjs +285 -0
  37. package/src/review/engine.mjs +141 -42
  38. package/src/review/fixloop.mjs +75 -23
  39. package/src/review/gate-check.mjs +3 -2
  40. package/src/review/lenses/A.md +2 -0
  41. package/src/review/lenses/B.md +2 -0
  42. package/src/review/lenses/full.md +2 -0
  43. package/src/review/lenses/quick.md +1 -0
  44. package/src/review/lenses/recheck.md +2 -0
  45. package/src/review/packet.mjs +48 -14
  46. package/src/review/paired.mjs +80 -0
  47. package/src/review/reopen.mjs +4 -6
  48. package/src/review/reviewed-lines.mjs +94 -0
  49. package/src/review/route.mjs +232 -0
  50. package/src/review/schema-fallback.mjs +77 -22
  51. package/src/review/session-retry.mjs +44 -9
  52. package/src/review/test-quality.mjs +201 -0
  53. package/src/review/verb-forms.mjs +39 -0
  54. package/src/session/spawn.mjs +39 -2
  55. package/src/state/block-end.mjs +10 -6
  56. package/src/state/block.mjs +13 -3
  57. package/src/state/config-snapshot.mjs +39 -0
  58. package/src/state/run.mjs +4 -0
  59. package/src/util/forbidden.mjs +2 -2
  60. package/src/worker/engine.mjs +43 -15
  61. package/src/worker/loop.mjs +98 -15
  62. package/src/worker/ticket.mjs +9 -1
@@ -108,6 +108,25 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
108
108
  | `review.min_tokens_out` | integer | | `120` | min 1; The stub guard: a review answer with fewer output tokens is unavailable (too_short). A clean pass (passed: true, no findings, a non-blank summary) needs only min(this, 40), whatever the diff size (B53). |
109
109
  | `review.batch_small_files` | boolean | | `true` | |
110
110
  | `review.session_timeout_s` | integer | | `300` | min 1 |
111
+ | `review.routes` | array<object> | | — | B90: size-based review routing. min_lines and model are required. A rule's level decides which sessions it changes: an L2 rule (level: L2, the default) changes only the reviewers' model, and in consensus mode only slot A (slot B stays review.second_levels.L2); an L3 rule changes only the judge's model. The count is the file's lines at the reviewed state (the bytes in the review packet): newline bytes, plus one for a last line without one; a deleted file is never routed. Rules compete only within their level: among the rules of one level, the largest min_lines at or under the count wins; the same level and min_lines twice is refused. Left out, an optional key gets: provider the top-level provider (the rule replaces the level entry, so the level's own provider is not inherited; validation checks the rule with that same provider); effort none; fallback none; session_timeout_s nothing (the rule does not count in the timeout); second review.second_levels for the rule's level. The timeout is one value for every session of the ticket: the largest session_timeout_s among the matched rules that set one (a matched rule without one is left out of that max), else review.session_timeout_s. The second-level try uses the rule's second, except for an L2 rule in consensus mode. The worker writes one signed review.route row per routed level before the first session, never twice for one ticket; its second_model is null when the rule's second is not used, which includes an L2 rule in consensus mode, because slot B is not the rule's. run reload accepts a change. |
112
+ | `review.routes[].min_lines` | integer | yes | — | min 0; B90: the rule applies to a file with at least this many lines (0: every file that is not deleted). |
113
+ | `review.routes[].level` | enum: `L2`, `L3` | | `"L2"` | B90: the level the rule replaces, L2 (the reviewers) or L3 (the judge). |
114
+ | `review.routes[].model` | string | yes | — | minLength 1 |
115
+ | `review.routes[].effort` | string | | — | |
116
+ | `review.routes[].provider` | enum: `anthropic`, `openai`, `xai` | | — | |
117
+ | `review.routes[].fallback` | array<object> | | — | |
118
+ | `review.routes[].fallback[].provider` | enum: `anthropic`, `openai`, `xai` | yes | — | |
119
+ | `review.routes[].fallback[].model` | string | yes | — | minLength 1 |
120
+ | `review.routes[].fallback[].effort` | string | | — | |
121
+ | `review.routes[].session_timeout_s` | integer | | — | min 1; B90: a matched rule's timeout; every session of the ticket runs at the largest one among the matched rules that set one. Unset: the rule does not count in that max (review.session_timeout_s applies only when no matched rule sets one). |
122
+ | `review.routes[].second` | object | | — | |
123
+ | `review.routes[].second.model` | string | yes | — | minLength 1 |
124
+ | `review.routes[].second.effort` | string | | — | |
125
+ | `review.routes[].second.provider` | enum: `anthropic`, `openai`, `xai` | | — | |
126
+ | `review.routes[].second.fallback` | array<object> | | — | |
127
+ | `review.routes[].second.fallback[].provider` | enum: `anthropic`, `openai`, `xai` | yes | — | |
128
+ | `review.routes[].second.fallback[].model` | string | yes | — | minLength 1 |
129
+ | `review.routes[].second.fallback[].effort` | string | | — | |
111
130
  | `review.max_rounds_per_file` | integer | | `4` | min 2; max 6 |
112
131
  | `review.recheck_scope` | enum: `fix_hunks`, `file` | | `"fix_hunks"` | |
113
132
  | `review.late_findings` | enum: `sweep`, `block` | | `"sweep"` | |
@@ -153,6 +172,18 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
153
172
  | `proof.export` | object | | — | |
154
173
  | `proof.export.link_dirs` | array<string> | | `["vendor","node_modules"]` | |
155
174
  | `proof.export.copy_untracked` | array<string> | | `[".env",".env.testing"]` | |
175
+ | `proof.runners` | array<object> | | — | minItems 1; B87: how proof red-green runs a test file that is not a node:test file. proof red-green uses the first entry whose match covers the --test file (list order). With no match, a .mjs/.cjs/.js file uses the built-in node:test command and any other file is refused. Fixed for the run: run start copies the list into the run snapshot, run reload refuses to change it, and with it set no block may own .code-forge.yml. An empty list is refused: leave the key out for no runner. |
176
+ | `proof.runners[].match` | array<string> | yes | — | minItems 1; Globs of the test files this runner runs, relative to the repository (the same glob rules as owned entries). |
177
+ | `proof.runners[].adapter` | enum: `junit`, `swift`, `xcodebuild` | yes | — | How the result is read. junit: the runner writes a JUnit XML report to {report} (PHPUnit, Pest, pytest and other JUnit runners). swift: swift test with --xunit-output {report}. xcodebuild: xcodebuild test with -resultBundlePath {result_bundle}; the result is read with xcrun xcresulttool, and a precheck needs full Xcode and the destination's simulator first. |
178
+ | `proof.runners[].argv` | array<string> | yes | — | minItems 1; The command, an argv array (never a shell string). These items are replaced when an item is exactly the token. junit and swift: {test_file} (the test path), {test_id} (<file>::<case>, or <file> with no case), {test_filter} (the case, regex-escaped), {report} (a fresh report path outside the work dir); {report} and one test token are required. xcodebuild: {workspace}, {project}, {scheme}, {destination} (from xcode), {only_testing} (one item, -only-testing:<test_target>/<Suite>[/<case>]), {result_bundle} (a fresh result bundle path outside the work dir); {scheme}, {destination}, {only_testing} and {result_bundle} are required. A token cannot be the command (argv[0]) or part of a longer item. |
179
+ | `proof.runners[].xcode` | object | | — | |
180
+ | `proof.runners[].xcode.workspace` | string | | — | minLength 1; The .xcworkspace for {workspace}. At most one of workspace and project; neither = a Swift package in the work dir. |
181
+ | `proof.runners[].xcode.project` | string | | — | minLength 1; The .xcodeproj for {project}. At most one of workspace and project. |
182
+ | `proof.runners[].xcode.scheme` | string | yes | — | minLength 1; The scheme for {scheme}. |
183
+ | `proof.runners[].xcode.destination` | string | yes | — | minLength 1; The destination for {destination}, e.g. platform=iOS Simulator,name=iPhone 17. A simulator destination must name a device xcrun simctl lists as available (by name or id, and OS when given); platform=macOS needs none. code-forge never boots, creates or deletes a simulator. |
184
+ | `proof.runners[].xcode.test_target` | string | yes | — | minLength 1; The test target (one name, no /) that {only_testing} starts with; every test in the result must sit under <test_target>/<Suite>. |
185
+ | `proof.runners[].env` | object | | — | |
186
+ | `proof.runners[].timeout_s` | integer | | `600` | min 60; max 7200; Seconds each step (RED, GREEN) may run before it is stopped. |
156
187
  | `budget` | object | | — | |
157
188
  | `budget.block_lines` | integer | | `600` | min 1 |
158
189
  | `budget.block_tokens_soft` | integer | | — | min 1 |
@@ -175,6 +206,7 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
175
206
  | `calibration` | object | | — | |
176
207
  | `calibration.shadow_rate` | number | | `0` | min 0; max 0.5 |
177
208
  | `keys` | object | | — | |
209
+ | `keys.cache_hours` | integer | | `8` | min 1; max 24; B84: how many whole hours a key read from 1Password stays cached in the per-user key store (keys set --op, keys test, jev ask, doctor, the worker). keys set --cache-hours wins over it. A cached key is never trusted for longer than the reader's value. |
178
210
  | `keys.<key>` | string | | — | user-defined key |
179
211
  | `tmp` | object | | — | |
180
212
  | `tmp.root` | string \| null | | `null` | |
@@ -194,4 +226,8 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
194
226
  | `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). |
195
227
  | `autopilot` | object | | — | |
196
228
  | `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. |
229
+ | `coder` | object | | — | |
230
+ | `coder.sandbox` | object | | — | |
231
+ | `coder.sandbox.profile` | enum: `workspace`, `toolchain`, `full` | | `"workspace"` | workspace (the default; no key gives today's argv): `-s workspace-write`, the project tree is the only writable root. toolchain: the same plus one `--add-dir <abs>` per add_dirs entry (a build cache, a package store). full: `-s danger-full-access`, the coder writes anywhere the user can; like the Claude coder, the forbidden list, the transcript grep and the gates are the only limits, and doctor prints a WARN row. Every profile keeps the $TMPDIR and /tmp exclusions in the argv; Codex ignores them under danger-full-access. |
232
+ | `coder.sandbox.add_dirs` | array<string> | | `[]` | Extra writable directories for profile toolchain; under workspace or full they are unused and validate warns (rule sandbox-dirs-unused), and a refused entry is still refused. Each entry is absolute or starts with ~/ (expanded against the process HOME) and becomes an absolute realpath: symlinks are followed, even a dangling one, so a symlink is judged by its target. Refused (rule sandbox-dir-refused, by validate and again by the builder; the message names the key path, never the value): /, the home directory itself, and any directory that is, holds or sits inside ~/.code-forge, ~/.codex or $CODEX_HOME, ~/.ssh, ~/.gnupg, ~/.aws, ~/.config, ~/Library/Keychains, the run temp root (so $TMPDIR or /tmp when it holds it), or the session CODEX_HOME; for example ~/Library is refused because it holds ~/Library/Keychains. |
197
233
  | `telemetry` | const `"off"` | | `"off"` | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codedology/code-forge",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -38,7 +38,7 @@
38
38
  "LICENSE"
39
39
  ],
40
40
  "scripts": {
41
- "test": "CODE_FORGE_NO_LEAK=snapshot node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs && node --import ./test/helpers/isolate.mjs --experimental-test-module-mocks --test 'test/**/*.test.mjs' && CODE_FORGE_NO_LEAK=final node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs",
41
+ "test": "export CODE_FORGE_TEST_SUITE=${CODE_FORGE_TEST_SUITE:-$(node -e 'process.stdout.write(require(\"node:crypto\").randomUUID())')}; CODE_FORGE_NO_LEAK=snapshot node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs && { node --import ./test/helpers/isolate.mjs --experimental-test-module-mocks --test 'test/**/*.test.mjs'; cf_test_status=$?; CODE_FORGE_NO_LEAK=final node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs; cf_leak_status=$?; [ \"$cf_test_status\" -eq 0 ] && [ \"$cf_leak_status\" -eq 0 ]; }",
42
42
  "typecheck": "tsc --checkJs --noEmit",
43
43
  "release": "node scripts/release.mjs",
44
44
  "changelog": "node scripts/changelog.mjs",
@@ -139,6 +139,37 @@
139
139
  "min_tokens_out": { "type": "integer", "minimum": 1, "default": 120, "description": "The stub guard: a review answer with fewer output tokens is unavailable (too_short). A clean pass (passed: true, no findings, a non-blank summary) needs only min(this, 40), whatever the diff size (B53)." },
140
140
  "batch_small_files": { "type": "boolean", "default": true },
141
141
  "session_timeout_s": { "type": "integer", "minimum": 1, "default": 300 },
142
+ "routes": {
143
+ "type": "array",
144
+ "description": "B90: size-based review routing. min_lines and model are required. A rule's level decides which sessions it changes: an L2 rule (level: L2, the default) changes only the reviewers' model, and in consensus mode only slot A (slot B stays review.second_levels.L2); an L3 rule changes only the judge's model. The count is the file's lines at the reviewed state (the bytes in the review packet): newline bytes, plus one for a last line without one; a deleted file is never routed. Rules compete only within their level: among the rules of one level, the largest min_lines at or under the count wins; the same level and min_lines twice is refused. Left out, an optional key gets: provider the top-level provider (the rule replaces the level entry, so the level's own provider is not inherited; validation checks the rule with that same provider); effort none; fallback none; session_timeout_s nothing (the rule does not count in the timeout); second review.second_levels for the rule's level. The timeout is one value for every session of the ticket: the largest session_timeout_s among the matched rules that set one (a matched rule without one is left out of that max), else review.session_timeout_s. The second-level try uses the rule's second, except for an L2 rule in consensus mode. The worker writes one signed review.route row per routed level before the first session, never twice for one ticket; its second_model is null when the rule's second is not used, which includes an L2 rule in consensus mode, because slot B is not the rule's. run reload accepts a change.",
145
+ "items": {
146
+ "type": "object",
147
+ "additionalProperties": false,
148
+ "required": ["min_lines", "model"],
149
+ "properties": {
150
+ "min_lines": { "type": "integer", "minimum": 0, "description": "B90: the rule applies to a file with at least this many lines (0: every file that is not deleted)." },
151
+ "level": { "type": "string", "enum": ["L2", "L3"], "default": "L2", "description": "B90: the level the rule replaces, L2 (the reviewers) or L3 (the judge)." },
152
+ "model": { "type": "string", "minLength": 1 },
153
+ "effort": { "type": "string" },
154
+ "provider": { "$ref": "#/$defs/provider" },
155
+ "fallback": {
156
+ "type": "array",
157
+ "items": {
158
+ "type": "object",
159
+ "additionalProperties": false,
160
+ "required": ["provider", "model"],
161
+ "properties": {
162
+ "provider": { "$ref": "#/$defs/provider" },
163
+ "model": { "type": "string", "minLength": 1 },
164
+ "effort": { "type": "string" }
165
+ }
166
+ }
167
+ },
168
+ "session_timeout_s": { "type": "integer", "minimum": 1, "description": "B90: a matched rule's timeout; every session of the ticket runs at the largest one among the matched rules that set one. Unset: the rule does not count in that max (review.session_timeout_s applies only when no matched rule sets one)." },
169
+ "second": { "$ref": "#/$defs/level" }
170
+ }
171
+ }
172
+ },
142
173
  "max_rounds_per_file": { "type": "integer", "minimum": 2, "maximum": 6, "default": 4 },
143
174
  "recheck_scope": { "type": "string", "enum": ["fix_hunks", "file"], "default": "fix_hunks" },
144
175
  "late_findings": { "type": "string", "enum": ["sweep", "block"], "default": "sweep" },
@@ -258,6 +289,45 @@
258
289
  "default": [".env", ".env.testing"]
259
290
  }
260
291
  }
292
+ },
293
+ "runners": {
294
+ "type": "array",
295
+ "minItems": 1,
296
+ "description": "B87: how proof red-green runs a test file that is not a node:test file. proof red-green uses the first entry whose match covers the --test file (list order). With no match, a .mjs/.cjs/.js file uses the built-in node:test command and any other file is refused. Fixed for the run: run start copies the list into the run snapshot, run reload refuses to change it, and with it set no block may own .code-forge.yml. An empty list is refused: leave the key out for no runner.",
297
+ "items": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "required": ["match", "adapter", "argv"],
301
+ "if": { "patternProperties": { "^adapter$": { "const": "xcodebuild" } } },
302
+ "then": { "not": { "propertyNames": { "not": { "const": "xcode" } } } },
303
+ "else": { "propertyNames": { "not": { "const": "xcode" } } },
304
+ "properties": {
305
+ "match": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Globs of the test files this runner runs, relative to the repository (the same glob rules as owned entries)." },
306
+ "adapter": { "type": "string", "enum": ["junit", "swift", "xcodebuild"], "description": "How the result is read. junit: the runner writes a JUnit XML report to {report} (PHPUnit, Pest, pytest and other JUnit runners). swift: swift test with --xunit-output {report}. xcodebuild: xcodebuild test with -resultBundlePath {result_bundle}; the result is read with xcrun xcresulttool, and a precheck needs full Xcode and the destination's simulator first." },
307
+ "argv": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "The command, an argv array (never a shell string). These items are replaced when an item is exactly the token. junit and swift: {test_file} (the test path), {test_id} (<file>::<case>, or <file> with no case), {test_filter} (the case, regex-escaped), {report} (a fresh report path outside the work dir); {report} and one test token are required. xcodebuild: {workspace}, {project}, {scheme}, {destination} (from xcode), {only_testing} (one item, -only-testing:<test_target>/<Suite>[/<case>]), {result_bundle} (a fresh result bundle path outside the work dir); {scheme}, {destination}, {only_testing} and {result_bundle} are required. A token cannot be the command (argv[0]) or part of a longer item." },
308
+ "xcode": {
309
+ "type": "object",
310
+ "additionalProperties": false,
311
+ "required": ["scheme", "destination", "test_target"],
312
+ "dependentSchemas": { "workspace": { "propertyNames": { "not": { "const": "project" } } } },
313
+ "description": "B102: the xcodebuild adapter's values; required for it, refused on any other adapter. Each value is a non-empty string that does not start with -. At most one of workspace and project.",
314
+ "properties": {
315
+ "workspace": { "type": "string", "minLength": 1, "pattern": "^[^-]", "description": "The .xcworkspace for {workspace}. At most one of workspace and project; neither = a Swift package in the work dir." },
316
+ "project": { "type": "string", "minLength": 1, "pattern": "^[^-]", "description": "The .xcodeproj for {project}. At most one of workspace and project." },
317
+ "scheme": { "type": "string", "minLength": 1, "pattern": "^[^-]", "description": "The scheme for {scheme}." },
318
+ "destination": { "type": "string", "minLength": 1, "pattern": "^[^-]", "description": "The destination for {destination}, e.g. platform=iOS Simulator,name=iPhone 17. A simulator destination must name a device xcrun simctl lists as available (by name or id, and OS when given); platform=macOS needs none. code-forge never boots, creates or deletes a simulator." },
319
+ "test_target": { "type": "string", "minLength": 1, "pattern": "^[^-/][^/]*$", "description": "The test target (one name, no /) that {only_testing} starts with; every test in the result must sit under <test_target>/<Suite>." }
320
+ }
321
+ },
322
+ "env": {
323
+ "type": "object",
324
+ "additionalProperties": false,
325
+ "patternProperties": { "^[A-Za-z_][A-Za-z0-9_]*$": { "type": "string" } },
326
+ "description": "Variables added to the runner's environment, NAME: value. code-forge never prints or records an env value; only the names are shown (env_keys). Child output is masked only for values of 8 or more characters. Not a secret store."
327
+ },
328
+ "timeout_s": { "type": "integer", "minimum": 60, "maximum": 7200, "default": 600, "description": "Seconds each step (RED, GREEN) may run before it is stopped." }
329
+ }
330
+ }
261
331
  }
262
332
  }
263
333
  },
@@ -326,6 +396,9 @@
326
396
  },
327
397
  "keys": {
328
398
  "type": "object",
399
+ "properties": {
400
+ "cache_hours": { "type": "integer", "minimum": 1, "maximum": 24, "default": 8, "description": "B84: how many whole hours a key read from 1Password stays cached in the per-user key store (keys set --op, keys test, jev ask, doctor, the worker). keys set --cache-hours wins over it. A cached key is never trusted for longer than the reader's value." }
401
+ },
329
402
  "additionalProperties": { "type": "string" }
330
403
  },
331
404
  "tmp": {
@@ -384,6 +457,22 @@
384
457
  "min_confidence": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.7, "description": "B46: the lowest confidence at which the autopilot delegate's answer is acted on; below it the question goes to the owner." }
385
458
  }
386
459
  },
460
+ "coder": {
461
+ "type": "object",
462
+ "additionalProperties": false,
463
+ "description": "B93: what the coder session may write beyond the project tree. Read by the Codex (openai) coder only; it has no effect on the Claude (anthropic) and Grok (xai) coders, whose argv does not change. No key means profile workspace and today's coder argv. Authority: fixed at run start; `run reload` refuses a change; `forge spawn --role coder --run <id>` refuses a live value that differs from the run-start snapshot (sandbox-changed). Without --run, spawn reads the live config.",
464
+ "properties": {
465
+ "sandbox": {
466
+ "type": "object",
467
+ "additionalProperties": false,
468
+ "description": "The Codex coder sandbox profile and its extra writable directories.",
469
+ "properties": {
470
+ "profile": { "type": "string", "enum": ["workspace", "toolchain", "full"], "default": "workspace", "description": "workspace (the default; no key gives today's argv): `-s workspace-write`, the project tree is the only writable root. toolchain: the same plus one `--add-dir <abs>` per add_dirs entry (a build cache, a package store). full: `-s danger-full-access`, the coder writes anywhere the user can; like the Claude coder, the forbidden list, the transcript grep and the gates are the only limits, and doctor prints a WARN row. Every profile keeps the $TMPDIR and /tmp exclusions in the argv; Codex ignores them under danger-full-access." },
471
+ "add_dirs": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "^(/|~/)" }, "uniqueItems": true, "default": [], "description": "Extra writable directories for profile toolchain; under workspace or full they are unused and validate warns (rule sandbox-dirs-unused), and a refused entry is still refused. Each entry is absolute or starts with ~/ (expanded against the process HOME) and becomes an absolute realpath: symlinks are followed, even a dangling one, so a symlink is judged by its target. Refused (rule sandbox-dir-refused, by validate and again by the builder; the message names the key path, never the value): /, the home directory itself, and any directory that is, holds or sits inside ~/.code-forge, ~/.codex or $CODEX_HOME, ~/.ssh, ~/.gnupg, ~/.aws, ~/.config, ~/Library/Keychains, the run temp root (so $TMPDIR or /tmp when it holds it), or the session CODEX_HOME; for example ~/Library is refused because it holds ~/Library/Keychains." }
472
+ }
473
+ }
474
+ }
475
+ },
387
476
  "telemetry": { "type": "string", "const": "off", "default": "off" }
388
477
  },
389
478
  "$defs": {
package/skill/SKILL.md CHANGED
@@ -41,7 +41,7 @@ State lives in the plan file's header — `facts → planned → hardened → co
41
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
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.
44
+ 7. `forge block close <id> --run <r> --report <coder report>` is the gate. It needs every changed file accepted by the review gate — a signed `review.approved` row for its current content that no later review of the same content overrode (else `not_approved`), or every finding its review left open waived or answered (`reviewed_by_waiver`) — 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`. The final report needs no `reviewed <path> ticket <id>` lines: `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. The close prints the reviewed lines, then the `reviewed_by_waiver` lines, then the `closed` line. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr when the block closes, and the close goes on. A file with a `review.waived` finding still gets a line; only a close with every review waived (`--no-require-reviews`) or `review.external` prints none. The printed line has no `via`: `--json` and the block's done file carry the lines as `reviewed: [{file, ticket, via}]`, with `via: approved` for a file with a signed approval, `via: waiver` when at least one of its open findings was waived (also when the others were answered), and `via: answered` when every one was answered. 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, waived or external reviews or not (`references/code.md` §3). 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
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`)
@@ -12,7 +12,7 @@ The harness's `Agent` tool runs the coder as a subagent; everything else — the
12
12
 
13
13
  The tool return is the completion signal; the report must begin with `ACK <sha8>` and end with `===BLOCK <id> COMPLETE===` (+ test summary + `git diff --stat`) or `===BLOCK <id> FAILED: <reason>===`. A report without the sentinel is a failed block, never a partial success.
14
14
 
15
- **Between completion and close come the review rounds.** The subagent ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket inside its turn, so most rounds happen before the sentinel; when the tool returns, first drain every `pending` ticket with `forge review-file --wait <ticket>` yourself — a pending ticket is a review in flight, not an open finding, and waiting on it costs no round. Then only an open `fix_now` finding, or a file the coder never submitted, starts a fix round (each one counts toward the file's cap): dispatch a new `Agent` call at the level the ladder says (`review.md` §5 — the coder is gone with its tool return, so the fix brief carries the open findings and the fix history) and it runs `review-file` again on the fixed file. Rounds are bounded: four rounds per file, then the L3 rung once, then `stopped: review_cap` for the human. Only when every changed file has a signed `review.approved` row do you run the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --report <file>` (the subagent's final report, saved to a file) — the transcript grep reads `.code-forge/runs/<run>/<block>.log`; pass `--transcript <file>` when the harness saved the subagent's transcript elsewhere. Closing with a file still unreviewed is a human decision: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, or `forge block close … --no-require-reviews`, which the human runs at their own terminal (it asks for a yes, has no `--yes`, and is refused under an active autopilot grant), both audited as their own rows.
15
+ **Between completion and close come the review rounds.** The subagent ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket inside its turn, so most rounds happen before the sentinel; when the tool returns, first drain every `pending` ticket with `forge review-file --wait <ticket>` yourself — a pending ticket is a review in flight, not an open finding, and waiting on it costs no round. Then the open `fix_now` findings, the failing tests and the files the coder never submitted go into one fix round. Fix rounds are one per block: wait for every `review-file` ticket of the block and its tests, then send ONE fix message with every failing test and every open finding; a finding that arrives later waits for the next round (`code.md` §4). Dispatch that message as one new `Agent` call at the level the ladder says (`review.md` §5 — the coder is gone with its tool return, so the fix brief carries every failing test, every open finding and the fix history). Its brief says: in that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. One block round is one round for each file it touched. Rounds are bounded: four rounds per file, then the L3 rung once, then `stopped: review_cap` for the human. Only when every changed file has a signed `review.approved` row do you run the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --report <file>` (the subagent's final report, saved to a file) — the transcript grep reads `.code-forge/runs/<run>/<block>.log`; pass `--transcript <file>` when the harness saved the subagent's transcript elsewhere. Closing with a file still unreviewed is a human decision: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, which asks for a yes at a terminal and has no `--yes`, or `forge block close … --no-require-reviews`, which also asks and is refused under an active autopilot grant, all audited as their own rows. `forge block reopen <id> --file <path> --run <r> --reason "<why>"` does not close anything: it sends a fixed stopped file back to review, and it is also human-only and asks the same way. Print the exact command: the orchestrator never answers this prompt; the human types the command in their own terminal. You and the subagent never run it. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
16
16
 
17
17
  ## §2.1 Plan approval
18
18
 
@@ -21,8 +21,8 @@ Solo adds spawn, watch, timers, scratchpads, locks and `kv` on top of the CLI; i
21
21
  4. **Launch-verification gate** — the one thing you babysit: poll `get_process_output` / `search_output` until the coder shows the ACK and tool activity. Dead-idle after the pointer (no prompt on screen) ⇒ one Enter (`send_input bytes=[13]`) and re-check. A **permission or model-picker prompt** is never answered with Enter — it would accept an unreviewed default: `close_process`, respawn with the correct `extra_args`, and record a `spawn-flag defect` line in the block record (the flags came from `forge resolve`; a prompt means they were not pinned).
22
22
  5. **Wake channels, event-driven, never fixed polls:** record your own pid once (`whoami` → `kv_set("forge:orchestrator_pid")`); coders that can call Solo carry the push contract (post-sentinel `kv_set("block:<id>", …)` + `timer_set(1s, body, delivery_process_id=<your pid>)`); for every group arm `timer_fire_when_idle_any([coders], max_wait≈20–25 min, delivery_process_id=<your pid>)` right after the launch gate (idle-any ignores already-idle processes, so arm while they are provably running); keep one long dead-man fallback per 30–40 min with an L0 triage first. On wake: sentinel ⇒ gate; no sentinel and active again ⇒ re-arm; no sentinel and idle ⇒ parked, intervene now.
23
23
  6. Completion = `search_output(pattern="===BLOCK <id> COMPLETE===")`, last occurrence (the `kv` key is the truncation backup). Completion is not closure.
24
- 7. **Review rounds, with the coder still alive.** The coder ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket; read the ledger (`forge ledger tail --slug <slug>`) for what is still open — a `fix_now` finding, a `pending` ticket, a file never submitted. Each open finding is a fix round sent to the **warm coder** by `send_input` (the findings by id, `<<<EOM>>>`), which fixes and re-runs `review-file` on that file; round ≥ 2 re-checks only the fix hunks (`review.md` §5). **One ladder** (`decisions.md` §5): a stall or exhausted rounds *below L2* climbs one level — close this coder and respawn a fresh one at the higher level with the open findings and the fix history in its brief; **the respawn keeps the per-file round count**, the cap does not reset. L2 is the highest running level: a trigger at L2, or the four-round cap on a file, goes to the **L3 rung once** — a fresh L3 patch session, never this coder and never a coder at L3 — and if findings stay open after the `patch_check` round the file is `stopped: review_cap` for the human. No other path exists.
25
- 8. **Close** only when every changed file has a signed `review.approved` row: the re-measurement of `code.md` §5, then `forge block close <id> --run <r> --report <file>` (the coder's final report) — the transcript grep reads the Solo output. Closing with a file still unreviewed is a human decision, audited: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, or `forge block close … --no-require-reviews`, which the human runs at their own terminal (it asks for a yes, has no `--yes`, and is refused under an active autopilot grant).
24
+ 7. **Review rounds, with the coder still alive.** The coder ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket; read the ledger (`forge ledger tail --slug <slug>`) for what is still open — a `fix_now` finding, a `pending` ticket, a file never submitted. After the coder's first COMPLETE, wait until every `review-file` ticket of the block is answered and the block's tests ran; then send ONE fix message with every failing test (name and first error line) and every open finding (by file and id). Never one finding or one file at a time. A finding that arrives after that message waits for the next round. In that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. The message goes to the **warm coder** by `send_input`, ending in `<<<EOM>>>`; when it is over the input limit (§1), write it to a file and send its path. Round ≥ 2 re-checks only the fix hunks (`review.md` §5), and one block round is one round for each file it touched. **One ladder** (`decisions.md` §5): a stall or exhausted rounds *below L2* climbs one level — close this coder and respawn a fresh one at the higher level with the open findings and the fix history in its brief; **the respawn keeps the per-file round count**, the cap does not reset. L2 is the highest running level: a trigger at L2, or the four-round cap on a file, goes to the **L3 rung once** — a fresh L3 patch session, never this coder and never a coder at L3 — and if findings stay open after the `patch_check` round the file is `stopped: review_cap` for the human. No other path exists.
25
+ 8. **Close** only when every changed file has a signed `review.approved` row: the re-measurement of `code.md` §5, then `forge block close <id> --run <r> --report <file>` (the coder's final report) — the transcript grep reads the Solo output. Closing with a file still unreviewed is a human decision, audited: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, which asks for a yes at a terminal and has no `--yes`, or `forge block close … --no-require-reviews`, which also asks. `forge block reopen <id> --file <path> --run <r> --reason "<why>"` does not close anything: it sends a fixed stopped file back to review, and it is also human-only and asks the same way. Under an active autopilot grant `block waive` and `block reopen` still prompt; only `block close --no-require-reviews` is refused. Autopilot's own waive path, `forge autopilot waive` (`by: autopilot`), needs no terminal. Print the exact command: the orchestrator never answers this prompt; the human types the command in their own terminal. You and the coder never run it. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
26
26
  9. **Reap after the close**: `close_process` the coder and confirm `lock_status` shows no lease before the next group. Locks: a coder that can call Solo holds `lock_acquire` on its file area while editing; coders that cannot never overlap another block.
27
27
 
28
28
  ## §4 Latency overlaps (standing)
@@ -8,8 +8,18 @@ Never selected automatically (`degraded.md` §1). Coders are detached CLI proces
8
8
 
9
9
  ## §2 Completion and evidence
10
10
 
11
- The coder's stdout is captured; completion is the **last** `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` line in it (a quoted sentinel inside a sentence never matches). Process exit without a sentinel is a failed block. Then the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --transcript <captured stdout> --report <captured stdout>` — the transcript grep runs on the capture. A coder that hangs is killed at `--timeout` together with its process group; the pid entry is cleared.
11
+ The coder's stdout is captured; completion is the **last** `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` line in it (a quoted sentinel inside a sentence never matches). Process exit without a sentinel is a failed block. Then the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --transcript <captured stdout> --report <captured stdout>` — the transcript grep runs on the capture. A coder that hangs is killed at `--timeout` together with its process group; the pid entry is cleared. Fix rounds are one per block: wait for every `review-file` ticket of the block and its tests, then send ONE fix message with every failing test and every open finding; a finding that arrives later waits for the next round (`code.md` §4). Here a fix round is a new spawn, and the brief of that spawn is the one fix message.
12
12
 
13
13
  ## §3 What the coder's sandbox looks like per provider
14
14
 
15
- Each provider's coder runs in its non-interactive auto mode with writes limited to the workspace where the CLI supports it; every closed-book role runs read-only with an empty cwd and the packet on stdin. `forge doctor` probes, per provider, that the flags exist, that a one-token call works per role builder, that the closed-book session sees no project document, and that the deny rules are honoured — read its rows before the first real spawn. Where a provider cannot load a rules file, the forbidden list is prose-only in the brief and the transcript grep is the detector (`security.md` §2).
15
+ Each provider's coder runs in its non-interactive auto mode with writes limited to the workspace where the CLI supports it, except under `coder.sandbox.profile: full`; every closed-book role runs read-only with an empty cwd and the packet on stdin. `forge doctor` probes, per provider, that the flags exist, that a one-token call works per role builder, that the closed-book session sees no project document, and that the deny rules are honoured — read its rows before the first real spawn. Where a provider cannot load a rules file, the forbidden list is prose-only in the brief and the transcript grep is the detector (`security.md` §2).
16
+
17
+ **The Codex coder's sandbox profile (`coder.sandbox`, B93).** Only the Codex (openai) coder reads it; it has no effect on the Claude and Grok coders, whose argv does not change. `doctor` prints no `coder sandbox` row without the key, one INFO row with it, a WARN row for `full`, and a no-effect INFO row only when a Claude or Grok coder is configured.
18
+
19
+ - `workspace` (the default, and what no `coder` key means): `-s workspace-write`, today's argv. The project tree (`-C <cwd>`) is the only writable root; `$TMPDIR` and `/tmp` stay excluded, so the session `CODEX_HOME` under the run temp root cannot be written.
20
+ - `toolchain`: the same, plus one `--add-dir <abs>` per `coder.sandbox.add_dirs` entry right after `-s workspace-write`, so a build cache or a package store is writable too.
21
+ - `full`: `-s danger-full-access`, which turns the Codex sandbox off: the coder writes anywhere you can, with network as the CLI allows. Like the Claude coder, the deny rules, the transcript grep and the gates are the only limits; `doctor` prints a WARN row. The argv keeps the `$TMPDIR` and `/tmp` exclusion flags, which Codex ignores there.
22
+
23
+ **`add_dirs` entries.** Each entry is absolute or starts with `~/`, which expands against HOME, and becomes an absolute realpath: symlinks are judged by their target, a dangling symlink too. `validate` (rule `sandbox-dir-refused`) and the builder refuse `/`, the home directory itself, and an entry that is, holds or sits inside any refused dir: `~/.code-forge`, `~/.codex` (and `$CODEX_HOME`), `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.config`, `~/Library/Keychains`, the run temp root or the session `CODEX_HOME`. So `~/Library` is refused (it holds `~/Library/Keychains`), and so is `$TMPDIR` or `/tmp` when it holds the run temp root. The message names the key path (`coder.sandbox.add_dirs[<i>]`), never the value. Under `workspace` or `full` the entries are unused: `validate` warns `sandbox-dirs-unused`, and a refused entry is still refused.
24
+
25
+ The key is fixed for the run: `run reload` refuses a change, and `forge spawn --role coder --run <r>` exits 1 `sandbox-changed` when the live value differs from the run-start snapshot. Without `--run`, spawn reads the live config, so give a run's coder `--run <r>`. A run recorded before config snapshots has no run-start value; spawn then uses the live config. A Codex coder session row carries `sandbox: <profile>` only when the key is set.
@@ -25,9 +25,10 @@ The brief file carries, in this order:
25
25
  1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
26
26
  2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
27
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>===`.
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>`.
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 and `git diff --stat`, or `===BLOCK <id> FAILED: <reason>===`.
29
+ 5. **The close writes the ticket lines (B96)** (`templates/coder-brief.md`): The final report needs no `reviewed <path> ticket <id>` lines: `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. The close prints the reviewed lines, then the `reviewed_by_waiver` lines, then the `closed` line. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr when the block closes, and the close goes on. A file with a `review.waived` finding still gets a line; only a close whose every review the human waived, or a `review.external` close, prints none. This holds for the first attempt and every fix round. Pass the report to the gate with `forge block close <id> --run <r> --report <file>`. The printed line has no `via`: `--json` and the block's done file carry the lines as `reviewed: [{file, ticket, via}]`, with `via: approved` for a file with a signed approval, `via: waiver` when at least one of its open findings was waived (also when the others were answered), and `via: answered` when every one was answered. A file the gate accepts gets no line when the queue cannot give its ticket (its content changed after the gate read it, or its path cannot be resolved): the close prints `WARN no ticket for <path>`, or `WARN bad ticket for <path>` when the ticket is not 24 lowercase hex characters. If the lines cannot be built at all, the close still goes on, with no line and one `WARN reviewed lines unavailable: <message>`. Like the mismatch WARN, these go to stderr and print only when the block closes; a refused close prints none of them. The close refuses `coder report FAILED: no review-file ticket id for <files>` only when, under `--report`, a file the review gate accepts has no reviewed line from the close and no hand line for it in the report. For that check a hand line is a report line that names that file, no other file of the block, and a ticket id of 24 lowercase hex characters. So when the lines cannot be built at all, every file the gate accepts needs a hand line, and no hand line is compared with a ticket. For the mismatch WARN only a line that is exactly `reviewed <path> ticket <id>` counts (a `- `, `* ` or `1. ` list marker and backticks are fine), and since a ticket id is 24 lowercase hex characters, an id in capitals names another ticket.
30
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
+ 7. **Tests prove behaviour (B92).** The brief says: proof is behaviour, not text: never read a source file in a test to assert on its text. A test calls the code and checks what it does; reading a fixture is fine. A test file that reads a source file and asserts on its text gets a `## test-quality` section in its review packet (`review.md` §3). The section is a hint, not a finding: the reviewer checks those lines and, when the test really reads source text to assert on it, raises a `warning` (unless the file read is a fixture).
31
32
 
32
33
  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
 
@@ -45,6 +46,8 @@ Waived or external reviews (`--no-require-reviews`, `review.external`) do not li
45
46
 
46
47
  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`).
47
48
 
49
+ **One fix round per block (B94).** After the coder's first COMPLETE, wait until every `review-file` ticket of the block is answered and the block's tests ran; then send ONE fix message with every failing test (name and first error line) and every open finding (by file and id). Never one finding or one file at a time. A finding that arrives after that message waits for the next round. In that round the coder fixes, runs the block's tests, re-runs `review-file` on every changed file, and prints its `TESTS` line. The per-file cap still counts per file: one block round is one round for each file it touched (`review.md` §5).
50
+
48
51
  ## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
49
52
 
50
53
  The coder's report is a claim sheet. The gate reproduces each number in the measurement export (`proof.md` §3), and so do you when you read the report:
@@ -52,7 +55,7 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
52
55
  | claim | re-measured by |
53
56
  |---|---|
54
57
  | only owned paths touched | file set = tracked changes since the base ∪ untracked (not ignored), listed from the repository top level, outside the repository top-level `.code-forge/` (keep transcripts and reports there; a subdirectory workspace's own `.code-forge/` is source tree except this run's record mirror and the block's default transcript: a `--transcript` or `--report` path in the source tree is a change like any other); each file must be in `owned_files`, unchanged since `block open` recorded the tree (`tree_at_open`; a path unreadable at open or at close never counts as unchanged), owned by another block of the run that is still **open**, or a closed block's file still exactly at the content its gate approved — else `unowned_change <file>` until `forge block claim <id> <path> --run <r>` or the human puts it back (not lifted by `--no-require-reviews`). The gate cannot tell which coder wrote a file an open sibling owns: it passes it to that sibling's gate and prints `WARN block <id>: <file> changed and is owned by open block <other>` for each one — read those lines |
55
- | every file reviewed | file set = owned files changed since the base ∪ owned files with any review row of the block (`review.result`/`round`/`approved`/`triage`/`cap`/`late_finding`), an owned entry with no glob covering the paths below it; every path is repo-root-relative (what the worker signs), and an owned entry also matches relative to a workspace in a subdirectory; an empty set refuses `no_changes` (only the owner's `--no-require-reviews`, confirmed at a terminal, lifts it), and an empty diff with review rows WARNs (work committed before `block open`, or in another worktree — open blocks on a base without the work, in the workspace the coder edits). The report names a `review-file` ticket id per changed file (`--report`, which the orchestrator always passes) and a **signed** `review.approved` row for the current content hash of every changed or new file that no later review of the same content overrode (a `review.result` with `approved: false`, a `review.round` with findings open, a `review.cap`): else `not_approved <file> <finding>` per open finding, cleared only by a fix and a fresh approval or a waiver — a critical only by the human's `block waive` — or an architect-ruled disposition to a named successor block |
58
+ | every file reviewed | file set = owned files changed since the base ∪ owned files with any review row of the block (`review.result`/`round`/`approved`/`triage`/`cap`/`late_finding`), an owned entry with no glob covering the paths below it; every path is repo-root-relative (what the worker signs), and an owned entry also matches relative to a workspace in a subdirectory; an empty set refuses `no_changes` (only the owner's `--no-require-reviews`, confirmed at a terminal, lifts it), and an empty diff with review rows WARNs (work committed before `block open`, or in another worktree — open blocks on a base without the work, in the workspace the coder edits). The close prints a `reviewed <path> ticket <id>` line per file the review gate accepts (the `--report` the orchestrator always passes needs none). The gate accepts a changed or new file with a **signed** `review.approved` row for its current content hash that no later review of the same content overrode (a `review.result` with `approved: false`, a `review.round` with findings open, a `review.cap`), or with every finding its review left open waived or answered (`reviewed_by_waiver`): else `not_approved <file> <finding>` per open finding, cleared only by a fix and a fresh approval or a waiver — a critical only by the human's `block waive` — or an architect-ruled disposition to a named successor block |
56
59
  | tests green | every gate command run once in the export, each exit status read |
57
60
  | clauses covered | every clause names ≥ 1 test id; each test exists and passed in this run; S1 `scope` on the hunks outside the named tests says `false` |
58
61
  | test count | the filtered run's delta equals the new tests' own count |
@@ -70,12 +73,16 @@ Without the flag the tree stays. On a block without a worktree the flag is a usa
70
73
 
71
74
  **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
75
 
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.
76
+ 1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then print the exact command, `forge block reopen <id> --file <path> --run <r> --reason <text>` with the human's reason, and the human runs it in their own terminal, exactly like `block waive`. Never run it yourself, and never for a coder (it is on the coder forbidden list). After it, send the coder's `review-file` again: the next round is a full round 1 when the content differs from what the stop reviewed; on the stop's own content the stop repeats. A `reopen-same-content` refusal means the content is still what the stop reviewed: offer the waive instead.
74
77
  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.
78
+ 3. **`block waive`.** The human waives each open finding: print the exact `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` command per finding, and the human runs it in their own terminal. A file whose findings are all waived or answered closes with a signed `gate.review_waived_all` row.
76
79
  4. **Re-decompose.** The human decides, and the architect plans new blocks.
77
80
 
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.
81
+ **`block waive` and `block reopen` are run by the human, never by you (B80).** Both ask for a yes at a terminal. You print the exact command; the human runs it in their own terminal. The rule: a coder or the orchestrator never runs it and never answers its prompt. Separately, as a fact of the verb: when stdin or stdout is not a terminal (a pipe, or a process started with no terminal) it exits 2 with `block <verb> needs the human at a terminal to confirm; nothing was recorded (there is no --yes)` and records nothing. There is no `--yes`, and an autopilot grant does not change this (the delegate waives through `forge autopilot waive`). The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
82
+
83
+ `block attempt` never resets review state: it only raises the attempt counter, and a stopped file stays stopped. Use `review-file --fresh` only after a result that never counted (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); it never re-runs a counted round. A missing state file needs no exit: the worker first rebuilds it from its ledger anchor (`review.state_restored`). Only a state whose anchor has no `state_body` is `lost` (`review-state-lost`); that one needs the human's `block reopen`, on its current content. A `review-state-tampered` file has none of these exits: only the owner's `block close --no-require-reviews` gets past it.
84
+
85
+ A cached round-1 `split_required` ran no session and counted no round (B110). Plain `review-file` re-queues it with `retry: true`, and `--fresh` re-queues it too, with the same ticket id. After a budget change and `run reload`, the worker uses the live config. If the budget is still too small, it returns `split_required` again without a session. A recheck stop (`status: stopped`, `stopped: split_required`) keeps its `review.cap` row and still needs `block reopen` or `block waive`; plain review returns the cached stop and `--fresh` refuses `fresh-needs-reopen`.
79
86
 
80
87
  `--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.
81
88
 
@@ -97,7 +104,7 @@ Both `autonomy` keys default to false. Whatever they say, a block's branch is ha
97
104
  2. The block's test command green, case count read from the runner and written to the block record.
98
105
  3. Type check and lint green under the project's gates.
99
106
  4. Every changed or new file reviewed by a fresh L2 session and converged under the convergence rule (≤ `review.max_rounds_per_file`, or a human decision on record).
100
- 5. Red→green rows for every new test; proof rows per tier (`proof.md`).
107
+ 5. Red→green rows for every new test; proof rows per tier (`proof.md`). A test that is not a `node:test` file (PHPUnit, Pest, pytest, Swift) is proven through the `proof.runners` entry the owner set before the run (`proof.md` §2): `forge proof red-green <id> --run <r> --test <file>::<case>`, with the case as the runner itself names it. The `::<case>` is needed only when the runner's argv has `{test_filter}`. Without `{test_filter}`, a case only labels the proof: `{test_id}` becomes `<file>::<case>`, and `{test_file}` still runs the whole file. A coder never edits `proof.runners`.
101
108
  6. Forecast vs actual (cases and lines) recorded; misses attributed (architect-added scope vs coder under-forecast) — a finished green block is accepted and recorded either way.
102
109
  7. One commit for the block; the draft PR (when the project uses PRs) carries the evidence.
103
110
  8. Every artifact that proves something is committed, not left in a scratch dir; no claim shipped that the evidence does not carry.
@@ -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` (`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`.
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}` (the human's `block waive`, run in their own terminal after a yes, B80; no row on a "no", a cancel, no terminal, `--yes` (a usage error), or a pre-check refusal (a malformed or control-character finding id or `--file`, an unknown block, a file the block does not own, an unknown finding). The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).), `review.state {block, file, seq, state_sha, l3_rung_used, state_body}` (signed; the fix-loop state anchor, `review.md` §5 step 8; `state_body` carries `reviewed_content_sha` and `reviewed_content_blob`, the git blob id of the reviewed text in the reviewed tree's object store, or null when there was no reviewed text or no content root, or the write failed; when the signed body is over 64 KB, `state_omitted: "too_large"` replaces `state_body`, and a lost state file then refuses as `review-state-lost`), `review.state_restored {block, file, seq, from_seq}` (signed; a lost state file was rebuilt from the anchor at `from_seq` and saved as `seq`), `review.reopened {block, file, from: stopped | lost, stop_reason, at_seq, from_hash, to_hash, reason, by: human}` (signed; B72: the human's `block reopen` of a stopped or lost file, run in their own terminal after a yes (B80; no row on a "no", a cancel, no terminal, `--yes` (a usage error), or a pre-check refusal (a malformed or control-character `--file`, an unknown or not-open block, a file the block does not own)) — `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` · `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.
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 (one block round is one round for each file it touched, `review.md` §5), rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine` · `outside_review` (14th, 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