@juzser/blacksmith 0.1.0 → 0.2.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 (72) hide show
  1. package/.claude/agents/coder.md +9 -3
  2. package/.claude/agents/grader.md +13 -5
  3. package/.claude/agents/merger.md +2 -0
  4. package/.claude/agents/planner.md +22 -1
  5. package/.claude/agents/researcher.md +3 -1
  6. package/.claude/agents/reviewer.md +9 -2
  7. package/.claude/agents/security-reviewer.md +15 -7
  8. package/.claude/agents/spec-reviewer.md +12 -3
  9. package/.claude/agents/tester.md +5 -2
  10. package/.claude/agents/uiux.md +10 -3
  11. package/.claude/agents/verifier.md +8 -3
  12. package/.claude/skills/bs/SKILL.md +119 -0
  13. package/.claude/skills/bs/audit.md +252 -0
  14. package/.claude/skills/bs/dispatch.md +394 -0
  15. package/.claude/skills/bs/lessons.md +73 -0
  16. package/.claude/skills/bs/mcp.md +29 -0
  17. package/.claude/skills/bs/new.md +35 -0
  18. package/.claude/skills/bs/plan.md +172 -0
  19. package/.claude/skills/bs/report.md +129 -0
  20. package/.claude/skills/bs/run.md +327 -0
  21. package/.claude/skills/bs/status.md +54 -0
  22. package/.claude/skills/bs/ui.md +19 -0
  23. package/.claude/skills/bs/waivers.md +13 -0
  24. package/.claude/skills/bs/wave.md +267 -0
  25. package/README.md +136 -79
  26. package/factory/orchestrator/dist/actors.js +4 -3
  27. package/factory/orchestrator/dist/agents-registry.js +96 -19
  28. package/factory/orchestrator/dist/artifacts.js +36 -4
  29. package/factory/orchestrator/dist/budgets.js +7 -5
  30. package/factory/orchestrator/dist/claims.js +27 -1
  31. package/factory/orchestrator/dist/cli.js +196 -27
  32. package/factory/orchestrator/dist/crosscheck.js +11 -0
  33. package/factory/orchestrator/dist/daemon.js +30 -5
  34. package/factory/orchestrator/dist/db/projector.js +272 -90
  35. package/factory/orchestrator/dist/db/queries.js +83 -14
  36. package/factory/orchestrator/dist/db/schema.js +7 -0
  37. package/factory/orchestrator/dist/events.js +88 -4
  38. package/factory/orchestrator/dist/harness.js +516 -0
  39. package/factory/orchestrator/dist/impact.js +76 -5
  40. package/factory/orchestrator/dist/judges.js +49 -8
  41. package/factory/orchestrator/dist/lessons.js +4 -4
  42. package/factory/orchestrator/dist/mcp.js +4 -3
  43. package/factory/orchestrator/dist/paths.js +169 -15
  44. package/factory/orchestrator/dist/plan.js +61 -1
  45. package/factory/orchestrator/dist/planQuorum.js +36 -8
  46. package/factory/orchestrator/dist/providers/api-transport.js +62 -5
  47. package/factory/orchestrator/dist/providers/cli-transport.js +7 -73
  48. package/factory/orchestrator/dist/providers/index.js +1 -0
  49. package/factory/orchestrator/dist/providers/schema-validate.js +58 -7
  50. package/factory/orchestrator/dist/providers/types.js +5 -0
  51. package/factory/orchestrator/dist/roadmap.js +3 -3
  52. package/factory/orchestrator/dist/run-cli.js +130 -0
  53. package/factory/orchestrator/dist/runner.js +264 -0
  54. package/factory/orchestrator/dist/scaffold.js +3 -2
  55. package/factory/orchestrator/dist/spawn.js +84 -0
  56. package/factory/orchestrator/dist/stack.js +10 -2
  57. package/factory/orchestrator/dist/templateFragments.js +178 -0
  58. package/factory/orchestrator/dist/usage.js +30 -9
  59. package/factory/orchestrator/dist/waveNext.js +42 -14
  60. package/factory/orchestrator/dist/waveSchedule.js +7 -1
  61. package/factory/orchestrator/dist/workroot.js +113 -0
  62. package/factory/orchestrator/drizzle/0011_findings_spec_ref.sql +3 -0
  63. package/factory/orchestrator/drizzle/meta/_journal.json +7 -0
  64. package/factory/policies/budgets.yml +7 -4
  65. package/factory/policies/crosscheck.yml +10 -8
  66. package/factory/policies/harness.yml +82 -0
  67. package/factory/policies/lessons.md +122 -0
  68. package/factory/policies/severity.yml +1 -1
  69. package/factory/policies/taxonomy.yml +6 -2
  70. package/factory/specs/roadmap.md +154 -0
  71. package/factory/specs/schema/task-spec.schema.json +5 -0
  72. package/package.json +10 -7
@@ -88,8 +88,8 @@ export const COMMANDS = [
88
88
  {
89
89
  command: 'plan quorum',
90
90
  positionals: '',
91
- flags: '--epic <id> --plan-version <n> --session <id> --causal-parent <event-id> [--confidence <0-1>] [--actor <name>] [--state-dir <dir>]',
92
- summary: 'Critique-only review of a drafted plan. Exit 1 means the operator must look first.',
91
+ flags: '--plan-version <n> --session <id> --causal-parent <event-id> [--epic <id>] [--plan <draft.json>] [--confidence <0-1>] [--out <file>] [--actor <name>] [--specs-dir <dir>] [--state-dir <dir>]',
92
+ summary: 'Critique-only review of a drafted plan: --plan <draft.json> before it is filed, or --epic for the filed version. Exit 1 means the operator must look first.',
93
93
  },
94
94
  {
95
95
  command: 'plan amend',
@@ -162,6 +162,13 @@ export const COMMANDS = [
162
162
  'wide, or were dispatched one task at a time. Reads, never writes. Exit 1 on a wave that ' +
163
163
  'ran strictly serially, 2 when a wave was admitted and the log shows no work for it.',
164
164
  },
165
+ {
166
+ command: 'init',
167
+ positionals: '',
168
+ flags: '[--work-root <dir>]',
169
+ summary: 'Prepare a work root in the current directory: the state directories, and a copy of every ' +
170
+ 'file the factory ships a default of for you to edit. Idempotent; a clone needs none of it.',
171
+ },
165
172
  {
166
173
  command: 'new',
167
174
  positionals: '<project>',
@@ -191,6 +198,7 @@ export const COMMANDS = [
191
198
  positionals: '',
192
199
  flags: `[--interval <seconds>] [--once] [--dir <dir>] [--project <dir>...] [--no-self] [--db <path>] [--no-db] [--state-dir <dir>]`,
193
200
  summary: 'Watch the event log in the foreground: budgets, stale agents, work that is due. ' +
201
+ 'Prints one report per tick, one JSON line each; --once prints one document and exits. ' +
194
202
  'Never dispatches.',
195
203
  },
196
204
  {
@@ -293,8 +301,9 @@ export const COMMANDS = [
293
301
  {
294
302
  command: 'event tail',
295
303
  positionals: '<session-id>',
296
- flags: '[--n <count>] [--task <task-id>] [--lineage] [--state-dir <dir>]',
297
- summary: 'Print the last n records of a session that exists. Exit 1 if it does not.',
304
+ flags: '[--n <count>] [--task <task-id>] [--lineage] [--follow] [--state-dir <dir>]',
305
+ summary: 'Print the last n records of a session that exists. Exit 1 if it does not. ' +
306
+ '--follow keeps printing as the log grows, one JSON record per line, until ^C.',
298
307
  },
299
308
  {
300
309
  command: 'event lineage',
@@ -333,14 +342,14 @@ export const COMMANDS = [
333
342
  form: '--plan',
334
343
  positionals: '<task-id>...',
335
344
  flags: '--plan <file> [--repo <dir>]',
336
- summary: 'Pre-run: refuse a wave whose tasks sit on either end of an import edge. Exit 1 when coupled.',
345
+ summary: 'Pre-run: refuse a wave whose tasks sit on either end of an import edge the producer did not promise in keeps_exports. Exit 1 when coupled.',
337
346
  },
338
347
  {
339
348
  command: 'claims impact',
340
349
  form: 'spec',
341
350
  positionals: '<worktree-dir> <spec.json>',
342
351
  flags: '',
343
- summary: 'Post-run: an export this task removed, still imported outside its claims. Exit 1 on a proven break.',
352
+ summary: "Post-run: an export this task removed, still imported outside its claims, and verifies the spec's keeps_exports promises. Exit 1 on a proven break or a broken promise.",
344
353
  },
345
354
  {
346
355
  command: 'dispatch check',
@@ -378,6 +387,18 @@ export const COMMANDS = [
378
387
  flags: '--task <task.json> [--policy <file>] [--case <case>] [--epic-tag <tag>...] [--recheck]',
379
388
  summary: 'Compute whether the security reviewer must be dispatched. A fired trigger is not a red.',
380
389
  },
390
+ {
391
+ command: 'harness list',
392
+ positionals: '',
393
+ flags: '[--policy <file>]',
394
+ summary: 'The harnesses that may run a worker turn, and which roles each one serves. Shipped policy unless --policy names a file.',
395
+ },
396
+ {
397
+ command: 'harness plan',
398
+ positionals: '',
399
+ flags: '--role <role> --task <id> --prompt-file <path> [--harness <name>] [--worktree <dir>] [--policy <file>] [--tier <frontier|mid|small>] [--schema <name>]',
400
+ summary: 'Render the invocation that would run one worker turn, as JSON. Prints it; never starts it.',
401
+ },
381
402
  {
382
403
  command: 'sandbox open',
383
404
  positionals: '<worktree-dir>',
@@ -637,7 +658,7 @@ export const COMMANDS = [
637
658
  {
638
659
  command: 'crossfind request',
639
660
  positionals: '',
640
- flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--policy <file>]',
661
+ flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>]',
641
662
  summary: 'Print the finder request without sending it — exactly what would leave the machine. Refuses when send_diff is false.',
642
663
  },
643
664
  {
@@ -649,13 +670,13 @@ export const COMMANDS = [
649
670
  {
650
671
  command: 'crossfind run',
651
672
  positionals: '',
652
- flags: `--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--status <status>] [--timeout-ms <n>] [--max-output-bytes <n>] [--policy <file>] ${EVENTS_DIR}`,
673
+ flags: `--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--status <status>] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>] ${EVENTS_DIR}`,
653
674
  summary: 'Run the independent finder over a diff and reconcile it against the native findings. Exit 1 when the result would change a gate.',
654
675
  },
655
676
  {
656
677
  command: 'ui serve',
657
678
  positionals: '',
658
- flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>]',
679
+ flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>] [--now-iso <iso>]',
659
680
  summary: 'Serve the dashboard from the built ui/server.',
660
681
  },
661
682
  {
@@ -85,10 +85,36 @@ function admissionOrder(candidates, edges, crossings) {
85
85
  * until `computeNextWave` has partitioned them by status.
86
86
  */
87
87
  export function liveWaveTasks(plan) {
88
- return livePlanTasks(plan).map((record) => readClaimList({
89
- task_id: record.task_id,
90
- claims: record.claims,
91
- }));
88
+ return livePlanTasks(plan).map((record) => readClaimList(proposedFrom(record)));
89
+ }
90
+ /**
91
+ * The plan record as `readClaimList` wants it: claims always, the
92
+ * `keeps_exports` promise only when the record has one, so a task that
93
+ * promised nothing reads back without the key rather than with `undefined`.
94
+ */
95
+ function proposedFrom(record) {
96
+ const { claims, keeps_exports } = record;
97
+ return keeps_exports === undefined
98
+ ? { task_id: record.task_id, claims }
99
+ : { task_id: record.task_id, claims, keeps_exports };
100
+ }
101
+ const NAMED_SYMBOLS = 3;
102
+ /**
103
+ * The deferral names both files and both remedies, because the operator's
104
+ * choice is between them: order the tasks, or have the producer promise the
105
+ * exporting file in `keeps_exports` (impact.ts verifies it post-run). Only
106
+ * the producer can promise, so the sentence names the producer either way.
107
+ */
108
+ function describeCoupling(taskId, blocker, coupling) {
109
+ const { crossing, role } = coupling;
110
+ const shown = crossing.symbols.slice(0, NAMED_SYMBOLS);
111
+ const symbols = [...shown, ...(crossing.symbols.length > NAMED_SYMBOLS ? ['…'] : [])].join(', ');
112
+ const typeOnly = crossing.typeOnly ? ', type-only' : '';
113
+ return role === 'consumes'
114
+ ? `Imports ${symbols} from ${crossing.exportedBy} (${blocker}) into ${crossing.importedBy}${typeOnly}: ` +
115
+ `the producer runs first, or ${blocker} promises the file in keeps_exports and both run now.`
116
+ : `${blocker} imports ${symbols} from ${crossing.exportedBy} into ${crossing.importedBy}${typeOnly}${typeOnly ? ',' : ''} ` +
117
+ `and is already scheduled, so this one follows, or ${taskId} promises the file in keeps_exports and both run now.`;
92
118
  }
93
119
  export function computeNextWave(input) {
94
120
  const { plan, policy } = input;
@@ -107,7 +133,7 @@ export function computeNextWave(input) {
107
133
  }
108
134
  // Past this door claims are a real string[], for a candidate and for a
109
135
  // task merely holding ground — the wave is unanswerable either way.
110
- const task = readClaimList({ task_id: id, claims: record.claims });
136
+ const task = readClaimList(proposedFrom(record));
111
137
  if (CANDIDATE_TASK_STATUSES.has(status)) {
112
138
  candidates.push(task);
113
139
  }
@@ -125,15 +151,19 @@ export function computeNextWave(input) {
125
151
  list.push(edge.dependsOn);
126
152
  dependsByTask.set(edge.task, list);
127
153
  }
154
+ // task -> (other task -> the first crossing between them, and this task's
155
+ // side of it). First wins: the caller hands crossings in a stable order,
156
+ // and one named crossing is what the deferral renders.
128
157
  const coupledWith = new Map();
129
- const link = (a, b, role) => {
158
+ const link = (a, b, coupling) => {
130
159
  const map = coupledWith.get(a) ?? new Map();
131
- map.set(b, role);
160
+ if (!map.has(b))
161
+ map.set(b, coupling);
132
162
  coupledWith.set(a, map);
133
163
  };
134
164
  for (const crossing of crossings) {
135
- link(crossing.consumer, crossing.producer, 'consumes');
136
- link(crossing.producer, crossing.consumer, 'produces');
165
+ link(crossing.consumer, crossing.producer, { role: 'consumes', crossing });
166
+ link(crossing.producer, crossing.consumer, { role: 'produces', crossing });
137
167
  }
138
168
  const admitted = [];
139
169
  const deferred = [];
@@ -189,11 +219,9 @@ export function computeNextWave(input) {
189
219
  const couplings = coupledWith.get(id);
190
220
  const coupled = couplings ? holders.filter((h) => couplings.has(h.task_id)) : [];
191
221
  const blocker = coupled[0];
192
- if (couplings !== undefined && blocker !== undefined) {
193
- const detail = couplings.get(blocker.task_id) === 'consumes'
194
- ? `Imports symbols from ${blocker.task_id}, which has not merged: the producer runs first.`
195
- : `${blocker.task_id} imports symbols from it and is already scheduled, so this one follows.`;
196
- defer(id, 'symbol-coupled', coupled.map((h) => h.task_id), detail);
222
+ const coupling = couplings?.get(blocker?.task_id ?? '');
223
+ if (blocker !== undefined && coupling !== undefined) {
224
+ defer(id, 'symbol-coupled', coupled.map((h) => h.task_id), describeCoupling(id, blocker.task_id, coupling));
197
225
  continue;
198
226
  }
199
227
  admitted.push(task);
@@ -65,10 +65,16 @@ function buildHint(stalled, occupied, constraints, depth) {
65
65
  }
66
66
  if (constraints.length > 0) {
67
67
  const reasons = constraints.map((c) => c.reason);
68
+ // A crossing has a remedy that is not a re-slice, and the hint is where
69
+ // the operator learns it exists: the producer promises the exporting file
70
+ // in `keeps_exports`, and `claims impact` verifies the promise post-run.
71
+ const promise = reasons.includes('symbol-coupled')
72
+ ? ' A symbol-coupled task widens when its producer lists the exporting file in keeps_exports (verified post-run).'
73
+ : '';
68
74
  return (`This plan runs in ${depth} ${depth === 1 ? 'round' : 'rounds'}, and some of that ` +
69
75
  `depth is claim geometry rather than dependencies (${nameList(reasons)}). ` +
70
76
  'Re-slicing the claims may widen it; whether it does depends on the dependency graph, ' +
71
- 'which this command does not speculate about.');
77
+ `which this command does not speculate about.${promise}`);
72
78
  }
73
79
  return '';
74
80
  }
@@ -0,0 +1,113 @@
1
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { REPO_ROOT, ROADMAP_DEFAULT_PATH, ROADMAP_PATH, SPECS_ACTIVE_DIR, STACK_POLICY_DEFAULT_PATH, STATE_ARTIFACTS_DIR, STATE_EVENTS_DIR, WORK_ROOT, } from './paths.js';
4
+ /**
5
+ * The files `paths.ts` declares twice: a default that ships and a copy the
6
+ * operator then edits. Spelled relative to their own root, because the pairing
7
+ * rule -- enforced in packaging.test.ts -- is that both halves are the same
8
+ * relative path under two roots, which is what lets seeding be a plain copy.
9
+ *
10
+ * Derived from the constants rather than retyped, so a third overlay file
11
+ * added to `paths.ts` is seeded without anyone remembering to edit this list.
12
+ */
13
+ export const OVERLAY_FILES = [
14
+ path.relative(REPO_ROOT, ROADMAP_DEFAULT_PATH),
15
+ path.relative(REPO_ROOT, STACK_POLICY_DEFAULT_PATH),
16
+ ];
17
+ // `factory/policies/lessons.md` is the third overlay in `paths.ts` and is
18
+ // deliberately not in that list. The two above are seeded because INSTALL.md
19
+ // has the operator open them and edit them in place -- they must exist before
20
+ // anyone can answer. Nobody hand-edits lessons.md: it appears the first time
21
+ // `smith lessons compile` runs, and until then reading the shipped set is the
22
+ // right answer. Seeding a copy would freeze the lessons at install time and
23
+ // stop every later upgrade from delivering new ones.
24
+ /**
25
+ * The directories the CLI writes into. Same derivation, same reason: the
26
+ * relative spelling is `paths.ts`'s to decide, and this module should follow
27
+ * it rather than hold a second opinion about where `state/events` is.
28
+ */
29
+ const WORK_DIRS = [
30
+ path.relative(WORK_ROOT, STATE_EVENTS_DIR),
31
+ path.relative(WORK_ROOT, STATE_ARTIFACTS_DIR),
32
+ path.relative(WORK_ROOT, SPECS_ACTIVE_DIR),
33
+ ];
34
+ /**
35
+ * `state/` is a cache of the event log and `workspaces/` holds worktrees; both
36
+ * are this machine's, not the project's. `factory/` is deliberately absent:
37
+ * the roadmap and the stack answers are declarations the operator made, and a
38
+ * team sharing the repository should share them.
39
+ */
40
+ const WORK_ROOT_GITIGNORE = `# Written by \`smith init\`. Blacksmith's state is a cache of its event log
41
+ # and belongs to this machine; the answers under factory/ are declarations
42
+ # this project made, so they are tracked.
43
+ state/
44
+ workspaces/
45
+ .env
46
+ `;
47
+ /**
48
+ * Give the operator their own copy of a file the factory ships a default of.
49
+ *
50
+ * Never overwrites: an operator who answers the stack questionnaire, upgrades,
51
+ * and runs init again out of habit must not find the defaults back. And never
52
+ * copies a file onto itself, which is the clone case -- there the personal
53
+ * copy *is* the shipped default, and the honest report is that nothing needed
54
+ * doing.
55
+ */
56
+ export function seedFromShipped(personal, shipped) {
57
+ if (personal === shipped || existsSync(personal)) {
58
+ return { personal, shipped, status: 'kept' };
59
+ }
60
+ if (!existsSync(shipped)) {
61
+ // Not an throw: a missing default is a packaging defect, and the caller --
62
+ // `smith init`, which is reporting a list -- can say which file is missing
63
+ // more usefully than an exception thrown halfway through the list can.
64
+ return { personal, shipped, status: 'missing-default' };
65
+ }
66
+ mkdirSync(path.dirname(personal), { recursive: true });
67
+ copyFileSync(shipped, personal);
68
+ return { personal, shipped, status: 'seeded' };
69
+ }
70
+ /**
71
+ * The roadmap, guaranteed writable, returned.
72
+ *
73
+ * Used as the *default argument* of the functions that append to the roadmap,
74
+ * so that an explicitly-passed path is never seeded -- a caller naming a file
75
+ * is naming the file it means, and a missing one is still an error there.
76
+ */
77
+ export function ensureWritableRoadmap() {
78
+ seedFromShipped(ROADMAP_PATH, ROADMAP_DEFAULT_PATH);
79
+ return ROADMAP_PATH;
80
+ }
81
+ /**
82
+ * Make a work root that the CLI can run against: the directories it writes
83
+ * into, a copy of each file the operator is expected to edit, and -- only when
84
+ * the work root is a directory of our own inside somebody else's repository --
85
+ * a `.gitignore` saying which half of it is state.
86
+ *
87
+ * Idempotent by construction: every step asks whether it has already been
88
+ * done. Running it twice is not an undo.
89
+ */
90
+ export function initWorkRoot(options = {}) {
91
+ const workRoot = options.workRoot ?? WORK_ROOT;
92
+ const repoRoot = options.repoRoot ?? REPO_ROOT;
93
+ const inPlace = path.resolve(workRoot) === path.resolve(repoRoot);
94
+ for (const rel of WORK_DIRS) {
95
+ mkdirSync(path.join(workRoot, rel), { recursive: true });
96
+ }
97
+ const files = OVERLAY_FILES.map((rel) => seedFromShipped(path.join(workRoot, rel), path.join(repoRoot, rel)));
98
+ // In a clone this is the repository's own .gitignore, which means something
99
+ // already. Appending to it would be a surprise and replacing it would be
100
+ // data loss, so the only correct move is to leave it entirely alone.
101
+ let gitignore = 'not-applicable';
102
+ if (!inPlace) {
103
+ const ignorePath = path.join(workRoot, '.gitignore');
104
+ if (existsSync(ignorePath)) {
105
+ gitignore = 'kept';
106
+ }
107
+ else {
108
+ writeFileSync(ignorePath, WORK_ROOT_GITIGNORE, 'utf8');
109
+ gitignore = 'written';
110
+ }
111
+ }
112
+ return { workRoot, repoRoot, inPlace, directories: WORK_DIRS, files, gitignore };
113
+ }
@@ -0,0 +1,3 @@
1
+ ALTER TABLE `findings` ADD `finding_scope` text DEFAULT 'diff' NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE `findings` ADD `spec_plan_version` integer;--> statement-breakpoint
3
+ ALTER TABLE `findings` ADD `criterion_ref` text;
@@ -78,6 +78,13 @@
78
78
  "when": 1788429177107,
79
79
  "tag": "0010_milestone_kind",
80
80
  "breakpoints": true
81
+ },
82
+ {
83
+ "idx": 11,
84
+ "version": "6",
85
+ "when": 1789391823211,
86
+ "tag": "0011_findings_spec_ref",
87
+ "breakpoints": true
81
88
  }
82
89
  ]
83
90
  }
@@ -62,9 +62,12 @@
62
62
  # Every other number below is prompt-level: read by agents from their own
63
63
  # template and by the operator from the /bs playbook. They are requests, not
64
64
  # limits, and the sections that follow say which is which. `max_turns` is no
65
- # longer among them at all — no dispatch harness can enforce a turn limit
66
- # (agent-interviews.md M-4), so plan validation now rejects it as a task budget
67
- # field and the number belongs in the dispatch prompt.
65
+ # longer among them at all: the turn ceiling is the role template's
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 —
68
+ # agent-interviews.md M-4 had called it inert), and a plan cannot raise or
69
+ # lower a role's ceiling. So plan validation rejects it as a task budget
70
+ # field, and the dispatch prompt restates the template's number verbatim.
68
71
  #
69
72
  # No concurrency cap on cost grounds, by design (removed 2026-08-05). Workers
70
73
  # run in isolated worktrees on disjoint path claims, so parallelism is bounded
@@ -232,7 +235,7 @@ escalation_ladder:
232
235
  trigger: 2 failed rounds on the same task
233
236
  failed_rounds: 2
234
237
  enforce: model-tier
235
- action: escalate model tier automatically (sonnet -> opus), logged
238
+ action: escalate model tier automatically (mid -> frontier, taxonomy.yml model_tier), logged
236
239
  - rung: 3
237
240
  trigger: 3 failed rounds on the same task
238
241
  failed_rounds: 3
@@ -113,14 +113,16 @@ providers:
113
113
  #
114
114
  # Funded for THIS box, 2026-09-04, by the operator who owns the key.
115
115
  enabled: auto
116
- # `shadow`, deliberately, and the difference from codex above is the point:
117
- # this box declined shadow for codex and chose it here. A shadow provider
118
- # is still called and still recorded as a participant -- it forfeits only
119
- # gating power -- so what the mode buys is a run of recorded disagreement
120
- # to read before a second vendor gets a vote (docs/runbooks/providers.md
121
- # §3). Promote by editing this to `active`; that is the calibration pass,
122
- # and it is an operator decision, not a default.
123
- mode: shadow
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`
118
+ # with all three participants at `confirm`: with claude excluded as the
119
+ # finder and deepseek in shadow, the gating pool was codex alone (1 <
120
+ # `quorum_rule.min_providers` 2), so no mechanically-ready epic could ever
121
+ # reach quorum on this box (epic.ts, "exactly 1 active external"). The
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).
124
+ # Rollback is `mode: shadow` here; nothing else remembers the promotion.
125
+ mode: active
124
126
 
125
127
  quorum_triggers:
126
128
  # architecture §6, agent-constraints.md "cross-check (phase 8)" — all four,
@@ -0,0 +1,82 @@
1
+ # Blacksmith — worker harness policy
2
+ #
3
+ # architecture §19 "Worker harness port". A *provider* is the model vendor a
4
+ # judge's verdict comes from (crosscheck.yml); a *harness* is the program
5
+ # that holds a worker turn — gives the agent its tools, its working directory,
6
+ # its turn limit. HOST: `smith harness list` / `smith harness plan`
7
+ # (factory/orchestrator/src/harness.ts), which read this file when `--policy`
8
+ # names no other. `smith` renders an invocation from it and never starts one
9
+ # (§18 rule 3); `smith-run` (src/run-cli.ts) is the separate binary that does.
10
+ #
11
+ # Shape, per harness:
12
+ # kind `in-process` — the Claude Code Agent tool, no program started
13
+ # (`command`/`args` unused); `cli` — a program `smith-run` spawns
14
+ # with the rendered prompt on stdin.
15
+ # roles taxonomy roles this harness serves; empty = every role that
16
+ # ships a `.claude/agents/<role>.md`.
17
+ # args argv for every turn. Placeholders, closed set:
18
+ # {prompt_file} {worktree} {role} {task} {model} {schema_file}.
19
+ # worker_args appended for a worker or write-scoped role (guardrails.yml
20
+ # `role_write_scopes`).
21
+ # judge_args appended for a judge role (guardrails.yml `judge_sandbox.roles`).
22
+ # Non-empty means the program itself enforces read-only, and
23
+ # only then may a judge on this harness be handed a worktree
24
+ # (§18 rule 5 — the refusal `harness.judge-worktree` stands
25
+ # when this is empty). Rule 6's fingerprint runs regardless.
26
+ # schema_args appended only when the turn names a schema (`--schema`), so a
27
+ # turn that validates nothing renders without one.
28
+ # output how smith-run reads the program's output: `text` (default),
29
+ # `codex-json` (the `codex exec --json` JSONL stream),
30
+ # `claude-json` (the `claude -p --output-format json` object).
31
+ # models tier → model id for `{model}`; tiers are taxonomy.yml
32
+ # `model_tier` (frontier | mid | small). Optional.
33
+ # env variable NAMES the child may read. Never values.
34
+ # timeout_ms wall-clock cap per turn; max_output_bytes defaults to 2 MiB.
35
+ #
36
+ # A cli harness runs in the worktree it is handed (`cwd` on the rendered
37
+ # invocation), so no `-C`/`--add-dir` is needed to point it there, and a turn
38
+ # with no worktree still renders.
39
+
40
+ version: 2
41
+ default: claude-code
42
+ harnesses:
43
+ # The Claude Code Agent tool: in-process, starts no program. Serves every
44
+ # role with a template. Still the default (§19: switching harnesses is a
45
+ # `--harness` flag on `plan`, not an edit here).
46
+ - name: claude-code
47
+ kind: in-process
48
+
49
+ # OpenAI Codex CLI. `-s workspace-write` / `-s read-only` are Codex's own
50
+ # sandbox modes, which is what lets a judge role hold a worktree here. They
51
+ # land after the `-` (read the prompt from stdin) positional, which Codex's
52
+ # parser accepts: `codex exec - -s bogus` is refused as an invalid --sandbox
53
+ # value, not taken as a second prompt.
54
+ # No `-m {model}` and no `models:` are shipped: Codex runs whatever model its
55
+ # own config names, and this file does not guess which of its models is
56
+ # frontier, mid or small. An operator who wants tier routing adds
57
+ # `models: { frontier: <id>, mid: <id>, small: <id> }` and `-m`, `{model}`
58
+ # to `args`; the templates' move to `tier:` frontmatter (PR 2) fills it in.
59
+ - name: codex-cli
60
+ kind: cli
61
+ command: codex
62
+ args: ["exec", "--json", "--color", "never", "--skip-git-repo-check", "-"]
63
+ worker_args: ["-s", "workspace-write"]
64
+ judge_args: ["-s", "read-only"]
65
+ schema_args: ["--output-schema", "{schema_file}"]
66
+ output: codex-json
67
+ env: [HOME, PATH, CODEX_HOME]
68
+ timeout_ms: 1800000
69
+
70
+ # Claude Code as a separate process, for a runner that is not itself a Claude
71
+ # Code session. A judge gets no write tools at all (`--disallowedTools`),
72
+ # which is the read-only promise `judge_args` requires.
73
+ - name: claude-cli
74
+ kind: cli
75
+ command: claude
76
+ args: ["-p", "--output-format", "json", "--model", "{model}"]
77
+ worker_args: []
78
+ judge_args: ["--disallowedTools", "Write,Edit,MultiEdit,NotebookEdit"]
79
+ output: claude-json
80
+ models: { frontier: opus, mid: sonnet, small: haiku }
81
+ env: [HOME, PATH, ANTHROPIC_API_KEY, CLAUDE_CONFIG_DIR]
82
+ timeout_ms: 1800000
@@ -138,6 +138,43 @@ nothing.
138
138
  - claim_path: **
139
139
  - statement: A wrapper that collapses a child's exit signal into an exit code has deleted the only evidence that distinguishes a crash from a failure, and every diagnosis downstream of it will be about the wrong subsystem.
140
140
 
141
+ ### lesson-raised-7299eef43e7d: npm-packlist drops `.gitignore` and `.npmignore` unconditionally, eve...
142
+
143
+ - lesson_id: lesson-raised-7299eef43e7d
144
+ - claim_path: {package.json,factory/scaffold/**}
145
+ - statement: npm-packlist drops `.gitignore` and `.npmignore` unconditionally, even inside directories the `files` allowlist names; a scaffold that must ship one ships a template under another name (`.gitignore.tmpl`) and renames it on write.
146
+
147
+ ### lesson-raised-699f909a6ab2: `npm pack --json` stdout carries the `prepare` script's echo before t...
148
+
149
+ - lesson_id: lesson-raised-699f909a6ab2
150
+ - claim_path: package.json
151
+ - statement: `npm pack --json` stdout carries the `prepare` script's echo before the JSON; parse from the first `[` or `{`, not the whole stream.
152
+
153
+ ### lesson-raised-fee67ee36513: In a backtick `smith` span, the `docCommands` lint reads an unspaced ...
154
+
155
+ - lesson_id: lesson-raised-fee67ee36513
156
+ - claim_path: {docs,.claude}/**
157
+ - statement: In a backtick `smith` span, the `docCommands` lint reads an unspaced `|` as alternation between verbs and a spaced ` | ` as a shell pipe; write the one you mean.
158
+
159
+ ### lesson-raised-96cb1b268004: `tsc` does not delete orphaned outputs; a deleted source leaves its `...
160
+
161
+ - lesson_id: lesson-raised-96cb1b268004
162
+ - claim_path: factory/orchestrator/dist/**
163
+ - statement: `tsc` does not delete orphaned outputs; a deleted source leaves its `dist/*.js` behind and the tarball ships it. Clean `dist/` before a pack.
164
+
165
+ ### lesson-raised-f78f3641f4bd: A `files` allowlist is a claim about what the product is, true only o...
166
+
167
+ - lesson_id: lesson-raised-f78f3641f4bd
168
+ - finding_category: correctness
169
+ - claim_path: package.json
170
+ - statement: A `files` allowlist is a claim about what the product is, true only once someone installs the tarball into an empty directory and runs it; a packaging change is not done until that install proof has run.
171
+
172
+ ### lesson-raised-ccadfedf2299: A packaging guard reads each path constant's spelling from source tex...
173
+
174
+ - lesson_id: lesson-raised-ccadfedf2299
175
+ - claim_path: factory/orchestrator/test/packaging.test.ts
176
+ - statement: A packaging guard reads each path constant's spelling from source text, not its runtime value, because under an install the roots resolve to places the test suite can never stand.
177
+
141
178
  ## case-type
142
179
 
143
180
  _(none yet)_
@@ -234,6 +271,91 @@ _(none yet)_
234
271
  - lesson_id: lesson-raised-b8db8efe922d
235
272
  - 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.
236
273
 
274
+ ### lesson-raised-66bfd37ce01a: Never override the commit author with `git -c user.email=...`; the re...
275
+
276
+ - lesson_id: lesson-raised-66bfd37ce01a
277
+ - statement: Never override the commit author with `git -c user.email=...`; the repo's configured identity is the author, and a per-command override writes a commit under the wrong name that a rewrite has to undo.
278
+
279
+ ### lesson-raised-962e5eed0b7b: A skill's `description:` frontmatter is system-prompt cost on every t...
280
+
281
+ - lesson_id: lesson-raised-962e5eed0b7b
282
+ - statement: A skill's `description:` frontmatter is system-prompt cost on every turn; keep it to routing, not prose.
283
+
284
+ ### lesson-raised-c11ee5a1819d: A truncated listing (`| head -N`) is not a measurement; count from th...
285
+
286
+ - lesson_id: lesson-raised-c11ee5a1819d
287
+ - statement: A truncated listing (`| head -N`) is not a measurement; count from the full output or a counter before stating a number.
288
+
289
+ ### lesson-raised-cf980af1e570: After a pipe, `$?` is the last stage's exit code; use `${pipestatus[n...
290
+
291
+ - lesson_id: lesson-raised-cf980af1e570
292
+ - statement: After a pipe, `$?` is the last stage's exit code; use `${pipestatus[n]}` (zsh) and read it before any subshell, which discards it. The "unknown flag exits 0" observation was this.
293
+
294
+ ### lesson-raised-fc0191ec66ae: `cd` inside a compound command re-scopes the rest of it, and the work...
295
+
296
+ - lesson_id: lesson-raised-fc0191ec66ae
297
+ - statement: `cd` inside a compound command re-scopes the rest of it, and the working directory resets between tool calls; use absolute paths or `git -C <dir>` rather than relying on a prior `cd`.
298
+
299
+ ### lesson-raised-0d7ff58845b2: `pnpm biome check` must run from the repo root; run from `factory/orc...
300
+
301
+ - lesson_id: lesson-raised-0d7ff58845b2
302
+ - statement: `pnpm biome check` must run from the repo root; run from `factory/orchestrator` it checks a different file set and reports a different baseline.
303
+
304
+ ### lesson-raised-720003607334: Resolve another branch's merge conflict in a throwaway worktree off `...
305
+
306
+ - lesson_id: lesson-raised-720003607334
307
+ - statement: Resolve another branch's merge conflict in a throwaway worktree off `origin/<branch>` (`git worktree add --quiet <dir> -b <branch> origin/<ref>`), never by switching the current tree.
308
+
309
+ ### lesson-raised-49b3f7dc407d: When both sides of a conflict appended to the same list (a CHANGELOG ...
310
+
311
+ - lesson_id: lesson-raised-49b3f7dc407d
312
+ - statement: When both sides of a conflict appended to the same list (a CHANGELOG section, a `files` allowlist), the resolution is the union of both additions, not a pick of one side.
313
+
314
+ ### lesson-raised-5b1e157a4b4d: A red `main` makes every PR's check meaningless; fix `main` first, th...
315
+
316
+ - lesson_id: lesson-raised-5b1e157a4b4d
317
+ - statement: A red `main` makes every PR's check meaningless; fix `main` first, then re-run the PR checks against it.
318
+
319
+ ### lesson-raised-47c273e5949b: Every `.md` under the instruction surface is doc-linted, so a scope d...
320
+
321
+ - lesson_id: lesson-raised-47c273e5949b
322
+ - statement: Every `.md` under the instruction surface is doc-linted, so a scope doc that names a verb not yet built turns `main` red; declare it with a self-expiring `<!-- PLANNED-NAMESPACE: <ns> -->` marker rather than widening the lint's exclusion list.
323
+
324
+ ### lesson-raised-6e713c40cfc7: `git stash` unstages a `git mv`; the rename comes back as a delete pl...
325
+
326
+ - lesson_id: lesson-raised-6e713c40cfc7
327
+ - statement: `git stash` unstages a `git mv`; the rename comes back as a delete plus an untracked file, so re-stage the rename after `stash pop`.
328
+
329
+ ### lesson-raised-9b485954d931: `rm -rf` is hook-blocked outside `workspaces/` and `state/`; remove a...
330
+
331
+ - lesson_id: lesson-raised-9b485954d931
332
+ - statement: `rm -rf` is hook-blocked outside `workspaces/` and `state/`; remove a worktree with `git worktree remove --force` and a file with a targeted `rm`.
333
+
334
+ ### lesson-raised-ffadc4019340: Verify a new guard by mutation, not by a green run: reintroduce each ...
335
+
336
+ - lesson_id: lesson-raised-ffadc4019340
337
+ - statement: Verify a new guard by mutation, not by a green run: reintroduce each defect it claims to catch and watch it fail by name before trusting the pass.
338
+
339
+ ### lesson-raised-3c9f8b1c9d17: `npm publish` answering `404 ...
340
+
341
+ - lesson_id: lesson-raised-3c9f8b1c9d17
342
+ - statement: `npm publish` answering `404 ... is not in this registry` for a scoped package is an authentication failure in disguise, not a missing package; check `npm whoami` before anything else.
343
+
344
+ ### lesson-raised-43a115153ca8: npm's read path lags a first publish by roughly 45 seconds; confirm a...
345
+
346
+ - lesson_id: lesson-raised-43a115153ca8
347
+ - statement: npm's read path lags a first publish by roughly 45 seconds; confirm a publish with `npm access list packages`, not `npm view`, before concluding it failed.
348
+
349
+ ### lesson-raised-b223426aa4f6: `grep -r ...
350
+
351
+ - lesson_id: lesson-raised-b223426aa4f6
352
+ - statement: `grep -r ... --include=...` with no path argument reads stdin and hangs the tool call forever; always pass an explicit root.
353
+
354
+ ### lesson-raised-bc8c041f3a8c: A hand-maintained list inside a guard is the hole the guard was built...
355
+
356
+ - lesson_id: lesson-raised-bc8c041f3a8c
357
+ - 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
+
237
359
  ## security
238
360
 
239
361
  _(none yet)_
@@ -33,7 +33,7 @@ levels:
33
33
  - behavioral-drift touching a core flow
34
34
  on_trigger: >
35
35
  bounce to coder on the same branch; after 2 failed rounds, escalate
36
- model tier (sonnet -> opus, budgets.yml); after 3, escalate to
36
+ model tier (mid -> frontier, budgets.yml); after 3, escalate to
37
37
  operator.
38
38
 
39
39
  S3-minor: