@juzser/blacksmith 0.2.0 → 0.4.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 (71) hide show
  1. package/.claude/agents/auditor.md +3 -0
  2. package/.claude/agents/grader.md +12 -5
  3. package/.claude/agents/reviewer.md +3 -0
  4. package/.claude/agents/security-reviewer.md +3 -0
  5. package/.claude/agents/spec-reviewer.md +3 -0
  6. package/.claude/agents/verifier.md +3 -0
  7. package/.claude/skills/bs/audit.md +25 -9
  8. package/.claude/skills/bs/dispatch.md +60 -7
  9. package/.claude/skills/bs/plan.md +33 -1
  10. package/.claude/skills/bs/run.md +18 -13
  11. package/.claude/skills/bs/wave.md +68 -10
  12. package/README.md +4 -4
  13. package/factory/orchestrator/dist/agentsSync.js +178 -0
  14. package/factory/orchestrator/dist/audit.js +134 -17
  15. package/factory/orchestrator/dist/autonomy.js +25 -0
  16. package/factory/orchestrator/dist/budgets.js +91 -2
  17. package/factory/orchestrator/dist/claims.js +8 -4
  18. package/factory/orchestrator/dist/cli.js +394 -26
  19. package/factory/orchestrator/dist/crossFinding.js +20 -0
  20. package/factory/orchestrator/dist/daemon.js +230 -41
  21. package/factory/orchestrator/dist/db/projector.js +93 -36
  22. package/factory/orchestrator/dist/db/queries.js +58 -35
  23. package/factory/orchestrator/dist/db/schema.js +24 -0
  24. package/factory/orchestrator/dist/dispatchAudit.js +10 -2
  25. package/factory/orchestrator/dist/dispatchLint.js +132 -0
  26. package/factory/orchestrator/dist/epic.js +192 -39
  27. package/factory/orchestrator/dist/epicWidth.js +25 -20
  28. package/factory/orchestrator/dist/errorIssues.js +209 -24
  29. package/factory/orchestrator/dist/eventOrder.js +102 -0
  30. package/factory/orchestrator/dist/events.js +73 -91
  31. package/factory/orchestrator/dist/findingContext.js +1 -13
  32. package/factory/orchestrator/dist/findings.js +132 -11
  33. package/factory/orchestrator/dist/gh.js +29 -6
  34. package/factory/orchestrator/dist/git.js +20 -2
  35. package/factory/orchestrator/dist/harness.js +92 -10
  36. package/factory/orchestrator/dist/hookDecision.js +320 -37
  37. package/factory/orchestrator/dist/impact.js +24 -2
  38. package/factory/orchestrator/dist/integration.js +11 -1
  39. package/factory/orchestrator/dist/issueReporter.js +415 -0
  40. package/factory/orchestrator/dist/judgeStopHook.js +181 -0
  41. package/factory/orchestrator/dist/judges.js +1 -1
  42. package/factory/orchestrator/dist/lessons.js +46 -18
  43. package/factory/orchestrator/dist/logCache.js +291 -0
  44. package/factory/orchestrator/dist/paths.js +10 -0
  45. package/factory/orchestrator/dist/plan.js +138 -4
  46. package/factory/orchestrator/dist/planQuorum.js +1 -1
  47. package/factory/orchestrator/dist/policy.js +57 -11
  48. package/factory/orchestrator/dist/projects.js +12 -1
  49. package/factory/orchestrator/dist/queue.js +477 -29
  50. package/factory/orchestrator/dist/scheduler.js +51 -0
  51. package/factory/orchestrator/dist/severity.js +40 -8
  52. package/factory/orchestrator/dist/spec.js +70 -0
  53. package/factory/orchestrator/dist/taskEvents.js +13 -4
  54. package/factory/orchestrator/dist/taskStatus.js +74 -0
  55. package/factory/orchestrator/dist/taxonomy.js +11 -0
  56. package/factory/orchestrator/dist/testgate.js +32 -3
  57. package/factory/orchestrator/dist/usage.js +42 -9
  58. package/factory/orchestrator/dist/waivers.js +60 -9
  59. package/factory/orchestrator/dist/waveConcurrency.js +29 -0
  60. package/factory/orchestrator/dist/waveNext.js +12 -10
  61. package/factory/orchestrator/dist/waveSchedule.js +84 -0
  62. package/factory/orchestrator/dist/worktree.js +19 -6
  63. package/factory/orchestrator/drizzle/0012_issue_reports_error_issues.sql +20 -0
  64. package/factory/orchestrator/drizzle/meta/_journal.json +7 -0
  65. package/factory/policies/budgets.yml +9 -2
  66. package/factory/policies/crosscheck.yml +2 -2
  67. package/factory/policies/lessons.md +298 -0
  68. package/factory/policies/scheduler.yml +27 -2
  69. package/factory/policies/taxonomy.yml +10 -3
  70. package/factory/specs/roadmap.md +70 -23
  71. package/package.json +8 -8
@@ -28,15 +28,7 @@
28
28
  import { claimsOverlap, readClaimList, readEdgeList, touchesSerializeAlways, } from './claims.js';
29
29
  import { topoSort } from './graph.js';
30
30
  import { livePlanTasks } from './plan.js';
31
- /**
32
- * A task is *done* only at the two statuses epic.ts already calls terminal —
33
- * `completed` and `waived`. Everything else non-terminal still holds a
34
- * worktree, so its claims are still spoken for even when nobody is typing in
35
- * it: `blocked`, `failed` and `escalated` are waiting on a person, not on
36
- * nothing, and their uncommitted work is exactly what a second agent in the
37
- * same paths would destroy.
38
- */
39
- const TERMINAL_TASK_STATUSES = new Set(['completed', 'waived']);
31
+ import { TERMINAL_OK_TASK_STATUSES } from './taskStatus.js';
40
32
  /** The two statuses that mean "nothing has been dispatched for this yet". */
41
33
  const CANDIDATE_TASK_STATUSES = new Set(['todo', 'ready']);
42
34
  function nameList(ids) {
@@ -127,7 +119,17 @@ export function computeNextWave(input) {
127
119
  for (const record of livePlanTasks(plan)) {
128
120
  const id = record.task_id;
129
121
  const status = input.statusById?.get(id) ?? String(record.task_status);
130
- if (TERMINAL_TASK_STATUSES.has(status)) {
122
+ // *Done* here is the narrow question — the work ended well, so the
123
+ // worktree is free. Its neighbour in taskStatus.ts, TERMINAL_TASK_STATUSES,
124
+ // answers a different one — which rows the log refuses to overwrite — and
125
+ // holds `failed` and `escalated` too. Those are waiting on a person, not
126
+ // on nothing, and their uncommitted work is exactly what a second agent in
127
+ // the same paths would destroy: reading that set here is a data-loss bug,
128
+ // and this file carried its own copy of this one under *that* name until
129
+ // both answers moved to the module that owns the dimension.
130
+ // (`superseded` is in neither conversation: livePlanTasks drops an id
131
+ // whose every record is superseded, so one never reaches this line.)
132
+ if (TERMINAL_OK_TASK_STATUSES.has(status)) {
131
133
  done.push(id);
132
134
  continue;
133
135
  }
@@ -40,6 +40,8 @@
40
40
  * Pure, and it writes nothing. Like `waveNext`, admission stays where it was:
41
41
  * a round simulated here has been admitted by nobody.
42
42
  */
43
+ import { claimsOverlap, readEdgeList } from './claims.js';
44
+ import { planClaimedTasks } from './plan.js';
43
45
  import { computeNextWave, } from './waveNext.js';
44
46
  /**
45
47
  * Reported in this order, which is the order `computeNextWave` tests them in.
@@ -150,3 +152,85 @@ export function scheduleWaves(input) {
150
152
  exitCode: stalled.length > 0 ? 1 : constraints.length > 0 ? 2 : 0,
151
153
  };
152
154
  }
155
+ /**
156
+ * Directed reachability over the plan's `depends_on` edges, both ways: the
157
+ * tasks reachable by walking forward (its descendants) and the tasks
158
+ * reachable by walking backward (its ancestors). Two tasks are connected —
159
+ * "a dependency path between them in either direction" — when either set
160
+ * contains the other, which a chain like C -> B -> A catches for A and C
161
+ * even though no edge names that pair directly.
162
+ */
163
+ function connectedTaskIds(ids, edges) {
164
+ const descendantsOf = new Map();
165
+ const ancestorsOf = new Map();
166
+ for (const id of ids) {
167
+ descendantsOf.set(id, []);
168
+ ancestorsOf.set(id, []);
169
+ }
170
+ for (const edge of edges) {
171
+ if (!ids.has(edge.task) || !ids.has(edge.dependsOn))
172
+ continue;
173
+ descendantsOf.get(edge.dependsOn)?.push(edge.task);
174
+ ancestorsOf.get(edge.task)?.push(edge.dependsOn);
175
+ }
176
+ const reachable = (start, adjacency) => {
177
+ const seen = new Set();
178
+ const stack = [...(adjacency.get(start) ?? [])];
179
+ while (stack.length > 0) {
180
+ const next = stack.pop();
181
+ if (seen.has(next))
182
+ continue;
183
+ seen.add(next);
184
+ for (const n of adjacency.get(next) ?? [])
185
+ if (!seen.has(n))
186
+ stack.push(n);
187
+ }
188
+ return seen;
189
+ };
190
+ const connected = new Map();
191
+ for (const id of ids) {
192
+ connected.set(id, new Set([...reachable(id, descendantsOf), ...reachable(id, ancestorsOf)]));
193
+ }
194
+ return connected;
195
+ }
196
+ const SMALL_MAX_TASKS = 3;
197
+ const SMALL_MAX_CLAIMS = 10;
198
+ /**
199
+ * Pure. Superseded tasks are absent from `planClaimedTasks` already (D-126),
200
+ * so they never enter `widest`, `parallelWith`, or `size` — no separate
201
+ * filter needed here.
202
+ */
203
+ export function computePlanParallelism(plan, policy) {
204
+ const claimedTasks = planClaimedTasks(plan).filter((t) => t.task_id !== undefined);
205
+ const ids = new Set(claimedTasks.map((t) => t.task_id));
206
+ const edges = readEdgeList(plan.edges);
207
+ const connected = connectedTaskIds(ids, edges);
208
+ const parallelWith = {};
209
+ for (const a of claimedTasks) {
210
+ const withIds = [];
211
+ for (const b of claimedTasks) {
212
+ if (a.task_id === b.task_id)
213
+ continue;
214
+ if (connected.get(a.task_id)?.has(b.task_id))
215
+ continue;
216
+ if (claimsOverlap(a, b).overlaps)
217
+ continue;
218
+ withIds.push(b.task_id);
219
+ }
220
+ parallelWith[a.task_id] = withIds.sort();
221
+ }
222
+ const { widest } = scheduleWaves({ plan, policy });
223
+ const claimPaths = new Set();
224
+ for (const t of claimedTasks)
225
+ for (const c of t.claims)
226
+ claimPaths.add(c);
227
+ return {
228
+ widest,
229
+ parallel_with: parallelWith,
230
+ size: {
231
+ tasks: claimedTasks.length,
232
+ claims: claimPaths.size,
233
+ small: claimedTasks.length <= SMALL_MAX_TASKS && claimPaths.size <= SMALL_MAX_CLAIMS,
234
+ },
235
+ };
236
+ }
@@ -127,18 +127,31 @@ function ensureIntegrationBranch(projectDir, epic) {
127
127
  * sibling of whatever directory the project was handed as, never a path of
128
128
  * this module's own choosing, so a project outside this clone keeps its
129
129
  * worktrees outside it too (see taskWorktreeDir)
130
- * on branch smith/<epic>/<task-id>, cut from the CURRENT head of
131
- * smith/<epic>/integration (creating the integration branch from the default
132
- * branch first if it doesn't exist yet).
130
+ * on branch smith/<epic>/<task-id>.
131
+ *
132
+ * Without `opts.from`, cut from the CURRENT head of smith/<epic>/integration
133
+ * (creating the integration branch from the default branch first if it
134
+ * doesn't exist yet). With `opts.from`, cut from smith/<epic>/<from> instead
135
+ * — the predecessor's branch, commits and all — and integration is never
136
+ * consulted.
133
137
  */
134
- export function createTaskWorktree(projectDir, epic, taskId) {
138
+ export function createTaskWorktree(projectDir, epic, taskId, opts = {}) {
135
139
  if (taskId === RESERVED_TASK_ID) {
136
140
  throw new WorktreeError('worktree.reserved-task-id', `Task id "${RESERVED_TASK_ID}" is reserved for the integration branch itself.`, { taskId });
137
141
  }
138
- const integrationBranch = ensureIntegrationBranch(projectDir, epic);
139
142
  const branch = taskBranchName(epic, taskId);
140
143
  const worktreeDir = taskWorktreeDir(projectDir, epic, taskId);
141
- git(projectDir, ['worktree', 'add', '-b', branch, worktreeDir, integrationBranch]);
144
+ let sourceBranch;
145
+ if (opts.from !== undefined) {
146
+ sourceBranch = taskBranchName(epic, opts.from);
147
+ if (!localBranchExists(projectDir, sourceBranch)) {
148
+ throw new WorktreeError('worktree.from-missing', `Predecessor branch ${sourceBranch} does not exist; cannot cut ${taskId} from it.`, { epic, taskId, from: opts.from, branch: sourceBranch });
149
+ }
150
+ }
151
+ else {
152
+ sourceBranch = ensureIntegrationBranch(projectDir, epic);
153
+ }
154
+ git(projectDir, ['worktree', 'add', '-b', branch, worktreeDir, sourceBranch]);
142
155
  return { worktreeDir, branch, epic, taskId };
143
156
  }
144
157
  /** Remove a task's worktree and its local branch after it has merged. */
@@ -0,0 +1,20 @@
1
+ CREATE TABLE `issue_reports` (
2
+ `event_id` text PRIMARY KEY NOT NULL,
3
+ `session_id` text NOT NULL,
4
+ `ts` text NOT NULL,
5
+ `task_ref` text,
6
+ `error_class` text NOT NULL,
7
+ `fingerprint` text NOT NULL,
8
+ `issue_url` text,
9
+ `latest_event_id` text NOT NULL,
10
+ `outcome` text NOT NULL,
11
+ `reason` text,
12
+ `repo_slug` text,
13
+ `source` text NOT NULL,
14
+ `project` text
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE INDEX `issue_reports_session_idx` ON `issue_reports` (`session_id`);--> statement-breakpoint
18
+ CREATE INDEX `issue_reports_fingerprint_idx` ON `issue_reports` (`fingerprint`);--> statement-breakpoint
19
+ CREATE INDEX `issue_reports_outcome_idx` ON `issue_reports` (`outcome`);--> statement-breakpoint
20
+ ALTER TABLE `milestones` ADD `error_issues` integer DEFAULT true NOT NULL;
@@ -85,6 +85,13 @@
85
85
  "when": 1789391823211,
86
86
  "tag": "0011_findings_spec_ref",
87
87
  "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "6",
92
+ "when": 1789464480602,
93
+ "tag": "0012_issue_reports_error_issues",
94
+ "breakpoints": true
88
95
  }
89
96
  ]
90
97
  }
@@ -64,7 +64,7 @@
64
64
  # limits, and the sections that follow say which is which. `max_turns` is no
65
65
  # longer among them at all: the turn ceiling is the role template's
66
66
  # `maxTurns` (`.claude/agents/<role>.md`), which Claude Code enforces per
67
- # dispatch (measured 2026-09-07/-11, dogfood-csb-audit-1 FD-7 and FD-14 —
67
+ # dispatch (measured on two dogfood runs, 2026-09-07/-11 —
68
68
  # agent-interviews.md M-4 had called it inert), and a plan cannot raise or
69
69
  # lower a role's ceiling. So plan validation rejects it as a task budget
70
70
  # field, and the dispatch prompt restates the template's number verbatim.
@@ -97,7 +97,14 @@ epic:
97
97
  # Two things this does NOT fix, both worth a
98
98
  # follow-up: (1) there is no per-epic override,
99
99
  # so a number raised for one epic is raised for
100
- # every epic black-smith will ever run; (2) a
100
+ # every epic black-smith will ever run. What
101
+ # exists is a per-BOX override, a different
102
+ # thing: SMITH_EPIC_CAP_TOKENS in the env (or
103
+ # `.env`) replaces this number for every epic
104
+ # run on that box while it is set — the
105
+ # SMITH_EPIC_* / SMITH_TASK_* names in
106
+ # .env.example cover each number in this file
107
+ # that a box may want to move; (2) a
101
108
  # large share of that 1.5M was rework — stopped
102
109
  # dispatches and re-dispatches that `smith
103
110
  # escalation check` recorded as ladder
@@ -114,13 +114,13 @@ providers:
114
114
  # Funded for THIS box, 2026-09-04, by the operator who owns the key.
115
115
  enabled: auto
116
116
  # Promoted shadow -> active by the operator on 2026-09-14, after the
117
- # csb-signing-policy-1 epic-final verdict held on `insufficient-providers`
117
+ # epic-final verdict of a dogfooded product epic held on `insufficient-providers`
118
118
  # with all three participants at `confirm`: with claude excluded as the
119
119
  # finder and deepseek in shadow, the gating pool was codex alone (1 <
120
120
  # `quorum_rule.min_providers` 2), so no mechanically-ready epic could ever
121
121
  # reach quorum on this box (epic.ts, "exactly 1 active external"). The
122
122
  # calibration run it replaced is the recorded shadow verdicts from
123
- # csb-audit-1 and csb-signing-policy-1 (docs/runbooks/providers.md §3).
123
+ # two dogfooded product epics (docs/runbooks/providers.md §3).
124
124
  # Rollback is `mode: shadow` here; nothing else remembers the promotion.
125
125
  mode: active
126
126
 
@@ -112,6 +112,18 @@ nothing.
112
112
  - agent_role: grader
113
113
  - statement: A judge's output contract fixes where the verdict sits as strictly as what it says: a correct verdict at the wrong key is read as no verdict at all.
114
114
 
115
+ ### lesson-raised-75658b0d107b: Write the declared artifact as a parseable skeleton before your secon...
116
+
117
+ - lesson_id: lesson-raised-75658b0d107b
118
+ - agent_role: spec-reviewer
119
+ - statement: Write the declared artifact as a parseable skeleton before your second turn ends and refine it in place; a turn cap that lands on an unwritten artifact discards the whole review, and 15 turns of reading is the usual way to reach it.
120
+
121
+ ### lesson-raised-dd1d1b5bcf68: An acceptance criterion that probes a script running under 'set -e' m...
122
+
123
+ - lesson_id: lesson-raised-dd1d1b5bcf68
124
+ - agent_role: planner
125
+ - statement: An acceptance criterion that probes a script running under 'set -e' must name the outer exit status it expects, never an echo the script prints afterwards: errexit ends the script at the first failing command, so a line printed after it is unreachable and the grader can only mark the wording, not the property.
126
+
115
127
  ## claim-path
116
128
 
117
129
  ### lesson-raised-3b5746704cfc: A function that computes a filesystem path and hands it to a child pr...
@@ -271,6 +283,117 @@ _(none yet)_
271
283
  - lesson_id: lesson-raised-b8db8efe922d
272
284
  - statement: An agent that owes a file should write it before refining it: the declared-artifact check catches an agent that ended on an intention only after the tokens are spent, and if that agent could write to shared state, only after the damage has spread.
273
285
 
286
+ ### lesson-raised-1da73f3f36ee: A dispatched agent's turn budget is set by the runtime, not by the di...
287
+
288
+ - lesson_id: lesson-raised-1da73f3f36ee
289
+ - agent_role: planner
290
+ - statement: A dispatched agent's turn budget is set by the runtime, not by the dispatch prompt: stating a larger number in the prompt does not raise it, so an agent that researches before it writes can spend an entire stint and return no artifact at all.
291
+
292
+ ### lesson-raised-2fb53a40b114: Register an obligation only from the dispatches whose artifact the di...
293
+
294
+ - lesson_id: lesson-raised-2fb53a40b114
295
+ - finding_category: maintainability
296
+ - statement: Register an obligation only from the dispatches whose artifact the discharging reader can actually accept; a ledger that registers on any declared artifact but discharges on one artifact shape mints obligations the system can never close.
297
+
298
+ ### lesson-raised-6a0d232d948d: An agent cut off by the turn cap after writing its placeholder artifa...
299
+
300
+ - lesson_id: lesson-raised-6a0d232d948d
301
+ - statement: An agent cut off by the turn cap after writing its placeholder artifact returns a file that is schema-valid and materially unchanged, so a diff of the artifact reports the round as wasted at precisely the moment the round's work exists but is unspliced; the recoverable signal is the agent's own tool trace, and the recovery is to resume that agent, because re-dispatching discards work the diff could not see.
302
+
303
+ ### lesson-raised-c941f4645adb: A rule asserted over text its author has not read will over-reject in...
304
+
305
+ - lesson_id: lesson-raised-c941f4645adb
306
+ - finding_category: correctness
307
+ - agent_role: planner
308
+ - statement: A rule asserted over text its author has not read will over-reject inside that text; if a criterion binds every construct in its task, the sweep of every construct is part of writing it, not a later check.
309
+
310
+ ### lesson-raised-33474964213a: Once a document begins to discuss its own markers, counting a marker ...
311
+
312
+ - lesson_id: lesson-raised-33474964213a
313
+ - finding_category: correctness
314
+ - statement: Once a document begins to discuss its own markers, counting a marker by its text stops measuring what the marker marks: prose quoting the marker is indistinguishable from the marker itself, so count by position — a real truncation marker terminates its string — and state which test the count used.
315
+
316
+ ### lesson-raised-7670023e5f79: A document that measures itself must take the measurement after the e...
317
+
318
+ - lesson_id: lesson-raised-7670023e5f79
319
+ - finding_category: correctness
320
+ - statement: A document that measures itself must take the measurement after the edit that measurement is part of: a figure computed from the draft being read and written into the draft being produced describes the version it replaced, while reading as a present-tense fact about the version that carries it.
321
+
322
+ ### lesson-raised-3b009dfb0e81: A turn-cap notification is evidence about the return channel only: it...
323
+
324
+ - lesson_id: lesson-raised-3b009dfb0e81
325
+ - statement: A turn-cap notification is evidence about the return channel only: it says the agent stopped mid-turn, never that the agent wrote nothing. Stat and parse the declared artifact before recording that a dispatch produced no result.
326
+
327
+ ### lesson-raised-1931b084725f: A negative claim built on a substring search is a claim about spellin...
328
+
329
+ - lesson_id: lesson-raised-1931b084725f
330
+ - statement: A negative claim built on a substring search is a claim about spelling, not about content, unless the search is robust to hyphenation, case and whitespace; the reviewer will report a count of zero and the document will say the thing in the next spelling over.
331
+
332
+ ### lesson-raised-e89a7a73adbb: A scope sentence placed after the sentence it qualifies will be read ...
333
+
334
+ - lesson_id: lesson-raised-e89a7a73adbb
335
+ - statement: A scope sentence placed after the sentence it qualifies will be read as scoping the sentence before it; a reader who quotes the pair back will attach the scope to the wrong clause and the citation will look sourced.
336
+
337
+ ### lesson-raised-e543bb1a5d0b: A claim-confinement check built on `git diff --name-only` must pass `...
338
+
339
+ - lesson_id: lesson-raised-e543bb1a5d0b
340
+ - finding_category: correctness
341
+ - statement: A claim-confinement check built on `git diff --name-only` must pass `--no-renames`: with rename detection on, a committed rename lists only its destination, so moving a file OUT of an unclaimed path INTO a claimed one reports nothing deleted and passes.
342
+
343
+ ### lesson-raised-6ea1eb68b2bf: A revision that fixes a defect in one instrument must sweep every sib...
344
+
345
+ - lesson_id: lesson-raised-6ea1eb68b2bf
346
+ - finding_category: correctness
347
+ - statement: A revision that fixes a defect in one instrument must sweep every sibling instrument doing the same job: a plan that names the disease in one task and leaves the other task carrying it reads as a deliberate distinction rather than an oversight, and the second site never gets reviewed again.
348
+
349
+ ### lesson-raised-9e3e3f16efec: A common-prefix plus common-suffix comparison can only ever detect ON...
350
+
351
+ - lesson_id: lesson-raised-9e3e3f16efec
352
+ - finding_category: correctness
353
+ - statement: A common-prefix plus common-suffix comparison can only ever detect ONE insertion point, so it reports a legitimate multi-site append as a deletion or rewrite; the exact test for insert-only is whether the old string is a subsequence of the new one.
354
+
355
+ ### lesson-raised-e24e9fb2785f: A confinement claim -- 'all of them are here', 'nothing else referenc...
356
+
357
+ - lesson_id: lesson-raised-e24e9fb2785f
358
+ - finding_category: correctness
359
+ - statement: A confinement claim -- 'all of them are here', 'nothing else references this' -- is the one kind of review evidence a reader cannot falsify from the quoted text, so it must be measured independently before it is repeated to the next tier.
360
+
361
+ ### lesson-raised-80aae5ddff34: A draft that has survived N rounds of text review has still never bee...
362
+
363
+ - lesson_id: lesson-raised-80aae5ddff34
364
+ - finding_category: correctness
365
+ - statement: A draft that has survived N rounds of text review has still never been mechanically validated unless someone actually ran the validator: reviewers grade prose and validators grade shape, and neither notices that the other never ran.
366
+
367
+ ### lesson-raised-6fa07c6529b9: A correction that declares its reach as "wherever this document spell...
368
+
369
+ - lesson_id: lesson-raised-6fa07c6529b9
370
+ - finding_category: correctness
371
+ - statement: A correction that declares its reach as "wherever this document spells X" and then enumerates the sites has created a count it must prove; enumerate by mechanical occurrence search, or the correction silently exempts the site it forgot.
372
+
373
+ ### lesson-raised-c90b1840a097: Telling an agent to write early is not enough: the artifact contract ...
374
+
375
+ - lesson_id: lesson-raised-c90b1840a097
376
+ - finding_category: correctness
377
+ - statement: Telling an agent to write early is not enough: the artifact contract must mandate a minimum object count with explicit clean and not-reached sentinels, one write per question, so that a budget death leaves a readable partial instead of a file that cannot be told apart from a completed clean review.
378
+
379
+ ### lesson-raised-1aabef29e2e1: A priority order stated in a resume prompt is as unenforceable as a t...
380
+
381
+ - lesson_id: lesson-raised-1aabef29e2e1
382
+ - finding_category: correctness
383
+ - statement: A priority order stated in a resume prompt is as unenforceable as a turn budget: the agent will spend its whole stint on the first item it finds tractable. If the order matters, dispatch one question per resume, or measure the remaining questions yourself.
384
+
385
+ ### lesson-raised-eb10cdf5761b: A claim about how many leaves a revision changed must be measured ove...
386
+
387
+ - lesson_id: lesson-raised-eb10cdf5761b
388
+ - finding_category: correctness
389
+ - statement: A claim about how many leaves a revision changed must be measured over every leaf including scalars: a strings-only walker undercounts by exactly the version-number bumps, which is the one change every revision makes.
390
+
391
+ ### lesson-raised-18c6a8c89228: A zero-occurrence sweep is evidence only after the coordinate is conf...
392
+
393
+ - lesson_id: lesson-raised-18c6a8c89228
394
+ - finding_category: correctness
395
+ - statement: A zero-occurrence sweep is evidence only after the coordinate is confirmed against the finding own location field: searching the wrong criterion returns a null that is indistinguishable from a real defect, and it will manufacture a false finding against correct text.
396
+
274
397
  ### lesson-raised-66bfd37ce01a: Never override the commit author with `git -c user.email=...`; the re...
275
398
 
276
399
  - lesson_id: lesson-raised-66bfd37ce01a
@@ -356,6 +479,181 @@ _(none yet)_
356
479
  - lesson_id: lesson-raised-bc8c041f3a8c
357
480
  - statement: A hand-maintained list inside a guard is the hole the guard was built to close; derive the list from the source it checks (a `roots` array in `packaging.test.ts` did not name `ROADMAP_PATH`, so the guard passed the very gap it existed to catch).
358
481
 
482
+ ### lesson-raised-04cd391ca791: A 404 from a scoped npm registry on a PUT is an authentication failur...
483
+
484
+ - lesson_id: lesson-raised-04cd391ca791
485
+ - statement: A 404 from a scoped npm registry on a PUT is an authentication failure, not a missing package: the registry hides an existing scope from an unauthorised token instead of returning 401.
486
+
487
+ ### lesson-raised-1804a19612db: npm two-factor authentication makes publishing an operator-run step: ...
488
+
489
+ - lesson_id: lesson-raised-1804a19612db
490
+ - statement: npm two-factor authentication makes publishing an operator-run step: the OTP prompt cannot be satisfied by an agent, so a release runbook must hand the publish command to the operator rather than run it.
491
+
492
+ ### lesson-raised-f39c1a53b31b: An npm packument carries `gitHead` for every published version, so th...
493
+
494
+ - lesson_id: lesson-raised-f39c1a53b31b
495
+ - statement: An npm packument carries `gitHead` for every published version, so the commit a release was cut from can be read back from the registry without relying on a tag.
496
+
497
+ ### lesson-raised-bac9a3bd3b81: The auto-mode classifier denies `smith plan ingest`, an `mv` of a pla...
498
+
499
+ - lesson_id: lesson-raised-bac9a3bd3b81
500
+ - statement: The auto-mode classifier denies `smith plan ingest`, an `mv` of a plan file, and Bash reads of the skill playbooks under `.claude/skills/`; those steps need the operator or a different tool.
501
+
502
+ ### lesson-raised-2528cf4e5f25: `smith wave check` requires the full event envelope and prints no eve...
503
+
504
+ - lesson_id: lesson-raised-2528cf4e5f25
505
+ - statement: `smith wave check` requires the full event envelope and prints no event id, so the id of the event it wrote has to be read back from the session log.
506
+
507
+ ### lesson-raised-ccacd8f42e4e: Cut a worktree from the current base: a worktree cut before N merges ...
508
+
509
+ - lesson_id: lesson-raised-ccacd8f42e4e
510
+ - statement: Cut a worktree from the current base: a worktree cut before N merges tests a tree nobody will ship, and a green suite on it says nothing about the tree that lands.
511
+
512
+ ### lesson-raised-3a856caf80ac: An effort-floor scan is not a security-reviewer dispatch; the two ans...
513
+
514
+ - lesson_id: lesson-raised-3a856caf80ac
515
+ - statement: An effort-floor scan is not a security-reviewer dispatch; the two answer different questions, and passing one does not discharge the other.
516
+
517
+ ### lesson-raised-31b13840f6c5: An `error-logged` event requires `task_ref` in its payload; without i...
518
+
519
+ - lesson_id: lesson-raised-31b13840f6c5
520
+ - statement: An `error-logged` event requires `task_ref` in its payload; without it the append is refused.
521
+
522
+ ### lesson-raised-612b4017e024: zsh expands an unquoted `--include` glob before grep ever sees it, so...
523
+
524
+ - lesson_id: lesson-raised-612b4017e024
525
+ - statement: zsh expands an unquoted `--include` glob before grep ever sees it, so the pattern must be quoted.
526
+
527
+ ### lesson-raised-bf090949f8e9: The live plan is the `plan-v<n>.json` inside the epic worktree, not t...
528
+
529
+ - lesson_id: lesson-raised-bf090949f8e9
530
+ - statement: The live plan is the `plan-v<n>.json` inside the epic worktree, not the copy of it in the clone.
531
+
532
+ ### lesson-raised-5b592f5d0052: `smith queue run --project <dir>` checks out the integration branch i...
533
+
534
+ - lesson_id: lesson-raised-5b592f5d0052
535
+ - statement: `smith queue run --project <dir>` checks out the integration branch in that directory, so it must never be pointed at the main clone.
536
+
537
+ ### lesson-raised-9101d200fc02: The visible way to correct a false record in the event log is an oper...
538
+
539
+ - lesson_id: lesson-raised-9101d200fc02
540
+ - statement: The visible way to correct a false record in the event log is an operator-note carrying a non-operator actor, never a rewrite of the event that was wrong.
541
+
542
+ ### lesson-raised-612b6c33c722: A `subprocess.run` environment must inherit `PATH`; a wholly replaced...
543
+
544
+ - lesson_id: lesson-raised-612b6c33c722
545
+ - statement: A `subprocess.run` environment must inherit `PATH`; a wholly replaced env silently loses every tool the command needs.
546
+
547
+ ### lesson-raised-babd51405588: The factory leaked `SMITH_*` environment variables into project comma...
548
+
549
+ - lesson_id: lesson-raised-babd51405588
550
+ - statement: The factory leaked `SMITH_*` environment variables into project commands on two separate call paths, so one fix at one call site did not close the leak.
551
+
552
+ ### lesson-raised-2ab8ba0c5201: `smith event append` takes one inline JSON envelope and no envelope f...
553
+
554
+ - lesson_id: lesson-raised-2ab8ba0c5201
555
+ - statement: `smith event append` takes one inline JSON envelope and no envelope flags, while `smith delegation check` takes its argument as a positional.
556
+
557
+ ### lesson-raised-2b2a1e81e546: An orchestrator taking over a dead wave-runner's session must open it...
558
+
559
+ - lesson_id: lesson-raised-2b2a1e81e546
560
+ - statement: An orchestrator taking over a dead wave-runner's session must open its own session with `--continues`, rather than writing into the dead one.
561
+
562
+ ### lesson-raised-f6787a2c84c7: Every markdown file in the repo is scanned by the doc tests unless it...
563
+
564
+ - lesson_id: lesson-raised-f6787a2c84c7
565
+ - statement: Every markdown file in the repo is scanned by the doc tests unless it matches the record-of-the-past shape.
566
+
567
+ ### lesson-raised-b5d795d4b6d3: Verify a research finding against the code before writing it into the...
568
+
569
+ - lesson_id: lesson-raised-b5d795d4b6d3
570
+ - statement: Verify a research finding against the code before writing it into the roadmap.
571
+
572
+ ### lesson-raised-27e836565b55: A reviewer finding can be right about the fact and wrong about the fi...
573
+
574
+ - lesson_id: lesson-raised-27e836565b55
575
+ - statement: A reviewer finding can be right about the fact and wrong about the fix, so the remedy has to be verified separately from the observation.
576
+
577
+ ### lesson-raised-37f438433dfa: Prove a regression test discriminates by reverting the fix and watchi...
578
+
579
+ - lesson_id: lesson-raised-37f438433dfa
580
+ - statement: Prove a regression test discriminates by reverting the fix and watching the test fail; a green run on the already-fixed tree proves nothing.
581
+
582
+ ### lesson-raised-80a2f7a7a5d5: Biome rejects `${VAR+set}` inside a JavaScript string.
583
+
584
+ - lesson_id: lesson-raised-80a2f7a7a5d5
585
+ - statement: Biome rejects `${VAR+set}` inside a JavaScript string.
586
+
587
+ ### lesson-raised-378453209c3b: A red check in a stored background output may predate the fix; compar...
588
+
589
+ - lesson_id: lesson-raised-378453209c3b
590
+ - statement: A red check in a stored background output may predate the fix; compare timestamps before believing it.
591
+
592
+ ### lesson-raised-46dd036c0dd9: `gh pr merge --delete-branch` fails to delete the local branch while ...
593
+
594
+ - lesson_id: lesson-raised-46dd036c0dd9
595
+ - statement: `gh pr merge --delete-branch` fails to delete the local branch while a worktree holds it, even though the merge and the remote branch deletion both succeeded; that error is not a failed merge.
596
+
597
+ ### lesson-raised-e1d2d89d388f: A backticked dotted token in any instruction file is read as an error...
598
+
599
+ - lesson_id: lesson-raised-e1d2d89d388f
600
+ - statement: A backticked dotted token in any instruction file is read as an error code by the doc test.
601
+
602
+ ### lesson-raised-3b940e498974: The event log's key is `event_type`, not `type`.
603
+
604
+ - lesson_id: lesson-raised-3b940e498974
605
+ - statement: The event log's key is `event_type`, not `type`.
606
+
607
+ ### lesson-raised-02785579f29c: A node script that imports a repo dependency must live inside that re...
608
+
609
+ - lesson_id: lesson-raised-02785579f29c
610
+ - statement: A node script that imports a repo dependency must live inside that repo, unless it uses `createRequire` rooted at the target's own manifest.
611
+
612
+ ### lesson-raised-ead91bf35c28: Before proposing that the factory call a library, check whether it is...
613
+
614
+ - lesson_id: lesson-raised-ead91bf35c28
615
+ - statement: Before proposing that the factory call a library, check whether it is a devDependency: the published package ships `dist` plus its runtime dependencies only.
616
+
617
+ ### lesson-raised-d54fac4e1d74: The auto-mode classifier denies `gh pr merge` when it is chained with...
618
+
619
+ - lesson_id: lesson-raised-d54fac4e1d74
620
+ - statement: The auto-mode classifier denies `gh pr merge` when it is chained with another command; the bare form passes.
621
+
622
+ ### lesson-raised-2c7a4d56b146: `gh pr checks --json state` returns `IN_PROGRESS` or `QUEUED`, never ...
623
+
624
+ - lesson_id: lesson-raised-2c7a4d56b146
625
+ - statement: `gh pr checks --json state` returns `IN_PROGRESS` or `QUEUED`, never `PENDING`.
626
+
627
+ ### lesson-raised-d62b53f459bf: About twenty test files read `factory/specs/roadmap.md`, not only the...
628
+
629
+ - lesson_id: lesson-raised-d62b53f459bf
630
+ - statement: About twenty test files read `factory/specs/roadmap.md`, not only the six doc tests, so a wording change there needs the full suite.
631
+
632
+ ### lesson-raised-19254ec2a441: Do not publish a gate claim before the gate finishes: state what was ...
633
+
634
+ - lesson_id: lesson-raised-19254ec2a441
635
+ - statement: Do not publish a gate claim before the gate finishes: state what was actually observed and name CI as the authority.
636
+
637
+ ### lesson-raised-f2920a320b64: Never edit a file while a suite is running against it: the result the...
638
+
639
+ - lesson_id: lesson-raised-f2920a320b64
640
+ - statement: Never edit a file while a suite is running against it: the result then describes no single tree. Kill the run and restart it on the committed tree.
641
+
642
+ ### lesson-raised-7eeb398fef59: Right after a push, `gh pr checks` reports that no checks were report...
643
+
644
+ - lesson_id: lesson-raised-7eeb398fef59
645
+ - statement: Right after a push, `gh pr checks` reports that no checks were reported; a waiter must treat that as still-pending, not as finished.
646
+
647
+ ### lesson-raised-5a731cdab38f: A verification command and the commit that depends on it must be one ...
648
+
649
+ - lesson_id: lesson-raised-5a731cdab38f
650
+ - statement: A verification command and the commit that depends on it must be one `&&` chain, and its output must not be piped past the pass/fail line.
651
+
652
+ ### lesson-raised-3a400ca69b5e: When two verbs are compared for cost, check when each one runs before...
653
+
654
+ - lesson_id: lesson-raised-3a400ca69b5e
655
+ - statement: When two verbs are compared for cost, check when each one runs before concluding: the cheaper one may run after the very gate it was meant to save.
656
+
359
657
  ## security
360
658
 
361
659
  _(none yet)_
@@ -56,14 +56,39 @@ autonomy:
56
56
  #
57
57
  # The operator's standing decision, 2026-09-02: auto-dispatch with a
58
58
  # whitelist. Rechecks and maintenance may run themselves; growth review
59
- # and anything touching a security surface always wait; PRs are still
60
- # merged by a person, which is the backstop that makes this safe to widen.
59
+ # and anything touching a security surface always wait.
60
+ #
61
+ # Amended 2026-09-18: PRs are no longer merged by a person. That sentence
62
+ # used to stand here as the backstop that made auto-dispatch safe to widen,
63
+ # and deleting it without a replacement would leave this block resting on
64
+ # nothing. What replaces it is the six conjunctive conditions in
65
+ # docs/specs/continuous-loop-scope.md 3.5 — green gate with no pending
66
+ # waiver, no open S1/S2, green CI on the merge head, no protected
67
+ # declaration touched, no security keyword matched, and a merge through the
68
+ # PR rather than a push to main. The third of those is the load-bearing
69
+ # one: an epic that would change THIS FILE, autonomy.ts, budgets.yml,
70
+ # delegation.yml, .claude/settings.json, an agent template, a spec schema
71
+ # or the enable marker classifies operator and never reaches the merge at
72
+ # all. The loop may merge what it builds; it may not merge what bounds it.
73
+ #
74
+ # None of those six is enforced by code yet, because `smith loop` does not
75
+ # exist yet. Until it does, this block's `enabled: true` still only governs
76
+ # dispatch, and the merge is performed by whoever is driving the session.
61
77
  enabled: true
62
78
 
63
79
  # Nothing outside this list is ever auto-dispatched, and listing
64
80
  # `growth-review-due` would not change that: architecture §12 keeps scope
65
81
  # with the operator unconditionally, so autonomy.ts denies it whatever this
66
82
  # says. The list is a ceiling, never a floor.
83
+ #
84
+ # `error-report` — the daemon proposing that an error the log holds with
85
+ # no `issue-reported` event be filed (`smith issues report`) — is left out
86
+ # on purpose, and listing it would not change that either. Filing writes
87
+ # to an external issue tracker under an authenticated `gh` token whose
88
+ # scope includes `repo`, and this block's standing decision is that
89
+ # anything touching auth or secrets always waits for a person; autonomy.ts
90
+ # refuses the kind ahead of this list (`tracker-write-never-auto`), the
91
+ # same shape `growth-never-auto` uses.
67
92
  auto_dispatch_kinds: [recheck, maintenance]
68
93
 
69
94
  # Rechecks are whitelisted BY REASON, and this is the one knob in the file
@@ -9,7 +9,7 @@
9
9
  # across months of runs. Changing the taxonomy is a PR that bumps `version` —
10
10
  # never a runtime write. Analytics may group only by taxonomy dimensions.
11
11
 
12
- version: 10
12
+ version: 11
13
13
 
14
14
  # ── Work classification ─────────────────────────────────────────────
15
15
  case: [feature, bugfix, refactor, research, spec-review, recheck,
@@ -32,7 +32,7 @@ agent: [planner, spec-reviewer, researcher, coder, tester, grader,
32
32
  # operator — the human, as a `found_by`: a defect read off the code
33
33
  # by a person. Never dispatched — no template, no budget, no
34
34
  # delegation grant, no judge turn — so nothing that counts
35
- # rounds or pairs roles ever sees it (dogfood-csb-audit-1 FD-21)
35
+ # rounds or pairs roles ever sees it (added after a dogfood run)
36
36
 
37
37
  provider: [claude, codex, deepseek]
38
38
  model_tier: [frontier, mid, small] # fable/opus · sonnet · haiku
@@ -77,7 +77,7 @@ gate_event: [schema-check-result, artifact-check-result,
77
77
  finding-raised, finding-reverified, finding-suppressed,
78
78
  finding-transitioned, finding-reattributed,
79
79
  severity-decisions, waiver-granted, waiver-denied,
80
- gate-outcome]
80
+ gate-outcome, issue-reported]
81
81
  # schema-check-result — task Result vs result.schema.json
82
82
  # artifact-check-result — every artifacts[].path in that
83
83
  # Result, resolved against the task's
@@ -196,6 +196,13 @@ gate_event: [schema-check-result, artifact-check-result,
196
196
  # keyed by finding fingerprint
197
197
  # gate-outcome — the composed gate pipeline's
198
198
  # final structured result
199
+ # issue-reported — issueReporter.ts's record of one
200
+ # attempt to open or comment on an
201
+ # external tracker issue for a
202
+ # gate-outcome, task-failed or
203
+ # error-logged candidate: outcome,
204
+ # reason and the fingerprint group's
205
+ # latest_event_id
199
206
 
200
207
  # ── Errors: grouped two-level classes (always logged as group.class) ─
201
208
  error: