@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,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates `docs/reference/config.md` from `schema/code-forge.schema.json` (plan §1.3; block
4
+ * B16). The schema is the single source of truth for every `.code-forge.yml` key — this script
5
+ * only reads the schema file and `$defs` inside it; it never imports `src/config/**` (B16's
6
+ * `depends_on` is B13a only, so it stays out of the config blocks' seam).
7
+ *
8
+ * `node scripts/gen-config-doc.mjs` writes docs/reference/config.md
9
+ * `node scripts/gen-config-doc.mjs --check` prints nothing on a match and exits 0; on a mismatch
10
+ * prints the first differing line to stderr and exits 1. Never edit docs/reference/config.md by
11
+ * hand — regenerate it and commit the result.
12
+ */
13
+
14
+ import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const REPO_ROOT = path.join(__dirname, '..');
20
+ const SCHEMA_PATH = path.join(REPO_ROOT, 'schema', 'code-forge.schema.json');
21
+ const OUT_PATH = path.join(REPO_ROOT, 'docs', 'reference', 'config.md');
22
+
23
+ /**
24
+ * @param {any} node @param {Record<string, any>} defs
25
+ * @returns {any} `node` itself, or the `$defs` entry it points at when `node.$ref` is set.
26
+ */
27
+ function resolveRef(node, defs) {
28
+ if (node && typeof node === 'object' && typeof node.$ref === 'string') {
29
+ const name = node.$ref.split('/').pop();
30
+ return defs[String(name)];
31
+ }
32
+ return node;
33
+ }
34
+
35
+ /**
36
+ * A short, table-cell-safe type label. Pipes inside a union are escaped (`\|`) so they never read
37
+ * as a markdown table column separator.
38
+ * @param {any} nodeIn @param {Record<string, any>} defs
39
+ * @returns {string}
40
+ */
41
+ function typeLabel(nodeIn, defs) {
42
+ const node = resolveRef(nodeIn, defs);
43
+ if (!node || typeof node !== 'object') return 'unknown';
44
+ if (node.const !== undefined) return `const \`${JSON.stringify(node.const)}\``;
45
+ if (Array.isArray(node.enum)) return `enum: ${node.enum.map((v) => `\`${v}\``).join(', ')}`;
46
+ if (Array.isArray(node.anyOf)) return node.anyOf.map((sub) => typeLabel(sub, defs)).join(' \\| ');
47
+ if (node.type === 'array') return `array<${typeLabel(node.items, defs)}>`;
48
+ if (Array.isArray(node.type)) return node.type.join(' \\| ');
49
+ return typeof node.type === 'string' ? node.type : 'object';
50
+ }
51
+
52
+ /** @param {any} nodeIn @param {Record<string, any>} defs @returns {string} */
53
+ function defaultLabel(nodeIn, defs) {
54
+ const node = resolveRef(nodeIn, defs);
55
+ return node && node.default !== undefined ? `\`${JSON.stringify(node.default)}\`` : '—';
56
+ }
57
+
58
+ /** @param {any} nodeIn @param {Record<string, any>} defs @returns {string} */
59
+ function notesLabel(nodeIn, defs) {
60
+ const node = resolveRef(nodeIn, defs) ?? {};
61
+ const parts = [];
62
+ if (node.minimum !== undefined) parts.push(`min ${node.minimum}`);
63
+ if (node.maximum !== undefined) parts.push(`max ${node.maximum}`);
64
+ if (node.exclusiveMinimum !== undefined) parts.push(`> ${node.exclusiveMinimum}`);
65
+ if (node.minLength !== undefined) parts.push(`minLength ${node.minLength}`);
66
+ if (node.minItems !== undefined) parts.push(`minItems ${node.minItems}`);
67
+ if (typeof node.description === 'string' && node.type !== 'object') parts.push(node.description);
68
+ return parts.join('; ');
69
+ }
70
+
71
+ /** @param {string[]} keys @returns {string} `` `a` ``, `` `a` and `b` `` or `` `a`, `b` and `c` `` */
72
+ function backtickList(keys) {
73
+ const quoted = keys.map((k) => `\`${k}\``);
74
+ if (quoted.length <= 1) return quoted.join('');
75
+ return `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`;
76
+ }
77
+
78
+ /**
79
+ * The intro sentence naming the top-level required keys — built from `schema.required` (never
80
+ * hard-coded, so a schema amendment that adds/removes a required key changes this sentence on the
81
+ * next regeneration instead of silently going stale) plus a note for whichever of those required
82
+ * keys carry no schema-level `default` (an omitted one is refused, not defaulted).
83
+ * @param {any} schema @param {Record<string, any>} defs
84
+ * @returns {string}
85
+ */
86
+ function requiredKeysSentence(schema, defs) {
87
+ const required = Array.isArray(schema.required) ? schema.required : [];
88
+ if (required.length === 0) return 'No top-level key is required by the schema.';
89
+ const noDefault = required.filter((key) => {
90
+ const propNode = resolveRef(schema.properties?.[key], defs);
91
+ return !propNode || propNode.default === undefined;
92
+ });
93
+ const be = required.length === 1 ? 'is' : 'are';
94
+ let sentence = `${backtickList(required)} ${be} required.`;
95
+ if (noDefault.length > 0) {
96
+ const have = noDefault.length === 1 ? 'has' : 'have';
97
+ const it = noDefault.length === 1 ? 'it' : 'any of them';
98
+ sentence += ` ${backtickList(noDefault)} ${have} no schema-level default; a config omitting ${it} is refused.`;
99
+ }
100
+ const withDefault = required.filter((k) => !noDefault.includes(k));
101
+ if (withDefault.length > 0) {
102
+ const have = withDefault.length === 1 ? 'has' : 'have';
103
+ sentence += ` ${backtickList(withDefault)} ${have} a schema-level default, so omitting it is not itself refused.`;
104
+ }
105
+ return sentence;
106
+ }
107
+
108
+ /**
109
+ * Depth-first walk over `properties` and typed `additionalProperties` maps, in the order the
110
+ * schema file lists them (`Object.entries` preserves string-key insertion order — the walk is
111
+ * therefore stable across runs, which is what makes `--check` byte-identical meaningful).
112
+ * @param {any} nodeIn @param {string} pathStr @param {Record<string, any>} defs
113
+ * @param {{path: string, type: string, required: string, default: string, notes: string}[]} rows
114
+ */
115
+ function walk(nodeIn, pathStr, defs, rows) {
116
+ const node = resolveRef(nodeIn, defs);
117
+ if (!node || typeof node !== 'object') return;
118
+ if (node.type === 'object') {
119
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
120
+ if (node.properties && typeof node.properties === 'object') {
121
+ for (const [key, child] of Object.entries(node.properties)) {
122
+ const childPath = pathStr ? `${pathStr}.${key}` : key;
123
+ rows.push({
124
+ path: childPath,
125
+ type: typeLabel(child, defs),
126
+ required: required.has(key) ? 'yes' : '',
127
+ default: defaultLabel(child, defs),
128
+ notes: notesLabel(child, defs),
129
+ });
130
+ walk(child, childPath, defs, rows);
131
+ }
132
+ }
133
+ if (node.additionalProperties && typeof node.additionalProperties === 'object') {
134
+ const childPath = pathStr ? `${pathStr}.<key>` : '<key>';
135
+ const ap = node.additionalProperties;
136
+ rows.push({
137
+ path: childPath,
138
+ type: typeLabel(ap, defs),
139
+ required: '',
140
+ default: defaultLabel(ap, defs),
141
+ notes: 'user-defined key' + (notesLabel(ap, defs) ? `; ${notesLabel(ap, defs)}` : ''),
142
+ });
143
+ walk(ap, childPath, defs, rows);
144
+ }
145
+ } else if (node.type === 'array' && node.items) {
146
+ walk(node.items, `${pathStr}[]`, defs, rows);
147
+ }
148
+ }
149
+
150
+ /** @returns {string} the exact bytes `docs/reference/config.md` should hold. */
151
+ export function renderConfigDoc() {
152
+ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
153
+ const defs = schema.$defs ?? {};
154
+ const rows = [];
155
+ walk(schema, '', defs, rows);
156
+
157
+ const lines = [];
158
+ lines.push('# `.code-forge.yml` configuration reference');
159
+ lines.push('');
160
+ lines.push(
161
+ `Generated by \`node scripts/gen-config-doc.mjs\` from \`schema/code-forge.schema.json\` — do ` +
162
+ `not hand-edit this file. Run \`node scripts/gen-config-doc.mjs --check\` in CI to catch a ` +
163
+ `schema change that was not followed by a regeneration.`,
164
+ );
165
+ lines.push('');
166
+ if (typeof schema.description === 'string') {
167
+ lines.push(schema.description);
168
+ lines.push('');
169
+ }
170
+ lines.push(requiredKeysSentence(schema, defs));
171
+ lines.push('');
172
+ lines.push(
173
+ 'Every other key defaults as shown, or is entirely absent from a minimal file. ' +
174
+ '`additionalProperties: false` applies everywhere a key set is fixed; the rows marked ' +
175
+ '`<key>` are the handful of open-ended maps where the schema accepts a caller-chosen name ' +
176
+ '(a question id, a level name, a key name, a provider id).',
177
+ );
178
+ lines.push('');
179
+ lines.push('| Key | Type | Required | Default | Notes |');
180
+ lines.push('|---|---|---|---|---|');
181
+ for (const row of rows) {
182
+ const notes = row.notes.replaceAll('|', '\\|');
183
+ lines.push(`| \`${row.path}\` | ${row.type} | ${row.required} | ${row.default} | ${notes} |`);
184
+ }
185
+ lines.push('');
186
+ return lines.join('\n');
187
+ }
188
+
189
+ function main() {
190
+ const check = process.argv.includes('--check');
191
+ const generated = renderConfigDoc();
192
+
193
+ if (!check) {
194
+ writeFileSync(OUT_PATH, generated);
195
+ process.stdout.write(`wrote ${path.relative(REPO_ROOT, OUT_PATH)}\n`);
196
+ return 0;
197
+ }
198
+
199
+ let onDisk;
200
+ try {
201
+ onDisk = readFileSync(OUT_PATH, 'utf8');
202
+ } catch {
203
+ process.stderr.write(`gen-config-doc --check: ${path.relative(REPO_ROOT, OUT_PATH)} does not exist\n`);
204
+ return 1;
205
+ }
206
+ if (onDisk === generated) {
207
+ process.stdout.write('gen-config-doc --check: docs/reference/config.md is up to date\n');
208
+ return 0;
209
+ }
210
+ const onDiskLines = onDisk.split('\n');
211
+ const generatedLines = generated.split('\n');
212
+ let i = 0;
213
+ while (i < onDiskLines.length && i < generatedLines.length && onDiskLines[i] === generatedLines[i]) i += 1;
214
+ process.stderr.write(
215
+ `gen-config-doc --check: docs/reference/config.md is stale (first difference at line ${i + 1})\n` +
216
+ ` on disk: ${JSON.stringify(onDiskLines[i] ?? '<eof>')}\n` +
217
+ ` generated: ${JSON.stringify(generatedLines[i] ?? '<eof>')}\n`,
218
+ );
219
+ return 1;
220
+ }
221
+
222
+ /**
223
+ * True when this file is the process entry point. Both sides go through `realpathSync`: Node
224
+ * resolves a symlinked entry to its real path for `import.meta.url` but leaves `argv[1]` as typed,
225
+ * so a plain `path.resolve` comparison is false through any symlink (macOS `/var` -> `/private/var`
226
+ * included) and `--check` would then exit 0 without checking anything.
227
+ * @returns {boolean}
228
+ */
229
+ function isEntryPoint() {
230
+ if (!process.argv[1]) return false;
231
+ try {
232
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
238
+ const isMain = isEntryPoint();
239
+ if (isMain) {
240
+ process.exitCode = main();
241
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: code-forge
3
+ description: Plan, harden and code a feature with levels and roles instead of model names — an L0 delegate builds the facts sheet, an L3 session authors the plan, coders run at the level System 1 picks, a fresh L2 session reviews every finished file, and the block gate re-measures everything. Use when the user says "/code-forge", "forge this feature", or "code-forge plan|harden|code|full".
4
+ ---
5
+
6
+ # code-forge — the orchestrator's skill
7
+
8
+ You are the **orchestrator**: the session that invoked this skill. You dispatch, re-measure and record. You **never code, never review, never judge** — every model call that is not your own turn goes through the CLI as a fresh, isolated session. `forge` below is `scripts/forge` next to this file (a shim for the `code-forge` CLI). Every fact — model ids, efforts, thresholds, paths — lives in `.code-forge.yml` or in the CLI, never in this text; when a verb's usage line disagrees with this file, the verb is right.
9
+
10
+ ## §0 Preflight (every invocation)
11
+
12
+ 1. `forge validate` — exit 0, or stop and show the refusal (it names the key, never the value).
13
+ 2. `forge doctor --quick` — config, links, key resolution. On the first run of a day run `forge doctor` in full and read its `engine` row and its `signer: same-user boundary only` row (`references/security.md`).
14
+ 3. `forge run start` — prints `run <id> started · engine <e> · worker pid <n>`. The engine is `solo`, `harness` or `subprocess` (`engine: auto` picks; `subprocess` only by explicit config). No engine ⇒ the four-line stop text in `references/degraded.md` §1 is printed and you stop. Keep `<id>`: every later verb takes `--run <id>`.
15
+ 4. When the engine is not `solo`, print the banner of `references/degraded.md` §2 once.
16
+
17
+ ## §1 Invocation and state
18
+
19
+ `/code-forge plan|harden|code|full [brief file | plan file]`. No job given ⇒ infer it: a brief ⇒ `plan`; a plan whose header says `status: planned` ⇒ `harden`; `status: hardened` ⇒ `code`. `full` runs the three in order with one human checkpoint after `harden`.
20
+
21
+ State lives in the plan file's header — `facts → planned → hardened → coding → done` — and in the run record: `forge run status --run <id>` prints the active blocks, `forge ledger tail --slug <slug>` the last rows. Takeover after a lost session: `references/continuity.md`.
22
+
23
+ ## §2 Roles by level (never by model name)
24
+
25
+ | role | level | session | who runs it |
26
+ |---|---|---|---|
27
+ | facts delegate | L0 | read-only tools, empty cwd | `forge facts` |
28
+ | coder | the lane S1 picks (L0–L2); climbs one rung per escalation | the project tree | the engine adapter |
29
+ | reviewer, recheck | L2 | closed-book, empty cwd, packet on stdin | the worker |
30
+ | judge, S2, author | L3 | closed-book, one turn | the worker, `forge s2`, `forge author` |
31
+ | L3 rung | L3 | a patch ≤ 80 lines, once per block | `forge spawn --level L3 --role coder` |
32
+
33
+ `forge resolve L<n>` prints what a level resolves to (provider, model, effort, fallback); `forge models` lists the catalog. With `review.multimodel` on, the second reviewer is another provider's L2 and the judge comes from a third provider.
34
+
35
+ ## §3 The loop
36
+
37
+ 1. `forge facts --brief <brief> --out <plans_dir>/<slug>.facts.md` — an L0 delegate checks every claim with a read-only command. You read the sheet; you never read the sources yourself (`references/plan.md` §0).
38
+ 2. `forge author --job plan --brief <brief> --facts <sheet> --out <plan>` — refuses without `--facts` (exit 2) and refuses a sheet older than the brief. The draft's §0 is the sheet, verbatim.
39
+ 3. Harden: `forge author --job harden --brief <brief> --facts <sheet> --draft <plan> --answers <file>` until `questions[]` is empty. The human answers; you relay, never answer for them (`references/harden.md`). Exit gate: `forge plan check <plan>` green.
40
+ 4. Per block, in dispatch order, never more than `caps.coders` alive: `forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> --brief <file> --lines <n>` prints the pointer `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>`. Send the pointer per the engine's adapter — `solo` → `references/adapters/solo.md`, `harness` → `references/adapters/claude-code.md`, `subprocess` → `references/adapters/subprocess.md` — and wait for `ACK <sha8> lines=<n>`.
41
+ 5. The coder's first message is its **facts diff**, then its forecast (cases and lines, `templates/block-record.md`), then code, file by file: after each file `forge review-file <path> --block <id> --run <r>`, then `forge review-file --wait <ticket>` before the next file. Rounds ≥ 2 re-check only the fix hunks (`references/review.md`).
42
+ 6. `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` ends the coder's turn. Its report is a claim sheet: re-measure it (`references/code.md` §5).
43
+ 7. `forge block close <id> --run <r>` is the gate: a signed `review.approved` row per changed file, gates green, every clause covered, forecast vs actual, no `rule_break`. Red ⇒ `references/decisions.md` (S1 `next`, escalation). Green ⇒ one commit for the block in the project's house style; `forge block rebase <id> --run <r>` first when HEAD moved.
44
+ 8. `forge run end --run <r>` when every block is `done`; `forge report --slug <slug>` prints cost per block, escalations and review rounds.
45
+
46
+ ## §4 Hard rules (the headings of `references/rules-core.md`)
47
+
48
+ - **R1** Coder ≠ reviewer, always
49
+ - **R2** The orchestrator never codes, reviews or judges
50
+ - **R3** State lives outside the conversation
51
+ - **R4** The evidence gate is a re-measurement
52
+ - **R5** No destructive git, no production, no regex edits
53
+ - **R6** The budget stop is the feature
54
+ - **R7** Wire truncation is detectable
55
+ - **R8** A mechanism needs a caller
56
+ - **R9** Own-family adjudication is visible and asymmetric
57
+ - **R10** A fresh isolated session per job
58
+ - **R11** A review without matching `reviewed_hunks` is not a review
59
+ - **R12** The L3 rung writes a patch, never a feature
60
+ - **R13** Measurement runs in an export or behind the proof lock
61
+ - **R14** No design before facts
62
+ - **R15** Re-checks shrink: fix hunks only, four rounds, then a patch, then a human
63
+ - **R16** Proof is exact assertions on the main behaviour
64
+
65
+ ## §5 Where to read more
66
+
67
+ | when | read |
68
+ |---|---|
69
+ | decomposing, forecasting, `plan check` | `references/plan.md`, `templates/plan.md`, `templates/facts-sheet.md` |
70
+ | the author loop and the tranche gate | `references/harden.md` |
71
+ | briefing a coder, the block gate, the PR contract, Definition of Done | `references/code.md`, `templates/block-record.md` |
72
+ | review depth, packets, the convergence rule, `block waive` | `references/review.md` |
73
+ | System 1 / System 2, thresholds, escalation, the L3 rung | `references/decisions.md` |
74
+ | proof tiers, red→green, the measurement export | `references/proof.md` |
75
+ | ledger rows, `report`, calibration | `references/ledger.md` |
76
+ | takeover, base moves | `references/continuity.md` |
77
+ | no Solo, no subagent tool, a level unavailable | `references/degraded.md` |
78
+ | keys, the forbidden list, signed rows and their limit | `references/security.md` |
79
+ | spawning coders per engine | `references/adapters/solo.md`, `references/adapters/claude-code.md`, `references/adapters/subprocess.md` |
@@ -0,0 +1,19 @@
1
+ # Claude Code adapter — engine `harness` (the default without Solo)
2
+
3
+ The harness's `Agent` tool runs the coder as a subagent; everything else — the worker, reviewers, judges, S2, author, facts — still runs as isolated CLI sessions.
4
+
5
+ ## §1 Spawn
6
+
7
+ 1. `forge run start` once; `forge block open <id> --run <r> …` before every spawn (`code.md` §3). `forge resolve L<n>` prints the level's provider, model and effort; the `Agent` tool takes a family **alias** for its model, derived from the configured id's family prefix — effort cannot be passed to the tool, so the ledger records `effort_effective: null` and `doctor` prints it.
8
+ 2. `Agent(subagent_type: general-purpose, model: <the alias `forge resolve L<n>` maps to>, prompt: <the pointer>)` — the prompt is the pointer `forge block open` printed (`BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>`) preceded by one line: *read the brief at the pointer; reply `ACK <sha8> lines=<n>` as the first line of your report; append a progress line to `.code-forge/runs/<run>/<block>.log` after every file.* No worktree (`harness.claude.worktree` stays false): parallelism rests on disjoint `owned_files`, and without locks `caps.coders` is 1.
9
+ 3. The brief file carries the forbidden list verbatim; the harness's own permission rules are set from the same list by `forge init`.
10
+
11
+ ## §2 Completion and evidence
12
+
13
+ The tool return is the completion signal; the report must begin with `ACK <sha8>` and end with `===BLOCK <id> COMPLETE===` (+ test summary + `git diff --stat`) or `===BLOCK <id> FAILED: <reason>===`. A report without the sentinel is a failed block, never a partial success.
14
+
15
+ **Between completion and close come the review rounds.** The subagent ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket inside its turn, so most rounds happen before the sentinel; when the tool returns, first drain every `pending` ticket with `forge review-file --wait <ticket>` yourself — a pending ticket is a review in flight, not an open finding, and waiting on it costs no round. Then only an open `fix_now` finding, or a file the coder never submitted, starts a fix round (each one counts toward the file's cap): dispatch a new `Agent` call at the level the ladder says (`review.md` §5 — the coder is gone with its tool return, so the fix brief carries the open findings and the fix history) and it runs `review-file` again on the fixed file. Rounds are bounded: four rounds per file, then the L3 rung once, then `stopped: review_cap` for the human. Only when every changed file has a signed `review.approved` row do you run the re-measurement of `code.md` §5 and `forge block close <id> --run <r>` — the transcript grep reads `.code-forge/runs/<run>/<block>.log`; pass `--transcript <file>` when the harness saved the subagent's transcript elsewhere. Closing with a file still unreviewed is a human decision: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, or `forge block close … --no-require-reviews`, both audited as their own rows.
16
+
17
+ ## §3 What is lost and what is not
18
+
19
+ Lost: live observation of the coder (only the progress log and the final report), a resumable transcript (a lost subagent is re-dispatched with the fix history in a new brief — `continuity.md` §2). Not lost: session isolation, signed rows, the gate, the ladder, the facts rule, the convergence rule (`degraded.md` §2).
@@ -0,0 +1,34 @@
1
+ # Solo adapter — engine `solo`
2
+
3
+ Solo adds spawn, watch, timers, scratchpads, locks and `kv` on top of the CLI; it is not what isolates sessions (the worker does that in every engine). `forge run start` runs before the first spawn; `forge block open` runs before **every** spawn.
4
+
5
+ ## §1 Once per machine: measure the input limit
6
+
7
+ `send_input` truncates silently. Before the first real spawn on a machine, send a throwaway L0 process four payloads of 1 200 / 1 600 / 2 000 / 2 400 bytes, each beginning with a known marker and ending in `<<<EOM>>>`, and read back which arrive with both ends intact. Write the largest intact size to `kv forge:solo_input_limit` and to `~/.code-forge/cache/solo-limit.json` (`{bytes, measured_at, solo_version}`). Every later first message is capped at `limit − 200` bytes; until measured, the cap is 1 200 B. The pointer itself stays ≤ 200 B.
8
+
9
+ ## §2 The wire: pointer, sentinel, receipt
10
+
11
+ 1. Long instructions live in the brief file; the wire carries the pointer `forge block open` printed — `BRIEF <path> lines=<n> sha=<sha8> <<<EOM>>>` — after the agent's own instructions, nothing more.
12
+ 2. Every wire message ends in `<<<EOM>>>`. No sentinel ⇒ truncated ⇒ the receiver does not act, says so, asks for a resend. A coder that refuses half an instruction is correct.
13
+ 3. The receipt: the coder replies `ACK <sha8> lines=<n>` **before any tool call**. The launch gate reads `get_process_output(lines=40)` for it; no ACK or a wrong sha ⇒ clear the input box, resend once; still none ⇒ `parked`, respawn once, then `failed`. **Clearing is sized, then confirmed:** a fixed number of deletes can leave the front of a long payload in the box, and a resend appended to that junk still ends in `<<<EOM>>>` and passes the sentinel check. Send a kill-line sequence when the agent's input supports one; otherwise send as many DELs as the largest possible payload — the measured limit of §1, or 2 400 B when it has not been measured — and then read `get_process_output` to confirm the input line is empty before resending. A box that will not empty is a `parked` coder, not a resend target. `search_output` looks for the ACK line during idle triage.
14
+ 4. Every dispatch or GO names the plan file, its revision and its line count; the coder echoes them back. Unsent text in a coder's input box is never an instruction: clear the box before every send.
15
+
16
+ ## §3 Spawn protocol per block
17
+
18
+ 1. Pre-flight: `list_processes` and close **only** agents this run spawned (own pids only); `list_agent_tools` to resolve runtime ids live, never from memory. Never more than `caps.coders` coder processes alive; a further spawn waits for a completion wake, never for a timer.
19
+ 2. `forge block open <id> --run <r> …` (`code.md` §3), then `spawn_agent(agent_tool_id=<coder runtime>, name="coder-<id>", extra_args=<the argv the level resolves to>)` — the model, effort and permission mode are pinned **at spawn** from `forge resolve L<n>` and the provider's coder flags (`forge spawn --level L<n> --role coder --brief <file>` prints the exact argv without running it when you need to see it); never a post-boot model switch. Coders run in the provider's non-interactive auto mode: a session parked at a permission prompt is the first failure mode of every run.
20
+ 3. First `send_input` after the `❯` prompt: the agent's instructions, then the pointer (§2).
21
+ 4. **Launch-verification gate** — the one thing you babysit: poll `get_process_output` / `search_output` until the coder shows the ACK and tool activity. Dead-idle after the pointer (no prompt on screen) ⇒ one Enter (`send_input bytes=[13]`) and re-check. A **permission or model-picker prompt** is never answered with Enter — it would accept an unreviewed default: `close_process`, respawn with the correct `extra_args`, and record a `spawn-flag defect` line in the block record (the flags came from `forge resolve`; a prompt means they were not pinned).
22
+ 5. **Wake channels, event-driven, never fixed polls:** record your own pid once (`whoami` → `kv_set("forge:orchestrator_pid")`); coders that can call Solo carry the push contract (post-sentinel `kv_set("block:<id>", …)` + `timer_set(1s, body, delivery_process_id=<your pid>)`); for every group arm `timer_fire_when_idle_any([coders], max_wait≈20–25 min, delivery_process_id=<your pid>)` right after the launch gate (idle-any ignores already-idle processes, so arm while they are provably running); keep one long dead-man fallback per 30–40 min with an L0 triage first. On wake: sentinel ⇒ gate; no sentinel and active again ⇒ re-arm; no sentinel and idle ⇒ parked, intervene now.
23
+ 6. Completion = `search_output(pattern="===BLOCK <id> COMPLETE===")`, last occurrence (the `kv` key is the truncation backup). Completion is not closure.
24
+ 7. **Review rounds, with the coder still alive.** The coder ran `forge review-file <path> --block <id> --run <r>` per file and `--wait`ed on each ticket; read the ledger (`forge ledger tail --slug <slug>`) for what is still open — a `fix_now` finding, a `pending` ticket, a file never submitted. Each open finding is a fix round sent to the **warm coder** by `send_input` (the findings by id, `<<<EOM>>>`), which fixes and re-runs `review-file` on that file; round ≥ 2 re-checks only the fix hunks (`review.md` §5). **One ladder** (`decisions.md` §5): a stall or exhausted rounds *below L2* climbs one level — close this coder and respawn a fresh one at the higher level with the open findings and the fix history in its brief; **the respawn keeps the per-file round count**, the cap does not reset. L2 is the highest running level: a trigger at L2, or the four-round cap on a file, goes to the **L3 rung once** — a fresh L3 patch session, never this coder and never a coder at L3 — and if findings stay open after the `patch_check` round the file is `stopped: review_cap` for the human. No other path exists.
25
+ 8. **Close** only when every changed file has a signed `review.approved` row: the re-measurement of `code.md` §5, then `forge block close <id> --run <r>` — the transcript grep reads the Solo output. Closing with a file still unreviewed is a human decision, audited: `forge block waive <id> <finding> --run <r> --file <path> --reason "<why>"` per finding, or `forge block close … --no-require-reviews`.
26
+ 9. **Reap after the close**: `close_process` the coder and confirm `lock_status` shows no lease before the next group. Locks: a coder that can call Solo holds `lock_acquire` on its file area while editing; coders that cannot never overlap another block.
27
+
28
+ ## §4 Latency overlaps (standing)
29
+
30
+ Draft block N+1's brief while N codes; dispatch the gate the moment the sentinel lands; keep the coder alive until its last review round (fix rounds go to the warm coder); spawn-ahead N+1 during N's review when N+1 depends only on interfaces that already passed N's gate — never for a security-keystone block.
31
+
32
+ ## §5 State in Solo
33
+
34
+ Plan and research digests → scratchpad sections, read back selectively (`scratchpad_read` on a section, `scratchpad_tail`); block state → todos with `todo_set_blockers` mirroring `depends_on`; small shared facts (branch, slug, runtime ids, the input limit) → `kv`; long agent output stays in the process (`search_output`, never a full dump). The handoff log and the run record still exist (`continuity.md` §1): Solo is the convenience, not the only copy.
@@ -0,0 +1,15 @@
1
+ # Subprocess adapter — engine `subprocess` (power users, by explicit config only)
2
+
3
+ Never selected automatically (`degraded.md` §1). Coders are detached CLI processes of the provider the level resolves to, built by the CLI from `.code-forge.yml`: the argv is never a shell string, the forbidden list is rendered as the CLI's own deny rules, and the brief is a file whose pointer travels in the argv.
4
+
5
+ ## §1 Spawn
6
+
7
+ `forge run start` once; then per block `forge block open <id> --run <r> …` (`code.md` §3) and `forge spawn --level L<n> --role coder --brief <file> --run <r> --block <id> [--cwd <dir>] [--timeout <seconds>] --background`. The spawner writes a pid file and a log under the run's temp root, registers the pid so the reaper can find it, and prints `level=<Lx> provider=<p> model=<id> effort=<e> fallback_step=<n>`; a coder argv that hits the forbidden list is refused before the spawn. A missing or wrong `ACK <sha8> lines=<n>` within the launch window ⇒ `parked`, one resend, then `failed`.
8
+
9
+ ## §2 Completion and evidence
10
+
11
+ The coder's stdout is captured; completion is the **last** `===BLOCK <id> COMPLETE===` or `===BLOCK <id> FAILED: <reason>===` line in it (a quoted sentinel inside a sentence never matches). Process exit without a sentinel is a failed block. Then the re-measurement of `code.md` §5 and `forge block close <id> --run <r> --transcript <captured stdout>` — the transcript grep runs on the capture. A coder that hangs is killed at `--timeout` together with its process group; the pid entry is cleared.
12
+
13
+ ## §3 What the coder's sandbox looks like per provider
14
+
15
+ Each provider's coder runs in its non-interactive auto mode with writes limited to the workspace where the CLI supports it; every closed-book role runs read-only with an empty cwd and the packet on stdin. `forge doctor` probes, per provider, that the flags exist, that a one-token call works per role builder, that the closed-book session sees no project document, and that the deny rules are honoured — read its rows before the first real spawn. Where a provider cannot load a rules file, the forbidden list is prose-only in the brief and the transcript grep is the detector (`security.md` §2).
@@ -0,0 +1,64 @@
1
+ # Code — brief, dispatch, re-measure, gate
2
+
3
+ ## §1 The coder's first deliverable is a facts diff
4
+
5
+ Before any forecast or code, the coder reads the code it will call and reports **every contradiction between the plan and the code** in one message — a signature, a flag, a key, a version. One stop for facts, never one per discovery. Contract corrections (a key, a rule, a signature) are the architect's alone and are recorded, not asked; a ruling reaches the human only when it changes user-visible behaviour, money semantics or an approved decision. A coder that stops three times for three true facts did nothing wrong; the protocol did.
6
+
7
+ ## §2 The forecast and its floor
8
+
9
+ The coder then forecasts **cases** (tests) and **lines**. Compute the floor yourself before reading its number: test lines ≥ cases × the rate measured on the last two gated blocks (the lower of the two) and docblocks ≥ 10 % of source; reject a forecast below the floor for re-pricing. Record both numbers in the block record (`templates/block-record.md`); `forge block open … --lines <n>` stores the line forecast the gate compares against.
10
+
11
+ ## §3 Dispatch — what the brief contains
12
+
13
+ `forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> --brief <file> [--attempt <n>] [--base <sha>] --lines <n>` records the base sha, the owned files (overlap with another active block ⇒ refused), the level, 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. The brief file carries, in this order:
14
+
15
+ 1. `id`, `title`, the level, `owned_files` ("edit ONLY these"), the acceptance clauses with test ids, `test_command`.
16
+ 2. The reply contract: `ACK <sha8> lines=<n>` before any tool call; the facts diff; the forecast.
17
+ 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 and `git diff --stat`, or `===BLOCK <id> FAILED: <reason>===`.
19
+
20
+ 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.
21
+
22
+ ## §4 The file loop, from your side
23
+
24
+ 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 ledger, not the coder's output: `forge ledger tail --slug <slug>` shows `review.plan`, `review.round`, `review.done`. Idle is a wake, never proof; the sentinel is the only completion signal (`adapters/*.md`).
25
+
26
+ ## §5 The block gate is a re-measurement — `forge block close <id> --run <r>`
27
+
28
+ The coder's report is a claim sheet. The gate reproduces each number in the measurement export (`proof.md` §3), and so do you when you read the report:
29
+
30
+ | claim | re-measured by |
31
+ |---|---|
32
+ | only owned paths touched | file set = tracked changes ∪ untracked, minus other active blocks' owned sets ⊆ `owned_files`; anything else is an orphan that stops every gate until `forge block claim <id> <path> --run <r>` or the human deletes it |
33
+ | every file reviewed | a **signed** `review.approved` row for the current content hash of every changed or new file, or an architect-ruled disposition to a named successor block |
34
+ | tests green | every gate command run once in the export, each exit status read |
35
+ | clauses covered | every clause names ≥ 1 test id; each test exists and passed in this run; S1 `scope` on the hunks outside the named tests says `false` |
36
+ | test count | the filtered run's delta equals the new tests' own count |
37
+ | line and case forecast | `git diff --stat` net lines against `--lines`: > 1.25 × ⇒ stop (`===BLOCK <id> FAILED: forecast exceeded, split plan <id>a/<id>b===`); cases > 1.5 × forecast ⇒ a `forecast.cases_exceeded` WARN row, never a stop |
38
+ | no rule break | the transcript grep for forbidden verbs and paths (`--transcript <file>`, else the run record's log); a missing transcript ⇒ a signed `gate.transcript_missing` row and a WARN, never silence |
39
+ | high tier proven | every changed high-tier file (`proof.md` §1) has a signed, proven red→green row with `red_kind: assertion` for this block whose `covers` names it (light tier: optional), else `unproven <file>`; only the human's `block waive <id> proof --file <path>` clears it |
40
+ | rows genuine | the MAC of every gate-relevant row verifies; the live worker matches the pinned pid |
41
+ | nothing left open | no unruled `late_findings`; no file at `review_cap` without a signed `review.waived` row |
42
+
43
+ `--no-require-reviews` is the human's way out, written as its own audit row; the orchestrator runs it only after the human said so in chat. 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.
44
+
45
+ ## §6 Budget — the stop is the feature (R6)
46
+
47
+ `budget.block_lines` and `budget.block_cases` bind the plan; the gate's 1.25 × stop binds the block. A coder that overruns and stops before expanding did the right thing: never compress or defer coverage to fit. One raise per run, by the human, logged; a second overrun in the same run re-decomposes at the architect level. A post-acceptance ruling that adds required lines re-baselines the cap at ruling time, priced from the assertions it orders; a STOP defined mid-turn binds from the coder's next reported boundary. A coder never trims to fit; landing exactly on a cap is a tell that is checked. Review tokens count toward `budget.block_tokens_soft` / `budget.block_tokens_hard` (`review.md` §7).
48
+
49
+ ## §7 The PR contract — a draft PR only after independent review
50
+
51
+ With `autonomy.coder_may_open_draft_pr` and `autonomy.coder_may_push` 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. Re-measurement then runs on the pushed sha; the orchestrator marks ready; the human merges — at most `wip.ready_prs` at a time.
52
+
53
+ **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.
54
+
55
+ ## §8 Definition of Done (per block)
56
+
57
+ 1. `owned_files` untouched outside the set (`git status` read, not quoted).
58
+ 2. The block's test command green, case count read from the runner and written to the block record.
59
+ 3. Type check and lint green under the project's gates.
60
+ 4. Every changed or new file reviewed by a fresh L2 session and converged under the convergence rule (≤ `review.max_rounds_per_file`, or a human decision on record).
61
+ 5. Red→green rows for every new test; proof rows per tier (`proof.md`).
62
+ 6. Forecast vs actual (cases and lines) recorded; misses attributed (architect-added scope vs coder under-forecast) — a finished green block is accepted and recorded either way.
63
+ 7. One commit for the block; the draft PR (when the project uses PRs) carries the evidence.
64
+ 8. Every artifact that proves something is committed, not left in a scratch dir; no claim shipped that the evidence does not carry.
@@ -0,0 +1,31 @@
1
+ # Continuity — the orchestrator will die mid-run
2
+
3
+ ## §1 Three durable state sources, read in this order
4
+
5
+ 1. **The run record** — `~/.code-forge/runs/<run>.json` (authoritative, outside the workspace; `forge run status --run <id>` prints it) mirrored to `.code-forge/runs/<run>.json`: active blocks with base sha, owned files, level, attempt; orphans; the pinned worker pid and start time.
6
+ 2. **The ledger tail for the run** — `forge ledger tail --slug <slug>`: what was dispatched, reviewed, escalated, stopped.
7
+ 3. **The plan file** — status header, block table, the Q&A and risks appended by harden, the block records (`templates/block-record.md`) appended by code.
8
+ 4. When Solo is present: the run's scratchpad and the `kv` run-state key (current block, who is root, what is open, what each agent is doing), and an append-only handoff log `<plans_dir>/<run>-handoff.md` — every root decision with its evidence and what would reverse it, written for the architect's return.
9
+
10
+ A fired timer body is a snapshot, not truth: rebuild state from the sources above and from `git`, never from the timer, and say so inside the timer body.
11
+
12
+ ## §2 Takeover checklist
13
+
14
+ 1. Read the sources of §1, in order. Verify every branch and PR claim against `git` (and `gh` when the project uses PRs).
15
+ 2. `forge run status --run <id>`; if the worker is not pinned or its pid is dead, `forge run start --reattach --run <id>` (with `--worker-pid <pid>` when you started one by hand with `forge worker --run <id>`). A replaced worker is a `worker.replaced` row: blocks gated under the old pin are stopped, not trusted.
16
+ 3. Under Solo: `whoami`, rewrite the `kv` root field to yourself keeping `prior_root`, and re-arm every watch and timer to your own pid — a watch delivering to a dead pid is worse than none.
17
+ 4. Read each agent's *unsubmitted* input box as evidence of intent, never as an instruction; clear it before any send. Never sign as the previous root.
18
+ 5. Every open block: re-read its block record; a block mid-file resumes at `forge review-file --wait <ticket>`; a block whose coder is gone is re-dispatched at the same attempt number with the fix history in the brief (`forge block attempt <id> --run <r>`).
19
+ 6. Handing back: the architect gets the handoff log, not a summary, ending in an explicit open-questions list.
20
+
21
+ ## §3 When the base moves under an open block or PR
22
+
23
+ Symptoms: a PR flips to conflicting, or its diff suddenly shows the whole stack; or another block committed and HEAD advanced while this block's `base_sha` did not.
24
+
25
+ - **Map before touching:** tips, PR states, merge-bases, whether the incoming tree is identical to what the block was built on.
26
+ - **`forge block rebase <id> --run <r>`** before the block commits: it verifies the block's base is an ancestor of HEAD, records `{old_base, new_base}` as a `block.rebase` row and re-runs the file-set check; a conflict on an owned file ⇒ `stop`. Never a silent re-base.
27
+ - **Every branch is cut from the remote default branch after a fetch, never from the local ref** — in a checkout where the human merges on the hosting side and nobody updates the local ref, the local branch only ever falls further behind and looks exactly like a valid base. A plan names the remote ref in the command itself.
28
+ - **Prefer a sha-preserving merge over a rebase on a published branch** (no force-push — it is on the forbidden list). Rebase only when explicitly sanctioned, right after a predecessor merges, never mid-review.
29
+ - **Duplicate-append is the common conflict** in append-only files: read every hunk anyway — one in four is a genuinely different block.
30
+ - **Prove with a tree comparison:** `git diff <pre> <post>` empty when the merge reconciles history only; `git merge-base --is-ancestor`; the line count back to block size. Merged ≠ in the default branch: check both.
31
+ - **Re-run classifiers and forecasts after any base change**: a size or hot-file figure taken across a stale base is not a number.
@@ -0,0 +1,50 @@
1
+ # Decisions — tools first, System 1 in half a second, System 2 when unsure, a deterministic ladder
2
+
3
+ Deterministic tools answer facts (gates, diffs, counts). **System 1 (S1)** answers typed questions from a small JSON state built from facts. **System 2 (S2)** is a fresh L3 session that answers only when S1 is unsure. Same input ⇒ same answer; every answer and its outcome go to the ledger. 4–7 S1 calls per block is a target that is reported, never gated. A fact is never a model's opinion.
4
+
5
+ ## §1 The S1 questions (`forge jev ask <question-id> --state <file>`)
6
+
7
+ | id | type | fires | answers |
8
+ |---|---|---|---|
9
+ | `lane` | choice | plan, per block; code, on re-decomposition | `L0` trivial · `L1` a plain feature following an existing pattern · `L2` money, dates, tenancy, migrations, concurrency, unknown failure · `split` too large or two concerns. **L3 is not a lane** |
10
+ | `risk` | score 0–3 | plan, per block; code, per finished file | `0` cosmetic · `1` wrong behaviour, easy to notice and revert · `2` silent wrong data for some users · `3` silent wrong money or a cross-tenant leak |
11
+ | `security_sensitive` | yes/no | plan, per block; code, per file unless path floors already forced `high` | touches authentication, authorization, tenancy, secrets, money movement or webhook verification |
12
+ | `next` | choice | code, after each attempt's tool gate | `complete` all green, tests added or `no_new_tests_reason` given · `retry` a contained failure the same coder fixes in one more attempt · `escalate` repeated or spreading failures, or a risky area · `stop` the evidence contradicts the brief — the human decides |
13
+ | `scope` | yes/no | block gate, once, on the hunks outside the named tests | a change unrelated to the acceptance (clause coverage itself is deterministic) |
14
+ | `defect` | yes/no | review, per finding that has not passed a judge | a real defect that must be fixed before merge, not a style preference |
15
+ | `resolved` | yes/no | fix loop, per open finding after a fix (state = the fix hunk) | the fix hunk resolves the finding without a new problem in the shown lines |
16
+
17
+ The state never contains a secret or a whole file; `system1.disable` turns a question off for projects that will not send diff hunks (`defect`, `resolved` then go to S2). Only the worker or the orchestrator's own CLI call sends the question — never a coder process.
18
+
19
+ ## §2 Thresholds — when S1 acts and when S2 checks
20
+
21
+ Per answer the CLI computes `confidence` and `margin`: for a choice or score `confidence = p(top1)`, `margin = p(top1) − p(top2)`; for a yes/no question `answer = p ≥ 0.5`, `confidence = max(p, 1 − p)`, `margin = |2p − 1|` — a confident **no** at `p = 0.03` is `confidence 0.97` and S1 decides. Bands per question (`thresholds.default` or `thresholds.<question>`): `confidence ≥ act` and `margin ≥ close_margin` ⇒ S1 acts (`s1.decided`) · `check ≤ confidence < act` or a small margin ⇒ **S2 checks** (sees state, S1's answer and probabilities; returns `confirm|overrule`) · `confidence < check` ⇒ **S2 decides** from the state alone. Findings keep their own bands (`thresholds.findings.fix`, `.nit`, `.resolved`).
22
+
23
+ ## §3 System 2 — `forge s2 --packet <file.json> [--run <id>] [--block <id>]`
24
+
25
+ A fresh L3 closed-book session, empty cwd, packet on stdin, one turn, the compiled schema for the provider. Output `{decision, confidence, reason, overrule, ask_human, human_question}`. More than 6 non-fallback S2 calls in one block ⇒ the block is flagged `s2-heavy`: re-check its decomposition. `ask_human` routes to the human with `human_question` verbatim.
26
+
27
+ ## §4 When S1 is unavailable (`system1.fallback`)
28
+
29
+ No key ⇒ a banner at run start; 401 ⇒ stop; 422 ⇒ stop with the request id; 429/529/network ⇒ backoff, then per-call fallback with one banner. `rules` (default): deterministic rules answer `lane`, `risk`, `security_sensitive` and an unambiguous `next` — path floors (`proof.tiers.high.paths` ⇒ risk 3, security yes), diff size (> 200 added lines or > 4 files ⇒ ≥ L1; migrations, policies, middleware ⇒ L2), keywords (money, policy, auth, webhook, migration); a rule that fires is final, no S2 check; the questions no rule covers go to S2 (`source: s2-fallback`, exempt from the `s2-heavy` counter). `s2`: every question to S2. `stop`: refuse to run without S1. The block gate refuses a block whose S1 calls fell back unexpectedly unless the human accepts it in chat.
30
+
31
+ ## §5 Escalation precedence — deterministic, first rule wins, `trigger` recorded
32
+
33
+ 1. **Attempts:** attempts at the current level equal `escalation.retries_per_level` and not green ⇒ +1 level, whatever S1 said (`retries`).
34
+ 2. **Review rounds:** `escalation.review_rounds_per_level` exhausted with an open `fix_now` ⇒ +1 level (`review_rounds`).
35
+ 3. **Review stall:** a re-check round whose open `fix_now` set did not strictly shrink ⇒ +1 level at once (`review_stall`, `review.md` §5).
36
+ 4. **Security floor, at dispatch:** `security_sensitive` ⇒ first-attempt level `min(max(lane, L1) + 1, L2)` (`security`).
37
+ 5. **S2 ruling:** S1 `next = escalate` is never acted on directly — it goes to S2 as a check (`s2_ruling`); S1 `next = stop` ⇒ the human.
38
+ 6. **Review cap:** `review.max_rounds_per_file` reached on a file with an open `fix_now` ⇒ the L3 rung for that file, once per block, always as a patch whatever `escalation.l3_mode` says (`review_cap`); checked last, only when no rule above returned an action this attempt.
39
+
40
+ **The ceiling constraint (not a ranked rule).** *Never skip a level* constrains every rule above: a "+1 level" is always the next level, and **the highest running level is L2**. When rule 1, 2 or 3 fires at L2 there is no coding level above, and the **L3 rung** takes over (§6): a patch session, never a coder at L3 — the block continues at L2 afterwards. `escalation.stop_at` = L3 (fixed) names that rung, not a level a coder runs at; `l3_mode: code` is the only way a coder runs at L3, and it is not the default. The rung is one per block from either source (a trigger at L2 or the review cap, rule 6): a second hit from either always `stop`s (`review_cap` for the cap; the ceiling reason for the trigger), so `stop_at` means something in every mode. A fresh coder spawned by a level climb inherits the file's round count: the cap counts rounds per file, not per coder.
41
+
42
+ **Own-family adjudication (R9).** When the orchestrator and the coder share a model family, every ruling the orchestrator makes about that coder's work is a judgement inside the family: it is written as a ledger row named as such, and it is asymmetric — the orchestrator may **accept** a finding alone; it may **not dismiss, defer or waive** one. Dismissals, cap raises and scope changes go to the architect (an L3 session) or the human.
43
+
44
+ ## §6 The L3 rung (`escalation.l3_mode: patch`, R12)
45
+
46
+ A fresh L3 session receives the failure evidence — the last attempt's diff, the red gate output, the open findings, the brief — and returns `{kind: patch|redecompose|stop, patch, reason}`. `patch` (a unified diff ≤ 80 lines, owned files only): the CLI applies it after `git apply --check`, a fresh L2 `full` review of the patch runs as a `patch_check` round outside the per-file cap, and the block **continues at L2** with the attempt counter reset and `l3_patch: true`; a second L3 rung on the same block ⇒ `stop`. `redecompose` ⇒ `stopped: redecompose`, the human decides. `stop` ⇒ stop and ask. `l3_mode: code` lets L3 code the block; it is not the default.
47
+
48
+ ## §7 What reaches the human
49
+
50
+ `next = stop`, `review.over_budget`, an orphan file, `worker_down`, `review_cap`, `redecompose`, `ask_human`, a security question, consensus unavailable, and one budget raise per run. Everything else is decided by S1, S2 or the rules above and recorded.
@@ -0,0 +1,40 @@
1
+ # Degraded — no Solo, no subagent tool, a level unavailable
2
+
3
+ ## §1 Engine selection and the stop text
4
+
5
+ `engine: auto` at preflight: Solo present (an MCP entry and a working `whoami`) ⇒ `solo`; else the invoking harness exposes a subagent tool ⇒ `harness`; else the run stops with exactly:
6
+
7
+ ```
8
+ code-forge: no Solo and no subagent tool in this harness.
9
+ To run coders as detached CLI processes (power-user mode) add to .code-forge.yml:
10
+ engine: subprocess
11
+ Then re-run. (R2: this engine is never selected automatically.)
12
+ ```
13
+
14
+ and writes ledger `run.stop {reason: no_engine, harness: <name>}`. `subprocess` is chosen only by explicit config (`adapters/subprocess.md`). The engine is printed by `forge run start`, written to the run record, and never changes mid-run. `forge init` prints the same text at setup when it detects the situation, so nobody discovers it at the first run.
15
+
16
+ ## §2 What degrades without Solo (print this banner once per run)
17
+
18
+ ```
19
+ code-forge: engine <harness|subprocess> — no Solo on this machine.
20
+ no locks ⇒ caps.coders = 1 (one coder at a time)
21
+ completion ⇒ the tool return (harness) or the process exit (subprocess); the sentinel is still required
22
+ state ⇒ the run record + the plan file + the handoff log (references/continuity.md)
23
+ live view ⇒ forge ledger tail --slug <slug>
24
+ ```
25
+
26
+ **Unchanged in every engine:** session isolation and the packet on stdin; the worker and signed rows; the block gate; proof; the escalation ladder and the L3 patch rung; the D4 stop at L3; the facts rule; the convergence rule.
27
+
28
+ ## §3 A level unavailable (CLI missing, login expired, HTTP 402, rate-limited twice in a block)
29
+
30
+ Per level, in `levels.L<n>.fallback` order: the first entry rides on the provider's own fallback flag inside one session; every further entry is a fresh spawn after the previous one reported unavailability. Every spawn prints `level=<Lx> provider=<p> model=<id> effort=<e> fallback_step=<n>`.
31
+
32
+ - **L3 unavailable** ⇒ banner; `harden` and `author` refuse to run on a fallback below L3's family rank unless the human says so; judge and S2 use the fallback (`l3_fallback: true`); none ⇒ high-tier files are `review.unavailable`, S2 ⇒ stop and ask.
33
+ - **L2 unavailable** ⇒ next fallback; none ⇒ reviewers `review.unavailable`, coders stop.
34
+ - **L1 / L0 unavailable** ⇒ next fallback; none ⇒ the next level up takes the call and the row is flagged `overspend` (the facts delegate too: L0 ⇒ L1, flagged).
35
+
36
+ A second provider unavailable at run start in consensus mode ⇒ `consensus unavailable: <reason>`; ask the human once per run whether to continue adaptive; non-interactive ⇒ stop.
37
+
38
+ ## §4 Other degradations `doctor` reports
39
+
40
+ `ping=skipped(402)` for a provider whose balance is exhausted is a WARN, never a FAIL. A provider with no coder binary on this machine is detection-and-linking only. `codex: forbidden list is prose-only` means the rules file was not honoured — the transcript grep remains the detector. `claude: path deny rules not honoured` likewise. `tmp: <n> stale roots, <m> stale pids` above zero is cleaned by the next `forge run start`.