@codedology/code-forge 0.15.3 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/docs/reference/blocks.json +9 -1
- package/docs/reference/config.md +11 -1
- package/native/README.md +10 -0
- package/native/bin/darwin-arm64/code-forge-native +0 -0
- package/native/bin/darwin-x64/code-forge-native +0 -0
- package/native/bin/linux-arm64/code-forge-native +0 -0
- package/native/bin/linux-x64/code-forge-native +0 -0
- package/package.json +4 -1
- package/schema/code-forge.schema.json +26 -1
- package/skill/references/autopilot.md +6 -1
- package/skill/references/code.md +37 -0
- package/skill/references/plan.md +5 -1
- package/skill/references/review.md +46 -8
- package/skill/scripts/forge +1 -1
- package/src/cli/block.mjs +284 -11
- package/src/cli/doctor.mjs +32 -1
- package/src/cli/help.mjs +1 -0
- package/src/cli/native-status.mjs +13 -0
- package/src/cli/review-file.mjs +121 -14
- package/src/cli/status.mjs +43 -9
- package/src/doctor/index.mjs +11 -0
- package/src/doctor/worker-probe.mjs +1 -1
- package/src/review/agent-waiver.mjs +95 -8
- package/src/review/gate-check.mjs +13 -9
- package/src/review/model-stats.mjs +125 -0
- package/src/review/packet.mjs +54 -3
- package/src/review/reopen.mjs +38 -13
- package/src/review/route.mjs +58 -4
- package/src/session/facts.mjs +19 -7
- package/src/session/plan-check.mjs +4 -1
- package/src/state/block.mjs +39 -16
- package/src/util/native.mjs +131 -0
- package/src/worker/loop.mjs +144 -16
- package/src/worker/queue.mjs +1 -1
- package/src/worker/watch.mjs +16 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.4] — 2026-10-31
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Go core, step 1: `native/` Go library (canonical JSON, row signing, ticket ids, ledger read) and a golden compat kit made from the Node functions; Go tests prove byte-equal output (B180)
|
|
14
|
+
- `block land <id>` runs review join, changed-file gates and close in one step, stopping at the first failure with the next command; `--merge` also merges the block branch into its base (B185)
|
|
15
|
+
- The npm package ships a prebuilt Go helper (`code-forge-native`, darwin/linux, arm64/x64; built at release) with `version` and `verify-ledger`; `doctor` shows it, and Node is used when it is missing or `CODE_FORGE_NATIVE=0` (B181)
|
|
16
|
+
- `block land` and `block close --auto-claim` claim and review a file changed outside the owned list (unless another open block owns it), and claims are kept when a block is reopened with `--attempt N` (B186)
|
|
17
|
+
- Review packets always keep the file's import header, the judge packet shows the cited `path:line` windows (ranges like `a-b` too), and `block reopen --yes` works on an open file with findings when the judge agrees (B192)
|
|
18
|
+
- `block land --max <dur>` (default 30m) waits for reviews, retries the join up to 3 times while a findings round is open, and on timeout stops with `wait_timeout` and a next line that names a larger `--max` (B190)
|
|
19
|
+
- Merge gate: `block land --merge` runs the full suite on the merged result in a temporary worktree first; a red suite or a conflict leaves main unchanged. Skip with `--no-merge-gate` or `gates.merge_gate: false` (B189)
|
|
20
|
+
- A file whose review stopped with findings is reviewed again automatically when a fix changes its content; unchanged or waived files are not re-queued (B194)
|
|
21
|
+
- Every Go command the tests run sets `CGO_ENABLED=0`, so `go test` passes on macOS without extra setup; `native/README.md` says how to test the Go code (B195)
|
|
22
|
+
- `block waive --yes` now says the evidence may cite `:line or :start-end`, the same as `block reopen --yes`; a range citation is accepted (B193)
|
|
23
|
+
- `forge facts` cuts an excerpt over 200 characters (ending with `…`) instead of refusing the whole sheet; repo-relative paths with an extension or a top-level folder, and absolute paths, are now path claims; `§ 12.1` is no longer a version claim; a long brief with almost no claims prints a warning (#29, B197)
|
|
24
|
+
- Critical findings still need a human waiver by default; under an active autopilot grant, `block waive --yes` may waive a critical when the reason cites a passing acceptance test and the finding's lines and the judge agrees; the row records `by: agent` and `autopilot: true` (B196)
|
|
25
|
+
- Go core, step 3: `code-forge-native daemon` serves `wait-block`, `block-tickets` and `status` over a Unix socket with a ledger tail index, a queue index and a cached worker start time; answers match the Node commands (compat tests). Not used by the CLI yet (B182)
|
|
26
|
+
- The full test suite is green again: reopen output keeps its old text when no claims are kept, the config doc is current, and a plan clause that only cites a planned test file is no longer refused. (B204)
|
|
27
|
+
- The native daemon status helper now lives with the other command files, so the import-order rule passes again. (B218)
|
|
28
|
+
- Commit watch waits until a burst of commits ends (15 s quiet, 120 s at most) and reviews only the last commit, so a coder making several quick commits no longer starts extra reviews. (B211)
|
|
29
|
+
- The worker and `status` use the native Go daemon when it runs and fall back to Node when it does not; status output is the same in every mode, and `doctor` shows the daemon state. (B183)
|
|
30
|
+
- Two block tests now give the commit watch its second quiet tick, so they pass with the B211 debounce. (B219)
|
|
31
|
+
- The worker runs up to 6 review tickets at once (was 3), with at most 4 sessions per provider (`review.provider_parallel`); tickets waiting for a provider still count toward the limit. (B208)
|
|
32
|
+
- code-forge picks the primary reviewer from this project's measured speed, availability and accuracy (`review.adaptive`, on by default). A matching route still competes on performance unless it has `pin: true`. (B207)
|
|
33
|
+
|
|
9
34
|
## [0.15.3] — 2026-10-30
|
|
10
35
|
|
|
11
36
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"plans/code-forge-plan-v1.3.md §10.4",
|
|
5
5
|
"plans/NIGHT-LOG.md"
|
|
6
6
|
],
|
|
7
|
-
"block_count":
|
|
7
|
+
"block_count": 165,
|
|
8
8
|
"baseline_ids": ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
|
|
9
9
|
"baseline_note": "Wave 0-1 blocks, landed before v1.3; depends_on may name them, but they have no entry under blocks.",
|
|
10
10
|
"blocks": {
|
|
@@ -3036,6 +3036,14 @@
|
|
|
3036
3036
|
],
|
|
3037
3037
|
"depends_on": ["B151"],
|
|
3038
3038
|
"note": "F007 step 2; also amends the baseline-owned src/cli/validate.mjs to check the pipeline after config validation. Its pipeline import is recorded as an exact file/directory exemption in test/imports-order.test.mjs, following the existing later-block CLI amendments."
|
|
3039
|
+
},
|
|
3040
|
+
"B182": {
|
|
3041
|
+
"title": "native daemon status companion",
|
|
3042
|
+
"wave": 15,
|
|
3043
|
+
"level": "L2",
|
|
3044
|
+
"status": "landed",
|
|
3045
|
+
"owned": ["src/cli/native-status.mjs"],
|
|
3046
|
+
"depends_on": ["B64a"]
|
|
3039
3047
|
}
|
|
3040
3048
|
}
|
|
3041
3049
|
}
|
package/docs/reference/config.md
CHANGED
|
@@ -11,6 +11,8 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
11
11
|
| Key | Type | Required | Default | Notes |
|
|
12
12
|
|---|---|---|---|---|
|
|
13
13
|
| `version` | const `1` | yes | — | |
|
|
14
|
+
| `native` | object | | — | |
|
|
15
|
+
| `native.daemon` | boolean | | `true` | Use the optional native daemon for run status and block waits. False uses Node only; CODE_FORGE_NATIVE=0 also disables it. |
|
|
14
16
|
| `project` | object | | — | |
|
|
15
17
|
| `project.name` | string | | — | |
|
|
16
18
|
| `project.slug` | string | | — | B50: the ledger name. Default: the project root's folder name (the nearest .code-forge.yml up to the git top level, else the git top level), so every verb run from a subfolder writes to the same ledger. |
|
|
@@ -115,9 +117,15 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
115
117
|
| `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). |
|
|
116
118
|
| `review.batch_small_files` | boolean | | `true` | |
|
|
117
119
|
| `review.session_timeout_s` | integer | | `300` | min 1 |
|
|
120
|
+
| `review.adaptive` | object | | — | Choose the primary reviewer by availability × (1 − waived share) ÷ median session seconds from this project's ledger. No history keeps configured order; a matching explicit route has configured priority but competes on performance unless pinned. Disabling restores legacy routing exactly. |
|
|
121
|
+
| `review.adaptive.enabled` | boolean | | `true` | |
|
|
122
|
+
| `review.adaptive.window_days` | integer | | `14` | min 1 |
|
|
123
|
+
| `review.adaptive.min_sessions` | integer | | `10` | min 1 |
|
|
124
|
+
| `review.adaptive.explore` | number | | `0.1` | min 0; max 1; Share of tickets given to a less-measured candidate, deterministically hashed from the ticket id. |
|
|
118
125
|
| `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. |
|
|
119
126
|
| `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). |
|
|
120
127
|
| `review.routes[].level` | enum: `L2`, `L3` | | `"L2"` | B90: the level the rule replaces, L2 (the reviewers) or L3 (the judge). |
|
|
128
|
+
| `review.routes[].pin` | boolean | | `false` | Force this matching route's model instead of adaptive primary-reviewer selection. |
|
|
121
129
|
| `review.routes[].model` | string | yes | — | minLength 1 |
|
|
122
130
|
| `review.routes[].effort` | string | | — | |
|
|
123
131
|
| `review.routes[].provider` | enum: `anthropic`, `openai`, `xai` | | — | |
|
|
@@ -143,7 +151,8 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
143
151
|
| `review.provider_concurrency.anthropic` | integer | | `4` | min 1 |
|
|
144
152
|
| `review.provider_concurrency.openai` | integer | | `2` | min 1 |
|
|
145
153
|
| `review.provider_concurrency.xai` | integer | | `2` | min 1 |
|
|
146
|
-
| `review.parallel_tickets` | integer | | `
|
|
154
|
+
| `review.parallel_tickets` | integer | | `6` | min 1; max 16; B42: how many review tickets the worker runs at once (oldest first); 1 = one at a time. Tickets for the same file never overlap. |
|
|
155
|
+
| `review.provider_parallel` | integer | | `4` | min 1; max 16; Maximum simultaneous review sessions per provider in the worker. Tickets waiting for this cap still count toward parallel_tickets; stricter provider_concurrency limits still apply. |
|
|
147
156
|
| `review.fair_share` | boolean | | `false` | Give each free ticket slot to the block with the fewest running tickets; ties stay oldest first. |
|
|
148
157
|
| `review.file_loop` | enum: `wait`, `overlap`, `watch` | | `"wait"` | After each green file, enqueue its review. wait (default): wait for its ticket before the next file. overlap: continue coding, then run review-file --wait-block <id> --run <run> before the completion sentinel; inspect findings with --wait <ticket>. watch: commit each green file instead of running review-file. A file's first commit is queued at once. Later commits of a file that already has a ticket of this block are held (a watch.held row) until --wait-block, which queues the latest content once before the sentinel. Older pending tickets are still superseded. |
|
|
149
158
|
| `review.agent_waivers` | object | | — | Human acknowledgement of counted agent waivers. Set before run start; run reload refuses to change require_ack. |
|
|
@@ -165,6 +174,7 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
165
174
|
| `gates.lint` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. With gates run --run --block --changed, {test_files} expands to changed tests plus stem-paired tests of changed sources, and {changed_files} expands to changed paths for lint or format. 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}. Deleted files count for pairing and the threshold, but both tokens receive only files that exist now. Without --changed, {changed_files} is dropped. Changed rows carry scope changed; narrowed rows have no reason. Only fallback rows carry a reason: no_baseline uses today's scope, unpaired uses today's test scope, threshold runs the full suite above the changed-file threshold, and no_changes uses today's scope when nothing is left to run. Gates without either token run unchanged. Any green gates run --run --block records 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. A fix-round TESTS line may use --changed; the orchestrator runs full gates before block close. |
|
|
166
175
|
| `gates.types` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. With gates run --run --block --changed, {test_files} expands to changed tests plus stem-paired tests of changed sources, and {changed_files} expands to changed paths for lint or format. 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}. Deleted files count for pairing and the threshold, but both tokens receive only files that exist now. Without --changed, {changed_files} is dropped. Changed rows carry scope changed; narrowed rows have no reason. Only fallback rows carry a reason: no_baseline uses today's scope, unpaired uses today's test scope, threshold runs the full suite above the changed-file threshold, and no_changes uses today's scope when nothing is left to run. Gates without either token run unchanged. Any green gates run --run --block records 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. A fix-round TESTS line may use --changed; the orchestrator runs full gates before block close. |
|
|
167
176
|
| `gates.format` | array<string> \| object \| null | | — | An argv array (never a shell string), an {argv, env} object, or null. An argv item that is exactly {test_files} is replaced by the block's test files. It must be a whole item. It cannot be the command or the only item. The test files are the block open --tests paths, else the owned entries that are concrete test files. The item is dropped, and the full suite runs, in two cases. The block owns more files than full_suite_threshold_files (reason threshold). Or no test file is known, or the gate runs without --run/--block (reason no_tests). The gate.red and gate.done rows then carry scope related with test_count, or scope full with reason. With gates run --run --block --changed, {test_files} expands to changed tests plus stem-paired tests of changed sources, and {changed_files} expands to changed paths for lint or format. 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}. Deleted files count for pairing and the threshold, but both tokens receive only files that exist now. Without --changed, {changed_files} is dropped. Changed rows carry scope changed; narrowed rows have no reason. Only fallback rows carry a reason: no_baseline uses today's scope, unpaired uses today's test scope, threshold runs the full suite above the changed-file threshold, and no_changes uses today's scope when nothing is left to run. Gates without either token run unchanged. Any green gates run --run --block records 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. A fix-round TESTS line may use --changed; the orchestrator runs full gates before block close. |
|
|
177
|
+
| `gates.merge_gate` | boolean | | `true` | With block land --merge, run the configured full suite on a trial merge in a temporary detached worktree under the run worktree root before merging into main. Failure or conflict leaves main unchanged and removes the temporary tree. False or --no-merge-gate skips this check with a warning. Without --merge the gate does not run. |
|
|
168
178
|
| `gates.kinds` | object | | — | Optional gate sets for code, docs and contract blocks. Each value is an array of fixed gate names (test, lint, types, format) or gates.custom[].name values. Unknown names and explicitly null fixed gates are refused with the kind and name. With gates run --run R --block B, the declared block kind wins; otherwise the kind is inferred from owned files using block open's rules. An entry runs only its listed gates; every other configured gate is reported skipped with reason not in gates.kinds.<kind>. A missing entry runs all gates as usual. An empty array runs no commands, reports 0 gates for kind <kind>, and writes gate.done with all_ok: true so block close can proceed to its remaining checks. Light blocks keep their light rules and ignore kinds. A gates run without --block ignores kinds. Example: gates: {types: [npm, run, typecheck], kinds: {docs: [types], contract: []}}. |
|
|
169
179
|
| `gates.kinds.code` | array<string> | | — | |
|
|
170
180
|
| `gates.kinds.docs` | array<string> | | — | |
|
package/native/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Native accelerator
|
|
2
|
+
|
|
3
|
+
`native/` contains the optional Go accelerator and compatibility packages for code-forge.
|
|
4
|
+
The build is pure Go, uses only the standard library, and keeps CGO off (`CGO_ENABLED=0`).
|
|
5
|
+
This also avoids the macOS 26 LC_UUID linker error during local tests.
|
|
6
|
+
Test from the repository root:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
cd native && CGO_ENABLED=0 go vet ./... && CGO_ENABLED=0 go test ./...
|
|
10
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codedology/code-forge",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
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",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin",
|
|
26
|
+
"native/bin",
|
|
26
27
|
"src",
|
|
27
28
|
"schema",
|
|
28
29
|
"skill",
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
"!scripts/changelog.mjs",
|
|
34
35
|
"!scripts/known-fix.mjs",
|
|
35
36
|
"!scripts/bench-review.mjs",
|
|
37
|
+
"!scripts/build-native.mjs",
|
|
38
|
+
"!scripts/compat-golden.mjs",
|
|
36
39
|
"README.md",
|
|
37
40
|
"CHANGELOG.md",
|
|
38
41
|
"LICENSE"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"$comment": "version/provider/levels are REQUIRED with NO schema-level default (a config omitting them is refused here) — `migrate.mjs` fills a MISSING `version` to CURRENT_VERSION before this schema ever runs (so an on-disk file may omit `version`), but `provider` has no such auto-fill anywhere and must be present in the loaded document.",
|
|
10
10
|
"properties": {
|
|
11
11
|
"version": { "type": "integer", "const": 1 },
|
|
12
|
+
"native": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"daemon": { "type": "boolean", "default": true, "description": "Use the optional native daemon for run status and block waits. False uses Node only; CODE_FORGE_NATIVE=0 also disables it." }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"project": {
|
|
13
20
|
"type": "object",
|
|
14
21
|
"additionalProperties": false,
|
|
@@ -166,6 +173,17 @@
|
|
|
166
173
|
"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)." },
|
|
167
174
|
"batch_small_files": { "type": "boolean", "default": true },
|
|
168
175
|
"session_timeout_s": { "type": "integer", "minimum": 1, "default": 300 },
|
|
176
|
+
"adaptive": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"additionalProperties": false,
|
|
179
|
+
"description": "Choose the primary reviewer by availability × (1 − waived share) ÷ median session seconds from this project's ledger. No history keeps configured order; a matching explicit route has configured priority but competes on performance unless pinned. Disabling restores legacy routing exactly.",
|
|
180
|
+
"properties": {
|
|
181
|
+
"enabled": { "type": "boolean", "default": true },
|
|
182
|
+
"window_days": { "type": "integer", "minimum": 1, "default": 14 },
|
|
183
|
+
"min_sessions": { "type": "integer", "minimum": 1, "default": 10 },
|
|
184
|
+
"explore": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.1, "description": "Share of tickets given to a less-measured candidate, deterministically hashed from the ticket id." }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
169
187
|
"routes": {
|
|
170
188
|
"type": "array",
|
|
171
189
|
"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.",
|
|
@@ -176,6 +194,7 @@
|
|
|
176
194
|
"properties": {
|
|
177
195
|
"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)." },
|
|
178
196
|
"level": { "type": "string", "enum": ["L2", "L3"], "default": "L2", "description": "B90: the level the rule replaces, L2 (the reviewers) or L3 (the judge)." },
|
|
197
|
+
"pin": { "type": "boolean", "default": false, "description": "Force this matching route's model instead of adaptive primary-reviewer selection." },
|
|
179
198
|
"model": { "type": "string", "minLength": 1 },
|
|
180
199
|
"effort": { "type": "string" },
|
|
181
200
|
"provider": { "$ref": "#/$defs/provider" },
|
|
@@ -212,7 +231,8 @@
|
|
|
212
231
|
"xai": { "type": "integer", "minimum": 1, "default": 2 }
|
|
213
232
|
}
|
|
214
233
|
},
|
|
215
|
-
"parallel_tickets": { "type": "integer", "minimum": 1, "maximum": 16, "default":
|
|
234
|
+
"parallel_tickets": { "type": "integer", "minimum": 1, "maximum": 16, "default": 6, "description": "B42: how many review tickets the worker runs at once (oldest first); 1 = one at a time. Tickets for the same file never overlap." },
|
|
235
|
+
"provider_parallel": { "type": "integer", "minimum": 1, "maximum": 16, "default": 4, "description": "Maximum simultaneous review sessions per provider in the worker. Tickets waiting for this cap still count toward parallel_tickets; stricter provider_concurrency limits still apply." },
|
|
216
236
|
"fair_share": { "type": "boolean", "default": false, "description": "Give each free ticket slot to the block with the fewest running tickets; ties stay oldest first." },
|
|
217
237
|
"file_loop": { "type": "string", "enum": ["wait", "overlap", "watch"], "default": "wait", "description": "After each green file, enqueue its review. wait (default): wait for its ticket before the next file. overlap: continue coding, then run review-file --wait-block <id> --run <run> before the completion sentinel; inspect findings with --wait <ticket>. watch: commit each green file instead of running review-file. A file's first commit is queued at once. Later commits of a file that already has a ticket of this block are held (a watch.held row) until --wait-block, which queues the latest content once before the sentinel. Older pending tickets are still superseded." },
|
|
218
238
|
"agent_waivers": {
|
|
@@ -266,6 +286,11 @@
|
|
|
266
286
|
"lint": { "$ref": "#/$defs/argvOrNull" },
|
|
267
287
|
"types": { "$ref": "#/$defs/argvOrNull" },
|
|
268
288
|
"format": { "$ref": "#/$defs/argvOrNull" },
|
|
289
|
+
"merge_gate": {
|
|
290
|
+
"type": "boolean",
|
|
291
|
+
"default": true,
|
|
292
|
+
"description": "With block land --merge, run the configured full suite on a trial merge in a temporary detached worktree under the run worktree root before merging into main. Failure or conflict leaves main unchanged and removes the temporary tree. False or --no-merge-gate skips this check with a warning. Without --merge the gate does not run."
|
|
293
|
+
},
|
|
269
294
|
"kinds": {
|
|
270
295
|
"type": "object",
|
|
271
296
|
"additionalProperties": false,
|
|
@@ -13,6 +13,11 @@ 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
|
+
`block waive --yes` may waive a critical only while the run's autopilot grant is active; otherwise `waive-yes-critical`.
|
|
17
|
+
Cite a test file named in the block's acceptance `tests:` and passed in its latest signed gate record for the current content; otherwise `waive-yes-critical-no-test`.
|
|
18
|
+
Also cite the finding's path with `:line` or `:start-end`, and obtain agreement from the fresh L3 judge. Refusals write nothing.
|
|
19
|
+
The signed waiver records `by: agent`, `autopilot: true` and the cited `test` path; the human can undo it with `block unwaive`. Warning/nit waivers are unchanged.
|
|
20
|
+
|
|
16
21
|
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
22
|
|
|
18
23
|
- **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`.
|
|
@@ -44,7 +49,7 @@ These never go to the delegate, whatever the grant says (the CLI's fixed deny li
|
|
|
44
49
|
- plan approval (`plan.md` §4.1) and design approval;
|
|
45
50
|
- a budget raise (`code.md` §6, `review.md` §7);
|
|
46
51
|
- a threshold edit or any other limit or rule change — only the owner's own `forge autopilot approve` at a terminal;
|
|
47
|
-
- a critical waiver, a proof waiver, or closing a block without its reviews;
|
|
52
|
+
- a critical waiver through the delegate's `autopilot waive` action (the evidence-backed agent exception is in §2), a proof waiver, or closing a block without its reviews;
|
|
48
53
|
- merges and destructive actions.
|
|
49
54
|
|
|
50
55
|
When one of these comes up during the window: stop that line of work, add it to Actions only you can take, and wait. The delegate never relaxes a rule or a limit to keep work moving (`rules-core.md` R17).
|
package/skill/references/code.md
CHANGED
|
@@ -87,6 +87,43 @@ Each `review-file` ticket is served by the worker `forge run start` launched: to
|
|
|
87
87
|
|
|
88
88
|
## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
|
|
89
89
|
|
|
90
|
+
Paths added by `block claim` survive retries: `block open <id> --attempt N` (N > 1)
|
|
91
|
+
restores earlier signed claims alongside `--owned`, and `block attempt` keeps them too.
|
|
92
|
+
Both print `kept claims: <n>`. Do not claim the same paths again after a retry.
|
|
93
|
+
|
|
94
|
+
The orchestrator can use `code-forge block land <id> --run <r> --report <file> --transcript <file> [--max <dur>] [--merge] [--no-merge-gate] [--json]`
|
|
95
|
+
to join reviews, run changed gates in the recorded block worktree, then run the normal close checks.
|
|
96
|
+
The first failure exits 1 and names the step, findings, gate output or close refusal, and the next
|
|
97
|
+
command. Human checks must finish before a merge. With `--merge`, a closed block's branch is merged
|
|
98
|
+
with `--no-ff` in the clean main checkout on the recorded base branch; the block worktree must also
|
|
99
|
+
be clean. Before the real merge, land merges the current base branch HEAD and block branch in a
|
|
100
|
+
temporary detached worktree under the run's worktree root and runs the configured full gates
|
|
101
|
+
without block scope or `--changed`, using the shared gate runner. A green suite removes the
|
|
102
|
+
temporary tree, prints `merge gate: full suite passed (<n>s)`, and permits the real merge.
|
|
103
|
+
A red suite or conflict stops at `merge-gate` with the first ten failing TAP test names (or gate
|
|
104
|
+
diagnostics) or conflict files and a `next:` action. Every outcome removes the temporary tree;
|
|
105
|
+
a refusal leaves main clean and its HEAD unchanged. `gates.merge_gate` defaults to `true` in
|
|
106
|
+
the run config; `false` or `--no-merge-gate` skips the check with a warning.
|
|
107
|
+
It never forces or switches branches. Success prints `block <id> landed`; without
|
|
108
|
+
`--merge`, it prints `block <id> closed; merge: <cmd>` and a hint that the merge gate runs only
|
|
109
|
+
with `--merge`, and leaves the base unchanged. A merge refusal
|
|
110
|
+
leaves the block closed; use the displayed next command after correcting the checkout. `--json`
|
|
111
|
+
prints one object with `block`, `event` and `merge`, or a refusal with `step`, `reason` and `next`.
|
|
112
|
+
|
|
113
|
+
`--max` defaults to `30m`; durations also accept seconds (`90s`) and milliseconds (`1500ms`).
|
|
114
|
+
Land makes up to three join attempts while a held findings round remains open, all within one
|
|
115
|
+
wait budget. Completed findings stop it. An exhausted wait stops at `join` with `wait_timeout`
|
|
116
|
+
and prints a `next:` land command with a larger `--max`, preserving the report and transcript.
|
|
117
|
+
|
|
118
|
+
Land automatically claims changed files outside the owned set before joining reviews.
|
|
119
|
+
`block close --auto-claim` also claims, queues reviews in the block worktree, waits, and
|
|
120
|
+
continues only after approval. Each file prints an auto-claim line and gets the same signed
|
|
121
|
+
`block claim` row with `auto: true` and reason `auto-claim: changed outside owned files`.
|
|
122
|
+
Findings stop the operation. A file owned or claimed by another open block still refuses
|
|
123
|
+
`unowned_change`, naming that block, including separate worktrees. `.git/`, `.code-forge/`
|
|
124
|
+
and paths outside the repository are never auto-claimed. Plain close keeps refusing
|
|
125
|
+
unowned files; manual `block claim` remains available.
|
|
126
|
+
|
|
90
127
|
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.
|
|
91
128
|
|
|
92
129
|
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.
|
package/skill/references/plan.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## §0 Facts before design (step 0 of every plan)
|
|
4
4
|
|
|
5
|
-
Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else.
|
|
5
|
+
Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else.
|
|
6
|
+
|
|
7
|
+
`--sources <path>` adds the top-level .md/.txt files of a folder as more CLAIM TEXT; it is not a list of folders the delegate may read.
|
|
8
|
+
|
|
9
|
+
What it does:
|
|
6
10
|
|
|
7
11
|
1. Deterministic: the CLI extracts every *claim token* from the brief and the sources it names — CLI flags, `<cli> <verb>` commands, paths, versions, endpoints, package names, environment variable names — and writes a claim list. A `<cli> <verb>` written without its CLI (`run reload`) whose first word is not one of the project's CLIs, not a well-known CLI (`git gh npm npx node pnpm yarn bun deno ls cat grep which find test make docker curl claude codex python python3 pip go cargo op`), and that the brief never uses as a CLI of its own (`` `gh` `` or `` `gh --repo x` `` in a code span) — decided from the brief and `package.json` alone, never PATH — becomes `<project CLI> <verb>` (`code-forge run reload`) when the brief itself (not a source) names one of the project's CLIs (its `package.json` `bin` names) as a whole word — `.code-forge.yml` or `code-forge.config` do not count; `plan check` then counts a clause citing `run reload` as backed by that VERIFIED fact, and only when the extra word is one of the project's own CLIs.
|
|
8
12
|
2. One L0 session whose cwd is a **read-only snapshot of the repository at HEAD plus the `--sources` files**, built under the run's temp root and removed at session end — so `./path` claims are checkable, while **uncommitted files are not visible unless named as sources** (the snapshot is HEAD, not the working tree) — and whose packet is the claim list and the rule "run the cheapest read-only check, quote the command and the output line, tag VERIFIED / NOT-FOUND / UNVERIFIABLE, never infer". Two lines of defence, exactly as built. **First, at spawn:** the session gets the shell tool only, with the forbidden list plus every write verb rendered as deny rules and the provider's restricted mode (file tools confined to the working directory). Nothing is pre-approved: the delegate only runs what the provider already treats as read-only. A flag claim is therefore checked in the project's **source text**, never by running a program: `grep -rn -- <flag> <path>` over a top-level source folder of the snapshot such as `src` (never `.` or the root); it is VERIFIED only when the claim is that flag and the excerpt has a `file:line:text` hit from a source file — not `.md`/`.markdown`/`.txt`, not under `docs/ plans/ test/ tests/ fixtures/ node_modules/`, not the brief, and under the grep's path — holding the flag as a whole token (`--force-push` does not prove `--force`); a NOT-FOUND counts only over one whole top-level source folder that exists as a directory in the snapshot (not hidden, not `dist build coverage out` or the folders above). The grep's path must be an existing source directory of the snapshot (a nested one such as `src/cli` is fine for VERIFIED; a single file never is), and no other command — a plain grep included — ever proves or disproves a flag. Every check is ONE command: no pipe and no list (`| ; && || &`). There is no pre-execution gate on individual commands beyond the deny rules: the delegate runs what it runs. **Second, after the fact:** the CLI checks every `command` the delegate reports against an allow-list, fail closed, and refuses the whole sheet on the first violation. Allowed forms (never `rg`, which searches recursively by default): `ls cat head tail wc grep stat file uname sw_vers jq which` with in-cwd arguments (`grep` options only `-n -c -i -w -F` before `--`, recursive only as the whole command `grep -rn -- <--long-flag> <path>` with one relative subfolder, never `.` or a secret path; only a source folder proves a flag), `find` with only these predicates — `-name -iname -path -ipath -type -maxdepth -mindepth -print -print0 -newer -size -empty -mtime -mmin -not ! -a -o -and -or` — every other predicate refused, `git log|show|status|rev-parse|ls-files|cat-file`, `npm view|ls`, `test -e|-f|-d|-n <one operand>`, and `curl` as a HEAD request of exactly one http(s) URL with `-s -S -I -L -f --max-time <N>` only. No program ever runs with `--help`, `-h` or `--version` (BSD `rm foo --help` deletes `foo`): a version claim is checked with `npm view`, `which` or `ls`; an env claim is only `printenv NAME >/dev/null` with its excerpt blanked. Refused anywhere: backticks, `$`, redirection, unquoted globs, `~`, `..`, absolute paths outside `/usr`, `/opt/homebrew`, `/bin`, and secret-looking paths. **`npm view` and `curl -I` reach the network** — they are what makes a package version or an endpoint checkable; nothing else the delegate may run does.
|
|
@@ -6,9 +6,22 @@ A light block reviews all owned changed files in one packet, with each path head
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
+
**Changed stopped findings restart automatically.** When the latest review stopped with
|
|
10
|
+
findings and the current content hash in the block tree differs from the stopped hash,
|
|
11
|
+
`review-file --wait-block` queues a fresh full review at round 1 through the reopen ticket
|
|
12
|
+
path. It writes one signed `review.reopened` row (`by: auto`) with reason
|
|
13
|
+
`auto-requeue: content changed after stopped review`, prints one `auto-requeue <file>: …
|
|
14
|
+
(ticket <id>)` line on stderr, and waits for the new ticket normally. Rejoining reuses the
|
|
15
|
+
attempt. A signed review after that automatic restart supersedes earlier cap findings;
|
|
16
|
+
its own findings still count. Unchanged content, findings fully covered by valid waivers, and files owned by
|
|
17
|
+
another open block do not requeue. Old waivers do not approve changed bytes. `block land`
|
|
18
|
+
uses this join before gates and close. Direct `block close` also queues changed stopped
|
|
19
|
+
findings and waits for their fresh tickets before its review gate. Explicit reopen remains available for stopped or
|
|
20
|
+
lost reviews outside this automatic case.
|
|
21
|
+
|
|
9
22
|
`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.
|
|
10
23
|
|
|
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
|
|
24
|
+
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 with changed content and unwaived findings restarts automatically as described above. 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.
|
|
12
25
|
|
|
13
26
|
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`.
|
|
14
27
|
|
|
@@ -52,6 +65,26 @@ Without those lists, attachments remain tracked-only. Existing path and read-onl
|
|
|
52
65
|
|
|
53
66
|
Every reviewer, recheck, judge and S2 session runs with cwd = a fresh empty temp dir under the run's temp root and the packet **on stdin** — a no-tools session never opens a path and the packet never enters argv. Packet order is fixed so provider prompt caching applies: lens, project rules digest (≤ 60 lines), facts-sheet excerpt for the symbols in the diff, context, `## paired` (optional), `## moved code` (optional), diff. With a `context` key in the config (B63a, `code.md` §3) the project rules digest is the rules files' text and then, under a `### lessons` subheading, the lessons file, read from the main tree, never from a block worktree, each time a packet is built. `context` is read from the live config; `run reload` accepts a change to it. Rules and lessons share the one 60-line cap and are trimmed together, from the end. Without the key the slot is `(none)`, as before. A rules or lessons file missing, outside the repository or unreadable when the ticket is reviewed (or a bad `context` value) makes the ticket `review.unavailable` with `reason: context-unreadable`: no session runs and its packet directory is removed. The worker does not retry it: the ticket is done with that signed result. `review-file` on the same content is a fresh attempt (`retry: true`) each time it is run, with no cap (no session ran, so the one re-run of a failed packet is not used up); once the file is back, that attempt is reviewed normally. Context: a file at or under `review.context.whole_file_max_lines` travels whole; above it each hunk carries ± `review.context.hunk_context_lines` lines; over `review.budgets.full_in` the context shrinks to ± `review.context.min_context_lines`, then the digest is trimmed, never the diff; a diff alone over budget ⇒ `split_required`, with `tokens_in` and `budget` in the result, its ledger `review.result` row and the `review-file --wait` answer (`stop: split_required`) — except a Markdown file (`.md`, `.markdown`), which is reviewed **section by section**: the diff is cut at its headings (`#`..`######`, not inside fenced code), consecutive sections share a packet while it fits, every section packet runs the file's whole plan with its own stub guard, the file is approved only when every section is, and findings are merged with ids `S<section>.<id>`; a recheck over budget is split the same way, each open finding listed in its own section's packet. A section that alone is over budget keeps the file `split_required` (with that `section`), and so does a deleted Markdown file or one with no heading (fail-safe: one section); nothing is ever truncated. Moved code (B56): code moved between two changed files of the same block is named in a `## moved code` section before the diff (`- base lines a-b removed here were added in <file> lines c-d in this block.` / `- lines c-d were moved here from <file> (its base lines a-b) in this block.`), in round 1, rechecks, patch checks and the judge's packet. Detection is deterministic line matching against the block's other changed owned files diffed against the same base: lines trimmed with whitespace collapsed, lines with fewer than 4 letters or digits ignored, a moved stretch needs ≥ 3 such lines or ≥ 120 such characters; at most 12 ranges are listed; a Markdown section packet lists only the moves under its own headings (none ⇒ no section); the peers are listed and read once per review (one `git ls-tree <base>`, one `git diff <base>` over the peers that exist in the base — a peer deleted in the block, staged or not, is all removed lines — one `git check-ignore` and one `git diff --no-index` per new peer; `a/`/`b/` prefixes forced, `core.quotePath=false`; a peer name with a control, bidi or separator character, a quote or a backslash is skipped; a peer whose diff is over 2 MiB is skipped, never buffered past that); a removed block cut right above a heading belongs to the section holding its removed hunk; a move inside one file is not detected. It is extra text counted in the packet budget: every hunk stays, `reviewed_hunks` is unchanged, and a removal listed there is not a defect by itself — only stale references or imports are. The closed-book rule is in every lens: judge only what is in the packet; a file you cannot see goes in `needs_file`, and the worker attaches it (read-only, trimmed) for **one** more round.
|
|
54
67
|
|
|
68
|
+
### How code-forge picks the reviewer
|
|
69
|
+
|
|
70
|
+
The L2 model and its fallbacks compete by availability × (1 − waived finding share) ÷ median
|
|
71
|
+
session seconds, measured across this project's runs over `review.adaptive.window_days` (14).
|
|
72
|
+
Timeouts, schema errors, exits and silence kills count as unavailable. Models below
|
|
73
|
+
`min_sessions` (10) keep configured order; no history uses the configured reviewer. An `explore`
|
|
74
|
+
share (0.1), hashed from the ticket id, gives a less-measured model a reproducible turn.
|
|
75
|
+
`doctor` shows the model's sessions, median seconds, ok percentage and waived
|
|
76
|
+
percentage. The worker writes its chosen model and reason in a signed `review.route` row before
|
|
77
|
+
the first session. `review.adaptive.enabled: false` gives legacy routing exactly. There is no
|
|
78
|
+
default file-size route; packets are capped at 32k tokens.
|
|
79
|
+
|
|
80
|
+
A matching explicit L2 route joins the candidates with configured priority at cold start;
|
|
81
|
+
`pin: true` forces it. L3 rules retain their explicit judge selection. Retries reuse the signed
|
|
82
|
+
choice. `run reload` accepts route and adaptive changes.
|
|
83
|
+
|
|
84
|
+
The following routing details describe legacy mode with adaptive selection disabled. With
|
|
85
|
+
adaptive selection on, L2 routes contribute candidates and each primary choice gets a signed
|
|
86
|
+
row with its reason.
|
|
87
|
+
|
|
55
88
|
Big files (B90): to send a big file to a long-context reviewer, set `review.routes` once — do not edit `.code-forge.yml` per file and restore it after. A rule is `{min_lines, level?, model, provider?, effort?, fallback?, session_timeout_s?, second?}`; `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 (`level: L3`) changes only the judge's model. What is counted: the file's lines at the reviewed state (the bytes in the review packet) — its newline bytes, plus one for a last line with no newline — once per ticket. A deleted file is never routed. Rules compete only within their level: among the rules of one level, the one with the largest `min_lines` at or under the count wins, and one ticket can take one L2 rule and one L3 rule; two rules with the same `level` and `min_lines` are 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), and validation checks the rule with that same provider; `effort` — none (the CLI's default); `fallback` — none; `session_timeout_s` — the rule does not count in the timeout; `second` — `review.second_levels.<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, it does not count as `review.session_timeout_s`), else `review.session_timeout_s`. The rule's `second` is the second-level try's model (§4) for its level, in place of `review.second_levels.<level>` — except an L2 rule in consensus mode, whose `second` is not used. One signed `review.route {level, min_lines, lines, provider, model, session_timeout_s, second_model}` row per routed level is written before the ticket's first session: `provider` is the one the routed level runs on (the inherited one when the rule names none), and `second_model` is the rule's `second` model when that second is used, else null — always null for an L2 rule in consensus mode, because slot B is `review.second_levels.L2`, not the rule's. A retry of the same ticket never writes its rows twice. A row that cannot be written ⇒ `review.unavailable` with `reason: route-row`, and a routes list in the snapshot that cannot be used (or a routed level that cannot be resolved) ⇒ `reason: route-invalid` — no session runs. No rule matches (or no `review.routes`) ⇒ the ticket runs exactly as before. `run reload` accepts a change to it; validation refuses two rules with the same `level` and `min_lines`, and, with `review.multimodel`, an L2 rule whose provider (its own, else the inherited top-level one) equals the second reviewer's (slot B's, or the rule's own `second.provider`) (`review-routes-invalid`).
|
|
56
89
|
|
|
57
90
|
Deleted files (B74): a deleted owned file is reviewed at the content hash `deleted`; its round-1 packets (each reviewer's and the judge's; a file that stays deleted has no later round, since its content cannot change) have a `## deleted file` section before the diff saying the file is deleted and listing the tree paths that still name its basename — one `git grep -l -z --untracked -F -e <basename> -- .` with the reviewed working tree (the block's worktree or the main tree, the one the diff is read from) as cwd, at most 20 paths sorted, never the deleted path itself or a secret-like path; a path with a control character is never printed, and it and the paths past 20 are counted as `(<n> more not listed)`. No match ⇒ `no references found`; a failed grep, a basename under 3 characters or a deleted path with a control character (printed JSON-escaped) ⇒ `references not checked`. A file is "deleted" only when its diff is not empty and the path is absent from the reviewed tree (a binary file that still exists never gets the note); a note reader that fails gives `references not checked` and the review goes on. No exemption: the deletion still needs a signed approval (or the waiver/answers exits) to close, and an approval at `deleted` stops counting once the file comes back.
|
|
@@ -76,7 +109,7 @@ Only real rechecks count toward the round cap, per-level ladder and stall rules
|
|
|
76
109
|
3a. **Heavy rounds climb at once** (`review_warnings`, `decisions.md` §5 rule 3a): below L2, a round — round 1 included — that leaves ≥ `escalation.warning_threshold` (2) warnings or any `critical` open, `escalation.after_rounds_with_warnings` (1) times at the current level, climbs one level for the next fix. One round with a stronger model is faster and cheaper than three with a cheap one. The per-file cap (step 5) stays the outer limit.
|
|
77
110
|
4. **Ladder**: the running ceiling is `min(L2, escalation.stop_at)` — L3 is never a *running* level. A trigger — rule 1 (`retries`), rule 2 (`escalation.review_rounds_per_level` exhausted with an open `fix_now`), rule 2b (`review_stall`) or step 3a (`review_warnings`) — below the ceiling climbs one level, coded by a fresh session with the open findings and the fix history in its brief (an L1 block's rounds 3–4 run at L2). At the ceiling — an L2 block, or an L1 block that has climbed to L2 — it takes the **L3 rung**: a patch ≤ 80 lines, owned files only, reviewed as a `patch_check` round outside the cap. Under `escalation.l3_mode: code` (opt-in) the rung is instead one L3 coding turn. Either way the rung fires **once per block** and the same post-patch order follows it. After it, in order: `patch_check` clean ⇒ complete; the patch closed no finding that was open before it (ids compared, not counts) ⇒ `stopped: l3_patch_exhausted` (`review_stall`); round ≥ `review.max_rounds_per_file` ⇒ `stopped: review_cap`; otherwise the fixes continue at L2 with `rounds_at_level` reset and the patch in the brief. A patch that closed at least one old finding is progress, not a stall, even when new findings in its fix hunks keep the open set from shrinking (B73). Any later trigger in that block stops with its named reason (`l3_patch_exhausted`, `review_cap`) and never patches again.
|
|
78
111
|
5. **Cap — four rounds per file** (`review.max_rounds_per_file`, default 4, range 2–6). The per-file cap still counts per file: the orchestrator sends one fix message per block (`code.md` §4), and one block round is one round for each file it touched. Round 4 still open ⇒ the L3 rung if the block has not used it (same post-patch order as step 4), else `stopped: review_cap`. A stopped file (`review_cap`, `l3_patch_exhausted`, `split_required`, …) keeps the block from closing; the ledger lists its open findings. The stop names its ways out: a `stopped` answer from `review-file` carries `exits: ["block reopen", "block waive", "review-file --answers", "re-decompose"]`, and the close's `review_cap` detail and `unreviewed` detail for a stopped file end with `exits: block reopen, block waive, review-file --answers, re-decompose` (B73). That printed list is the code's list, not an order; follow Exit 1–4 below. **The ways out of a stop, in this order** (take the first that fits):
|
|
79
|
-
- **Exit 1 — fix, then `block reopen` (B72).** The file is fixed by hand, then the human restarts its review: `forge block reopen <id> --file <path> --run <r> --reason <text>`. It also restarts a `lost` state (step 8) on its current content. Without `--yes` it needs human confirmation; see **Human confirmation** below. `block reopen` and `block waive` both have evidence-backed agent paths through `--yes` with a fresh judge. A stopped file must differ from the content its stop reviewed. Every stop records that content as `stop_content_hash` in the signed state; a state written before B72, or before B114 by a fix-loop stop, has none, and then only the `review.cap` row of that stop (after the file's latest reopen, same round and reason) names it — otherwise the content is unknown. With unknown stop content, reopen accepts only when no verified `review.round` of this run, block and file names the current hash (B114). It records `from_hash: null`; the worker enforces the same check. If that hash was reviewed, change the file and reopen, or the owner can use `block close --no-require-reviews`. Waiving findings at an old hash cannot clear unreviewed current bytes. The verb first removes the done marker of the current content's ticket, so the next `review-file` runs instead of returning the cached stop. Then it writes one signed `review.reopened {block, file, from: stopped|lost, stop_reason, at_seq, from_hash, to_hash, reason, by: human | agent}` row (`at_seq` is the file's latest `review.state` anchor seq; `from_hash` the content the stop reviewed, null for `lost` or unknown stop content
|
|
112
|
+
- **Exit 1 — fix, then `block reopen` (B72).** The file is fixed by hand, then the human restarts its review: `forge block reopen <id> --file <path> --run <r> --reason <text>`. It also restarts a `lost` state (step 8) on its current content. Without `--yes` it needs human confirmation; see **Human confirmation** below. `block reopen` and `block waive` both have evidence-backed agent paths through `--yes` with a fresh judge. A stopped file must differ from the content its stop reviewed. Every stop records that content as `stop_content_hash` in the signed state; a state written before B72, or before B114 by a fix-loop stop, has none, and then only the `review.cap` row of that stop (after the file's latest reopen, same round and reason) names it — otherwise the content is unknown. With unknown stop content, reopen accepts only when no verified `review.round` of this run, block and file names the current hash (B114). It records `from_hash: null`; the worker enforces the same check. If that hash was reviewed, change the file and reopen, or the owner can use `block close --no-require-reviews`. Waiving findings at an old hash cannot clear unreviewed current bytes. The verb first removes the done marker of the current content's ticket, so the next `review-file` runs instead of returning the cached stop. Then it writes one signed `review.reopened {block, file, from: stopped|lost|open, stop_reason, at_seq, from_hash, to_hash, reason, by: human | agent}` row (`at_seq` is the file's latest `review.state` anchor seq; `from_hash` the content the stop reviewed, null for `lost` or unknown stop content, the current content for `open`; `to_hash` the current content). There is at most one row per `at_seq`: a retry on the same content finds its row and writes no second one, and other content at the same `at_seq` is refused. The worker starts a new state for a row whose MAC verifies, of this run, `by: human` or `by: agent`, whose `at_seq` is the latest anchor seq: round 1, a full review (no fix hunks), the level from the run record, up to `review.max_rounds_per_file` rounds again. It saves at `at_seq + 1`, so a row works once. For a stop the worker checks again that the content changed; on the stop's own content the stop repeats. The block's one L3 rung stays spent when it was used, so a reopened file that reaches the cap stops at `review_cap`. After a verified human or agreed agent reopen and a fresh signed review of the current bytes, the old cap ids are cleared. The fresh review’s own findings still count. An approval written after the `review.cap` row also clears the cap. Refusals (exit 1, no row): `reopen-same-content` (a stopped file still at the content its stop reviewed, or unknown stop content whose current hash has a verified round: for unknown content, change the file and reopen, or the owner can use `block close --no-require-reviews`), `reopen-tampered` (an integrity failure stays a security event), `reopen-not-stopped` (no state, a complete one, or an open one unless the agent path is used with findings), `reopen-block-not-open` (the block is stopped or closed), `reopen-not-owned`, `unknown_block` (the block lookup code `block waive` uses; it and `reopen-block-not-open` are checked before anything else is read), `reopen-config` (the run's config snapshot or `tmp.root` cannot be read), `reopen-already-recorded` (a row for this `at_seq` already names other content: run `review-file`, or the content changed since that reopen), `reopen-write-failed` (the row was not written). A missing `--file` or `--reason`, or a `--file` that is not a plain repo-root-relative path (no `..`, `./`, `//` or `/./`, and no control character), is `usage` (exit 2).
|
|
80
113
|
- **Exit 2 — `review-file --answers` (B71).** A finding fixed in ANOTHER file of the block (a missing test added in the test file, say) is answered with `forge review-file <answering file> --block <id> --answers <id,id,…> --of <source> --run <r>` (1 to 10 ids, none twice; `--of` names the source because finding ids are numbered per file). A coder may run it.
|
|
81
114
|
- **Refusals.** The checks run when the ticket is queued and again in the worker, whose check is the one that counts. The block must own both files (`answers-not-owned`), they must be two files (`answers-same-file`), and every id must be open on the source's CURRENT hash (`answers-not-open`). Open means named by the latest signed refusal row whose `content_hash` is the current hash, or by ANY signed `review.cap` row whose `content_hash` is the current hash, not waived or answered yet, and one the close could count as answered: never the proof finding (a proof finding is cleared by its proof, not by a reviewer) and never an id read at an unknown severity (a cap id no signed refusal at that hash records, or a value outside critical/warning/nit). A cap id from an older hash (or a cap row with no `content_hash`) is never open for an answer. The close still requires a human waiver unless the cap row is superseded. A verified human or agreed agent `review.reopened` row after the cap and a signed `review.round` of the current hash after the reopen supersede it. An agent row counts only with `verdict: {verdict: agree, reason}`. A missing `--of`, no ids, more than 10 ids, an id given twice or a malformed id is `usage` (exit 2). The other refusals exit 1 and queue nothing. In the worker, an answering file that is gone or cannot be read is `answers-file-missing`.
|
|
82
115
|
- **The review.** The ticket is its own kind (`kind: answers`); its id also covers the source, the source hash and the sorted ids, so the same request is the same ticket. The worker sends ONE fresh L2 reviewer session (lens `answers`) a packet with the findings, the source lines around each, and the answering file's diff against the block base (its full content when the base has no such file). Each file is read ONCE: those bytes are hashed and the packet is built from them, so the row's `by_hash` is the hash of what the reviewer saw. The packet file is removed after the session; a packet that cannot be built is `unavailable` (`packet-unreadable`), one that cannot be written `unavailable` (`packet-write-failed`). The reviewer answers `resolved` or `not_resolved` per id. A missing, repeated or broken verdict is `not_resolved`. A failed session (no result, not `ok`, a non-zero exit, or a spawn that throws: `session-error`) is `unavailable` (retry it; it is not a verdict) and writes no row. An answering file changed since the ticket is `stale` (`answering-file-changed`); a source changed since the ticket is `stale` (`source-changed`). All verdicts are built first, then each resolved id gets one signed `review.answered {block, file, content_hash, finding, by_file, by_hash, ticket}` row. If a write fails the ticket is `unavailable` (`ledger-write-failed`); the same request again retries the same ticket, the ids it already answered stay open for it, and it writes only the missing rows, so it ends with one row per id. `review-file --wait` prints `{answers: [{id, verdict, reason}]}`.
|
|
@@ -85,14 +118,19 @@ Only real rechecks count toward the round cap, per-level ladder and stall rules
|
|
|
85
118
|
- **Undo and acknowledge agent waivers.** The human runs `forge block unwaive <id> <finding-id> --run <r> --file <path> --reason <text>` to reopen a finding. It requires a verified agent waiver for an owned file. Unknown blocks, unowned files and missing verified agent waivers refuse with `unwaive-no-agent-waiver` (exit 1) before any prompt. A yes writes one signed `review.unwaived {block, file, finding, reason, by: human}` row. It cancels all earlier agent waivers for that finding and file in that block. Human waivers remain in force. A later agreed agent waiver counts again. Unsigned or tampered unwaives never cancel a waiver at the gate.
|
|
86
119
|
Set `review.agent_waivers.require_ack: true` before run start to require human acknowledgement before close. The key is read only from the run snapshot and cannot change through run reload. The human runs `forge block ack-waivers <id> --run <r>`. It prints each counted waiver's file, finding, reason and verdict before asking. No counted waivers refuses with `ack-waivers-none` (exit 1), without a prompt. A yes writes one signed `agent_waivers.acked {block, digest, count, by: human}` row. The digest hashes the sorted file, finding and signed row identity set. A new or removed waiver changes it. Close refuses with `agent-waivers-not-acked` until a verified human ack matches the current set. With the key absent or false, close is unchanged.
|
|
87
120
|
Both verbs are human-only and coder-forbidden. They ask at a terminal and have no `--yes`. No terminal exits 2. A no or cancel exits 1. These paths record nothing.
|
|
88
|
-
- **Agent waivers.** Use `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text> --yes`. The orchestrator may run `block waive --yes`
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
- **Agent waivers.** Use `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text> --yes`. The orchestrator may run `block waive --yes` for a recorded warning or nit finding, or for a recorded critical under an active autopilot grant with a cited acceptance test that passed its latest signed gate for the current content; evidence and agreement from a fresh L3 judge are required.
|
|
122
|
+
A critical needs an active autopilot grant; otherwise it refuses `waive-yes-critical`. Unknown and unrecorded findings always refuse that code.
|
|
123
|
+
For a critical, cite a test file named in the block's acceptance `tests:` that passed in its latest signed gate record for the current content; otherwise `waive-yes-critical-no-test`.
|
|
124
|
+
Also cite the finding's own path with `:line` or `:start-end` (`waive-yes-no-evidence` if missing), and obtain L3 agreement; disagreement refuses `waive-yes-disagreed`, a broken answer `waive-yes-broken`. Refusals write nothing.
|
|
125
|
+
Agreement writes the existing signed `review.waived` row with `by: agent`, severity, reason, `verdict: agree`, and `judge_reason`; a critical also records `autopilot: true` and the cited `test` path. Human `block unwaive` still undoes it.
|
|
126
|
+
Warning/nit evidence remains an existing repo path with a line or range, an existing test file, or a numbered block acceptance clause. The fresh judge receives the recorded finding, reason, current file bytes and numbered windows for up to ten valid line citations (ten context lines either side, eighty lines maximum per window). Missing files, escaping paths and symlinks are omitted; the reviewer session is never resumed.
|
|
127
|
+
- **Agent reopens.** Use `block reopen <id> --run <r> --file <path> --reason <text> --yes`. It runs without a terminal or prompt. For an agent reopen of an open review with findings, the ordinary `reopen-not-stopped` and `reopen-same-content` checks are skipped. The file and reason form checks, block lookup and open status, ownership, state integrity and valid anchor sequence, and `reopen-already-recorded` check still run before agent evidence, limit and judge checks. Stopped and lost reviews retain their ordinary reopen checks; a stopped review still requires changed content. Evidence uses the same rule as agent waivers, including numbered citation windows. An open review with findings is also eligible, including critical findings and unchanged content, but requires a path with a line or line range; a bare test file or acceptance clause alone does not suffice. No evidence refuses `reopen-yes-no-evidence` (exit 1). Only one verified agent reopen per block and file is allowed in a run; another refuses `reopen-yes-limit` (exit 1). Neither refusal calls the judge. One fresh L3 judge gets the stop reason (null for an open review), last findings, reason, current file bytes and citation windows. A disagreement or invalid answer refuses `reopen-yes-disagreed` (exit 1) and prints the judge reason. The review stays unchanged and nothing is written. Agreement removes the done marker, writes a signed reopen row with `by: agent`, `verdict: {verdict, reason}` and `from: open` for an open review, then queues a fresh full review. It does not approve or waive the findings. There is no severity restriction. The human path remains available after the agent limit and still requires a stopped or lost review. Close prints `agent reopen <file>: <reason> (verdict: agree)` for each agent reopen. Its JSON adds `agent_reopens: [{file, reason, verdict}]` only when nonempty.
|
|
128
|
+
- **Human confirmation.** Without `--yes`, `block waive` keeps its human confirmation and writes `by: human`. Critical findings outside the agent exception above, and all unknown-severity findings, need the human terminal path. With no terminal it exits 2: `block waive needs the human at a terminal to confirm; nothing was recorded (--yes works only for a recorded warning or nit finding, with evidence)`. `block reopen --yes` needs evidence and agreement from a fresh judge, at most once per block and file in a run. With no terminal it exits 2: `block reopen needs the human at a terminal to confirm; nothing was recorded (--yes works only with evidence and agreement from a fresh judge)`. Without `--yes`, the orchestrator prints the exact command with the human's reason; the human runs it in their own terminal. The orchestrator never answers a human confirmation prompt. A coder never runs either verb. Both verbs validate the file and block before asking. A waive also checks ownership and that the finding is recorded. A no or cancel records nothing. Under an active autopilot grant these human paths still prompt. The delegate's existing `autopilot waive` path still writes `by: autopilot`. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
91
129
|
- **False transcript hits.** The human runs `forge block accept-hit <id> --run <r> --rule <rule-id> --line <n> --transcript <path> --reason <text>` in their terminal. It uses the same human confirmation and has no `--yes`. It checks the run, block, readable transcript and scanned rule/line pair before asking. A missing hit refuses `accept-hit-no-such-hit` (exit 1). Bad forms are exit 2. No terminal is exit 2. A no or cancel writes nothing. A yes writes one signed `rule_break.accepted {block, rule, line, line_sha256, reason, by: human}` row. Close matches its run, block, rule and exact line content hash, even if the line moves in a joined log. Other hits and ledger `rule_break` rows still refuse. Close prints each acceptance it used with the current line and reason. Its JSON adds `accepted_hits: [{rule, line, reason}]`. A coder never runs this verb.
|
|
92
|
-
- **Agent waivers at close.** A signed `by: agent` waiver with `verdict: agree` clears a warning or nit finding for `reviewed_by_waiver`. Unsigned or tampered rows never count. Its `gate.review_waived_all` row uses `by: agent`. `block close` prints one line per signed agent waiver with file, finding, reason and verdict. `--json` adds `agent_waivers: [{file, finding, severity, reason, verdict}]` when there are agent waivers.
|
|
130
|
+
- **Agent waivers at close.** A signed `by: agent` waiver with `verdict: agree` clears a warning or nit finding, or a critical recorded with `autopilot: true` and a cited `test`, for `reviewed_by_waiver`. Unsigned or tampered rows never count. Its `gate.review_waived_all` row uses `by: agent`. `block close` prints one line per signed agent waiver with file, finding, reason and verdict. `--json` adds `agent_waivers: [{file, finding, severity, reason, verdict}]` when there are agent waivers.
|
|
93
131
|
- **Exit 4 — re-decompose.** The human decides, and the architect plans the work again as new blocks (`decisions.md` §6, `redecompose`). Each new block reviews its files from round 1.
|
|
94
|
-
- **A fully waived or answered file closes with a `gate.review_waived_all` row.** A file with no approval for its current hash counts as reviewed — `reviewed_by_waiver`, never approved — when a signed `review.round` exists for that hash (a repeated stop or an `unavailable` result never counts), the latest refusal row for the hash is signed, carries `content_hash` equal to that hash (a refusal row with no `content_hash` still refuses) and names at least one finding, and every finding it names and every open id of the file's unsuperseded `review.cap` rows is waived or answered. A cap row adds no ids when a verified human or agreed agent `review.reopened` row follows it and a signed `review.round` of the current hash follows the reopen. A critical waiver counts
|
|
95
|
-
- **Not exits.** `block attempt` never resets review state: it only raises the attempt counter (a coder may run it)
|
|
132
|
+
- **A fully waived or answered file closes with a `gate.review_waived_all` row.** A file with no approval for its current hash counts as reviewed — `reviewed_by_waiver`, never approved — when a signed `review.round` exists for that hash (a repeated stop or an `unavailable` result never counts), the latest refusal row for the hash is signed, carries `content_hash` equal to that hash (a refusal row with no `content_hash` still refuses) and names at least one finding, and every finding it names and every open id of the file's unsuperseded `review.cap` rows is waived or answered. A cap row adds no ids when a verified human or agreed agent `review.reopened` row follows it and a signed `review.round` of the current hash follows the reopen. A critical waiver counts `by: human` or as an agreed agent waiver with `autopilot: true` and a cited `test`; an autopilot waiver counts only when `autopilotWaiverProblem` finds nothing, and only for a finding a signed refusal row at that hash records as warning or nit (or records with no severity at all). A cap id no refusal row records a severity for, or a cap row whose `open` is not a list of ids, is the human's alone. A signed, still-current answer clears its finding there, and in the late-finding and cap checks only at the severity the rows RECORD for that id (a late finding's own severity; a cap id's from the signed refusals at the current hash) — a missing or unknown one is never cleared by an answer. An answer counts only while BOTH the source and the answering file have the row's hashes (a change to either makes it stale), and it may clear a critical. Otherwise the file stays `unreviewed`. The close writes one signed `gate.review_waived_all {block, file, content_hash, findings, by}` row per such file just before `block.close`: `findings` are the sorted waived ids, `by: human` when every clearing waiver is the human's, else `agent` when an agent waiver cleared a finding, else `autopilot`; `answered` lists the answered ids (only when there is one); with every id answered, `findings` is `[]` and `by` is null. When an id is both waived and answered, a human waiver is reported as the waiver; otherwise the answer is listed under `answered` instead of an autopilot waiver. The close line prints only the parts that apply: `block <id>: <file> reviewed_by_waiver (<ids>; by <human|autopilot|agent>)`, `reviewed_by_waiver (W1; by human; answered C1)`, or `reviewed_by_waiver (answered C1, C2)` (ids sorted and joined by `, `). A refused close writes no such row, and a close where no file needs it writes none.
|
|
133
|
+
- **Not exits.** `block attempt` never resets review state: it only raises the attempt counter (a coder may run it). A stopped file needs a signed reopen row to restart; the block join writes that row automatically for changed, unwaived findings (§1). `review-file --fresh` (§1) re-runs only a result that never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); on a recheck stop it refuses `fresh-needs-reopen`. A `tampered` state (step 8) is not a stop and gets no exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
96
134
|
6. **Block-close sweep**: `forge block close` batches the `late_findings` per file to one L3 judge ruling (`fix_now | nit`); a `fix_now` opens a round on that file at the block's current level and counts toward its cap. The gate counts a ruling only when it is `nit` on a warning or nit finding; a critical late finding stays open until the human waives it.
|
|
97
135
|
7. Ledger: one `review.round` row per round with `open_before`, `closed`, `new_in_hunks`, `late`, `open_after`; `forge report` prints rounds per file and rounds that stalled.
|
|
98
136
|
8. **A state file that goes missing is rebuilt from the ledger.** The worker keeps each file's round, level, open and late findings and stop in a signed state file. Every save first writes a signed `review.state` anchor row, and the anchor carries the state, so a missing file is rebuilt from it (Restore, below). Only an anchor with no state leaves the file `lost` (Lost, below).
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.15.
|
|
13
|
+
PINNED_VERSION="0.15.4"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|