@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.
- package/.claude/agents/coder.md +9 -3
- package/.claude/agents/grader.md +13 -5
- package/.claude/agents/merger.md +2 -0
- package/.claude/agents/planner.md +22 -1
- package/.claude/agents/researcher.md +3 -1
- package/.claude/agents/reviewer.md +9 -2
- package/.claude/agents/security-reviewer.md +15 -7
- package/.claude/agents/spec-reviewer.md +12 -3
- package/.claude/agents/tester.md +5 -2
- package/.claude/agents/uiux.md +10 -3
- package/.claude/agents/verifier.md +8 -3
- package/.claude/skills/bs/SKILL.md +119 -0
- package/.claude/skills/bs/audit.md +252 -0
- package/.claude/skills/bs/dispatch.md +394 -0
- package/.claude/skills/bs/lessons.md +73 -0
- package/.claude/skills/bs/mcp.md +29 -0
- package/.claude/skills/bs/new.md +35 -0
- package/.claude/skills/bs/plan.md +172 -0
- package/.claude/skills/bs/report.md +129 -0
- package/.claude/skills/bs/run.md +327 -0
- package/.claude/skills/bs/status.md +54 -0
- package/.claude/skills/bs/ui.md +19 -0
- package/.claude/skills/bs/waivers.md +13 -0
- package/.claude/skills/bs/wave.md +267 -0
- package/README.md +136 -79
- package/factory/orchestrator/dist/actors.js +4 -3
- package/factory/orchestrator/dist/agents-registry.js +96 -19
- package/factory/orchestrator/dist/artifacts.js +36 -4
- package/factory/orchestrator/dist/budgets.js +7 -5
- package/factory/orchestrator/dist/claims.js +27 -1
- package/factory/orchestrator/dist/cli.js +196 -27
- package/factory/orchestrator/dist/crosscheck.js +11 -0
- package/factory/orchestrator/dist/daemon.js +30 -5
- package/factory/orchestrator/dist/db/projector.js +272 -90
- package/factory/orchestrator/dist/db/queries.js +83 -14
- package/factory/orchestrator/dist/db/schema.js +7 -0
- package/factory/orchestrator/dist/events.js +88 -4
- package/factory/orchestrator/dist/harness.js +516 -0
- package/factory/orchestrator/dist/impact.js +76 -5
- package/factory/orchestrator/dist/judges.js +49 -8
- package/factory/orchestrator/dist/lessons.js +4 -4
- package/factory/orchestrator/dist/mcp.js +4 -3
- package/factory/orchestrator/dist/paths.js +169 -15
- package/factory/orchestrator/dist/plan.js +61 -1
- package/factory/orchestrator/dist/planQuorum.js +36 -8
- package/factory/orchestrator/dist/providers/api-transport.js +62 -5
- package/factory/orchestrator/dist/providers/cli-transport.js +7 -73
- package/factory/orchestrator/dist/providers/index.js +1 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +58 -7
- package/factory/orchestrator/dist/providers/types.js +5 -0
- package/factory/orchestrator/dist/roadmap.js +3 -3
- package/factory/orchestrator/dist/run-cli.js +130 -0
- package/factory/orchestrator/dist/runner.js +264 -0
- package/factory/orchestrator/dist/scaffold.js +3 -2
- package/factory/orchestrator/dist/spawn.js +84 -0
- package/factory/orchestrator/dist/stack.js +10 -2
- package/factory/orchestrator/dist/templateFragments.js +178 -0
- package/factory/orchestrator/dist/usage.js +30 -9
- package/factory/orchestrator/dist/waveNext.js +42 -14
- package/factory/orchestrator/dist/waveSchedule.js +7 -1
- package/factory/orchestrator/dist/workroot.js +113 -0
- package/factory/orchestrator/drizzle/0011_findings_spec_ref.sql +3 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +7 -0
- package/factory/policies/budgets.yml +7 -4
- package/factory/policies/crosscheck.yml +10 -8
- package/factory/policies/harness.yml +82 -0
- package/factory/policies/lessons.md +122 -0
- package/factory/policies/severity.yml +1 -1
- package/factory/policies/taxonomy.yml +6 -2
- package/factory/specs/roadmap.md +154 -0
- package/factory/specs/schema/task-spec.schema.json +5 -0
- package/package.json +10 -7
|
@@ -88,8 +88,8 @@ export const COMMANDS = [
|
|
|
88
88
|
{
|
|
89
89
|
command: 'plan quorum',
|
|
90
90
|
positionals: '',
|
|
91
|
-
flags: '--
|
|
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:
|
|
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
|
-
|
|
90
|
-
|
|
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(
|
|
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,
|
|
158
|
+
const link = (a, b, coupling) => {
|
|
130
159
|
const map = coupledWith.get(a) ?? new Map();
|
|
131
|
-
map.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
66
|
-
# (
|
|
67
|
-
#
|
|
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 (
|
|
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
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
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 (
|
|
36
|
+
model tier (mid -> frontier, budgets.yml); after 3, escalate to
|
|
37
37
|
operator.
|
|
38
38
|
|
|
39
39
|
S3-minor:
|