@codedology/code-forge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/bin/code-forge.mjs +145 -0
  5. package/docs/README.md +37 -0
  6. package/docs/reference/blocks.json +288 -0
  7. package/docs/reference/config.md +162 -0
  8. package/examples/first-use/README.md +11 -0
  9. package/examples/first-use/clamp.acceptance.yml +8 -0
  10. package/examples/first-use/clamp.brief.md +16 -0
  11. package/examples/first-use/coder/src/math.mjs +31 -0
  12. package/examples/first-use/coder/test/clamp.test.mjs +26 -0
  13. package/examples/node-lib/.env.example +3 -0
  14. package/examples/node-lib/.gitattributes +3 -0
  15. package/examples/node-lib/.github/workflows/test.yml +11 -0
  16. package/examples/node-lib/CHANGELOG.md +13 -0
  17. package/examples/node-lib/README.md +17 -0
  18. package/examples/node-lib/gitignore +3 -0
  19. package/examples/node-lib/package.json +19 -0
  20. package/examples/node-lib/src/list.mjs +23 -0
  21. package/examples/node-lib/src/math.mjs +21 -0
  22. package/examples/node-lib/src/strings.mjs +34 -0
  23. package/examples/node-lib/test/list.test.mjs +12 -0
  24. package/examples/node-lib/test/math.test.mjs +13 -0
  25. package/examples/node-lib/test/package.test.mjs +18 -0
  26. package/examples/node-lib/test/strings.test.mjs +28 -0
  27. package/package.json +52 -0
  28. package/schema/code-forge.schema.json +331 -0
  29. package/schema/facts.schema.json +28 -0
  30. package/scripts/first-use.mjs +517 -0
  31. package/scripts/gen-config-doc.mjs +241 -0
  32. package/skill/SKILL.md +79 -0
  33. package/skill/references/adapters/claude-code.md +19 -0
  34. package/skill/references/adapters/solo.md +34 -0
  35. package/skill/references/adapters/subprocess.md +15 -0
  36. package/skill/references/code.md +64 -0
  37. package/skill/references/continuity.md +31 -0
  38. package/skill/references/decisions.md +50 -0
  39. package/skill/references/degraded.md +40 -0
  40. package/skill/references/harden.md +26 -0
  41. package/skill/references/ledger.md +23 -0
  42. package/skill/references/plan.md +43 -0
  43. package/skill/references/proof.md +34 -0
  44. package/skill/references/review.md +42 -0
  45. package/skill/references/rules-core.md +72 -0
  46. package/skill/references/security.md +29 -0
  47. package/skill/scripts/forge +40 -0
  48. package/skill/templates/block-record.md +19 -0
  49. package/skill/templates/facts-sheet.md +22 -0
  50. package/skill/templates/plan.md +53 -0
  51. package/src/cli/author.mjs +87 -0
  52. package/src/cli/block.mjs +262 -0
  53. package/src/cli/doctor.mjs +74 -0
  54. package/src/cli/facts.mjs +124 -0
  55. package/src/cli/gates.mjs +280 -0
  56. package/src/cli/help.mjs +48 -0
  57. package/src/cli/init.mjs +14 -0
  58. package/src/cli/jev.mjs +175 -0
  59. package/src/cli/keys.mjs +170 -0
  60. package/src/cli/ledger.mjs +160 -0
  61. package/src/cli/list.mjs +68 -0
  62. package/src/cli/models.mjs +131 -0
  63. package/src/cli/plan.mjs +70 -0
  64. package/src/cli/proof.mjs +316 -0
  65. package/src/cli/remove.mjs +76 -0
  66. package/src/cli/report.mjs +73 -0
  67. package/src/cli/resolve.mjs +79 -0
  68. package/src/cli/review-file.mjs +152 -0
  69. package/src/cli/run.mjs +159 -0
  70. package/src/cli/s2.mjs +93 -0
  71. package/src/cli/spawn.mjs +104 -0
  72. package/src/cli/upgrade.mjs +103 -0
  73. package/src/cli/validate.mjs +108 -0
  74. package/src/cli/version.mjs +18 -0
  75. package/src/cli/worker.mjs +76 -0
  76. package/src/config/defaults/anthropic.mjs +18 -0
  77. package/src/config/defaults/index.mjs +24 -0
  78. package/src/config/defaults/openai.mjs +16 -0
  79. package/src/config/defaults/xai.mjs +18 -0
  80. package/src/config/known-ids.mjs +171 -0
  81. package/src/config/load.mjs +135 -0
  82. package/src/config/migrate.mjs +46 -0
  83. package/src/config/refresh.mjs +276 -0
  84. package/src/config/schema-compile.mjs +211 -0
  85. package/src/config/schema-paths.mjs +84 -0
  86. package/src/config/secret-patterns.mjs +85 -0
  87. package/src/config/validate.mjs +694 -0
  88. package/src/decide/author-loop.mjs +90 -0
  89. package/src/decide/escalation.mjs +262 -0
  90. package/src/decide/fallback-rules.mjs +172 -0
  91. package/src/decide/jev-client.mjs +216 -0
  92. package/src/decide/questions.mjs +161 -0
  93. package/src/decide/thresholds.mjs +199 -0
  94. package/src/doctor/index.mjs +105 -0
  95. package/src/doctor/local.mjs +162 -0
  96. package/src/doctor/probes.mjs +358 -0
  97. package/src/doctor/rows.mjs +51 -0
  98. package/src/doctor/worker-probe.mjs +125 -0
  99. package/src/engines/alias-map.mjs +78 -0
  100. package/src/engines/builders/claude.mjs +224 -0
  101. package/src/engines/builders/codex.mjs +242 -0
  102. package/src/engines/builders/grok.mjs +132 -0
  103. package/src/engines/builders/index.mjs +35 -0
  104. package/src/engines/builders/validate-params.mjs +34 -0
  105. package/src/engines/codex-home.mjs +230 -0
  106. package/src/engines/isolation.mjs +84 -0
  107. package/src/engines/probe.mjs +116 -0
  108. package/src/engines/provider-cli.mjs +24 -0
  109. package/src/engines/render-rules.mjs +41 -0
  110. package/src/engines/sentinel.mjs +133 -0
  111. package/src/engines/usage-parse.mjs +149 -0
  112. package/src/gates/acceptance.mjs +95 -0
  113. package/src/gates/detect.mjs +187 -0
  114. package/src/gates/invariants.mjs +43 -0
  115. package/src/gates/run.mjs +72 -0
  116. package/src/gates/safe-edit.mjs +266 -0
  117. package/src/gates/scope.mjs +154 -0
  118. package/src/gates/secret-scan.mjs +79 -0
  119. package/src/gates/transcript-grep.mjs +27 -0
  120. package/src/install/agent-env.mjs +76 -0
  121. package/src/install/detect.mjs +121 -0
  122. package/src/install/harnesses.mjs +131 -0
  123. package/src/install/link.mjs +463 -0
  124. package/src/install/wizard/answers.mjs +389 -0
  125. package/src/install/wizard/flags.mjs +248 -0
  126. package/src/install/wizard/project-file.mjs +128 -0
  127. package/src/install/wizard/run.mjs +366 -0
  128. package/src/install/wizard/steps.mjs +201 -0
  129. package/src/keys/backends/constants.mjs +7 -0
  130. package/src/keys/backends/file.mjs +99 -0
  131. package/src/keys/backends/keychain.mjs +81 -0
  132. package/src/keys/backends/security-cli.mjs +105 -0
  133. package/src/keys/onepassword.mjs +46 -0
  134. package/src/keys/store.mjs +347 -0
  135. package/src/ledger/calibration.mjs +109 -0
  136. package/src/ledger/export.mjs +52 -0
  137. package/src/ledger/outcome.mjs +397 -0
  138. package/src/ledger/paths.mjs +30 -0
  139. package/src/ledger/prices.mjs +49 -0
  140. package/src/ledger/report.mjs +250 -0
  141. package/src/ledger/write.mjs +279 -0
  142. package/src/proof/export.mjs +358 -0
  143. package/src/proof/lock.mjs +138 -0
  144. package/src/proof/red-green.mjs +519 -0
  145. package/src/proof/red-parse.mjs +103 -0
  146. package/src/proof/tiers.mjs +62 -0
  147. package/src/review/budget.mjs +241 -0
  148. package/src/review/context.mjs +156 -0
  149. package/src/review/engine.mjs +264 -0
  150. package/src/review/finding.schema.json +47 -0
  151. package/src/review/fixloop.mjs +467 -0
  152. package/src/review/gate-check.mjs +249 -0
  153. package/src/review/lenses/A.md +22 -0
  154. package/src/review/lenses/B.md +23 -0
  155. package/src/review/lenses/full.md +23 -0
  156. package/src/review/lenses/judge.md +21 -0
  157. package/src/review/lenses/quick.md +21 -0
  158. package/src/review/lenses/recheck.md +23 -0
  159. package/src/review/packet.mjs +355 -0
  160. package/src/review/triage.mjs +149 -0
  161. package/src/review/validate-review.mjs +70 -0
  162. package/src/session/author.mjs +228 -0
  163. package/src/session/facts.mjs +855 -0
  164. package/src/session/plan-check.mjs +294 -0
  165. package/src/session/s2.mjs +164 -0
  166. package/src/session/spawn.mjs +610 -0
  167. package/src/state/block.mjs +289 -0
  168. package/src/state/cli-args.mjs +67 -0
  169. package/src/state/paths.mjs +92 -0
  170. package/src/state/registry.mjs +189 -0
  171. package/src/state/run.mjs +266 -0
  172. package/src/state/signer.mjs +132 -0
  173. package/src/util/exec.mjs +315 -0
  174. package/src/util/forbidden.mjs +787 -0
  175. package/src/util/git.mjs +381 -0
  176. package/src/util/log.mjs +100 -0
  177. package/src/util/reaper.mjs +300 -0
  178. package/src/util/redact.mjs +212 -0
  179. package/src/util/tmp.mjs +409 -0
  180. package/src/worker/engine.mjs +338 -0
  181. package/src/worker/loop.mjs +392 -0
  182. package/src/worker/queue.mjs +186 -0
  183. package/src/worker/review-state.mjs +145 -0
  184. package/src/worker/ticket.mjs +195 -0
@@ -0,0 +1,26 @@
1
+ # Harden — the adversary is an L3 session; the human answers; you relay
2
+
3
+ ## §1 The author loop
4
+
5
+ `forge author --job harden --brief <brief> --facts <sheet> --draft <plan> [--answers <file>] --out <plan>` runs a fresh L3 closed-book session over the brief, the facts sheet, the current draft and the answers so far. It returns `{draft, questions: [{id, question, options[], why, blocking}], cost}`.
6
+
7
+ Loop: write the draft to the plan file → print `questions[]` to the human in chat (and to the Solo scratchpad when present) → collect answers into `<answers file>` → re-run with `--draft` and `--answers`. Stop when **no blocking question is left**: `questions[]` is empty, or every remaining item is non-blocking and the human chose to skip it — a skipped question is written into the plan's Q&A as `skipped` with the human's word, never silently dropped. Each round prints its cost. **The human answers; the orchestrator never answers for them.** Under Solo the human may run harden as an interactive L3 session (`harness.solo.interactive_harden`).
8
+
9
+ ## §2 What the L3 session interrogates (so you can judge its questions)
10
+
11
+ - Correctness and data integrity: idempotency (can it run twice?), rounding and units for money-like values, timezone and date handling, migration reversibility, backfill for existing rows, races between blocks at runtime.
12
+ - Authorization and tenancy: every new surface behind the project's policy pattern; cross-tenant and cross-user leak paths.
13
+ - Performance and safety: N+1s, heavy work queued, retry and backoff, rate limits on new public endpoints.
14
+ - Testing and rollout: happy, failure and weird paths named per block; permission-denial tests; a flag or backout plan where state changes are risky.
15
+ - Decomposition audit: `owned_files` truly disjoint, `depends_on` acyclic, every block independently testable, every clause mapped (`plan.md` §3), forecasts within the split rule.
16
+ - Benchmark: how comparable products solve the core mechanic — concrete adoptable gaps, not "best practices".
17
+
18
+ ## §3 The tranche gate — a plan is spent when its blocks are
19
+
20
+ Never roll from one tranche into the next without an architect pass. Risk class is not size: a tranche of pure computation says nothing about the next one's migrations, money or historical data. Hard stops: the plan's blocks are delivered; the next work touches **schema, money, auth/tenancy or historical data** when the current one did not; candidates are tagged planned-not-hardened; or several waves shipped mechanism nothing calls (ask *adoption or more mechanism?*).
21
+
22
+ Before a coder is spawned the architect answers in writing: which block and why; the risk class and what the plan must therefore carry (down-safety, backfill strategy, idempotency, a dry-run path, proof of correctness without touching production); size, shape, owned paths, split rule; and what would make the block unreviewable, prevented up front. The S1 `security_sensitive` answer and the `proof.tiers.high.paths` floors raise the first-attempt level automatically (`decisions.md` §5); the tranche gate is the human judgement on top.
23
+
24
+ ## §4 Exit
25
+
26
+ `forge plan check <plan>` green (`plan.md` §4) and no blocking question left (skipped non-blocking ones recorded as `skipped` in the Q&A, §1) ⇒ set `status: hardened` in the plan header, append the Q&A and the risks table to the plan, and say: *next: `/code-forge code <plan>`*.
@@ -0,0 +1,23 @@
1
+ # Ledger — `~/.code-forge/ledger/<slug>.jsonl`, append-only, never committed
2
+
3
+ ## §1 What a row carries
4
+
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
+
7
+ Events you will read most: `run.start`, `run.stop {reason}`, `run.end`, `dispatch`, `gate.red`, `review.plan`, `review.round`, `review.done`, `review.approved`, `review.unavailable {reason}`, `review.late_finding`, `review.waived {by: human}`, `review.budget`, `forecast.cases_exceeded`, `proof`, `outcome`, `block.rebase`, `block.orphan`, `ledger.tamper`, `worker.replaced`, `facts.built`, `tmp.swept`.
8
+
9
+ ## §2 `forge report --slug <slug> [--json] [--export <dir>]`
10
+
11
+ Terminal output in this version. Sections: cost per completed block (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`. `--export <dir>` writes one JSON file per section — the durable dataset a future UI reads.
12
+
13
+ ## §3 Live views
14
+
15
+ `forge ledger tail --slug <slug> [--n <count>]` prints the last rows — the orchestrator's live view when no Solo process list exists. `forge run status --run <id>` prints the run record: active blocks, orphans, whether the worker is pinned.
16
+
17
+ ## §4 Calibration — `forge ledger calibration --slug <slug> [--question <id>]`
18
+
19
+ 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.
20
+
21
+ ## §5 Durability
22
+
23
+ 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, so a run from a sub-directory still lands in the same file.
@@ -0,0 +1,43 @@
1
+ # Plan — facts first, then an L3 draft, then a deterministic check
2
+
3
+ ## §0 Facts before design (step 0 of every plan)
4
+
5
+ Run `forge facts --brief <brief> [--sources <path>...] --out <plans_dir>/<slug>.facts.md` before anything else. What it does:
6
+
7
+ 1. Deterministic: the CLI extracts every *claim token* from the brief and the sources it names — CLI flags, `<cli> <verb>` commands, paths, versions, endpoints, package names, environment variable names — and writes a claim list.
8
+ 2. One L0 session whose cwd is a **read-only snapshot of the repository at HEAD plus the `--sources` files**, built under the run's temp root and removed at session end — so `./path` claims are checkable, while **uncommitted files are not visible unless named as sources** (the snapshot is HEAD, not the working tree) — and whose packet is the claim list and the rule "run the cheapest read-only check, quote the command and the output line, tag VERIFIED / NOT-FOUND / UNVERIFIABLE, never infer". Two lines of defence, exactly as built. **First, at spawn:** the session gets the shell tool only, with the forbidden list plus every write verb rendered as deny rules and the provider's restricted mode (file tools confined to the working directory). There is no pre-execution gate on individual commands beyond those deny rules: the delegate runs what it runs. **Second, after the fact:** the CLI checks every `command` the delegate reports against an allow-list, fail closed, and refuses the whole sheet on the first violation. Allowed forms: `ls cat head tail wc grep rg stat file uname sw_vers jq which` with in-cwd arguments (`grep` never recursive), `find` with only these predicates — `-name -iname -path -ipath -type -maxdepth -mindepth -print -print0 -newer -size -empty -mtime -mmin -not ! -a -o -and -or` — every other predicate refused, `git log|show|status|rev-parse|ls-files|cat-file|--version`, `npm view|ls|--version`, `node --version`, `test -e|-f|-d|-n <one operand>`, `curl` as a HEAD request of exactly one http(s) URL with `-s -S -I -L -f --max-time <N>` only, and `<cli> --help|-h|--version` for a bare CLI name that is not a build runner; an env claim is only `printenv NAME >/dev/null` with its excerpt blanked. Refused anywhere: backticks, `$`, redirection, unquoted globs, `~`, `..`, absolute paths outside `/usr`, `/opt/homebrew`, `/bin`, and secret-looking paths. **`npm view` and `curl -I` reach the network** — they are what makes a package version or an endpoint checkable; nothing else the delegate may run does.
9
+ **Path claims:** `./path` claims resolve against the HEAD snapshot, so a committed file is VERIFIED or NOT-FOUND by a plain `ls`/`test -e`; a file that exists only uncommitted in the working tree reads NOT-FOUND unless you passed it with `--sources`. `~/…` claims stay UNVERIFIABLE (`~` is refused), and absolute paths are allowed only under `/usr`, `/opt/homebrew` and `/bin`; a home-directory path that matters to a block is checked by you with one read-only command and recorded in the unbackable list with that command as the tolerance.
10
+ 3. Deterministic again: the answer is validated against the facts schema, rendered with `templates/facts-sheet.md`, and refused when a VERIFIED tag has no output excerpt. Every claim, command and excerpt passes through the redactor. The header records the brief's sha and mtime.
11
+
12
+ **Do not read the sources yourself; read the sheet.** Your context is spent on design, never on `--help` output. A fact is never a model's opinion: every claim carries the command that produced it.
13
+
14
+ ## §1 The author job
15
+
16
+ `forge author --job plan --brief <brief> --facts <sheet> --out <plan>` runs a fresh L3 closed-book session (empty cwd, the packet on stdin) and returns `{draft, questions[], cost}`. It refuses without `--facts` (exit 2, `facts sheet required: run forge facts first`) and refuses a sheet older than the brief. The draft follows `templates/plan.md`; its §0 **is** the facts sheet, verbatim, and it must contain the section "Acceptance clauses the facts sheet cannot back" — `none` when empty, never absent.
17
+
18
+ ## §2 Decomposition — what every block carries
19
+
20
+ | field | rule |
21
+ |---|---|
22
+ | `id`, `title` | one deliverable per block |
23
+ | `owned_files` | exact paths or disjoint globs; a glob never contains `[ ] ( ) ! + @` (an exact path may); pairwise disjoint across blocks that can run together |
24
+ | `depends_on` | acyclic; a block imports only modules of blocks it depends on — a batch-mate's module is a `next = stop` |
25
+ | `level` | the S1 `lane` answer (`L0` trivial · `L1` a plain feature following a pattern · `L2` money, dates, tenancy, migrations, concurrency · `split` too large or two concerns); **L3 is not a lane** |
26
+ | `acceptance` | clauses that each name ≥ 1 test id; a clause citing a flag, path, version or API cites a VERIFIED fact id or sits in the unbackable list with a tolerance naming this block |
27
+ | `cases`, `lines` | forecasts: `cases = 2 × counted clauses`, `lines = 26 × cases` with a floor of 1 200 for a greenfield block and 500 for an amendment; above `budget.block_cases` or `budget.block_lines` the block is split in the plan, never "split-ready" |
28
+ | `test_command` | a quoted glob or an explicit file list, never a bare directory |
29
+ | `commit_message` | house style |
30
+
31
+ A file everyone needs (a route file, an enum, a registry) is a serialization point: one early block owns it, the others depend on it. Never more than `caps.coders` blocks are marked concurrent in one dispatch slot.
32
+
33
+ ## §3 A mechanism needs a caller (R8)
34
+
35
+ Every clause of the parent's acceptance maps to exactly one block; an unmapped clause keeps the parent open whatever the sub-blocks report. A block that ships a mechanism is not shipped until a caller block is dispatched or deferred with a named id. The plan carries a **caller map** (mechanism → block that invokes it); `plan check` refuses a block table whose id is missing from it.
36
+
37
+ ## §4 The deterministic exit — `forge plan check <plan> [--facts <sheet>]`
38
+
39
+ Prints every failing row by name and exits 1 on any of: an acceptance clause with a claim token that resolves to no VERIFIED fact and no tolerance row (`unbackable clause without tolerance: <block> <clause>`); a missing caller map or a block absent from it; a block without `cases` and `lines`; a forecast above `budget.block_cases` / `budget.block_lines` without a named split; overlapping `owned_files` in one wave; a cyclic `depends_on` or an import of a batch-mate; a glob with `[ ] ( ) ! + @`; more than `caps.coders` concurrent blocks in a slot; `questions[]` still open; the facts sheet's sha absent from the draft. Green ⇒ set `status: hardened` after the harden job (`harden.md`), never before.
40
+
41
+ ## §5 Cost
42
+
43
+ One L0 session per plan for the facts (typically a claim list of 40–80 items) and one L3 session per author round; `forge report --slug <slug>` shows both as `facts $` and coder/review splits.
@@ -0,0 +1,34 @@
1
+ # Proof — two tiers, red→green, the measurement export
2
+
3
+ ## §1 Tiers and what the gate enforces (R16)
4
+
5
+ Tier per file = `high` when `risk ≥ 2`, or a `proof.tiers.high.paths` match, or `security_sensitive`; else `light`. `forge proof tier --file <path> --risk <0-3> [--security] [--cwd <dir>]` prints it; it is decided at `review-file` time, recorded, and the block gate enforces the union.
6
+
7
+ | tier | proof required | when |
8
+ |---|---|---|
9
+ | light | red→green recommended for every new or changed test; **optional at the gate** | the coder or orchestrator runs the runner per test (§2) |
10
+ | high | red→green **required at the gate** for every changed high-tier file, plus the deeper **review** depth of `review.md` §2 (two lenses and a judge) | as above; `block close` refuses `unproven <file>` |
11
+
12
+ **`forge block close` enforces the high tier:** tier inputs are `proof.tiers.high.paths` (the union of the config at the block's base and the current one), the recorded `risk`, and `security_sensitive`; a row naming the file whose MAC fails makes it high. Each changed high-tier file needs a signed `proof` row of this run and block with `step: red-green`, `proven: true`, `red_kind: assertion` whose `covers` list names the file (`revert` covers the sources it put back; `assertion-deletion` covers only its test), else `unproven <file>`; only the human's `forge block waive <id> proof --file <path>` clears it.
13
+
14
+ **Mutation testing is not part of this product.** The question whether tool-made mutants on changed lines (Infection, Stryker) should be a user-facing proof tier was decided: **cut**. There is no `proof mutate`, no nightly mutation run, no `min_msi` floor, and no mutation-tool key in `.code-forge.yml`; a consumer that runs a mutation tool does so outside code-forge and never beside a coder's test run in the same tree. What replaces it as proof is the rule itself: one exact test per acceptance clause plus one negative or boundary where the clause has a failure mode — assertions that say **which** element and **how many**, never merely whether something exists — and the isolated per-file review with the convergence rule, which on this package's own build found the defects green suites had missed.
15
+
16
+ `proof.extra` (an argv array) keeps a consumer's legacy proof commands running inside the gate.
17
+
18
+ ## §2 Red→green — journaled, killable, RED must mean an assertion
19
+
20
+ **One call proves one test** — a test file, or one case of it (`<file>::<case>` is the label) — with one of two mechanisms: `revert` (the block's changed source files are put back to their base version, so the test must fail against the code it was written for) or `assertion-deletion` (a characterization test, proven by deleting its assertions). The runner does not distinguish a new test from a changed one: the caller runs it once per test the acceptance names, new or changed alike, and the count of `RED` rows is the count of proven tests — there is no separate "new cases" tally. In this version the runner is invoked per test from the coder's or the orchestrator's side; the block gate reads the resulting signed `proof` rows but does not launch the runner itself.
21
+
22
+ The runner works in the export (§3) or under the lock with a journal: it refuses when a journal already exists (an earlier run was interrupted) → journals every source file the test covers (bytes, sha256, mode — or `existed: false`) → writes the **base** version of each (`git show <base>:<path>`, never `stash` or `checkout --`); **a file the block added has no base version, so it is removed for the red run** → runs the filtered tests → **parses** the output: an assertion failure ⇒ `RED`; a fatal, compile or import error ⇒ `RED_INVALID`, which proves nothing (a removed module that the test imports is the usual cause — such a test needs an assertion that fails against the base, not an import that crashes) → restores every journaled file byte for byte, **verifies each sha256**, deletes the journal (in a `finally`, so an ordinary error restores too) → when the red step was `RED`, runs again ⇒ `GREEN`. A characterization test (`proof: characterization`) is proven by assertion deletion. The ledger `proof` row records `mechanism` and `red_kind`; one `RED` + `GREEN` pair per proven test. SIGTERM mid-run leaves the journal; `forge proof restore <block> --run <r>` restores the exact hashes and refuses when no journal exists.
23
+
24
+ ## §3 The measurement export (R13) — `proof.isolation: export`, the default
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.
27
+
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
+
30
+ Test-count delta = 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.
31
+
32
+ ## §4 Verified and unverified stacks
33
+
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`). A PHP/Pest or Vitest stack is **UNVERIFIED** until a real second consumer runs it; `proof.isolation: lock` is the documented fallback for those until then. `doctor` says which gate commands are on PATH.
@@ -0,0 +1,42 @@
1
+ # Review — per file, fresh L2 session, bounded rounds
2
+
3
+ ## §1 The trigger is a CLI call, and the file set is complete
4
+
5
+ `forge review-file <path> --block <id> --run <r>` enqueues a ticket and returns `{ticket, status: queued}` in under a second; `forge review-file --wait <ticket> [--max 90s]` polls it. The worker computes the diff itself against the base sha `forge block open` recorded: tracked changes plus untracked files, filtered by `owned_files` for the review and unfiltered for the scope check. A coder that never calls `review-file` gains nothing: the block gate refuses any changed or new file without a signed `review.approved` row for its final content hash. The reviewer is always a new OS process (R1, R10) — identical under Solo, a harness subagent, a detached CLI process, or a human typing.
6
+
7
+ ## §2 Depth by risk (adaptive mode, `review.multimodel` off)
8
+
9
+ 1. Tools on the file first: lint, types, the file's own tests. Red ⇒ `tools_red` back to the coder, no model review, ledger `gate.red`.
10
+ 2. S1 `risk` (path floors first; `confidence < check` ⇒ S2 picks the depth).
11
+ 3. Planned depth: `risk < 1` ⇒ one L2 `quick` · `1 ≤ risk < 2` ⇒ one L2 `full` · `risk ≥ 2` ⇒ two blind L2 sessions (lens A correctness, lens B contracts and security) plus an L3 judge that reads the two reports — and the diff only when `review.judge_sees_diff` is true.
12
+ 4. Triage: findings from a single L2 session go to S1 `defect` (`thresholds.findings.fix` fix now · mid-band batched to one L3 ruling per file · below `thresholds.findings.nit` a nit). Findings that come from a judge are final.
13
+
14
+ ## §3 Isolation and the packet
15
+
16
+ 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`. 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.
17
+
18
+ ## §4 The stub guard — what counts as a review (R11)
19
+
20
+ 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. Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`) and **is not approval**; the worker retries once on the level's fallback, then the file stays unreviewed and the block cannot close. A session that hangs is killed at `review.session_timeout_s`, process group and all.
21
+
22
+ ## §5 The convergence rule — bounded fix rounds (R15)
23
+
24
+ Per file, per block:
25
+
26
+ 1. Round 1 is the tiered review of §2 on the full packet.
27
+ 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).
28
+ 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.
29
+ 4. **Ladder**: the running ceiling is `min(L2, escalation.stop_at)` — L3 is never a *running* level. A trigger — rule 1 (`retries`), rule 2 (`escalation.review_rounds_per_level` exhausted with an open `fix_now`) or rule 2b (`review_stall`) — below the ceiling climbs one level, coded by a fresh session with the open findings and the fix history in its brief (an L1 block's rounds 3–4 run at L2). At the ceiling — an L2 block, or an L1 block that has climbed to L2 — it takes the **L3 rung**: a patch ≤ 80 lines, owned files only, reviewed as a `patch_check` round outside the cap. Under `escalation.l3_mode: code` (opt-in) the rung is instead one L3 coding turn. Either way the rung fires **once per block** and the same post-patch order follows it. After it, in order: `patch_check` clean ⇒ complete; open set not smaller than before the patch ⇒ `stopped: l3_patch_exhausted` (`review_stall`); round ≥ `review.max_rounds_per_file` ⇒ `stopped: review_cap`; otherwise the fixes continue at L2 with `rounds_at_level` reset and the patch in the brief. Any later trigger in that block stops with its named reason (`l3_patch_exhausted`, `review_cap`) and never patches again.
30
+ 5. **Cap — four rounds per file** (`review.max_rounds_per_file`, default 4, range 2–6). Round 4 still open ⇒ the L3 rung if the block has not used it (same post-patch order as step 4), else `stopped: review_cap`. A stop means the block cannot close, the ledger lists the open findings, and the human decides — fix by hand, `forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>` (a signed `review.waived` row, `by: human`), or re-decompose. The coder never sees a `waive` verb; it is on the forbidden list.
31
+ 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.
32
+ 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.
33
+
34
+ Rechecks are never degraded by the budget: a round-2+ packet is already the minimum.
35
+
36
+ ## §6 Consensus mode (`review.multimodel` on)
37
+
38
+ Two L2 reviewers, one per effective provider (`resolve(L2)` and `review.second_levels`), each a fresh blind session with the `full` lens; then a fresh L3 judge **from the third provider** reads the two reports and returns the findings plus `agreement: both|A-only|B-only`. The judge's `fix_now` list is final. Second provider unavailable at run start ⇒ `consensus unavailable: <reason>`; ask the human once per run whether to continue adaptive, stop when non-interactive. Rechecks run on the provider that raised the finding; §5 applies unchanged.
39
+
40
+ ## §7 The review budget per block
41
+
42
+ `review.block_budget_tokens` (and optionally `review.block_budget_usd`) bounds every review, recheck, judge and S2 session made for the block. When the forecast exceeds it the worker degrades one step at a time, cheapest first: batch small files of one tier into one packet (`review.batch_small_files`) → one judge per block instead of per file → standard band `full` → `quick` → high tier `A + B + judge` → one `full` L2 + judge, the floor. Tools-only is never a review; a light `quick` stays `quick`. Still over ⇒ `review.over_budget`: raise the budget once per run (a config edit by the human, on record) or split the block — never silently, never below the floors. The ledger keeps `depth_unconstrained` and `depth_chosen` per file so `forge report` can show `missed_after_degrade`.
@@ -0,0 +1,72 @@
1
+ # Core rules — sixteen, each with its why
2
+
3
+ The headings below are the list in `SKILL.md` §4. A rule names levels and roles, never a model. Config keys are the ones `forge validate` accepts.
4
+
5
+ ### R1 — Coder ≠ reviewer, always
6
+ Every `forge review-file` call starts a new L2 session in an empty directory; the session that wrote a file never judges it. Why: a same-session review reads its own intent, not the diff.
7
+
8
+ ### R2 — The orchestrator never codes, reviews or judges
9
+ Plans and patches are L3 sessions (`forge author`, the L3 rung); code is a coder's; verdicts come from the worker's sessions. Why: the orchestrator's context is the scarce resource, and a hand-written "surgical fix" is unreviewed code.
10
+
11
+ ### R3 — State lives outside the conversation
12
+ The run record, the ledger, the plan file, then Solo scratchpad/kv when present. Why: a session dies mid-run; anything only in its context is lost (`continuity.md` §1).
13
+
14
+ ### R4 — The evidence gate is a re-measurement
15
+ A coder's report is a claim sheet; `forge block close` recomputes every row itself (`code.md` §5). Why: green assertions pass for wrong reasons; a quoted number is not a number.
16
+
17
+ ### R5 — No destructive git, no production, no regex edits
18
+ The forbidden list (`security.md` §2) is rendered into every spawn and grepped from every transcript; code is restructured by hand; a check's output is read, never discarded. Why: each entry names a real incident.
19
+
20
+ ### R6 — The budget stop is the feature
21
+ A coder that forecasts, overruns and stops did the right thing; the cap moves once per run, by the human, on record (`code.md` §6). Why: a rule that never fires protects nothing.
22
+
23
+ ### R7 — Wire truncation is detectable
24
+ The wire carries a pointer ≤ 200 B ending in `<<<EOM>>>`; the coder echoes `ACK <sha8> lines=<n>` before any tool call; no ACK ⇒ resend once, then park. Why: an input channel truncated a message to its last 240 bytes, silently.
25
+
26
+ ### R8 — A mechanism needs a caller
27
+ Every acceptance clause of a parent maps to exactly one block; a block that ships a mechanism names the block that calls it or is not shipped (`plan.md` §3). Why: sub-blocks marked done while the parent's acceptance was unmet.
28
+
29
+ ### R9 — Own-family adjudication is visible and asymmetric
30
+ When the orchestrator shares a family with the coder it may accept a finding alone but never dismiss, defer or waive one; every such call is a named ledger row (`decisions.md` §5). Why: a judgement inside the family is not independent.
31
+
32
+ ### R10 — A fresh isolated session per job
33
+ Reviewer, recheck, judge, S2, author and facts each run as a new print-mode process with an empty cwd and the packet on stdin, whatever the engine. Why: isolation is what makes the harness irrelevant.
34
+
35
+ ### R11 — A review without matching `reviewed_hunks` is not a review
36
+ Exit 0, a valid schema, `reviewed_hunks` equal to the packet's hunk headers in order, and enough output; anything else is `review.unavailable`, never approval. Why: three reviewers returned stubs on a green gate that hid a money bug.
37
+
38
+ ### R12 — The L3 rung writes a patch, never a feature
39
+ On escalation past L2 an L3 session returns a diff ≤ 80 lines inside the owned files, reviewed as a `patch_check` round; the block then continues at L2. Why: bulk coding at L3 is the cost sink the ladder exists to avoid.
40
+
41
+ ### R13 — Measurement runs in an export or behind the proof lock
42
+ Gates, test-count deltas and red→green run in `.code-forge/export/<block>/`, never in the shared tree while other blocks are active (`proof.md` §3). Why: parallel blocks in one tree cannot be measured apart.
43
+
44
+ ### R14 — No design before facts
45
+ `forge author --job plan` refuses without `--facts`; every clause that cites a flag, path, version or API cites a VERIFIED fact or sits in the unbackable list with a tolerance (`plan.md` §0). Why: a plan built a reviewer on a flag that did not exist.
46
+
47
+ ### R15 — Re-checks shrink: fix hunks only, four rounds, then a patch, then a human
48
+ Round n ≥ 2 reviews the fix hunks and the open findings; the open set shrinks every round or the coder climbs; `review.max_rounds_per_file` rounds, then the L3 rung once, then `stopped: review_cap` (`review.md` §5). Why: whole-file re-checks added new defects every round.
49
+
50
+ ### R16 — Proof is exact assertions on the main behaviour
51
+ One exact test per clause plus a negative or boundary where the clause has a failure mode; red→green for every new test; there is no mutation tier (`proof.md` §1). Why: exhaustive suites hid the defects the isolated review found.
52
+
53
+ ## What survives from the previous pipeline (14 rows)
54
+
55
+ | survivor | lands in | kind |
56
+ |---|---|---|
57
+ | Coder ≠ reviewer session | `rules-core.md` R1; `review.md` §1 | core rule + mechanism |
58
+ | State outside the conversation | `rules-core.md` R3; `continuity.md` §1 | core rule |
59
+ | Launch verification + completion sentinels | `adapters/solo.md` §3; `adapters/subprocess.md` §2 | mechanism |
60
+ | Evidence gate = re-measurement | `rules-core.md` R4; `code.md` §5 | core rule + mechanism |
61
+ | Wire truncation detection | `rules-core.md` R7; `adapters/solo.md` §2 | core rule + mechanism |
62
+ | Facts sheet before dispatch + coder facts diff | `plan.md` §0; `code.md` §1 | core rule |
63
+ | Risk-class gate between tranches | `harden.md` §3 | core rule |
64
+ | Budget caps; the stop is the feature | `rules-core.md` R6; `code.md` §6 | config + core rule |
65
+ | Mechanism needs a caller | `rules-core.md` R8; `plan.md` §3 | core rule |
66
+ | Draft PR only after independent review | `code.md` §7 | config + core rule |
67
+ | Continuity / takeover | `continuity.md` §2 | core rule |
68
+ | Base-moves rules | `continuity.md` §3 | core rule |
69
+ | Definition of Done | `code.md` §8 | core rule |
70
+ | Own-family adjudication visible and asymmetric | `rules-core.md` R9; `decisions.md` §5 | core rule |
71
+
72
+ Working habits folded into the rules above rather than listed: verification chains use `;` not `&&` and arrays for word-splitting; resolve a target before any destructive state command; spawned agents inherit the production ban; test every fix red→green; commit trailers follow `commit.trailers`; one PR ready at a time (`wip.ready_prs`); never close a process this run did not spawn; long input goes to a file plus a pointer; simplest solution first; no duplicate logic; read the source before reviewing it.
@@ -0,0 +1,29 @@
1
+ # Security — keys, the forbidden list, signed rows and where their guarantee ends
2
+
3
+ ## §1 Keys never reach a coder
4
+
5
+ `keys.<name>` in `.code-forge.yml` holds a reference only (`user`, `op://…`, `env:NAME`, `keychain:<name>`), never a value. Resolution runs the chain — environment → OS keychain → 1Password `op read` (cached 8 h) → ask at setup — and runs **only inside the worker or the orchestrator's own CLI calls, never in a coder process**. `forge keys list | set <name> [--op <ref>] | test <name> [--ref <ref>] | remove <name>`; `list` never prints a value. Every output path is redacted: a message names the key path, never the value. Coder sessions receive neither the S1 key nor the signer key in their environment (subprocess env allow-list; Solo agent env likewise); reviewer sessions keep only the provider login variables they need to authenticate.
6
+
7
+ ## §2 The forbidden list — one list, rendered per CLI at spawn, grepped from every transcript
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` and `block waive` from a coder (also via `npx`) · any write under `.code-forge/reviews/` or `.code-forge/queue/*.done`.
10
+
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
+
13
+ ## §3 Signed ledger rows — the threat model, stated honestly
14
+
15
+ **Mechanism.** A per-run HMAC key (32 random bytes) lives in `~/.code-forge/runs/<run>.key`, mode 0600, outside the workspace; `forge run start` generates it, the worker and `forge block close` read it, and it is never placed in a coder's environment or workspace. Gate-relevant rows carry `mac`; a verification failure is a `ledger.tamper` row and the block is `stopped`. Key rotation = a new run.
16
+
17
+ **What it defends against.** A coder that never called `review-file` and appends or edits rows or result files with its ordinary tools — no valid MAC, refused. A coder that edits a review result after the worker wrote it — the MAC no longer matches. A stale approval reused after the file changed — rows are keyed by content hash. A coder that starts its own worker to sign rows — the pinned pid and start time in the run record no longer match, `worker.replaced`, block stopped. A coder that waives its own finding — `block waive` is forbidden and the row requires `by: human` signed by the worker's key.
18
+
19
+ **What it does not defend against, and the product does not claim.** A coder running as the same OS user with a shell can read the key file and mint a MAC, or edit the run record. Within one OS user there is no boundary that a determined bypass-permissions process cannot cross; the deny rules make it a deliberate, named rule break rather than an accident, and the transcript grep makes the attempt visible (`rule_break: key_path`). `doctor` prints `signer: same-user boundary only` on every full run so nobody reads "signed" as "unforgeable".
20
+
21
+ **The real boundary is the consumer's choice:** run coders as a second OS user, or in a container that cannot reach `~/.code-forge/runs/` (a Codex `workspace-write` sandbox already blocks writes there; reads remain open). An in-memory key over a socket was considered and rejected: a same-user process can still replace the worker or edit the pin.
22
+
23
+ ## §4 Isolation of closed-book sessions
24
+
25
+ Reviewer, recheck, judge, S2, author and facts sessions run in print mode with the provider's own isolation flags, cwd = an empty temp dir under the run's temp root, and the packet on stdin; `doctor` proves that no project document is visible (it asks the reviewer builder to echo the first line of the project's rules file and expects nothing) and that the coder builder cannot read a known file outside the workspace (`claude: path deny rules not honoured` when it can). The facts delegate gets read-only shell tools with every write verb denied.
26
+
27
+ ## §5 What the package itself never does
28
+
29
+ No telemetry (`telemetry: off`, reserved). No `postinstall`. No secret in argv — a keychain write goes through stdin. Temp dirs live under one per-run root and are swept by the next `run start`; the reaper kills only processes this package registered whose start time still matches — never a foreign pid. Every child `git` runs with inherited `GIT_*` variables stripped, so a gate invoked from a hook still sees the right repository.
@@ -0,0 +1,40 @@
1
+ #!/bin/sh
2
+ # code-forge skill shim: `forge <verb> [args…]` runs the package's `bin/code-forge.mjs`.
3
+ #
4
+ # Resolution order (first hit wins):
5
+ # 1. $CODE_FORGE_BIN, when set and a file (a checkout or a pinned version);
6
+ # 2. <this skill dir>/../../bin/code-forge.mjs — the skill lives inside the package, and a
7
+ # symlinked install (`code-forge init`, the default) resolves back into it;
8
+ # 3. `code-forge` on PATH (a global npm install);
9
+ # 4. `npx @codedology/code-forge@<version>` pinned to the version this skill was built with (a
10
+ # copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
11
+ # equals package.json's version.
12
+ # The shim holds no logic of its own: every verb, flag and message is the CLI's.
13
+ PINNED_VERSION="0.1.0"
14
+ set -eu
15
+
16
+ self="$0"
17
+ while [ -L "$self" ]; do
18
+ link=$(readlink "$self")
19
+ case "$link" in
20
+ /*) self="$link" ;;
21
+ *) self="$(dirname "$self")/$link" ;;
22
+ esac
23
+ done
24
+ here=$(cd "$(dirname "$self")" && pwd -P)
25
+ packaged="$here/../../bin/code-forge.mjs"
26
+
27
+ if [ -n "${CODE_FORGE_BIN:-}" ] && [ -f "$CODE_FORGE_BIN" ]; then
28
+ exec node "$CODE_FORGE_BIN" "$@"
29
+ fi
30
+ if [ -f "$packaged" ]; then
31
+ exec node "$packaged" "$@"
32
+ fi
33
+ if on_path=$(command -v code-forge 2>/dev/null); then
34
+ exec "$on_path" "$@"
35
+ fi
36
+ if npx_path=$(command -v npx 2>/dev/null); then
37
+ exec "$npx_path" --yes "@codedology/code-forge@$PINNED_VERSION" "$@"
38
+ fi
39
+ echo "forge: no code-forge found. Install it: npm install -g @codedology/code-forge@$PINNED_VERSION (or set CODE_FORGE_BIN=<path>/bin/code-forge.mjs)" >&2
40
+ exit 127
@@ -0,0 +1,19 @@
1
+ ### Block record — `<id>` `<title>`
2
+
3
+ | field | value |
4
+ |---|---|
5
+ | run / block | `<run id>` / `<id>` |
6
+ | level, attempt | `L<n>`, attempt `<n>` (escalations: `<trigger>` → `L<n+1>` …) |
7
+ | base sha | `<sha>` (rebased: `<old>` → `<new>` on `<date>`) |
8
+ | owned files | `<paths…>` |
9
+ | brief pointer | `BRIEF <path> lines=<n> sha=<sha8>` · ACK received `<yes/no>` |
10
+ | facts diff | `<n>` contradictions: `<one line each — plan said X, code says Y, ruling>` |
11
+ | forecast | cases `<n>` · lines `<n>` · orchestrator's floor: test lines ≥ `<cases × rate>`, docblocks ≥ 10 % |
12
+ | actual | cases `<n>` (runner count) · lines `<n>` (`git diff --stat`) · miss attributed to `<architect-added scope | coder under-forecast | none>` |
13
+ | review rounds | per file: `<path>` `<n>` rounds (`<stalled | converged | review_cap>`) |
14
+ | late findings | `<n>` ruled at close: `<fix_now | nit>` |
15
+ | dispositions / waivers | `<finding id → successor block>` (architect) · `review.waived` via `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` (human) |
16
+ | spawn-flag defects | `<n>` — a coder parked at a permission or model prompt: closed and respawned with the pinned flags (`adapters/solo.md` §3) |
17
+ | gate | `<complete | stopped: <reason>>` at `<iso-8601>` |
18
+ | commit | `<sha>` · draft PR `<url or none>` |
19
+ | cost | coder `<$>` · review `<$>` · S1+S2 `<$>` · tokens `<reported | estimated>` |
@@ -0,0 +1,22 @@
1
+ # Facts sheet — `<slug>`
2
+
3
+ <!-- rendered by `forge facts`; edit nothing below the header by hand -->
4
+
5
+ | field | value |
6
+ |---|---|
7
+ | brief | `<path>` |
8
+ | brief sha | `<sha256>` |
9
+ | brief mtime | `<iso-8601>` |
10
+ | built at | `<iso-8601>` |
11
+ | delegate | L0, read-only tools, empty cwd |
12
+ | claims | `<n>` — VERIFIED `<v>` · NOT-FOUND `<nf>` · UNVERIFIABLE `<u>` |
13
+
14
+ ## Claims
15
+
16
+ | fact id | claim | kind | command | output excerpt | tag |
17
+ |---|---|---|---|---|---|
18
+ | F1 | `<claim token as written in the brief>` | flag \| command \| path \| version \| endpoint \| package \| env | `<the read-only command that was run>` | `<≤ 200 characters of its output>` | VERIFIED |
19
+ | F2 | … | … | … | `<why it could not be checked>` | UNVERIFIABLE |
20
+ | F3 | … | … | … | `<the empty or negative output>` | NOT-FOUND |
21
+
22
+ Rules the sheet was refused against: every `command` is read-only (checked against the forbidden list and a write-verb list); a VERIFIED tag has a non-empty excerpt; a tag is never inferred. An acceptance clause in the plan cites a fact by id (`F<n>`); a clause whose claim is NOT-FOUND or UNVERIFIABLE must appear in the plan's "Acceptance clauses the facts sheet cannot back" list with a tolerance naming a block, or `forge plan check` refuses it.
@@ -0,0 +1,53 @@
1
+ # `<feature>` — plan
2
+
3
+ | field | value |
4
+ |---|---|
5
+ | status | facts \| planned \| hardened \| coding \| done |
6
+ | brief | `<path>` (sha `<sha8>`) |
7
+ | facts sheet | `<path>` (sha `<sha8>`, built `<iso-8601>`) |
8
+ | authored at | L3 via `forge author --job plan`; hardened via `forge author --job harden` (`<n>` rounds) |
9
+ | run | `<run id>` once `forge run start` has run |
10
+
11
+ ## §0 Facts sheet (verbatim)
12
+
13
+ <!-- the whole facts sheet, unchanged; `forge plan check` looks for its sha here -->
14
+
15
+ ### §0.x Acceptance clauses the facts sheet cannot back
16
+
17
+ `none` — or one row per clause:
18
+
19
+ | block | clause | claim | tag | tolerance (how the block's acceptance survives it) |
20
+ |---|---|---|---|---|
21
+
22
+ ## §1 Architecture decision
23
+
24
+ Chosen approach and why · rejected alternatives (one line each) · the project conventions that apply · risk class of this tranche (`harden.md` §3).
25
+
26
+ ## §2 Blocks
27
+
28
+ | id | title | level | depends_on | owned_files | cases | lines | acceptance (clauses, each naming its test ids and fact ids) | test_command | commit_message |
29
+ |---|---|---|---|---|---|---|---|---|---|
30
+ | B1 | … | L1 | — | `src/x.mjs`, `test/x.test.mjs` | 12 | 1 200 | (1) … (`test/x.test.mjs`, F3) · (2) … | `node --test 'test/x/**/*.test.mjs'` | … |
31
+
32
+ Split rule: `cases = 2 × clauses`, `lines = 26 × cases` (floor 1 200 greenfield, 500 amendment); above `budget.block_cases` or `budget.block_lines` the block is split here. A glob in `owned_files` never contains `[ ] ( ) ! + @`.
33
+
34
+ ## §3 Caller map
35
+
36
+ | mechanism | shipped by | called by |
37
+ |---|---|---|
38
+
39
+ Every block id above appears in this map; a mechanism with no caller names the block that will call it or is not shipped (`plan.md` §3).
40
+
41
+ ## §4 Dispatch order
42
+
43
+ Slots = `caps.coders`. A block starts when a slot frees and every block in its `depends_on` has been committed: `1. B1 ∥ 2. B2 → 3. B3 (when B1 lands) → …`.
44
+
45
+ ## §5 Hardening Q&A
46
+
47
+ <!-- appended by the harden job: question · options · the human's answer · what changed in §2 -->
48
+
49
+ ## §6 Risks and mitigations
50
+
51
+ ## §7 Block records
52
+
53
+ <!-- one `templates/block-record.md` per dispatched block, appended by the code job -->
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `code-forge author --job plan|harden --brief <file> [--facts <file>] [--draft <file>]
3
+ * [--answers <file>] [--out <file>] [--run <id>] [--timeout <s>]` (plan §3.7, §3.8 rule 2;
4
+ * block B9b). One author round: writes the draft to `--out` (default
5
+ * `<project.plans_dir | plans>/<brief name>.<job>.md`) and prints `{draft, questions, cost}` as one
6
+ * JSON line (`draft` is the path). The orchestrator shows the questions to the human and calls the
7
+ * verb again with `--draft` and `--answers`.
8
+ * Exit codes: 0 answered, 1 failed or invalid answer, 2 usage / no facts sheet / stale sheet,
9
+ * 3 L3 and every fallback unavailable.
10
+ */
11
+
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { loadProjectConfig } from '../config/load.mjs';
15
+ import { AuthorError, runAuthor } from '../session/author.mjs';
16
+ import { runRootFor, SessionError } from '../session/spawn.mjs';
17
+ import { intFlag, parseFlags } from '../state/cli-args.mjs';
18
+ import { StateError } from '../state/paths.mjs';
19
+ import { writeSafe } from '../util/redact.mjs';
20
+ import { slugFor } from './run.mjs';
21
+ import { exitCodeFor } from './spawn.mjs';
22
+
23
+ const USAGE = 'usage: code-forge author --job plan|harden --brief <file> [--facts <file>] [--draft <file>] [--answers <file>] [--out <file>] [--run <id>] [--timeout <seconds>]\n';
24
+
25
+ /**
26
+ * @param {string[]} args
27
+ * @param {import('../session/spawn.mjs').SessionDeps & {stdout?: {write: (s: string) => unknown}, cwd?: string}} [deps]
28
+ * @returns {Promise<number>}
29
+ */
30
+ export async function runAuthorVerb(args, deps = {}) {
31
+ const stdout = deps.stdout ?? process.stdout;
32
+ const stderr = deps.stderr ?? process.stderr;
33
+ const cwd = deps.cwd ?? process.cwd();
34
+ try {
35
+ const { flags, positionals } = parseFlags(args, { values: ['job', 'brief', 'facts', 'draft', 'answers', 'out', 'run', 'timeout'] });
36
+ if (positionals.length > 0 || typeof flags.job !== 'string' || typeof flags.brief !== 'string') {
37
+ writeSafe(stderr, USAGE);
38
+ return 2;
39
+ }
40
+ /** @param {string | string[] | true | undefined} v */
41
+ const abs = (v) => (typeof v === 'string' ? path.resolve(cwd, v) : undefined);
42
+ const briefPath = /** @type {string} */ (abs(flags.brief));
43
+ const loaded = await loadProjectConfig(cwd);
44
+ if (!loaded.ok) {
45
+ writeSafe(stderr, `author: ${loaded.message}\n`);
46
+ return 2;
47
+ }
48
+ const cfg = /** @type {Record<string, any>} */ (loaded.config);
49
+ const timeout = intFlag(flags.timeout, 'timeout');
50
+ const run = typeof flags.run === 'string' ? flags.run : undefined;
51
+ const result = await runAuthor(
52
+ {
53
+ cfg,
54
+ job: flags.job,
55
+ briefPath,
56
+ factsPath: abs(flags.facts),
57
+ draftPath: abs(flags.draft),
58
+ answersPath: abs(flags.answers),
59
+ run,
60
+ runRoot: run ? runRootFor(run, cfg?.tmp?.root) : undefined,
61
+ slug: slugFor(cfg, cwd),
62
+ timeoutMs: timeout === undefined ? undefined : timeout * 1000,
63
+ },
64
+ { ...deps, stderr },
65
+ );
66
+ if (result.status !== 'ok') {
67
+ writeSafe(stderr, `author: ${result.status}${result.reason ? ` (${result.reason})` : ''}\n`);
68
+ return exitCodeFor(result);
69
+ }
70
+ const out = abs(flags.out) ?? path.join(path.resolve(cwd, cfg.project?.plans_dir ?? 'plans'), `${path.basename(briefPath, path.extname(briefPath))}.${flags.job}.md`);
71
+ mkdirSync(path.dirname(out), { recursive: true });
72
+ writeFileSync(out, /** @type {string} */ (result.draft));
73
+ writeSafe(stdout, `${JSON.stringify({ draft: out, questions: result.questions, cost: result.cost })}\n`);
74
+ return 0;
75
+ } catch (err) {
76
+ if (err instanceof AuthorError || err instanceof StateError || err instanceof SessionError) {
77
+ writeSafe(stderr, `author: ${/** @type {Error} */ (err).message}\n`);
78
+ return 2;
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ /** @param {string[]} args */
85
+ export default async function authorVerb(args) {
86
+ return runAuthorVerb(args);
87
+ }