@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.
- package/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +4 -4
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +34 -27
- package/.claude/skills/bs/dispatch.md +100 -50
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +27 -18
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +181 -49
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +109 -53
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +48 -14
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +125 -17
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +790 -100
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +154 -36
- package/factory/orchestrator/dist/db/projector.js +278 -29
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +13 -37
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +323 -38
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +10 -4
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +105 -3
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +83 -12
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +92 -3
- package/factory/orchestrator/dist/integration.js +9 -7
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +90 -32
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +10 -7
- package/factory/orchestrator/dist/plan.js +269 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +4 -4
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +177 -13
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +179 -27
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +16 -13
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +97 -40
- package/factory/orchestrator/dist/waivers.js +39 -5
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +61 -19
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +7 -7
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- 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
|
|
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 = [
|
|
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 \`
|
|
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 (
|
|
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
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
201
|
-
const
|
|
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 @@
|
|
|
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';
|
|
@@ -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
|
}
|