@wrongstack/sdd 0.295.0 → 0.295.1
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/dist/auto-executor.d.ts +3 -4
- package/dist/auto-executor.d.ts.map +1 -1
- package/dist/board-types.d.ts +4 -0
- package/dist/board-types.d.ts.map +1 -1
- package/dist/conflict-resolver.d.ts +7 -1
- package/dist/conflict-resolver.d.ts.map +1 -1
- package/dist/critical-path.d.ts.map +1 -1
- package/dist/decompose-task.d.ts +21 -0
- package/dist/decompose-task.d.ts.map +1 -1
- package/dist/graph-split.d.ts +25 -0
- package/dist/graph-split.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +529 -240
- package/dist/index.js.map +4 -4
- package/dist/plan-decompose.d.ts +47 -0
- package/dist/plan-decompose.d.ts.map +1 -0
- package/dist/sdd-board-projector.d.ts +14 -1
- package/dist/sdd-board-projector.d.ts.map +1 -1
- package/dist/sdd-board-store.d.ts +16 -0
- package/dist/sdd-board-store.d.ts.map +1 -1
- package/dist/sdd-interview-driver.d.ts +2 -2
- package/dist/sdd-interview-driver.d.ts.map +1 -1
- package/dist/sdd-lifecycle.d.ts.map +1 -1
- package/dist/sdd-parallel-run.d.ts +9 -3
- package/dist/sdd-parallel-run.d.ts.map +1 -1
- package/dist/spec-builder.d.ts +1 -3
- package/dist/spec-builder.d.ts.map +1 -1
- package/dist/spec-parser.d.ts +1 -1
- package/dist/spec-parser.d.ts.map +1 -1
- package/dist/spec-versioning.d.ts +1 -2
- package/dist/spec-versioning.d.ts.map +1 -1
- package/dist/start-sdd-run.d.ts +8 -2
- package/dist/start-sdd-run.d.ts.map +1 -1
- package/dist/task-flow.d.ts +1 -3
- package/dist/task-flow.d.ts.map +1 -1
- package/dist/task-generator.d.ts +31 -2
- package/dist/task-generator.d.ts.map +1 -1
- package/dist/task-visualizer.d.ts +1 -2
- package/dist/task-visualizer.d.ts.map +1 -1
- package/dist/verify-task.d.ts +31 -2
- package/dist/verify-task.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -57,13 +57,13 @@ var SpecParser = class {
|
|
|
57
57
|
if (h2) {
|
|
58
58
|
if (currentSection && currentLines.length > 0) {
|
|
59
59
|
sections.push({
|
|
60
|
-
type: this.mapSectionType(currentSection.title
|
|
61
|
-
title: currentSection.title
|
|
60
|
+
type: this.mapSectionType(currentSection.title),
|
|
61
|
+
title: currentSection.title,
|
|
62
62
|
level: depth,
|
|
63
63
|
content: currentLines.join("\n").trim()
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
-
currentSection = { title: h2[1]
|
|
66
|
+
currentSection = { title: h2[1] };
|
|
67
67
|
currentLines = [];
|
|
68
68
|
depth = 2;
|
|
69
69
|
continue;
|
|
@@ -78,8 +78,8 @@ var SpecParser = class {
|
|
|
78
78
|
}
|
|
79
79
|
if (currentSection && currentLines.length > 0) {
|
|
80
80
|
sections.push({
|
|
81
|
-
type: this.mapSectionType(currentSection.title
|
|
82
|
-
title: currentSection.title
|
|
81
|
+
type: this.mapSectionType(currentSection.title),
|
|
82
|
+
title: currentSection.title,
|
|
83
83
|
level: depth,
|
|
84
84
|
content: currentLines.join("\n").trim()
|
|
85
85
|
});
|
|
@@ -227,6 +227,39 @@ var SpecParser = class {
|
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
// src/task-generator.ts
|
|
230
|
+
import { assessAtomicity } from "@wrongstack/kanban";
|
|
231
|
+
var OVERVIEW_ESTIMATE_HOURS = 4;
|
|
232
|
+
var REQUIREMENT_ESTIMATE_HOURS = {
|
|
233
|
+
critical: 8,
|
|
234
|
+
high: 4,
|
|
235
|
+
medium: 2,
|
|
236
|
+
low: 1
|
|
237
|
+
};
|
|
238
|
+
var API_PARENT_ESTIMATE_HOURS = 0;
|
|
239
|
+
var API_BASE_ESTIMATE_HOURS = 2;
|
|
240
|
+
var TESTS_ESTIMATE_HOURS = 4;
|
|
241
|
+
var DOCS_ESTIMATE_HOURS = 2;
|
|
242
|
+
function assessGeneratedTaskAtomicity(task, config) {
|
|
243
|
+
const criteria = task.acceptanceCriteria ?? [];
|
|
244
|
+
const assessment = assessAtomicity(
|
|
245
|
+
{
|
|
246
|
+
title: task.title,
|
|
247
|
+
description: task.description,
|
|
248
|
+
estimatedHours: task.estimateHours,
|
|
249
|
+
// Graph edges are wired after generation; fan-in is unknown here.
|
|
250
|
+
dependencyCount: 0,
|
|
251
|
+
successCriteriaCount: criteria.length,
|
|
252
|
+
hasVerifiableOutput: extractVerificationCommand(criteria) !== void 0,
|
|
253
|
+
childCount: 0
|
|
254
|
+
},
|
|
255
|
+
config
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
verdict: assessment.verdict,
|
|
259
|
+
score: assessment.score,
|
|
260
|
+
reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
230
263
|
function extractVerificationCommand(criteria) {
|
|
231
264
|
const marker = /^\s*(?:\$\s+|(?:run|verify|cmd)\s*:\s*)(.+\S)\s*$/i;
|
|
232
265
|
for (const c of criteria) {
|
|
@@ -240,17 +273,29 @@ var TaskGenerator = class {
|
|
|
240
273
|
this.opts = opts;
|
|
241
274
|
}
|
|
242
275
|
opts;
|
|
276
|
+
/** metadata.atomicity payload when the option is enabled, else undefined. */
|
|
277
|
+
atomicityMetadata(task) {
|
|
278
|
+
if (!this.opts.atomicity) return void 0;
|
|
279
|
+
return { atomicity: assessGeneratedTaskAtomicity(task, this.opts.atomicity.config) };
|
|
280
|
+
}
|
|
243
281
|
async generateFromSpec(spec) {
|
|
244
282
|
const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
|
|
245
283
|
const overviewSection = spec.sections?.find((s) => s.type === "overview");
|
|
246
284
|
if (overviewSection?.content) {
|
|
247
|
-
|
|
285
|
+
const overview = {
|
|
248
286
|
title: `Implement: ${spec.title}`,
|
|
249
287
|
description: overviewSection.content,
|
|
288
|
+
estimateHours: OVERVIEW_ESTIMATE_HOURS
|
|
289
|
+
};
|
|
290
|
+
const metadata = this.atomicityMetadata(overview);
|
|
291
|
+
this.opts.taskTracker.addNode({
|
|
292
|
+
title: overview.title,
|
|
293
|
+
description: overview.description,
|
|
250
294
|
type: "feature",
|
|
251
295
|
priority: "high",
|
|
252
296
|
status: "pending",
|
|
253
|
-
estimateHours:
|
|
297
|
+
estimateHours: overview.estimateHours,
|
|
298
|
+
...metadata ? { metadata } : {}
|
|
254
299
|
});
|
|
255
300
|
}
|
|
256
301
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -258,7 +303,7 @@ var TaskGenerator = class {
|
|
|
258
303
|
(a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
|
|
259
304
|
);
|
|
260
305
|
for (const req of sorted) {
|
|
261
|
-
const estimateHours = req.priority
|
|
306
|
+
const estimateHours = REQUIREMENT_ESTIMATE_HOURS[req.priority] ?? 1;
|
|
262
307
|
const tags = [req.type, req.priority];
|
|
263
308
|
const acLines = (req.acceptanceCriteria ?? []).map((ac) => `- ${ac}`).join("\n");
|
|
264
309
|
const blockedLine = req.blockedBy?.length ? `
|
|
@@ -270,7 +315,14 @@ var TaskGenerator = class {
|
|
|
270
315
|
|
|
271
316
|
**Acceptance Criteria:**
|
|
272
317
|
${acLines}` : "") + blockedLine;
|
|
273
|
-
const metadata = {
|
|
318
|
+
const metadata = {
|
|
319
|
+
...this.atomicityMetadata({
|
|
320
|
+
title: req.description,
|
|
321
|
+
description,
|
|
322
|
+
estimateHours,
|
|
323
|
+
acceptanceCriteria: req.acceptanceCriteria ?? []
|
|
324
|
+
})
|
|
325
|
+
};
|
|
274
326
|
if (this.opts.verificationFromAcceptance) {
|
|
275
327
|
const cmd = extractVerificationCommand(req.acceptanceCriteria ?? []);
|
|
276
328
|
if (cmd) metadata.verificationCommand = cmd;
|
|
@@ -294,38 +346,58 @@ ${acLines}` : "") + blockedLine;
|
|
|
294
346
|
type: "feature",
|
|
295
347
|
priority: "high",
|
|
296
348
|
status: "pending",
|
|
297
|
-
estimateHours:
|
|
349
|
+
estimateHours: API_PARENT_ESTIMATE_HOURS
|
|
298
350
|
});
|
|
299
351
|
for (const ep of spec.apiEndpoints) {
|
|
300
|
-
const baseHours = 2;
|
|
301
352
|
const authHours = ep.auth ? 1 : 0;
|
|
302
353
|
const reqHours = ep.request ? 1 : 0;
|
|
303
|
-
|
|
354
|
+
const endpoint = {
|
|
304
355
|
title: `${ep.method} ${ep.path} \u2014 ${ep.description}`,
|
|
305
356
|
description: `${ep.method} ${ep.path}: ${ep.description}`,
|
|
357
|
+
estimateHours: API_BASE_ESTIMATE_HOURS + authHours + reqHours
|
|
358
|
+
};
|
|
359
|
+
const metadata = this.atomicityMetadata(endpoint);
|
|
360
|
+
this.opts.taskTracker.addNode({
|
|
361
|
+
title: endpoint.title,
|
|
362
|
+
description: endpoint.description,
|
|
306
363
|
type: "feature",
|
|
307
364
|
priority: "medium",
|
|
308
365
|
status: "pending",
|
|
309
|
-
estimateHours:
|
|
310
|
-
parentId: apiParent.id
|
|
366
|
+
estimateHours: endpoint.estimateHours,
|
|
367
|
+
parentId: apiParent.id,
|
|
368
|
+
...metadata ? { metadata } : {}
|
|
311
369
|
});
|
|
312
370
|
}
|
|
313
371
|
}
|
|
314
|
-
|
|
372
|
+
const testsTask = {
|
|
315
373
|
title: "Write Tests",
|
|
316
374
|
description: "Write comprehensive tests for the implemented features.",
|
|
375
|
+
estimateHours: TESTS_ESTIMATE_HOURS
|
|
376
|
+
};
|
|
377
|
+
const testsMetadata = this.atomicityMetadata(testsTask);
|
|
378
|
+
this.opts.taskTracker.addNode({
|
|
379
|
+
title: testsTask.title,
|
|
380
|
+
description: testsTask.description,
|
|
317
381
|
type: "test",
|
|
318
382
|
priority: "high",
|
|
319
383
|
status: "pending",
|
|
320
|
-
estimateHours:
|
|
384
|
+
estimateHours: testsTask.estimateHours,
|
|
385
|
+
...testsMetadata ? { metadata: testsMetadata } : {}
|
|
321
386
|
});
|
|
322
|
-
|
|
387
|
+
const docsTask = {
|
|
323
388
|
title: "Update Documentation",
|
|
324
389
|
description: "Update project documentation to reflect the changes.",
|
|
390
|
+
estimateHours: DOCS_ESTIMATE_HOURS
|
|
391
|
+
};
|
|
392
|
+
const docsMetadata = this.atomicityMetadata(docsTask);
|
|
393
|
+
this.opts.taskTracker.addNode({
|
|
394
|
+
title: docsTask.title,
|
|
395
|
+
description: docsTask.description,
|
|
325
396
|
type: "docs",
|
|
326
397
|
priority: "low",
|
|
327
398
|
status: "pending",
|
|
328
|
-
estimateHours:
|
|
399
|
+
estimateHours: docsTask.estimateHours,
|
|
400
|
+
...docsMetadata ? { metadata: docsMetadata } : {}
|
|
329
401
|
});
|
|
330
402
|
return graph;
|
|
331
403
|
}
|
|
@@ -356,8 +428,9 @@ import {
|
|
|
356
428
|
} from "@wrongstack/core/tasking";
|
|
357
429
|
|
|
358
430
|
// src/task-flow.ts
|
|
359
|
-
import { SddError, ERROR_CODES } from "@wrongstack/core/types";
|
|
360
431
|
import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
|
|
432
|
+
import { ERROR_CODES, SddError } from "@wrongstack/core/types";
|
|
433
|
+
import { expectDefined } from "@wrongstack/core/utils";
|
|
361
434
|
var TaskFlow = class {
|
|
362
435
|
constructor(opts) {
|
|
363
436
|
this.opts = opts;
|
|
@@ -397,11 +470,12 @@ var TaskFlow = class {
|
|
|
397
470
|
return this.graph;
|
|
398
471
|
}
|
|
399
472
|
async execute(ctx) {
|
|
400
|
-
if (!this.graph)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
473
|
+
if (!this.graph)
|
|
474
|
+
throw new SddError({
|
|
475
|
+
message: "No graph loaded. Call fromSpec first.",
|
|
476
|
+
code: ERROR_CODES.SDD_INVALID_STATE,
|
|
477
|
+
context: { phase: this.phase }
|
|
478
|
+
});
|
|
405
479
|
this.setPhase("executing");
|
|
406
480
|
this.stopped = false;
|
|
407
481
|
const pendingTasks = this.getExecutableTasks();
|
|
@@ -412,9 +486,8 @@ var TaskFlow = class {
|
|
|
412
486
|
batch.map((task) => this.executeSingleTask(task, ctx))
|
|
413
487
|
);
|
|
414
488
|
for (let i = 0; i < results.length; i++) {
|
|
415
|
-
const result = results[i];
|
|
416
|
-
const task = batch[i];
|
|
417
|
-
if (!result || !task) continue;
|
|
489
|
+
const result = expectDefined(results[i]);
|
|
490
|
+
const task = expectDefined(batch[i]);
|
|
418
491
|
if (result.status === "rejected") {
|
|
419
492
|
const reason = result.reason;
|
|
420
493
|
this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
|
|
@@ -441,11 +514,12 @@ var TaskFlow = class {
|
|
|
441
514
|
}
|
|
442
515
|
async reviewTask(taskId, approved, comment) {
|
|
443
516
|
const task = this.opts.tracker.getNode(taskId);
|
|
444
|
-
if (!task)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
517
|
+
if (!task)
|
|
518
|
+
throw new SddError({
|
|
519
|
+
message: `Task ${taskId} not found`,
|
|
520
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
521
|
+
context: { taskId }
|
|
522
|
+
});
|
|
449
523
|
if (approved) {
|
|
450
524
|
this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
|
|
451
525
|
this.emit("task.completed", { taskId });
|
|
@@ -474,7 +548,7 @@ var TaskFlow = class {
|
|
|
474
548
|
getExecutableTasks() {
|
|
475
549
|
return this.opts.tracker.getAllNodes({ status: ["pending", "blocked"] }).filter((n) => n.status === "pending" && this.opts.tracker.canStart(n.id)).sort((a, b) => {
|
|
476
550
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
477
|
-
return
|
|
551
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
478
552
|
});
|
|
479
553
|
}
|
|
480
554
|
async executeSingleTask(task, ctx) {
|
|
@@ -792,14 +866,14 @@ function buildBoardTasks(graph) {
|
|
|
792
866
|
return d;
|
|
793
867
|
};
|
|
794
868
|
const toTask = (n) => {
|
|
795
|
-
const deps = blockers.get(n.id)
|
|
869
|
+
const deps = blockers.get(n.id);
|
|
796
870
|
const allDepsDone = deps.every((b) => statusOf(b) === "completed");
|
|
797
871
|
const meta = n.metadata ?? {};
|
|
798
872
|
const cancelled = Boolean(meta["cancelled"]);
|
|
799
873
|
const displayStatus = cancelled ? "cancelled" : n.status === "pending" && deps.length > 0 && allDepsDone ? "queued" : n.status;
|
|
800
874
|
return {
|
|
801
875
|
id: n.id,
|
|
802
|
-
shortId: shortId.get(n.id)
|
|
876
|
+
shortId: shortId.get(n.id),
|
|
803
877
|
title: n.title,
|
|
804
878
|
description: n.description,
|
|
805
879
|
status: n.status,
|
|
@@ -815,7 +889,9 @@ function buildBoardTasks(graph) {
|
|
|
815
889
|
model: typeof meta["model"] === "string" ? meta["model"] : void 0,
|
|
816
890
|
provider: typeof meta["provider"] === "string" ? meta["provider"] : void 0,
|
|
817
891
|
fallbackModels: Array.isArray(meta["fallbackModels"]) ? meta["fallbackModels"] : void 0,
|
|
818
|
-
verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0
|
|
892
|
+
verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0,
|
|
893
|
+
verificationState: meta["verificationState"] === "passed" || meta["verificationState"] === "failed" ? meta["verificationState"] : void 0,
|
|
894
|
+
verificationDetail: typeof meta["verificationDetail"] === "string" ? meta["verificationDetail"] : void 0
|
|
819
895
|
};
|
|
820
896
|
};
|
|
821
897
|
const tasks = nodes.map(toTask);
|
|
@@ -823,9 +899,9 @@ function buildBoardTasks(graph) {
|
|
|
823
899
|
for (const n of nodes) {
|
|
824
900
|
const d = depthOf(n.id);
|
|
825
901
|
if (!byDepth.has(d)) byDepth.set(d, []);
|
|
826
|
-
byDepth.get(d)
|
|
902
|
+
byDepth.get(d).push(shortId.get(n.id));
|
|
827
903
|
}
|
|
828
|
-
const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d)
|
|
904
|
+
const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) }));
|
|
829
905
|
return { tasks, columns };
|
|
830
906
|
}
|
|
831
907
|
function buildBoardSnapshot(graph, run, now) {
|
|
@@ -864,6 +940,7 @@ var SddBoardStore = class {
|
|
|
864
940
|
eventMaxBytes;
|
|
865
941
|
eventKeepBytes;
|
|
866
942
|
eventSizeCheckEvery;
|
|
943
|
+
controlFileIO;
|
|
867
944
|
eventChains = /* @__PURE__ */ new Map();
|
|
868
945
|
eventWritesSinceCheck = /* @__PURE__ */ new Map();
|
|
869
946
|
controlDrains = /* @__PURE__ */ new Map();
|
|
@@ -882,6 +959,7 @@ var SddBoardStore = class {
|
|
|
882
959
|
1,
|
|
883
960
|
Math.floor(opts.eventSizeCheckEvery ?? DEFAULT_EVENT_SIZE_CHECK_EVERY)
|
|
884
961
|
);
|
|
962
|
+
this.controlFileIO = opts.controlFileIO ?? fsp3;
|
|
885
963
|
}
|
|
886
964
|
snapshotPath(runId) {
|
|
887
965
|
return path3.join(this.baseDir, `${this.safe(runId)}.json`);
|
|
@@ -952,12 +1030,12 @@ var SddBoardStore = class {
|
|
|
952
1030
|
try {
|
|
953
1031
|
return await drain;
|
|
954
1032
|
} finally {
|
|
955
|
-
|
|
1033
|
+
this.controlDrains.delete(filePath);
|
|
956
1034
|
}
|
|
957
1035
|
}
|
|
958
1036
|
async delete(runId) {
|
|
959
1037
|
const eventPath = this.eventsPath(runId);
|
|
960
|
-
await this.eventChains.get(eventPath)
|
|
1038
|
+
await this.eventChains.get(eventPath);
|
|
961
1039
|
this.eventChains.delete(eventPath);
|
|
962
1040
|
await Promise.allSettled([
|
|
963
1041
|
fsp3.unlink(this.snapshotPath(runId)),
|
|
@@ -1005,13 +1083,10 @@ var SddBoardStore = class {
|
|
|
1005
1083
|
const buffer = Buffer.allocUnsafe(length);
|
|
1006
1084
|
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
1007
1085
|
retained = buffer.subarray(0, bytesRead);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const firstNewline = retained.indexOf(10);
|
|
1013
|
-
retained = firstNewline >= 0 ? retained.subarray(firstNewline + 1) : retained.subarray(0, 0);
|
|
1014
|
-
}
|
|
1086
|
+
const previous = Buffer.allocUnsafe(1);
|
|
1087
|
+
await handle.read(previous, 0, 1, start - 1);
|
|
1088
|
+
if (previous[0] !== 10) {
|
|
1089
|
+
retained = retained.subarray(retained.indexOf(10) + 1);
|
|
1015
1090
|
}
|
|
1016
1091
|
} finally {
|
|
1017
1092
|
await handle.close();
|
|
@@ -1020,7 +1095,7 @@ var SddBoardStore = class {
|
|
|
1020
1095
|
}
|
|
1021
1096
|
async drainControlInternal(filePath) {
|
|
1022
1097
|
try {
|
|
1023
|
-
const stat2 = await
|
|
1098
|
+
const stat2 = await this.controlFileIO.stat(filePath);
|
|
1024
1099
|
if (stat2.size === 0) return [];
|
|
1025
1100
|
} catch {
|
|
1026
1101
|
return [];
|
|
@@ -1028,14 +1103,14 @@ var SddBoardStore = class {
|
|
|
1028
1103
|
return withFileLock(filePath, async () => {
|
|
1029
1104
|
let raw;
|
|
1030
1105
|
try {
|
|
1031
|
-
const stat2 = await
|
|
1106
|
+
const stat2 = await this.controlFileIO.stat(filePath);
|
|
1032
1107
|
if (stat2.size === 0) return [];
|
|
1033
|
-
raw = await
|
|
1108
|
+
raw = await this.controlFileIO.readFile(filePath, "utf8");
|
|
1034
1109
|
} catch {
|
|
1035
1110
|
return [];
|
|
1036
1111
|
}
|
|
1037
1112
|
try {
|
|
1038
|
-
await
|
|
1113
|
+
await this.controlFileIO.truncate(filePath, 0);
|
|
1039
1114
|
} catch {
|
|
1040
1115
|
return [];
|
|
1041
1116
|
}
|
|
@@ -1160,7 +1235,6 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1160
1235
|
mergedCommits = [];
|
|
1161
1236
|
/** Base branch reported by the run at start (overrides the constructor option). */
|
|
1162
1237
|
runBaseBranch;
|
|
1163
|
-
dirty = false;
|
|
1164
1238
|
timer = null;
|
|
1165
1239
|
unsubs = [];
|
|
1166
1240
|
/** Latest snapshot waiting behind an in-flight disk write. */
|
|
@@ -1189,7 +1263,11 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1189
1263
|
});
|
|
1190
1264
|
this.onRun("sdd.wave", (e) => {
|
|
1191
1265
|
this.wave = e.wave;
|
|
1192
|
-
this.pushFeed({
|
|
1266
|
+
this.pushFeed({
|
|
1267
|
+
ts: this.now(),
|
|
1268
|
+
kind: "wave",
|
|
1269
|
+
text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel`
|
|
1270
|
+
});
|
|
1193
1271
|
this.markDirty();
|
|
1194
1272
|
});
|
|
1195
1273
|
this.onRun("sdd.deadlock", (e) => {
|
|
@@ -1197,7 +1275,11 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1197
1275
|
blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
|
|
1198
1276
|
blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
|
|
1199
1277
|
}));
|
|
1200
|
-
this.pushFeed({
|
|
1278
|
+
this.pushFeed({
|
|
1279
|
+
ts: this.now(),
|
|
1280
|
+
kind: "deadlock",
|
|
1281
|
+
text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work`
|
|
1282
|
+
});
|
|
1201
1283
|
this.markDirty();
|
|
1202
1284
|
});
|
|
1203
1285
|
this.onRun("sdd.task.started", (e) => {
|
|
@@ -1377,7 +1459,8 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1377
1459
|
}
|
|
1378
1460
|
pushFeed(entry) {
|
|
1379
1461
|
this.feed.unshift(entry);
|
|
1380
|
-
if (this.feed.length > _SddBoardProjector.FEED_CAP)
|
|
1462
|
+
if (this.feed.length > _SddBoardProjector.FEED_CAP)
|
|
1463
|
+
this.feed.length = _SddBoardProjector.FEED_CAP;
|
|
1381
1464
|
if (entry.taskId) {
|
|
1382
1465
|
const taskFeed = this.taskEvents.get(entry.taskId) ?? [];
|
|
1383
1466
|
taskFeed.unshift(entry);
|
|
@@ -1402,12 +1485,7 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1402
1485
|
}
|
|
1403
1486
|
/** Resolve once all in-flight snapshot persistence has settled. */
|
|
1404
1487
|
async drain() {
|
|
1405
|
-
|
|
1406
|
-
const observed = this.saveLoop;
|
|
1407
|
-
if (!observed) return;
|
|
1408
|
-
await observed;
|
|
1409
|
-
if (observed === this.saveLoop && !this.pendingSnapshot) return;
|
|
1410
|
-
}
|
|
1488
|
+
while (this.saveLoop) await this.saveLoop;
|
|
1411
1489
|
}
|
|
1412
1490
|
/** Stop projecting and release subscriptions. */
|
|
1413
1491
|
dispose() {
|
|
@@ -1474,15 +1552,13 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1474
1552
|
return snap;
|
|
1475
1553
|
}
|
|
1476
1554
|
markDirty() {
|
|
1477
|
-
this.dirty = true;
|
|
1478
1555
|
if (this.timer || this.finished) return;
|
|
1479
1556
|
this.timer = setTimeout(() => {
|
|
1480
1557
|
this.timer = null;
|
|
1481
|
-
|
|
1558
|
+
this.flush();
|
|
1482
1559
|
}, this.throttleMs);
|
|
1483
1560
|
}
|
|
1484
1561
|
flush() {
|
|
1485
|
-
this.dirty = false;
|
|
1486
1562
|
if (this.timer) {
|
|
1487
1563
|
clearTimeout(this.timer);
|
|
1488
1564
|
this.timer = null;
|
|
@@ -1504,9 +1580,7 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1504
1580
|
const loop = this.persistPendingSnapshots(store);
|
|
1505
1581
|
this.saveLoop = loop;
|
|
1506
1582
|
void loop.finally(() => {
|
|
1507
|
-
if (this.saveLoop !== loop) return;
|
|
1508
1583
|
this.saveLoop = void 0;
|
|
1509
|
-
if (this.pendingSnapshot) this.startSaveLoop(store);
|
|
1510
1584
|
});
|
|
1511
1585
|
}
|
|
1512
1586
|
async persistPendingSnapshots(store) {
|
|
@@ -1537,9 +1611,12 @@ var SddRunRegistry = class {
|
|
|
1537
1611
|
}
|
|
1538
1612
|
};
|
|
1539
1613
|
|
|
1614
|
+
// src/sdd-interview-driver.ts
|
|
1615
|
+
import { DefaultTaskStore as DefaultTaskStore2, TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
|
|
1616
|
+
|
|
1540
1617
|
// src/spec-builder.ts
|
|
1541
|
-
import {
|
|
1542
|
-
import {
|
|
1618
|
+
import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
|
|
1619
|
+
import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
|
|
1543
1620
|
function buildQuestioningPrompt(session, min, max) {
|
|
1544
1621
|
const answered = session.answers.length;
|
|
1545
1622
|
const remaining = Math.max(0, min - answered);
|
|
@@ -1588,7 +1665,7 @@ function buildQuestioningPrompt(session, min, max) {
|
|
|
1588
1665
|
if (answered > 0) {
|
|
1589
1666
|
lines.push("", "**Conversation so far:**");
|
|
1590
1667
|
for (let i = 0; i < answered; i++) {
|
|
1591
|
-
const a =
|
|
1668
|
+
const a = expectDefined2(session.answers[i]);
|
|
1592
1669
|
lines.push(``, `Q${i + 1}: ${a.question}`, `A${i + 1}: ${a.answer}`);
|
|
1593
1670
|
}
|
|
1594
1671
|
}
|
|
@@ -1791,17 +1868,9 @@ var AISpecBuilder = class {
|
|
|
1791
1868
|
} catch {
|
|
1792
1869
|
}
|
|
1793
1870
|
}
|
|
1794
|
-
/** Auto-save helper
|
|
1795
|
-
* Failures are surfaced via process.emitWarning so a persistent
|
|
1796
|
-
* ENOSPC / EACCES doesn't silently strand session edits in memory. */
|
|
1871
|
+
/** Auto-save helper. saveSession() already handles best-effort persistence. */
|
|
1797
1872
|
autoSave() {
|
|
1798
|
-
this.saveSession()
|
|
1799
|
-
const detail = toErrorMessage(err);
|
|
1800
|
-
process.emitWarning(
|
|
1801
|
-
`SpecBuilder autoSave failed: ${detail}`,
|
|
1802
|
-
"SpecBuilderWarning"
|
|
1803
|
-
);
|
|
1804
|
-
});
|
|
1873
|
+
void this.saveSession();
|
|
1805
1874
|
}
|
|
1806
1875
|
// ── Session Lifecycle ─────────────────────────────────────────────────────
|
|
1807
1876
|
/** Start a new session with a title and optional intent. */
|
|
@@ -1975,7 +2044,7 @@ var AISpecBuilder = class {
|
|
|
1975
2044
|
message: "Invalid JSON for spec",
|
|
1976
2045
|
code: ERROR_CODES2.SDD_PARSE_FAILED,
|
|
1977
2046
|
cause: e,
|
|
1978
|
-
context: { detail: e
|
|
2047
|
+
context: { detail: toErrorMessage(e) }
|
|
1979
2048
|
});
|
|
1980
2049
|
}
|
|
1981
2050
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -1987,7 +2056,7 @@ var AISpecBuilder = class {
|
|
|
1987
2056
|
}
|
|
1988
2057
|
const raw = parsed;
|
|
1989
2058
|
const now = Date.now();
|
|
1990
|
-
const title = String(raw.title ?? this.session.title
|
|
2059
|
+
const title = String(raw.title ?? this.session.title);
|
|
1991
2060
|
const overview = String(raw.overview ?? "");
|
|
1992
2061
|
if (!overview || overview === "undefined") {
|
|
1993
2062
|
throw new SddError2({
|
|
@@ -1998,7 +2067,15 @@ var AISpecBuilder = class {
|
|
|
1998
2067
|
}
|
|
1999
2068
|
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
2000
2069
|
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
2001
|
-
type: [
|
|
2070
|
+
type: [
|
|
2071
|
+
"overview",
|
|
2072
|
+
"requirements",
|
|
2073
|
+
"architecture",
|
|
2074
|
+
"api",
|
|
2075
|
+
"data",
|
|
2076
|
+
"security",
|
|
2077
|
+
"acceptance"
|
|
2078
|
+
].includes(String(s.type)) ? String(s.type) : "overview",
|
|
2002
2079
|
title: String(s.title ?? ""),
|
|
2003
2080
|
content: String(s.content ?? ""),
|
|
2004
2081
|
level: Number(s.level) || 1
|
|
@@ -2006,7 +2083,9 @@ var AISpecBuilder = class {
|
|
|
2006
2083
|
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
2007
2084
|
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
2008
2085
|
id: String(r.id ?? `REQ-${i + 1}`),
|
|
2009
|
-
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2086
|
+
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2087
|
+
String(r.type)
|
|
2088
|
+
) ? String(r.type) : "functional",
|
|
2010
2089
|
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
2011
2090
|
description: String(r.description ?? ""),
|
|
2012
2091
|
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
@@ -2095,7 +2174,6 @@ var AISpecBuilder = class {
|
|
|
2095
2174
|
};
|
|
2096
2175
|
|
|
2097
2176
|
// src/sdd-interview-driver.ts
|
|
2098
|
-
import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
|
|
2099
2177
|
var SddInterviewDriver = class {
|
|
2100
2178
|
builder;
|
|
2101
2179
|
o;
|
|
@@ -2308,16 +2386,17 @@ var SddInterviewDriver = class {
|
|
|
2308
2386
|
);
|
|
2309
2387
|
if (valid.length === 0) return void 0;
|
|
2310
2388
|
const spec = this.builder.getSession().spec;
|
|
2311
|
-
if (!
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
this.
|
|
2315
|
-
this.tracker = tracker;
|
|
2389
|
+
if (!this.tracker) {
|
|
2390
|
+
const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
|
|
2391
|
+
this.graph = await tracker2.createGraph(spec.id, spec.title);
|
|
2392
|
+
this.tracker = tracker2;
|
|
2316
2393
|
}
|
|
2394
|
+
const tracker = this.tracker;
|
|
2395
|
+
const graph = this.graph;
|
|
2317
2396
|
const refMap = /* @__PURE__ */ new Map();
|
|
2318
2397
|
const created = [];
|
|
2319
2398
|
valid.forEach((task, i) => {
|
|
2320
|
-
const node = addTaskToTracker(
|
|
2399
|
+
const node = addTaskToTracker(tracker, task);
|
|
2321
2400
|
created.push({ nodeId: node.id, task });
|
|
2322
2401
|
if (typeof task.id === "string" && task.id.trim()) {
|
|
2323
2402
|
refMap.set(task.id.trim().toLowerCase(), node.id);
|
|
@@ -2330,13 +2409,13 @@ var SddInterviewDriver = class {
|
|
|
2330
2409
|
const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
|
|
2331
2410
|
for (const ref of deps) {
|
|
2332
2411
|
const depId = refMap.get(normalizeTaskRef(String(ref)));
|
|
2333
|
-
if (depId && depId !== nodeId)
|
|
2412
|
+
if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
|
|
2334
2413
|
}
|
|
2335
2414
|
}
|
|
2336
|
-
await this.persistGraph(
|
|
2337
|
-
this.builder.setTaskGraphId(
|
|
2415
|
+
await this.persistGraph(graph);
|
|
2416
|
+
this.builder.setTaskGraphId(graph.id);
|
|
2338
2417
|
await this.builder.saveSession();
|
|
2339
|
-
return
|
|
2418
|
+
return graph.id;
|
|
2340
2419
|
}
|
|
2341
2420
|
};
|
|
2342
2421
|
var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
|
|
@@ -2364,10 +2443,48 @@ function isExplanatoryText(text) {
|
|
|
2364
2443
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
2365
2444
|
|
|
2366
2445
|
// src/sdd-parallel-run.ts
|
|
2367
|
-
import { expectDefined as
|
|
2446
|
+
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
2368
2447
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2369
|
-
import {
|
|
2370
|
-
|
|
2448
|
+
import {
|
|
2449
|
+
assignNickname,
|
|
2450
|
+
DefaultMultiAgentCoordinator,
|
|
2451
|
+
makeAgentSubagentRunner,
|
|
2452
|
+
withDisabledToolFiltering
|
|
2453
|
+
} from "@wrongstack/core/coordination";
|
|
2454
|
+
import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
|
|
2455
|
+
|
|
2456
|
+
// src/graph-split.ts
|
|
2457
|
+
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
2458
|
+
const node = tracker.getNode(taskId);
|
|
2459
|
+
if (!node) return [];
|
|
2460
|
+
if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
|
|
2461
|
+
if (!subtasks.length) return [];
|
|
2462
|
+
const blockers = tracker.getBlockers(taskId);
|
|
2463
|
+
const dependents = tracker.getDependents(taskId);
|
|
2464
|
+
const leafIds = subtasks.map((s) => {
|
|
2465
|
+
const criterion = s.successCriterion?.trim();
|
|
2466
|
+
const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
|
|
2467
|
+
const description = criterion && !verificationCommand ? `${s.description}
|
|
2468
|
+
|
|
2469
|
+
**Acceptance Criteria:**
|
|
2470
|
+
- ${criterion}` : s.description;
|
|
2471
|
+
return tracker.addNode({
|
|
2472
|
+
title: s.title,
|
|
2473
|
+
description,
|
|
2474
|
+
type: s.type ?? node.type,
|
|
2475
|
+
priority: s.priority ?? node.priority,
|
|
2476
|
+
status: "pending",
|
|
2477
|
+
parentId: taskId,
|
|
2478
|
+
...verificationCommand ? { metadata: { verificationCommand } } : {}
|
|
2479
|
+
}).id;
|
|
2480
|
+
});
|
|
2481
|
+
for (const leaf of leafIds) {
|
|
2482
|
+
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
2483
|
+
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
2484
|
+
}
|
|
2485
|
+
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2486
|
+
return leafIds;
|
|
2487
|
+
}
|
|
2371
2488
|
|
|
2372
2489
|
// src/sdd-task-decomposer.ts
|
|
2373
2490
|
var SddTaskDecomposer = class {
|
|
@@ -2491,7 +2608,9 @@ var SddParallelRun = class {
|
|
|
2491
2608
|
this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
|
|
2492
2609
|
this.maxWallClockMs = opts.maxWallClockMs;
|
|
2493
2610
|
this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
|
|
2494
|
-
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2611
|
+
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2612
|
+
parallelSlots: this.slots
|
|
2613
|
+
});
|
|
2495
2614
|
}
|
|
2496
2615
|
opts;
|
|
2497
2616
|
slots;
|
|
@@ -2614,7 +2733,8 @@ var SddParallelRun = class {
|
|
|
2614
2733
|
* revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
|
|
2615
2734
|
*/
|
|
2616
2735
|
async rollback() {
|
|
2617
|
-
if (this.isRunning())
|
|
2736
|
+
if (this.isRunning())
|
|
2737
|
+
return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
|
|
2618
2738
|
const wt = this.opts.worktrees;
|
|
2619
2739
|
if (!wt || !this.baseBranch) {
|
|
2620
2740
|
return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
|
|
@@ -2647,7 +2767,10 @@ var SddParallelRun = class {
|
|
|
2647
2767
|
*/
|
|
2648
2768
|
setTaskModel(taskId, model, provider) {
|
|
2649
2769
|
if (!this.opts.tracker.getNode(taskId)) return false;
|
|
2650
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
2770
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
2771
|
+
model,
|
|
2772
|
+
...provider !== void 0 ? { provider } : {}
|
|
2773
|
+
});
|
|
2651
2774
|
return true;
|
|
2652
2775
|
}
|
|
2653
2776
|
/** Set/override a task's fallback model chain (applied on its next dispatch). */
|
|
@@ -2679,7 +2802,12 @@ var SddParallelRun = class {
|
|
|
2679
2802
|
this.cancelledTasks.add(taskId);
|
|
2680
2803
|
this.opts.tracker.patchMetadata(taskId, { cancelled: true });
|
|
2681
2804
|
this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
|
|
2682
|
-
this.emit("sdd.task.failed", {
|
|
2805
|
+
this.emit("sdd.task.failed", {
|
|
2806
|
+
runId: this.runId,
|
|
2807
|
+
taskId,
|
|
2808
|
+
subagentId: "",
|
|
2809
|
+
error: "cancelled by user"
|
|
2810
|
+
});
|
|
2683
2811
|
const subagentId = this.taskSubagents.get(taskId);
|
|
2684
2812
|
if (subagentId && this.coordinator) {
|
|
2685
2813
|
await this.coordinator.stop(subagentId).catch(() => {
|
|
@@ -2710,30 +2838,12 @@ var SddParallelRun = class {
|
|
|
2710
2838
|
* The scheduler picks the new pending leaves up on its next dispatch pass.
|
|
2711
2839
|
*/
|
|
2712
2840
|
splitTask(taskId, subtasks) {
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
if (
|
|
2717
|
-
if (!subtasks.length) return [];
|
|
2718
|
-
const blockers = tracker.getBlockers(taskId);
|
|
2719
|
-
const dependents = tracker.getDependents(taskId);
|
|
2720
|
-
const leafIds = subtasks.map(
|
|
2721
|
-
(s) => tracker.addNode({
|
|
2722
|
-
title: s.title,
|
|
2723
|
-
description: s.description,
|
|
2724
|
-
type: s.type ?? node.type,
|
|
2725
|
-
priority: s.priority ?? node.priority,
|
|
2726
|
-
status: "pending",
|
|
2727
|
-
parentId: taskId
|
|
2728
|
-
}).id
|
|
2729
|
-
);
|
|
2730
|
-
for (const leaf of leafIds) {
|
|
2731
|
-
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
2732
|
-
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
2733
|
-
}
|
|
2841
|
+
const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
|
|
2842
|
+
isRunning: (id) => this.taskSubagents.has(id)
|
|
2843
|
+
});
|
|
2844
|
+
if (!leafIds.length) return [];
|
|
2734
2845
|
this.retryMap.delete(taskId);
|
|
2735
2846
|
this.persistRetries(taskId, 0);
|
|
2736
|
-
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2737
2847
|
this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
|
|
2738
2848
|
return leafIds;
|
|
2739
2849
|
}
|
|
@@ -2781,7 +2891,12 @@ var SddParallelRun = class {
|
|
|
2781
2891
|
return await this.executeOne(task);
|
|
2782
2892
|
} catch (err) {
|
|
2783
2893
|
this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
|
|
2784
|
-
this.emit("sdd.task.failed", {
|
|
2894
|
+
this.emit("sdd.task.failed", {
|
|
2895
|
+
runId: this.runId,
|
|
2896
|
+
taskId: task.id,
|
|
2897
|
+
subagentId: "",
|
|
2898
|
+
error: String(err)
|
|
2899
|
+
});
|
|
2785
2900
|
return { taskId: task.id, success: false };
|
|
2786
2901
|
} finally {
|
|
2787
2902
|
running.delete(task.id);
|
|
@@ -2795,16 +2910,18 @@ var SddParallelRun = class {
|
|
|
2795
2910
|
await this.waitWhilePaused();
|
|
2796
2911
|
if (this.stopRequested) break;
|
|
2797
2912
|
let dispatchedThisRound = 0;
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
dispatchedThisRound++;
|
|
2804
|
-
}
|
|
2913
|
+
const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
|
|
2914
|
+
for (const task of ready) {
|
|
2915
|
+
if (running.size >= this.slots) break;
|
|
2916
|
+
dispatch(task);
|
|
2917
|
+
dispatchedThisRound++;
|
|
2805
2918
|
}
|
|
2806
2919
|
if (dispatchedThisRound > 0) {
|
|
2807
|
-
this.emit("sdd.wave", {
|
|
2920
|
+
this.emit("sdd.wave", {
|
|
2921
|
+
runId: this.runId,
|
|
2922
|
+
wave: this.round,
|
|
2923
|
+
batchSize: dispatchedThisRound
|
|
2924
|
+
});
|
|
2808
2925
|
this.round++;
|
|
2809
2926
|
}
|
|
2810
2927
|
if (running.size === 0) {
|
|
@@ -2835,7 +2952,6 @@ var SddParallelRun = class {
|
|
|
2835
2952
|
this.opts.onProgress?.(this.buildProgress());
|
|
2836
2953
|
}
|
|
2837
2954
|
}
|
|
2838
|
-
if (running.size > 0) await Promise.allSettled(running.values());
|
|
2839
2955
|
if (this.stopRequested) await this.teardown();
|
|
2840
2956
|
const finalProgress = this.opts.tracker.getProgress();
|
|
2841
2957
|
this.emit("sdd.run.finished", {
|
|
@@ -3033,10 +3149,11 @@ var SddParallelRun = class {
|
|
|
3033
3149
|
}
|
|
3034
3150
|
this.opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
3035
3151
|
await this.allocateWorktrees([task]);
|
|
3036
|
-
if (!this.coordinator)
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3152
|
+
if (!this.coordinator)
|
|
3153
|
+
throw new SddError3({
|
|
3154
|
+
message: "SDD parallel runner requires a coordinator",
|
|
3155
|
+
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3156
|
+
});
|
|
3040
3157
|
const coordinator = this.coordinator;
|
|
3041
3158
|
const subagentId = `sdd-d${this.dispatchSeq++}`;
|
|
3042
3159
|
const correlationId = randomUUID2();
|
|
@@ -3046,7 +3163,7 @@ var SddParallelRun = class {
|
|
|
3046
3163
|
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
|
|
3047
3164
|
const spawnResult = await coordinator.spawn({
|
|
3048
3165
|
id: subagentId,
|
|
3049
|
-
name: agentName
|
|
3166
|
+
name: agentName,
|
|
3050
3167
|
role: "executor",
|
|
3051
3168
|
// Idle reaper is always on; the hard wall-clock cap only when opted in.
|
|
3052
3169
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
@@ -3068,7 +3185,7 @@ var SddParallelRun = class {
|
|
|
3068
3185
|
runId: this.runId,
|
|
3069
3186
|
taskId,
|
|
3070
3187
|
subagentId,
|
|
3071
|
-
agentName
|
|
3188
|
+
agentName,
|
|
3072
3189
|
worktreeBranch: this.taskBranches.get(taskId)
|
|
3073
3190
|
});
|
|
3074
3191
|
const directivePreamble = [
|
|
@@ -3103,7 +3220,7 @@ var SddParallelRun = class {
|
|
|
3103
3220
|
let result;
|
|
3104
3221
|
try {
|
|
3105
3222
|
const got = await coordinator.awaitTasks([correlationId]);
|
|
3106
|
-
result =
|
|
3223
|
+
result = expectDefined3(got[0]);
|
|
3107
3224
|
} catch (err) {
|
|
3108
3225
|
result = {
|
|
3109
3226
|
subagentId,
|
|
@@ -3131,12 +3248,22 @@ var SddParallelRun = class {
|
|
|
3131
3248
|
} catch (err) {
|
|
3132
3249
|
verificationFailReason = `verification error: ${String(err)}`;
|
|
3133
3250
|
}
|
|
3251
|
+
const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
|
|
3134
3252
|
if (verificationFailReason) {
|
|
3253
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
3254
|
+
verificationState: "failed",
|
|
3255
|
+
verificationDetail: verificationFailReason
|
|
3256
|
+
});
|
|
3135
3257
|
this.emit("sdd.task.verification_failed", {
|
|
3136
3258
|
runId: this.runId,
|
|
3137
3259
|
taskId,
|
|
3138
3260
|
reason: verificationFailReason
|
|
3139
3261
|
});
|
|
3262
|
+
} else if (hadVerifiable) {
|
|
3263
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
3264
|
+
verificationState: "passed",
|
|
3265
|
+
verificationDetail: void 0
|
|
3266
|
+
});
|
|
3140
3267
|
}
|
|
3141
3268
|
}
|
|
3142
3269
|
let success = false;
|
|
@@ -3161,12 +3288,13 @@ var SddParallelRun = class {
|
|
|
3161
3288
|
});
|
|
3162
3289
|
await this.applyTaskFailure(taskId, subagentId, merged.reason);
|
|
3163
3290
|
} else {
|
|
3291
|
+
const conflictFiles = merged.conflictFiles ?? [];
|
|
3164
3292
|
this.emit("sdd.task.conflict", {
|
|
3165
3293
|
runId: this.runId,
|
|
3166
3294
|
taskId,
|
|
3167
|
-
conflictFiles
|
|
3295
|
+
conflictFiles
|
|
3168
3296
|
});
|
|
3169
|
-
const reason = `merge conflict${
|
|
3297
|
+
const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
|
|
3170
3298
|
await this.applyTaskFailure(taskId, subagentId, reason);
|
|
3171
3299
|
}
|
|
3172
3300
|
} else {
|
|
@@ -3269,7 +3397,11 @@ var SddParallelRun = class {
|
|
|
3269
3397
|
const res = await wt.merge(handle, {
|
|
3270
3398
|
squash: true,
|
|
3271
3399
|
...this.opts.conflictResolver ? {
|
|
3272
|
-
resolve: (info) => this.opts.conflictResolver({
|
|
3400
|
+
resolve: (info) => this.opts.conflictResolver({
|
|
3401
|
+
task,
|
|
3402
|
+
conflictFiles: info.conflictFiles,
|
|
3403
|
+
cwd: info.cwd
|
|
3404
|
+
})
|
|
3273
3405
|
} : {}
|
|
3274
3406
|
});
|
|
3275
3407
|
if (res.ok) {
|
|
@@ -3281,7 +3413,8 @@ var SddParallelRun = class {
|
|
|
3281
3413
|
result: result ?? {},
|
|
3282
3414
|
cwd: this.opts.projectRoot
|
|
3283
3415
|
});
|
|
3284
|
-
if (!verdict.ok)
|
|
3416
|
+
if (!verdict.ok)
|
|
3417
|
+
regressed = verdict.reason ?? "verification failed after conflict resolution";
|
|
3285
3418
|
} catch (err) {
|
|
3286
3419
|
regressed = `verification error after conflict resolution: ${String(err)}`;
|
|
3287
3420
|
}
|
|
@@ -3397,6 +3530,32 @@ var SddParallelRun = class {
|
|
|
3397
3530
|
};
|
|
3398
3531
|
|
|
3399
3532
|
// src/start-sdd-run.ts
|
|
3533
|
+
function applySddControlCommand(run, command) {
|
|
3534
|
+
const payload = command.payload ?? {};
|
|
3535
|
+
if (command.type === "pause") run.pause();
|
|
3536
|
+
else if (command.type === "resume") run.resume();
|
|
3537
|
+
else if (command.type === "stop") run.stop();
|
|
3538
|
+
else if (command.type === "retry" && payload.taskId) run.retryTask(payload.taskId);
|
|
3539
|
+
else if (command.type === "retry_all_failed") run.retryAllFailed();
|
|
3540
|
+
else if (command.type === "reassign" && payload.taskId)
|
|
3541
|
+
run.reassignTask(payload.taskId, payload.agentName ?? "");
|
|
3542
|
+
else if (command.type === "set_task_model" && payload.taskId)
|
|
3543
|
+
run.setTaskModel(payload.taskId, payload.model, payload.provider);
|
|
3544
|
+
else if (command.type === "set_task_fallbacks" && payload.taskId)
|
|
3545
|
+
run.setTaskFallbacks(payload.taskId, payload.fallbackModels);
|
|
3546
|
+
else if (command.type === "set_task_verification" && payload.taskId)
|
|
3547
|
+
run.setTaskVerification(payload.taskId, payload.verificationCommand);
|
|
3548
|
+
else if (command.type === "cancel_task" && payload.taskId)
|
|
3549
|
+
void run.cancelTask(payload.taskId).catch(() => {
|
|
3550
|
+
});
|
|
3551
|
+
else if (command.type === "delete_task" && payload.taskId) run.deleteTask(payload.taskId);
|
|
3552
|
+
else if (command.type === "split_task" && payload.taskId && payload.subtasks?.length)
|
|
3553
|
+
run.splitTask(payload.taskId, payload.subtasks);
|
|
3554
|
+
else if (command.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
|
|
3555
|
+
});
|
|
3556
|
+
else if (command.type === "rollback") void run.rollback().catch(() => {
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3400
3559
|
function startSddRun(opts) {
|
|
3401
3560
|
SddParallelRun.resetOrphans(opts.tracker);
|
|
3402
3561
|
const run = new SddParallelRun({
|
|
@@ -3460,25 +3619,7 @@ function startSddRun(opts) {
|
|
|
3460
3619
|
const controlTimer = setInterval(() => {
|
|
3461
3620
|
void opts.boardStore.drainControl(run.runId).then((cmds) => {
|
|
3462
3621
|
for (const c of cmds) {
|
|
3463
|
-
|
|
3464
|
-
if (c.type === "pause") run.pause();
|
|
3465
|
-
else if (c.type === "resume") run.resume();
|
|
3466
|
-
else if (c.type === "stop") run.stop();
|
|
3467
|
-
else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
|
|
3468
|
-
else if (c.type === "retry_all_failed") run.retryAllFailed();
|
|
3469
|
-
else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
|
|
3470
|
-
else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
|
|
3471
|
-
else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
|
|
3472
|
-
else if (c.type === "set_task_verification" && p.taskId)
|
|
3473
|
-
run.setTaskVerification(p.taskId, p.verificationCommand);
|
|
3474
|
-
else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId).catch(() => {
|
|
3475
|
-
});
|
|
3476
|
-
else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
|
|
3477
|
-
else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
|
|
3478
|
-
else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
|
|
3479
|
-
});
|
|
3480
|
-
else if (c.type === "rollback") void run.rollback().catch(() => {
|
|
3481
|
-
});
|
|
3622
|
+
applySddControlCommand(run, c);
|
|
3482
3623
|
}
|
|
3483
3624
|
}).catch(() => {
|
|
3484
3625
|
});
|
|
@@ -3506,6 +3647,7 @@ function startSddRun(opts) {
|
|
|
3506
3647
|
|
|
3507
3648
|
// src/sdd-lifecycle.ts
|
|
3508
3649
|
import * as fsp4 from "node:fs/promises";
|
|
3650
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
3509
3651
|
import { WorktreeManager } from "@wrongstack/core/worktree";
|
|
3510
3652
|
async function cleanupSddWorktrees(projectRoot) {
|
|
3511
3653
|
const wt = new WorktreeManager({ projectRoot });
|
|
@@ -3517,19 +3659,21 @@ async function cleanupStaleWorktrees(projectRoot) {
|
|
|
3517
3659
|
}
|
|
3518
3660
|
async function cleanupStaleSddWorktrees(opts) {
|
|
3519
3661
|
const now = opts.now?.() ?? Date.now();
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
}
|
|
3662
|
+
const store = new SddBoardStore({ baseDir: opts.boardsDir });
|
|
3663
|
+
const latest = (await store.list())[0];
|
|
3664
|
+
if (latest) {
|
|
3665
|
+
const age = now - latest.updatedAt;
|
|
3666
|
+
if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
|
|
3667
|
+
return {
|
|
3668
|
+
swept: false,
|
|
3669
|
+
removed: 0,
|
|
3670
|
+
detected: 0,
|
|
3671
|
+
skippedReason: "a run appears live (running)"
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
|
|
3675
|
+
return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
|
|
3531
3676
|
}
|
|
3532
|
-
} catch {
|
|
3533
3677
|
}
|
|
3534
3678
|
try {
|
|
3535
3679
|
const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
|
|
@@ -3546,7 +3690,11 @@ async function rollbackSddRunFromDisk(opts) {
|
|
|
3546
3690
|
const snap = await store.load(runId);
|
|
3547
3691
|
if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
|
|
3548
3692
|
if (!snap.baseBranch) {
|
|
3549
|
-
return {
|
|
3693
|
+
return {
|
|
3694
|
+
ok: false,
|
|
3695
|
+
reverted: 0,
|
|
3696
|
+
reason: "this run did not record a base branch (no worktree run)"
|
|
3697
|
+
};
|
|
3550
3698
|
}
|
|
3551
3699
|
const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
|
|
3552
3700
|
if (shas.length === 0) {
|
|
@@ -3564,7 +3712,7 @@ async function destroySddProject(opts) {
|
|
|
3564
3712
|
projectRoot: opts.projectRoot,
|
|
3565
3713
|
boardsDir: opts.paths.projectSddBoards,
|
|
3566
3714
|
runId: opts.runId
|
|
3567
|
-
}).catch((err) => ({ ok: false, reverted: 0, reason:
|
|
3715
|
+
}).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
|
|
3568
3716
|
reverted = r.reverted;
|
|
3569
3717
|
revertOk = r.ok;
|
|
3570
3718
|
revertReason = r.reason;
|
|
@@ -3591,9 +3739,6 @@ async function destroySddProject(opts) {
|
|
|
3591
3739
|
await rmDir(opts.paths.projectSddBoards, "boards");
|
|
3592
3740
|
return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
|
|
3593
3741
|
}
|
|
3594
|
-
function toReason(err) {
|
|
3595
|
-
return err instanceof Error ? err.message : String(err);
|
|
3596
|
-
}
|
|
3597
3742
|
async function applySddLifecycle(op, opts) {
|
|
3598
3743
|
try {
|
|
3599
3744
|
if (op === "cleanup_worktrees") {
|
|
@@ -3625,7 +3770,7 @@ async function applySddLifecycle(op, opts) {
|
|
|
3625
3770
|
reason: r.revertOk === false ? r.revertReason : void 0
|
|
3626
3771
|
};
|
|
3627
3772
|
} catch (err) {
|
|
3628
|
-
return { op, ok: false, reason:
|
|
3773
|
+
return { op, ok: false, reason: toErrorMessage2(err) };
|
|
3629
3774
|
}
|
|
3630
3775
|
}
|
|
3631
3776
|
|
|
@@ -3747,7 +3892,7 @@ function templateToMarkdown(template, title) {
|
|
|
3747
3892
|
|
|
3748
3893
|
// src/task-visualizer.ts
|
|
3749
3894
|
import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
|
|
3750
|
-
import {
|
|
3895
|
+
import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
|
|
3751
3896
|
var STATUS_ICON = {
|
|
3752
3897
|
pending: "\u25CB",
|
|
3753
3898
|
in_progress: "\u25D0",
|
|
@@ -3774,7 +3919,9 @@ function renderTaskGraph(graph, opts) {
|
|
|
3774
3919
|
const lines = [];
|
|
3775
3920
|
const compact = opts?.compact ?? false;
|
|
3776
3921
|
lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
|
|
3777
|
-
lines.push(
|
|
3922
|
+
lines.push(
|
|
3923
|
+
`\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`
|
|
3924
|
+
);
|
|
3778
3925
|
lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
|
|
3779
3926
|
lines.push("");
|
|
3780
3927
|
const progress = computeTaskProgress2(graph);
|
|
@@ -3809,19 +3956,18 @@ function renderTaskGraph(graph, opts) {
|
|
|
3809
3956
|
function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
|
|
3810
3957
|
if (rendered.has(nodeId)) return;
|
|
3811
3958
|
rendered.add(nodeId);
|
|
3812
|
-
const node = graph.nodes.get(nodeId);
|
|
3813
|
-
if (!node) return;
|
|
3959
|
+
const node = expectDefined4(graph.nodes.get(nodeId));
|
|
3814
3960
|
const icon = STATUS_ICON[node.status];
|
|
3815
3961
|
const prioIcon = PRIORITY_ICON[node.priority];
|
|
3816
3962
|
const typeIcon = TYPE_ICON[node.type];
|
|
3817
|
-
const title = compact ?
|
|
3963
|
+
const title = compact ? truncate(node.title, 40) : node.title;
|
|
3818
3964
|
const blockedBy = childrenMap.get(nodeId) ?? [];
|
|
3819
3965
|
const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
|
|
3820
3966
|
lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
|
|
3821
3967
|
if (!compact && node.description) {
|
|
3822
3968
|
const descLines = node.description.split("\n").slice(0, 3);
|
|
3823
3969
|
for (const dl of descLines) {
|
|
3824
|
-
lines.push(`${prefix} \u2514 ${
|
|
3970
|
+
lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
|
|
3825
3971
|
}
|
|
3826
3972
|
}
|
|
3827
3973
|
const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
|
|
@@ -3851,7 +3997,7 @@ function renderTaskList(graph) {
|
|
|
3851
3997
|
completed: []
|
|
3852
3998
|
};
|
|
3853
3999
|
for (const node of nodes) {
|
|
3854
|
-
groups[node.status]
|
|
4000
|
+
groups[node.status].push(node);
|
|
3855
4001
|
}
|
|
3856
4002
|
for (const [status, group] of Object.entries(groups)) {
|
|
3857
4003
|
if (group.length === 0) continue;
|
|
@@ -3899,8 +4045,8 @@ function renderSpecAnalysis(spec, analysis) {
|
|
|
3899
4045
|
}
|
|
3900
4046
|
|
|
3901
4047
|
// src/critical-path.ts
|
|
3902
|
-
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
3903
4048
|
import { topologicalSort } from "@wrongstack/core/tasking";
|
|
4049
|
+
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
3904
4050
|
function analyzeCriticalPath(graph) {
|
|
3905
4051
|
const nodes = Array.from(graph.nodes.values());
|
|
3906
4052
|
const topoOrder = topologicalSort(graph);
|
|
@@ -3954,8 +4100,7 @@ function analyzeCriticalPath(graph) {
|
|
|
3954
4100
|
bottlenecks.sort((a, b) => b.severity - a.severity);
|
|
3955
4101
|
const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
|
|
3956
4102
|
const totalHours = criticalPath.reduce((sum, id) => {
|
|
3957
|
-
|
|
3958
|
-
return sum + (n?.estimateHours ?? 0);
|
|
4103
|
+
return sum + (graph.nodes.get(id).estimateHours ?? 0);
|
|
3959
4104
|
}, 0);
|
|
3960
4105
|
const parallelGroups = computeParallelGroups(graph, blockedByMap);
|
|
3961
4106
|
const executionOrder = topoOrder.filter((id) => {
|
|
@@ -3976,7 +4121,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
|
|
|
3976
4121
|
const visited = /* @__PURE__ */ new Set();
|
|
3977
4122
|
const queue = [taskId];
|
|
3978
4123
|
while (queue.length > 0) {
|
|
3979
|
-
const current =
|
|
4124
|
+
const current = expectDefined5(queue.shift());
|
|
3980
4125
|
const blocked = blocksMap.get(current);
|
|
3981
4126
|
if (!blocked) continue;
|
|
3982
4127
|
for (const id of blocked) {
|
|
@@ -4011,7 +4156,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
4011
4156
|
const blocked = blocksMap.get(id);
|
|
4012
4157
|
if (!blocked) continue;
|
|
4013
4158
|
for (const blockedId of blocked) {
|
|
4014
|
-
const candidateDist =
|
|
4159
|
+
const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
|
|
4015
4160
|
if (candidateDist > (dist.get(blockedId) ?? 0)) {
|
|
4016
4161
|
dist.set(blockedId, candidateDist);
|
|
4017
4162
|
prev.set(blockedId, id);
|
|
@@ -4022,9 +4167,9 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
4022
4167
|
if (!changed) break;
|
|
4023
4168
|
}
|
|
4024
4169
|
let maxDist = 0;
|
|
4025
|
-
let maxId =
|
|
4170
|
+
let maxId = expectDefined5(allIds[0]);
|
|
4026
4171
|
for (const id of allIds) {
|
|
4027
|
-
const d = dist.get(id)
|
|
4172
|
+
const d = dist.get(id);
|
|
4028
4173
|
if (d > maxDist) {
|
|
4029
4174
|
maxDist = d;
|
|
4030
4175
|
maxId = id;
|
|
@@ -4059,8 +4204,7 @@ function computeParallelGroups(graph, blockedByMap) {
|
|
|
4059
4204
|
}
|
|
4060
4205
|
}
|
|
4061
4206
|
if (group.length === 0) {
|
|
4062
|
-
|
|
4063
|
-
if (first) group.push(first);
|
|
4207
|
+
group.push(expectDefined5(Array.from(remaining)[0]));
|
|
4064
4208
|
}
|
|
4065
4209
|
for (const id of group) {
|
|
4066
4210
|
assigned.add(id);
|
|
@@ -4274,7 +4418,6 @@ var AutoExecutor = class {
|
|
|
4274
4418
|
for (let i = 0; i < results.length; i++) {
|
|
4275
4419
|
const result = results[i];
|
|
4276
4420
|
const task = batch[i];
|
|
4277
|
-
if (!result || !task) continue;
|
|
4278
4421
|
if (result.status === "fulfilled") {
|
|
4279
4422
|
const { result: execResult, retries } = result.value;
|
|
4280
4423
|
if (execResult.success) {
|
|
@@ -4324,14 +4467,14 @@ var AutoExecutor = class {
|
|
|
4324
4467
|
}
|
|
4325
4468
|
}
|
|
4326
4469
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4327
|
-
ready.sort((a, b) =>
|
|
4470
|
+
ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
4328
4471
|
return ready;
|
|
4329
4472
|
}
|
|
4330
4473
|
/** Execute a single task with retry logic. */
|
|
4331
4474
|
async executeTaskWithRetry(task, graph, spec) {
|
|
4332
4475
|
const maxRetries = this.opts.maxRetries ?? 2;
|
|
4333
4476
|
let retryCount = this.retryMap.get(task.id) ?? 0;
|
|
4334
|
-
while (
|
|
4477
|
+
while (true) {
|
|
4335
4478
|
this.opts.tracker.updateNodeStatus(task.id, "in_progress");
|
|
4336
4479
|
this.opts.onTaskStart?.(task);
|
|
4337
4480
|
const dependencies = this.getTaskDependencies(task.id, graph);
|
|
@@ -4375,7 +4518,6 @@ var AutoExecutor = class {
|
|
|
4375
4518
|
};
|
|
4376
4519
|
}
|
|
4377
4520
|
}
|
|
4378
|
-
return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
|
|
4379
4521
|
}
|
|
4380
4522
|
/** Get tasks that this task depends on. */
|
|
4381
4523
|
getTaskDependencies(taskId, graph) {
|
|
@@ -4462,6 +4604,54 @@ Supervisor rescues already used: ${attempts}`,
|
|
|
4462
4604
|
|
|
4463
4605
|
// src/verify-task.ts
|
|
4464
4606
|
import { spawn } from "node:child_process";
|
|
4607
|
+
function verificationShell(platform) {
|
|
4608
|
+
return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4609
|
+
}
|
|
4610
|
+
function makeCompositeVerifier(parts) {
|
|
4611
|
+
return async function verifyTask(info) {
|
|
4612
|
+
for (const part of parts) {
|
|
4613
|
+
const outcome = await part(info);
|
|
4614
|
+
if (!outcome.ok) return outcome;
|
|
4615
|
+
}
|
|
4616
|
+
return { ok: true };
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
function makeAcceptanceCriteriaVerifier(options) {
|
|
4620
|
+
const maxResultChars = options.maxResultChars ?? 4e3;
|
|
4621
|
+
return async function verifyTask(info) {
|
|
4622
|
+
const description = info.task.description ?? "";
|
|
4623
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4624
|
+
if (marker === -1) return { ok: true };
|
|
4625
|
+
const criteria = description.slice(marker);
|
|
4626
|
+
const resultText = typeof info.result.result === "string" ? info.result.result.slice(0, maxResultChars) : JSON.stringify(info.result.result ?? "").slice(0, maxResultChars);
|
|
4627
|
+
let text;
|
|
4628
|
+
try {
|
|
4629
|
+
text = await options.run(
|
|
4630
|
+
[
|
|
4631
|
+
"You are a strict acceptance reviewer for one completed engineering task.",
|
|
4632
|
+
`Task: ${info.task.title}`,
|
|
4633
|
+
"",
|
|
4634
|
+
criteria,
|
|
4635
|
+
"",
|
|
4636
|
+
"Worker's reported result:",
|
|
4637
|
+
resultText || "(no result text)",
|
|
4638
|
+
"",
|
|
4639
|
+
"Does the reported result plausibly satisfy EVERY acceptance criterion?",
|
|
4640
|
+
'Answer with exactly one line: "VERDICT: PASS" or "VERDICT: FAIL \u2014 <short reason>".'
|
|
4641
|
+
].join("\n")
|
|
4642
|
+
);
|
|
4643
|
+
} catch {
|
|
4644
|
+
return { ok: true };
|
|
4645
|
+
}
|
|
4646
|
+
const match = text.match(/VERDICT:\s*(PASS|FAIL)(?:\s*[—-]\s*(.*))?/i);
|
|
4647
|
+
if (!match) return { ok: true };
|
|
4648
|
+
if (match[1].toUpperCase() === "PASS") return { ok: true };
|
|
4649
|
+
return {
|
|
4650
|
+
ok: false,
|
|
4651
|
+
reason: `acceptance criteria not met: ${match[2]?.trim() || "judge rejected the result"}`
|
|
4652
|
+
};
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4465
4655
|
function makeCommandVerifier(options = {}) {
|
|
4466
4656
|
const metadataKey = options.metadataKey ?? "verificationCommand";
|
|
4467
4657
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
@@ -4469,8 +4659,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4469
4659
|
const cmd = info.task.metadata?.[metadataKey];
|
|
4470
4660
|
if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
|
|
4471
4661
|
return await new Promise((resolve) => {
|
|
4472
|
-
const
|
|
4473
|
-
const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4662
|
+
const [shell, ...shellArgs] = verificationShell(process.platform);
|
|
4474
4663
|
const child = spawn(shell, [...shellArgs, cmd], {
|
|
4475
4664
|
cwd: info.cwd,
|
|
4476
4665
|
shell: false,
|
|
@@ -4492,7 +4681,6 @@ function makeCommandVerifier(options = {}) {
|
|
|
4492
4681
|
});
|
|
4493
4682
|
child.on("error", (err) => {
|
|
4494
4683
|
clearTimeout(timer);
|
|
4495
|
-
if (timedOut) return;
|
|
4496
4684
|
resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
|
|
4497
4685
|
});
|
|
4498
4686
|
});
|
|
@@ -4500,10 +4688,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4500
4688
|
}
|
|
4501
4689
|
|
|
4502
4690
|
// src/decompose-task.ts
|
|
4503
|
-
import {
|
|
4504
|
-
readBundledInstructionText,
|
|
4505
|
-
renderInstructionTemplate
|
|
4506
|
-
} from "@wrongstack/core/utils";
|
|
4691
|
+
import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
|
|
4507
4692
|
var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
|
|
4508
4693
|
var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
4509
4694
|
function extractJsonArray(text) {
|
|
@@ -4527,48 +4712,144 @@ function buildPrompt(task, error, min, max) {
|
|
|
4527
4712
|
error: error || "(none recorded)"
|
|
4528
4713
|
});
|
|
4529
4714
|
}
|
|
4530
|
-
function
|
|
4715
|
+
function parseSubtaskSpecs(text, min, max, options = {}) {
|
|
4716
|
+
const json = extractJsonArray(text ?? "");
|
|
4717
|
+
if (!json) return [];
|
|
4718
|
+
let raw;
|
|
4719
|
+
try {
|
|
4720
|
+
raw = JSON.parse(json);
|
|
4721
|
+
} catch {
|
|
4722
|
+
return [];
|
|
4723
|
+
}
|
|
4724
|
+
const items = raw;
|
|
4725
|
+
const specs = [];
|
|
4726
|
+
for (const item of items) {
|
|
4727
|
+
if (!item || typeof item !== "object") continue;
|
|
4728
|
+
const r = item;
|
|
4729
|
+
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
4730
|
+
const description = typeof r["description"] === "string" ? r["description"].trim() : "";
|
|
4731
|
+
if (!title || !description) continue;
|
|
4732
|
+
const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
|
|
4733
|
+
const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
|
|
4734
|
+
const successCriterion = options.acceptSuccessCriterion && typeof r["successCriterion"] === "string" ? r["successCriterion"].trim() || void 0 : void 0;
|
|
4735
|
+
specs.push({ title, description, type, priority, successCriterion });
|
|
4736
|
+
if (specs.length >= max) break;
|
|
4737
|
+
}
|
|
4738
|
+
return specs.length >= min ? specs : [];
|
|
4739
|
+
}
|
|
4740
|
+
function makePlanningDecomposer(opts) {
|
|
4531
4741
|
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4532
|
-
const max = Math.max(min, opts.maxSubtasks ??
|
|
4533
|
-
return async function
|
|
4742
|
+
const max = Math.max(min, opts.maxSubtasks ?? 5);
|
|
4743
|
+
return async function decompose(info) {
|
|
4534
4744
|
let text;
|
|
4535
4745
|
try {
|
|
4536
|
-
text = await opts.run(
|
|
4746
|
+
text = await opts.run(
|
|
4747
|
+
renderInstructionTemplate(readBundledInstructionText("sdd/decompose-task-planning.md"), {
|
|
4748
|
+
minSubtasks: String(min),
|
|
4749
|
+
maxSubtasks: String(max),
|
|
4750
|
+
title: info.title,
|
|
4751
|
+
description: info.description,
|
|
4752
|
+
reasons: info.reasons.length ? info.reasons.map((r) => `- ${r}`).join("\n") : "- (unspecified)"
|
|
4753
|
+
})
|
|
4754
|
+
);
|
|
4537
4755
|
} catch {
|
|
4538
4756
|
return [];
|
|
4539
4757
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4758
|
+
return parseSubtaskSpecs(text, min, max, { acceptSuccessCriterion: true });
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
function makeLlmSubtaskGenerator(opts) {
|
|
4762
|
+
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4763
|
+
const max = Math.max(min, opts.maxSubtasks ?? 4);
|
|
4764
|
+
return async function generateSubtasks(info) {
|
|
4765
|
+
let text;
|
|
4543
4766
|
try {
|
|
4544
|
-
|
|
4767
|
+
text = await opts.run(buildPrompt(info.task, info.error, min, max));
|
|
4545
4768
|
} catch {
|
|
4546
4769
|
return [];
|
|
4547
4770
|
}
|
|
4548
|
-
|
|
4549
|
-
const specs = [];
|
|
4550
|
-
for (const item of raw) {
|
|
4551
|
-
if (!item || typeof item !== "object") continue;
|
|
4552
|
-
const r = item;
|
|
4553
|
-
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
4554
|
-
const description = typeof r["description"] === "string" ? r["description"].trim() : "";
|
|
4555
|
-
if (!title || !description) continue;
|
|
4556
|
-
const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
|
|
4557
|
-
const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
|
|
4558
|
-
specs.push({ title, description, type, priority });
|
|
4559
|
-
if (specs.length >= max) break;
|
|
4560
|
-
}
|
|
4561
|
-
return specs.length >= min ? specs : [];
|
|
4771
|
+
return parseSubtaskSpecs(text, min, max);
|
|
4562
4772
|
};
|
|
4563
4773
|
}
|
|
4564
4774
|
|
|
4775
|
+
// src/plan-decompose.ts
|
|
4776
|
+
import {
|
|
4777
|
+
assessAtomicity as assessAtomicity2
|
|
4778
|
+
} from "@wrongstack/kanban";
|
|
4779
|
+
function countAcceptanceCriteria(description) {
|
|
4780
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4781
|
+
if (marker === -1) return 0;
|
|
4782
|
+
const tail = description.slice(marker);
|
|
4783
|
+
return (tail.match(/^\s*-\s+\S/gm) ?? []).length;
|
|
4784
|
+
}
|
|
4785
|
+
function assessTaskNodeAtomicity(tracker, node, config) {
|
|
4786
|
+
const criteriaCount = countAcceptanceCriteria(node.description ?? "");
|
|
4787
|
+
const verificationCommand = node.metadata?.["verificationCommand"] ?? extractVerificationCommand([node.description ?? ""]);
|
|
4788
|
+
return assessAtomicity2(
|
|
4789
|
+
{
|
|
4790
|
+
title: node.title,
|
|
4791
|
+
description: node.description,
|
|
4792
|
+
estimatedHours: node.estimateHours,
|
|
4793
|
+
dependencyCount: tracker.getBlockers(node.id).length,
|
|
4794
|
+
successCriteriaCount: criteriaCount,
|
|
4795
|
+
hasVerifiableOutput: Boolean(verificationCommand),
|
|
4796
|
+
childCount: tracker.getAllNodes().filter((n) => n.parentId === node.id).length
|
|
4797
|
+
},
|
|
4798
|
+
config
|
|
4799
|
+
);
|
|
4800
|
+
}
|
|
4801
|
+
async function decomposeNonAtomicTasks(opts) {
|
|
4802
|
+
const maxDecompositions = Math.max(1, opts.maxDecompositions ?? 10);
|
|
4803
|
+
const result = { applied: [], proposals: [], flagged: [] };
|
|
4804
|
+
const nodes = opts.tracker.getAllNodes();
|
|
4805
|
+
const childCounts = /* @__PURE__ */ new Map();
|
|
4806
|
+
for (const node of nodes) {
|
|
4807
|
+
if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
|
|
4808
|
+
}
|
|
4809
|
+
const candidates = nodes.filter(
|
|
4810
|
+
(node) => node.status === "pending" && !childCounts.get(node.id)
|
|
4811
|
+
);
|
|
4812
|
+
let spent = 0;
|
|
4813
|
+
for (const node of candidates) {
|
|
4814
|
+
if (spent >= maxDecompositions) break;
|
|
4815
|
+
const assessment = assessTaskNodeAtomicity(opts.tracker, node, opts.config);
|
|
4816
|
+
opts.tracker.patchMetadata(node.id, {
|
|
4817
|
+
atomicity: {
|
|
4818
|
+
verdict: assessment.verdict,
|
|
4819
|
+
score: assessment.score,
|
|
4820
|
+
reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
|
|
4821
|
+
}
|
|
4822
|
+
});
|
|
4823
|
+
if (assessment.verdict !== "needs_decomposition") continue;
|
|
4824
|
+
result.flagged.push(node.id);
|
|
4825
|
+
spent += 1;
|
|
4826
|
+
const reasons = assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason);
|
|
4827
|
+
const subtasks = await opts.decompose({
|
|
4828
|
+
title: node.title,
|
|
4829
|
+
description: node.description ?? "",
|
|
4830
|
+
reasons
|
|
4831
|
+
});
|
|
4832
|
+
if (!subtasks.length) continue;
|
|
4833
|
+
if (opts.mode === "auto") {
|
|
4834
|
+
const subtaskIds = splitGraphNode(opts.tracker, node.id, subtasks);
|
|
4835
|
+
if (subtaskIds.length) result.applied.push({ nodeId: node.id, subtaskIds });
|
|
4836
|
+
} else {
|
|
4837
|
+
result.proposals.push({ nodeId: node.id, title: node.title, reasons, subtasks });
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
return result;
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4565
4843
|
// src/conflict-resolver.ts
|
|
4566
4844
|
import { readFile as readFile4, writeFile } from "node:fs/promises";
|
|
4567
|
-
import { join as join4
|
|
4568
|
-
import {
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4845
|
+
import { isAbsolute, join as join4 } from "node:path";
|
|
4846
|
+
import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
|
|
4847
|
+
var defaultFileIO = {
|
|
4848
|
+
read: (path4) => readFile4(path4, "utf8"),
|
|
4849
|
+
write: async (path4, content) => {
|
|
4850
|
+
await writeFile(path4, content, "utf8");
|
|
4851
|
+
}
|
|
4852
|
+
};
|
|
4572
4853
|
var START = "<<<<<<<";
|
|
4573
4854
|
var BASE = "|||||||";
|
|
4574
4855
|
var SEP = "=======";
|
|
@@ -4606,21 +4887,21 @@ function hasConflictMarkers(text) {
|
|
|
4606
4887
|
return m === START || m === SEP || m === END || m === BASE;
|
|
4607
4888
|
});
|
|
4608
4889
|
}
|
|
4609
|
-
function makePreferSideConflictResolver(side) {
|
|
4890
|
+
function makePreferSideConflictResolver(side, io = defaultFileIO) {
|
|
4610
4891
|
return async function conflictResolver(info) {
|
|
4611
4892
|
if (info.conflictFiles.length === 0) return false;
|
|
4612
4893
|
for (const rel of info.conflictFiles) {
|
|
4613
4894
|
const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
|
|
4614
4895
|
let content;
|
|
4615
4896
|
try {
|
|
4616
|
-
content = await
|
|
4897
|
+
content = await io.read(abs);
|
|
4617
4898
|
} catch {
|
|
4618
4899
|
return false;
|
|
4619
4900
|
}
|
|
4620
4901
|
const resolved = resolveConflictText(content, side);
|
|
4621
4902
|
if (hasConflictMarkers(resolved)) return false;
|
|
4622
4903
|
try {
|
|
4623
|
-
await
|
|
4904
|
+
await io.write(abs, resolved);
|
|
4624
4905
|
} catch {
|
|
4625
4906
|
return false;
|
|
4626
4907
|
}
|
|
@@ -4640,13 +4921,14 @@ function nonMarkerLineCount(text) {
|
|
|
4640
4921
|
}
|
|
4641
4922
|
function makeLlmConflictResolver(opts) {
|
|
4642
4923
|
const minFraction = opts.minRetainedFraction ?? 0.5;
|
|
4924
|
+
const io = opts.io ?? defaultFileIO;
|
|
4643
4925
|
return async function conflictResolver(info) {
|
|
4644
4926
|
if (info.conflictFiles.length === 0) return false;
|
|
4645
4927
|
for (const rel of info.conflictFiles) {
|
|
4646
4928
|
const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
|
|
4647
4929
|
let content;
|
|
4648
4930
|
try {
|
|
4649
|
-
content = await
|
|
4931
|
+
content = await io.read(abs);
|
|
4650
4932
|
} catch {
|
|
4651
4933
|
return false;
|
|
4652
4934
|
}
|
|
@@ -4670,7 +4952,7 @@ function makeLlmConflictResolver(opts) {
|
|
|
4670
4952
|
return false;
|
|
4671
4953
|
}
|
|
4672
4954
|
try {
|
|
4673
|
-
await
|
|
4955
|
+
await io.write(abs, resolved);
|
|
4674
4956
|
} catch {
|
|
4675
4957
|
return false;
|
|
4676
4958
|
}
|
|
@@ -4700,21 +4982,27 @@ export {
|
|
|
4700
4982
|
TaskTracker3 as TaskTracker,
|
|
4701
4983
|
analyzeCriticalPath,
|
|
4702
4984
|
applySddLifecycle,
|
|
4985
|
+
assessGeneratedTaskAtomicity,
|
|
4986
|
+
assessTaskNodeAtomicity,
|
|
4703
4987
|
buildBoardSnapshot,
|
|
4704
4988
|
buildBoardTasks,
|
|
4705
4989
|
cleanupSddWorktrees,
|
|
4706
4990
|
cleanupStaleSddWorktrees,
|
|
4707
4991
|
cleanupStaleWorktrees,
|
|
4708
4992
|
createAutoExecutor,
|
|
4993
|
+
decomposeNonAtomicTasks,
|
|
4709
4994
|
destroySddProject,
|
|
4710
4995
|
extractVerificationCommand,
|
|
4711
4996
|
getTemplate,
|
|
4712
4997
|
hasConflictMarkers,
|
|
4713
4998
|
isExplanatoryText,
|
|
4714
4999
|
listTemplates,
|
|
5000
|
+
makeAcceptanceCriteriaVerifier,
|
|
4715
5001
|
makeCommandVerifier,
|
|
5002
|
+
makeCompositeVerifier,
|
|
4716
5003
|
makeLlmConflictResolver,
|
|
4717
5004
|
makeLlmSubtaskGenerator,
|
|
5005
|
+
makePlanningDecomposer,
|
|
4718
5006
|
makePreferSideConflictResolver,
|
|
4719
5007
|
renderProgress,
|
|
4720
5008
|
renderSpecAnalysis,
|
|
@@ -4723,6 +5011,7 @@ export {
|
|
|
4723
5011
|
resolveConflictText,
|
|
4724
5012
|
rollbackSddRunFromDisk,
|
|
4725
5013
|
shortIdMap,
|
|
5014
|
+
splitGraphNode,
|
|
4726
5015
|
startSddRun,
|
|
4727
5016
|
templateToMarkdown
|
|
4728
5017
|
};
|