@juzser/blacksmith 0.4.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 (131) hide show
  1. package/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
  2. package/.claude/agents/{coder.md → bs-coder.md} +8 -7
  3. package/.claude/agents/{grader.md → bs-grader.md} +4 -4
  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} +13 -5
  8. package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
  9. package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
  10. package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -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} +4 -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 +34 -27
  17. package/.claude/skills/bs/dispatch.md +100 -50
  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 +27 -18
  22. package/.claude/skills/bs/report.md +11 -11
  23. package/.claude/skills/bs/run.md +181 -49
  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 +109 -53
  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 +48 -14
  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 +125 -17
  35. package/factory/orchestrator/dist/budgetAlarm.js +132 -37
  36. package/factory/orchestrator/dist/budgets.js +302 -84
  37. package/factory/orchestrator/dist/claims.js +9 -19
  38. package/factory/orchestrator/dist/cli.js +790 -100
  39. package/factory/orchestrator/dist/cliName.js +27 -0
  40. package/factory/orchestrator/dist/crosscheck.js +136 -8
  41. package/factory/orchestrator/dist/daemon.js +154 -36
  42. package/factory/orchestrator/dist/db/projector.js +278 -29
  43. package/factory/orchestrator/dist/db/queries.js +1831 -94
  44. package/factory/orchestrator/dist/db/schema.js +57 -4
  45. package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
  46. package/factory/orchestrator/dist/delegation.js +86 -19
  47. package/factory/orchestrator/dist/diffstat.js +15 -8
  48. package/factory/orchestrator/dist/dispatchLint.js +13 -37
  49. package/factory/orchestrator/dist/effort.js +14 -2
  50. package/factory/orchestrator/dist/effortTiers.js +13 -0
  51. package/factory/orchestrator/dist/env.js +26 -0
  52. package/factory/orchestrator/dist/epic.js +323 -38
  53. package/factory/orchestrator/dist/epicBudget.js +93 -0
  54. package/factory/orchestrator/dist/epicWidth.js +22 -5
  55. package/factory/orchestrator/dist/errorIssues.js +10 -4
  56. package/factory/orchestrator/dist/escalation.js +2 -2
  57. package/factory/orchestrator/dist/events.js +105 -3
  58. package/factory/orchestrator/dist/feedback.js +169 -0
  59. package/factory/orchestrator/dist/findingContext.js +26 -0
  60. package/factory/orchestrator/dist/findings.js +83 -12
  61. package/factory/orchestrator/dist/gate.js +120 -10
  62. package/factory/orchestrator/dist/goalCheck.js +2 -2
  63. package/factory/orchestrator/dist/harness.js +25 -17
  64. package/factory/orchestrator/dist/hookDecision.js +92 -3
  65. package/factory/orchestrator/dist/integration.js +9 -7
  66. package/factory/orchestrator/dist/judgePreflight.js +25 -4
  67. package/factory/orchestrator/dist/judgeRoles.js +47 -0
  68. package/factory/orchestrator/dist/judgeStopHook.js +90 -32
  69. package/factory/orchestrator/dist/judges.js +419 -21
  70. package/factory/orchestrator/dist/lessons.js +190 -11
  71. package/factory/orchestrator/dist/mcp.js +6 -6
  72. package/factory/orchestrator/dist/paths.js +10 -7
  73. package/factory/orchestrator/dist/plan.js +269 -33
  74. package/factory/orchestrator/dist/planQuorum.js +5 -2
  75. package/factory/orchestrator/dist/policy.js +1275 -30
  76. package/factory/orchestrator/dist/projectName.js +25 -0
  77. package/factory/orchestrator/dist/promptCapture.js +306 -0
  78. package/factory/orchestrator/dist/promptHook.js +33 -0
  79. package/factory/orchestrator/dist/promptImport.js +281 -0
  80. package/factory/orchestrator/dist/prompts.js +13 -0
  81. package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
  82. package/factory/orchestrator/dist/providers/types.js +1 -1
  83. package/factory/orchestrator/dist/queue.js +4 -4
  84. package/factory/orchestrator/dist/quorum.js +18 -5
  85. package/factory/orchestrator/dist/results.js +25 -7
  86. package/factory/orchestrator/dist/roadmap.js +10 -5
  87. package/factory/orchestrator/dist/run-cli.js +15 -10
  88. package/factory/orchestrator/dist/runner.js +2 -2
  89. package/factory/orchestrator/dist/schemas.js +56 -8
  90. package/factory/orchestrator/dist/severity.js +8 -4
  91. package/factory/orchestrator/dist/spec.js +177 -13
  92. package/factory/orchestrator/dist/specChange.js +13 -9
  93. package/factory/orchestrator/dist/stack.js +6 -6
  94. package/factory/orchestrator/dist/storeRoot.js +90 -0
  95. package/factory/orchestrator/dist/symbols.js +261 -6
  96. package/factory/orchestrator/dist/taskEvents.js +179 -27
  97. package/factory/orchestrator/dist/taskStatus.js +14 -0
  98. package/factory/orchestrator/dist/testerAudit.js +31 -12
  99. package/factory/orchestrator/dist/testgate.js +16 -13
  100. package/factory/orchestrator/dist/uiuxGate.js +83 -0
  101. package/factory/orchestrator/dist/usage.js +97 -40
  102. package/factory/orchestrator/dist/waivers.js +39 -5
  103. package/factory/orchestrator/dist/waveBudget.js +2 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +143 -7
  105. package/factory/orchestrator/dist/workroot.js +1 -1
  106. package/factory/orchestrator/dist/worktree.js +61 -19
  107. package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
  108. package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
  109. package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
  110. package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
  111. package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
  112. package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
  113. package/factory/policies/budgets.yml +123 -33
  114. package/factory/policies/crosscheck.yml +51 -13
  115. package/factory/policies/delegation.yml +3 -3
  116. package/factory/policies/effort.yml +18 -10
  117. package/factory/policies/guardrails.yml +8 -6
  118. package/factory/policies/harness.yml +5 -5
  119. package/factory/policies/lessons.md +53 -2
  120. package/factory/policies/scheduler.yml +4 -4
  121. package/factory/policies/sensitive-paths.yml +1 -1
  122. package/factory/policies/severity.yml +13 -1
  123. package/factory/policies/stack.yml +6 -6
  124. package/factory/policies/taxonomy.yml +9 -3
  125. package/factory/policies/worktree.yml +15 -9
  126. package/factory/specs/roadmap.md +7 -7
  127. package/factory/specs/schema/event.schema.json +8 -1
  128. package/factory/specs/schema/result.schema.json +21 -8
  129. package/factory/specs/schema/task-spec.schema.json +12 -0
  130. package/package.json +16 -12
  131. package/.claude/agents/uiux.md +0 -133
@@ -1,4 +1,6 @@
1
- import { foldAgents } from './agents-registry.js';
1
+ import { DISPATCH_EVENT_TYPE, foldAgents } from './agents-registry.js';
2
+ import { eventTaskId } from './events.js';
3
+ import { epicOfTaskId, isQualifiedTaskId, taskIdsMatch } from './taskId.js';
2
4
  /**
3
5
  * Did the wave that was admitted N wide actually run N wide?
4
6
  *
@@ -24,6 +26,7 @@ import { foldAgents } from './agents-registry.js';
24
26
  * intervals open forever and score every wave after one as parallel.
25
27
  */
26
28
  const ADMITTED_EVENT_TYPE = 'wave-admitted';
29
+ const MERGED_EVENT_TYPE = 'wave-merged';
27
30
  /**
28
31
  * What the log says a wave did.
29
32
  *
@@ -33,12 +36,29 @@ const ADMITTED_EVENT_TYPE = 'wave-admitted';
33
36
  * - `single` — the wave admitted one task. Nothing to be parallel about,
34
37
  * and scoring it against a width it never claimed would make
35
38
  * the failing verdicts unreadable.
39
+ * - `unlinked` — this fold's intervals hold nothing, but the lineage still
40
+ * has proof the work happened: a `wave-merged` for one of
41
+ * the admitted tasks, or a `dispatch_decision` parented
42
+ * directly on the admission. A wave-runner session started
43
+ * without `--continues` has `causal_parent: null` on its
44
+ * root, so its own dispatch_decisions never reach this fold
45
+ * even though its agents genuinely ran. Width is unmeasured
46
+ * here, not zero — a different fact from `unobserved`, which
47
+ * is exactly why it is not folded into it.
36
48
  * - `unobserved` — the wave was admitted and the log records no work for any
37
- * of its tasks. Not the same fact as `serialized`, and the
49
+ * of its tasks, and nothing else on the lineage says
50
+ * otherwise. Not the same fact as `serialized`, and the
38
51
  * whole point of separating them: one says the factory ran
39
52
  * narrow, the other says nobody can tell.
40
53
  */
41
- export const WAVE_VERDICTS = ['parallel', 'partial', 'serialized', 'single', 'unobserved'];
54
+ export const WAVE_VERDICTS = [
55
+ 'parallel',
56
+ 'partial',
57
+ 'serialized',
58
+ 'single',
59
+ 'unlinked',
60
+ 'unobserved',
61
+ ];
42
62
  /**
43
63
  * A counter with one bucket per key, all at zero.
44
64
  *
@@ -61,6 +81,17 @@ export function zeroedCounts(keys) {
61
81
  export const UNOBSERVED_HINT = 'A wave admitted with no dispatch_decision under any of its tasks ran nowhere this log can ' +
62
82
  'see. Either the dispatcher never started it, or the agents ran outside the lineage that was ' +
63
83
  'read — narrow with --epic, or check that the run wrote its dispatches to this state dir.';
84
+ /**
85
+ * Said when a wave came back `unlinked`: this fold has no dispatch interval
86
+ * for it, but the lineage holds a `wave-merged` for one of its tasks or a
87
+ * dispatch_decision parented on the admission itself, so the work is real —
88
+ * only its width is unmeasured. The usual cause is a wave-runner session
89
+ * started without `--continues`, which leaves its own dispatches outside the
90
+ * lineage this read walks even though they ran.
91
+ */
92
+ export const UNLINKED_HINT = 'A wave admitted with merged work or a dispatch on record, but not inside this lineage, so ' +
93
+ 'its width cannot be measured. Likely a wave-runner session started without --continues — ' +
94
+ 'its dispatches are real but outside the chain this read walked.';
64
95
  function readAdmission(event) {
65
96
  const payload = event.record.payload;
66
97
  const raw = payload.task_ids;
@@ -119,17 +150,109 @@ function peakOverlap(runs) {
119
150
  }
120
151
  return peak;
121
152
  }
122
- function verdictFor(declared, observed, peak) {
153
+ function verdictFor(declared, observed, peak, offLineageEvidence) {
123
154
  if (declared <= 1)
124
155
  return 'single';
125
- if (observed === 0)
156
+ if (observed === 0) {
157
+ if (offLineageEvidence)
158
+ return 'unlinked';
126
159
  return 'unobserved';
160
+ }
127
161
  if (peak >= declared)
128
162
  return 'parallel';
129
163
  if (peak <= 1)
130
164
  return 'serialized';
131
165
  return 'partial';
132
166
  }
167
+ /**
168
+ * Whether `admission` is the one a `wave-merged` id names.
169
+ *
170
+ * A qualified id ("A/task-1") must match the admission's epic exactly — no
171
+ * falling back to the bare half, because that fallback is exactly how a bare
172
+ * merge under one epic used to mark an unrelated epic's qualified admission
173
+ * as having merge evidence. A bare id ("task-1") carries no epic of its own,
174
+ * so it may match any admission's declared task by its bare half; which
175
+ * admission it actually belongs to is for {@link latestMatchingAdmission} to
176
+ * decide, not this predicate.
177
+ */
178
+ function admissionClaims(admission, mergedId) {
179
+ if (isQualifiedTaskId(mergedId) && epicOfTaskId(mergedId) !== admission.epicId)
180
+ return false;
181
+ return admission.taskIds.some((taskId) => taskIdsMatch(taskId, mergedId));
182
+ }
183
+ /**
184
+ * The one admission a `wave-merged` id belongs to: the latest whose `ts` is
185
+ * at or before the merge's `ts` and whose declared tasks match it — the same
186
+ * "nearest preceding admission" rule `admittedAtByTask` uses to close a
187
+ * task's window, reused here rather than re-derived, so a task re-admitted in
188
+ * a later wave sends its merge to that wave and not the one before it. A
189
+ * merge timestamped before every candidate admission belongs to none of them;
190
+ * returning null rather than the earliest is what keeps that merge from
191
+ * counting at all.
192
+ */
193
+ function latestMatchingAdmission(mergedId, mergeTs, admissions) {
194
+ let best = null;
195
+ for (const admission of admissions) {
196
+ if (admission.ts > mergeTs)
197
+ continue;
198
+ if (!admissionClaims(admission, mergedId))
199
+ continue;
200
+ if (best === null || admission.ts > best.ts)
201
+ best = admission;
202
+ }
203
+ return best;
204
+ }
205
+ /**
206
+ * The `wave-admitted` event ids that own at least one `wave-merged` id,
207
+ * attributed one merge at a time over the whole log so each admission can ask
208
+ * "is my work here" without rescanning — and so an admission the epic filter
209
+ * excludes must not narrow what another admission can see.
210
+ *
211
+ * Both the envelope's `task_id` and the payload's `task_ids` are read,
212
+ * because `emitWaveMerged` writes the same id to both; either is enough.
213
+ */
214
+ function admissionsWithMergeEvidence(events, admissions) {
215
+ const credited = new Set();
216
+ for (const event of events) {
217
+ if (event.record.event_type !== MERGED_EVENT_TYPE)
218
+ continue;
219
+ const ids = [];
220
+ const fromEnvelope = eventTaskId(event.record);
221
+ if (fromEnvelope)
222
+ ids.push(fromEnvelope);
223
+ const payload = event.record.payload;
224
+ if (Array.isArray(payload?.task_ids)) {
225
+ for (const id of payload.task_ids) {
226
+ if (typeof id === 'string' && id.length > 0)
227
+ ids.push(id);
228
+ }
229
+ }
230
+ for (const id of ids) {
231
+ const owner = latestMatchingAdmission(id, event.record.ts, admissions);
232
+ if (owner)
233
+ credited.add(owner.eventId);
234
+ }
235
+ }
236
+ return credited;
237
+ }
238
+ /**
239
+ * The `wave-admitted` event ids that a `dispatch_decision` names as its
240
+ * direct causal parent — the shape a wave-runner's own agent takes when it is
241
+ * dispatched straight off the admission rather than through a task. Read from
242
+ * raw events rather than `foldAgents`, because that fold only keeps agents
243
+ * with a task id, and this dispatch has none.
244
+ */
245
+ function dispatchedDirectlyOn(events) {
246
+ const parents = new Set();
247
+ for (const event of events) {
248
+ if (event.record.event_type !== DISPATCH_EVENT_TYPE)
249
+ continue;
250
+ const parent = event.record.causal_parent;
251
+ if (typeof parent === 'string' && parent.length > 0)
252
+ parents.add(parent);
253
+ }
254
+ return parents;
255
+ }
133
256
  /**
134
257
  * Fold one task's agents into the interval it was worked on. Every role counts,
135
258
  * not just the coder's: a task whose reviewer is still running is still work
@@ -199,6 +322,14 @@ export function auditWaveConcurrency(events, options = {}) {
199
322
  else
200
323
  agentsByTask.set(agent.taskId, [agent]);
201
324
  }
325
+ // Also built from every event, unfiltered: off-lineage evidence for a wave
326
+ // the caller narrowed away must not leak in, but an admission's own window
327
+ // must see all of it regardless of which epic the caller asked for. Merge
328
+ // attribution needs every admission as a candidate, not just the ones the
329
+ // caller's epic filter kept — the same reason `admissions` above is built
330
+ // before that filter is applied.
331
+ const mergeCredited = admissionsWithMergeEvidence(events, admissions);
332
+ const directDispatchParents = dispatchedDirectlyOn(events);
202
333
  const waves = [];
203
334
  for (const admission of admissions) {
204
335
  if (options.epicId !== undefined && admission.epicId !== options.epicId)
@@ -219,6 +350,7 @@ export function auditWaveConcurrency(events, options = {}) {
219
350
  }
220
351
  observed.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
221
352
  const peak = peakOverlap(observed);
353
+ const offLineageEvidence = directDispatchParents.has(admission.eventId) || mergeCredited.has(admission.eventId);
222
354
  waves.push({
223
355
  eventId: admission.eventId,
224
356
  admittedAt: admission.ts,
@@ -227,7 +359,7 @@ export function auditWaveConcurrency(events, options = {}) {
227
359
  observed,
228
360
  unobserved,
229
361
  peak,
230
- verdict: verdictFor(admission.taskIds.length, observed.length, peak),
362
+ verdict: verdictFor(admission.taskIds.length, observed.length, peak, offLineageEvidence),
231
363
  });
232
364
  }
233
365
  return waves;
@@ -264,16 +396,20 @@ function labelsWith(waves, verdict) {
264
396
  export function summariseWaveConcurrency(waves) {
265
397
  const serialized = labelsWith(waves, 'serialized');
266
398
  const unobserved = labelsWith(waves, 'unobserved');
399
+ const unlinked = labelsWith(waves, 'unlinked');
267
400
  return {
268
401
  waves: [...waves],
269
402
  serialized,
270
403
  partial: labelsWith(waves, 'partial'),
271
404
  unobserved,
405
+ unlinked,
272
406
  widest: {
273
407
  declared: waves.reduce((max, wave) => Math.max(max, wave.declared.length), 0),
274
408
  observed: waves.reduce((max, wave) => Math.max(max, wave.peak), 0),
275
409
  },
276
- hint: unobserved.length > 0 ? UNOBSERVED_HINT : '',
410
+ hint: unobserved.length > 0 ? UNOBSERVED_HINT : unlinked.length > 0 ? UNLINKED_HINT : '',
411
+ // `unlinked` is real work off-lineage, not a failure to judge, so it never
412
+ // worsens the exit code the way `unobserved` does.
277
413
  exitCode: serialized.length > 0 ? 1 : unobserved.length > 0 ? 2 : 0,
278
414
  };
279
415
  }
@@ -37,7 +37,7 @@ const WORK_DIRS = [
37
37
  * the roadmap and the stack answers are declarations the operator made, and a
38
38
  * team sharing the repository should share them.
39
39
  */
40
- const WORK_ROOT_GITIGNORE = `# Written by \`smith init\`. Blacksmith's state is a cache of its event log
40
+ const WORK_ROOT_GITIGNORE = `# Written by \`bs init\`. Blacksmith's state is a cache of its event log
41
41
  # and belongs to this machine; the answers under factory/ are declarations
42
42
  # this project made, so they are tracked.
43
43
  state/
@@ -3,7 +3,7 @@ import { SmithError } from './errors.js';
3
3
  import { runGit as git } from './git.js';
4
4
  export class WorktreeError extends SmithError {
5
5
  }
6
- /** The task-id segment reserved for an epic's integration branch (smith/<epic>/integration) —
6
+ /** The task-id segment reserved for an epic's integration branch (<prefix>/<epic>/integration) —
7
7
  * never a real task. Exported for db/projector.ts's foldTasks() guard and epic.ts's
8
8
  * runEpicVerdict(), both of which need to recognise `<epic>/integration` as non-task. */
9
9
  export const RESERVED_TASK_ID = 'integration';
@@ -16,6 +16,19 @@ function localBranchExists(projectDir, branch) {
16
16
  return false;
17
17
  }
18
18
  }
19
+ /**
20
+ * Whether a task's own branch (`taskBranchName`) already exists locally —
21
+ * i.e. whether the task has already been cut. Exported for cli.ts's
22
+ * `worktree create --from` guard: spec.ts's `successorChainPath` hands back
23
+ * the bare hop ids between a `--from` predecessor and its target purely from
24
+ * the event log, with no notion of git state; this is the git-aware
25
+ * predicate the guard pairs it with to refuse skipping past a hop that was
26
+ * already cut (`createTaskWorktree` branches straight off `--from`'s own
27
+ * branch, so a cut hop's commits would otherwise vanish silently).
28
+ */
29
+ export function taskBranchExists(projectDir, epic, taskId) {
30
+ return localBranchExists(projectDir, taskBranchName(epic, taskId, epicBranchPrefix(projectDir, epic)));
31
+ }
19
32
  /**
20
33
  * Best-effort default-branch detection: origin/HEAD symbolic ref first (the
21
34
  * normal case after a clone), then whatever branch is currently checked out,
@@ -92,13 +105,39 @@ export function taskWorktreeDir(projectDir, epic, taskId) {
92
105
  const project = path.resolve(projectDir);
93
106
  return path.join(path.dirname(project), '.wt', path.basename(project), bareTaskId(epic, taskId));
94
107
  }
95
- /** `smith/<epic>/integration` — the one branch an epic assembles onto. Exported for
96
- * integration.ts, which runs the check suite against it (D-42/P9-26). */
97
- export function integrationBranchName(epic) {
98
- return `smith/${epic}/integration`;
108
+ /** The branch prefix every new epic gets (bs-rename operator decision 1). */
109
+ export const BRANCH_PREFIX = 'bs';
110
+ /** The prefix `BRANCH_PREFIX` replaced. Still recognised so an epic already
111
+ * cut under it keeps landing new branches there (operator decision 3) rather
112
+ * than splitting one epic's worktrees across two branch families. */
113
+ export const LEGACY_BRANCH_PREFIX = 'smith';
114
+ /**
115
+ * Which prefix THIS epic's factory branches use: `smith` when
116
+ * `smith/<epic>/integration` already exists locally, `bs` otherwise —
117
+ * including for a brand-new epic, which has neither yet. Git-aware on
118
+ * purpose: this is the one place that decides, so integrationBranchName and
119
+ * taskBranchName can stay pure string joins driven by whatever prefix their
120
+ * caller resolved (or hands the bs default straight through, for the
121
+ * git-free callers — taskEvents.ts and db/projector.ts's fallback — that
122
+ * have no projectDir to ask).
123
+ */
124
+ export function epicBranchPrefix(projectDir, epic) {
125
+ return localBranchExists(projectDir, `${LEGACY_BRANCH_PREFIX}/${epic}/${RESERVED_TASK_ID}`)
126
+ ? LEGACY_BRANCH_PREFIX
127
+ : BRANCH_PREFIX;
128
+ }
129
+ /** `<prefix>/<epic>/integration` — the one branch an epic assembles onto.
130
+ * `prefix` defaults to BRANCH_PREFIX (`bs`); a caller with a projectDir
131
+ * passes `epicBranchPrefix(projectDir, epic)` to get the epic's actual
132
+ * prefix, legacy `smith/` included. Exported for integration.ts, which runs
133
+ * the check suite against it (D-42/P9-26). */
134
+ export function integrationBranchName(epic, prefix = BRANCH_PREFIX) {
135
+ return `${prefix}/${epic}/integration`;
99
136
  }
100
137
  /**
101
- * `smith/<epic>/<bare-task-id>` — the branch convention, defined once.
138
+ * `<prefix>/<epic>/<bare-task-id>` — the branch convention, defined once.
139
+ * `prefix` defaults to BRANCH_PREFIX (`bs`) for the same reason as
140
+ * integrationBranchName above.
102
141
  *
103
142
  * D-23/P9-12: exported because three places need to agree on it — this module
104
143
  * (which creates the branch), taskEvents.ts's `task-added` (which declares it
@@ -108,14 +147,14 @@ export function integrationBranchName(epic) {
108
147
  *
109
148
  * A task_id already embeds its epic ("epic-7/task-142", findings.ts's
110
149
  * convention) so strip that prefix before rejoining — concatenating epic +
111
- * full task_id double-embeds it (smith/epic-7/epic-7/task-142). The CLI hands
150
+ * full task_id double-embeds it (bs/epic-7/epic-7/task-142). The CLI hands
112
151
  * this whatever the operator typed, so both shapes really do arrive.
113
152
  */
114
- export function taskBranchName(epic, taskId) {
115
- return `smith/${epic}/${bareTaskId(epic, taskId)}`;
153
+ export function taskBranchName(epic, taskId, prefix = BRANCH_PREFIX) {
154
+ return `${prefix}/${epic}/${bareTaskId(epic, taskId)}`;
116
155
  }
117
156
  function ensureIntegrationBranch(projectDir, epic) {
118
- const branch = integrationBranchName(epic);
157
+ const branch = integrationBranchName(epic, epicBranchPrefix(projectDir, epic));
119
158
  if (!localBranchExists(projectDir, branch)) {
120
159
  const defaultBranch = detectDefaultBranch(projectDir);
121
160
  git(projectDir, ['branch', branch, defaultBranch]);
@@ -127,11 +166,12 @@ function ensureIntegrationBranch(projectDir, epic) {
127
166
  * sibling of whatever directory the project was handed as, never a path of
128
167
  * this module's own choosing, so a project outside this clone keeps its
129
168
  * worktrees outside it too (see taskWorktreeDir)
130
- * on branch smith/<epic>/<task-id>.
169
+ * on branch <prefix>/<epic>/<task-id>, where <prefix> is epicBranchPrefix's
170
+ * answer for this epic (`bs`, or `smith` for an epic already cut under it).
131
171
  *
132
- * Without `opts.from`, cut from the CURRENT head of smith/<epic>/integration
172
+ * Without `opts.from`, cut from the CURRENT head of <prefix>/<epic>/integration
133
173
  * (creating the integration branch from the default branch first if it
134
- * doesn't exist yet). With `opts.from`, cut from smith/<epic>/<from> instead
174
+ * doesn't exist yet). With `opts.from`, cut from <prefix>/<epic>/<from> instead
135
175
  * — the predecessor's branch, commits and all — and integration is never
136
176
  * consulted.
137
177
  */
@@ -139,11 +179,12 @@ export function createTaskWorktree(projectDir, epic, taskId, opts = {}) {
139
179
  if (taskId === RESERVED_TASK_ID) {
140
180
  throw new WorktreeError('worktree.reserved-task-id', `Task id "${RESERVED_TASK_ID}" is reserved for the integration branch itself.`, { taskId });
141
181
  }
142
- const branch = taskBranchName(epic, taskId);
182
+ const prefix = epicBranchPrefix(projectDir, epic);
183
+ const branch = taskBranchName(epic, taskId, prefix);
143
184
  const worktreeDir = taskWorktreeDir(projectDir, epic, taskId);
144
185
  let sourceBranch;
145
186
  if (opts.from !== undefined) {
146
- sourceBranch = taskBranchName(epic, opts.from);
187
+ sourceBranch = taskBranchName(epic, opts.from, prefix);
147
188
  if (!localBranchExists(projectDir, sourceBranch)) {
148
189
  throw new WorktreeError('worktree.from-missing', `Predecessor branch ${sourceBranch} does not exist; cannot cut ${taskId} from it.`, { epic, taskId, from: opts.from, branch: sourceBranch });
149
190
  }
@@ -157,7 +198,7 @@ export function createTaskWorktree(projectDir, epic, taskId, opts = {}) {
157
198
  /** Remove a task's worktree and its local branch after it has merged. */
158
199
  export function removeTaskWorktree(projectDir, epic, taskId) {
159
200
  const worktreeDir = taskWorktreeDir(projectDir, epic, taskId);
160
- const branch = taskBranchName(epic, taskId);
201
+ const branch = taskBranchName(epic, taskId, epicBranchPrefix(projectDir, epic));
161
202
  try {
162
203
  git(projectDir, ['worktree', 'remove', worktreeDir]);
163
204
  }
@@ -193,12 +234,13 @@ function parseWorktreePorcelain(output) {
193
234
  return entries;
194
235
  }
195
236
  /**
196
- * List task worktrees whose branch is fully merged into smith/<epic>/integration.
237
+ * List task worktrees whose branch is fully merged into <prefix>/<epic>/integration.
197
238
  * Reports only — never deletes.
198
239
  */
199
240
  export function listStale(projectDir, epic) {
200
- const integrationBranch = integrationBranchName(epic);
201
- const prefix = `smith/${epic}/`;
241
+ const branchPrefix = epicBranchPrefix(projectDir, epic);
242
+ const integrationBranch = integrationBranchName(epic, branchPrefix);
243
+ const prefix = `${branchPrefix}/${epic}/`;
202
244
  const porcelain = git(projectDir, ['worktree', 'list', '--porcelain']);
203
245
  const entries = parseWorktreePorcelain(porcelain);
204
246
  const stale = [];
@@ -0,0 +1,19 @@
1
+ CREATE TABLE `operator_feedback` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `task_id` text NOT NULL,
4
+ `session_id` text NOT NULL,
5
+ `body` text NOT NULL,
6
+ `kind` text NOT NULL,
7
+ `source` text NOT NULL,
8
+ `external_id` text,
9
+ `author` text,
10
+ `recorded_at` text NOT NULL,
11
+ `recorded_event_id` text NOT NULL,
12
+ `resolved_at` text,
13
+ `resolution` text,
14
+ `follow_up_task_id` text
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE INDEX `operator_feedback_task_idx` ON `operator_feedback` (`task_id`);--> statement-breakpoint
18
+ CREATE INDEX `operator_feedback_session_idx` ON `operator_feedback` (`session_id`);--> statement-breakpoint
19
+ CREATE INDEX `operator_feedback_external_idx` ON `operator_feedback` (`external_id`);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `tasks` ADD `title` text;--> statement-breakpoint
2
+ ALTER TABLE `tasks` ADD `summary` text;
@@ -0,0 +1 @@
1
+ ALTER TABLE `tasks` ADD `terminal_at` text;
@@ -0,0 +1,30 @@
1
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
2
+ CREATE TABLE `__new_milestones` (
3
+ `milestone_id` text PRIMARY KEY NOT NULL,
4
+ `name` text NOT NULL,
5
+ `status` text NOT NULL,
6
+ `sequence` integer NOT NULL,
7
+ `goal` text,
8
+ `epic_ids` text NOT NULL,
9
+ `project` text DEFAULT 'blacksmith' NOT NULL,
10
+ `kind` text DEFAULT 'factory' NOT NULL,
11
+ `error_issues` integer DEFAULT true NOT NULL
12
+ );
13
+ --> statement-breakpoint
14
+ INSERT INTO `__new_milestones`("milestone_id", "name", "status", "sequence", "goal", "epic_ids", "project", "kind", "error_issues") SELECT "milestone_id", "name", "status", "sequence", "goal", "epic_ids", "project", "kind", "error_issues" FROM `milestones`;--> statement-breakpoint
15
+ DROP TABLE `milestones`;--> statement-breakpoint
16
+ ALTER TABLE `__new_milestones` RENAME TO `milestones`;--> statement-breakpoint
17
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
18
+ CREATE INDEX `milestones_project_idx` ON `milestones` (`project`);--> statement-breakpoint
19
+ -- The factory's own project was renamed from 'black-smith' to 'blacksmith'.
20
+ -- History (events, committed plan files, old roadmap.md bullets) keeps the
21
+ -- old spelling, but every table's read model should not: rewrite every row
22
+ -- that already carries the old name in every table with a project column.
23
+ UPDATE `events_raw` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
24
+ UPDATE `dispatches` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
25
+ UPDATE `tasks` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
26
+ UPDATE `errors` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
27
+ UPDATE `findings` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
28
+ UPDATE `epics` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
29
+ UPDATE `milestones` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';--> statement-breakpoint
30
+ UPDATE `issue_reports` SET `project` = 'blacksmith' WHERE `project` = 'black-smith';
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `events_raw` ADD `cli_session_id` text;--> statement-breakpoint
2
+ CREATE INDEX `events_raw_cli_session_idx` ON `events_raw` (`cli_session_id`);
@@ -92,6 +92,41 @@
92
92
  "when": 1789464480602,
93
93
  "tag": "0012_issue_reports_error_issues",
94
94
  "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "6",
99
+ "when": 1790738459253,
100
+ "tag": "0013_operator_feedback",
101
+ "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "6",
106
+ "when": 1790818969124,
107
+ "tag": "0014_misty_madripoor",
108
+ "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "6",
113
+ "when": 1790924037810,
114
+ "tag": "0015_gifted_omega_flight",
115
+ "breakpoints": true
116
+ },
117
+ {
118
+ "idx": 16,
119
+ "version": "6",
120
+ "when": 1791191766572,
121
+ "tag": "0016_milestones_project_default",
122
+ "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "6",
127
+ "when": 1791262161043,
128
+ "tag": "0017_events_raw_cli_session_id",
129
+ "breakpoints": true
95
130
  }
96
131
  ]
97
132
  }