@codedology/code-forge 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +18 -0
- package/docs/reference/blocks.json +234 -1
- package/package.json +1 -1
- package/skill/references/code.md +15 -6
- package/skill/references/ledger.md +1 -1
- package/skill/references/review.md +21 -3
- package/skill/references/security.md +1 -1
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +2 -2
- package/src/cli/block.mjs +137 -10
- package/src/cli/review-file.mjs +264 -13
- package/src/doctor/index.mjs +5 -4
- package/src/doctor/local.mjs +89 -2
- package/src/ledger/write.mjs +7 -1
- package/src/review/answers.mjs +445 -0
- package/src/review/deletion.mjs +106 -0
- package/src/review/engine.mjs +36 -2
- package/src/review/fixloop.mjs +25 -15
- package/src/review/gate-check.mjs +375 -78
- package/src/review/reopen.mjs +258 -0
- package/src/review/stop-exits.mjs +15 -0
- package/src/util/forbidden.mjs +10 -0
- package/src/util/tmp.mjs +16 -4
- package/src/worker/engine.mjs +75 -9
- package/src/worker/loop.mjs +3 -1
- package/src/worker/queue.mjs +265 -10
- package/src/worker/review-state.mjs +346 -51
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,62 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] — 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- review-file --answers <ids> --of <source>: a fresh L2 session judges a fix made in another owned file; resolved ids
|
|
14
|
+
get signed review.answered rows bound to both content hashes, and the close gate counts a still-current answer like a
|
|
15
|
+
waiver (answered list on gate.review_waived_all).
|
|
16
|
+
- block reopen <id> --run <r> --file <path> --reason <text>: a human-only verb that restarts a stopped file on changed
|
|
17
|
+
content (or a lost one on its current content) with a full round 1; signed review.reopened row, one per anchor seq;
|
|
18
|
+
refusal codes reopen-same-content, reopen-tampered, reopen-not-stopped, reopen-block-not-open, reopen-not-owned,
|
|
19
|
+
reopen-config, reopen-already-recorded, reopen-write-failed.
|
|
20
|
+
- A stopped `review-file` answer carries `exits`, and the gate's `review_cap` and stopped-file `unreviewed` detail end
|
|
21
|
+
with the same exits (B73, issue #12).
|
|
22
|
+
- The review packet of a deleted owned file carries a `## deleted file` note: up to 20 tracked or untracked paths that
|
|
23
|
+
still name its basename (one bounded `git grep`, secret-like and control-character paths left out), `no references
|
|
24
|
+
found`, or `references not checked` (B74, issue #8).
|
|
25
|
+
- `review-file --fresh` drops a cached result that never counted and queues the ticket again; counted results refuse
|
|
26
|
+
`fresh-needs-reopen`, tampered ones `fresh-tampered`, and the cached marker is never moved (B75, issue #10 item 4).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Review state is saved under ~/.code-forge/runs/<run>/review-state/ instead of the temp root; older state files are
|
|
31
|
+
moved once after an anchor check; doctor warns about missing state files of active runs (#15)
|
|
32
|
+
- Review state: the reviewed text is stored as a git blob (reviewed_content_blob in the signed review.state body), so a
|
|
33
|
+
restored state keeps reviewed_content after the file changed; a missing, malformed or mismatching blob falls back to
|
|
34
|
+
the full-file review.
|
|
35
|
+
- A `patch_check` that closes at least one finding open before the patch (by id) and is under
|
|
36
|
+
`review.max_rounds_per_file` continues the fix loop at L2; a patch that closes nothing still stops
|
|
37
|
+
`l3_patch_exhausted` (B73, issue #12).
|
|
38
|
+
- The skill and docs name the review exits in order, say where review state lives and how the ledger rebuilds it; the
|
|
39
|
+
coder brief names `review-file --answers`, and the security reference lists `block reopen` as forbidden to a coder
|
|
40
|
+
(B76).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- A lost review state file is rebuilt from the signed ledger and recorded as review.state_restored; rounds, findings and
|
|
45
|
+
stops stay; with no ledger copy the ticket is refused as review-state-lost, not tampered (#15)
|
|
46
|
+
- A file whose every finding is cleared by accepted waivers counts as reviewed (reviewed_by_waiver) and block close
|
|
47
|
+
writes a signed gate.review_waived_all row; unknown severities are human-only (#9)
|
|
48
|
+
- Test isolation guard: new files of a run that existed before the test process (its lock, key or run directory) no
|
|
49
|
+
longer count as leaks; a new run id or any other new path still fails the process.
|
|
50
|
+
- `review-file --answers` reads the severity of every `review.cap` id from the signed refusal rows, so an id the close
|
|
51
|
+
cannot count is refused before a reviewer session (B79).
|
|
52
|
+
- The answers packet checks the source file with the secret-path rule, redacts each part before it is cut, keeps every
|
|
53
|
+
part and the whole packet inside the budget, and refuses `packet-too-large` when the fixed text alone is over it
|
|
54
|
+
(B79).
|
|
55
|
+
- `block reopen` refuses a non-string `tmp.root` as `reopen-config`, and `reopen-write-failed` names the error code,
|
|
56
|
+
never a path (B79).
|
|
57
|
+
- The ledger rotation race test forces the order of the two racers with an `afterLink` hook instead of scheduling them,
|
|
58
|
+
so it no longer fails at random under load (B81).
|
|
59
|
+
- The worker restart test waits up to 5 s for killed sessions to exit instead of checking once, so it no longer fails at
|
|
60
|
+
random under load (B82).
|
|
61
|
+
- `review-file --fresh`: a failed restore of the done marker no longer leaves the ticket looking pending; it writes a
|
|
62
|
+
refused `fresh-restore-failed` marker or keeps the claim, `block reopen` clears leftover claims, and the messages say
|
|
63
|
+
which state was left (B83).
|
|
64
|
+
|
|
9
65
|
## [0.8.0] — 2026-10-12
|
|
10
66
|
|
|
11
67
|
### Added
|
package/README.md
CHANGED
|
@@ -10,6 +10,24 @@ for cross-model, parallel, evidence-gated feature delivery — on Claude Code, C
|
|
|
10
10
|
|
|
11
11
|
## What's new in 0.8
|
|
12
12
|
|
|
13
|
+
In the next release:
|
|
14
|
+
|
|
15
|
+
- **A lost review state is rebuilt, not reset.** When the review state file of a file is gone (for example, macOS cleaned its temp folder), the worker rebuilds it from the signed ledger and writes a `review.state_restored` row. Rounds, open findings and stops stay as they were. If the ledger cannot rebuild it, the ticket is refused as `review-state-lost`, not `tampered` (#15).
|
|
16
|
+
- **Review state no longer lives in the temp folder.** It is saved under `~/.code-forge/runs/<run>/review-state/`, so a temp cleanup or a worker restart does not touch it. A state file from an older run is moved there once, after a check against its signed anchor. `code-forge doctor` warns when a state file of an active run is missing and says whether the ledger can rebuild it (#15).
|
|
17
|
+
- **Waiving every finding is a real way out.** A file whose signed review on its current content left findings, all cleared by accepted waivers, now counts as reviewed (`reviewed_by_waiver`, never approved). `block close` writes one signed `gate.review_waived_all` row for it. A critical still needs a human waiver, and a finding with an unknown severity does too (#9).
|
|
18
|
+
- **Tests no longer fail because of your own live run.** The test guard used to fail when a code-forge run you had open wrote its lock file during `npm test`. Now new files of a run that existed before the tests are not counted as leaks. A new run or any other new file still fails the tests.
|
|
19
|
+
- **A restore brings back the exact reviewed text.** The reviewed text is now also kept as a git object, and the signed ledger points to it. After a lost state file is rebuilt, the next round reviews only what changed since the last review, even when the file changed. If git has removed the object, the round reviews the full file, as before (#15).
|
|
20
|
+
- **A fix in another file can answer a finding.** `review-file <file> --block <id> --answers <ids> --of <source>` asks a fresh reviewer whether a change in another owned file of the block fixes findings of the source file. Each finding it judges `resolved` gets a signed `review.answered` row, and the source file can then close as `reviewed_by_waiver` with an `answered` list. If either file changes later, the answer no longer counts. The answering file still needs its own approval (#9).
|
|
21
|
+
- **`block reopen` restarts a stopped file.** After you change a file whose review stopped, `code-forge block reopen <id> --run <r> --file <path> --reason <text>` starts a fresh round 1 for it. A file whose review state is lost can be reopened on its current content. Only a human may run it: a coder's command line is blocked, like `block waive`. The block's one L3 step stays used. The same content is refused, so you cannot simply roll again (#12, #15).
|
|
22
|
+
- **Safer answers packets and clearer reopen errors (B79).** An id named only by a `review.cap` row with no recorded severity is refused before any reviewer session. The answers packet checks the source file for secrets, stays inside the packet budget and refuses `packet-too-large` instead of going over it. `block reopen` refuses a bad `tmp.root` and names the cause of a failed write.
|
|
23
|
+
- **A last-chance patch that fixes something is progress (B73).** When the L3 patch closes at least one old finding and the file is under the round cap, the fix loop goes on at L2 instead of stopping `l3_patch_exhausted`. A stopped answer from `review-file` and the gate's `review_cap` detail now list the exits: `block reopen`, `block waive`, `review-file --answers`, re-decompose.
|
|
24
|
+
- **A steadier test suite (B81).** The ledger rotation race test forces the race order through a test hook, so it no longer fails at random under load.
|
|
25
|
+
- **Deleted files get a useful review (B74).** When a block deletes an owned file, the review packet says the file is deleted and lists up to 20 files that still name it, so the reviewer can look for dangling references. A deletion still needs an approval.
|
|
26
|
+
- **`review-file --fresh` (B75).** Runs a file's review again only when the cached result never counted (`stale`, `unavailable`, or a non-verdict refusal such as `other-run`). A counted result is refused `fresh-needs-reopen` and names `block reopen` and `block waive`; a tampered one is refused `fresh-tampered`. A coder may use it.
|
|
27
|
+
- **Another steadier test (B82).** The worker restart test waits for killed sessions to exit instead of checking once, so it no longer fails at random under load.
|
|
28
|
+
- **The review exits, in one place (B76).** `skill/references/review.md` §5, `code.md` and `docs/how-it-works.md` name the ways out of a stopped file in order: fix and the human's `block reopen`, `review-file --answers`, `block waive`, re-decompose. They also say `block attempt` never resets review state and that a lost state file is rebuilt from the ledger.
|
|
29
|
+
- **`review-file --fresh` fails closed (B83).** When a cached result cannot be put back, the ticket gets a refused `fresh-restore-failed` marker (or keeps its claim), so the worker does not run it by itself. `block reopen` clears it. Messages name the error code only.
|
|
30
|
+
|
|
13
31
|
In 0.8.0:
|
|
14
32
|
|
|
15
33
|
- **Run only the tests a block touches.** `block open --tests <paths>` names the block's test
|
|
@@ -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": 88,
|
|
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": {
|
|
@@ -1006,6 +1006,239 @@
|
|
|
1006
1006
|
"B66"
|
|
1007
1007
|
],
|
|
1008
1008
|
"note": "root-added 2026-10-12; GitHub issue #13 adoption plan (plans/adoption-13/plan.md); also amends skill/SKILL.md, skill/references/code.md, docs/README.md, docs/how-it-works.md and test/skill/structure.test.mjs"
|
|
1009
|
+
},
|
|
1010
|
+
"B68": {
|
|
1011
|
+
"title": "A lost review-state file is rebuilt from the signed ledger, never reset",
|
|
1012
|
+
"wave": 14,
|
|
1013
|
+
"level": "L2",
|
|
1014
|
+
"status": "landed",
|
|
1015
|
+
"owned": [
|
|
1016
|
+
"src/worker/review-state.mjs",
|
|
1017
|
+
"test/worker/review-state-restore.test.mjs"
|
|
1018
|
+
],
|
|
1019
|
+
"depends_on": [
|
|
1020
|
+
"B67"
|
|
1021
|
+
],
|
|
1022
|
+
"note": "root-added 2026-10-12; GitHub issue #15 review-exits plan (plans/review-exits/plan.md); review.state anchors carry a slim signed state_body; also amends src/worker/engine.mjs, src/worker/loop.mjs, skill/references/review.md, skill/references/ledger.md and test/worker/fixloop-wiring.test.mjs"
|
|
1023
|
+
},
|
|
1024
|
+
"B69": {
|
|
1025
|
+
"title": "Review state lives under the code-forge home, not the temp root",
|
|
1026
|
+
"wave": 14,
|
|
1027
|
+
"level": "L1",
|
|
1028
|
+
"status": "landed",
|
|
1029
|
+
"owned": [
|
|
1030
|
+
"src/worker/review-state.mjs",
|
|
1031
|
+
"src/doctor/local.mjs",
|
|
1032
|
+
"test/worker/review-state-durable.test.mjs"
|
|
1033
|
+
],
|
|
1034
|
+
"depends_on": [
|
|
1035
|
+
"B68"
|
|
1036
|
+
],
|
|
1037
|
+
"note": "root-added 2026-10-12; GitHub issue #15 review-exits plan (plans/review-exits/plan.md); state at <home>/runs/<run>/review-state, one-time checked move from the temp root, doctor review-state row; also amends src/util/tmp.mjs (runRootPath), src/worker/engine.mjs, src/doctor/index.mjs, skill/references/review.md, docs/how-it-works.md, docs/getting-started.md and the B68/fixloop-wiring tests"
|
|
1038
|
+
},
|
|
1039
|
+
"B70": {
|
|
1040
|
+
"title": "A fully waived review counts as reviewed",
|
|
1041
|
+
"wave": 14,
|
|
1042
|
+
"level": "L2",
|
|
1043
|
+
"status": "landed",
|
|
1044
|
+
"owned": [
|
|
1045
|
+
"src/review/gate-check.mjs",
|
|
1046
|
+
"test/review/waived-all.test.mjs"
|
|
1047
|
+
],
|
|
1048
|
+
"depends_on": [
|
|
1049
|
+
"B69"
|
|
1050
|
+
],
|
|
1051
|
+
"note": "root-added 2026-10-12; GitHub issue #9 review-exits plan (plans/review-exits/plan.md); reviewed_by_waiver status and signed gate.review_waived_all row before block.close; unknown severity is human-only; also amends src/cli/block.mjs, skill/references/review.md, skill/references/ledger.md, docs/how-it-works.md and docs/concepts.md"
|
|
1052
|
+
},
|
|
1053
|
+
"B77": {
|
|
1054
|
+
"title": "Test guard ignores live runs of the developer",
|
|
1055
|
+
"wave": 14,
|
|
1056
|
+
"level": "L1",
|
|
1057
|
+
"status": "landed",
|
|
1058
|
+
"owned": [
|
|
1059
|
+
"test/helpers/isolate.mjs",
|
|
1060
|
+
"test/helpers-isolate.test.mjs"
|
|
1061
|
+
],
|
|
1062
|
+
"depends_on": [
|
|
1063
|
+
"B70"
|
|
1064
|
+
],
|
|
1065
|
+
"note": "root-added 2026-10-12; tool fix found while building (plans/pipeline.md); leakedEntries: a new path of a run whose record was in the baseline is not a leak; also amends test/no-leak.test.mjs"
|
|
1066
|
+
},
|
|
1067
|
+
"B78": {
|
|
1068
|
+
"title": "Full reviewed text comes back after a restore",
|
|
1069
|
+
"wave": 14,
|
|
1070
|
+
"level": "L1",
|
|
1071
|
+
"status": "landed",
|
|
1072
|
+
"owned": [
|
|
1073
|
+
"src/worker/review-state.mjs",
|
|
1074
|
+
"test/worker/review-state-restore.test.mjs"
|
|
1075
|
+
],
|
|
1076
|
+
"depends_on": [
|
|
1077
|
+
"B77"
|
|
1078
|
+
],
|
|
1079
|
+
"note": "root-added 2026-10-12; tool fix found while building (plans/pipeline.md), follows B68 (issue #15); reviewed_content_blob in the signed state_body, git hash-object/cat-file with a discovery ceiling, sha-checked, falls back to B68; also amends src/worker/engine.mjs, skill/references/review.md and skill/references/ledger.md"
|
|
1080
|
+
},
|
|
1081
|
+
"B71": {
|
|
1082
|
+
"title": "review-file --answers: a reviewer judges a fix made in another owned file",
|
|
1083
|
+
"wave": 14,
|
|
1084
|
+
"level": "L2",
|
|
1085
|
+
"status": "landed",
|
|
1086
|
+
"owned": [
|
|
1087
|
+
"src/review/answers.mjs",
|
|
1088
|
+
"test/review/answers.test.mjs"
|
|
1089
|
+
],
|
|
1090
|
+
"depends_on": [
|
|
1091
|
+
"B78"
|
|
1092
|
+
],
|
|
1093
|
+
"note": "root-added 2026-10-12; GitHub issue #9 review-exits plan (plans/review-exits/plan.md); answers ticket kind, signed review.answered rows bound to both content hashes, gate counts a still-current answer like a waiver (answered list on gate.review_waived_all), unknown severities fail closed; also amends src/review/gate-check.mjs, src/cli/review-file.mjs, src/cli/block.mjs, src/worker/queue.mjs, src/worker/engine.mjs, src/worker/loop.mjs, skill/references/review.md, skill/references/ledger.md and docs/how-it-works.md"
|
|
1094
|
+
},
|
|
1095
|
+
"B72": {
|
|
1096
|
+
"title": "block reopen: a human restarts a stopped or lost file on new content",
|
|
1097
|
+
"wave": 14,
|
|
1098
|
+
"level": "L2",
|
|
1099
|
+
"status": "landed",
|
|
1100
|
+
"owned": [
|
|
1101
|
+
"src/review/reopen.mjs",
|
|
1102
|
+
"test/review/reopen.test.mjs"
|
|
1103
|
+
],
|
|
1104
|
+
"depends_on": [
|
|
1105
|
+
"B71"
|
|
1106
|
+
],
|
|
1107
|
+
"note": "root-added 2026-10-12; GitHub issues #12 and #15 review-exits plan (plans/review-exits/plan.md); human-only verb on CODER_ONLY_FORBIDDEN, signed review.reopened row bound to the latest anchor seq, stop_content_hash in the state, one row per at_seq, L3 rung stays spent; also amends src/cli/block.mjs, src/util/forbidden.mjs, src/worker/engine.mjs, src/worker/queue.mjs, the forbidden-list pin tests, skill/references/review.md, skill/references/ledger.md, skill/references/code.md and docs/how-it-works.md"
|
|
1108
|
+
},
|
|
1109
|
+
"B79": {
|
|
1110
|
+
"title": "Review-exit leftovers: cap-row severity, answers packet secrets and budget, reopen config and write cause",
|
|
1111
|
+
"wave": 14,
|
|
1112
|
+
"level": "L1",
|
|
1113
|
+
"status": "landed",
|
|
1114
|
+
"owned": [
|
|
1115
|
+
"src/review/answers.mjs",
|
|
1116
|
+
"src/review/gate-check.mjs",
|
|
1117
|
+
"src/cli/block.mjs",
|
|
1118
|
+
"src/review/reopen.mjs",
|
|
1119
|
+
"test/review/answers.test.mjs",
|
|
1120
|
+
"test/review/reopen.test.mjs"
|
|
1121
|
+
],
|
|
1122
|
+
"depends_on": [
|
|
1123
|
+
"B72"
|
|
1124
|
+
],
|
|
1125
|
+
"note": "root-added 2026-10-12; leftovers from the B71 and B72 round-3 reviews; also amends B71 (answers packet, openAt) and B72 (reopen config, write failure)"
|
|
1126
|
+
},
|
|
1127
|
+
"B73": {
|
|
1128
|
+
"title": "A patch check that closes findings is progress; stop answers name the exits",
|
|
1129
|
+
"wave": 14,
|
|
1130
|
+
"level": "L2",
|
|
1131
|
+
"status": "landed",
|
|
1132
|
+
"owned": [
|
|
1133
|
+
"src/review/fixloop.mjs",
|
|
1134
|
+
"src/review/stop-exits.mjs",
|
|
1135
|
+
"src/cli/review-file.mjs",
|
|
1136
|
+
"src/review/gate-check.mjs",
|
|
1137
|
+
"test/review/patch-progress.test.mjs",
|
|
1138
|
+
"test/worker/fix-list.test.mjs",
|
|
1139
|
+
"test/autopilot/actions.test.mjs",
|
|
1140
|
+
"docs/how-it-works.md",
|
|
1141
|
+
"skill/references/review.md"
|
|
1142
|
+
],
|
|
1143
|
+
"depends_on": [
|
|
1144
|
+
"B79"
|
|
1145
|
+
],
|
|
1146
|
+
"note": "root-added 2026-10-12; issue #12 items 9-10; also amends B71 (review-file stopped answer) and the gate review_cap/unreviewed detail"
|
|
1147
|
+
},
|
|
1148
|
+
"B81": {
|
|
1149
|
+
"title": "Deterministic ledger rotation race test",
|
|
1150
|
+
"wave": 14,
|
|
1151
|
+
"level": "L1",
|
|
1152
|
+
"status": "landed",
|
|
1153
|
+
"owned": [
|
|
1154
|
+
"src/ledger/write.mjs",
|
|
1155
|
+
"test/ledger/write.test.mjs"
|
|
1156
|
+
],
|
|
1157
|
+
"depends_on": [
|
|
1158
|
+
"B73"
|
|
1159
|
+
],
|
|
1160
|
+
"note": "root-added 2026-10-12; the claimRotatedName two-racers test failed once under full-suite load during the B73 land; an afterLink hook forces the race order"
|
|
1161
|
+
},
|
|
1162
|
+
"B74": {
|
|
1163
|
+
"title": "A deleted owned file is reviewed with a deletion note",
|
|
1164
|
+
"wave": 14,
|
|
1165
|
+
"level": "L1",
|
|
1166
|
+
"status": "landed",
|
|
1167
|
+
"owned": [
|
|
1168
|
+
"src/review/engine.mjs",
|
|
1169
|
+
"src/review/deletion.mjs",
|
|
1170
|
+
"test/worker/deletion-review.test.mjs",
|
|
1171
|
+
"docs/how-it-works.md",
|
|
1172
|
+
"skill/references/review.md"
|
|
1173
|
+
],
|
|
1174
|
+
"depends_on": [
|
|
1175
|
+
"B81"
|
|
1176
|
+
],
|
|
1177
|
+
"note": "root-added 2026-10-12; issue #8; the deletion flow already worked end to end (pinned by test); adds the deletion note with bounded reference search"
|
|
1178
|
+
},
|
|
1179
|
+
"B75": {
|
|
1180
|
+
"title": "review-file --fresh re-runs only results that never counted",
|
|
1181
|
+
"wave": 14,
|
|
1182
|
+
"level": "L1",
|
|
1183
|
+
"status": "landed",
|
|
1184
|
+
"owned": [
|
|
1185
|
+
"src/cli/review-file.mjs",
|
|
1186
|
+
"src/worker/queue.mjs",
|
|
1187
|
+
"test/worker/fresh.test.mjs",
|
|
1188
|
+
"test/worker/queue.test.mjs",
|
|
1189
|
+
"skill/references/review.md",
|
|
1190
|
+
"skill/references/code.md"
|
|
1191
|
+
],
|
|
1192
|
+
"depends_on": [
|
|
1193
|
+
"B74"
|
|
1194
|
+
],
|
|
1195
|
+
"note": "root-added 2026-10-12; issue #10 item 4; classify the done marker in place, claim only a re-runnable one by atomic rename, restore by link; leftovers in B83"
|
|
1196
|
+
},
|
|
1197
|
+
"B82": {
|
|
1198
|
+
"title": "Deterministic worker restart kill test",
|
|
1199
|
+
"wave": 14,
|
|
1200
|
+
"level": "L1",
|
|
1201
|
+
"status": "landed",
|
|
1202
|
+
"owned": [
|
|
1203
|
+
"test/worker/worker.test.mjs"
|
|
1204
|
+
],
|
|
1205
|
+
"depends_on": [
|
|
1206
|
+
"B75"
|
|
1207
|
+
],
|
|
1208
|
+
"note": "root-added 2026-10-12; the 'new worker re-reads 3 queued tickets' test checked a killed pid once and failed under load (zombie window); it now waits for ESRCH with a 5 s deadline"
|
|
1209
|
+
},
|
|
1210
|
+
"B76": {
|
|
1211
|
+
"title": "the skill and docs name the review exits in order",
|
|
1212
|
+
"wave": 14,
|
|
1213
|
+
"level": "L1",
|
|
1214
|
+
"status": "landed",
|
|
1215
|
+
"owned": [
|
|
1216
|
+
"skill/references/review.md",
|
|
1217
|
+
"skill/references/code.md",
|
|
1218
|
+
"skill/references/security.md",
|
|
1219
|
+
"skill/templates/coder-brief.md",
|
|
1220
|
+
"docs/how-it-works.md",
|
|
1221
|
+
"test/skill/structure.test.mjs"
|
|
1222
|
+
],
|
|
1223
|
+
"depends_on": [
|
|
1224
|
+
"B75"
|
|
1225
|
+
],
|
|
1226
|
+
"note": "root-added 2026-10-12; group A docs; exits in order (fix + block reopen, --answers, block waive, re-decompose), block attempt never resets review state, a lost state is rebuilt from the ledger, when --fresh helps"
|
|
1227
|
+
},
|
|
1228
|
+
"B83": {
|
|
1229
|
+
"title": "review-file --fresh fails closed when a marker cannot be restored",
|
|
1230
|
+
"wave": 14,
|
|
1231
|
+
"level": "L1",
|
|
1232
|
+
"status": "landed",
|
|
1233
|
+
"owned": [
|
|
1234
|
+
"src/worker/queue.mjs",
|
|
1235
|
+
"src/cli/review-file.mjs",
|
|
1236
|
+
"test/worker/fresh.test.mjs"
|
|
1237
|
+
],
|
|
1238
|
+
"depends_on": [
|
|
1239
|
+
"B76"
|
|
1240
|
+
],
|
|
1241
|
+
"note": "root-added 2026-10-12; B75 leftovers (plans/reviews/b75/r3.txt a-d); a failed link-back writes a refused fresh-restore-failed marker or keeps the claim; a kept claim holds the ticket; block reopen clears claims"
|
|
1009
1242
|
}
|
|
1010
1243
|
}
|
|
1011
1244
|
}
|
package/package.json
CHANGED
package/skill/references/code.md
CHANGED
|
@@ -12,11 +12,11 @@ The coder then forecasts **cases** (tests) and **lines**. Compute the floor your
|
|
|
12
12
|
|
|
13
13
|
`forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] [--kind code|docs|contract] [--tests <paths…>] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused; an owned entry with no glob character — `*`, `?`, `{` — owns that path and every path below it, so `src/feature` owns `src/feature/a.swift` but not `src/featureX/a.swift`, and `src/feature` overlaps `src/feature/x.swift` or `src` in another block — unless the entry is a regular file in the workspace at `block open` (or `block claim`): then it owns only itself, recorded as `owned_kinds`, and every later check reads that answer; a glob owns what it matches, and a brace entry `src/{a,b}` is a glob, so `src/a` owns nothing below it; no trailing `/`), the block kind (declared, else detected — `decisions.md` §5), the level (a docs/contract block below `levels.coder_floor_docs`, default L1, is raised to it and the CLI prints the floor), the clauses with their test ids and the forecast, writes the ledger `dispatch` row and prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` (≤ 200 B). Call it before every spawn, in every engine. `--level` may be left out only in a brief-mode run (`plan.md` §6: the rules pick the lane from the owned entries, `--lines` and `proof.tiers.high.paths`, and a signed `decision` row with `source: rules` records it) or when autopilot recorded a level; otherwise its absence is a usage error. A docs block that owns a glob (`docs/**`) must declare `--kind docs`: a glob is judged by its literal ending, so undeclared it counts as `code`. `--tests <paths…>` (same path rules as `--owned`, else `bad-tests`; exact paths only — a file or a directory; `*` and `?` are refused, and so is a path starting with `-`, both a usage error, exit 2; `[` and `{` are allowed, as in `app/[id]/page.test.tsx`) names the block's test files, stored as `test_files` on the block and the `dispatch` row; a `{test_files}` gate runs them (`proof.md` §3). Without it the owned entries that are concrete files (no glob) and look like tests are used; `--tests` is the precise way.
|
|
14
14
|
|
|
15
|
-
Under `proof.isolation: worktree` (B62a, `proof.md` §3), `block open` first makes the block's own worktree. The output then has a second line, `worktree <path> · branch <branch>` (with `· reused` when a
|
|
15
|
+
Under `proof.isolation: worktree` (B62a, `proof.md` §3), `block open` first makes the block's own worktree. The output then has a second line, `worktree <path> · branch <branch>` (with `· reused` when a second `block open` took the earlier attempt's tree), before the pointer. Give the coder that path as its working directory. Two worktree blocks may own the same path. A refused worktree refuses the open, and nothing is recorded. A second `block open` keeps the tree's base: do not pass a different `--base`. The coder works in that tree path: it edits there, commits on the block's branch, and calls `review-file` there. Run `gates run --run <r> --block <id>` without `--cwd` (another `--cwd` is refused, exit 2). Gates, reviews and the close all measure the tree (B62b, `proof.md` §3): a change made only in the main checkout is not the block's work, and a block whose tree was deleted cannot close until the tree is restored.
|
|
16
16
|
|
|
17
|
-
**`--json` on every block verb (B64a).** `block open|attempt|rebase|claim|close|stop|waive … --json` prints exactly one JSON line on success instead of the text: `{"block":<id>,"event":<open|attempt|rebased|claimed|closed|stopped|waived>, …}` with the facts the text prints — open: `level`, `asked`, `source` (`flag`/`rules`/`autopilot`), `kind`, `attempt`, the full `base_sha`, `tests` (count), `worktree` (`{path, branch, reused}` or null) and `brief` (the pointer or null); attempt: `attempt`; rebase: `old_base`, `new_base`; claim: `path`; waive: `finding`, `file`, `by`; stop: nothing more; close: `status`, `reason`, `close_event` (or null), plus `worktree {removed, path, branch}` with `--remove-worktree` (on a block with no worktree that flag is a usage error, exit 2). On failure the exit code and stderr are as without the flag and stdout stays empty — except a close the gate refused, which also prints `{"block":<id>,"ok":false,"reason":<text>}`; every other close refusal (usage, not open, a cancelled `--no-require-reviews`, a failed worktree removal) leaves stdout empty. Without `--json` the output is unchanged.
|
|
17
|
+
**`--json` on every block verb (B64a).** `block open|attempt|rebase|claim|close|stop|waive|reopen … --json` prints exactly one JSON line on success instead of the text: `{"block":<id>,"event":<open|attempt|rebased|claimed|closed|stopped|waived|reopened>, …}` with the facts the text prints — open: `level`, `asked`, `source` (`flag`/`rules`/`autopilot`), `kind`, `attempt`, the full `base_sha`, `tests` (count), `worktree` (`{path, branch, reused}` or null) and `brief` (the pointer or null); attempt: `attempt`; rebase: `old_base`, `new_base`; claim: `path`; waive: `finding`, `file`, `by`; reopen: `file`, `from`, `stop_reason`, `at_seq`, `from_hash`, `to_hash`, `ticket_cleared`, `by`; stop: nothing more; close: `status`, `reason`, `close_event` (or null), plus `worktree {removed, path, branch}` with `--remove-worktree` (on a block with no worktree that flag is a usage error, exit 2). On failure the exit code and stderr are as without the flag and stdout stays empty — except a close the gate refused, which also prints `{"block":<id>,"ok":false,"reason":<text>}`; every other close refusal (usage, not open, a cancelled `--no-require-reviews`, a failed worktree removal) leaves stdout empty. Without `--json` the output is unchanged.
|
|
18
18
|
|
|
19
|
-
**Block end: done file and `on_block_end` hook (B64b).** Every block end — `block close` that closes or stops it (a pin event or `ledger.tamper`) and `block stop` — writes `<run root>/blocks/<id>.done.json` (the run's state folder, `~/.code-forge/runs/<run>/`) atomically after the end's rows, overwritten on a later end; its absolute path is in the hook's `CODE_FORGE_DONE_FILE` and in `done_file` of `close`/`stop --json`. Content: `{block, run_id, status, event, reason, commit, spent_usd, unknown_usd_sessions, findings, at}` — `status` `closed`/`stopped`; `event` `block.close`, `block.stop` or the close's own stop event (e.g. `worker.replaced`, `ledger.tamper`); `reason` null for a plain close; `commit` the HEAD of the block's tree or the repository, null when unreadable; `spent_usd`/`unknown_usd_sessions` from the block's own rows as `forge status` computes them, both null when the ledger cannot be read; `findings` the open review findings, null when the ledger cannot be read or the count fails. It never holds transcript text, diff text or secrets. A failed write changes nothing else and prints `warning: done file not written: <reason>` on stderr. Wait for that file (or the hook) instead of polling `forge status` or the coder's transcript. A done file from an earlier end stays after a
|
|
19
|
+
**Block end: done file and `on_block_end` hook (B64b).** Every block end — `block close` that closes or stops it (a pin event or `ledger.tamper`) and `block stop` — writes `<run root>/blocks/<id>.done.json` (the run's state folder, `~/.code-forge/runs/<run>/`) atomically after the end's rows, overwritten on a later end; its absolute path is in the hook's `CODE_FORGE_DONE_FILE` and in `done_file` of `close`/`stop --json`. Content: `{block, run_id, status, event, reason, commit, spent_usd, unknown_usd_sessions, findings, at}` — `status` `closed`/`stopped`; `event` `block.close`, `block.stop` or the close's own stop event (e.g. `worker.replaced`, `ledger.tamper`); `reason` null for a plain close; `commit` the HEAD of the block's tree or the repository, null when unreadable; `spent_usd`/`unknown_usd_sessions` from the block's own rows as `forge status` computes them, both null when the ledger cannot be read; `findings` the open review findings, null when the ledger cannot be read or the count fails. It never holds transcript text, diff text or secrets. A failed write changes nothing else and prints `warning: done file not written: <reason>` on stderr. Wait for that file (or the hook) instead of polling `forge status` or the coder's transcript. A done file from an earlier end stays after a second `block open` of the block, so count it only when its `at` is later than the current dispatch; if no fresh done file appears by the time the coder agent reports, run `forge status` once. With `hooks.on_block_end: {argv, timeout_s}` set before `run start` (immutable; `run reload` refuses a change), the CLI runs `argv` once per end — also when a later step such as `--remove-worktree` fails — after the done file and outside the run lock: no shell, cwd = the repository top, stdin closed, output discarded, the process group killed at `timeout_s` (1–600, default 60), with `CODE_FORGE_RUN`, `CODE_FORGE_BLOCK`, `CODE_FORGE_BLOCK_STATUS` and `CODE_FORGE_DONE_FILE` (`""` when the done file was not written) added to its env; one signed `hook.ran` row per end (`ledger.md`). A hook that does not succeed (exit 0) only warns: `warning: on_block_end hook exit N` | `timed out` | `not found` | `signal <NAME>` | `failed to start (<code>)`; if the `hook.ran` row itself cannot be written, one more line `warning: on_block_end hook row not written (<code>)` follows (an error code only, never message text). With the hook set, `close --json` and `stop --json` add `done_file` (the absolute path, or null when it was not written); without it the output is byte-identical to before. The hook is the owner's command and runs outside coders: a coder never runs it (the close gate flags a transcript line that runs its argv[0] basename plus argv[1] when present and not blank — both trimmed, argv[1] one literal token even with spaces inside, which a transcript line split on spaces never matches — as `rule_break … code-forge-on-block-end-hook`), and `.code-forge.yml` is non-ownable (the same guard as `review.external.required`) with the hook set — also when `hooks.on_block_end` is the only key that guards it.
|
|
20
20
|
|
|
21
21
|
**The context pack (B63a).** With a `context` key in `.code-forge.yml` (`rules`, `lessons`, `commands`, `max_lines`), build the block's pack right after `block open`, before the pointer is sent, and again before every later coder task (fix rounds too): `forge context pack --run <r> --block <id>` writes `<run root>/context/<id>.md` and prints its path and `tokens ~<n>` (`--json`: `{"path":…,"tokens":…,"lines":…,"cut":…,"sha256":…}`). The pack file lives under the run root, which a brief never names (see below): paste the pack's **text** into the coder task with the pointer and with every later coder task (or copy the file to a location outside the run root and name that), and keep the run-root path out of the brief. The pack holds, in order: the block header (run, block, level, owned files, the test files when present, the worktree path and branch when present), the project's build, test, lint and format commands as argv text in POSIX shell quoting and redacted like the rules (code-forge never runs them; a control character in a token, a path or a header value is refused), each rules file, then the lessons file, each under its source path (an empty or whitespace-only file gives its heading alone). Over `max_lines` (default 300, minimum 20) the lessons are cut first, then the rules files from the last one backwards, each cut section ending in `[cut: N lines dropped]`; the header and the commands are never cut. Rules and lessons are read from the main tree. A missing file, or a path outside the repository (`..`, an absolute path, a symlink leading out — a broken symlink is judged by where it points), refuses the pack (exit 1, naming the key and the path, nothing written); so does a config without `context` or with a bad `context` value. Each new pack writes a signed `context.pack` row; the same pack again writes none and prints the same output. The key is read from the live config, not the run's snapshot: it is not authority, and `run reload` takes a change to it — except `context.require_coder_tests` (the close check for the TESTS line, below), which is fixed for the run. The coder may run the verb too, but a pack the coder builds (and its path) is for the coder's own use only, never what the orchestrator sends; `.code-forge.yml` stays non-ownable.
|
|
22
22
|
|
|
@@ -24,12 +24,12 @@ The brief file carries, in this order:
|
|
|
24
24
|
|
|
25
25
|
1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
|
|
26
26
|
2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
|
|
27
|
-
3. The file loop: *after you finish each file in `owned_files` (tests written and green) run `forge review-file <path> --block <id> --run <r>`; before the next file run `forge review-file --wait <ticket> [--max 90s]` and act on the packet.* A `worker_down` answer ⇒ print `===BLOCK <id> FAILED: worker down===` (never approval).
|
|
27
|
+
3. The file loop: *after you finish each file in `owned_files` (tests written and green) run `forge review-file <path> --block <id> --run <r>`; before the next file run `forge review-file --wait <ticket> [--max 90s]` and act on the packet.* A `worker_down` answer ⇒ print `===BLOCK <id> FAILED: worker down===` (never approval). A cached result that never counted (`stale`, `unavailable`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`) is run again with `forge review-file <path> --block <id> --run <r> --fresh` (B75); anything else (a counted round, `review-state-lost`, or an unknown result) answers `fresh-needs-reopen` (`fresh-tampered` for a tampered state), and the coder stops and reports. A finding whose fix belongs in another owned file (a missing test, say) is fixed there and sent with `forge review-file <that file> --block <id> --run <r> --answers <ids> --of <source>` (`review.md` §5).
|
|
28
28
|
4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines) — left out with a `context` key, because the pasted pack carries the rules and lessons — the forbidden list verbatim (`security.md` §2), and the sentinel contract: the `TESTS` line of item 6, then `===BLOCK <id> COMPLETE===` followed by the test summary, `git diff --stat` and one `reviewed <path> ticket <ticket-id>` line per changed file, or `===BLOCK <id> FAILED: <reason>===`.
|
|
29
29
|
5. **FAILED unless** (`templates/coder-brief.md`): a final report — first attempt or fix round — that does not list the `review-file` ticket id of every changed file counts as FAILED whatever its sentinel says. Pass the report to the gate with `forge block close <id> --run <r> --report <file>`; it refuses `coder report FAILED: no review-file ticket id for <files>`.
|
|
30
30
|
6. **The green test run.** The coder runs the block's test command and prints one line that is exactly `TESTS <command> exit=<n>` just before `===BLOCK <id> COMPLETE===` — on the first attempt and again, freshly run, in every fix round. `<command>` is the command it ran (not empty); `<n>` is its exit code as a decimal 0–255 with no leading zeros. The line is matched after trimming; a line quoted inside prose, a non-numeric, signed or out-of-range `<n>`, or a leading zero does not count.
|
|
31
31
|
|
|
32
|
-
The brief never mentions `block waive`, keys, or `~/.code-forge/runs/`. The coder process gets no Jev key (the System 1 model's key, `keys.jev`) and no signer key.
|
|
32
|
+
The brief never mentions `block waive`, `block reopen`, keys, or `~/.code-forge/runs/`. The coder process gets no Jev key (the System 1 model's key, `keys.jev`) and no signer key.
|
|
33
33
|
|
|
34
34
|
**The close check for the TESTS line (B63b).** The gate reads the LAST `TESTS` line in the segment of the block's last sentinel: after the previous sentinel line of any block (an earlier attempt, or another block in a shared log) and before this one; a line after the sentinel is ignored. Set `context.require_coder_tests: true` in `.code-forge.yml` **before `run start`** to make it a close check: it is read from the run's config snapshot only, never the live file, and `run reload` refuses a change to it (`context.require_coder_tests cannot change mid-run`). With it on, `--report` is required, and `block close` refuses (exit 1, the block stays open, no `block.close` row) with one of the messages below. This check runs before the other close checks and only reads the report, so a refusal here writes no row at all — not even the human waiver row of `--no-require-reviews`:
|
|
35
35
|
|
|
@@ -60,7 +60,7 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
|
|
|
60
60
|
| no rule break | the transcript grep for forbidden verbs and paths (`--transcript <file>`, else the run record's log); a missing transcript ⇒ a signed `gate.transcript_missing` row and a WARN, never silence |
|
|
61
61
|
| high tier proven | every changed high-tier file (`proof.md` §1) has a signed, proven red→green row with `red_kind: assertion` for this block whose `covers` names it (light tier: optional), else `unproven <file>`; only the human's `block waive <id> proof --file <path>` clears it |
|
|
62
62
|
| rows genuine | the MAC of every gate-relevant row verifies; the live worker matches the pinned pid |
|
|
63
|
-
| nothing left open | no unruled `late_findings` (a ruling clears only a warning or nit ruled `nit`; a critical late finding needs the human's waiver); no file
|
|
63
|
+
| nothing left open | no unruled `late_findings` (a ruling clears only a warning or nit ruled `nit`; a critical late finding needs the human's waiver); no stopped file: every stop (`review_cap`, `l3_patch_exhausted`, `split_required`, …) writes a `review.cap` row, and the file holds the close until a signed approval of its current content comes after that row, or every open finding is waived or answered (then it closes with a `gate.review_waived_all` row) |
|
|
64
64
|
|
|
65
65
|
`forge block close <id> --run <r> --remove-worktree` (B62a, worktree isolation only) removes the block's worktree after a successful close. It runs `git worktree remove` without `--force` and keeps the branch.
|
|
66
66
|
- Success prints `block <id> worktree removed: <path> (branch <branch> kept)` and writes a signed `worktree.removed` row.
|
|
@@ -68,6 +68,15 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
|
|
|
68
68
|
|
|
69
69
|
Without the flag the tree stays. On a block without a worktree the flag is a usage error (exit 2). The flag is forbidden to coders: only you remove a tree.
|
|
70
70
|
|
|
71
|
+
**When a file stops** (`review_cap`, `l3_patch_exhausted`, `split_required`, …), the block cannot close. The stop's `exits` name the ways out. Take them in this order; `review.md` §5 gives each one's rules and refusals:
|
|
72
|
+
|
|
73
|
+
1. **Fix, then `block reopen`.** When the fix belongs in the stopped file, have it fixed. Then run `forge block reopen <id> --file <path> --run <r> --reason <text>` only after the human says so in chat, with their reason, exactly like `block waive`. Never run it on your own or for a coder (it is on the coder forbidden list). After it, send the coder's `review-file` again: the next round is a full round 1 when the content differs from what the stop reviewed; on the stop's own content the stop repeats. A `reopen-same-content` refusal means the content is still what the stop reviewed: offer the waive instead.
|
|
74
|
+
2. **`review-file --answers`.** Use it when the fix belongs in another owned file (a missing test, say). The coder fixes it there and runs `forge review-file <that file> --block <id> --run <r> --answers <ids> --of <source>`. A fresh reviewer judges the fix. It needs no human step.
|
|
75
|
+
3. **`block waive`.** Waive each open finding, after the human says so. A file whose findings are all waived or answered closes with a signed `gate.review_waived_all` row.
|
|
76
|
+
4. **Re-decompose.** The human decides, and the architect plans new blocks.
|
|
77
|
+
|
|
78
|
+
`block attempt` never resets review state: it only raises the attempt counter, and a stopped file stays stopped. Use `review-file --fresh` only after a result that never counted (`stale`, `unavailable`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); it never re-runs a counted round. A missing state file needs no exit: the worker first rebuilds it from its ledger anchor (`review.state_restored`). Only a state whose anchor has no `state_body` is `lost` (`review-state-lost`); that one needs the human's `block reopen`, on its current content. A `review-state-tampered` file has none of these exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
79
|
+
|
|
71
80
|
`--no-require-reviews` is the human's way out, written as its own audit row. The human runs it themselves at a terminal: it asks for a yes, has no `--yes`, exits 2 unless stdin and stdout are both terminals, lifts `no_changes` and is refused while an autopilot grant is active — so never run it for them; tell them the command when they say so in chat. The owner's other way out is `review.external` in the config before `run start` (`review.md` §1): the gate then skips the review rows and the report's ticket ids and writes `gate.reviews_waived {by: config}`; every other check, `no_changes` included, still applies. A finding counts only under the gate's own configuration; a tautology (an expected value computed by the code under test) is a finding. Reproduce disclosures: a coder that reports its own rule break did the hard part.
|
|
72
81
|
|
|
73
82
|
## §6 Budget — the stop is the feature (R6)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Every row has the run, the block, the event and a timestamp; the ones the block gate relies on carry `mac` (`security.md` §3). Useful fields: `level`, `provider`, `model`, `effort`, `fallback_step` (every spawn), `role ∈ {coder, reviewer, judge, s2, author, facts}`, `trigger` (escalations), `source ∈ {jev, rules, s2, s2-fallback, shadow}` (decisions), `depth_unconstrained` / `depth_chosen` / `degrade_step` and `context_mode ∈ {whole, hunks, minimal, recheck}` (reviews), `round` and `kind ∈ {full, recheck, patch_check, sweep}` (rounds), `isolation ∈ {export, lock}` (proof), `l3_patch`, `l3_fallback`, `overspend`. **Tokens are facts, dollars are estimates**: `tokens_source: reported` when the CLI's JSON carries usage, else `estimated`. File paths in rows are relative to the repository root.
|
|
6
6
|
|
|
7
|
-
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch` (`test_files` when `block open --tests` named them), `gate.red` and `gate.done` (with `scope: related` + `test_count` or `scope: full` + `reason: threshold | no_tests` when a gate has the `{test_files}` item, `no_tests` also without `--run/--block`; with `--run/--block` they name `run` and `block`, and for a block with a worktree also `worktree: <path>` (the gates ran there); a gate with env names only `env_keys` in its result, `proof.md` §3), `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}`, `gate.reviews_waived {by: human | config}` (`by: config` from `review.external`), `coder.tests {block, command, exit}` (signed; under `context.require_coder_tests` the close read the coder's `TESTS <command> exit=0` line from `--report`, written with the close rows just before `block.close` and only when the block closes, `code.md` §3), `decision {question: lane, source: rules}` (a brief-mode `block open` without `--level`), `worktree.created {block, path, branch, base}` (`block open` under `proof.isolation: worktree` made the block's tree, written before `dispatch`; a re-open that reuses it writes none), `worktree.removed {block, path, branch}` and `worktree.remove_failed {block, path, reason}` (after `block close --remove-worktree`; the branch is always kept, and on a failure the tree stays, `proof.md` §3), `context.pack {block, path, sha256, tokens, lines, cut}` (signed; `forge context pack` wrote a new pack for the block — the same pack again, same sha256 as the block's last row, writes none; `code.md` §3), `hook.ran {block, hook: on_block_end, argv0, exit, signal, timed_out, duration_ms}` (signed; one row per block end with `hooks.on_block_end` set — also when the hook fails, times out or is not found; none when the key is absent; `argv0` is the basename only. Exited: `exit` its code, `signal` null. Killed by a signal: `exit` null, `signal` its name. Timed out: `exit` null, `signal` `SIGKILL`, `timed_out` true. Not found (argv[0] ENOENT) or any other spawn error (a missing cwd, EACCES): `exit` null, `signal` null, `timed_out` false; `code.md` §3), `outside_review {run, pr, rounds, findings, link (or null), blocks}` (signed; `forge ledger outside-review`, §4 — the latest row per PR wins in every reader), `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
|
|
7
|
+
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch` (`test_files` when `block open --tests` named them), `gate.red` and `gate.done` (with `scope: related` + `test_count` or `scope: full` + `reason: threshold | no_tests` when a gate has the `{test_files}` item, `no_tests` also without `--run/--block`; with `--run/--block` they name `run` and `block`, and for a block with a worktree also `worktree: <path>` (the gates ran there); a gate with env names only `env_keys` in its result, `proof.md` §3), `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}`, `review.state {block, file, seq, state_sha, l3_rung_used, state_body}` (signed; the fix-loop state anchor, `review.md` §5 step 8; `state_body` carries `reviewed_content_sha` and `reviewed_content_blob`, the git blob id of the reviewed text in the reviewed tree's object store, or null when there was no reviewed text or no content root, or the write failed; when the signed body is over 64 KB, `state_omitted: "too_large"` replaces `state_body`, and a lost state file then refuses as `review-state-lost`), `review.state_restored {block, file, seq, from_seq}` (signed; a lost state file was rebuilt from the anchor at `from_seq` and saved as `seq`), `review.reopened {block, file, from: stopped | lost, stop_reason, at_seq, from_hash, to_hash, reason, by: human}` (signed; B72: the human's `block reopen` of a stopped or lost file — `at_seq` the file's latest `review.state` anchor seq, `from_hash` the content the stop reviewed (null for `lost`), `to_hash` the current content, `stop_reason` null for `lost`; at most one row per `at_seq`, the verb refuses a second one for other content as `reopen-already-recorded`; the worker starts a fresh round-1 state for it once, saved at `at_seq + 1` — for `from: stopped` only when the current content still differs from the stop's content (the state's `stop_content_hash`) and from `from_hash`, else the stop repeats; round 1 is a full review (`reviewed_content` null) and `l3_rung_used` is kept, so the file gets up to `review.max_rounds_per_file` rounds at L2 again; the worker ignores a row that does not verify, of another run, not `by: human` or at another seq), `review.answered {block, file, content_hash, finding, by_file, by_hash, ticket}` (signed; B71: an answers ticket's reviewer found that `by_file` at `by_hash` resolves the open finding `finding` of `file` at `content_hash` — exactly one row per (finding, `by_hash`, ticket): a retried ticket writes no duplicate, only the rows a failed write left out; the close counts it only while both hashes are current, and ignores a row without a valid run-key MAC or from another run; an answer may clear a critical and never approves `by_file`, `review.md` §5 step 5), `gate.reviews_waived {by: human | config}` (`by: config` from `review.external`), `gate.review_waived_all {block, file, content_hash, findings, answered?, by: human | autopilot | null}` (signed; a file with no approval closed as `reviewed_by_waiver`: a signed `review.round` at its current hash, and every finding of the latest hash-bound refusal and every open `review.cap` id cleared by an accepted waiver or a still-current signed `review.answered` row (B71) — a file cleared only by answers is `reviewed_by_waiver` too. `findings` are the sorted WAIVED ids and `answered` (B71, only when there is one) the sorted ids `review.answered` rows cleared; `by: human` only when every clearing waiver is by a human, else `autopilot`, and `by: null` only when every id was answered (`findings: []`) — a reader must handle null. One row per such file just before `block.close`, none on a refused close, `review.md` §5 step 5), `coder.tests {block, command, exit}` (signed; under `context.require_coder_tests` the close read the coder's `TESTS <command> exit=0` line from `--report`, written with the close rows just before `block.close` and only when the block closes, `code.md` §3), `decision {question: lane, source: rules}` (a brief-mode `block open` without `--level`), `worktree.created {block, path, branch, base}` (`block open` under `proof.isolation: worktree` made the block's tree, written before `dispatch`; a re-open that reuses it writes none), `worktree.removed {block, path, branch}` and `worktree.remove_failed {block, path, reason}` (after `block close --remove-worktree`; the branch is always kept, and on a failure the tree stays, `proof.md` §3), `context.pack {block, path, sha256, tokens, lines, cut}` (signed; `forge context pack` wrote a new pack for the block — the same pack again, same sha256 as the block's last row, writes none; `code.md` §3), `hook.ran {block, hook: on_block_end, argv0, exit, signal, timed_out, duration_ms}` (signed; one row per block end with `hooks.on_block_end` set — also when the hook fails, times out or is not found; none when the key is absent; `argv0` is the basename only. Exited: `exit` its code, `signal` null. Killed by a signal: `exit` null, `signal` its name. Timed out: `exit` null, `signal` `SIGKILL`, `timed_out` true. Not found (argv[0] ENOENT) or any other spawn error (a missing cwd, EACCES): `exit` null, `signal` null, `timed_out` false; `code.md` §3), `outside_review {run, pr, rounds, findings, link (or null), blocks}` (signed; `forge ledger outside-review`, §4 — the latest row per PR wins in every reader), `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
|
|
8
8
|
|
|
9
9
|
## §2 `forge report --slug <slug> [--json] [--export <dir>]`
|
|
10
10
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## §1 The trigger is a CLI call, and the file set is complete
|
|
4
4
|
|
|
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 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. 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.
|
|
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`, 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. A coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
|
|
6
6
|
|
|
7
7
|
Under `proof.isolation: worktree` (`proof.md` §3) the coder calls `review-file` inside the block's tree. The ticket still goes to the main tree's queue, where the worker runs. The content hash, the diff against the block's base and the peers' diffs are read in the block's tree, which the worker takes from the run record, and the ticket and the answer carry `tree: <path>`. The tree is part of the ticket id, and a ticket whose tree is not the one in the run record, or a ticket with no tree for a block that has one, is answered `review.unavailable` with `reason: worktree-mismatch`. A block the run record gives no worktree is hashed in the main tree, wherever `review-file` is called, and its ticket names no tree. The packet's facts slot names the tree and its branch. A change made only in the main tree is never seen. A recorded tree that cannot be used, or an unreadable run record, refuses `review-file` itself (exit 1, nothing queued); after the ticket was queued the worker answers `review.unavailable` with the same reason. The reasons: `worktree-missing` (gone), `worktree-not-dir` (a file), `worktree-malformed` (a bad record), `worktree-branch-mismatch` (off its branch), `worktree-foreign` (a checkout of another repository), `worktree-workspace-missing` (no workspace subdirectory in the tree), `git-failed` (git could not run, with its reason), `run-record-unreadable` (the record cannot be parsed or names no workspace), plus `worktree-mismatch` (worker only) and `bad-path` (`review-file` only). See `proof.md` §3 for each cause.
|
|
8
8
|
|
|
@@ -21,6 +21,8 @@ The topology is decided in one place (`planSessions`). Consensus (§6) runs only
|
|
|
21
21
|
|
|
22
22
|
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, 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.
|
|
23
23
|
|
|
24
|
+
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.
|
|
25
|
+
|
|
24
26
|
## §4 The stub guard — what counts as a review (R11)
|
|
25
27
|
|
|
26
28
|
A result is a review only if the process exited 0, the JSON validates against the finding schema, `reviewed_hunks` equals the packet's hunk headers exactly and in order, and the output has at least `review.min_tokens_out` tokens (a clean pass — `passed: true`, no findings, a non-blank summary — needs only 40, whatever the diff size). Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`, `budget` — the run reached `budget.usd`, `ledger.md` §3.1; `context-unreadable`, before any session, is retried differently: see §3) and **is not approval**; `review-file` on the same content re-runs the same packet once, then the file stays unreviewed and the block cannot close. Every `schema` miss leaves a signed `review.schema_invalid` row with a structural diagnosis of the answer — its size and kind (`json`, `not-json`, `empty`), its top-level keys and their types, and the schema errors (path, keyword, message), or the parse error and its byte offset — never the answer's text. At a packet's **second** `schema` miss (same run, file, content hash, packet text, level and role — the first try plus that re-run), the worker tries the packet ONCE on `review.second_levels.<level>` when the owner configured one, and writes a signed `review.schema_fallback {lens, role, level, packet_hash, from_provider, from_model, to, to_provider, to_model, attempts, refused?}` row first (no row written ⇒ no try; any earlier row for the packet at that level ⇒ no try). Its first model that is not the failed one — and, for a closed-book role, not Codex unless `review.allow_open_book_codex` — runs (`refused: same-model` or `refused: closed-book` when there is none). The session's tokens are both sessions'. Its answer goes through the same stub guard; a valid one is a normal answer, a failed one leaves the file unavailable. Never in consensus mode; `budget.usd` refuses it like any session. Without a second level only the diagnosis row is new. A session that hangs is killed at `review.session_timeout_s` (default 300), process group and all, and is spawned once more on the same packet and level; only a second timeout is `unavailable: timeout`. Each timed-out attempt leaves a `review.session_timeout` ledger note with the last 4 KB of its stderr (redacted, never the packet) and only the byte count and last event types of its stdout.
|
|
@@ -33,10 +35,26 @@ Per file, per block:
|
|
|
33
35
|
2. **Round n ≥ 2 reviews only the fix hunks**: the diff between the content hash reviewed in round n−1 and the current content, ± `review.context.hunk_context_lines`, plus the open findings by id (`review.recheck_scope: fix_hunks`, the default; `file` re-sends the whole packet for a consumer that wants it). The `recheck` lens answers `resolved[]` per finding — S1 `resolved` at or above `thresholds.findings.resolved` closes it, else the fresh L2 recheck decides — and may raise a new finding **only inside a fix hunk**; a finding outside becomes a `late_finding`, ruled at block close (`review.late_findings: sweep`; `block` makes it block the round).
|
|
34
36
|
3. **Strictly shrinking**: the open `fix_now` set after round n is smaller than after round n−1, counting closures minus new in-hunk findings. Equal or larger ⇒ `review_stall` (rule 2b), a trigger at once.
|
|
35
37
|
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.
|
|
36
|
-
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;
|
|
37
|
-
5. **Cap — four rounds per file** (`review.max_rounds_per_file`, default 4, range 2–6). 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
|
|
38
|
+
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.
|
|
39
|
+
5. **Cap — four rounds per file** (`review.max_rounds_per_file`, default 4, range 2–6). 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):
|
|
40
|
+
- **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. It is human-only like `block waive`: on the coder forbidden list, run by the orchestrator only after the human says so. 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 from before B72 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 and the reopen is refused. 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}` row (`at_seq` is the file's latest `review.state` anchor seq; `from_hash` the content the stop reviewed, null for `lost`; `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`, 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`. The gate needs nothing new: an approval written after the `review.cap` row clears the cap. Refusals (exit 1, no row): `reopen-same-content` (a stopped file still at the content its stop reviewed, or a stop whose content is unknown: change the file, or waive its findings instead), `reopen-tampered` (an integrity failure stays a security event), `reopen-not-stopped` (no state, or an open or complete one), `reopen-block-not-open` (the block is stopped or closed), `reopen-not-owned`, `unknown_block` (the block lookup code `block waive` uses; it and `reopen-block-not-open` are checked before anything else is read), `reopen-config` (the run's config snapshot or `tmp.root` cannot be read), `reopen-already-recorded` (a row for this `at_seq` already names other content: run `review-file`, or the content changed since that reopen), `reopen-write-failed` (the row was not written). A missing `--file` or `--reason`, or a `--file` that is not a plain repo-root-relative path (no `..`, `./`, `//` or `/./`), is `usage` (exit 2).
|
|
41
|
+
- **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.
|
|
42
|
+
- **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 it, and the human waive exit clears it. A missing `--of`, no ids, more than 10 ids, an id given twice or a malformed id is `usage` (exit 2). The other refusals exit 1 and queue nothing. In the worker, an answering file that is gone or cannot be read is `answers-file-missing`.
|
|
43
|
+
- **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}]}`.
|
|
44
|
+
- **Never an approval.** The answering file still needs its own `review.approved`, and the source's fix-loop state is unchanged.
|
|
45
|
+
- **Exit 3 — `block waive`.** The human waives each open finding: `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` (a signed `review.waived` row, `by: human`). The coder never sees a `waive` verb; it is on the forbidden list. **Autopilot:** under a grant, a warning or nit finding may be waived by the delegate (`forge autopilot waive … --decision <id>`, written `by: autopilot`) and a file at the cap may get one extra round (`forge autopilot round`); a critical or proof waiver is never delegated (`autopilot.md`).
|
|
46
|
+
- **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.
|
|
47
|
+
- **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 `review.cap` rows is waived or answered. A critical waiver counts only `by: human`; an autopilot waiver counts only when `autopilotWaiverProblem` finds nothing, and only for a finding a signed refusal row at that hash records as warning or nit (or records with no severity at all). A cap id no refusal row records a severity for, or a cap row whose `open` is not a list of ids, is the human's alone. A signed, still-current answer clears its finding there, and in the late-finding and cap checks only at the severity the rows RECORD for that id (a late finding's own severity; a cap id's from the signed refusals at the current hash) — a missing or unknown one is never cleared by an answer. An answer counts only while BOTH the source and the answering file have the row's hashes (a change to either makes it stale), and it may clear a critical. Otherwise the file stays `unreviewed`. The close writes one signed `gate.review_waived_all {block, file, content_hash, findings, by}` row per such file just before `block.close`: `findings` are the sorted waived ids, `by: human` when every clearing waiver is the human's, else `autopilot`; `answered` lists the answered ids (only when there is one); with every id answered, `findings` is `[]` and `by` is null. When an id is both waived and answered, a human waiver is reported as the waiver; otherwise the answer is listed under `answered` instead of an autopilot waiver. The close line prints only the parts that apply: `block <id>: <file> reviewed_by_waiver (<ids>; by <human|autopilot>)`, `reviewed_by_waiver (W1; by human; answered C1)`, or `reviewed_by_waiver (answered C1, C2)` (ids sorted and joined by `, `). A refused close writes no such row, and a close where no file needs it writes none.
|
|
48
|
+
- **Not exits.** `block attempt` never resets review state: it only raises the attempt counter (a coder may run it), and a stopped file stays stopped, with no session. New content without a reopen row still gets the cached stop, with no session. `review-file --fresh` (§1) re-runs only a result that never counted as a review (`stale`, `unavailable`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); on a 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.
|
|
38
49
|
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.
|
|
39
50
|
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.
|
|
51
|
+
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).
|
|
52
|
+
- **Where it lives.** `<code-forge home>/runs/<run>/review-state/<block>/<sha24>.json` (default `~/.code-forge`; dir 0700, file 0600). It is not under the run's temp root, so a temp sweep or a removed run temp root does not touch it. Every save goes there.
|
|
53
|
+
- **Upgrade from the old place.** A state file from an older version may still be at the old temp-root path (`<run temp root>/review-state/<block>/`). It is read once. If it matches its anchor, it is moved to the new path (status `ok`, no `review.state_restored` row). If it does not, it is `tampered` and stays where it is.
|
|
54
|
+
- **Doctor row.** Full `forge doctor` (not `--quick`) prints one WARN `review state` row per active run with state files missing. A file counts as missing only when it is at neither the new path nor the old one (a file still at the old path is moved on its next load if it matches its anchor, and refused as `tampered` otherwise). The row counts the files the ledger can rebuild and the `lost` ones, and lists at most 10. When the run's `tmp.root` is not an absolute path, the row says the old temp location could not be checked and counts nothing. When an anchor of the run does not verify, the row says the review state was not checked.
|
|
55
|
+
- **Restore.** The anchor carries the slim signed state (`state_body`: the state without the reviewed text, plus that text's sha256). Over 64 KB it carries `state_omitted: "too_large"` instead. When the state file is gone (deleted, or a home directory restored without it), the next ticket rebuilds it from the latest anchor's `state_body`. It saves it as a new seq and writes one signed `review.state_restored {block, file, seq, from_seq}` row. The round counters, the open and late findings and a stop stay as they were; nothing restarts at round 1. The reviewed text itself is kept as a git blob in the reviewed tree's object store, and `state_body` names it (`reviewed_content_blob`). A restore reads that blob back and uses it only when its sha256 equals the reviewed text's sha, so the next round still reviews only what changed since the last review, even when the file changed meanwhile. When the blob is gone (git gc pruned it), does not match, its id is malformed, no content tree is known, or the anchor is older and has none, the current file is used if it still matches the sha; otherwise the next round reviews the full file instead of the fix hunks. A blob that cannot be written for any reason (for example the tree is not a git repository) leaves `reviewed_content_blob: null` and never fails the round.
|
|
56
|
+
- **Lost.** A missing file whose anchor has no `state_body` (an older run, or `state_omitted`) is `lost`. The ticket is refused with `review-state-lost` and no session runs. The refusal message names `forge block reopen`: the human's reopen (step 5, exit 1) restarts the file at round 1 on its current content.
|
|
57
|
+
- **Tampered.** A present file that does not match its anchor (changed bytes, an older signed copy, or no anchor at all) is `tampered` (`review-state-tampered`) and is never overwritten. A missing file is `tampered` too, not restored, when its latest anchor's `state_body` fails its signature or its run/block/file/seq check, or its `state.file` is not the file. Any `review.state` anchor of the block that fails verification makes the file `tampered`.
|
|
40
58
|
|
|
41
59
|
Rechecks are never degraded by the budget: a round-2+ packet is already the minimum.
|
|
42
60
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## §2 The forbidden list — one list, rendered per CLI at spawn, grepped from every transcript
|
|
8
8
|
|
|
9
|
-
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload` and `block
|
|
9
|
+
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload`, `block waive` and `block reopen` from a coder (also via `npx`) · `--remove-worktree` from a coder (B62a) · any write under `.code-forge/reviews/` or `.code-forge/queue/*.done`.
|
|
10
10
|
|
|
11
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 is a `rule_break` row that fails the gate. Where a CLI cannot load a rules file, `doctor` says so and the transcript grep remains the detector.
|
|
12
12
|
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.
|
|
13
|
+
PINNED_VERSION="0.9.0"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|