@codedology/code-forge 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +1 -1
  3. package/docs/reference/blocks.json +9 -1
  4. package/docs/reference/config.md +11 -1
  5. package/native/README.md +10 -0
  6. package/native/bin/darwin-arm64/code-forge-native +0 -0
  7. package/native/bin/darwin-x64/code-forge-native +0 -0
  8. package/native/bin/linux-arm64/code-forge-native +0 -0
  9. package/native/bin/linux-x64/code-forge-native +0 -0
  10. package/package.json +7 -2
  11. package/schema/code-forge.schema.json +26 -1
  12. package/schema/facts.schema.json +1 -1
  13. package/skill/SKILL.md +1 -1
  14. package/skill/references/adapters/solo.md +1 -1
  15. package/skill/references/autopilot.md +6 -1
  16. package/skill/references/code.md +37 -0
  17. package/skill/references/decisions.md +1 -1
  18. package/skill/references/plan.md +18 -5
  19. package/skill/references/review.md +47 -9
  20. package/skill/scripts/forge +1 -1
  21. package/skill/templates/facts-sheet.md +9 -3
  22. package/src/cli/block.mjs +284 -11
  23. package/src/cli/doctor.mjs +32 -1
  24. package/src/cli/help.mjs +1 -0
  25. package/src/cli/native-status.mjs +13 -0
  26. package/src/cli/review-file.mjs +126 -14
  27. package/src/cli/spawn.mjs +26 -22
  28. package/src/cli/status.mjs +80 -14
  29. package/src/decide/escalation.mjs +5 -5
  30. package/src/doctor/index.mjs +11 -0
  31. package/src/doctor/worker-probe.mjs +1 -1
  32. package/src/gates/dependents.mjs +200 -0
  33. package/src/gates/expand.mjs +83 -26
  34. package/src/gates/run.mjs +76 -14
  35. package/src/ledger/tail.mjs +89 -0
  36. package/src/review/agent-waiver.mjs +95 -8
  37. package/src/review/definitions.mjs +170 -0
  38. package/src/review/fixloop.mjs +4 -4
  39. package/src/review/gate-check.mjs +33 -13
  40. package/src/review/model-stats.mjs +125 -0
  41. package/src/review/packet.mjs +111 -22
  42. package/src/review/reopen.mjs +38 -13
  43. package/src/review/route.mjs +58 -4
  44. package/src/session/facts.mjs +252 -70
  45. package/src/session/plan-check.mjs +4 -1
  46. package/src/session/spawn.mjs +70 -19
  47. package/src/state/block-end.mjs +3 -1
  48. package/src/state/block.mjs +56 -21
  49. package/src/state/run.mjs +12 -5
  50. package/src/util/native.mjs +133 -0
  51. package/src/worker/engine.mjs +11 -26
  52. package/src/worker/loop.mjs +150 -20
  53. package/src/worker/queue.mjs +3 -2
  54. package/src/worker/review-state.mjs +15 -3
  55. package/src/worker/watch.mjs +97 -83
  56. package/src/worker/worker-warning.mjs +35 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,57 @@ waits on a second real consumer (plan §10.4, Q17).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.16.0] — 2026-11-01
10
+
11
+ ### Added
12
+
13
+ - `forge spawn --print-argv` prints the exact command it would run and runs nothing; the Solo adapter guide step 2 now uses it (B229)
14
+ - `npm run bench:block` measures one block flow under Node and under the Go daemon (process count, peak memory, wall time); each pass runs in its own child process (B184)
15
+ - Forge facts checks the code names in a brief, marks ignored or outside-snapshot paths UNVERIFIABLE (never NOT-FOUND), takes claims from the brief only, maps absolute paths inside any `--sources` root to that source, and handles globs, `/dev/null` and route patterns (B227)
16
+
17
+ ### Changed
18
+
19
+ - Reviewers get the bodies of the functions the changed code calls, inside the packet budget, so they stop raising false findings about code they could not see (B212)
20
+ - A second stalled file now gets its own L3 patch rung instead of stopping the whole block with `l3_patch_exhausted` (B213)
21
+ - The worker prunes done queue tickets and keeps an index; worker polls and done checks read only new ledger rows instead of the whole ledger each time (B214)
22
+
23
+ ### Fixed
24
+
25
+ - Worker start times use one format everywhere: `ps` pads days 1-9 with two spaces, so worker pins and route checks failed on those days. Old padded pins still match (B225)
26
+ - `gates run --changed` also runs the tests of files that import a changed file, so a change cannot break callers in other folders unseen (B221)
27
+ - Daemon tests wait for readiness against a deadline instead of fixed sleeps, so they do not fail on a busy machine (B226)
28
+ - `gates run --changed` gives each test runner only its own test files (no more `.go` files passed to `node --test`); when none fit, the gate runs the full suite and reports scope `full` (B203)
29
+ - A gate that hits its time limit now says `TIMEOUT after <n> s (not a test failure)` with the flag to raise it, and test files left unfinished by the timeout are no longer listed as failed tests (B223)
30
+ - `wait-block` no longer reports done while a file's last review was unavailable; it re-queues the file or names it, so `block close` does not refuse it later as unreviewed (B217)
31
+ - `block close` no longer refuses a file that the block's own commits created and then deleted; a missing file with no such history still needs review (B230)
32
+ - `status` warns when a live worker of the run is not the pinned one and names `run start --reattach`, before `block close` can stop blocks it served (B224)
33
+ - CI is green again on Linux: `block rebase` works when git has no user name or email and keeps each commit's committer, and overlapping `watch` ticks for one block no longer lose state (B231)
34
+
35
+ ## [0.15.4] — 2026-10-31
36
+
37
+ ### Added
38
+
39
+ - 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)
40
+ - `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)
41
+ - 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)
42
+ - `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)
43
+ - 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)
44
+ - `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)
45
+ - 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)
46
+ - 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)
47
+ - 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)
48
+ - `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)
49
+ - `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)
50
+ - 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)
51
+ - 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)
52
+ - 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)
53
+ - The native daemon status helper now lives with the other command files, so the import-order rule passes again. (B218)
54
+ - 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)
55
+ - 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)
56
+ - Two block tests now give the commit watch its second quiet tick, so they pass with the B211 debounce. (B219)
57
+ - 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)
58
+ - 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)
59
+
9
60
  ## [0.15.3] — 2026-10-30
10
61
 
11
62
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A CLI and Agent Skill for cross-model, parallel, evidence-gated feature delivery.
4
4
 
5
- Latest: 0.15.3 - see [CHANGELOG.md](CHANGELOG.md)
5
+ Latest: 0.16.0 - see [CHANGELOG.md](CHANGELOG.md)
6
6
 
7
7
  ## The problem
8
8
 
@@ -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": 164,
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
  }
@@ -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 | | `3` | 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. |
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> | | — | |
@@ -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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codedology/code-forge",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "description": "Plan/harden/code/review orchestration CLI and Agent Skill for cross-model, parallel, evidence-gated feature delivery.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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,9 @@
33
34
  "!scripts/changelog.mjs",
34
35
  "!scripts/known-fix.mjs",
35
36
  "!scripts/bench-review.mjs",
37
+ "!scripts/bench-block.mjs",
38
+ "!scripts/build-native.mjs",
39
+ "!scripts/compat-golden.mjs",
36
40
  "README.md",
37
41
  "CHANGELOG.md",
38
42
  "LICENSE"
@@ -43,7 +47,8 @@
43
47
  "release": "node scripts/release.mjs",
44
48
  "changelog": "node scripts/changelog.mjs",
45
49
  "known-fix": "node scripts/known-fix.mjs",
46
- "bench:review": "node scripts/bench-review.mjs"
50
+ "bench:review": "node scripts/bench-review.mjs",
51
+ "bench:block": "node scripts/bench-block.mjs"
47
52
  },
48
53
  "dependencies": {
49
54
  "@clack/prompts": "1.8.1",
@@ -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": 3, "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." },
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,
@@ -12,7 +12,7 @@
12
12
  "properties": {
13
13
  "fact_id": { "type": "string", "pattern": "^F[0-9]+$" },
14
14
  "claim": { "type": "string", "minLength": 1 },
15
- "kind": { "type": "string", "enum": ["flag", "command", "path", "version", "endpoint", "package", "env"] },
15
+ "kind": { "type": "string", "enum": ["flag", "command", "path", "version", "endpoint", "package", "env", "symbol"] },
16
16
  "command": { "type": "string" },
17
17
  "output_excerpt": { "type": "string", "maxLength": 200 },
18
18
  "tag": { "type": "string", "enum": ["VERIFIED", "NOT-FOUND", "UNVERIFIABLE"] },
package/skill/SKILL.md CHANGED
@@ -32,7 +32,7 @@ State lives in the plan file's header — `facts → planned → hardened → co
32
32
  | coder | the lane S1 picks (L0–L2); climbs one rung per escalation | the project tree | the engine adapter |
33
33
  | reviewer, recheck | L2 | closed-book, empty cwd, packet on stdin | the worker |
34
34
  | judge, S2, author | L3 | closed-book, one turn (never Codex: `references/security.md` §4) | the worker, `forge s2`, `forge author` |
35
- | L3 rung | L3 | a patch ≤ 80 lines, once per block | `forge spawn --level L3 --role coder` |
35
+ | L3 rung | L3 | a patch ≤ 80 lines, once per file | `forge spawn --level L3 --role coder` |
36
36
 
37
37
  `forge resolve L<n>` prints what a level resolves to (provider, model, effort, fallback); `forge models` lists the catalog. With `review.multimodel` on, the second reviewer is another provider's L2 and the judge comes from a third provider.
38
38
 
@@ -16,7 +16,7 @@ Solo adds spawn, watch, timers, scratchpads, locks and `kv` on top of the CLI; i
16
16
  ## §3 Spawn protocol per block
17
17
 
18
18
  1. Pre-flight: `list_processes` and close **only** agents this run spawned (own pids only); `list_agent_tools` to resolve runtime ids live, never from memory. Never more than `caps.coders` coder processes alive; a further spawn waits for a completion wake, never for a timer.
19
- 2. `forge block open <id> --run <r> …` (`code.md` §3), then `spawn_agent(agent_tool_id=<coder runtime>, name="coder-<id>", extra_args=<the argv the level resolves to>)` — the model, effort and permission mode are pinned **at spawn** from `forge resolve L<n>` and the provider's coder flags (`forge spawn --level L<n> --role coder --brief <file>` prints the exact argv without running it when you need to see it); never a post-boot model switch. Coders run in the provider's non-interactive auto mode: a session parked at a permission prompt is the first failure mode of every run.
19
+ 2. `forge block open <id> --run <r> …` (`code.md` §3), then `spawn_agent(agent_tool_id=<coder runtime>, name="coder-<id>", extra_args=<the argv the level resolves to>)` — the model, effort and permission mode are pinned **at spawn** from `forge resolve L<n>` and the provider's coder flags (`forge spawn --level L<n> --role coder --brief <file> --print-argv` prints the exact argv and runs nothing when you need to see it); never a post-boot model switch. Coders run in the provider's non-interactive auto mode: a session parked at a permission prompt is the first failure mode of every run.
20
20
  3. First `send_input` after the `❯` prompt: the agent's instructions, then the pointer (§2).
21
21
  4. **Launch-verification gate** — the one thing you babysit: poll `get_process_output` / `search_output` until the coder shows the ACK and tool activity. Dead-idle after the pointer (no prompt on screen) ⇒ one Enter (`send_input bytes=[13]`) and re-check. A **permission or model-picker prompt** is never answered with Enter — it would accept an unreviewed default: `close_process`, respawn with the correct `extra_args`, and record a `spawn-flag defect` line in the block record (the flags came from `forge resolve`; a prompt means they were not pinned).
22
22
  5. **Wake channels, event-driven, never fixed polls:** record your own pid once (`whoami` → `kv_set("forge:orchestrator_pid")`); coders that can call Solo carry the push contract (post-sentinel `kv_set("block:<id>", …)` + `timer_set(1s, body, delivery_process_id=<your pid>)`); for every group arm `timer_fire_when_idle_any([coders], max_wait≈20–25 min, delivery_process_id=<your pid>)` right after the launch gate (idle-any ignores already-idle processes, so arm while they are provably running); keep one long dead-man fallback per 30–40 min with an L0 triage first. On wake: sentinel ⇒ gate; no sentinel and active again ⇒ re-arm; no sentinel and idle ⇒ parked, intervene now.
@@ -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).
@@ -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.
@@ -36,7 +36,7 @@ No key ⇒ a banner at run start; 401 ⇒ stop; 422 ⇒ stop with the request id
36
36
  3a. **Heavy rounds:** below L2, `escalation.after_rounds_with_warnings` (default 1) rounds at the current level whose open `fix_now` set holds ≥ `escalation.warning_threshold` (default 2) warnings or any `critical` ⇒ +1 level at once (`review_warnings`). A cheap coder that fails a round with several warnings climbs; it never spends three rounds at the same level. At L2 it does not fire — the L3 rung keeps its own triggers (rules 2, 3, 6). `after_rounds_with_warnings: 0` turns it off.
37
37
  4. **Security floor, at dispatch:** `security_sensitive` ⇒ first-attempt level `min(max(lane, L1) + 1, L2)` (`security`). Then the **docs floor**: a `docs` or `contract` block (below) is never dispatched under `levels.coder_floor_docs` (default L1) — `forge block open` raises the level and the `dispatch` row carries `requested_level` and `trigger: docs_floor`. L0 never codes docs or contracts.
38
38
  5. **S2 ruling:** S1 `next = escalate` is never acted on directly — it goes to S2 as a check (`s2_ruling`); S1 `next = stop` ⇒ the human.
39
- 6. **Review cap:** `review.max_rounds_per_file` reached on a file with an open `fix_now` ⇒ the L3 rung for that file, once per block, always as a patch whatever `escalation.l3_mode` says (`review_cap`); checked last, only when no rule above returned an action this attempt.
39
+ 6. **Review cap:** `review.max_rounds_per_file` reached on a file with an open `fix_now` ⇒ the L3 rung for that file, once per file, always as a patch whatever `escalation.l3_mode` says (`review_cap`); checked last, only when no rule above returned an action this attempt.
40
40
 
41
41
  **Block kind.** `forge block open --kind code|docs|contract` declares it; without the flag it is detected from the owned paths: every path ends in `.md .mdx .markdown .rst .adoc` ⇒ `docs` (`.txt` is not docs: `requirements.txt`); every path is docs or a contract (`*.schema.json|yaml|yml`, `.proto`, `.graphql`, `.gql`, `.avsc`, `*.openapi.json|yaml|yml`, or a `.json/.yaml/.yml` under a `schema/`, `schemas/`, `contract/` or `contracts/` directory) and one is a contract ⇒ `contract`; anything else ⇒ `code`. A glob is judged by its literal ending, so a block owning `docs/**` declares its kind. The kind is recorded in the run record and drives the docs floor (rule 4) and the review topology (`review.md` §2).
42
42
 
@@ -2,12 +2,25 @@
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. What it does:
5
+ Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else.
6
6
 
7
- 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
- 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.
9
- **Path claims:** `./path` claims resolve against the HEAD snapshot, so a committed file is VERIFIED or NOT-FOUND by a plain `ls`/`test -e`; a file that exists only uncommitted in the working tree reads NOT-FOUND unless you passed it with `--sources`. `~/…` claims stay UNVERIFIABLE (`~` is refused), and absolute paths are allowed only under `/usr`, `/opt/homebrew` and `/bin`; a home-directory path that matters to a block is checked by you with one read-only command and recorded in the unbackable list with that command as the tolerance.
10
- 3. Deterministic again: the answer is validated against the facts schema, rendered with `templates/facts-sheet.md`, and refused when a VERIFIED tag has no output excerpt. Every claim, command and excerpt passes through the redactor. The header records the brief's sha and mtime.
7
+ Claims come from the brief only. `--sources <path>` chooses where checks run. Source text adds no claims.
8
+
9
+ What it does:
10
+
11
+ 1. The CLI extracts claim tokens from the brief: flags, commands, paths, versions, endpoints, packages and environment names. Whole code spans also name `symbol` claims: PascalCase types with two or more humps, snake_case identifiers and code file names such as `SigmaClient.php`. A prose word such as Model adds no symbol. Command prefixes still come from the brief and the project's `package.json` bin names. For example, `run reload` becomes `code-forge run reload` when the brief names that project CLI. A known CLI or a CLI named separately in the brief keeps its own name.
12
+ 2. One L0 delegate checks a read-only snapshot. The project uses its git HEAD tree plus sources inside it. Each outside source directory appears under `_sources/<label>/`. Its label is its folder name; repeated names get `-2`, `-3`. Git sources use HEAD, limited to the named subfolder. Non-git sources use a copy. An outside source file appears at `_sources/<label>/<file name>`. Symlinks and secret files are dropped. Files and directories are sealed. The snapshot is removed when the session ends.
13
+
14
+ Absolute paths inside a root map to its snapshot path. Relative paths get one row per root when several roots are present. Flags and symbols also get a row per root. These rows have a `source` label. The sheet adds a `source` column after `kind` when any row has that label. A single-root relative claim keeps the old row format. Routes such as `/api/v1/*`, HTML fragments such as `/h1`, and `/dev/null` are not claims. An absolute path outside every source root and the tool roots is UNVERIFIABLE when its first directory exists on this machine.
15
+
16
+ Before the packet is sent, one `git check-ignore` batch checks each root's paths. Ignored paths are UNVERIFIABLE: `git-ignored in <label>: the snapshot leaves ignored paths out`. A path present on disk but absent from the snapshot is UNVERIFIABLE: `not at HEAD in <label>: outside the snapshot`. Neither goes to the delegate. Sources inside the project may still supply untracked files to the snapshot.
17
+
18
+ The delegate runs one read-only command per row and quotes its literal output. A flag needs `grep -rn -- <flag> <source folder>`. An identifier symbol needs `grep -rn -- 'TenantSubscription' app` or `grep -rn -- 'class TenantSubscription' app`. The quoted pattern is the token alone or one of `class interface trait enum struct type function func def` followed by the token. VERIFIED needs a `file:line:text` source-file hit holding the whole token. Markdown, text, docs, plans, tests, fixtures and the brief do not count. Symbols search one whole top-level source folder. A flag may use a nested source folder for VERIFIED; NOT-FOUND needs a whole top-level folder. Top-level means directly below a root: `src` or `_sources/<label>/app`. `_sources` itself never counts.
19
+
20
+ A file-name symbol needs `find <root> -name SigmaClient.php`, where the root is `.` or `_sources/<label>`. VERIFIED needs an output path ending in `/SigmaClient.php`. A glob path needs `find lang/es -name '*.php'`. Its pattern is quoted. Only that claim may also use `ls -d lang/es/*.php`, quoted or unquoted. VERIFIED needs a matching output path. NOT-FOUND needs `No such file` output. Unquoted globs on other claims are refused.
21
+
22
+ The session gets the shell tool with write deny rules and provider restrictions. Node validates the reported command afterward. Allowed reads include `ls cat head tail wc grep stat file uname sw_vers jq which`, `command -v`, the restricted `find` predicates, `git log|show|status|rev-parse|ls-files|cat-file`, `npm view|ls`, `test -e|-f|-d|-n`, and a single-URL HEAD `curl` request. No pipes or command lists. No program runs with `--help`, `-h` or `--version`. Versions use `npm view`, `which` or `ls`. Environment claims use only `printenv NAME >/dev/null`; their excerpts are blanked. Backticks, `$`, other redirections, `~`, `..`, secret paths and absolute paths outside `/usr`, `/opt/homebrew`, `/bin` are refused. Root paths in the brief use their mapped snapshot paths for checks. `npm view` and `curl -I` may reach the network.
23
+ 3. Node validates the closed delegate schema. A bad proof or an empty VERIFIED excerpt downgrades that row to UNVERIFIABLE with a reason. It does not count. Source labels are added by Node. Every claim, command, excerpt and reason passes through the redactor. The rendered sheet records the brief's sha and mtime.
11
24
 
12
25
  **Do not read the sources yourself; read the sheet.** Your context is spent on design, never on `--help` output. A fact is never a model's opinion: every claim carries the command that produced it.
13
26