@codedology/code-forge 0.14.1 → 0.15.1

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 (41) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/docs/reference/blocks.json +495 -1
  4. package/docs/reference/config.md +7 -0
  5. package/package.json +1 -1
  6. package/schema/code-forge.schema.json +22 -0
  7. package/scripts/first-use.mjs +11 -5
  8. package/skill/SKILL.md +4 -0
  9. package/skill/references/autopilot.md +2 -0
  10. package/skill/references/code.md +28 -2
  11. package/skill/references/review.md +10 -2
  12. package/skill/references/security.md +2 -0
  13. package/skill/scripts/forge +1 -1
  14. package/skill/templates/coder-brief.md +3 -1
  15. package/src/autopilot/scopes.mjs +2 -1
  16. package/src/cli/block.mjs +271 -50
  17. package/src/cli/gates.mjs +36 -6
  18. package/src/cli/ledger.mjs +21 -2
  19. package/src/cli/report.mjs +14 -0
  20. package/src/cli/review-file.mjs +56 -16
  21. package/src/cli/run.mjs +44 -7
  22. package/src/cli/status.mjs +7 -3
  23. package/src/config/validate.mjs +13 -1
  24. package/src/gates/acceptance.mjs +7 -3
  25. package/src/gates/expand.mjs +25 -2
  26. package/src/gates/run.mjs +38 -8
  27. package/src/ledger/report.mjs +29 -7
  28. package/src/review/accept-hit.mjs +22 -4
  29. package/src/review/engine.mjs +30 -10
  30. package/src/review/finding.schema.json +2 -1
  31. package/src/review/fixloop.mjs +79 -14
  32. package/src/review/gate-check.mjs +33 -7
  33. package/src/review/lenses/recheck.md +3 -0
  34. package/src/review/packet.mjs +26 -0
  35. package/src/state/block.mjs +196 -22
  36. package/src/state/run.mjs +35 -8
  37. package/src/util/forbidden.mjs +230 -6
  38. package/src/util/reaper.mjs +3 -3
  39. package/src/worker/engine.mjs +74 -6
  40. package/src/worker/loop.mjs +19 -10
  41. package/src/worker/queue.mjs +38 -8
@@ -25,8 +25,9 @@
25
25
  * 8 review `code-forge review-file <file>` + `review-file --wait` per owned file
26
26
  * 9 proof `proof export` (lists the export-ignore paths it restored), the test gate inside the
27
27
  * export, `proof red-green` per test the acceptance names, `proof export --remove`
28
- * 10 close `code-forge block close B1` — then the cost of the completed block is printed
29
- * 11 report `code-forge report --slug <slug>`
28
+ * 10 gates `code-forge gates run --run <run> --block B1` — green before close
29
+ * 11 close `code-forge block close B1` — then the cost of the completed block is printed
30
+ * 12 report `code-forge report --slug <slug>`
30
31
  * then `code-forge run end` (always, once the run started: it stops the worker).
31
32
  *
32
33
  * Engines: `harness` is the default (R2: the no-Solo default). `subprocess` is OPT-IN: it is
@@ -60,7 +61,7 @@ const DEGRADED_DOC = path.join(PKG, 'skill', 'references', 'degraded.md');
60
61
 
61
62
  export const BLOCK = 'B1';
62
63
  export const OWNED = Object.freeze(['src/math.mjs', 'test/clamp.test.mjs']);
63
- export const STEPS = Object.freeze(['copy', 'init', 'facts', 'author', 'start', 'open', 'code', 'review', 'proof', 'close', 'report']);
64
+ export const STEPS = Object.freeze(['copy', 'init', 'facts', 'author', 'start', 'open', 'code', 'review', 'proof', 'gates', 'close', 'report']);
64
65
  const ENGINES = ['harness', 'subprocess'];
65
66
  const CODERS = ['scripted', 'harness', 'spawn'];
66
67
  const CLI_TIMEOUT_MS = 30 * 60 * 1000;
@@ -385,7 +386,12 @@ export async function main(argv, io = {}) {
385
386
  await cf(['proof', 'export', BLOCK, '--run', run, '--remove']);
386
387
  if (done('proof')) return 0;
387
388
 
388
- // 10 — the gate, then what the completed block cost
389
+ // 10 — record green gates for this run and block, after the proofs
390
+ step('gates');
391
+ say((await cf(['gates', 'run', '--run', run, '--block', BLOCK])).stdout.trimEnd());
392
+ if (done('gates')) return 0;
393
+
394
+ // 11 — the close check, then what the completed block cost
389
395
  step('close');
390
396
  say((await cf(['block', 'close', BLOCK, '--run', run])).stdout.trimEnd());
391
397
  blockOpen = false;
@@ -398,7 +404,7 @@ export async function main(argv, io = {}) {
398
404
  );
399
405
  if (done('close')) return 0;
400
406
 
401
- // 11 — the report
407
+ // 12 — the report
402
408
  step('report');
403
409
  const report = await cf(['report', '--slug', /** @type {string} */ (summary.slug)]);
404
410
  stdout.write(report.stdout);
package/skill/SKILL.md CHANGED
@@ -22,6 +22,10 @@ State lives in the plan file's header — `facts → planned → hardened → co
22
22
 
23
23
  ## §2 Roles by level (never by model name)
24
24
 
25
+ `block amend-acceptance <id> --run <r> --acceptance <file> --reason <text>` is human-only: it corrects an open block's clauses after terminal confirmation, requires a nonblank reason, has no `--yes`, and refuses an active autopilot grant. Earlier reviews become stale and a fresh review uses the new clauses. Coders never run it (`references/code.md` §5).
26
+
27
+ `block human-pass <id> --run <r> --note <text> [--proof <dir-or-file>]` is human-only: the owner checks the listed device checks and confirms in their own terminal, with no `--yes` and no active autopilot grant. The note is required; proof is optional, must name an existing path when given, and is recorded as `proof: null` when omitted. A successful code close with `kind: human_check` acceptance clauses leaves `awaiting_human_check`, which status and report list; it has no fresh done file until the human passes or stops it. Never run this verb for the human, and do not end the run or treat the block as ready to land while it is awaiting a check (`references/code.md` §5).
28
+
25
29
  | role | level | session | who runs it |
26
30
  |---|---|---|---|
27
31
  | facts delegate | L0 | read-only tools, empty cwd | `forge facts` |
@@ -13,6 +13,8 @@ Autopilot lets the owner leave for a set time. A delegate (a fresh L2 or L3 sess
13
13
 
14
14
  ## §2 During the window
15
15
 
16
+ The allowable scope `close:routine` means “close a routine block.” It lets plain `block close` pass `close.require_human` without a terminal when every changed file has a signed approval, a signed green gate row exists, and the block has no waiver rows, transcript hits or accepted hits. Waived or answered-only reviews do not qualify. Refusal is `close-not-routine: <reason>` (exit 1); success signs `autopilot.close {block, grant}` alongside the normal close. It never permits `block close --decide` or critical waivers.
17
+
16
18
  - **Every owner-level question goes to the delegate first**: `forge autopilot ask --run <r> --scope <scope> --question <text> --options <words> [--block <id>] [--file <path>] [--finding <id>]`. Use the fixed option words, or the action will refuse the answer: a waiver `waive,fix` · an extra round `allow,deny` · a coder level the candidate levels, e.g. `L1,L2`.
17
19
  - **Act only when it acted** (exit 0): run the action with `--decision <id>` from the answer — `forge autopilot waive|round|level … --decision <id>`. Exit 3 means the question goes to the owner: leave it open, it shows under Open questions. Exit 1 means the grant refused it: also the owner's.
18
20
  - **A paused category** (`autopilot paused: … waiting for the owner`): wait. Never raise the cap, never route around it.
@@ -1,5 +1,7 @@
1
1
  # Code — brief, dispatch, re-measure, gate
2
2
 
3
+ Small changes may use `block open <id> --kind light`. Close allows at most `light.max_files` owned changed files (default 5) and `light.max_lines` added plus removed lines (default 300), including new files. Auto ticket lines are unchanged. `gates run --block` runs the block's tests through `{test_files}`, ignoring the full-suite threshold; supply block tests and a test command containing that item. A close refusal `light-limits <files> files, <lines> lines` means the block outgrew this kind: the orchestrator stops the open attempt, then opens the same id with `block open <id> --kind code --attempt <n+1>` and the original base, ownership and acceptance.
4
+
3
5
  ## §1 The coder's first deliverable is a facts diff
4
6
 
5
7
  Before any forecast or code, the coder reads the code it will call and reports **every contradiction between the plan and the code** in one message — a signature, a flag, a key, a version. One stop for facts, never one per discovery. Contract corrections (a key, a rule, a signature) are the architect's alone and are recorded, not asked; a ruling reaches the human only when it changes user-visible behaviour, money semantics or an approved decision. A coder that stops three times for three true facts did nothing wrong; the protocol did.
@@ -10,6 +12,8 @@ The coder then forecasts **cases** (tests) and **lines**. Compute the floor your
10
12
 
11
13
  ## §3 Dispatch — what the brief contains
12
14
 
15
+ Acceptance (`block open --acceptance`) is a YAML/JSON list. Code clauses need `tests: [<test ids>]`. A device check instead uses `- clause: "Check the layout on a phone"` with `kind: human_check` on the next indented line and no `tests` key (even an empty list is refused). These texts are recorded as the block's `human_checks` and skipped by test-coverage checks; every other clause still needs tests.
16
+
13
17
  Run a Codex coder with `codex exec --json` in its worktree, save stdout to a log, and pass that
14
18
  log to `block close --transcript <file>`. A Codex JSON log (every non-empty line is an object
15
19
  with a string `type`, first event `thread.started`) is checked by its commands: command items'
@@ -17,6 +21,10 @@ with a string `type`, first event `thread.started`) is checked by its commands:
17
21
  Agent messages, reasoning, command output (`aggregated_output`) and non-item events are skipped. Any other log
18
22
  is checked line by line, where a mention of a forbidden command or path is a hit.
19
23
 
24
+ `block open` refuses a missing `--owned` or `--tests` entry containing whitespace or a comma outside a `{...}` brace group (exit 2: use spaces, not commas). A glob character does not exempt an outside comma: `src/*,lib` is refused when it is not an existing path. Brace globs like `src/{a,b}`, bracket paths like `app/[id]/page.tsx`, and new plain paths stay allowed under this check. Literal existing file and directory names remain valid.
25
+
26
+ If `run start` cannot launch because another run still has a live worker, the error names that run, its pid and `code-forge run end --run <other-run>`. Process start-time probes use `/bin/ps` when present, falling back to `ps` on PATH.
27
+
20
28
  `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.
21
29
 
22
30
  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.
@@ -60,10 +68,24 @@ Waived or external reviews (`--no-require-reviews`, `review.external`) do not li
60
68
 
61
69
  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`).
62
70
 
63
- **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, follows the selected file loop on every changed file, and prints its `TESTS` line. In fix rounds, follow the selected loop: `wait` uses `forge review-file` and `--wait`, `overlap` enqueues each green file, and `watch` commits each green file. Both `overlap` and `watch` run `forge review-file --wait-block <id> --run <r>` before the sentinel. The per-file cap still counts per file: one block round is one round for each file it touched (`review.md` §5).
71
+ **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). The message lists the block test command next to the project build/typecheck command from the context pack's `build` field (for example, `npm run typecheck`). 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 both commands, follows the selected file loop on every changed file, and prints its `TESTS` line. In fix rounds, follow the selected loop: `wait` uses `forge review-file` and `--wait`, `overlap` enqueues each green file, and `watch` commits each green file. Both `overlap` and `watch` run `forge review-file --wait-block <id> --run <r>` before the sentinel. The per-file cap still counts per file: one block round is one round for each file it touched (`review.md` §5).
64
72
 
65
73
  ## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
66
74
 
75
+ To correct an open block's snapshotted clauses, the human uses `code-forge block amend-acceptance <id> --run <r> --acceptance <file> --reason <text>` with a nonblank reason and the same acceptance format as `block open`. Both terminals and confirmation are required, with no `--yes` and no active autopilot grant. Invalid or unchanged lists write nothing; a successful amendment updates the human checks and signs one `block.acceptance_amended` row with both clause-list hashes. Earlier reviews no longer count at close; review requests, block waits and watch ticks queue a fresh review with the new clauses. Coders must never run this human verb.
76
+
77
+ The human can use `block close <id> --run <r> --decide --reason <text>` to see all waivable findings, unacknowledged agent waivers and acceptable transcript hits in one numbered list, then confirm once. Yes writes the same signed human rows as the individual verbs, with the shared reason for findings and hits, then runs normal close. No writes nothing. Unreviewed or stopped content needing a new review, failed gates, unowned changes and missing proof print `cannot decide here:` and write nothing. Both terminal streams are required; no `--yes`; an active autopilot grant refuses decision close. Coders must leave this step to the human.
78
+
79
+ `close.require_human: true` in the run snapshot also makes plain close require terminal confirmation. It cannot change through `run reload`. An active `close:routine` grant permits unattended close only with signed approvals for every changed file, a signed green gate row, no waiver rows of any kind and no transcript or accepted hits; otherwise `close-not-routine: <reason>` refuses it. A routine close also signs `autopilot.close {block, grant}`. With the key absent or false, ordinary close retains its existing behavior.
80
+
81
+ For a block with `human_checks`, a successful close runs all code gates and writes the normal close rows plus `block.awaiting_human_check {block, checks}`, then sets `awaiting_human_check`. It prints each check and the next command: `code-forge block human-pass <id> --run <r> --note "<device check note>" [--proof <dir-or-file>]`. Awaiting blocks are unfinished: no done file or end hook yet, no worktree removal, and `run end` refuses and names them. Both status commands and the report list `awaiting_human_check: [ids]`. The human checks the device, then types that command in their own terminal (both stdin and stdout must be TTYs); there is no `--yes`, and an active autopilot grant refuses it. The note is required. Proof is optional: when supplied, it must name an existing path and is recorded as a string; when omitted, the row records `proof: null`. One signed `block.human_pass {block, note, proof, checks, by: human}` closes the block and emits its normal end signal; `block stop` is the alternative when a check fails. Coders must never run `human-pass`. A block with no human checks closes as before.
82
+
83
+ With `gates.repeat_new_tests: N` (integer 2..10; absent means off), a passing `gates run --run <r> --block <id>` test gate runs N additional times for only the block's added or changed test files against its base, using a whole `{test_files}` argv item; without that item it prints `repeat_new_tests: skipped (gates.test has no {test_files})`, and without changed tests it does no repeats. A failed repeat makes the gate red with `reason: flaky-test`, the file list and failing output; JSON and signed gate rows carry `repeats: {n, files, failed}`, and the report's forecast section shows the count.
84
+
85
+ With `gates.test` configured, `block close` refuses (exit 1) without a green `gate.done` row for this run and block: `no green gates run; run: code-forge gates run --run <r> --block <id>`. When timestamps allow comparison, the green row must be newer than the last commit touching owned paths after the block base; otherwise any green row suffices. The history lookup must succeed; only a successful empty log means no later commit. Waiving reviews does not lift the test gate; the human confirmation and other bypass diagnostics still apply.
86
+
87
+ `block accept-hit <id> --run <r> --line <n> --transcript <path> --reason <text>` asks the human once for every rule hit on that line and its paired Codex `item.completed` line (same item id), then signs one acceptance per rule and line. Add `--rule <rule-id>` to keep the single-hit behavior.
88
+
67
89
  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:
68
90
 
69
91
  | claim | re-measured by |
@@ -71,7 +93,7 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
71
93
  | 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 |
72
94
  | 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 |
73
95
  | tests green | every gate command run once in the export, each exit status read |
74
- | 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` |
96
+ | clauses covered | every code clause names ≥ 1 test id; each test exists and passed in this run; `kind: human_check` clauses name no tests and wait for a human after code gates; S1 `scope` on the hunks outside the named tests says `false` |
75
97
  | test count | the filtered run's delta equals the new tests' own count |
76
98
  | line and case forecast | `git diff --stat` net lines against `--lines`: > 1.25 × ⇒ stop (`===BLOCK <id> FAILED: forecast exceeded, split plan <id>a/<id>b===`); cases > 1.5 × forecast ⇒ a `forecast.cases_exceeded` WARN row, never a stop |
77
99
  | 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 |
@@ -85,6 +107,8 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
85
107
 
86
108
  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.
87
109
 
110
+ After close, if the base moved, run `code-forge block rebase <id> --run <r>` before the fast-forward merge.
111
+
88
112
  **When a file stops** (`review_cap`, `l3_patch_exhausted`, `split_required`, …), the block cannot close. The stop's `exits` name the ways out. Take them in this order; `review.md` §5 gives each one's rules and refusals:
89
113
 
90
114
  1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then print the exact command, `forge block reopen <id> --file <path> --run <r> --reason <text>` with the human's reason, and the human runs it in their own terminal, exactly like `block waive`. For this comparison, `block waive` means its human path. Never run it yourself, and never for a coder (it is on the coder forbidden list). After it, send the coder's `review-file` again: the next round is a full round 1 when the content differs from what the stop reviewed; on the stop's own content the stop repeats. For known stop content, `reopen-same-content` means the content is still what the stop reviewed: offer the waive instead. A legacy stop with unknown content accepts a current hash without a verified `review.round` of this run/block/file and records `from_hash: null`. If that hash already has such a round, change the file and reopen, or the owner can use `block close --no-require-reviews`; waivers at an old hash cannot clear the current one.
@@ -110,6 +134,8 @@ Both `autonomy` keys default to false. Whatever they say, a block's branch is ha
110
134
 
111
135
  **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.
112
136
 
137
+ `report --slug <s>`, `ledger tail --slug <s>` and `ledger calibration --slug <s>` exit 1 when the ledger file is absent, naming the slug and sorted known slugs (rotations counted once, or `none`). An existing empty ledger succeeds with its empty output.
138
+
113
139
  **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.
114
140
 
115
141
  ## §8 Definition of Done (per block)
@@ -1,12 +1,16 @@
1
1
  # Review — per file, fresh L2 session, bounded rounds
2
2
 
3
+ A light block reviews all owned changed files in one packet, with each path heading and its full diff, using `review.budgets.full_in` and one full reviewer session. Findings name their file; each file receives signed `review.round` and, when clean, `review.approved` rows for its own content hash. `review-file --wait-block` joins those file tickets as usual. Open critical/warning findings end the round: fix them, then join again for one new batch of all changed files. At most two batches run per block, including across worker restarts; subsequent changes use the normal per-file loop. Keep large-file routing configured for the long-context reviewer.
4
+
3
5
  ## §1 The trigger is a CLI call or commit watch, and the file set is complete
4
6
 
5
- `forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The same content answers the cached result; `review-file <path> --block <id> --fresh` (B75) drops that cached result and queues the ticket again only when it never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`). Use it after such a transient answer to get a real review; a coder may run it, and with no cached result it is a plain `review-file`. A cached `review-state-tampered` is refused `fresh-tampered` (only the owner's `block close --no-require-reviews` gets past it); any other cached result is refused `fresh-needs-reopen` (the human's exits are `block reopen` and `block waive`). A refused marker is read in place and never moved; only a re-runnable one is claimed (an atomic rename, then a byte compare) and dropped. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. Without commit watch, a coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
7
+ Each file's `outcome` is `approved`, `findings`, `stopped`, `split_required` or `unavailable` from its completion marker, and `null` while `pending` or `not_queued`; a live join queues an owned changed file even if it has no earlier ticket. A `split_required` outcome returns `done: false`, `reason: split_required` and `next` naming the file: the owner raises `review.budgets`, runs `run reload`, then runs `review-file <path> --fresh`.
8
+
9
+ `forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The same content answers the cached result; `review-file <path> --block <id> --fresh` (B75) drops that cached result and queues the ticket again only when it never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`), or when it is a `split_required` stop whose measured lens budget changed since it was measured (see below). With an unchanged budget, a counted `split_required` stop is still refused `fresh-needs-reopen`. Use it after such a transient answer or budget change to get a real review; a coder may run it, and with no cached result it is a plain `review-file`. A cached `review-state-tampered` is refused `fresh-tampered` (only the owner's `block close --no-require-reviews` gets past it); any other cached result is refused `fresh-needs-reopen` (the human's exits are `block reopen` and `block waive`). A refused marker is read in place and never moved; only a re-runnable one is claimed (an atomic rename, then a byte compare) and dropped. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. Without commit watch, a coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
6
10
 
7
11
  With `review.file_loop: watch` (B104), commit each owned file after its tests are green and continue coding. Watch replaces per-file enqueueing. Before the completion sentinel, including fix rounds, run `forge review-file --wait-block <id> --run <r> [--max 90s]`, inspect finished tickets with `--wait <ticket>`, and fix findings. The worker watches the block worktree HEAD (otherwise repository HEAD) and reads owned content from git objects. A file's first commit is queued at once with the same ticket id as a manual request. Later commits of a file that already has a ticket of this block are held until `--wait-block`. If the last review found problems and the file changed blind, the join does not recheck it. It answers `status: findings` with that ticket and records delivery once. Read the findings with `review-file --wait <ticket>`, fix them, and the next join rechecks the latest content once. A file whose last review approved still queues its latest content at the join. A stopped file is not requeued. Each held file writes one `watch.held {block, file, content_hash}` row per new head. Older pending tickets are still marked `refused`, `reason: superseded`. A running ticket finishes for its old hash, which cannot approve newer bytes; `--wait-block` joins the current ticket per file. The join reruns a stale, unavailable or round-1 `split_required` review of the current content once. The file stays `pending` until that rerun finishes. No manual `--fresh` is needed. The retry is recorded per ticket across joins. If the rerun is stale again, later joins report `done`; `--wait <ticket>` shows that stale status. `--fresh` refuses a superseded marker; enqueueing that content again removes only its superseded marker so it can run, while a real result stays cached. `queue/watch/<block>.json` stores the last seen commit; missing or unreadable state restarts at `base_sha` with idempotent tickets. A rewound tick queues nothing and drops the last seen commit so the next tick restarts at `base_sha`. A `watch.skipped {block, reason}` row is written once per reason until a healthy head clears the skip reasons: `unreadable_head` (head, tree or objects cannot be read), `not_descendant` (head does not descend from the block base), `rewound` (head no longer descends from the last seen commit), or `unowned_path` (unowned paths are ignored; owned paths still queue). A skip never fails the block. After a `watch.skipped` row, use `forge review-file <path> --block <id> --run <r>` when the file needs a manual review; use `forge review-file <fix file> --block <id> --run <r> --answers <ids> --of <source>` for a fix in another owned file that answers findings. Set watch before starting the worker. Without the `review.file_loop: watch` setting, the default `wait` loop performs no watch work or git calls.
8
12
 
9
- A cached round-1 `split_required` ran no session and counted no round. Plain `review-file` drops its marker and re-queues the same ticket with `retry: true`; `--fresh` also re-queues it (B110). After the human raises the budget and runs `run reload`, the worker re-plans with the live config; the ticket id stays the same. If it still does not fit, it returns `split_required` again with no session or cost. A recheck stop (`status: stopped`, `stopped: split_required`) has a `review.cap` row and still needs `block reopen` or `block waive`; plain review keeps it cached and `--fresh` refuses `fresh-needs-reopen`.
13
+ A cached round-1 `split_required` ran no session and counted no round. Plain `review-file` drops its marker and re-queues the same ticket with `retry: true`; `--fresh` also re-queues it (B110). After the human raises the budget and runs `run reload`, the worker re-plans with the live config; the ticket id stays the same. If it still does not fit, it returns `split_required` again with no session or cost. For a recheck `split_required` stop, raising its lens budget and running `run reload` is enough for plain `review-file` or `--fresh` to review the same content again, printing the old and new budgets; with an unchanged budget it stays cached and `--fresh` still refuses `fresh-needs-reopen`.
10
14
 
11
15
  A ticket whose session breaks is not a review. A broken result is a `schema` miss or a timed-out attempt (B91): a timeout counts as a broken result, like a schema miss. Misses and timeouts of the same packet, level and role are counted together, so one miss plus one timeout is two. A packet's second broken result is tried once on the ticket's second level, in the same ticket, only when a second level exists (`review.second_levels.<level>`, or for a routed ticket the rule's `second`) and never in consensus mode (§4).
12
16
 
@@ -14,6 +18,8 @@ Under `proof.isolation: worktree` (`proof.md` §3) the coder calls `review-file`
14
18
 
15
19
  **The owner's two ways out of the per-file review.** (1) `forge block close <id> --no-require-reviews`, typed by the owner at a terminal (`code.md` §5). (2) `review.external: {required: true, name: <reviewer>}` in `.code-forge.yml`, set before `run start`, when an outside reviewer reviews each block (B58). With `required: true`, `run start` refuses a missing or blank `name`, and writes a signed `review.external` row with the reviewer's name. `run reload` refuses to add, remove or change the key. With the key, `block close` does not check the review rows (a `not_approved` file included) or the report's ticket ids, needs no terminal and works under an autopilot grant. When the block closes, it writes a signed `gate.reviews_waived {by: config, reviewer}` row just before `block.close`; a refused close writes no such row. Unowned changes, `unproven`, `no_changes`, the transcript grep and the registry's orphans still refuse. No block may own `.code-forge.yml` while the key is on: `block open` and `block claim` refuse it. `block open`, `block claim` and `block close` read the run's config snapshot to find the key, so a snapshot that does not match its hash refuses them with `config-snapshot`, with or without the key. Never set or remove the key yourself: it is the owner's decision.
16
20
 
21
+ The orchestrator can add a review worker to an active run with `run start --add-worker --run <id>`, up to `review.workers` (1–4, default 1); coders never run this verb.
22
+
17
23
  ## §2 Depth by risk (adaptive mode)
18
24
 
19
25
  The topology is decided in one place (`planSessions`). Consensus (§6) runs only when `review.multimodel` is on **and** the file's risk is above `review.single_reviewer_max_risk` (default 1) **and** the block is not a `docs`/`contract` block (`decisions.md` §5) unless `review.multimodel_for_docs` is true (default false). Everything else is adaptive, below. Why: a consensus review of a four-file docs block cost $16.70 — review cost dominates, and a low-risk file gains nothing from a second provider.
@@ -99,6 +105,8 @@ Only real rechecks count toward the round cap, per-level ladder and stall rules
99
105
 
100
106
  Rechecks are never degraded by the budget: a round-2+ packet is already the minimum.
101
107
 
108
+ Recheck packets include each open finding's evidence; reviewers keeping one open must supply `current_lines` quoting current file lines (≤ 300 characters): a nonempty quote absent from the current file after whitespace normalisation closes it as stale with a signed `review.stale_finding` row, while missing or empty quotes keep the finding open and count as before. A stale finding does not make the round heavy, and a round whose open findings all close as stale passes. Only new ids in a recheck repeating an earlier round's trimmed claim and evidence for the same file are listed in `review.round.repeated`; rounds containing only repeated or stale findings add no per-level or warning-round weight and cannot trigger escalation on their own.
109
+
102
110
  ## §6 Consensus mode (`review.multimodel` on, risk above `review.single_reviewer_max_risk`, not a docs/contract block unless `review.multimodel_for_docs`)
103
111
 
104
112
  Two L2 reviewers, one per effective provider (`resolve(L2)` and `review.second_levels`), each a fresh blind session with the `full` lens; then a fresh L3 judge **from the third provider** reads the two reports and returns the findings plus `agreement: both|A-only|B-only`. The judge's `fix_now` list is final. Second provider unavailable at run start ⇒ `consensus unavailable: <reason>`; ask the human once per run whether to continue adaptive, stop when non-interactive. Rechecks run on the provider that raised the finding; §5 applies unchanged.
@@ -10,6 +10,8 @@
10
10
 
11
11
  Rendered into every coder spawn as the CLI's own deny rules (path rules widen a directory to `<dir>/**`; a read-write path denies read, edit and write); carried verbatim in the brief; refused by the package's own process spawner; and at block close the transcript (captured stdout, the harness log, or the Solo output) is grepped for the forbidden verbs **and paths**. A hit fails the gate as `rule_break`. Only the human can accept a false hit with `block accept-hit`, after terminal confirmation. That verb is also coder-forbidden. It writes one signed `rule_break.accepted` row for the run, block, rule and SHA-256 of the exact transcript line text. Close verifies the signature and matches by line content, so moving the line does not invalidate it. Unsigned, tampered or other-run or other-block rows never count. A ledger `rule_break` row still refuses. Where a CLI cannot load a rules file, `doctor` says so and the transcript grep remains the detector.
12
12
 
13
+ In Codex `exec --json` transcripts, node, Python, Ruby and Perl heredoc or quoted inline script bodies without a process-spawn word are not scanned for command or contains rules. Backticks and `$(` count when the shell expands the body (an unquoted heredoc delimiter or double-quoted inline script), or for Ruby/Perl; `%x` counts for Ruby/Perl, and `qx` for Perl. Quoted or backslash-escaped heredoc delimiters and single-quoted inline scripts prevent shell expansion. A single sh/bash/zsh/dash/ksh wrapper with `-c`, `-lc`, `-ic` or `-l -c` and one quoted script is decoded before this check; its prefix remains scanned. Path rules still apply inside those bodies. Nested or malformed wrappers, shell and unknown-command heredocs, unterminated heredocs and the shell text outside interpreter bodies are scanned whole; the plain-text transcript grep is unchanged.
14
+
13
15
  ## §3 Signed ledger rows — the threat model, stated honestly
14
16
 
15
17
  `block unwaive` and `block ack-waivers` are human-only and coder-forbidden with both `forge` and `code-forge` prefixes. Both require a yes at a terminal and have no `--yes`. No terminal exits 2 with no row. A no, cancel or pre-check refusal writes nothing. Only verified human undo and acknowledgement rows count at close.
@@ -10,7 +10,7 @@
10
10
  # copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
11
11
  # equals package.json's version.
12
12
  # The shim holds no logic of its own: every verb, flag and message is the CLI's.
13
- PINNED_VERSION="0.14.1"
13
+ PINNED_VERSION="0.15.1"
14
14
  set -eu
15
15
 
16
16
  self="$0"
@@ -33,6 +33,8 @@ Proof is behaviour, not text: never read a source file in a test to assert on it
33
33
 
34
34
  ## Final report — the close prints the ticket ids
35
35
 
36
+ If the sandbox refuses process probes with EPERM errors, rerun the block tests outside the sandbox. Keep and report only the final run. Name the final log file in the report. A saved log of a refused sandbox run is not proof either way.
37
+
36
38
  Run the block's test command (`test_command`) and print its result on a line of its own, exactly:
37
39
 
38
40
  `TESTS <command> exit=<n>`
@@ -54,7 +56,7 @@ Or `===BLOCK <id> FAILED: <reason>===`.
54
56
 
55
57
  ## Fix-round brief (round ≥ 2, or a respawn after a climb)
56
58
 
57
- The open findings by id, the fix history, the files they sit in. Fix only those hunks, then follow the selected file loop on **every file the round changed** (`wait`: run `forge review-file <path> --block <id> --run <r>` and `--wait`; `overlap`: enqueue each green file and continue; `watch`: commit each green file; both `overlap` and `watch`: run `forge review-file --wait-block <id> --run <r>` before the sentinel) — a fix the reviewer never saw is not a fix. A fix made in another owned file is sent with `--answers <ids> --of <source>` (The file loop). The round's final report follows the same rule as the first: it needs no ticket lines. Re-run the block's test command after the fixes and print a fresh `TESTS <command> exit=<n>` line just before the round's sentinel: a TESTS line from an earlier report does not count. In a fix round, you may use `code-forge gates run --run <r> --block <id> --changed` as the block's test command; the TESTS line names the command actually run (substitute the run and block ids). Any green `code-forge gates run --run <r> --block <id>` records the owned file hashes on `gate.done`, including runs without `--changed`; when `--slug` is omitted, these rows go to the run's project ledger. More than 200 owned files means no record, so the next `--changed` falls back with `no_baseline`; unreadable inputs also leave no record. `{test_files}` expands to changed tests and stem-paired tests of changed sources; `{changed_files}` expands to changed lint/format paths. Deleted files still count for pairing and the threshold, but both tokens receive only files that exist now. Both tokens must be whole argv items and cannot be the command or the only item; config validation refuses a gate whose command or only item is `{changed_files}`. Without `--changed`, `{changed_files}` is dropped. Gates without tokens run as before. Rows carry `scope: changed`, including fallback rows with a `reason`: `no_baseline` uses today's scope, `unpaired` uses today's test scope when a changed source has no test pair, `threshold` runs the full suite above `gates.full_suite_threshold_files` changed files, and `no_changes` uses today's scope when nothing is left to run. The orchestrator runs the full `code-forge gates run` before `block close` (D2).
59
+ The open findings by id, the fix history, the files they sit in. The message lists the block test command next to the project build/typecheck command from the context pack's `build` field (for example, `npm run typecheck`). Fix only those hunks, then follow the selected file loop on **every file the round changed** (`wait`: run `forge review-file <path> --block <id> --run <r>` and `--wait`; `overlap`: enqueue each green file and continue; `watch`: commit each green file; both `overlap` and `watch`: run `forge review-file --wait-block <id> --run <r>` before the sentinel) — a fix the reviewer never saw is not a fix. A fix made in another owned file is sent with `--answers <ids> --of <source>` (The file loop). The round's final report follows the same rule as the first: it needs no ticket lines. Run both commands after the fixes and before the round's sentinel. Print a fresh `TESTS <command> exit=<n>` line just before the round's sentinel: a TESTS line from an earlier report does not count. In a fix round, you may use `code-forge gates run --run <r> --block <id> --changed` as the block's test command; the TESTS line names the command actually run (substitute the run and block ids). Any green `code-forge gates run --run <r> --block <id>` records the owned file hashes on `gate.done`, including runs without `--changed`; when `--slug` is omitted, these rows go to the run's project ledger. More than 200 owned files means no record, so the next `--changed` falls back with `no_baseline`; unreadable inputs also leave no record. `{test_files}` expands to changed tests and stem-paired tests of changed sources; `{changed_files}` expands to changed lint/format paths. Deleted files still count for pairing and the threshold, but both tokens receive only files that exist now. Both tokens must be whole argv items and cannot be the command or the only item; config validation refuses a gate whose command or only item is `{changed_files}`. Without `--changed`, `{changed_files}` is dropped. Gates without tokens run as before. Rows carry `scope: changed`, including fallback rows with a `reason`: `no_baseline` uses today's scope, `unpaired` uses today's test scope when a changed source has no test pair, `threshold` runs the full suite above `gates.full_suite_threshold_files` changed files, and `no_changes` uses today's scope when nothing is left to run. The orchestrator runs the full `code-forge gates run` before `block close` (D2).
58
60
 
59
61
  ## Context pack
60
62
 
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  /** Scopes an owner may delegate. */
16
- export const ALLOWABLE_SCOPES = Object.freeze(['waive:warning', 'waive:nit', 'round:extra', 'model:choose']);
16
+ export const ALLOWABLE_SCOPES = Object.freeze(['waive:warning', 'waive:nit', 'round:extra', 'model:choose', 'close:routine']);
17
17
 
18
18
  /** What each allowable scope lets the delegate do (status output and docs). */
19
19
  export const ALLOWABLE_WHAT = Object.freeze({
@@ -21,6 +21,7 @@ export const ALLOWABLE_WHAT = Object.freeze({
21
21
  'waive:nit': 'waive a nit finding',
22
22
  'round:extra': 'one fix round past the cap for a file',
23
23
  'model:choose': 'choose the coder level for a block',
24
+ 'close:routine': 'close a routine block',
24
25
  });
25
26
 
26
27
  /**