@juzser/blacksmith 0.3.0 → 0.5.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 (141) hide show
  1. package/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
  2. package/.claude/agents/{coder.md → bs-coder.md} +8 -7
  3. package/.claude/agents/{grader.md → bs-grader.md} +16 -9
  4. package/.claude/agents/{merger.md → bs-merger.md} +4 -4
  5. package/.claude/agents/{planner.md → bs-planner.md} +20 -6
  6. package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
  7. package/.claude/agents/{reviewer.md → bs-reviewer.md} +16 -5
  8. package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
  9. package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
  10. package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -10
  11. package/.claude/agents/{tester.md → bs-tester.md} +40 -12
  12. package/.claude/agents/bs-uiux.md +257 -0
  13. package/.claude/agents/{verifier.md → bs-verifier.md} +7 -4
  14. package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
  15. package/.claude/skills/bs/SKILL.md +99 -21
  16. package/.claude/skills/bs/audit.md +45 -31
  17. package/.claude/skills/bs/dispatch.md +133 -47
  18. package/.claude/skills/bs/lessons.md +28 -14
  19. package/.claude/skills/bs/mcp.md +4 -4
  20. package/.claude/skills/bs/new.md +3 -3
  21. package/.claude/skills/bs/plan.md +57 -16
  22. package/.claude/skills/bs/report.md +11 -11
  23. package/.claude/skills/bs/run.md +190 -52
  24. package/.claude/skills/bs/status.md +14 -7
  25. package/.claude/skills/bs/ui.md +21 -5
  26. package/.claude/skills/bs/waivers.md +78 -2
  27. package/.claude/skills/bs/wave.md +143 -50
  28. package/README.md +53 -32
  29. package/factory/orchestrator/dist/agentNames.js +43 -0
  30. package/factory/orchestrator/dist/agents-registry.js +11 -4
  31. package/factory/orchestrator/dist/agentsSync.js +70 -13
  32. package/factory/orchestrator/dist/artifacts.js +31 -0
  33. package/factory/orchestrator/dist/attribution.js +4 -0
  34. package/factory/orchestrator/dist/audit.js +245 -20
  35. package/factory/orchestrator/dist/autonomy.js +6 -2
  36. package/factory/orchestrator/dist/budgetAlarm.js +132 -37
  37. package/factory/orchestrator/dist/budgets.js +302 -84
  38. package/factory/orchestrator/dist/claims.js +9 -19
  39. package/factory/orchestrator/dist/cli.js +1087 -116
  40. package/factory/orchestrator/dist/cliName.js +27 -0
  41. package/factory/orchestrator/dist/crossFinding.js +20 -0
  42. package/factory/orchestrator/dist/crosscheck.js +136 -8
  43. package/factory/orchestrator/dist/daemon.js +352 -67
  44. package/factory/orchestrator/dist/db/projector.js +327 -54
  45. package/factory/orchestrator/dist/db/queries.js +1831 -94
  46. package/factory/orchestrator/dist/db/schema.js +57 -4
  47. package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
  48. package/factory/orchestrator/dist/delegation.js +86 -19
  49. package/factory/orchestrator/dist/diffstat.js +15 -8
  50. package/factory/orchestrator/dist/dispatchLint.js +108 -0
  51. package/factory/orchestrator/dist/effort.js +14 -2
  52. package/factory/orchestrator/dist/effortTiers.js +13 -0
  53. package/factory/orchestrator/dist/env.js +26 -0
  54. package/factory/orchestrator/dist/epic.js +494 -42
  55. package/factory/orchestrator/dist/epicBudget.js +93 -0
  56. package/factory/orchestrator/dist/epicWidth.js +22 -5
  57. package/factory/orchestrator/dist/errorIssues.js +182 -11
  58. package/factory/orchestrator/dist/escalation.js +2 -2
  59. package/factory/orchestrator/dist/events.js +164 -14
  60. package/factory/orchestrator/dist/feedback.js +169 -0
  61. package/factory/orchestrator/dist/findingContext.js +26 -0
  62. package/factory/orchestrator/dist/findings.js +108 -14
  63. package/factory/orchestrator/dist/gate.js +120 -10
  64. package/factory/orchestrator/dist/gh.js +19 -2
  65. package/factory/orchestrator/dist/git.js +20 -2
  66. package/factory/orchestrator/dist/goalCheck.js +2 -2
  67. package/factory/orchestrator/dist/harness.js +25 -17
  68. package/factory/orchestrator/dist/hookDecision.js +411 -39
  69. package/factory/orchestrator/dist/integration.js +20 -8
  70. package/factory/orchestrator/dist/issueReporter.js +81 -16
  71. package/factory/orchestrator/dist/judgePreflight.js +25 -4
  72. package/factory/orchestrator/dist/judgeRoles.js +47 -0
  73. package/factory/orchestrator/dist/judgeStopHook.js +239 -0
  74. package/factory/orchestrator/dist/judges.js +419 -21
  75. package/factory/orchestrator/dist/lessons.js +190 -11
  76. package/factory/orchestrator/dist/logCache.js +291 -0
  77. package/factory/orchestrator/dist/mcp.js +6 -6
  78. package/factory/orchestrator/dist/paths.js +20 -7
  79. package/factory/orchestrator/dist/plan.js +399 -33
  80. package/factory/orchestrator/dist/planQuorum.js +5 -2
  81. package/factory/orchestrator/dist/policy.js +1275 -30
  82. package/factory/orchestrator/dist/projectName.js +25 -0
  83. package/factory/orchestrator/dist/projects.js +12 -1
  84. package/factory/orchestrator/dist/promptCapture.js +306 -0
  85. package/factory/orchestrator/dist/promptHook.js +33 -0
  86. package/factory/orchestrator/dist/promptImport.js +281 -0
  87. package/factory/orchestrator/dist/prompts.js +13 -0
  88. package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
  89. package/factory/orchestrator/dist/providers/types.js +1 -1
  90. package/factory/orchestrator/dist/queue.js +471 -30
  91. package/factory/orchestrator/dist/quorum.js +18 -5
  92. package/factory/orchestrator/dist/results.js +25 -7
  93. package/factory/orchestrator/dist/roadmap.js +10 -5
  94. package/factory/orchestrator/dist/run-cli.js +15 -10
  95. package/factory/orchestrator/dist/runner.js +2 -2
  96. package/factory/orchestrator/dist/scheduler.js +3 -3
  97. package/factory/orchestrator/dist/schemas.js +56 -8
  98. package/factory/orchestrator/dist/severity.js +8 -4
  99. package/factory/orchestrator/dist/spec.js +246 -12
  100. package/factory/orchestrator/dist/specChange.js +13 -9
  101. package/factory/orchestrator/dist/stack.js +6 -6
  102. package/factory/orchestrator/dist/storeRoot.js +90 -0
  103. package/factory/orchestrator/dist/symbols.js +261 -6
  104. package/factory/orchestrator/dist/taskEvents.js +192 -31
  105. package/factory/orchestrator/dist/taskStatus.js +14 -0
  106. package/factory/orchestrator/dist/taxonomy.js +11 -0
  107. package/factory/orchestrator/dist/testerAudit.js +31 -12
  108. package/factory/orchestrator/dist/testgate.js +20 -16
  109. package/factory/orchestrator/dist/uiuxGate.js +83 -0
  110. package/factory/orchestrator/dist/usage.js +113 -48
  111. package/factory/orchestrator/dist/waivers.js +99 -12
  112. package/factory/orchestrator/dist/waveBudget.js +2 -0
  113. package/factory/orchestrator/dist/waveConcurrency.js +143 -7
  114. package/factory/orchestrator/dist/waveSchedule.js +84 -0
  115. package/factory/orchestrator/dist/workroot.js +1 -1
  116. package/factory/orchestrator/dist/worktree.js +76 -21
  117. package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
  118. package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
  119. package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
  120. package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
  121. package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
  122. package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
  123. package/factory/policies/budgets.yml +123 -33
  124. package/factory/policies/crosscheck.yml +51 -13
  125. package/factory/policies/delegation.yml +3 -3
  126. package/factory/policies/effort.yml +18 -10
  127. package/factory/policies/guardrails.yml +8 -6
  128. package/factory/policies/harness.yml +5 -5
  129. package/factory/policies/lessons.md +53 -2
  130. package/factory/policies/scheduler.yml +4 -4
  131. package/factory/policies/sensitive-paths.yml +1 -1
  132. package/factory/policies/severity.yml +13 -1
  133. package/factory/policies/stack.yml +6 -6
  134. package/factory/policies/taxonomy.yml +9 -3
  135. package/factory/policies/worktree.yml +15 -9
  136. package/factory/specs/roadmap.md +27 -3
  137. package/factory/specs/schema/event.schema.json +8 -1
  138. package/factory/specs/schema/result.schema.json +21 -8
  139. package/factory/specs/schema/task-spec.schema.json +12 -0
  140. package/package.json +16 -12
  141. package/.claude/agents/uiux.md +0 -133
@@ -8,18 +8,37 @@ import { taskIdsMatch } from './taskId.js';
8
8
  */
9
9
  const TEST_GATE_EVENT = 'testgate-result';
10
10
  /**
11
- * How a dispatched agent's turn is recorded as over, and which payload field
12
- * names the role that ended it. Falling back to the record's `actor` is not a
13
- * nicety: the older half of the log names the role there and nowhere else,
14
- * and reading only the payload would report those runs as testers that never
15
- * reported — an `unverifiable` about evidence the log is holding.
11
+ * How a dispatched agent's turn is recorded as over, and which payload
12
+ * field(s) name the role that ended it, tried in order. Falling back to the
13
+ * record's `actor` is not a nicety: the older half of the log names the role
14
+ * there and nowhere else, and reading only the payload would report those
15
+ * runs as testers that never reported — an `unverifiable` about evidence the
16
+ * log is holding.
17
+ *
18
+ * `error-logged` tries `agent` before `agent_role`: this reader's own
19
+ * historical key first, `agent_role` (the dispatch's) as the fallback for a
20
+ * hand-written event that only carries that one — the same either-key
21
+ * reading delegation.ts and agents-registry.ts already give this event type.
22
+ * Each of the three readers keeps its own historical key first rather than
23
+ * all agreeing on one order, since `agent` and `agent_role` were each the
24
+ * only key some reader ever had for this event type before the other was
25
+ * added as a fallback.
16
26
  */
17
27
  const TERMINAL_ROLE_EVENTS = {
18
- 'task-result-recorded': 'agent',
19
- 'error-logged': 'agent',
20
- 'judge-reported': 'agent_role',
21
- 'judge-verdict': 'agent',
28
+ 'task-result-recorded': ['agent'],
29
+ 'error-logged': ['agent', 'agent_role'],
30
+ 'judge-reported': ['agent_role'],
31
+ 'judge-verdict': ['agent'],
22
32
  };
33
+ /** The first of `keys` that names a non-empty string in `payload`, or null. */
34
+ function firstPayloadString(payload, keys) {
35
+ for (const key of keys) {
36
+ const value = payloadString(payload, key);
37
+ if (value !== null)
38
+ return value;
39
+ }
40
+ return null;
41
+ }
23
42
  function payloadString(payload, key) {
24
43
  const value = payload[key];
25
44
  return typeof value === 'string' && value.length > 0 ? value : null;
@@ -46,11 +65,11 @@ export function readTerminalRoleRecords(events) {
46
65
  const records = [];
47
66
  for (const stored of events) {
48
67
  const record = stored.record;
49
- const field = TERMINAL_ROLE_EVENTS[record.event_type];
50
- if (!field)
68
+ const fields = TERMINAL_ROLE_EVENTS[record.event_type];
69
+ if (!fields)
51
70
  continue;
52
71
  const payload = (record.payload ?? {});
53
- const role = payloadString(payload, field) ?? record.actor;
72
+ const role = firstPayloadString(payload, fields) ?? record.actor;
54
73
  if (!role)
55
74
  continue;
56
75
  records.push({
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { constants as osConstants } from 'node:os';
3
+ import { BS_PREFIX, LEGACY_PREFIX } from './env.js';
3
4
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
4
5
  const TAIL_LINES = 50;
5
6
  /**
@@ -63,27 +64,29 @@ function signalNote(code, signal) {
63
64
  * factory's own namespace.
64
65
  *
65
66
  * A command the factory launches on a project's behalf belongs to the
66
- * project, not to the factory that launched it. Every `SMITH_*` variable in
67
- * this process was set for *this* process, and letting one through silently
68
- * makes the factory's own invocation part of the project's verdict.
69
- * `SMITH_HOME` is the one that bites: it moves `WORK_ROOT` off the worktree,
70
- * so a suite that asserts its own layout fails on the launcher's environment
71
- * alone, and anything the suite writes to state lands in the factory's live
72
- * `state/` instead of the worktree's. The verdict then depends on how the
73
- * factory happened to be invoked, which is not a verdict about the branch.
67
+ * project, not to the factory that launched it. Every `BS_*` variable — and,
68
+ * for the transition period, every legacy `SMITH_*` one — in this process
69
+ * was set for *this* process, and letting one through silently makes the
70
+ * factory's own invocation part of the project's verdict. `BS_HOME` (nee
71
+ * `SMITH_HOME`) is the one that bites: it moves `WORK_ROOT` off the
72
+ * worktree, so a suite that asserts its own layout fails on the launcher's
73
+ * environment alone, and anything the suite writes to state lands in the
74
+ * factory's live `state/` instead of the worktree's. The verdict then
75
+ * depends on how the factory happened to be invoked, which is not a verdict
76
+ * about the branch.
74
77
  *
75
78
  * The strip is a rule rather than a roster, deliberately: a roster is a list
76
- * that goes stale the next time a `SMITH_*` variable is added, and it goes
77
- * stale silently. Not every one of them is the factory configuring itself —
78
- * `SMITH_CROSSCHECK_OFFLINE` is an operator switch — but the runbook already
79
- * says of that one "pass it per command"
79
+ * that goes stale the next time a `BS_*`/`SMITH_*` variable is added, and it
80
+ * goes stale silently. Not every one of them is the factory configuring
81
+ * itself — `BS_CROSSCHECK_OFFLINE` is an operator switch — but the runbook
82
+ * already says of that one "pass it per command"
80
83
  * (`docs/runbooks/providers.md`, and it names an ambient copy of that switch
81
84
  * as its own hazard). The command the operator means is the one running
82
85
  * here, not the factory that spawned it, and stating it inside the command
83
86
  * string works: the shell applies it after this strip.
84
87
  */
85
88
  export function projectCommandEnv(env) {
86
- return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith('SMITH_')));
89
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith(BS_PREFIX) && !name.startsWith(LEGACY_PREFIX)));
87
90
  }
88
91
  /**
89
92
  * Run one check command in its own process group (POSIX `setsid`, via
@@ -94,8 +97,9 @@ export function projectCommandEnv(env) {
94
97
  * jobs, long-lived children) running as orphans after the parent shell
95
98
  * dies — a real leak in a 24/7 factory (reproduced: manual kill -9 needed).
96
99
  */
97
- function runOne(check, cwd, timeoutMs) {
100
+ function runOne(check, cwd, defaultTimeoutMs) {
98
101
  return new Promise((resolve) => {
102
+ const timeoutMs = check.timeout_ms ?? defaultTimeoutMs;
99
103
  const child = spawn(check.cmd, {
100
104
  cwd,
101
105
  shell: true,
@@ -176,10 +180,10 @@ function runOne(check, cwd, timeoutMs) {
176
180
  * no test-output parsing beyond the process exit code.
177
181
  */
178
182
  export async function run(checks, opts) {
179
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
183
+ const defaultTimeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
180
184
  const results = [];
181
185
  for (const check of checks) {
182
- const result = await runOne(check, opts.cwd, timeoutMs);
186
+ const result = await runOne(check, opts.cwd, defaultTimeoutMs);
183
187
  results.push(result);
184
188
  if (!result.pass && !opts.runAll)
185
189
  break;
@@ -0,0 +1,83 @@
1
+ import { resolveArtifactPath } from './artifacts.js';
2
+ import { eventTaskId, readLineageEvents } from './events.js';
3
+ import { foldJudgeTurns, JUDGE_DISPATCH_EVENT_TYPE } from './judges.js';
4
+ import { taskIdsMatch } from './taskId.js';
5
+ /** `judges.ts`'s uiux role, spelled out here rather than imported: the string is the taxonomy value, not an implementation detail of judges.ts. */
6
+ const UIUX_ROLE = 'uiux';
7
+ /** desktop × mobile, light × dark — the four shots a visual pass has to cover. */
8
+ const REQUIRED_COMBOS = [
9
+ ['desktop', 'light'],
10
+ ['desktop', 'dark'],
11
+ ['mobile', 'light'],
12
+ ['mobile', 'dark'],
13
+ ];
14
+ function latestTesterRecord(events, taskId) {
15
+ let latest = null;
16
+ events.forEach(({ record }, index) => {
17
+ if (record.event_type !== 'artifact-check-result')
18
+ return;
19
+ if (!taskIdsMatch(eventTaskId(record) ?? undefined, taskId))
20
+ return;
21
+ const head = record.payload.head;
22
+ if (typeof head !== 'string')
23
+ return; // an artifact-check-result with no --worktree behind it
24
+ const screenshots = Array.isArray(record.payload.screenshots)
25
+ ? record.payload.screenshots
26
+ : [];
27
+ latest = { index, head, screenshots };
28
+ });
29
+ return latest;
30
+ }
31
+ /** The lineage position of the dispatch that opened a judge turn — `JudgeTurn.dispatchedAt` is wall-clock only. */
32
+ function dispatchIndex(events, taskId, role, kind, round) {
33
+ let found = -1;
34
+ events.forEach(({ record }, index) => {
35
+ if (record.event_type !== JUDGE_DISPATCH_EVENT_TYPE)
36
+ return;
37
+ if (!taskIdsMatch(eventTaskId(record) ?? undefined, taskId))
38
+ return;
39
+ if (record.payload.agent_role !== role)
40
+ return;
41
+ if (record.payload.judge_kind !== kind)
42
+ return;
43
+ if (record.payload.round !== round)
44
+ return;
45
+ found = index;
46
+ });
47
+ return found;
48
+ }
49
+ /**
50
+ * `checkUiux`: the one function both the gate stage and `bs uiux check`
51
+ * (the preflight verb, D4) call, so the two can never disagree about the same
52
+ * fixture. Reads the lineage once and folds both judge turns and the tester's
53
+ * freshness record out of it — no separate dispatch, no separate store.
54
+ */
55
+ export async function checkUiux(input, ctx, opts = {}) {
56
+ const events = await readLineageEvents(ctx.sessionId, opts);
57
+ const turns = foldJudgeTurns(events, input.taskId);
58
+ const specTurn = turns.find((t) => t.role === UIUX_ROLE && t.kind === 'spec');
59
+ if (!specTurn?.reported) {
60
+ return { outcome: 'blocked', reason: 'uiux-spec-missing' };
61
+ }
62
+ const tester = latestTesterRecord(events, input.taskId);
63
+ if (tester === null) {
64
+ return { outcome: 'blocked', reason: 'screenshots-missing' };
65
+ }
66
+ const haveCombos = new Set(tester.screenshots.map((s) => `${s.viewport}:${s.theme}`));
67
+ const allCombosPresent = REQUIRED_COMBOS.every(([viewport, theme]) => haveCombos.has(`${viewport}:${theme}`));
68
+ const allOnDisk = tester.screenshots.every((s) => resolveArtifactPath(input.taskId, s.path, input.artifactsDir) !== null);
69
+ if (!allCombosPresent || !allOnDisk) {
70
+ return { outcome: 'blocked', reason: 'screenshots-missing' };
71
+ }
72
+ if (input.head !== undefined && tester.head !== input.head) {
73
+ return { outcome: 'blocked', reason: 'screenshots-stale' };
74
+ }
75
+ const visualTurn = turns.find((t) => t.role === UIUX_ROLE && t.kind === 'visual');
76
+ const visualDispatchIndex = visualTurn === undefined
77
+ ? -1
78
+ : dispatchIndex(events, input.taskId, UIUX_ROLE, 'visual', visualTurn.round);
79
+ if (!visualTurn?.reported || !(visualDispatchIndex > tester.index)) {
80
+ return { outcome: 'blocked', reason: 'uiux-visual-missing' };
81
+ }
82
+ return { outcome: 'pass' };
83
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * What `smith` can be asked to do, written down once (P9-21).
2
+ * What `bs` can be asked to do, written down once (P9-21).
3
3
  *
4
4
  * Before this, `smith` and `smith --help` both answered
5
5
  * `{"error":{"message":"Unknown command: "}}`, so the only way to learn a
@@ -31,7 +31,7 @@ const EVENTS_DIR = `${EVENTS} [--state-dir <dir>]`;
31
31
  */
32
32
  const STATS = '[--db <file>] [--session <id>] [--lineage]';
33
33
  /** The scope both `issues` actions accept: a session's lineage, narrowed by epic and ISO since. */
34
- const ISSUES = '--session <id> [--epic <id>] [--since <iso>] [--state-dir <dir>] [--roadmap-path <file>]';
34
+ const ISSUES = '--session <id> [--epic <id>] [--since <iso>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>]';
35
35
  export const COMMANDS = [
36
36
  {
37
37
  command: 'audit open',
@@ -66,8 +66,8 @@ export const COMMANDS = [
66
66
  {
67
67
  command: 'audit resolve',
68
68
  positionals: '<project-dir>',
69
- flags: `--epic <epic-id> ${EVENTS_DIR}`,
70
- summary: 'Append a fixed line for every finding the epic carried. Run from the epic-close step.',
69
+ flags: `--epic <epic-id> [--except <fp[,fp...]>] [--plan <plan.json>] [--specs-dir <dir>] ${EVENTS_DIR}`,
70
+ summary: "Append a fixed line for every carried finding a task in the epic's newest plan still claims, or a superseded task's change already merged; list the rest as deferred. Run from the epic-close step.",
71
71
  },
72
72
  {
73
73
  command: 'audit close',
@@ -90,14 +90,14 @@ export const COMMANDS = [
90
90
  {
91
91
  command: 'plan quorum',
92
92
  positionals: '',
93
- 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>]',
93
+ flags: '--plan-version <n> --session <id> --causal-parent <event-id> [--epic <id>] [--plan <draft.json>] [--confidence <0-1>] [--project <dir>] [--out <file>] [--actor <name>] [--specs-dir <dir>] [--state-dir <dir>]',
94
94
  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.',
95
95
  },
96
96
  {
97
97
  command: 'plan amend',
98
98
  positionals: '',
99
99
  flags: `--plan <plan.json> --findings <id[,id...]> --rationale <text> --sites <path[,path...]> [--changes <changes.json>] [--specs-dir <dir>] ${EVENTS_DIR}`,
100
- summary: 'Cut a new plan version against the spec findings that forced it. No finding, no amendment. --sites is every place the shape occurs, not only where it was reported.',
100
+ summary: 'Cut a new plan version against the spec findings that forced it. No finding, no amendment. --sites is every place the shape occurs, not only where it was reported. --specs-dir defaults to the directory --plan already lives in (#219), not factory/specs/active; an explicit --specs-dir that disagrees with it is refused rather than silently preferred.',
101
101
  },
102
102
  {
103
103
  command: 'plan propose',
@@ -126,8 +126,11 @@ export const COMMANDS = [
126
126
  {
127
127
  command: 'plan ingest',
128
128
  positionals: '<plan.json>',
129
- flags: EVENTS_DIR,
130
- summary: 'Record every task and dependency the plan declares, before any wave names them.',
129
+ flags: `[--project <dir>] ${EVENTS_DIR}`,
130
+ summary: 'Record every task and dependency the plan declares, before any wave names them. ' +
131
+ '--project resolves the branch prefix a legacy `smith/<epic>/integration` epic keeps ' +
132
+ 'using, rather than declaring `bs/...` for a branch `worktree create` will cut as ' +
133
+ '`smith/...`.',
131
134
  },
132
135
  {
133
136
  command: 'wave check',
@@ -198,7 +201,7 @@ export const COMMANDS = [
198
201
  {
199
202
  command: 'daemon run',
200
203
  positionals: '',
201
- flags: `[--interval <seconds>] [--once] [--dir <dir>] [--project <dir>...] [--no-self] [--db <path>] [--no-db] [--state-dir <dir>]`,
204
+ flags: `[--interval <seconds>] [--once] [--dir <dir>] [--project <dir>...] [--no-self] [--db <path>] [--no-db] [--state-dir <dir>] [--specs-dir <dir>]`,
202
205
  summary: 'Watch the event log in the foreground: budgets, stale agents, work that is due. ' +
203
206
  'Prints one report per tick, one JSON line each; --once prints one document and exits. ' +
204
207
  'Never dispatches.',
@@ -232,21 +235,21 @@ export const COMMANDS = [
232
235
  {
233
236
  command: 'scheduler run',
234
237
  positionals: '',
235
- flags: `[--dry] [--now <iso>] [--project <dir>...] [--no-self] ${EVENTS_DIR}`,
238
+ flags: `[--dry] [--now <iso>] [--project <dir>...] [--no-self] [--specs-dir <dir>] ${EVENTS_DIR}`,
236
239
  summary: 'Propose and enact the next scheduler moves for a session. --dry proposes only.',
237
240
  },
238
241
  {
239
242
  command: 'scheduler admit',
240
243
  positionals: '',
241
244
  flags: '--session <id> [--state-dir <dir>] [--now <iso>] [--project <dir>...] [--no-self] ' +
242
- '[--policy <file>] [--crosscheck <file>]',
245
+ '[--specs-dir <dir>] [--policy <file>] [--crosscheck <file>]',
243
246
  summary: "Classify what's due into auto or operator per scheduler.yml autonomy. Enacts nothing.",
244
247
  },
245
248
  {
246
249
  command: 'worktree create',
247
250
  positionals: '<project-dir> <epic> <task-id>',
248
- flags: '',
249
- summary: 'Cut the isolated worktree and task branch one task is allowed to write in.',
251
+ flags: '[--from <predecessor-task-id> --session <id> [--state-dir <dir>]]',
252
+ summary: 'Cut the isolated worktree and task branch one task is allowed to write in. --from cuts it from a predecessor branch instead of integration, and requires --session (which supplies the log --from is checked against): refused unless that session records <epic>/<task-id> as the logged successor of <epic>/<predecessor-task-id>.',
250
253
  },
251
254
  {
252
255
  command: 'worktree rm',
@@ -275,8 +278,12 @@ export const COMMANDS = [
275
278
  {
276
279
  command: 'queue run',
277
280
  positionals: '<epic>',
278
- flags: '--project <dir> --test-cmd <cmd> --tasks <tasks.json> [--select-test-cmd <cmd>] [--session <id> --causal-parent <event-id> --plan <plan.json> [--plan-version <n>] [--actor <name>]] [--state-dir <dir>]',
279
- summary: 'Serially rebase, test, and merge task branches into the integration branch.',
281
+ flags: '--project <dir> --test-cmd <cmd> --tasks <tasks.json> --session <id> --causal-parent <event-id> --plan <plan.json> [--plan-version <n>] [--actor <name>] [--select-test-cmd <cmd>] [--batch] [--state-dir <dir>]',
282
+ summary: 'Serially rebase, test, and merge task branches into the integration branch. ' +
283
+ 'A merge is a fact the log must carry, so --session/--causal-parent/--plan are required — ' +
284
+ 'refuses before any git write without them. ' +
285
+ '--batch (roadmap merge-lanes) groups claim-disjoint tasks with groupForBatch and ' +
286
+ 'tests each group once instead of once per task — see queue.ts and the operator guide.',
280
287
  },
281
288
  {
282
289
  command: 'queue adopt',
@@ -291,8 +298,8 @@ export const COMMANDS = [
291
298
  {
292
299
  command: 'session start',
293
300
  positionals: '<session-id>',
294
- flags: '[--continues <event-id>] [--actor <name>] [--state-dir <dir>]',
295
- summary: 'Open a session log with the one root event it is allowed. Exit 1 if the session is already open.',
301
+ flags: '[--continues <event-id>] [--prompt <event-id>] [--actor <name>] [--state-dir <dir>]',
302
+ summary: 'Open a session log with the one root event it is allowed; --prompt links the user_prompt that asked for it. Exit 1 if the session is already open.',
296
303
  },
297
304
  {
298
305
  command: 'event append',
@@ -325,6 +332,36 @@ export const COMMANDS = [
325
332
  flags: EVENTS_DIR,
326
333
  summary: "Put the operator's own words on the timeline verbatim, and print the event id a dispatch hangs off.",
327
334
  },
335
+ {
336
+ command: 'prompt capture',
337
+ positionals: '',
338
+ flags: '',
339
+ summary: 'Read one UserPromptSubmit hook payload on stdin and record it where the cwd is managed. Print one line, or nothing; always exit 0.',
340
+ },
341
+ {
342
+ command: 'prompt import',
343
+ positionals: '',
344
+ flags: '--transcript <file> [--dry-run]',
345
+ summary: "Replay a Claude Code transcript's prompts and option answers into prompts-<cli session>, keeping their timestamps. Re-running imports nothing; --dry-run prints each event and writes none. Print a summary line.",
346
+ },
347
+ {
348
+ command: 'feedback record',
349
+ positionals: '',
350
+ flags: `--task <task-id> [--body <text>|--body-file <file>] [--kind <must-fix|nice-to-have>] [--source <dashboard|github|cli>] [--external-id <id>] [--author <name>] ${EVENTS_DIR}`,
351
+ summary: "Record the operator's mid-run comment on a task. Print { event_id, feedback_id, deduped }; a repeated --external-id dedupes rather than appending.",
352
+ },
353
+ {
354
+ command: 'feedback pending',
355
+ positionals: '',
356
+ flags: '--session <id> [--task <task-id>] [--epic <id>] [--state-dir <dir>]',
357
+ summary: "List this session lineage's unresolved feedback, each with a suggestedAction: follow-up once its task is closed to further work, bounce otherwise.",
358
+ },
359
+ {
360
+ command: 'feedback resolve',
361
+ positionals: '',
362
+ flags: `--feedback <id> --resolution <bounced|follow-up|dismissed> [--note <text>] [--follow-up-task <task-id>] ${EVENTS_DIR}`,
363
+ summary: 'Resolve one recorded feedback item. Rejects an unknown or already-resolved feedback id.',
364
+ },
328
365
  {
329
366
  command: 'claims check',
330
367
  form: '--roots',
@@ -359,6 +396,12 @@ export const COMMANDS = [
359
396
  flags: '[--task <id>] [--policy <path>] [--state-dir <dir>]',
360
397
  summary: 'Assert crosscheck.yml’s role asymmetry against the log. Exit 1 on a violation OR an unverifiable answer.',
361
398
  },
399
+ {
400
+ command: 'dispatch lint',
401
+ positionals: '<prompt-file>',
402
+ flags: '--role <role> --task <id> --session <id> [--agents-dir <dir>] [--state-dir <dir>]',
403
+ summary: "Check a composed dispatch prompt's stated turn budget against the role template's maxTurns, and a judge prompt's declared-artifact line against the ledger. Exit 1 on a violation OR an unverifiable answer. `-` reads stdin.",
404
+ },
362
405
  {
363
406
  command: 'tester check',
364
407
  positionals: '<session-id>',
@@ -415,7 +458,7 @@ export const COMMANDS = [
415
458
  {
416
459
  command: 'harness plan',
417
460
  positionals: '',
418
- flags: '--role <role> --task <id> --prompt-file <path> [--harness <name>] [--worktree <dir>] [--policy <file>] [--tier <frontier|mid|small>] [--schema <name>]',
461
+ flags: '--role <role> --task <id> --prompt-file <path> [--harness <name>] [--worktree <dir>] [--policy <file>] [--tier <frontier|mid|small>] [--schema <name>] [--specs-dir <dir>]',
419
462
  summary: 'Render the invocation that would run one worker turn, as JSON. Prints it; never starts it.',
420
463
  },
421
464
  {
@@ -452,7 +495,7 @@ export const COMMANDS = [
452
495
  command: 'stack check',
453
496
  positionals: '',
454
497
  flags: '[--policy <file>]',
455
- summary: 'Which stack answers the shipped templates honour, which they only record, and which make `smith new` refuse. Exit 1 on a refusal.',
498
+ summary: 'Which stack answers the shipped templates honour, which they only record, and which make `bs new` refuse. Exit 1 on a refusal.',
456
499
  },
457
500
  {
458
501
  command: 'policy check',
@@ -469,9 +512,21 @@ export const COMMANDS = [
469
512
  {
470
513
  command: 'gate run',
471
514
  positionals: '<task-id>',
472
- flags: `--worktree <dir> --checks <checks.json> --result <result.json> [--base <ref>] [--grader <file>] [--agent <role> --provider <name> --model-tier <tier> --input-tokens <n> --output-tokens <n>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] [--findings <file>] [--no-findings <role>] [--lessons <lessons.md>] [--plan <file>] [--run-all] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
515
+ flags: `--worktree <dir> --checks <checks.json> --result <result.json> [--project <dir>] [--base <ref>] [--grader <file>] [--agent <role> --provider <name> --model-tier <tier> --input-tokens <n> --output-tokens <n>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] [--findings <file>] [--no-findings <role>] [--uiux-visual <file>] [--lessons <lessons.md>] [--plan <file>] [--run-all] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
473
516
  summary: 'The per-task gate: schema, tests, coverage, findings. Exit 1 when blocked.',
474
517
  },
518
+ {
519
+ command: 'results record',
520
+ positionals: '',
521
+ flags: `--task <task-id> --result <result.json> [--agent <role> --provider <name> --model-tier <tier> [--input-tokens <n> --output-tokens <n>]] [--artifacts-dir <dir>] [--worktree <dir>] ${EVENTS_DIR}`,
522
+ summary: "Project a worker's Result on its own, with no worktree to certify and no tests to run: schema-check, task-result-recorded, artifact-check. Wave.md's tester has no gate to run one through; this is how its screenshots reach the dashboard the moment it finishes. --worktree records that worktree's HEAD and the Result's screenshot artifacts, the freshness evidence the uiux gate stage reads back. Exit 1 when blocked.",
523
+ },
524
+ {
525
+ command: 'uiux check',
526
+ positionals: '',
527
+ flags: `--task <task-id> --plan <plan.json> [--worktree <dir>] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
528
+ summary: 'Preflight for the gate uiux stage: the same checkUiux a flagged task is gated on, so a dispatcher can ask before running the whole gate. Exit 1 when blocked.',
529
+ },
475
530
  {
476
531
  command: 'coverage check',
477
532
  positionals: '<worktree-dir>',
@@ -481,7 +536,7 @@ export const COMMANDS = [
481
536
  {
482
537
  command: 'budget alarm',
483
538
  positionals: '<session-id>',
484
- flags: '[--epic <id>] [--policy <path>] [--state-dir <dir>]',
539
+ flags: '[--epic <id>] [--policy <path>] [--specs-dir <dir>] [--state-dir <dir>]',
485
540
  summary: 'Epic spend against budgets.yml’s epic.alarm_ratio. Exit 1 on a crossing OR on a record too holey to rule one out.',
486
541
  },
487
542
  {
@@ -505,8 +560,8 @@ export const COMMANDS = [
505
560
  {
506
561
  command: 'epic spec-review',
507
562
  positionals: '',
508
- flags: `--epic <id> --project <dir> --plan <plan.json> --reviewed-by <role> [--reviewed-by-provider <name>] [--evidence <file>] ${EVENTS_DIR}`,
509
- summary: 'Record the closing spec review, pinned to the integration head it was read at.',
563
+ flags: `--epic <id> --project <dir> --plan <plan.json> --reviewed-by <role> [--reviewed-by-provider <name>] [--evidence <file>] [--no-findings] ${EVENTS_DIR}`,
564
+ summary: 'Record the closing spec review, pinned to the integration head it was read at. Exactly one of --evidence or --no-findings; --no-findings attests the clean case.',
510
565
  },
511
566
  {
512
567
  command: 'epic goal',
@@ -545,8 +600,8 @@ export const COMMANDS = [
545
600
  {
546
601
  command: 'findings for-dispatch',
547
602
  positionals: '',
548
- flags: '--session <id> --plan <plan.json> --task <task-id> [--state-dir <dir>]',
549
- summary: "Print the open findings a dispatching task's own claims cover, as a prompt block.",
603
+ flags: '--session <id> [--plan <plan.json> --task <task-id>] [--state-dir <dir>]',
604
+ summary: "Print the open findings a dispatching task's own claims cover, as a prompt block; with no --plan/--task, every open finding in the epic.",
550
605
  },
551
606
  {
552
607
  command: 'findings list',
@@ -587,10 +642,20 @@ export const COMMANDS = [
587
642
  flags: EVENTS_DIR,
588
643
  summary: 'Apply a batch of operator waiver decisions.',
589
644
  },
645
+ {
646
+ command: 'waivers approve-task',
647
+ positionals: '<task-id>',
648
+ flags: `--note <why> ${EVENTS_DIR}`,
649
+ summary: 'Record the operator approval of one waived task. Without it a waived task blocks the epic close; an approval counts only for the plan version it was given at or later. Requires --actor operator.',
650
+ },
590
651
  {
591
652
  command: 'lessons candidates',
592
653
  positionals: '',
593
- flags: STATS,
654
+ // Only `candidates` takes `--state-dir` alongside STATS: it validates
655
+ // `--session` against the event log the same way `raise`/`approve`/
656
+ // `reject`/`db rebuild` do (P9-28), which is a per-session log, not a
657
+ // per-DB-query concern the other STATS-flagged commands share.
658
+ flags: `${STATS} [--state-dir <dir>]`,
594
659
  summary: 'List the lesson candidates awaiting an approve or a reject.',
595
660
  },
596
661
  {
@@ -647,20 +712,20 @@ export const COMMANDS = [
647
712
  {
648
713
  command: 'judge dispatch',
649
714
  positionals: '',
650
- flags: `--task <task-id> --role <role> --artifact <file> --model <id> [--round <n>] [--provider <name>] [--model-tier <tier>] ${EVENTS_DIR}`,
651
- summary: 'Declare the file a judge owes, so a turn that never came back is visible.',
715
+ flags: `--task <task-id> --role <role> --artifact <absolute-path> --model <id> [--round <n>] [--provider <name>] [--model-tier <tier>] [--kind <spec|visual>] ${EVENTS_DIR}`,
716
+ summary: 'Declare the file a judge owes, so a turn that never came back is visible. --kind only applies to uiux, which opens a spec turn and a visual turn per task.',
652
717
  },
653
718
  {
654
719
  command: 'judge report',
655
720
  positionals: '',
656
- flags: `--task <task-id> --role <role> [--round <n>] [--artifact <file>] [--no-findings] ${EVENTS_DIR}`,
657
- summary: 'Close a judge turn against the artifact it declared. --no-findings attests the clean case.',
721
+ flags: `--task <task-id> --role <role> [--round <n>] [--artifact <file>] [--no-findings] [--kind <spec|visual>] ${EVENTS_DIR}`,
722
+ summary: "Close a judge turn against the artifact it declared, written during this turn. --no-findings attests the clean case. --kind picks which of uiux's two open turns to close when both are open.",
658
723
  },
659
724
  {
660
725
  command: 'judge outstanding',
661
726
  positionals: '',
662
- flags: '--session <id> --task <task-id> [--state-dir <dir>]',
663
- summary: 'List the judge turns still owed on a task. Exit 1 while the list is non-empty.',
727
+ flags: '--session <id> --task <task-id> [--kind <spec|visual>] [--state-dir <dir>]',
728
+ summary: 'Print { taskId, sessionId, outstanding, count } for the judge turns still owed on a task. Exit 1 while count is non-zero.',
664
729
  },
665
730
  {
666
731
  command: 'judge escalations',
@@ -671,8 +736,8 @@ export const COMMANDS = [
671
736
  {
672
737
  command: 'judge preflight',
673
738
  positionals: '',
674
- flags: '[--policy <file>]',
675
- summary: 'Can the enabled providers be called at all, and would a promotion decide anything? Exit 1 on a provider that costs a call it cannot make.',
739
+ flags: '[--policy <file>] [--project <dir>]',
740
+ summary: 'Can the enabled providers be called at all, and would a promotion decide anything? Exit 1 on a provider that costs a call it cannot make. --project merges <dir>/.blacksmith/crosscheck.yml over the policy (OpenRouter substitution/extra judges).',
676
741
  },
677
742
  {
678
743
  command: 'judge run',
@@ -683,25 +748,25 @@ export const COMMANDS = [
683
748
  {
684
749
  command: 'crossfind request',
685
750
  positionals: '',
686
- flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>]',
751
+ flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--max-output-tokens <n>] [--policy <file>] [--project <dir>] [--worktree <dir>]',
687
752
  summary: 'Print the finder request without sending it — exactly what would leave the machine. Refuses when send_diff is false.',
688
753
  },
689
754
  {
690
755
  command: 'crossfind reconcile',
691
756
  positionals: '',
692
- flags: '--task <id> --native <findings.json> --independent <runs.json> [--policy <file>]',
757
+ flags: '--task <id> --native <findings.json> --independent <runs.json> [--policy <file>] [--project <dir>] [--worktree <dir>]',
693
758
  summary: 'Reconcile two saved finding lists offline. No provider, no log. Exit 1 when the result would change a gate.',
694
759
  },
695
760
  {
696
761
  command: 'crossfind run',
697
762
  positionals: '',
698
- 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}`,
763
+ 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>] [--project <dir>] [--worktree <dir>] ${EVENTS_DIR}`,
699
764
  summary: 'Run the independent finder over a diff and reconcile it against the native findings. Exit 1 when the result would change a gate.',
700
765
  },
701
766
  {
702
767
  command: 'ui serve',
703
768
  positionals: '',
704
- flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>] [--now-iso <iso>]',
769
+ flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>] [--now-iso <iso>] [--claude-config-dir <dir>] [--store <dir>...]',
705
770
  summary: 'Serve the dashboard from the built ui/server.',
706
771
  },
707
772
  {
@@ -783,7 +848,7 @@ const COMMAND_NAMES = new Set(COMMANDS.map((doc) => doc.command));
783
848
  const NAMESPACES = new Set(COMMANDS.map(namespaceOf));
784
849
  /** The line that would have worked. Empty parts are dropped, never printed as a double space. */
785
850
  export function usageLine(doc) {
786
- return ['smith', doc.command, doc.positionals, doc.flags].filter((part) => part !== '').join(' ');
851
+ return ['bs', doc.command, doc.positionals, doc.flags].filter((part) => part !== '').join(' ');
787
852
  }
788
853
  /** The positional argument names, in order — what a missing-argument error names. */
789
854
  export function positionalNames(doc) {
@@ -825,7 +890,7 @@ export function flagSpecFor(namespace, action) {
825
890
  if (namespace === undefined)
826
891
  return undefined;
827
892
  const key = action === undefined ? namespace : `${namespace} ${action}`;
828
- // The second arm is what keeps the action-less verbs working: `smith new
893
+ // The second arm is what keeps the action-less verbs working: `bs new
829
894
  // my-project` spends its action slot on a positional. Same fallback as
830
895
  // isDocumented, deliberately — a command that dispatches must also parse.
831
896
  const docs = COMMANDS.filter((doc) => doc.command === key || doc.command === namespace).filter((doc) => doc.command === key || !COMMAND_NAMES.has(key));
@@ -853,23 +918,23 @@ function renderCommands(docs) {
853
918
  }
854
919
  /**
855
920
  * The whole command list, or one namespace's slice of it. An unknown namespace
856
- * throws rather than printing an empty list: "smith has no such namespace" and
921
+ * throws rather than printing an empty list: "bs has no such namespace" and
857
922
  * "that namespace has no commands" must not be the same observable answer.
858
923
  */
859
924
  export function usageText(namespace) {
860
925
  if (namespace !== undefined && !NAMESPACES.has(namespace)) {
861
- throw new SmithError('usage.unknown-namespace', `No such namespace: ${namespace}. Run "smith --help" for the list.`, { namespace });
926
+ throw new SmithError('usage.unknown-namespace', `No such namespace: ${namespace}. Run "bs --help" for the list.`, { namespace });
862
927
  }
863
928
  const docs = namespace === undefined ? COMMANDS : COMMANDS.filter((doc) => namespaceOf(doc) === namespace);
864
929
  const header = namespace === undefined
865
930
  ? [
866
- 'Usage: smith <namespace> <action> [args] [--flags]',
931
+ 'Usage: bs <namespace> <action> [args] [--flags]',
867
932
  '',
868
- ' smith <namespace> --help list one namespace',
869
- ' smith <command> --help show one command',
933
+ ' bs <namespace> --help list one namespace',
934
+ ' bs <command> --help show one command',
870
935
  '',
871
936
  ].join('\n')
872
- : `Usage: smith ${namespace} <action> [args] [--flags]\n`;
937
+ : `Usage: bs ${namespace} <action> [args] [--flags]\n`;
873
938
  let out = `${header}\n`;
874
939
  let current = '';
875
940
  for (const doc of docs) {
@@ -895,7 +960,7 @@ export function helpText(namespace, action) {
895
960
  const exact = COMMANDS.filter((doc) => doc.command === key);
896
961
  if (exact.length > 0)
897
962
  return renderCommands(exact);
898
- // `smith new my-project --help`: the action slot holds a positional, so the
963
+ // `bs new my-project --help`: the action slot holds a positional, so the
899
964
  // namespace alone is the command.
900
965
  const bare = COMMANDS.filter((doc) => doc.command === namespace);
901
966
  if (bare.length > 0)
@@ -908,7 +973,7 @@ export function helpText(namespace, action) {
908
973
  * Whether cli.ts's dispatcher has a branch for this pair. Checked before
909
974
  * dispatch, so an undocumented command cannot run — which is the half of
910
975
  * "the table and the dispatcher agree" that a test cannot enforce at runtime.
911
- * The second arm is what keeps the action-less verbs working: `smith new
976
+ * The second arm is what keeps the action-less verbs working: `bs new
912
977
  * my-project` spends its action slot on a positional.
913
978
  */
914
979
  export function isDocumented(namespace, action) {