@codedology/code-forge 0.7.7 → 0.8.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 +41 -0
- package/README.md +50 -2
- package/docs/README.md +2 -1
- package/docs/reference/blocks.json +157 -1
- package/docs/reference/config.md +26 -8
- package/package.json +1 -1
- package/schema/code-forge.schema.json +81 -4
- package/skill/SKILL.md +6 -6
- package/skill/references/code.md +33 -4
- package/skill/references/ledger.md +8 -4
- package/skill/references/proof.md +44 -3
- package/skill/references/review.md +4 -2
- package/skill/references/security.md +1 -1
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +18 -1
- package/src/autopilot/binnacle.mjs +21 -3
- package/src/cli/block.mjs +292 -33
- package/src/cli/context.mjs +175 -0
- package/src/cli/gates.mjs +125 -15
- package/src/cli/ledger.mjs +46 -2
- package/src/cli/proof.mjs +62 -16
- package/src/cli/report.mjs +7 -4
- package/src/cli/review-file.mjs +61 -6
- package/src/cli/status.mjs +212 -0
- package/src/config/validate.mjs +45 -11
- package/src/context/pack.mjs +319 -0
- package/src/doctor/local.mjs +2 -1
- package/src/engines/sentinel.mjs +27 -6
- package/src/gates/detect.mjs +21 -8
- package/src/gates/expand.mjs +202 -0
- package/src/gates/owned-count.mjs +66 -0
- package/src/gates/run.mjs +49 -12
- package/src/install/wizard/answers.mjs +34 -5
- package/src/install/wizard/flags.mjs +8 -1
- package/src/install/wizard/project-file.mjs +2 -0
- package/src/install/wizard/run.mjs +6 -0
- package/src/install/wizard/steps.mjs +24 -4
- package/src/install/wizard/summary.mjs +13 -5
- package/src/ledger/export.mjs +1 -1
- package/src/ledger/outside-review.mjs +164 -0
- package/src/ledger/report.mjs +17 -5
- package/src/proof/export.mjs +93 -10
- package/src/proof/lock.mjs +28 -5
- package/src/proof/worktree.mjs +482 -0
- package/src/review/gate-check.mjs +9 -5
- package/src/review/open-findings.mjs +59 -0
- package/src/state/block-end.mjs +290 -0
- package/src/state/block.mjs +103 -39
- package/src/state/brief-status.mjs +131 -0
- package/src/state/config-snapshot.mjs +51 -0
- package/src/state/registry.mjs +35 -4
- package/src/state/run.mjs +10 -1
- package/src/util/breadcrumbs.mjs +1 -0
- package/src/util/forbidden.mjs +36 -1
- package/src/util/gate-token.mjs +8 -0
- package/src/worker/engine.mjs +157 -17
- package/src/worker/loop.mjs +26 -7
- package/src/worker/queue.mjs +36 -10
- package/src/worker/ticket.mjs +46 -6
package/skill/references/code.md
CHANGED
|
@@ -10,19 +10,40 @@ The coder then forecasts **cases** (tests) and **lines**. Compute the floor your
|
|
|
10
10
|
|
|
11
11
|
## §3 Dispatch — what the brief contains
|
|
12
12
|
|
|
13
|
-
`forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] [--kind code|docs|contract] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused; an owned entry with no glob character — `*`, `?`, `{` — owns that path and every path below it, so `src/feature` owns `src/feature/a.swift` but not `src/featureX/a.swift`, and `src/feature` overlaps `src/feature/x.swift` or `src` in another block — unless the entry is a regular file in the workspace at `block open` (or `block claim`): then it owns only itself, recorded as `owned_kinds`, and every later check reads that answer; a glob owns what it matches, and a brace entry `src/{a,b}` is a glob, so `src/a` owns nothing below it; no trailing `/`), the block kind (declared, else detected — `decisions.md` §5), the level (a docs/contract block below `levels.coder_floor_docs`, default L1, is raised to it and the CLI prints the floor), the clauses with their test ids and the forecast, writes the ledger `dispatch` row and prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` (≤ 200 B). Call it before every spawn, in every engine. `--level` may be left out only in a brief-mode run (`plan.md` §6: the rules pick the lane from the owned entries, `--lines` and `proof.tiers.high.paths`, and a signed `decision` row with `source: rules` records it) or when autopilot recorded a level; otherwise its absence is a usage error. A docs block that owns a glob (`docs/**`) must declare `--kind docs`: a glob is judged by its literal ending, so undeclared it counts as `code`.
|
|
13
|
+
`forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] [--kind code|docs|contract] [--tests <paths…>] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused; an owned entry with no glob character — `*`, `?`, `{` — owns that path and every path below it, so `src/feature` owns `src/feature/a.swift` but not `src/featureX/a.swift`, and `src/feature` overlaps `src/feature/x.swift` or `src` in another block — unless the entry is a regular file in the workspace at `block open` (or `block claim`): then it owns only itself, recorded as `owned_kinds`, and every later check reads that answer; a glob owns what it matches, and a brace entry `src/{a,b}` is a glob, so `src/a` owns nothing below it; no trailing `/`), the block kind (declared, else detected — `decisions.md` §5), the level (a docs/contract block below `levels.coder_floor_docs`, default L1, is raised to it and the CLI prints the floor), the clauses with their test ids and the forecast, writes the ledger `dispatch` row and prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` (≤ 200 B). Call it before every spawn, in every engine. `--level` may be left out only in a brief-mode run (`plan.md` §6: the rules pick the lane from the owned entries, `--lines` and `proof.tiers.high.paths`, and a signed `decision` row with `source: rules` records it) or when autopilot recorded a level; otherwise its absence is a usage error. A docs block that owns a glob (`docs/**`) must declare `--kind docs`: a glob is judged by its literal ending, so undeclared it counts as `code`. `--tests <paths…>` (same path rules as `--owned`, else `bad-tests`; exact paths only — a file or a directory; `*` and `?` are refused, and so is a path starting with `-`, both a usage error, exit 2; `[` and `{` are allowed, as in `app/[id]/page.test.tsx`) names the block's test files, stored as `test_files` on the block and the `dispatch` row; a `{test_files}` gate runs them (`proof.md` §3). Without it the owned entries that are concrete files (no glob) and look like tests are used; `--tests` is the precise way.
|
|
14
|
+
|
|
15
|
+
Under `proof.isolation: worktree` (B62a, `proof.md` §3), `block open` first makes the block's own worktree. The output then has a second line, `worktree <path> · branch <branch>` (with `· reused` when a re-open took the earlier attempt's tree), before the pointer. Give the coder that path as its working directory. Two worktree blocks may own the same path. A refused worktree refuses the open, and nothing is recorded. A re-open keeps the tree's base: do not pass a different `--base`. The coder works in that tree path: it edits there, commits on the block's branch, and calls `review-file` there. Run `gates run --run <r> --block <id>` without `--cwd` (another `--cwd` is refused, exit 2). Gates, reviews and the close all measure the tree (B62b, `proof.md` §3): a change made only in the main checkout is not the block's work, and a block whose tree was deleted cannot close until the tree is restored.
|
|
16
|
+
|
|
17
|
+
**`--json` on every block verb (B64a).** `block open|attempt|rebase|claim|close|stop|waive … --json` prints exactly one JSON line on success instead of the text: `{"block":<id>,"event":<open|attempt|rebased|claimed|closed|stopped|waived>, …}` with the facts the text prints — open: `level`, `asked`, `source` (`flag`/`rules`/`autopilot`), `kind`, `attempt`, the full `base_sha`, `tests` (count), `worktree` (`{path, branch, reused}` or null) and `brief` (the pointer or null); attempt: `attempt`; rebase: `old_base`, `new_base`; claim: `path`; waive: `finding`, `file`, `by`; stop: nothing more; close: `status`, `reason`, `close_event` (or null), plus `worktree {removed, path, branch}` with `--remove-worktree` (on a block with no worktree that flag is a usage error, exit 2). On failure the exit code and stderr are as without the flag and stdout stays empty — except a close the gate refused, which also prints `{"block":<id>,"ok":false,"reason":<text>}`; every other close refusal (usage, not open, a cancelled `--no-require-reviews`, a failed worktree removal) leaves stdout empty. Without `--json` the output is unchanged.
|
|
18
|
+
|
|
19
|
+
**Block end: done file and `on_block_end` hook (B64b).** Every block end — `block close` that closes or stops it (a pin event or `ledger.tamper`) and `block stop` — writes `<run root>/blocks/<id>.done.json` (the run's state folder, `~/.code-forge/runs/<run>/`) atomically after the end's rows, overwritten on a later end; its absolute path is in the hook's `CODE_FORGE_DONE_FILE` and in `done_file` of `close`/`stop --json`. Content: `{block, run_id, status, event, reason, commit, spent_usd, unknown_usd_sessions, findings, at}` — `status` `closed`/`stopped`; `event` `block.close`, `block.stop` or the close's own stop event (e.g. `worker.replaced`, `ledger.tamper`); `reason` null for a plain close; `commit` the HEAD of the block's tree or the repository, null when unreadable; `spent_usd`/`unknown_usd_sessions` from the block's own rows as `forge status` computes them, both null when the ledger cannot be read; `findings` the open review findings, null when the ledger cannot be read or the count fails. It never holds transcript text, diff text or secrets. A failed write changes nothing else and prints `warning: done file not written: <reason>` on stderr. Wait for that file (or the hook) instead of polling `forge status` or the coder's transcript. A done file from an earlier end stays after a reopen, so count it only when its `at` is later than the current dispatch; if no fresh done file appears by the time the coder agent reports, run `forge status` once. With `hooks.on_block_end: {argv, timeout_s}` set before `run start` (immutable; `run reload` refuses a change), the CLI runs `argv` once per end — also when a later step such as `--remove-worktree` fails — after the done file and outside the run lock: no shell, cwd = the repository top, stdin closed, output discarded, the process group killed at `timeout_s` (1–600, default 60), with `CODE_FORGE_RUN`, `CODE_FORGE_BLOCK`, `CODE_FORGE_BLOCK_STATUS` and `CODE_FORGE_DONE_FILE` (`""` when the done file was not written) added to its env; one signed `hook.ran` row per end (`ledger.md`). A hook that does not succeed (exit 0) only warns: `warning: on_block_end hook exit N` | `timed out` | `not found` | `signal <NAME>` | `failed to start (<code>)`; if the `hook.ran` row itself cannot be written, one more line `warning: on_block_end hook row not written (<code>)` follows (an error code only, never message text). With the hook set, `close --json` and `stop --json` add `done_file` (the absolute path, or null when it was not written); without it the output is byte-identical to before. The hook is the owner's command and runs outside coders: a coder never runs it (the close gate flags a transcript line that runs its argv[0] basename plus argv[1] when present and not blank — both trimmed, argv[1] one literal token even with spaces inside, which a transcript line split on spaces never matches — as `rule_break … code-forge-on-block-end-hook`), and `.code-forge.yml` is non-ownable (the same guard as `review.external.required`) with the hook set — also when `hooks.on_block_end` is the only key that guards it.
|
|
20
|
+
|
|
21
|
+
**The context pack (B63a).** With a `context` key in `.code-forge.yml` (`rules`, `lessons`, `commands`, `max_lines`), build the block's pack right after `block open`, before the pointer is sent, and again before every later coder task (fix rounds too): `forge context pack --run <r> --block <id>` writes `<run root>/context/<id>.md` and prints its path and `tokens ~<n>` (`--json`: `{"path":…,"tokens":…,"lines":…,"cut":…,"sha256":…}`). The pack file lives under the run root, which a brief never names (see below): paste the pack's **text** into the coder task with the pointer and with every later coder task (or copy the file to a location outside the run root and name that), and keep the run-root path out of the brief. The pack holds, in order: the block header (run, block, level, owned files, the test files when present, the worktree path and branch when present), the project's build, test, lint and format commands as argv text in POSIX shell quoting and redacted like the rules (code-forge never runs them; a control character in a token, a path or a header value is refused), each rules file, then the lessons file, each under its source path (an empty or whitespace-only file gives its heading alone). Over `max_lines` (default 300, minimum 20) the lessons are cut first, then the rules files from the last one backwards, each cut section ending in `[cut: N lines dropped]`; the header and the commands are never cut. Rules and lessons are read from the main tree. A missing file, or a path outside the repository (`..`, an absolute path, a symlink leading out — a broken symlink is judged by where it points), refuses the pack (exit 1, naming the key and the path, nothing written); so does a config without `context` or with a bad `context` value. Each new pack writes a signed `context.pack` row; the same pack again writes none and prints the same output. The key is read from the live config, not the run's snapshot: it is not authority, and `run reload` takes a change to it — except `context.require_coder_tests` (the close check for the TESTS line, below), which is fixed for the run. The coder may run the verb too, but a pack the coder builds (and its path) is for the coder's own use only, never what the orchestrator sends; `.code-forge.yml` stays non-ownable.
|
|
22
|
+
|
|
23
|
+
The brief file carries, in this order:
|
|
14
24
|
|
|
15
25
|
1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
|
|
16
26
|
2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
|
|
17
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).
|
|
18
|
-
4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines), the forbidden list verbatim (`security.md` §2), and the sentinel contract: `===BLOCK <id> COMPLETE===` followed by the test summary, `git diff --stat` and one `reviewed <path> ticket <ticket-id>` line per changed file, or `===BLOCK <id> FAILED: <reason>===`.
|
|
28
|
+
4. The project's own rules digest (`CLAUDE.md`/`AGENTS.md`, ≤ 60 lines) — left out with a `context` key, because the pasted pack carries the rules and lessons — the forbidden list verbatim (`security.md` §2), and the sentinel contract: the `TESTS` line of item 6, then `===BLOCK <id> COMPLETE===` followed by the test summary, `git diff --stat` and one `reviewed <path> ticket <ticket-id>` line per changed file, or `===BLOCK <id> FAILED: <reason>===`.
|
|
19
29
|
5. **FAILED unless** (`templates/coder-brief.md`): a final report — first attempt or fix round — that does not list the `review-file` ticket id of every changed file counts as FAILED whatever its sentinel says. Pass the report to the gate with `forge block close <id> --run <r> --report <file>`; it refuses `coder report FAILED: no review-file ticket id for <files>`.
|
|
30
|
+
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.
|
|
20
31
|
|
|
21
32
|
The brief never mentions `block waive`, keys, or `~/.code-forge/runs/`. The coder process gets no Jev key (the System 1 model's key, `keys.jev`) and no signer key.
|
|
22
33
|
|
|
34
|
+
**The close check for the TESTS line (B63b).** The gate reads the LAST `TESTS` line in the segment of the block's last sentinel: after the previous sentinel line of any block (an earlier attempt, or another block in a shared log) and before this one; a line after the sentinel is ignored. Set `context.require_coder_tests: true` in `.code-forge.yml` **before `run start`** to make it a close check: it is read from the run's config snapshot only, never the live file, and `run reload` refuses a change to it (`context.require_coder_tests cannot change mid-run`). With it on, `--report` is required, and `block close` refuses (exit 1, the block stays open, no `block.close` row) with one of the messages below. This check runs before the other close checks and only reads the report, so a refusal here writes no row at all — not even the human waiver row of `--no-require-reviews`:
|
|
35
|
+
|
|
36
|
+
- `coder report FAILED: context.require_coder_tests needs --report` — no `--report`;
|
|
37
|
+
- `coder report FAILED: --report cannot be read`;
|
|
38
|
+
- `coder report FAILED: no ===BLOCK <id> COMPLETE=== sentinel` — none for the block, or its last one is FAILED;
|
|
39
|
+
- `coder report FAILED: no TESTS line` — none in that sentinel's segment;
|
|
40
|
+
- `coder report FAILED: TESTS exit=<n>` — `<n>` is not 0.
|
|
41
|
+
|
|
42
|
+
Waived or external reviews (`--no-require-reviews`, `review.external`) do not lift it: they waive reviews, not tests. A close that passes writes a signed `coder.tests {block, command, exit}` row (the command redacted) under the run lock with the close rows, only when every other check passed — after any `gate.reviews_waived {by: config}` row, just before `block.close`. With the key false or absent the brief still asks for the line, but the gate ignores it and writes no row.
|
|
43
|
+
|
|
23
44
|
## §4 The file loop, from your side
|
|
24
45
|
|
|
25
|
-
Each `review-file` ticket is served by the worker `forge run start` launched: tools first (the project's gates on that file — red ⇒ `tools_red`, no model review), then S1 `risk`, then the planned depth (`review.md` §2). You watch the
|
|
46
|
+
Each `review-file` ticket is served by the worker `forge run start` launched: tools first (the project's gates on that file — red ⇒ `tools_red`, no model review), then S1 `risk`, then the planned depth (`review.md` §2). You watch the run, not the coder's output: `forge status` (B64a) is the progress check — read-only (no ledger row, never the run lock), every run of this repository that is not ended, newest first, with a blank line between runs, at most 10 lines per run: `run <id> · <status> · <plan|brief> · spent $<x>[ of $<budget>]` (money with 2 decimals; `$<x>+?` when some session has no known price, `$?` when the ledger cannot be read), then per open or in-attempt block `<id> <open|attempt-<n>> <level> · <model or -> · gate <pass|fail|none>[ (<gate>)] · findings <n>` (the gate field is `gate pass`, `gate fail (<gate>)` with the failing gate's name, `gate fail` when no gate was named, or `gate none` when no gate ran; findings = the count of open review findings the close gate would refuse today), `… +<k> more blocks` as the last block line when they do not fit, and `closed <a> · stopped <b>` only when either is non-zero — printed LAST, but it keeps its place in the cap ahead of block lines (block lines are dropped first). No such run: `no active run`, exit 0. `--run <id>` shows one run (ended too); an unknown run exits 1, a usage error exits 2. `--json` prints one line, `{"runs":[…]}`, with every open block (no cap, so `more` is always 0), `unknown_usd_sessions`, and per block `gate: {result, name}` (`name` null for `pass`, for `none` and for an unnamed fail). Use it instead of reading logs or polling the coder's transcript: it never prints transcript or diff text. `forge ledger tail --slug <slug>` shows the rows themselves (`review.plan`, `review.round`, `review.done`). Idle is a wake, never proof; the sentinel is the only completion signal (`adapters/*.md`).
|
|
26
47
|
|
|
27
48
|
## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
|
|
28
49
|
|
|
@@ -41,6 +62,12 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
|
|
|
41
62
|
| rows genuine | the MAC of every gate-relevant row verifies; the live worker matches the pinned pid |
|
|
42
63
|
| nothing left open | no unruled `late_findings` (a ruling clears only a warning or nit ruled `nit`; a critical late finding needs the human's waiver); no file at `review_cap` without a signed `review.waived` row |
|
|
43
64
|
|
|
65
|
+
`forge block close <id> --run <r> --remove-worktree` (B62a, worktree isolation only) removes the block's worktree after a successful close. It runs `git worktree remove` without `--force` and keeps the branch.
|
|
66
|
+
- Success prints `block <id> worktree removed: <path> (branch <branch> kept)` and writes a signed `worktree.removed` row.
|
|
67
|
+
- A tree with modified or untracked files prints `block <id> worktree NOT removed: <reason> …` and writes `worktree.remove_failed`. The command exits 1, but the block stays closed.
|
|
68
|
+
|
|
69
|
+
Without the flag the tree stays. On a block without a worktree the flag is a usage error (exit 2). The flag is forbidden to coders: only you remove a tree.
|
|
70
|
+
|
|
44
71
|
`--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.
|
|
45
72
|
|
|
46
73
|
## §6 Budget — the stop is the feature (R6)
|
|
@@ -49,7 +76,9 @@ The coder's report is a claim sheet. The gate reproduces each number in the meas
|
|
|
49
76
|
|
|
50
77
|
## §7 The PR contract — a draft PR only after independent review
|
|
51
78
|
|
|
52
|
-
|
|
79
|
+
Both `autonomy` keys default to false. Whatever they say, a block's branch is handed back, pushed or put in a PR only when conditions (1)–(4) below hold; the orchestrator pushes only a branch that meets them. **Push off** (`autonomy.coder_may_push` false or absent): the coder never pushes and never opens a PR. It commits on its block branch and hands the branch back with the evidence of (1)–(4) and the claim sheet in its hand-back report; the orchestrator (or the human) pushes and opens the draft PR with the acceptance verbatim plus the claim sheet. **Push on, draft PR off** (`autonomy.coder_may_open_draft_pr` false or absent): when (1)–(4) hold and its commit meets the commit part of (5) the coder may push its block branch, and its evidence goes in the hand-back report (claim sheet), not in a PR body; it opens no PR. The orchestrator (or the human) opens the draft PR from the pushed branch with the acceptance verbatim plus the claim sheet. **Both on:** a coder may commit, push and open a **draft** PR on its own when all five hold, each pasted as evidence in the PR body: (1) facts diff delivered and no STOP tripped; (2) the block filter and regression anchors green in one run; formatter, static analysis, safe-edit and invariants clean; (3) red→green rows for every new test (`proof.md` §2); (4) every changed file carries a signed `review.approved` row — a stub, a timeout or `review.unavailable` forbids the push; (5) the commit is path-scoped to owned files, in house style, with trailers per `commit.trailers`, and the PR body carries the acceptance verbatim plus the claim sheet. A coder may not mark the PR ready, merge, dismiss a finding, raise a cap or push after a failed review. A GO carries a checksum (plan file, revision, line count) that the coder echoes back; unsent text in a coder's input box is never an instruction. Whoever pushed, re-measurement then runs on the pushed sha; the orchestrator marks ready; the human merges — at most `wip.ready_prs` at a time.
|
|
80
|
+
|
|
81
|
+
**An outside review of the PR (B66).** When a reviewer outside code-forge (the `review.external` reviewer, a person, a CI review bot) has finished with a PR, record its rounds and findings: `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...` — name every block the PR carries (`ledger.md` §4). A later call for the same PR replaces the earlier one in `forge report`, whose `outside_review` section sets the rounds next to code-forge's cost for those blocks.
|
|
53
82
|
|
|
54
83
|
**A finding dispositioned out of the block:** only the architect may rule it, and only when severity is warning or lower, every fix site lies outside the owned paths, no shipped UI or API reaches the failure path after merge, and a **named successor block** carries the failure path as its opening clause. The verdict stays recorded; a disposition without a successor clause is a dismissal, and dismissals do not exist.
|
|
55
84
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
Every row has the run, the block, the event and a timestamp; the ones the block gate relies on carry `mac` (`security.md` §3). Useful fields: `level`, `provider`, `model`, `effort`, `fallback_step` (every spawn), `role ∈ {coder, reviewer, judge, s2, author, facts}`, `trigger` (escalations), `source ∈ {jev, rules, s2, s2-fallback, shadow}` (decisions), `depth_unconstrained` / `depth_chosen` / `degrade_step` and `context_mode ∈ {whole, hunks, minimal, recheck}` (reviews), `round` and `kind ∈ {full, recheck, patch_check, sweep}` (rounds), `isolation ∈ {export, lock}` (proof), `l3_patch`, `l3_fallback`, `overspend`. **Tokens are facts, dollars are estimates**: `tokens_source: reported` when the CLI's JSON carries usage, else `estimated`. File paths in rows are relative to the repository root.
|
|
6
6
|
|
|
7
|
-
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch
|
|
7
|
+
Events you will read most: `run.start`, `run.mode {mode: brief, plan: none}` (a brief-mode run, `plan.md` §6), `review.external {required, reviewer}` (the owner's outside reviewer, `review.md`), `run.stop {reason}`, `run.end`, `dispatch` (`test_files` when `block open --tests` named them), `gate.red` and `gate.done` (with `scope: related` + `test_count` or `scope: full` + `reason: threshold | no_tests` when a gate has the `{test_files}` item, `no_tests` also without `--run/--block`; with `--run/--block` they name `run` and `block`, and for a block with a worktree also `worktree: <path>` (the gates ran there); a gate with env names only `env_keys` in its result, `proof.md` §3), `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}`, `gate.reviews_waived {by: human | config}` (`by: config` from `review.external`), `coder.tests {block, command, exit}` (signed; under `context.require_coder_tests` the close read the coder's `TESTS <command> exit=0` line from `--report`, written with the close rows just before `block.close` and only when the block closes, `code.md` §3), `decision {question: lane, source: rules}` (a brief-mode `block open` without `--level`), `worktree.created {block, path, branch, base}` (`block open` under `proof.isolation: worktree` made the block's tree, written before `dispatch`; a re-open that reuses it writes none), `worktree.removed {block, path, branch}` and `worktree.remove_failed {block, path, reason}` (after `block close --remove-worktree`; the branch is always kept, and on a failure the tree stays, `proof.md` §3), `context.pack {block, path, sha256, tokens, lines, cut}` (signed; `forge context pack` wrote a new pack for the block — the same pack again, same sha256 as the block's last row, writes none; `code.md` §3), `hook.ran {block, hook: on_block_end, argv0, exit, signal, timed_out, duration_ms}` (signed; one row per block end with `hooks.on_block_end` set — also when the hook fails, times out or is not found; none when the key is absent; `argv0` is the basename only. Exited: `exit` its code, `signal` null. Killed by a signal: `exit` null, `signal` its name. Timed out: `exit` null, `signal` `SIGKILL`, `timed_out` true. Not found (argv[0] ENOENT) or any other spawn error (a missing cwd, EACCES): `exit` null, `signal` null, `timed_out` false; `code.md` §3), `outside_review {run, pr, rounds, findings, link (or null), blocks}` (signed; `forge ledger outside-review`, §4 — the latest row per PR wins in every reader), `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
|
|
8
8
|
|
|
9
9
|
## §2 `forge report --slug <slug> [--json] [--export <dir>]`
|
|
10
10
|
|
|
11
|
-
Terminal output in this version. Sections: cost per block — completed and still open, with `completed` and the count of unknown-price sessions (per level and lane, split review / coder / S1+S2 / facts / proof time) · lane distribution · escalations per block with `trigger` · S1 calls per block · S2 rate and overrule rate per question · the review budget table with `missed_after_degrade` per degrade step · review depth and context-mode distribution, findings by category and severity · fix rounds per file, rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine
|
|
11
|
+
Terminal output in this version. Sections: cost per block — completed and still open, with `completed` and the count of unknown-price sessions (per level and lane, split review / coder / S1+S2 / facts / proof time) · lane distribution · escalations per block with `trigger` · S1 calls per block · S2 rate and overrule rate per question · the review budget table with `missed_after_degrade` per degrade step · review depth and context-mode distribution, findings by category and severity · fix rounds per file, rounds that stalled, files stopped at `review_cap` · `review.unavailable` by reason · proof time per block · forecast vs actual lines and cases with the measured lines-per-case · blocks stopped at L3 · `overspend` and `l3_fallback` counts · runs stopped for `no_engine` · `outside_review` (14th, last): per PR sorted by number, `{pr, rounds, findings, link, blocks, code_forge_usd, unknown_usd_sessions}` — `code_forge_usd` sums the `cost_per_block` totals of the PR's blocks in its run, rounded as `cost_per_block` rounds; unknown-price sessions count 0 and are counted; a PR with no blocks shows 0 and 0 unknown; `[]` with no row; in the terminal the lines print under `== outside_review ==`: `outside review: none`, or one line per PR (`PR #12 · 3 outside rounds · 7 findings · code-forge $3.75 (+1 unknown) · B1, B2`). `--export <dir>` writes one JSON file per section — the durable dataset a future UI reads. After the sections the terminal output prints the spend per run (open blocks included) and the total. `report` checks no row's signature in any section (an unsigned row counts like a signed one); shadow rows (`source: shadow`) are excluded from every section.
|
|
12
12
|
|
|
13
13
|
## §3 Live views
|
|
14
14
|
|
|
@@ -24,10 +24,14 @@ Sessions that run at the same time (B41): each session first takes its provider
|
|
|
24
24
|
|
|
25
25
|
Known limits, accepted: unknown-price sessions and `session.background` coders add **0** to the spent total — record their real cost with `forge ledger add coder --usd <n>`; a background coder's reservation is released at its start row, since its cost is never seen; a session refused because of running sessions' reservations is refused, not queued — run it again once they finish; `forge run status` shows `unknown_usd_sessions` and prints `N sessions have no known price; the budget does not count them` when N > 0. The lock and the reservations live in one process only: two separate code-forge processes starting a session of one run at the same moment can still each pass the check or each write a `budget.warning` row.
|
|
26
26
|
|
|
27
|
-
## §4
|
|
27
|
+
## §4 Outside review rounds — `forge ledger outside-review`
|
|
28
|
+
|
|
29
|
+
When a PR got an outside review (the owner's reviewer, a review bot), record it once the review is done: `forge ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...` — one signed `outside_review` row in the run's project ledger, stdout `outside_review pr <n> · rounds <r> · findings <f> · blocks <ids or ->`. `--pr` > 0, `--rounds` and `--findings` ≥ 0; `--pr`, `--rounds`, `--findings` decimal, no sign, no leading zeros except 0; `--link` an `https://`/`http://` URL, no whitespace, ≤ 2048 characters, redacted before it is stored; `--block` repeats, each a block of the run (unknown ⇒ exit 1 naming the id, nothing written), duplicates collapsed, order kept. Bad flags exit 2 and write nothing; an unknown run exits 1. A second call for the same PR replaces the first in `report` (both rows stay in the ledger). Orchestrator or owner only — never put it in a coder brief.
|
|
30
|
+
|
|
31
|
+
## §5 Calibration — `forge ledger calibration --slug <slug> [--question <id>]`
|
|
28
32
|
|
|
29
33
|
Per question id and `source`: reliability buckets × outcome, the count of `≥ 0.99` answers later marked wrong, and a threshold recommendation per question — **never applied automatically**; the human edits `thresholds.<question>`. Ground truth arrives from four callers: shadow sampling (`calibration.shadow_rate`; ships at 0 — the code path exists, turn it on by config once outcome rows flow), the block gate's `outcome` rows (`next = complete` then a red gate ⇒ `wrong`; an escalation after a `lane` pick ⇒ `lane.under`), `forge ledger outcome --slug <slug> --pr <n> --ci red|green|reverted` from the shipped CI template, and `forge ledger outcome --slug <slug> --scan-git [--cwd <dir>] [--days <n>]`, which walks `git log` for reverts (the `This reverts commit <sha>` chain, parity-aware) and marks `reverted` on the matching review rows — run by `run start` and on demand, no CI needed.
|
|
30
34
|
|
|
31
|
-
## §
|
|
35
|
+
## §6 Durability
|
|
32
36
|
|
|
33
37
|
Rotated at 10 MB; rows are signed only where the gate relies on them; `report --export` snapshots are what anything downstream reads. The ledger is per project slug, and every verb takes the config and the slug from the **project root** (the nearest regular `.code-forge.yml` up to the git top level, else the git top level; outside git never above HOME, and a `.git` at HOME does not count), so a run from a sub-directory still lands in the same file.
|
|
@@ -23,12 +23,53 @@ The runner works in the export (§3) or under the lock with a journal: it refuse
|
|
|
23
23
|
|
|
24
24
|
## §3 The measurement export (R13) — `proof.isolation: export`, the default
|
|
25
25
|
|
|
26
|
-
Parallel blocks share one working tree, so nothing is measured there. `forge proof export <block> --run <r>` builds `.code-forge/export/<block>/`: (a) `git archive <base_sha>` extracted; (b) every path the base's `.gitattributes` marks `export-ignore` restored from `git show` — the hazard is real: repositories mark `/.github` and `CHANGELOG.md` that way; (c) every path in `proof.export.copy_untracked` that exists in the main tree added (`.env`, `.env.testing` by default; never anything under `.code-forge/`); (d) the block's owned files at their **current** content — new files included, and an owned file that existed at the base but is deleted in the tree is **removed from the export** too (listed under `removed` in the export's result), so a deletion is measured as a deletion; (e) the dependency dirs in `proof.export.link_dirs` symlinked read-only. Gates, red→green and the test-count delta run there; `--remove` deletes it at block close. A gate command that fails inside the export with a missing-file error is reported as `export.incomplete: <path>` with the sentence *add it to `proof.export.copy_untracked` or set `proof.isolation: lock`* — never as a red test.
|
|
26
|
+
Parallel blocks share one working tree, so nothing is measured there. `forge proof export <block> --run <r>` builds `.code-forge/export/<block>/`: (a) `git archive <base_sha>` extracted; (b) every path the base's `.gitattributes` marks `export-ignore` restored from `git show` — the hazard is real: repositories mark `/.github` and `CHANGELOG.md` that way; (c) every path in `proof.export.copy_untracked` that exists in the main tree added (`.env`, `.env.testing` by default; never anything under `.code-forge/`); (d) the block's owned files at their **current** content — new files included, and an owned file that existed at the base but is deleted in the tree is **removed from the export** too (listed under `removed` in the export's result), so a deletion is measured as a deletion; (e) the dependency dirs in `proof.export.link_dirs` symlinked read-only — except a **Composer vendor dir** (one holding `composer/autoload_real.php`), which is **copied** (listed under `copied` in the export's result): PHP resolves `__DIR__` through symlinks, so a linked `vendor` makes Composer's autoloader load `App\…` and `Tests\…` from the main tree, where another block's half-written file would leak into the measurement. Gates, red→green and the test-count delta run there; `--remove` deletes it at block close. A gate command that fails inside the export with a missing-file error is reported as `export.incomplete: <path>` with the sentence *add it to `proof.export.copy_untracked` or set `proof.isolation: lock`* — never as a red test.
|
|
27
27
|
|
|
28
28
|
**`proof.isolation: lock`** is the fallback for stacks whose tests cannot run from an export: `forge proof lock <block> --run <r>` takes a run-wide lock (Solo `lock_acquire` when present, else a lock file) that pauses other blocks' gate runs (`proof.busy`); the runner works in the shared tree with the journal; `forge proof unlock <block> --run <r>` releases it. The ledger flags `isolation: lock`.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**`proof.isolation: worktree` (B62a)** gives each block its own git worktree on its own branch. `forge block open` runs `git worktree add <dir>/<block> -b <branch> <base>` before anything is recorded. It stores `worktree {path, branch, base_sha}` on the block (the path is absolute), writes a signed `worktree.created {block, path, branch, base}` row before `dispatch`, and prints `worktree <path> · branch <branch>` between the open line and the pointer. The keys are under `proof.worktree`:
|
|
31
|
+
- `dir`: where the trees go, resolved from the repository top level (not from the workspace, even when the workspace is a subdirectory). The default is `../<repo-name>-worktrees`, where `<repo-name>` is replaced with the basename of the repository top level: outside the repository, so the main tree never shows the worktrees as untracked files. A `dir` through a symlink is recorded by its realpath.
|
|
32
|
+
- `branch`: a template, default `forge/{run}/{block}`. Only `{run}` and `{block}` are substituted, and the name must pass `git check-ref-format --branch`.
|
|
33
|
+
- `base`: the commit-ish the tree starts from. The default is the block's base (`--base`, else HEAD).
|
|
34
|
+
|
|
35
|
+
The open is refused (exit 1, `…; nothing was recorded`: no block, no row, no dispatch, no tree, no branch) for a block id that is not one plain path segment, an invalid branch name, a branch that already exists (checked again just before `git worktree add`, so a branch made after the plan is refused and kept, never deleted), something already at `<dir>/<block>`, a base that is not a commit, or a failing `git worktree add` (git's reason is named; a tree git registered anyway and a branch the failed add made are removed again, and anything that cannot be removed is named: `…; nothing was recorded, but left behind: <what> (<git's reason>) — remove it by hand`). If the open fails after the tree was made (a ledger row or the run record cannot be written), the tree is removed again (`git worktree remove --force`) with the branch the open made; the open's own error is shown, and anything that could not be removed is added to it: `Removing the worktree of the failed open left behind: the worktree <path> (<reason>); the branch <branch> (<reason>) — remove them by hand.`
|
|
36
|
+
|
|
37
|
+
A re-open of the same block (`--attempt`) reuses its worktree and branch as they are, printing `· reused` with no new `worktree.created` row. The block's base is then the tree's `base_sha`, not HEAD; a `--base` that names another commit is refused (exit 1, nothing recorded). `block rebase` is refused for a block with a worktree (exit 1): its base is the base of its tree. A tree still registered at the path but on another branch or a detached HEAD refuses the re-open (`worktree <path> is on <branch | a detached HEAD>, not <branch>`). A tree that is gone — removed by `--remove-worktree`, or its directory deleted by hand (git then lists it `prunable`; the open runs `git worktree prune`) — is added back at the same path on the same branch, with a new `worktree.created` row.
|
|
38
|
+
|
|
39
|
+
Overlap: two open blocks that each have their own worktree, at different paths, may own the same path (at `block open` and `block claim`). A block without a worktree is checked against every other open block exactly as under export, and `.code-forge.yml` is still never an owned file.
|
|
40
|
+
|
|
41
|
+
`forge block close <id> --run <r> --remove-worktree` (orchestrator only, forbidden to coders) runs `git worktree remove <path>` after a successful close. It never uses `--force` and always keeps the branch. A tree whose directory is gone refuses the close itself (see below); one that vanishes after that check (git lists it `prunable`) is pruned with `git worktree prune` and reported as removed (`worktree.removed`, exit 0).
|
|
42
|
+
- Success writes a signed `worktree.removed {block, path, branch}` row.
|
|
43
|
+
- A tree with modified or untracked files is kept. The command writes a signed `worktree.remove_failed {block, path, reason}` row and exits 1, and the block stays closed. It prints `block <id> closed`, then `block <id> worktree NOT removed: <git's reason> — the block stays closed; the tree stays at <path> (commit or clean it, then git worktree remove it)`.
|
|
44
|
+
|
|
45
|
+
Without the flag the tree stays. The flag on a block without a worktree is a usage error (exit 2).
|
|
46
|
+
|
|
47
|
+
`proof.isolation` and `proof.worktree` are fixed for the run: they are read from the run's config snapshot, and `run reload` refuses to change them. Nothing is ever pushed or merged, and no branch is deleted except one a failed open made itself.
|
|
48
|
+
|
|
49
|
+
**Where a worktree block's work is measured.** Gates, reviews and the close check of a block with a worktree run in its tree, never in the main checkout:
|
|
50
|
+
- `forge gates run --run <r> --block <id>` runs every gate with cwd = the workspace's place in the tree (leave `--cwd` out for a worktree block): the tree root when the workspace is the repository top, else the workspace's subdirectory inside the tree. `{test_files}` resolves the tests in the tree, and `gates.full_suite_threshold_files` counts the owned files in the tree. The gates themselves still come from the main tree's `.code-forge.yml`. The `gate.red` and `gate.done` rows and the JSON output carry `worktree: <path>`. The only `--cwd` accepted is that same place in the tree (compared by realpath), which is the same as no `--cwd`; any other `--cwd` is refused (exit 2): `gates run: block <id> of run <r> works in its worktree <path>, not in --cwd <dir> — drop --cwd (or give the worktree); no gate was run`. Any `--cwd=<dir>` (one argument with `=`, which is not read as `--cwd`) is refused too (exit 2), whatever it names: `gates run: block <id> of run <r> works in its worktree <path>, not in --cwd=<dir> — drop it (--cwd=<dir> is not read as --cwd); no gate was run`. For a block without a worktree, `--cwd` and `--cwd=<dir>` behave as before. A recorded tree that is gone, not a directory, lacks the workspace's subdirectory, or is not on the block's branch (another branch, a detached HEAD, or a plain directory), or is malformed (no absolute path, a branch that is empty or has control characters, a `base_sha` that is not 40 or 64 lowercase hex) refuses the run (exit 1): `gates run: block <id> of run <r>: <reason>; no gate was run`, e.g. `the worktree <path> is gone (removed or moved)`. `gates run` takes no count at `base_sha`: there is no test-count delta for a worktree block.
|
|
51
|
+
- `forge review-file`, from the main tree or from inside the tree, queues in the main tree's `.code-forge/queue`, where the worker runs. The file must lie inside the tree: a path with a `..` segment, an absolute path, or a symlink that leads out of the tree is refused (exit 1, nothing queued) with `{"status":"refused","reason":"bad-path","message":"…"}`. The content hash, the diff against the tree's `base_sha`, the peers' diffs and the fix hunks are read in the tree the run record names (never a path from the ticket). The ticket and the answer carry `tree: <path>` (the realpath), and the tree is part of the ticket id, so the same bytes in the main tree and in the tree are two tickets. A repeat request answers with the tree it was given, never with a path stored in the ticket. The worker reads the tree from the run record; a ticket whose `tree` is not that tree, or a ticket with no `tree` for a block that has one, is `review.unavailable` with `reason: worktree-mismatch` (paths compared by realpath). A block the run record gives no worktree is hashed in the main tree, wherever `review-file` is called (also from inside another block's tree), and its ticket carries no `tree`; only a run record that cannot be opened here (no such file, no permission) falls back to the tree `review-file` is called from. The packet's facts slot says `Worktree: the file is reviewed as it is in the block's worktree <path> (branch <branch>), against the block base.`. A file changed only in the main tree is never seen. A recorded tree that is gone, off its branch (`worktree-branch-mismatch`) or malformed, or a run record that exists but cannot be read (`run-record-unreadable`), refuses `review-file` (exit 1, nothing queued): `{"status":"refused","reason":"worktree-missing","message":"block <id>: the worktree <path> is gone (removed or moved); nothing was queued"}`. A tree that goes away after the ticket was queued makes it `review.unavailable` with `reason: worktree-missing` (`worktree-malformed` for a bad record, `worktree-branch-mismatch` off the branch), which is retryable.
|
|
52
|
+
- The `reason` for each refusal (gates run, block close, proof, `review-file`, and the worker's `review.unavailable`): `worktree-missing` the tree is gone; `worktree-not-dir` its path is a file; `worktree-malformed` the record has no absolute path, a path or branch with control characters, an empty branch, or a `base_sha` that is not 40 or 64 lowercase hex; `worktree-branch-mismatch` the tree is on another branch, detached, or a plain directory; `worktree-foreign` the path is a checkout of another repository (its git common dir differs from the workspace's), even on a branch of the same name; `worktree-workspace-missing` the tree lacks the workspace's subdirectory; `git-failed` git could not run (timed out, could not start, killed), with git's reason; `run-record-unreadable` the run record exists but cannot be parsed or names no workspace; `worktree-mismatch` (worker only) the ticket's `tree` is not the recorded one; `bad-path` (`review-file` only) the file leaves the tree.
|
|
53
|
+
- `forge block close` runs no gate (for any block), so no gate runs outside the tree at close: a worktree block's gates are `gates run --block <id>`, which runs in the tree. It measures the changed, unowned and reviewed files in the tree against its `base_sha`, committed on the branch and uncommitted alike. A change in the main tree or in another block's tree never counts. In the tree nothing is exempt: the main tree's dirty files at open (`tree_at_open`) excuse nothing, and a file a sibling open block owns is unowned too (`block <id> open: unowned_change <file>`, exit 1), since a sibling's work is in its own tree. When no owned file differs but owned files have review rows, the WARN says the work was likely `done outside the block's worktree (in the main checkout or another tree)`. A tree that is off its branch or malformed refuses the close too (exit 1, `block close: block <id>: <reason>, so its work cannot be measured; nothing was recorded`). A tree that is gone refuses the close (exit 1): `block <id>: its worktree <path> is gone (removed or moved), so its work cannot be measured — restore it on branch <branch> (git worktree prune, then git worktree add <path> <branch>) and close again; nothing was recorded`.
|
|
54
|
+
- `forge proof export <id>` and `forge proof red-green <id>` read the block's changed files, the `--test` file and the base in the tree (the workspace's place in it, against the tree's `base_sha`). The mutation proof runs in an export built FROM the tree, never in the tree itself: the export is written to the main tree's `.code-forge/export/<id>` (where `export --remove` and `proof restore` look), so the tree's files and `git status` are exactly as they were after the proof. This was chosen over measuring in the tree under the proof lock, because a crash mid-proof could leave a reverted source in the tree. The config (`proof.export.link_dirs`, `copy_untracked`) still comes from the main tree's `.code-forge.yml`; the link dirs and untracked files are taken from the tree. The `proof` rows are `isolation: export` and carry `worktree: <path>`, even if the main tree's `.code-forge.yml` was later edited to `isolation: lock` (no proof lock is asked for). A recorded tree that is gone, off its branch or malformed refuses (exit 1, no row): `proof <verb>: block <id> of run <r>: <reason>; no proof was run`. `proof lock <id>` on a worktree block is refused (exit 2, no lock, no row): `proof lock: block <id> is proven in an export built from its tree; proof lock does not apply`. A block without a worktree under `isolation: lock` still refuses `proof export` (exit 1, `proof export: proof.isolation is lock — use proof lock instead of an export`). `unlock`, `restore` and `export --remove` act only on the run's lock and the export directory, as before.
|
|
55
|
+
|
|
56
|
+
None of them pushes, merges or checks anything out: the work stays on the block's branch, and the main tree's HEAD and index never move. Blocks without a worktree (export, lock) are unchanged.
|
|
57
|
+
|
|
58
|
+
Test-count delta (blocks without a worktree) = the count on the block's filtered run in the export minus the same filter at `base_sha` (a second export, cached per base). The full suite runs only above `gates.full_suite_threshold_files`, also in the export. This delta belongs to the proof/export path; `gates run` never takes it.
|
|
59
|
+
|
|
60
|
+
**Related tests (B60).** A gate argv item that is exactly `{test_files}` (one whole item, never part of one, never the command, never the only item — `validate` refuses those — never a shell string) runs only the block's tests under `forge gates run --cwd <dir> --run <r> --block <id> [--slug <slug>]`: the paths given at `block open --tests` (the precise way), else the owned entries that are concrete files (no glob, no trailing `/`) and look like tests (a `test`/`tests`/`__tests__`/`spec` directory, or a name ending `.test.*`, `.spec.*`, `Test.php`, `_test.go`, or `test_*.py`). When the block owns more files than `gates.full_suite_threshold_files` (default 10; owned files: globs and directories count the tracked and new (not ignored) files they cover, a path not created yet counts as 1; outside a git repository each entry counts 1, and any other git failure runs the full suite with `reason: threshold`), the item is dropped with `reason: threshold`; when no test file is known, or `gates run` has no `--run/--block`, it is dropped with `reason: no_tests`. Dropped, the gate runs the full suite. The `gate.red` and `gate.done` rows say which: `scope: related` with `test_count`, or `scope: full` with `reason`; with `--run/--block` they also name `run` and `block`. A gate without the item runs exactly as before (no scope is worked out and its rows carry none). A test path that starts with `-` is refused, never passed as an option. Example: `test: [npx, vitest, run, "{test_files}"]`. For a worktree block (`proof.isolation: worktree`), leave `--cwd` out (`gates run --run <r> --block <id>`): the gate runs in the tree, `{test_files}` resolves the tests there, and the threshold counts the owned files in the tree, never the main tree's.
|
|
61
|
+
|
|
62
|
+
**Gate env (B60).** A gate may be `{argv: [...], env: {NAME: value}}`; the variables are added to that gate's environment (`env: {}` is no env). code-forge never prints or records an env value. Gate env is not a secret store. Child output is masked only for values of 8 or more characters. `gates detect`, results, ledger rows, `doctor` and `init` show only the names (`env_keys`); put secrets in the environment or a secret manager (`validate` refuses secret-looking values). A short value (`CI=1`, `MODE=test`) is not masked, so `npm test` stays readable. A block that edits `.code-forge.yml` shows the values in its own diff.
|
|
31
63
|
|
|
32
64
|
## §4 Verified and unverified stacks
|
|
33
65
|
|
|
34
|
-
The export is proven on the package's own example repository (a Node library with a `node:test` suite, `/.github` and `CHANGELOG.md` restored from `export-ignore`).
|
|
66
|
+
The export is proven on the package's own example repository (a Node library with a `node:test` suite, `/.github` and `CHANGELOG.md` restored from `export-ignore`).
|
|
67
|
+
|
|
68
|
+
**PHP (Pest and PHPUnit, Laravel) under export: VERIFIED** on 2026-10-11 with PHP 8.4.25, Composer 2.10.2 and Laravel Framework 13.35.0 — a fresh `laravel/laravel` skeleton once on PHPUnit 12.5.38 (as shipped) and once on Pest 4.7.8 (`pestphp/pest` + `pestphp/pest-plugin-laravel` added). Re-run it with `CODE_FORGE_LIVE_PHP=1 node --import ./test/helpers/isolate.mjs --test test/proof/laravel-live.test.mjs` (needs `php`, `composer` and the network; skipped without the flag, so `npm test` never needs them; POSIX only — it was run on macOS, not on Windows). Each check builds the skeleton, commits it, adds the block's owned files (a new `app/` class, a unit test, a feature test that writes to the database), breaks a file the block does not own in the main tree (the main tree goes red), builds the export with the default lists and runs the detected gate `php artisan test --compact` there: exit 0, 4 tests passed. What a Laravel export needs:
|
|
69
|
+
- `vendor` in `proof.export.link_dirs` (the default): it is **copied**, not linked, as a Composer vendor dir (§3 (e)). Run `composer install` in the main tree first; the export never installs anything. The copy has a cost on every export: about 5 s and 69 MB for the Laravel skeleton where the file system cannot clone; code-forge asks for a copy-on-write clone (`COPYFILE_FICLONE`), which is near-free where it is supported (APFS, Btrfs, XFS). An absolute symlink inside `vendor` that points into the main tree (a Composer path repository: `vendor/acme/pkg -> <main>/packages/pkg`) is pointed at the same path in the export; relative links and links outside the main tree are kept.
|
|
70
|
+
- `.env` (and `.env.testing` when the project has one) in `proof.export.copy_untracked` (the default): Laravel needs its `APP_KEY`.
|
|
71
|
+
- A database the export can own: SQLite in memory. The skeleton's `phpunit.xml` already sets `DB_CONNECTION=sqlite` and `DB_DATABASE=:memory:`; when yours does not, set them as gate env: `test: {argv: [php, artisan, test, --compact], env: {DB_CONNECTION: sqlite, DB_DATABASE: ":memory:"}}`. A suite that needs a running database server is not isolated by the export — use `proof.isolation: lock`.
|
|
72
|
+
|
|
73
|
+
Detection: `composer.json` + `vendor/bin/pest` is `php-pest`; `composer.json` + `vendor/bin/phpunit` without Pest is `php-phpunit`. With an `artisan` file either runs `php artisan test --compact`; without one, `vendor/bin/pest` or `vendor/bin/phpunit` (a plain PHP project; detected, not run live).
|
|
74
|
+
|
|
75
|
+
A Vitest stack is **UNVERIFIED** until a real second consumer runs it; `proof.isolation: lock` is the documented fallback until then. `doctor` says which gate commands are on PATH.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. A coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
|
|
6
6
|
|
|
7
|
+
Under `proof.isolation: worktree` (`proof.md` §3) the coder calls `review-file` inside the block's tree. The ticket still goes to the main tree's queue, where the worker runs. The content hash, the diff against the block's base and the peers' diffs are read in the block's tree, which the worker takes from the run record, and the ticket and the answer carry `tree: <path>`. The tree is part of the ticket id, and a ticket whose tree is not the one in the run record, or a ticket with no tree for a block that has one, is answered `review.unavailable` with `reason: worktree-mismatch`. A block the run record gives no worktree is hashed in the main tree, wherever `review-file` is called, and its ticket names no tree. The packet's facts slot names the tree and its branch. A change made only in the main tree is never seen. A recorded tree that cannot be used, or an unreadable run record, refuses `review-file` itself (exit 1, nothing queued); after the ticket was queued the worker answers `review.unavailable` with the same reason. The reasons: `worktree-missing` (gone), `worktree-not-dir` (a file), `worktree-malformed` (a bad record), `worktree-branch-mismatch` (off its branch), `worktree-foreign` (a checkout of another repository), `worktree-workspace-missing` (no workspace subdirectory in the tree), `git-failed` (git could not run, with its reason), `run-record-unreadable` (the record cannot be parsed or names no workspace), plus `worktree-mismatch` (worker only) and `bad-path` (`review-file` only). See `proof.md` §3 for each cause.
|
|
8
|
+
|
|
7
9
|
**The owner's two ways out of the per-file review.** (1) `forge block close <id> --no-require-reviews`, typed by the owner at a terminal (`code.md` §5). (2) `review.external: {required: true, name: <reviewer>}` in `.code-forge.yml`, set before `run start`, when an outside reviewer reviews each block (B58). With `required: true`, `run start` refuses a missing or blank `name`, and writes a signed `review.external` row with the reviewer's name. `run reload` refuses to add, remove or change the key. With the key, `block close` does not check the review rows (a `not_approved` file included) or the report's ticket ids, needs no terminal and works under an autopilot grant. When the block closes, it writes a signed `gate.reviews_waived {by: config, reviewer}` row just before `block.close`; a refused close writes no such row. Unowned changes, `unproven`, `no_changes`, the transcript grep and the registry's orphans still refuse. No block may own `.code-forge.yml` while the key is on: `block open` and `block claim` refuse it. `block open`, `block claim` and `block close` read the run's config snapshot to find the key, so a snapshot that does not match its hash refuses them with `config-snapshot`, with or without the key. Never set or remove the key yourself: it is the owner's decision.
|
|
8
10
|
|
|
9
11
|
## §2 Depth by risk (adaptive mode)
|
|
@@ -17,11 +19,11 @@ The topology is decided in one place (`planSessions`). Consensus (§6) runs only
|
|
|
17
19
|
|
|
18
20
|
## §3 Isolation and the packet
|
|
19
21
|
|
|
20
|
-
Every reviewer, recheck, judge and S2 session runs with cwd = a fresh empty temp dir under the run's temp root and the packet **on stdin** — a no-tools session never opens a path and the packet never enters argv. Packet order is fixed so provider prompt caching applies: lens, project rules digest (≤ 60 lines), facts-sheet excerpt for the symbols in the diff, context, diff. 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.
|
|
22
|
+
Every reviewer, recheck, judge and S2 session runs with cwd = a fresh empty temp dir under the run's temp root and the packet **on stdin** — a no-tools session never opens a path and the packet never enters argv. Packet order is fixed so provider prompt caching applies: lens, project rules digest (≤ 60 lines), facts-sheet excerpt for the symbols in the diff, context, diff. With a `context` key in the config (B63a, `code.md` §3) the project rules digest is the rules files' text and then, under a `### lessons` subheading, the lessons file, read from the main tree, never from a block worktree, each time a packet is built. `context` is read from the live config; `run reload` accepts a change to it. Rules and lessons share the one 60-line cap and are trimmed together, from the end. Without the key the slot is `(none)`, as before. A rules or lessons file missing, outside the repository or unreadable when the ticket is reviewed (or a bad `context` value) makes the ticket `review.unavailable` with `reason: context-unreadable`: no session runs and its packet directory is removed. The worker does not retry it: the ticket is done with that signed result. `review-file` on the same content is a fresh attempt (`retry: true`) each time it is run, with no cap (no session ran, so the one re-run of a failed packet is not used up); once the file is back, that attempt is reviewed normally. Context: a file at or under `review.context.whole_file_max_lines` travels whole; above it each hunk carries ± `review.context.hunk_context_lines` lines; over `review.budgets.full_in` the context shrinks to ± `review.context.min_context_lines`, then the digest is trimmed, never the diff; a diff alone over budget ⇒ `split_required`, with `tokens_in` and `budget` in the result, its ledger `review.result` row and the `review-file --wait` answer (`stop: split_required`) — except a Markdown file (`.md`, `.markdown`), which is reviewed **section by section**: the diff is cut at its headings (`#`..`######`, not inside fenced code), consecutive sections share a packet while it fits, every section packet runs the file's whole plan with its own stub guard, the file is approved only when every section is, and findings are merged with ids `S<section>.<id>`; a recheck over budget is split the same way, each open finding listed in its own section's packet. A section that alone is over budget keeps the file `split_required` (with that `section`), and so does a deleted Markdown file or one with no heading (fail-safe: one section); nothing is ever truncated. Moved code (B56): code moved between two changed files of the same block is named in a `## moved code` section before the diff (`- base lines a-b removed here were added in <file> lines c-d in this block.` / `- lines c-d were moved here from <file> (its base lines a-b) in this block.`), in round 1, rechecks, patch checks and the judge's packet. Detection is deterministic line matching against the block's other changed owned files diffed against the same base: lines trimmed with whitespace collapsed, lines with fewer than 4 letters or digits ignored, a moved stretch needs ≥ 3 such lines or ≥ 120 such characters; at most 12 ranges are listed; a Markdown section packet lists only the moves under its own headings (none ⇒ no section); the peers are listed and read once per review (one `git ls-tree <base>`, one `git diff <base>` over the peers that exist in the base — a peer deleted in the block, staged or not, is all removed lines — one `git check-ignore` and one `git diff --no-index` per new peer; `a/`/`b/` prefixes forced, `core.quotePath=false`; a peer name with a control, bidi or separator character, a quote or a backslash is skipped; a peer whose diff is over 2 MiB is skipped, never buffered past that); a removed block cut right above a heading belongs to the section holding its removed hunk; a move inside one file is not detected. It is extra text counted in the packet budget: every hunk stays, `reviewed_hunks` is unchanged, and a removal listed there is not a defect by itself — only stale references or imports are. The closed-book rule is in every lens: judge only what is in the packet; a file you cannot see goes in `needs_file`, and the worker attaches it (read-only, trimmed) for **one** more round.
|
|
21
23
|
|
|
22
24
|
## §4 The stub guard — what counts as a review (R11)
|
|
23
25
|
|
|
24
|
-
A result is a review only if the process exited 0, the JSON validates against the finding schema, `reviewed_hunks` equals the packet's hunk headers exactly and in order, and the output has at least `review.min_tokens_out` tokens (a clean pass — `passed: true`, no findings, a non-blank summary — needs only 40, whatever the diff size). Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`, `budget` — the run reached `budget.usd`, `ledger.md` §3.1) and **is not approval**; `review-file` on the same content re-runs the same packet once, then the file stays unreviewed and the block cannot close. Every `schema` miss leaves a signed `review.schema_invalid` row with a structural diagnosis of the answer — its size and kind (`json`, `not-json`, `empty`), its top-level keys and their types, and the schema errors (path, keyword, message), or the parse error and its byte offset — never the answer's text. At a packet's **second** `schema` miss (same run, file, content hash, packet text, level and role — the first try plus that re-run), the worker tries the packet ONCE on `review.second_levels.<level>` when the owner configured one, and writes a signed `review.schema_fallback {lens, role, level, packet_hash, from_provider, from_model, to, to_provider, to_model, attempts, refused?}` row first (no row written ⇒ no try; any earlier row for the packet at that level ⇒ no try). Its first model that is not the failed one — and, for a closed-book role, not Codex unless `review.allow_open_book_codex` — runs (`refused: same-model` or `refused: closed-book` when there is none). The session's tokens are both sessions'. Its answer goes through the same stub guard; a valid one is a normal answer, a failed one leaves the file unavailable. Never in consensus mode; `budget.usd` refuses it like any session. Without a second level only the diagnosis row is new. A session that hangs is killed at `review.session_timeout_s` (default 300), process group and all, and is spawned once more on the same packet and level; only a second timeout is `unavailable: timeout`. Each timed-out attempt leaves a `review.session_timeout` ledger note with the last 4 KB of its stderr (redacted, never the packet) and only the byte count and last event types of its stdout.
|
|
26
|
+
A result is a review only if the process exited 0, the JSON validates against the finding schema, `reviewed_hunks` equals the packet's hunk headers exactly and in order, and the output has at least `review.min_tokens_out` tokens (a clean pass — `passed: true`, no findings, a non-blank summary — needs only 40, whatever the diff size). Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`, `budget` — the run reached `budget.usd`, `ledger.md` §3.1; `context-unreadable`, before any session, is retried differently: see §3) and **is not approval**; `review-file` on the same content re-runs the same packet once, then the file stays unreviewed and the block cannot close. Every `schema` miss leaves a signed `review.schema_invalid` row with a structural diagnosis of the answer — its size and kind (`json`, `not-json`, `empty`), its top-level keys and their types, and the schema errors (path, keyword, message), or the parse error and its byte offset — never the answer's text. At a packet's **second** `schema` miss (same run, file, content hash, packet text, level and role — the first try plus that re-run), the worker tries the packet ONCE on `review.second_levels.<level>` when the owner configured one, and writes a signed `review.schema_fallback {lens, role, level, packet_hash, from_provider, from_model, to, to_provider, to_model, attempts, refused?}` row first (no row written ⇒ no try; any earlier row for the packet at that level ⇒ no try). Its first model that is not the failed one — and, for a closed-book role, not Codex unless `review.allow_open_book_codex` — runs (`refused: same-model` or `refused: closed-book` when there is none). The session's tokens are both sessions'. Its answer goes through the same stub guard; a valid one is a normal answer, a failed one leaves the file unavailable. Never in consensus mode; `budget.usd` refuses it like any session. Without a second level only the diagnosis row is new. A session that hangs is killed at `review.session_timeout_s` (default 300), process group and all, and is spawned once more on the same packet and level; only a second timeout is `unavailable: timeout`. Each timed-out attempt leaves a `review.session_timeout` ledger note with the last 4 KB of its stderr (redacted, never the packet) and only the byte count and last event types of its stdout.
|
|
25
27
|
|
|
26
28
|
## §5 The convergence rule — bounded fix rounds (R15)
|
|
27
29
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## §2 The forbidden list — one list, rendered per CLI at spawn, grepped from every transcript
|
|
8
8
|
|
|
9
|
-
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload` and `block waive` from a coder (also via `npx`) · any write under `.code-forge/reviews/` or `.code-forge/queue/*.done`.
|
|
9
|
+
`gh pr ready|merge|close` and base retargets · `git push --force*` · `git reset --hard` · `git checkout --` · `git restore` · `git clean` · `git stash` · `git branch -D` · `git rm` · `rm -rf` outside `.code-forge/` · any argv containing a `production.markers` token or a `production.names` name · any read or write of `~/.code-forge/runs/` · `forge worker`, `run start`, `run reload` and `block waive` from a coder (also via `npx`) · `--remove-worktree` from a coder (B62a) · any write under `.code-forge/reviews/` or `.code-forge/queue/*.done`.
|
|
10
10
|
|
|
11
11
|
Rendered into every coder spawn as the CLI's own deny rules (path rules widen a directory to `<dir>/**`; a read-write path denies read, edit and write); carried verbatim in the brief; refused by the package's own process spawner; and at block close the transcript (captured stdout, the harness log, or the Solo output) is grepped for the forbidden verbs **and paths** — a hit is a `rule_break` row that fails the gate. Where a CLI cannot load a rules file, `doctor` says so and the transcript grep remains the detector.
|
|
12
12
|
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.
|
|
13
|
+
PINNED_VERSION="0.8.0"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|
|
@@ -19,6 +19,19 @@ After you finish each file in `owned_files` (tests written and green) run `forge
|
|
|
19
19
|
|
|
20
20
|
## Final report — FAILED unless it lists the ticket ids
|
|
21
21
|
|
|
22
|
+
Run the block's test command (`test_command`) and print its result on a line of its own, exactly:
|
|
23
|
+
|
|
24
|
+
`TESTS <command> exit=<n>`
|
|
25
|
+
|
|
26
|
+
`<command>` is the command you ran (not empty), `<n>` its exit code as a decimal 0–255 with no leading zeros (`exit=0`, never `exit=00`). Print it just before the sentinel; the last `TESTS` line after your previous sentinel and before this one is the one that counts, and a line inside prose does not count. When the run has `context.require_coder_tests`, `forge block close <id> --run <r> --report <file>` refuses a report without it or with an exit that is not 0 — the full list of refusals is in `references/code.md` §3 (the close check). For example:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
TESTS npm test exit=0
|
|
30
|
+
===BLOCK B7 COMPLETE===
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `TESTS` line stays ABOVE the sentinel; only the test summary comes after it.
|
|
34
|
+
|
|
22
35
|
End with `===BLOCK <id> COMPLETE===`, the test summary and `git diff --stat`, then one line per changed file:
|
|
23
36
|
|
|
24
37
|
`reviewed <path> ticket <ticket-id>`
|
|
@@ -29,7 +42,11 @@ Or `===BLOCK <id> FAILED: <reason>===`.
|
|
|
29
42
|
|
|
30
43
|
## Fix-round brief (round ≥ 2, or a respawn after a climb)
|
|
31
44
|
|
|
32
|
-
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. The final report follows the same rule: one `reviewed <path> ticket <ticket-id>` line per file changed in the round, or the round counts as FAILED.
|
|
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. The final report follows the same rule: one `reviewed <path> ticket <ticket-id>` line per file changed in the round, or the round counts as FAILED. 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.
|
|
46
|
+
|
|
47
|
+
## Context pack
|
|
48
|
+
|
|
49
|
+
<!-- the block's context pack, pasted as text: the block header (run, block, level, owned and test files, worktree), the project's build, test, lint and format commands, the rules files and the lessons file. `forge context pack --run <r> --block <id>` writes it; paste its contents, never its run-root path (`references/code.md` §3). Without a `context` key, leave this section out. -->
|
|
33
50
|
|
|
34
51
|
## Project rules
|
|
35
52
|
|
|
@@ -104,6 +104,18 @@ const isDelegateSession = (row) => row.event === 'session' && row.role === 'dele
|
|
|
104
104
|
/** @param {Row} row */
|
|
105
105
|
const inLog = (row) => (typeof row.event === 'string' && (row.event.startsWith('autopilot.') || EXTRA_EVENTS.includes(row.event))) || (row.event === 'review.waived' && row.by === 'autopilot') || isDelegateSession(row);
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* B60: what a `gate.done` row says about its tests — the related tests and their count, the full
|
|
109
|
+
* suite and why, or nothing for a gate run without the `{test_files}` item.
|
|
110
|
+
* @param {Row} row @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function gateScope(row) {
|
|
113
|
+
if (row.scope === 'related') return Number.isInteger(row.test_count) ? ` · related tests (${row.test_count})` : ' · related tests';
|
|
114
|
+
if (row.scope !== 'full') return '';
|
|
115
|
+
const why = row.reason === 'threshold' ? 'the block owns more files than the threshold' : row.reason === 'no_tests' ? 'no test file known' : null;
|
|
116
|
+
return why ? ` · full suite (${why})` : ' · full suite';
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
/**
|
|
108
120
|
* Event label and fixed-format detail of one row (never the packet, question or answer text).
|
|
109
121
|
* @param {Row} row @returns {{event: string, detail: string, link: string | null}}
|
|
@@ -159,7 +171,7 @@ function describe(row) {
|
|
|
159
171
|
case 'gate.red':
|
|
160
172
|
return { event: 'Gate red', detail: `${row.gate ?? 'gate'} · exit ${row.code ?? 'unknown'}`, link: null };
|
|
161
173
|
case 'gate.done':
|
|
162
|
-
return { event: 'Gates finished', detail: `${row.stack ?? 'stack'} · ${row.all_ok === true ? 'all green' : 'not all green'}`, link: null };
|
|
174
|
+
return { event: 'Gates finished', detail: `${row.stack ?? 'stack'} · ${row.all_ok === true ? 'all green' : 'not all green'}${gateScope(row)}`, link: null };
|
|
163
175
|
default:
|
|
164
176
|
return { event: String(row.event), detail: '', link: null };
|
|
165
177
|
}
|
|
@@ -329,7 +341,13 @@ export function buildBinnacle({ runId, rows, record, now, scrubCtx = scrubCtxOf(
|
|
|
329
341
|
const approved = new Set(all.filter((r) => r.event === 'review.approved').map((r) => `${u(r.block, 'unknown block')}\0${u(r.file, 'unknown file')}`)).size;
|
|
330
342
|
const results = all.filter((r) => r.event === 'review.result').length;
|
|
331
343
|
const prs = [...new Set(all.map((r) => first(r.pr_url, r.pr)).filter(str))];
|
|
332
|
-
|
|
344
|
+
// B60: a gate run on the block's related tests only (`scope: related`) is not the full suite; when
|
|
345
|
+
// related runs came after the last full one, the status says so, so an old full run does not look current
|
|
346
|
+
const gateRuns = all.filter((r) => r.event === 'gate.done');
|
|
347
|
+
const fullSuite = [...gateRuns].reverse().find((r) => r.scope !== 'related');
|
|
348
|
+
const fullSuiteText = !fullSuite
|
|
349
|
+
? gateRuns.length > 0 ? 'not recorded · related tests only' : 'not recorded'
|
|
350
|
+
: `${fullSuite.all_ok === true ? 'green' : 'red'}${gateRuns.at(-1) !== fullSuite ? ` · no full suite since ${u(fullSuite.ts, 'unknown time')}` : ''}`;
|
|
333
351
|
const readFiles = [...new Set([...all.filter((r) => (r.event === 'review.waived' && r.by === 'autopilot') || r.event === 'autopilot.extra_round').map((r) => r.file), ...toOwner.map((r) => r.subject?.file)].filter(str))];
|
|
334
352
|
const baseSha = ids.map((id) => blocksRec[id]?.base_sha).find(str);
|
|
335
353
|
const approx = mine.filter((x) => x.approx).length;
|
|
@@ -342,7 +360,7 @@ export function buildBinnacle({ runId, rows, record, now, scrubCtx = scrubCtxOf(
|
|
|
342
360
|
const status = [
|
|
343
361
|
{ item: 'Result', value: ids.length === 0 ? 'no blocks yet' : `${closedIds.length} of ${ids.length} block${ids.length === 1 ? '' : 's'} closed` },
|
|
344
362
|
{ item: 'Stack/PRs', value: prs.length === 0 ? 'none recorded' : prs.join(', ') },
|
|
345
|
-
{ item: 'Full suite', value:
|
|
363
|
+
{ item: 'Full suite', value: fullSuiteText },
|
|
346
364
|
{ item: 'Reviews', value: `${results} result${results === 1 ? '' : 's'} · ${approved} file${approved === 1 ? '' : 's'} approved · ${waived} waived by autopilot` },
|
|
347
365
|
{ item: 'Files to read', value: readFiles.length === 0 ? 'none' : `${readFiles.slice(0, 5).join(', ')}${readFiles.length > 5 ? ` and ${readFiles.length - 5} more` : ''}` },
|
|
348
366
|
{ item: 'Base', value: short(baseSha) ?? 'unknown' },
|