@codedology/code-forge 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +21 -2
- package/docs/reference/blocks.json +203 -1
- package/package.json +2 -2
- package/skill/SKILL.md +1 -1
- package/skill/references/code.md +6 -4
- package/skill/references/review.md +28 -3
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +4 -6
- package/src/cli/block.mjs +98 -7
- package/src/cli/review-file.mjs +17 -11
- package/src/review/engine.mjs +59 -12
- package/src/review/fixloop.mjs +30 -10
- package/src/review/lenses/A.md +1 -0
- package/src/review/lenses/B.md +1 -0
- package/src/review/lenses/full.md +1 -0
- package/src/review/lenses/recheck.md +1 -0
- package/src/review/packet.mjs +48 -14
- package/src/review/paired.mjs +80 -0
- package/src/review/reviewed-lines.mjs +94 -0
- package/src/state/block-end.mjs +10 -6
- package/src/worker/engine.mjs +43 -15
- package/src/worker/loop.mjs +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.0] — 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `npm run release` moves the README "In the next release:" bullets under the new version, refuses an empty [Unreleased]
|
|
14
|
+
or a missing README anchor in preflight, restores README on a failed check, and runs `gen-config-doc --check`;
|
|
15
|
+
`docs/releasing.md` gets a releasing checklist (B108).
|
|
16
|
+
- `pairsFor` and `summarize` (src/review/paired.mjs) and an optional `## paired` packet section after `## context`,
|
|
17
|
+
dropped first over budget; `attachFiles` attaches an untracked file only when it is an owned, changed file of the
|
|
18
|
+
block and not ignored or secret-like (B97).
|
|
19
|
+
- The review engine, the fix-loop recheck and the worker send `## paired` (the block's owned changed peers and
|
|
20
|
+
`test_files`); the needs_file path passes the block's owned and changed files so a new untracked owned test file can
|
|
21
|
+
be attached; the lenses say a test named in `## paired` exists (B98).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `block close` prints the `reviewed <path> ticket <id>` lines itself, for every file the review gate accepts; `--json`
|
|
26
|
+
and the done file carry `reviewed: [{file, ticket, via}]`; the coder report no longer needs hand ticket lines, and a
|
|
27
|
+
mismatched hand line under `--report` is a WARN, not a refusal (B96, #16).
|
|
28
|
+
- A quick review packet over `quick_in` steps up to one full L2 session when it fits `full_in`, before `split_required`;
|
|
29
|
+
the `review.plan` row carries `step_up: {from, tokens_in, budget}` (B109, #16).
|
|
30
|
+
- A cached round-1 `split_required` is re-queued by `review-file` (`retry: true`) and by `review-file --fresh`, with the
|
|
31
|
+
same ticket id; a recheck `split_required` stop still needs `block reopen` or `block waive` (B110, #16).
|
|
32
|
+
- A recheck with no fix hunks counts no round and returns `reviewed`, `approved: false`, `reason: 'no-fix-hunks'`; a
|
|
33
|
+
recheck answer that lacks a `resolved` entry for an open finding of its packet is a `schema` miss; the worker's
|
|
34
|
+
recheck summary adds `status`, `tokens_in`, `tokens_out` and `reason` (B111, #16).
|
|
35
|
+
- A review ticket reads the diffs of the block's other changed files only when the review asks for them, once per ticket
|
|
36
|
+
(B112)
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- A worker stops by itself, without taking a new ticket, when its run record or its HOME is removed (B113)
|
|
41
|
+
- Tests stop every worker they start, also when a test fails or times out; the final no-leak step of `npm test` fails
|
|
42
|
+
when a worker from this suite is still alive (B113)
|
|
43
|
+
|
|
9
44
|
## [0.10.0] — 2026-10-13
|
|
10
45
|
|
|
11
46
|
### Added
|
package/README.md
CHANGED
|
@@ -8,10 +8,26 @@ for cross-model, parallel, evidence-gated feature delivery — on Claude Code, C
|
|
|
8
8
|
> `examples/` have all landed (`docs/reference/blocks.json` lists every build block). What changed
|
|
9
9
|
> in each version is in [CHANGELOG.md](CHANGELOG.md).
|
|
10
10
|
|
|
11
|
-
## What's new in 0.
|
|
11
|
+
## What's new in 0.11
|
|
12
12
|
|
|
13
13
|
In the next release:
|
|
14
14
|
|
|
15
|
+
In 0.11.0:
|
|
16
|
+
|
|
17
|
+
- **The release moves the README What's new list (B108).** `npm run release` now moves the "In the next release:" bullets under "In X.Y.Z:". A minor or major release starts a new "What's new in X.Y" heading. The empty anchor stays for the next block. The release refuses an empty CHANGELOG [Unreleased] or a missing anchor before it changes anything, and a failed check puts the README back with the other files. The checks now include `gen-config-doc --check`. `docs/releasing.md` has a releasing checklist.
|
|
18
|
+
- **The close writes the ticket lines (B96).** The coder's final report no longer needs a `reviewed <path> ticket <id>` line per file. `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. `--json` and the block's done file carry them as `reviewed: [{file, ticket, via}]`. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr, and the close goes on.
|
|
19
|
+
- **A big-diff file steps up to a full review (B109).** A low-risk file whose diff is too big for the quick review budget used to stop with `split_required`, and splitting it only made the diff bigger. Now the review plans again as one full L2 session. If the diff fits the full budget, the file is reviewed. The `review.plan` row records the step as `step_up`.
|
|
20
|
+
- **A file that was too big can be reviewed again (B110).** A first-round `split_required` result ran no review. After you raise the budget and run `run reload`, `review-file` (or `review-file --fresh`) tries again with the same ticket. A recheck stop still needs `block reopen` or `block waive`.
|
|
21
|
+
- **A recheck that did not really review never stalls (B111).** A recheck on unchanged content runs no session and counts no round; it returns `reason: 'no-fix-hunks'`. A recheck answer that skips an open finding is a broken answer, not a "still open". So the stall rule sees only real rechecks. The recheck summary now shows each session's status and tokens.
|
|
22
|
+
- **Paired files for the reviewer, part 1 (B97).** code-forge can now find a changed file's test file (or a test's source file) by name, and list its test names or exported symbols in a small `## paired` packet section. The section is dropped first when the packet is too big. A reviewer can also ask for a new, untracked test file when the block owns and changed it. B98 sends these to the reviewers.
|
|
23
|
+
- **Reviewers see the paired test file (B98).** When a block changes `src/a.mjs` and `test/a.test.mjs`, the reviewer of `src/a.mjs` now gets the test names in a `## paired` section, in the first review and in rechecks. Tests named with `block open --tests` count too. The reviewer is told those tests exist, so it no longer reports them as missing. It can still ask for a test file's body, even a new untracked one the block owns.
|
|
24
|
+
- **Faster reviews (B112).** A review ticket reads the diffs of the block's other changed files only when the review asks for them, and only once.
|
|
25
|
+
- **No leftover workers (B113).** A worker stops by itself when its run record or its HOME is removed. `npm test` now fails when a test leaves a worker running.
|
|
26
|
+
|
|
27
|
+
## What's new in 0.10
|
|
28
|
+
|
|
29
|
+
In 0.10.0:
|
|
30
|
+
|
|
15
31
|
- **Choose how long a 1Password key stays cached (B84).** Set `keys.cache_hours` (1 to 24, default 8) in `.code-forge.yml`, or `keys set <name> --op <ref> --cache-hours <n>` for one key. A lower value shortens keys that are already cached at the next read.
|
|
16
32
|
- **Warnings before a cached key runs out (B85).** `keys list` and `doctor` warn when a 1Password key has less than 1 hour left. When the cached key expired and 1Password does not answer, `jev ask` prints the exact command to re-link it: `code-forge keys set jev --op <ref>`.
|
|
17
33
|
- **A test report reader for red-green proof (B86).** code-forge can now read a JUnit XML test report (XCTest, Swift Testing, PHPUnit, Pest, pytest). A missing, broken or empty report, or an error that is not a failed assertion, never counts as red. B87 connects it to `proof red-green`.
|
|
@@ -27,6 +43,8 @@ In the next release:
|
|
|
27
43
|
- **One fix round per block (B94).** The skill now waits for every review and test of a block. Then it sends the coder ONE fix message with every failing test and every open finding. It never sends findings one at a time.
|
|
28
44
|
- **Waive and reopen ask you at a terminal (B80).** `block waive` and `block reopen` now ask the human for a yes in their own terminal. There is no `--yes`. With no terminal they record nothing. The prompt is a speed bump, not proof of a human: a pseudo-terminal passes it.
|
|
29
45
|
|
|
46
|
+
## What's new in 0.9
|
|
47
|
+
|
|
30
48
|
In 0.9.0:
|
|
31
49
|
|
|
32
50
|
- **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).
|
|
@@ -411,5 +429,6 @@ depends on, kept in sync with the maintainer's design plan.
|
|
|
411
429
|
|
|
412
430
|
Releasing: record changes with `npm run changelog -- <type> "<text>"` and fixed error reports with
|
|
413
431
|
`npm run known-fix -- add <fp> <version> "<summary>"`, then `npm run release -- <patch|minor|major|x.y.z> [--dry-run]`
|
|
414
|
-
bumps,
|
|
432
|
+
bumps, moves the README's "In the next release:" bullets under the new version, checks the config
|
|
433
|
+
doc, commits and tags (it never pushes or publishes). Publish with `npm publish` from your
|
|
415
434
|
machine, then push the tag: CI only creates the GitHub release. See [docs/releasing.md](docs/releasing.md).
|
|
@@ -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": 111,
|
|
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": {
|
|
@@ -1623,6 +1623,208 @@
|
|
|
1623
1623
|
"B94"
|
|
1624
1624
|
],
|
|
1625
1625
|
"note": "root-added 2026-10-13; issue #10 owner item 1 (plans/iteration/plan.md B80, D1 = a); block waive and block reopen ask the human for a yes at a terminal, no --yes, no terminal exits 2 and records nothing; pre-checks run before the question; a pseudo-terminal passes the check (honest limit in security.md §3); D1 (b) is a later option"
|
|
1626
|
+
},
|
|
1627
|
+
"B108": {
|
|
1628
|
+
"title": "Release moves the README What's new bullets and checks the docs",
|
|
1629
|
+
"wave": 14,
|
|
1630
|
+
"level": "L2",
|
|
1631
|
+
"status": "landed",
|
|
1632
|
+
"owned": [
|
|
1633
|
+
"README.md",
|
|
1634
|
+
"docs/releasing.md",
|
|
1635
|
+
"scripts/release.mjs",
|
|
1636
|
+
"test/release-readme.test.mjs",
|
|
1637
|
+
"test/release.test.mjs"
|
|
1638
|
+
],
|
|
1639
|
+
"depends_on": [
|
|
1640
|
+
"B80"
|
|
1641
|
+
],
|
|
1642
|
+
"note": "root-added 2026-10-13; process fix after the 0.10.0 README miss (plans/field-bugs/plan.md B108); npm run release moves the 'In the next release:' bullets under 'In X.Y.Z:' (new '## What's new in X.Y' on a minor or major), keeps an empty anchor, refuses an empty [Unreleased] or a missing anchor in preflight, rolls README back with the other files, and runs gen-config-doc --check"
|
|
1643
|
+
},
|
|
1644
|
+
"B96": {
|
|
1645
|
+
"title": "The close writes the reviewed ticket lines",
|
|
1646
|
+
"wave": 14,
|
|
1647
|
+
"level": "L2",
|
|
1648
|
+
"status": "landed",
|
|
1649
|
+
"owned": [
|
|
1650
|
+
"docs/concepts.md",
|
|
1651
|
+
"docs/how-it-works.md",
|
|
1652
|
+
"docs/tutorial-first-test.md",
|
|
1653
|
+
"skill/SKILL.md",
|
|
1654
|
+
"skill/references/code.md",
|
|
1655
|
+
"skill/templates/coder-brief.md",
|
|
1656
|
+
"src/cli/block.mjs",
|
|
1657
|
+
"src/review/reviewed-lines.mjs",
|
|
1658
|
+
"src/state/block-end.mjs",
|
|
1659
|
+
"test/cli/close-report.test.mjs",
|
|
1660
|
+
"test/cli/status.test.mjs",
|
|
1661
|
+
"test/engines/sentinel-tests.test.mjs",
|
|
1662
|
+
"test/integration/review-flow.test.mjs",
|
|
1663
|
+
"test/review/answers.test.mjs",
|
|
1664
|
+
"test/review/close-gate.test.mjs",
|
|
1665
|
+
"test/review/external.test.mjs",
|
|
1666
|
+
"test/review/gate-check.test.mjs",
|
|
1667
|
+
"test/review/reopen.test.mjs",
|
|
1668
|
+
"test/review/waived-all.test.mjs",
|
|
1669
|
+
"test/session/plan-sections.test.mjs",
|
|
1670
|
+
"test/skill/structure.test.mjs",
|
|
1671
|
+
"test/state/block.test.mjs",
|
|
1672
|
+
"test/worker/deletion-review.test.mjs"
|
|
1673
|
+
],
|
|
1674
|
+
"depends_on": [
|
|
1675
|
+
"B108"
|
|
1676
|
+
],
|
|
1677
|
+
"note": "root-added 2026-10-13; issue #16 item 6 (Group E); block close prints `reviewed <path> ticket <id>` for every file the review gate accepts, --json and the done file carry reviewed [{file, ticket, via}]; the coder report needs no hand ticket lines; a mismatched hand line under --report prints a held WARN; no-ticket, bad-ticket and lines-unavailable WARNs are held and print only on a successful close"
|
|
1678
|
+
},
|
|
1679
|
+
"B109": {
|
|
1680
|
+
"title": "Step up from quick to one full review before split_required",
|
|
1681
|
+
"wave": 14,
|
|
1682
|
+
"level": "L2",
|
|
1683
|
+
"status": "landed",
|
|
1684
|
+
"owned": [
|
|
1685
|
+
"docs/how-it-works.md",
|
|
1686
|
+
"skill/references/review.md",
|
|
1687
|
+
"src/review/engine.mjs",
|
|
1688
|
+
"test/review/budget-step-up.test.mjs",
|
|
1689
|
+
"test/review/test-quality.test.mjs"
|
|
1690
|
+
],
|
|
1691
|
+
"depends_on": [
|
|
1692
|
+
"B96"
|
|
1693
|
+
],
|
|
1694
|
+
"note": "root-added 2026-10-13; issue #16 item 1 (plans/field-bugs/plan.md B109, D1 = a); a non-Markdown quick packet over quick_in is planned again at risk 1; if that plan is one full session and fits full_in it reviews with it (review.plan step_up {from: 'quick', tokens_in, budget}); otherwise split_required with the full packet's size; first block built by Codex at high effort"
|
|
1695
|
+
},
|
|
1696
|
+
"B110": {
|
|
1697
|
+
"title": "Re-run a cached round-1 split_required",
|
|
1698
|
+
"wave": 14,
|
|
1699
|
+
"level": "L2",
|
|
1700
|
+
"status": "landed",
|
|
1701
|
+
"owned": [
|
|
1702
|
+
"docs/how-it-works.md",
|
|
1703
|
+
"skill/references/code.md",
|
|
1704
|
+
"skill/references/review.md",
|
|
1705
|
+
"src/cli/review-file.mjs",
|
|
1706
|
+
"test/skill/structure.test.mjs",
|
|
1707
|
+
"test/worker/fresh-split.test.mjs",
|
|
1708
|
+
"test/worker/fresh.test.mjs"
|
|
1709
|
+
],
|
|
1710
|
+
"depends_on": [
|
|
1711
|
+
"B109"
|
|
1712
|
+
],
|
|
1713
|
+
"note": "root-added 2026-10-13; issue #16 item 2 (plans/field-bugs/plan.md B110); a cached round-1 split_required ran no session and counts no round: plain review-file re-queues it with retry: true and --fresh re-queues it, same ticket id; a recheck stop (status stopped, stopped split_required) still needs block reopen or waive; built by Codex at high effort"
|
|
1714
|
+
},
|
|
1715
|
+
"B111": {
|
|
1716
|
+
"title": "An unchanged or incomplete recheck never counts or stalls",
|
|
1717
|
+
"wave": 14,
|
|
1718
|
+
"level": "L2",
|
|
1719
|
+
"status": "landed",
|
|
1720
|
+
"owned": [
|
|
1721
|
+
"docs/how-it-works.md",
|
|
1722
|
+
"skill/references/review.md",
|
|
1723
|
+
"src/review/fixloop.mjs",
|
|
1724
|
+
"src/worker/engine.mjs",
|
|
1725
|
+
"test/review/recheck-real.test.mjs",
|
|
1726
|
+
"test/autopilot/actions.test.mjs",
|
|
1727
|
+
"test/cli/block-human-confirm.test.mjs",
|
|
1728
|
+
"test/review/fixloop.test.mjs",
|
|
1729
|
+
"test/review/moved-wiring.test.mjs",
|
|
1730
|
+
"test/review/patch-progress.test.mjs",
|
|
1731
|
+
"test/review/reopen.test.mjs",
|
|
1732
|
+
"test/worker/fixloop-wiring.test.mjs",
|
|
1733
|
+
"test/worker/pool.test.mjs",
|
|
1734
|
+
"test/worker/review-state-durable.test.mjs",
|
|
1735
|
+
"test/worker/review-state-restore.test.mjs"
|
|
1736
|
+
],
|
|
1737
|
+
"depends_on": [
|
|
1738
|
+
"B110"
|
|
1739
|
+
],
|
|
1740
|
+
"note": "root-added 2026-10-13; issue #16 item 3 (plans/field-bugs/plan.md B111, D3 = a); P1: a recheck with no fix hunks runs no session, counts no round and returns reviewed, approved false, reason no-fix-hunks; P2: a recheck answer without a resolved entry for each open finding of its packet is a schema miss (B55/B91 second level at the second broken result, else unavailable); the worker recheck summary gains status, tokens_in, tokens_out and reason; 10 fixtures now answer resolved explicitly; built by Codex at high effort in 2 rounds"
|
|
1741
|
+
},
|
|
1742
|
+
"B97": {
|
|
1743
|
+
"title": "Paired-file names in the packet; untracked owned files can be attached",
|
|
1744
|
+
"wave": 14,
|
|
1745
|
+
"level": "L2",
|
|
1746
|
+
"status": "landed",
|
|
1747
|
+
"owned": [
|
|
1748
|
+
"docs/how-it-works.md",
|
|
1749
|
+
"skill/references/review.md",
|
|
1750
|
+
"src/review/packet.mjs",
|
|
1751
|
+
"src/review/paired.mjs",
|
|
1752
|
+
"test/review/engine.test.mjs",
|
|
1753
|
+
"test/review/packet.test.mjs",
|
|
1754
|
+
"test/review/paired.test.mjs"
|
|
1755
|
+
],
|
|
1756
|
+
"depends_on": [
|
|
1757
|
+
"B111"
|
|
1758
|
+
],
|
|
1759
|
+
"note": "root-added 2026-10-13; Group E E5 part 1 (plans/run-speed/plan.md B97, D4 = a); pairsFor by stem for .mjs/.ts/PHP/Go/Python/Swift; summarize lists at most 40 unique test names or exported symbols of at most 120 chars, redacted before truncation; assemblePacket takes paired and renders ## paired after ## context, dropped first over budget, never split_required; attachFiles attaches an untracked file only when owned and changed in the block and not ignored or secret-like; builders wire it in B98; built by Codex at high effort in 2 rounds"
|
|
1760
|
+
},
|
|
1761
|
+
"B98": {
|
|
1762
|
+
"title": "The packet builders send the paired files; the lenses use them",
|
|
1763
|
+
"wave": 14,
|
|
1764
|
+
"level": "L2",
|
|
1765
|
+
"status": "landed",
|
|
1766
|
+
"owned": [
|
|
1767
|
+
"docs/how-it-works.md",
|
|
1768
|
+
"skill/references/review.md",
|
|
1769
|
+
"src/review/engine.mjs",
|
|
1770
|
+
"src/review/fixloop.mjs",
|
|
1771
|
+
"src/review/lenses/A.md",
|
|
1772
|
+
"src/review/lenses/B.md",
|
|
1773
|
+
"src/review/lenses/full.md",
|
|
1774
|
+
"src/review/lenses/recheck.md",
|
|
1775
|
+
"src/worker/engine.mjs",
|
|
1776
|
+
"test/review/fixloop.test.mjs",
|
|
1777
|
+
"test/review/paired-wiring.test.mjs",
|
|
1778
|
+
"test/review/route.test.mjs",
|
|
1779
|
+
"test/review/sections.test.mjs"
|
|
1780
|
+
],
|
|
1781
|
+
"depends_on": [
|
|
1782
|
+
"B97"
|
|
1783
|
+
],
|
|
1784
|
+
"note": "root-added 2026-10-13; Group E E5 part 2 (plans/run-speed/plan.md B98); pairedContext builds ## paired from the block's owned changed peers plus its test_files (glob entries dropped) in round 1, full rounds and rechecks; the worker passes ownedFiles/changedFiles so needs_file can attach a new untracked owned test file; a failing peer listing is fail-soft (empty context); the four lenses say a test named in ## paired exists; built by Codex at high effort in 2 rounds"
|
|
1785
|
+
},
|
|
1786
|
+
"B112": {
|
|
1787
|
+
"title": "Suite speed: peer diffs read lazily again (B98 regression); bench test load-proof",
|
|
1788
|
+
"wave": 14,
|
|
1789
|
+
"level": "L2",
|
|
1790
|
+
"status": "landed",
|
|
1791
|
+
"owned": [
|
|
1792
|
+
"src/worker/engine.mjs",
|
|
1793
|
+
"test/worker/peer-diffs-lazy.test.mjs",
|
|
1794
|
+
"test/bench-review.test.mjs"
|
|
1795
|
+
],
|
|
1796
|
+
"depends_on": [
|
|
1797
|
+
"B98"
|
|
1798
|
+
],
|
|
1799
|
+
"note": "root-added 2026-10-13; velocity; review.test.mjs 89 s -> 44 s"
|
|
1800
|
+
},
|
|
1801
|
+
"B113": {
|
|
1802
|
+
"title": "No leaked workers: orphan stop, failure-safe test cleanup, no-leak worker check",
|
|
1803
|
+
"wave": 14,
|
|
1804
|
+
"level": "L2",
|
|
1805
|
+
"status": "landed",
|
|
1806
|
+
"owned": [
|
|
1807
|
+
"docs/how-it-works.md",
|
|
1808
|
+
"package.json",
|
|
1809
|
+
"src/worker/loop.mjs",
|
|
1810
|
+
"test/cli/review.test.mjs",
|
|
1811
|
+
"test/docs-marketing.test.mjs",
|
|
1812
|
+
"test/harness-isolation.test.mjs",
|
|
1813
|
+
"test/helpers/isolate.mjs",
|
|
1814
|
+
"test/helpers/worker-processes.mjs",
|
|
1815
|
+
"test/no-leak.test.mjs",
|
|
1816
|
+
"test/router.test.mjs",
|
|
1817
|
+
"test/worker/cli-cleanup.test.mjs",
|
|
1818
|
+
"test/worker/helper-cleanup.test.mjs",
|
|
1819
|
+
"test/worker/helpers.mjs",
|
|
1820
|
+
"test/worker/orphan-guard.test.mjs",
|
|
1821
|
+
"test/worker/orphan.test.mjs",
|
|
1822
|
+
"test/worker/process-list.test.mjs"
|
|
1823
|
+
],
|
|
1824
|
+
"depends_on": [
|
|
1825
|
+
"B112"
|
|
1826
|
+
],
|
|
1827
|
+
"note": "root-added 2026-10-13; bug; 226 orphan test workers drove load to 350-450 and broke the release test step"
|
|
1626
1828
|
}
|
|
1627
1829
|
}
|
|
1628
1830
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codedology/code-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Plan/harden/code/review orchestration CLI and Agent Skill for cross-model, parallel, evidence-gated feature delivery.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"test": "CODE_FORGE_NO_LEAK=snapshot node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs && node --import ./test/helpers/isolate.mjs --experimental-test-module-mocks --test 'test/**/*.test.mjs'
|
|
41
|
+
"test": "export CODE_FORGE_TEST_SUITE=${CODE_FORGE_TEST_SUITE:-$(node -e 'process.stdout.write(require(\"node:crypto\").randomUUID())')}; CODE_FORGE_NO_LEAK=snapshot node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs && { node --import ./test/helpers/isolate.mjs --experimental-test-module-mocks --test 'test/**/*.test.mjs'; cf_test_status=$?; CODE_FORGE_NO_LEAK=final node --import ./test/helpers/isolate.mjs --test test/no-leak.test.mjs; cf_leak_status=$?; [ \"$cf_test_status\" -eq 0 ] && [ \"$cf_leak_status\" -eq 0 ]; }",
|
|
42
42
|
"typecheck": "tsc --checkJs --noEmit",
|
|
43
43
|
"release": "node scripts/release.mjs",
|
|
44
44
|
"changelog": "node scripts/changelog.mjs",
|
package/skill/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ State lives in the plan file's header — `facts → planned → hardened → co
|
|
|
41
41
|
4. Per block, in dispatch order, never more than `caps.coders` alive: `forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> --brief <file> --lines <n> [--kind code|docs|contract] [--tests <paths…>]` prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>`. With a `context` key, build the pack right after `block open`, before the pointer goes out: `forge context pack --run <r> --block <id>`; send the pack's text with the pointer and with every later coder task (`references/code.md` §3). Send the pointer per the engine's adapter — `solo` → `references/adapters/solo.md`, `harness` → `references/adapters/claude-code.md`, `subprocess` → `references/adapters/subprocess.md` — and wait for `ACK <sha8> lines=<n>`.
|
|
42
42
|
5. The coder's first message is its **facts diff**, then its forecast (cases and lines, `templates/block-record.md`), then code, file by file: after each file `forge review-file <path> --block <id> --run <r>`, then `forge review-file --wait <ticket>` before the next file. Rounds ≥ 2 re-check only the fix hunks (`references/review.md`).
|
|
43
43
|
6. `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` ends the coder's turn. Its report is a claim sheet: re-measure it (`references/code.md` §5). Just before the sentinel it carries the `TESTS <command> exit=<n>` line (`references/code.md` §3).
|
|
44
|
-
7. `forge block close <id> --run <r> --report <coder report>` is the gate
|
|
44
|
+
7. `forge block close <id> --run <r> --report <coder report>` is the gate. It needs every changed file accepted by the review gate — a signed `review.approved` row for its current content that no later review of the same content overrode (else `not_approved`), or every finding its review left open waived or answered (`reviewed_by_waiver`) — no `unowned_change`, no `no_changes` (the files are the owned diff plus every owned file reviewed for the block), gates green, every clause covered, forecast vs actual, no `rule_break`. The final report needs no `reviewed <path> ticket <id>` lines: `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. The close prints the reviewed lines, then the `reviewed_by_waiver` lines, then the `closed` line. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr when the block closes, and the close goes on. A file with a `review.waived` finding still gets a line; only a close with every review waived (`--no-require-reviews`) or `review.external` prints none. The printed line has no `via`: `--json` and the block's done file carry the lines as `reviewed: [{file, ticket, via}]`, with `via: approved` for a file with a signed approval, `via: waiver` when at least one of its open findings was waived (also when the others were answered), and `via: answered` when every one was answered. With `context.require_coder_tests` (set before `run start`) `--report` is required, and a report whose LAST `TESTS` line before its COMPLETE sentinel is missing or not `TESTS <command> exit=0` is FAILED, waived or external reviews or not (`references/code.md` §3). Red ⇒ `references/decisions.md` (S1 `next`, escalation). Green ⇒ one commit for the block in the project's house style; `forge block rebase <id> --run <r>` first when HEAD moved.
|
|
45
45
|
8. `forge run end --run <r>` when every block is `done`; `forge report --slug <slug>` prints cost per block, escalations, review rounds, the outside review rounds per PR next to their code-forge cost, and the spend per run (`budget.usd`, `references/ledger.md` §3.1); when a PR also got an outside review, record it first with `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]…` (`references/ledger.md` §4).
|
|
46
46
|
|
|
47
47
|
## §4 Hard rules (the headings of `references/rules-core.md`)
|
package/skill/references/code.md
CHANGED
|
@@ -25,8 +25,8 @@ The brief file carries, in this order:
|
|
|
25
25
|
1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
|
|
26
26
|
2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
|
|
27
27
|
3. The file loop: *after you finish each file in `owned_files` (tests written and green) run `forge review-file <path> --block <id> --run <r>`; before the next file run `forge review-file --wait <ticket> [--max 90s]` and act on the packet.* A `worker_down` answer ⇒ print `===BLOCK <id> FAILED: worker down===` (never approval). A cached result that never counted (`stale`, `unavailable`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`) is run again with `forge review-file <path> --block <id> --run <r> --fresh` (B75); anything else (a counted round, `review-state-lost`, or an unknown result) answers `fresh-needs-reopen` (`fresh-tampered` for a tampered state), and the coder stops and reports. A finding whose fix belongs in another owned file (a missing test, say) is fixed there and sent with `forge review-file <that file> --block <id> --run <r> --answers <ids> --of <source>` (`review.md` §5).
|
|
28
|
-
4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines) — left out with a `context` key, because the pasted pack carries the rules and lessons — the forbidden list verbatim (`security.md` §2), and the sentinel contract: the `TESTS` line of item 6, then `===BLOCK <id> COMPLETE===` followed by the test summary
|
|
29
|
-
5. **
|
|
28
|
+
4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines) — left out with a `context` key, because the pasted pack carries the rules and lessons — the forbidden list verbatim (`security.md` §2), and the sentinel contract: the `TESTS` line of item 6, then `===BLOCK <id> COMPLETE===` followed by the test summary and `git diff --stat`, or `===BLOCK <id> FAILED: <reason>===`.
|
|
29
|
+
5. **The close writes the ticket lines (B96)** (`templates/coder-brief.md`): The final report needs no `reviewed <path> ticket <id>` lines: `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. The close prints the reviewed lines, then the `reviewed_by_waiver` lines, then the `closed` line. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr when the block closes, and the close goes on. A file with a `review.waived` finding still gets a line; only a close whose every review the human waived, or a `review.external` close, prints none. This holds for the first attempt and every fix round. Pass the report to the gate with `forge block close <id> --run <r> --report <file>`. The printed line has no `via`: `--json` and the block's done file carry the lines as `reviewed: [{file, ticket, via}]`, with `via: approved` for a file with a signed approval, `via: waiver` when at least one of its open findings was waived (also when the others were answered), and `via: answered` when every one was answered. A file the gate accepts gets no line when the queue cannot give its ticket (its content changed after the gate read it, or its path cannot be resolved): the close prints `WARN no ticket for <path>`, or `WARN bad ticket for <path>` when the ticket is not 24 lowercase hex characters. If the lines cannot be built at all, the close still goes on, with no line and one `WARN reviewed lines unavailable: <message>`. Like the mismatch WARN, these go to stderr and print only when the block closes; a refused close prints none of them. The close refuses `coder report FAILED: no review-file ticket id for <files>` only when, under `--report`, a file the review gate accepts has no reviewed line from the close and no hand line for it in the report. For that check a hand line is a report line that names that file, no other file of the block, and a ticket id of 24 lowercase hex characters. So when the lines cannot be built at all, every file the gate accepts needs a hand line, and no hand line is compared with a ticket. For the mismatch WARN only a line that is exactly `reviewed <path> ticket <id>` counts (a `- `, `* ` or `1. ` list marker and backticks are fine), and since a ticket id is 24 lowercase hex characters, an id in capitals names another ticket.
|
|
30
30
|
6. **The green test run.** The coder runs the block's test command and prints one line that is exactly `TESTS <command> exit=<n>` just before `===BLOCK <id> COMPLETE===` — on the first attempt and again, freshly run, in every fix round. `<command>` is the command it ran (not empty); `<n>` is its exit code as a decimal 0–255 with no leading zeros. The line is matched after trimming; a line quoted inside prose, a non-numeric, signed or out-of-range `<n>`, or a leading zero does not count.
|
|
31
31
|
7. **Tests prove behaviour (B92).** The brief says: proof is behaviour, not text: never read a source file in a test to assert on its text. A test calls the code and checks what it does; reading a fixture is fine. A test file that reads a source file and asserts on its text gets a `## test-quality` section in its review packet (`review.md` §3). The section is a hint, not a finding: the reviewer checks those lines and, when the test really reads source text to assert on it, raises a `warning` (unless the file read is a fixture).
|
|
32
32
|
|
|
@@ -55,7 +55,7 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
|
|
|
55
55
|
| claim | re-measured by |
|
|
56
56
|
|---|---|
|
|
57
57
|
| only owned paths touched | file set = tracked changes since the base ∪ untracked (not ignored), listed from the repository top level, outside the repository top-level `.code-forge/` (keep transcripts and reports there; a subdirectory workspace's own `.code-forge/` is source tree except this run's record mirror and the block's default transcript: a `--transcript` or `--report` path in the source tree is a change like any other); each file must be in `owned_files`, unchanged since `block open` recorded the tree (`tree_at_open`; a path unreadable at open or at close never counts as unchanged), owned by another block of the run that is still **open**, or a closed block's file still exactly at the content its gate approved — else `unowned_change <file>` until `forge block claim <id> <path> --run <r>` or the human puts it back (not lifted by `--no-require-reviews`). The gate cannot tell which coder wrote a file an open sibling owns: it passes it to that sibling's gate and prints `WARN block <id>: <file> changed and is owned by open block <other>` for each one — read those lines |
|
|
58
|
-
| every file reviewed | file set = owned files changed since the base ∪ owned files with any review row of the block (`review.result`/`round`/`approved`/`triage`/`cap`/`late_finding`), an owned entry with no glob covering the paths below it; every path is repo-root-relative (what the worker signs), and an owned entry also matches relative to a workspace in a subdirectory; an empty set refuses `no_changes` (only the owner's `--no-require-reviews`, confirmed at a terminal, lifts it), and an empty diff with review rows WARNs (work committed before `block open`, or in another worktree — open blocks on a base without the work, in the workspace the coder edits). The
|
|
58
|
+
| every file reviewed | file set = owned files changed since the base ∪ owned files with any review row of the block (`review.result`/`round`/`approved`/`triage`/`cap`/`late_finding`), an owned entry with no glob covering the paths below it; every path is repo-root-relative (what the worker signs), and an owned entry also matches relative to a workspace in a subdirectory; an empty set refuses `no_changes` (only the owner's `--no-require-reviews`, confirmed at a terminal, lifts it), and an empty diff with review rows WARNs (work committed before `block open`, or in another worktree — open blocks on a base without the work, in the workspace the coder edits). The close prints a `reviewed <path> ticket <id>` line per file the review gate accepts (the `--report` the orchestrator always passes needs none). The gate accepts a changed or new file with a **signed** `review.approved` row for its current content hash that no later review of the same content overrode (a `review.result` with `approved: false`, a `review.round` with findings open, a `review.cap`), or with every finding its review left open waived or answered (`reviewed_by_waiver`): else `not_approved <file> <finding>` per open finding, cleared only by a fix and a fresh approval or a waiver — a critical only by the human's `block waive` — or an architect-ruled disposition to a named successor block |
|
|
59
59
|
| tests green | every gate command run once in the export, each exit status read |
|
|
60
60
|
| clauses covered | every clause names ≥ 1 test id; each test exists and passed in this run; S1 `scope` on the hunks outside the named tests says `false` |
|
|
61
61
|
| test count | the filtered run's delta equals the new tests' own count |
|
|
@@ -80,7 +80,9 @@ Without the flag the tree stays. On a block without a worktree the flag is a usa
|
|
|
80
80
|
|
|
81
81
|
**`block waive` and `block reopen` are run by the human, never by you (B80).** Both ask for a yes at a terminal. You print the exact command; the human runs it in their own terminal. The rule: a coder or the orchestrator never runs it and never answers its prompt. Separately, as a fact of the verb: when stdin or stdout is not a terminal (a pipe, or a process started with no terminal) it exits 2 with `block <verb> needs the human at a terminal to confirm; nothing was recorded (there is no --yes)` and records nothing. There is no `--yes`, and an autopilot grant does not change this (the delegate waives through `forge autopilot waive`). The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
82
82
|
|
|
83
|
-
`block attempt` never resets review state: it only raises the attempt counter, and a stopped file stays stopped. Use `review-file --fresh` only after a result that never counted (`stale`, `unavailable`, 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.
|
|
83
|
+
`block attempt` never resets review state: it only raises the attempt counter, and a stopped file stays stopped. Use `review-file --fresh` only after a result that never counted (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); it never re-runs a counted round. A missing state file needs no exit: the worker first rebuilds it from its ledger anchor (`review.state_restored`). Only a state whose anchor has no `state_body` is `lost` (`review-state-lost`); that one needs the human's `block reopen`, on its current content. A `review-state-tampered` file has none of these exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
84
|
+
|
|
85
|
+
A cached round-1 `split_required` ran no session and counted no round (B110). Plain `review-file` re-queues it with `retry: true`, and `--fresh` re-queues it too, with the same ticket id. After a budget change and `run reload`, the worker uses the live config. If the budget is still too small, it returns `split_required` again without a session. A recheck stop (`status: stopped`, `stopped: split_required`) keeps its `review.cap` row and still needs `block reopen` or `block waive`; plain review returns the cached stop and `--fresh` refuses `fresh-needs-reopen`.
|
|
84
86
|
|
|
85
87
|
`--no-require-reviews` is the human's way out, written as its own audit row. The human runs it themselves at a terminal: it asks for a yes, has no `--yes`, exits 2 unless stdin and stdout are both terminals, lifts `no_changes` and is refused while an autopilot grant is active — so never run it for them; tell them the command when they say so in chat. The owner's other way out is `review.external` in the config before `run start` (`review.md` §1): the gate then skips the review rows and the report's ticket ids and writes `gate.reviews_waived {by: config}`; every other check, `no_changes` included, still applies. A finding counts only under the gate's own configuration; a tautology (an expected value computed by the code under test) is a finding. Reproduce disclosures: a coder that reports its own rule break did the hard part.
|
|
86
88
|
|
|
@@ -2,7 +2,9 @@
|
|
|
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 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.
|
|
5
|
+
`forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The same content answers the cached result; `review-file <path> --block <id> --fresh` (B75) drops that cached result and queues the ticket again only when it never counted as a review (`stale`, `unavailable`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`). Use it after such a transient answer to get a real review; a coder may run it, and with no cached result it is a plain `review-file`. A cached `review-state-tampered` is refused `fresh-tampered` (only the owner's `block close --no-require-reviews` gets past it); any other cached result is refused `fresh-needs-reopen` (the human's exits are `block reopen` and `block waive`). A refused marker is read in place and never moved; only a re-runnable one is claimed (an atomic rename, then a byte compare) and dropped. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. 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
|
+
|
|
7
|
+
A cached round-1 `split_required` ran no session and counted no round. Plain `review-file` drops its marker and re-queues the same ticket with `retry: true`; `--fresh` also re-queues it (B110). After the human raises the budget and runs `run reload`, the worker re-plans with the live config; the ticket id stays the same. If it still does not fit, it returns `split_required` again with no session or cost. A recheck stop (`status: stopped`, `stopped: split_required`) has a `review.cap` row and still needs `block reopen` or `block waive`; plain review keeps it cached and `--fresh` refuses `fresh-needs-reopen`.
|
|
6
8
|
|
|
7
9
|
A ticket whose session breaks is not a review. A broken result is a `schema` miss or a timed-out attempt (B91): a timeout counts as a broken result, like a schema miss. Misses and timeouts of the same packet, level and role are counted together, so one miss plus one timeout is two. A packet's second broken result is tried once on the ticket's second level, in the same ticket, only when a second level exists (`review.second_levels.<level>`, or for a routed ticket the rule's `second`) and never in consensus mode (§4).
|
|
8
10
|
|
|
@@ -21,12 +23,33 @@ The topology is decided in one place (`planSessions`). Consensus (§6) runs only
|
|
|
21
23
|
|
|
22
24
|
## §3 Isolation and the packet
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
The review engine, fix-loop recheck and worker send `## paired` after `## context`
|
|
27
|
+
and before moved code and the diff, using the block's owned changed peers plus its explicit
|
|
28
|
+
`test_files` (`block open --tests`), even when their stems differ. Missing or unreadable pairs
|
|
29
|
+
are skipped; no pairs means no section. It names paired files and summarizes test names or exported
|
|
30
|
+
symbols: at most 40 entries per file, each at most 120 characters, with secret-looking and
|
|
31
|
+
registered values redacted. Stem matching supports `.mjs`/`.ts` (`.test`, `.spec`, and the `test/`
|
|
32
|
+
tree), PHP (`FooTest.php`), Go (`foo_test.go`), Python (`test_foo.py`), and Swift (`FooTests.swift`).
|
|
33
|
+
The entire section is capped at `review.budgets.full_in / 16` tokens. Over budget it is dropped
|
|
34
|
+
before shrinking context or trimming rules; it never causes `split_required` or cuts the diff.
|
|
35
|
+
Without paired input the packet bytes stay the same. The summary names tests; request their
|
|
36
|
+
bodies with `needs_file` when needed. A named test exists and must not be reported as unseen.
|
|
37
|
+
|
|
38
|
+
The attachment helper accepts a new untracked file only when the caller supplies it in both
|
|
39
|
+
the owned and changed concrete file lists of the same block and tree. The review engine passes
|
|
40
|
+
these lists to `attachFiles` in `reviewFile` for round 1 and full rounds, including those called
|
|
41
|
+
from the worker's fix loop. The fix-loop recheck has no `needs_file` path. A git-ignored file or a
|
|
42
|
+
secret path is refused, even when those lists name it; a failed git check also refuses it.
|
|
43
|
+
Without those lists, attachments remain tracked-only. Existing path and read-only checks apply.
|
|
44
|
+
|
|
45
|
+
Every reviewer, recheck, judge and S2 session runs with cwd = a fresh empty temp dir under the run's temp root and the packet **on stdin** — a no-tools session never opens a path and the packet never enters argv. Packet order is fixed so provider prompt caching applies: lens, project rules digest (≤ 60 lines), facts-sheet excerpt for the symbols in the diff, context, `## paired` (optional), `## moved code` (optional), diff. With a `context` key in the config (B63a, `code.md` §3) the project rules digest is the rules files' text and then, under a `### lessons` subheading, the lessons file, read from the main tree, never from a block worktree, each time a packet is built. `context` is read from the live config; `run reload` accepts a change to it. Rules and lessons share the one 60-line cap and are trimmed together, from the end. Without the key the slot is `(none)`, as before. A rules or lessons file missing, outside the repository or unreadable when the ticket is reviewed (or a bad `context` value) makes the ticket `review.unavailable` with `reason: context-unreadable`: no session runs and its packet directory is removed. The worker does not retry it: the ticket is done with that signed result. `review-file` on the same content is a fresh attempt (`retry: true`) each time it is run, with no cap (no session ran, so the one re-run of a failed packet is not used up); once the file is back, that attempt is reviewed normally. Context: a file at or under `review.context.whole_file_max_lines` travels whole; above it each hunk carries ± `review.context.hunk_context_lines` lines; over `review.budgets.full_in` the context shrinks to ± `review.context.min_context_lines`, then the digest is trimmed, never the diff; a diff alone over budget ⇒ `split_required`, with `tokens_in` and `budget` in the result, its ledger `review.result` row and the `review-file --wait` answer (`stop: split_required`) — except a Markdown file (`.md`, `.markdown`), which is reviewed **section by section**: the diff is cut at its headings (`#`..`######`, not inside fenced code), consecutive sections share a packet while it fits, every section packet runs the file's whole plan with its own stub guard, the file is approved only when every section is, and findings are merged with ids `S<section>.<id>`; a recheck over budget is split the same way, each open finding listed in its own section's packet. A section that alone is over budget keeps the file `split_required` (with that `section`), and so does a deleted Markdown file or one with no heading (fail-safe: one section); nothing is ever truncated. Moved code (B56): code moved between two changed files of the same block is named in a `## moved code` section before the diff (`- base lines a-b removed here were added in <file> lines c-d in this block.` / `- lines c-d were moved here from <file> (its base lines a-b) in this block.`), in round 1, rechecks, patch checks and the judge's packet. Detection is deterministic line matching against the block's other changed owned files diffed against the same base: lines trimmed with whitespace collapsed, lines with fewer than 4 letters or digits ignored, a moved stretch needs ≥ 3 such lines or ≥ 120 such characters; at most 12 ranges are listed; a Markdown section packet lists only the moves under its own headings (none ⇒ no section); the peers are listed and read once per review (one `git ls-tree <base>`, one `git diff <base>` over the peers that exist in the base — a peer deleted in the block, staged or not, is all removed lines — one `git check-ignore` and one `git diff --no-index` per new peer; `a/`/`b/` prefixes forced, `core.quotePath=false`; a peer name with a control, bidi or separator character, a quote or a backslash is skipped; a peer whose diff is over 2 MiB is skipped, never buffered past that); a removed block cut right above a heading belongs to the section holding its removed hunk; a move inside one file is not detected. It is extra text counted in the packet budget: every hunk stays, `reviewed_hunks` is unchanged, and a removal listed there is not a defect by itself — only stale references or imports are. The closed-book rule is in every lens: judge only what is in the packet; a file you cannot see goes in `needs_file`, and the worker attaches it (read-only, trimmed) for **one** more round.
|
|
25
46
|
|
|
26
47
|
Big files (B90): to send a big file to a long-context reviewer, set `review.routes` once — do not edit `.code-forge.yml` per file and restore it after. A rule is `{min_lines, level?, model, provider?, effort?, fallback?, session_timeout_s?, second?}`; `min_lines` and `model` are required. A rule's level decides which sessions it changes: an L2 rule (`level: L2`, the default) changes only the reviewers' model, and in consensus mode only slot A — slot B stays `review.second_levels.L2`; an L3 rule (`level: L3`) changes only the judge's model. What is counted: the file's lines at the reviewed state (the bytes in the review packet) — its newline bytes, plus one for a last line with no newline — once per ticket. A deleted file is never routed. Rules compete only within their level: among the rules of one level, the one with the largest `min_lines` at or under the count wins, and one ticket can take one L2 rule and one L3 rule; two rules with the same `level` and `min_lines` are refused. Left out, an optional key gets: `provider` — the top-level `provider` (the rule replaces the level entry, so the level's own `provider` is not inherited), and validation checks the rule with that same provider; `effort` — none (the CLI's default); `fallback` — none; `session_timeout_s` — the rule does not count in the timeout; `second` — `review.second_levels.<level>`. The timeout is one value for every session of the ticket: the largest `session_timeout_s` among the matched rules that set one (a matched rule without one is left out of that max, it does not count as `review.session_timeout_s`), else `review.session_timeout_s`. The rule's `second` is the second-level try's model (§4) for its level, in place of `review.second_levels.<level>` — except an L2 rule in consensus mode, whose `second` is not used. One signed `review.route {level, min_lines, lines, provider, model, session_timeout_s, second_model}` row per routed level is written before the ticket's first session: `provider` is the one the routed level runs on (the inherited one when the rule names none), and `second_model` is the rule's `second` model when that second is used, else null — always null for an L2 rule in consensus mode, because slot B is `review.second_levels.L2`, not the rule's. A retry of the same ticket never writes its rows twice. A row that cannot be written ⇒ `review.unavailable` with `reason: route-row`, and a routes list in the snapshot that cannot be used (or a routed level that cannot be resolved) ⇒ `reason: route-invalid` — no session runs. No rule matches (or no `review.routes`) ⇒ the ticket runs exactly as before. `run reload` accepts a change to it; validation refuses two rules with the same `level` and `min_lines`, and, with `review.multimodel`, an L2 rule whose provider (its own, else the inherited top-level one) equals the second reviewer's (slot B's, or the rule's own `second.provider`) (`review-routes-invalid`).
|
|
27
48
|
|
|
28
49
|
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.
|
|
29
50
|
|
|
51
|
+
Budget step-up (B109): the `quick` lens uses `review.budgets.quick_in` (default 6,000). A non-Markdown quick file whose diff packet exceeds that budget is planned again at risk 1 for the same block kind. If the new plan is `full`, the packet is rebuilt with `review.budgets.full_in` (default 32,000), and one full L2 session runs when it fits. Risk is unchanged. The result's `depth` and the `review.plan` row's `depth_unconstrained` and `depth_chosen` are `full`; the row adds `step_up: {from: 'quick', tokens_in, budget}` with the original quick packet's size and budget. Still over `full_in` ⇒ `split_required` with the full packet's size and budget and no session; nothing is truncated. A second plan that is not `full` keeps the original stop. Fitting quick files, Markdown section splitting and risk-1 files keep their plans and have no `step_up`; rechecks already use `full_in`.
|
|
52
|
+
|
|
30
53
|
Test quality (B92): a test file that reads a source file and asserts on its text gets a `## test-quality` section before the diff, in each reviewer's round-1 packet and the judge's. Rechecks get the lens rule but no `## test-quality` section. The section names the file and the flagged line numbers — at most 10, the rest counted as `(<n> more not listed)` — never the file's text. Detection is a text match with no model call: a test file (a test path, a `Tests/` dir, or a name ending in `Tests.<ext>` or `Test.<ext>`; never a `.md` or `.txt` file or a file under a `fixtures`, `fixture` or `__fixtures__` dir) with a line holding a read call and a line holding a contains or match call (`includes(`, `contains(`, `toContain(`, `toMatch(`, `assert.match(`, `assertIn(`, `strings.Contains(`). The read calls that count are `readFileSync`, `readFile`, `open(`, `read_text`, `String(contentsOf`, `contentsOfFile`, `File.read`, `os.ReadFile`, `fs::read_to_string`, `readText(`, `readString(` and `ReadAllText(`, matched as text. The call counts when its span — its own line, plus up to 3 more lines while the read call's own `(` is still open (a formatter split) — holds a quoted source path (`.swift`, `.ts`, `.tsx`, `.js`, `.mjs`, `.cjs`, `.py`, `.go`, `.rs`, `.kt`, `.java`, `.rb`, `.php`, `.cs`) or a name bound on an earlier line to one; the call's own line is the one listed. A name is bound only when its value IS the source-path literal: the whole right-hand side, or the literal passed straight to `join`, `path.join`, `resolve` or `path.resolve` (`spawnSync('node', ['bin/cli.mjs'])` binds nothing); the binding may carry modifiers and a type (`private val X`, `const val X`, `private const val X`, `private static let X`, `private static final String X`, `private const string X`, `const string X`, `export const X`). A fixture read never counts: a read whose span holds a string literal with a `fixtures`, `fixture` or `__fixtures__` path segment (`'test/fixtures/a.mjs'`, `join(dir, 'fixtures', 'a.mjs')`), or names a name bound on a line holding such a literal (`const FIXTURES = join(__dirname, 'fixtures')`). The section is a hint, not a finding: it writes no row and changes no gate. The lens rule tells the reviewer to check those lines and raise such a test as a `warning` ("test reads source text; prove behaviour instead") unless the read file is a fixture. It is extra text counted in the packet budget, like the moved-code section; a file with no flag gets no section. Over budget it takes the path any extra takes: the context shrinks, then the digest is trimmed; when the diff plus the extras alone are over budget, the file is `split_required` (with `tokens_in` and `budget`, no session; a Markdown file is split by section instead).
|
|
31
54
|
|
|
32
55
|
## §4 The stub guard — what counts as a review (R11)
|
|
@@ -37,6 +60,8 @@ A result is a review only if the process exited 0, the JSON validates against th
|
|
|
37
60
|
|
|
38
61
|
Per file, per block:
|
|
39
62
|
|
|
63
|
+
Only real rechecks count toward the round cap, per-level ladder and stall rules (B111). An unchanged recheck runs no session, writes no `review.round`, and returns `reviewed`, `approved: false`, `reason: 'no-fix-hunks'` with the open findings; round counters stay unchanged. A recheck answer that skips a `resolved` entry for an open finding in its packet is a schema miss. It follows §4's fallback: when eligible (the packet's second broken result), that schema miss tries the second level once. A successful second-level answer is accepted; only a failed second-level try, or no available second level, leaves `review.unavailable` and retry. An unavailable recheck closes nothing, counts no round and never stalls. Markdown packets check only their section's listed findings. A real `resolved: false` still counts and can stall; an unchanged `patch_check` still stops with `l3_patch_exhausted` (B73). The worker's recheck session summary shows `status`, `tokens_in`, `tokens_out`, and `reason` for a string status other than `ok`; missing status is `null` with no reason.
|
|
64
|
+
|
|
40
65
|
1. Round 1 is the tiered review of §2 on the full packet.
|
|
41
66
|
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).
|
|
42
67
|
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.
|
|
@@ -52,7 +77,7 @@ Per file, per block:
|
|
|
52
77
|
- **The human runs `block waive` and `block reopen` (B80).** Both verbs ask for a yes at a terminal. The orchestrator prints the exact command, with the human's reason, and never answers the prompt; the human types the command in their own terminal. The rule: a coder or the orchestrator never runs either verb. With stdin or stdout not a terminal (a pipe, or a process started with no terminal) the waive exits 2 with `block waive needs the human at a terminal to confirm; nothing was recorded (there is no --yes)` and the reopen with `block reopen needs the human at a terminal to confirm; nothing was recorded (there is no --yes)`; neither writes a row or removes a done marker. At a terminal the verb first refuses what it would refuse anyway, before it asks: a malformed `--file` or finding id, or one with a control character (exit 2), an unknown block (`unknown_block`), for a reopen a block that is not open (`reopen-block-not-open`) or a file it does not own (`reopen-not-owned`), and for a waive a file the block does not own (`waive-not-owned`) or a finding id that no review row of the block names for that file (`waive-unknown-finding`; the `proof` finding is exempt) — exit 1, nothing asked, nothing written. A "no" or a cancel prints `block <id> <verb>: cancelled — nothing was recorded` (exit 1). A "yes" writes the same signed row as before. There is no `--yes`: it is an unknown flag (exit 2). Under an active autopilot grant both verbs still prompt the same way (only `block close --no-require-reviews` is refused); the delegate's waivers go through `forge autopilot waive` (`by: autopilot`), which needs no terminal. The prompt checks only that stdin and stdout are terminals; a pseudo-terminal passes it, so it does not prove a human (`security.md` §3).
|
|
53
78
|
- **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.
|
|
54
79
|
- **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.
|
|
55
|
-
- **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.
|
|
80
|
+
- **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`, round-1 `split_required`, or `refused` with `other-run`, `context-unreadable` or `worktree-*`); on a recheck stop it refuses `fresh-needs-reopen`. A `tampered` state (step 8) is not a stop and gets no exits: only the owner's `block close --no-require-reviews` gets past it.
|
|
56
81
|
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.
|
|
57
82
|
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.
|
|
58
83
|
8. **A state file that goes missing is rebuilt from the ledger.** The worker keeps each file's round, level, open and late findings and stop in a signed state file. Every save first writes a signed `review.state` anchor row, and the anchor carries the state, so a missing file is rebuilt from it (Restore, below). Only an anchor with no state leaves the file `lost` (Lost, below).
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.
|
|
13
|
+
PINNED_VERSION="0.11.0"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|
|
@@ -19,7 +19,7 @@ After you finish each file in `owned_files` (tests written and green) run `forge
|
|
|
19
19
|
|
|
20
20
|
Proof is behaviour, not text: never read a source file in a test to assert on its text. Call the code and check what it does; a test may read a fixture. A test file that reads a source file and asserts on its text gets a `## test-quality` section in its review packet. The section is a hint, not a finding: the reviewer checks those lines and, when the test really reads source text to assert on it, raises a `warning` (unless the file read is a fixture).
|
|
21
21
|
|
|
22
|
-
## Final report —
|
|
22
|
+
## Final report — the close prints the ticket ids
|
|
23
23
|
|
|
24
24
|
Run the block's test command (`test_command`) and print its result on a line of its own, exactly:
|
|
25
25
|
|
|
@@ -34,17 +34,15 @@ TESTS npm test exit=0
|
|
|
34
34
|
|
|
35
35
|
The `TESTS` line stays ABOVE the sentinel; only the test summary comes after it.
|
|
36
36
|
|
|
37
|
-
End with `===BLOCK <id> COMPLETE===`, the test summary and `git diff --stat
|
|
37
|
+
End with `===BLOCK <id> COMPLETE===`, the test summary and `git diff --stat`.
|
|
38
38
|
|
|
39
|
-
`reviewed <path> ticket <ticket-
|
|
40
|
-
|
|
41
|
-
**FAILED unless:** the report lists the `review-file` ticket id of every changed file. A report with no ticket ids, or one that misses a changed file, counts as FAILED whatever its sentinel says: the orchestrator treats it as `===BLOCK <id> FAILED: no review-file ticket ids===` and `forge block close <id> --run <r> --report <file>` refuses it (`coder report FAILED: no review-file ticket id for <files>`). "All findings addressed" is not a report.
|
|
39
|
+
The final report needs no `reviewed <path> ticket <id>` lines: `block close` prints one for every file the review gate accepts (approved, waived or answered), with the queue's ticket. The close prints the reviewed lines, then the `reviewed_by_waiver` lines, then the `closed` line. With `--report`, a hand line that names another ticket prints `WARN report ticket mismatch <path>` on stderr when the block closes, and the close goes on. A file with a `review.waived` finding still gets a line; only a close whose every review the human waived, or a `review.external` close, prints none. The printed line has no `via`: `--json` and the block's done file carry the lines as `reviewed: [{file, ticket, via}]`, with `via: approved` for a file with a signed approval, `via: waiver` when at least one of its open findings was waived (also when the others were answered), and `via: answered` when every one was answered. Every changed file still needs its `review-file` run: a file the review did not accept keeps the block open. "All findings addressed" is not a report.
|
|
42
40
|
|
|
43
41
|
Or `===BLOCK <id> FAILED: <reason>===`.
|
|
44
42
|
|
|
45
43
|
## Fix-round brief (round ≥ 2, or a respawn after a climb)
|
|
46
44
|
|
|
47
|
-
The open findings by id, the fix history, the files they sit in. Fix only those hunks, then run `forge review-file <path> --block <id> --run <r>` and `--wait` on **every file the round changed** — a fix the reviewer never saw is not a fix. A fix made in another owned file is sent with `--answers <ids> --of <source>` (The file loop). The final report follows the same rule
|
|
45
|
+
The open findings by id, the fix history, the files they sit in. Fix only those hunks, then run `forge review-file <path> --block <id> --run <r>` and `--wait` on **every file the round changed** — a fix the reviewer never saw is not a fix. A fix made in another owned file is sent with `--answers <ids> --of <source>` (The file loop). The round's final report follows the same rule as the first: it needs no ticket lines. Re-run the block's test command after the fixes and print a fresh `TESTS <command> exit=<n>` line just before the round's sentinel: a TESTS line from an earlier report does not count.
|
|
48
46
|
|
|
49
47
|
## Context pack
|
|
50
48
|
|