@wrongstack/sdd 0.295.0 → 0.296.2
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 +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +848 -394
- 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/project-context.d.ts +6 -0
- package/dist/project-context.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 +17 -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-types.d.ts +200 -0
- package/dist/sdd-parallel-run-types.d.ts.map +1 -0
- package/dist/sdd-parallel-run.d.ts +22 -192
- package/dist/sdd-parallel-run.d.ts.map +1 -1
- package/dist/sdd-task-execution.d.ts +28 -0
- package/dist/sdd-task-execution.d.ts.map +1 -0
- package/dist/spec-builder.d.ts +21 -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
|
}
|
|
@@ -1759,10 +1836,10 @@ var AISpecBuilder = class {
|
|
|
1759
1836
|
async saveSession() {
|
|
1760
1837
|
if (!this.sessionPath) return;
|
|
1761
1838
|
try {
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
1839
|
+
const fsp6 = await import("node:fs/promises");
|
|
1840
|
+
const path6 = await import("node:path");
|
|
1764
1841
|
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
|
|
1765
|
-
await
|
|
1842
|
+
await fsp6.mkdir(path6.dirname(this.sessionPath), { recursive: true });
|
|
1766
1843
|
await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
1767
1844
|
} catch {
|
|
1768
1845
|
}
|
|
@@ -1771,8 +1848,8 @@ var AISpecBuilder = class {
|
|
|
1771
1848
|
async loadSession() {
|
|
1772
1849
|
if (!this.sessionPath) return false;
|
|
1773
1850
|
try {
|
|
1774
|
-
const
|
|
1775
|
-
const raw = await
|
|
1851
|
+
const fsp6 = await import("node:fs/promises");
|
|
1852
|
+
const raw = await fsp6.readFile(this.sessionPath, "utf8");
|
|
1776
1853
|
const loaded = JSON.parse(raw);
|
|
1777
1854
|
if (loaded?.id && loaded?.phase && loaded?.title) {
|
|
1778
1855
|
this.session = loaded;
|
|
@@ -1786,22 +1863,14 @@ var AISpecBuilder = class {
|
|
|
1786
1863
|
async deleteSession() {
|
|
1787
1864
|
if (!this.sessionPath) return;
|
|
1788
1865
|
try {
|
|
1789
|
-
const
|
|
1790
|
-
await
|
|
1866
|
+
const fsp6 = await import("node:fs/promises");
|
|
1867
|
+
await fsp6.unlink(this.sessionPath);
|
|
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. */
|
|
@@ -1946,6 +2015,43 @@ var AISpecBuilder = class {
|
|
|
1946
2015
|
getTaskGraphId() {
|
|
1947
2016
|
return this.session.taskGraphId;
|
|
1948
2017
|
}
|
|
2018
|
+
/** Persist the last agent utterance so resume can rehydrate the UI + Q/A pairing. */
|
|
2019
|
+
setLastAgentText(text) {
|
|
2020
|
+
this.session.lastAgentText = text;
|
|
2021
|
+
this.session.updatedAt = Date.now();
|
|
2022
|
+
this.autoSave();
|
|
2023
|
+
}
|
|
2024
|
+
getLastAgentText() {
|
|
2025
|
+
return this.session.lastAgentText;
|
|
2026
|
+
}
|
|
2027
|
+
/** Record a run kicked off from this interview (board deep-link after restart). */
|
|
2028
|
+
setLastRunId(runId) {
|
|
2029
|
+
this.session.lastRunId = runId;
|
|
2030
|
+
this.session.updatedAt = Date.now();
|
|
2031
|
+
this.autoSave();
|
|
2032
|
+
}
|
|
2033
|
+
getLastRunId() {
|
|
2034
|
+
return this.session.lastRunId;
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Hard-reset in-memory session fields while keeping the same session id /
|
|
2038
|
+
* store binding. Used when the operator abandons a resumed interview and
|
|
2039
|
+
* starts a brand-new goal (the next save overwrites the session file).
|
|
2040
|
+
*/
|
|
2041
|
+
resetForNewInterview() {
|
|
2042
|
+
this.session.phase = "questioning";
|
|
2043
|
+
this.session.title = "";
|
|
2044
|
+
this.session.userIntent = "";
|
|
2045
|
+
this.session.answers = [];
|
|
2046
|
+
this.session.questionCount = 0;
|
|
2047
|
+
this.session.spec = void 0;
|
|
2048
|
+
this.session.implementation = void 0;
|
|
2049
|
+
this.session.taskGraphId = void 0;
|
|
2050
|
+
this.session.lastAgentText = void 0;
|
|
2051
|
+
this.session.lastRunId = void 0;
|
|
2052
|
+
this.session.approved = false;
|
|
2053
|
+
this.session.updatedAt = Date.now();
|
|
2054
|
+
}
|
|
1949
2055
|
// ── Spec Persistence ──────────────────────────────────────────────────────
|
|
1950
2056
|
/**
|
|
1951
2057
|
* Save the current spec to the store.
|
|
@@ -1975,7 +2081,7 @@ var AISpecBuilder = class {
|
|
|
1975
2081
|
message: "Invalid JSON for spec",
|
|
1976
2082
|
code: ERROR_CODES2.SDD_PARSE_FAILED,
|
|
1977
2083
|
cause: e,
|
|
1978
|
-
context: { detail: e
|
|
2084
|
+
context: { detail: toErrorMessage(e) }
|
|
1979
2085
|
});
|
|
1980
2086
|
}
|
|
1981
2087
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -1987,7 +2093,7 @@ var AISpecBuilder = class {
|
|
|
1987
2093
|
}
|
|
1988
2094
|
const raw = parsed;
|
|
1989
2095
|
const now = Date.now();
|
|
1990
|
-
const title = String(raw.title ?? this.session.title
|
|
2096
|
+
const title = String(raw.title ?? this.session.title);
|
|
1991
2097
|
const overview = String(raw.overview ?? "");
|
|
1992
2098
|
if (!overview || overview === "undefined") {
|
|
1993
2099
|
throw new SddError2({
|
|
@@ -1998,7 +2104,15 @@ var AISpecBuilder = class {
|
|
|
1998
2104
|
}
|
|
1999
2105
|
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
2000
2106
|
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
2001
|
-
type: [
|
|
2107
|
+
type: [
|
|
2108
|
+
"overview",
|
|
2109
|
+
"requirements",
|
|
2110
|
+
"architecture",
|
|
2111
|
+
"api",
|
|
2112
|
+
"data",
|
|
2113
|
+
"security",
|
|
2114
|
+
"acceptance"
|
|
2115
|
+
].includes(String(s.type)) ? String(s.type) : "overview",
|
|
2002
2116
|
title: String(s.title ?? ""),
|
|
2003
2117
|
content: String(s.content ?? ""),
|
|
2004
2118
|
level: Number(s.level) || 1
|
|
@@ -2006,7 +2120,9 @@ var AISpecBuilder = class {
|
|
|
2006
2120
|
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
2007
2121
|
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
2008
2122
|
id: String(r.id ?? `REQ-${i + 1}`),
|
|
2009
|
-
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2123
|
+
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2124
|
+
String(r.type)
|
|
2125
|
+
) ? String(r.type) : "functional",
|
|
2010
2126
|
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
2011
2127
|
description: String(r.description ?? ""),
|
|
2012
2128
|
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
@@ -2095,7 +2211,6 @@ var AISpecBuilder = class {
|
|
|
2095
2211
|
};
|
|
2096
2212
|
|
|
2097
2213
|
// src/sdd-interview-driver.ts
|
|
2098
|
-
import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
|
|
2099
2214
|
var SddInterviewDriver = class {
|
|
2100
2215
|
builder;
|
|
2101
2216
|
o;
|
|
@@ -2103,6 +2218,8 @@ var SddInterviewDriver = class {
|
|
|
2103
2218
|
maxQuestions;
|
|
2104
2219
|
tracker = null;
|
|
2105
2220
|
graph = null;
|
|
2221
|
+
/** Set when {@link loadExisting} successfully rehydrated a session from disk. */
|
|
2222
|
+
resumedFromDisk = false;
|
|
2106
2223
|
constructor(opts) {
|
|
2107
2224
|
this.o = opts;
|
|
2108
2225
|
this.minQuestions = opts.minQuestions ?? 2;
|
|
@@ -2117,9 +2234,11 @@ var SddInterviewDriver = class {
|
|
|
2117
2234
|
}
|
|
2118
2235
|
/** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
|
|
2119
2236
|
start(title, intent) {
|
|
2237
|
+
this.builder.resetForNewInterview();
|
|
2120
2238
|
this.builder.startSession(title, intent);
|
|
2121
2239
|
this.tracker = null;
|
|
2122
2240
|
this.graph = null;
|
|
2241
|
+
this.resumedFromDisk = false;
|
|
2123
2242
|
return this.builder.getAIPrompt();
|
|
2124
2243
|
}
|
|
2125
2244
|
/**
|
|
@@ -2139,8 +2258,32 @@ var SddInterviewDriver = class {
|
|
|
2139
2258
|
this.tracker = tracker;
|
|
2140
2259
|
}
|
|
2141
2260
|
}
|
|
2261
|
+
this.resumedFromDisk = true;
|
|
2142
2262
|
return true;
|
|
2143
2263
|
}
|
|
2264
|
+
/** Drop the on-disk session (if any) and clear in-memory interview state. */
|
|
2265
|
+
async discard() {
|
|
2266
|
+
await this.builder.deleteSession();
|
|
2267
|
+
this.builder.resetForNewInterview();
|
|
2268
|
+
this.tracker = null;
|
|
2269
|
+
this.graph = null;
|
|
2270
|
+
this.resumedFromDisk = false;
|
|
2271
|
+
}
|
|
2272
|
+
setLastAgentText(text) {
|
|
2273
|
+
this.builder.setLastAgentText(text);
|
|
2274
|
+
}
|
|
2275
|
+
getLastAgentText() {
|
|
2276
|
+
return this.builder.getLastAgentText();
|
|
2277
|
+
}
|
|
2278
|
+
setLastRunId(runId) {
|
|
2279
|
+
this.builder.setLastRunId(runId);
|
|
2280
|
+
}
|
|
2281
|
+
getLastRunId() {
|
|
2282
|
+
return this.builder.getLastRunId();
|
|
2283
|
+
}
|
|
2284
|
+
wasResumed() {
|
|
2285
|
+
return this.resumedFromDisk;
|
|
2286
|
+
}
|
|
2144
2287
|
phase() {
|
|
2145
2288
|
return this.builder.getPhase();
|
|
2146
2289
|
}
|
|
@@ -2241,6 +2384,9 @@ var SddInterviewDriver = class {
|
|
|
2241
2384
|
minQuestions: this.minQuestions,
|
|
2242
2385
|
maxQuestions: this.maxQuestions,
|
|
2243
2386
|
answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
|
|
2387
|
+
lastAgentText: s.lastAgentText,
|
|
2388
|
+
lastRunId: s.lastRunId,
|
|
2389
|
+
resumed: this.resumedFromDisk || void 0,
|
|
2244
2390
|
spec: spec ? {
|
|
2245
2391
|
id: spec.id,
|
|
2246
2392
|
title: spec.title,
|
|
@@ -2308,16 +2454,17 @@ var SddInterviewDriver = class {
|
|
|
2308
2454
|
);
|
|
2309
2455
|
if (valid.length === 0) return void 0;
|
|
2310
2456
|
const spec = this.builder.getSession().spec;
|
|
2311
|
-
if (!
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
this.
|
|
2315
|
-
this.tracker = tracker;
|
|
2457
|
+
if (!this.tracker) {
|
|
2458
|
+
const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
|
|
2459
|
+
this.graph = await tracker2.createGraph(spec.id, spec.title);
|
|
2460
|
+
this.tracker = tracker2;
|
|
2316
2461
|
}
|
|
2462
|
+
const tracker = this.tracker;
|
|
2463
|
+
const graph = this.graph;
|
|
2317
2464
|
const refMap = /* @__PURE__ */ new Map();
|
|
2318
2465
|
const created = [];
|
|
2319
2466
|
valid.forEach((task, i) => {
|
|
2320
|
-
const node = addTaskToTracker(
|
|
2467
|
+
const node = addTaskToTracker(tracker, task);
|
|
2321
2468
|
created.push({ nodeId: node.id, task });
|
|
2322
2469
|
if (typeof task.id === "string" && task.id.trim()) {
|
|
2323
2470
|
refMap.set(task.id.trim().toLowerCase(), node.id);
|
|
@@ -2330,13 +2477,13 @@ var SddInterviewDriver = class {
|
|
|
2330
2477
|
const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
|
|
2331
2478
|
for (const ref of deps) {
|
|
2332
2479
|
const depId = refMap.get(normalizeTaskRef(String(ref)));
|
|
2333
|
-
if (depId && depId !== nodeId)
|
|
2480
|
+
if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
|
|
2334
2481
|
}
|
|
2335
2482
|
}
|
|
2336
|
-
await this.persistGraph(
|
|
2337
|
-
this.builder.setTaskGraphId(
|
|
2483
|
+
await this.persistGraph(graph);
|
|
2484
|
+
this.builder.setTaskGraphId(graph.id);
|
|
2338
2485
|
await this.builder.saveSession();
|
|
2339
|
-
return
|
|
2486
|
+
return graph.id;
|
|
2340
2487
|
}
|
|
2341
2488
|
};
|
|
2342
2489
|
var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
|
|
@@ -2364,10 +2511,219 @@ function isExplanatoryText(text) {
|
|
|
2364
2511
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
2365
2512
|
|
|
2366
2513
|
// src/sdd-parallel-run.ts
|
|
2367
|
-
import {
|
|
2514
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2515
|
+
import {
|
|
2516
|
+
DefaultMultiAgentCoordinator,
|
|
2517
|
+
makeAgentSubagentRunner,
|
|
2518
|
+
withDisabledToolFiltering
|
|
2519
|
+
} from "@wrongstack/core/coordination";
|
|
2520
|
+
|
|
2521
|
+
// src/graph-split.ts
|
|
2522
|
+
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
2523
|
+
const node = tracker.getNode(taskId);
|
|
2524
|
+
if (!node) return [];
|
|
2525
|
+
if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
|
|
2526
|
+
if (!subtasks.length) return [];
|
|
2527
|
+
const blockers = tracker.getBlockers(taskId);
|
|
2528
|
+
const dependents = tracker.getDependents(taskId);
|
|
2529
|
+
const leafIds = subtasks.map((s) => {
|
|
2530
|
+
const criterion = s.successCriterion?.trim();
|
|
2531
|
+
const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
|
|
2532
|
+
const description = criterion && !verificationCommand ? `${s.description}
|
|
2533
|
+
|
|
2534
|
+
**Acceptance Criteria:**
|
|
2535
|
+
- ${criterion}` : s.description;
|
|
2536
|
+
return tracker.addNode({
|
|
2537
|
+
title: s.title,
|
|
2538
|
+
description,
|
|
2539
|
+
type: s.type ?? node.type,
|
|
2540
|
+
priority: s.priority ?? node.priority,
|
|
2541
|
+
status: "pending",
|
|
2542
|
+
parentId: taskId,
|
|
2543
|
+
...verificationCommand ? { metadata: { verificationCommand } } : {}
|
|
2544
|
+
}).id;
|
|
2545
|
+
});
|
|
2546
|
+
for (const leaf of leafIds) {
|
|
2547
|
+
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
2548
|
+
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
2549
|
+
}
|
|
2550
|
+
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2551
|
+
return leafIds;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/sdd-task-execution.ts
|
|
2368
2555
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2369
|
-
import {
|
|
2370
|
-
import {
|
|
2556
|
+
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
2557
|
+
import { assignNickname } from "@wrongstack/core/coordination";
|
|
2558
|
+
import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
|
|
2559
|
+
async function executeSddTask(params) {
|
|
2560
|
+
const { task, opts } = params;
|
|
2561
|
+
const taskId = task.id;
|
|
2562
|
+
let agentName = task.assignee;
|
|
2563
|
+
if (!agentName) {
|
|
2564
|
+
const nick = assignNickname("executor", params.usedNicknames);
|
|
2565
|
+
params.usedNicknames.add(nick.key);
|
|
2566
|
+
agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
2567
|
+
opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
2568
|
+
}
|
|
2569
|
+
opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
2570
|
+
await params.allocateWorktrees([task]);
|
|
2571
|
+
if (!params.coordinator)
|
|
2572
|
+
throw new SddError3({
|
|
2573
|
+
message: "SDD parallel runner requires a coordinator",
|
|
2574
|
+
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
2575
|
+
});
|
|
2576
|
+
const coordinator = params.coordinator;
|
|
2577
|
+
const subagentId = params.nextSubagentId();
|
|
2578
|
+
const correlationId = randomUUID2();
|
|
2579
|
+
const meta = task.metadata ?? {};
|
|
2580
|
+
const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
|
|
2581
|
+
const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
|
|
2582
|
+
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
|
|
2583
|
+
const spawnResult = await coordinator.spawn({
|
|
2584
|
+
id: subagentId,
|
|
2585
|
+
name: agentName,
|
|
2586
|
+
role: "executor",
|
|
2587
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
2588
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
2589
|
+
cwd: params.taskCwds.get(taskId),
|
|
2590
|
+
disabledTools: ["delegate"],
|
|
2591
|
+
...model ? { model } : {},
|
|
2592
|
+
...provider ? { provider } : {},
|
|
2593
|
+
...fallbackModels?.length ? { fallbackModels } : {}
|
|
2594
|
+
});
|
|
2595
|
+
if (!spawnResult.subagentId) {
|
|
2596
|
+
throw new SddError3({
|
|
2597
|
+
message: "One or more subagent spawns failed",
|
|
2598
|
+
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
params.taskSubagents.set(taskId, subagentId);
|
|
2602
|
+
params.emit("sdd.task.started", {
|
|
2603
|
+
runId: params.runId,
|
|
2604
|
+
taskId,
|
|
2605
|
+
subagentId,
|
|
2606
|
+
agentName,
|
|
2607
|
+
worktreeBranch: params.taskBranches.get(taskId)
|
|
2608
|
+
});
|
|
2609
|
+
await coordinator.assign({
|
|
2610
|
+
id: correlationId,
|
|
2611
|
+
description: buildTaskDirective(opts.graph.title, task),
|
|
2612
|
+
subagentId,
|
|
2613
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
2614
|
+
context: {
|
|
2615
|
+
telemetryTaskId: taskId,
|
|
2616
|
+
telemetryRunId: params.runId,
|
|
2617
|
+
telemetryBoardId: opts.graph.id
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
let result;
|
|
2621
|
+
try {
|
|
2622
|
+
const got = await coordinator.awaitTasks([correlationId]);
|
|
2623
|
+
result = expectDefined3(got[0]);
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
result = {
|
|
2626
|
+
subagentId,
|
|
2627
|
+
taskId: correlationId,
|
|
2628
|
+
status: "failed",
|
|
2629
|
+
error: { kind: "unknown", message: String(err), retryable: false },
|
|
2630
|
+
iterations: 0,
|
|
2631
|
+
toolCalls: 0,
|
|
2632
|
+
durationMs: 0
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
params.taskSubagents.delete(taskId);
|
|
2636
|
+
if (params.cancelledTasks.has(taskId)) {
|
|
2637
|
+
await params.resolveWorktrees([task]);
|
|
2638
|
+
return { taskId, success: false, result };
|
|
2639
|
+
}
|
|
2640
|
+
const verificationFailReason = await verifyTaskResult(params, result);
|
|
2641
|
+
let success = false;
|
|
2642
|
+
if (result.status === "success" && !verificationFailReason) {
|
|
2643
|
+
const merged = await params.integrateWorktree(task, result);
|
|
2644
|
+
if (merged.ok) {
|
|
2645
|
+
success = true;
|
|
2646
|
+
opts.tracker.updateNodeStatus(taskId, "completed");
|
|
2647
|
+
params.emit("sdd.task.completed", {
|
|
2648
|
+
runId: params.runId,
|
|
2649
|
+
taskId,
|
|
2650
|
+
subagentId,
|
|
2651
|
+
durationMs: result.durationMs
|
|
2652
|
+
});
|
|
2653
|
+
} else if (merged.reason) {
|
|
2654
|
+
params.emit("sdd.task.verification_failed", {
|
|
2655
|
+
runId: params.runId,
|
|
2656
|
+
taskId,
|
|
2657
|
+
reason: merged.reason
|
|
2658
|
+
});
|
|
2659
|
+
await params.applyTaskFailure(taskId, subagentId, merged.reason);
|
|
2660
|
+
} else {
|
|
2661
|
+
const conflictFiles = merged.conflictFiles ?? [];
|
|
2662
|
+
params.emit("sdd.task.conflict", { runId: params.runId, taskId, conflictFiles });
|
|
2663
|
+
const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
|
|
2664
|
+
await params.applyTaskFailure(taskId, subagentId, reason);
|
|
2665
|
+
}
|
|
2666
|
+
} else {
|
|
2667
|
+
const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
|
|
2668
|
+
await params.applyTaskFailure(taskId, subagentId, errMsg);
|
|
2669
|
+
await params.resolveWorktrees([task]);
|
|
2670
|
+
}
|
|
2671
|
+
return { taskId, success, result };
|
|
2672
|
+
}
|
|
2673
|
+
function buildTaskDirective(graphTitle, task) {
|
|
2674
|
+
const directivePreamble = [
|
|
2675
|
+
"\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
|
|
2676
|
+
"",
|
|
2677
|
+
`Graph: ${graphTitle}`,
|
|
2678
|
+
"",
|
|
2679
|
+
"\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
|
|
2680
|
+
"\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
|
|
2681
|
+
"\u2022 Mark the task [done] in the tracker when complete.",
|
|
2682
|
+
"\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
|
|
2683
|
+
"\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
|
|
2684
|
+
].join("\n");
|
|
2685
|
+
return [
|
|
2686
|
+
directivePreamble,
|
|
2687
|
+
"",
|
|
2688
|
+
`\u2500\u2500 TASK \u2500\u2500`,
|
|
2689
|
+
`[${task.priority.toUpperCase()}] ${task.title}`,
|
|
2690
|
+
"",
|
|
2691
|
+
task.description
|
|
2692
|
+
].join("\n");
|
|
2693
|
+
}
|
|
2694
|
+
async function verifyTaskResult(params, result) {
|
|
2695
|
+
const { task, opts, taskCwds } = params;
|
|
2696
|
+
if (result.status !== "success" || !opts.verifyTask) return void 0;
|
|
2697
|
+
const taskId = task.id;
|
|
2698
|
+
const cwd = taskCwds.get(taskId) ?? opts.projectRoot;
|
|
2699
|
+
let verificationFailReason;
|
|
2700
|
+
try {
|
|
2701
|
+
const verdict = await opts.verifyTask({ task, result, cwd });
|
|
2702
|
+
if (!verdict.ok) {
|
|
2703
|
+
verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
|
|
2704
|
+
}
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
verificationFailReason = `verification error: ${String(err)}`;
|
|
2707
|
+
}
|
|
2708
|
+
const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
|
|
2709
|
+
if (verificationFailReason) {
|
|
2710
|
+
opts.tracker.patchMetadata(taskId, {
|
|
2711
|
+
verificationState: "failed",
|
|
2712
|
+
verificationDetail: verificationFailReason
|
|
2713
|
+
});
|
|
2714
|
+
params.emit("sdd.task.verification_failed", {
|
|
2715
|
+
runId: params.runId,
|
|
2716
|
+
taskId,
|
|
2717
|
+
reason: verificationFailReason
|
|
2718
|
+
});
|
|
2719
|
+
} else if (hadVerifiable) {
|
|
2720
|
+
opts.tracker.patchMetadata(taskId, {
|
|
2721
|
+
verificationState: "passed",
|
|
2722
|
+
verificationDetail: void 0
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
return verificationFailReason;
|
|
2726
|
+
}
|
|
2371
2727
|
|
|
2372
2728
|
// src/sdd-task-decomposer.ts
|
|
2373
2729
|
var SddTaskDecomposer = class {
|
|
@@ -2485,13 +2841,15 @@ var SddParallelRun = class {
|
|
|
2485
2841
|
this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
|
|
2486
2842
|
this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
|
|
2487
2843
|
this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
|
|
2488
|
-
this.runId = opts.runId ?? `sdd-${
|
|
2844
|
+
this.runId = opts.runId ?? `sdd-${randomUUID3().slice(0, 8)}`;
|
|
2489
2845
|
this.events = opts.events;
|
|
2490
2846
|
this.sessionIdSource = opts.sessionId;
|
|
2491
2847
|
this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
|
|
2492
2848
|
this.maxWallClockMs = opts.maxWallClockMs;
|
|
2493
2849
|
this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
|
|
2494
|
-
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2850
|
+
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2851
|
+
parallelSlots: this.slots
|
|
2852
|
+
});
|
|
2495
2853
|
}
|
|
2496
2854
|
opts;
|
|
2497
2855
|
slots;
|
|
@@ -2614,7 +2972,8 @@ var SddParallelRun = class {
|
|
|
2614
2972
|
* revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
|
|
2615
2973
|
*/
|
|
2616
2974
|
async rollback() {
|
|
2617
|
-
if (this.isRunning())
|
|
2975
|
+
if (this.isRunning())
|
|
2976
|
+
return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
|
|
2618
2977
|
const wt = this.opts.worktrees;
|
|
2619
2978
|
if (!wt || !this.baseBranch) {
|
|
2620
2979
|
return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
|
|
@@ -2647,7 +3006,10 @@ var SddParallelRun = class {
|
|
|
2647
3006
|
*/
|
|
2648
3007
|
setTaskModel(taskId, model, provider) {
|
|
2649
3008
|
if (!this.opts.tracker.getNode(taskId)) return false;
|
|
2650
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
3009
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
3010
|
+
model,
|
|
3011
|
+
...provider !== void 0 ? { provider } : {}
|
|
3012
|
+
});
|
|
2651
3013
|
return true;
|
|
2652
3014
|
}
|
|
2653
3015
|
/** Set/override a task's fallback model chain (applied on its next dispatch). */
|
|
@@ -2679,7 +3041,12 @@ var SddParallelRun = class {
|
|
|
2679
3041
|
this.cancelledTasks.add(taskId);
|
|
2680
3042
|
this.opts.tracker.patchMetadata(taskId, { cancelled: true });
|
|
2681
3043
|
this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
|
|
2682
|
-
this.emit("sdd.task.failed", {
|
|
3044
|
+
this.emit("sdd.task.failed", {
|
|
3045
|
+
runId: this.runId,
|
|
3046
|
+
taskId,
|
|
3047
|
+
subagentId: "",
|
|
3048
|
+
error: "cancelled by user"
|
|
3049
|
+
});
|
|
2683
3050
|
const subagentId = this.taskSubagents.get(taskId);
|
|
2684
3051
|
if (subagentId && this.coordinator) {
|
|
2685
3052
|
await this.coordinator.stop(subagentId).catch(() => {
|
|
@@ -2710,30 +3077,12 @@ var SddParallelRun = class {
|
|
|
2710
3077
|
* The scheduler picks the new pending leaves up on its next dispatch pass.
|
|
2711
3078
|
*/
|
|
2712
3079
|
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
|
-
}
|
|
3080
|
+
const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
|
|
3081
|
+
isRunning: (id) => this.taskSubagents.has(id)
|
|
3082
|
+
});
|
|
3083
|
+
if (!leafIds.length) return [];
|
|
2734
3084
|
this.retryMap.delete(taskId);
|
|
2735
3085
|
this.persistRetries(taskId, 0);
|
|
2736
|
-
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2737
3086
|
this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
|
|
2738
3087
|
return leafIds;
|
|
2739
3088
|
}
|
|
@@ -2781,7 +3130,12 @@ var SddParallelRun = class {
|
|
|
2781
3130
|
return await this.executeOne(task);
|
|
2782
3131
|
} catch (err) {
|
|
2783
3132
|
this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
|
|
2784
|
-
this.emit("sdd.task.failed", {
|
|
3133
|
+
this.emit("sdd.task.failed", {
|
|
3134
|
+
runId: this.runId,
|
|
3135
|
+
taskId: task.id,
|
|
3136
|
+
subagentId: "",
|
|
3137
|
+
error: String(err)
|
|
3138
|
+
});
|
|
2785
3139
|
return { taskId: task.id, success: false };
|
|
2786
3140
|
} finally {
|
|
2787
3141
|
running.delete(task.id);
|
|
@@ -2795,16 +3149,18 @@ var SddParallelRun = class {
|
|
|
2795
3149
|
await this.waitWhilePaused();
|
|
2796
3150
|
if (this.stopRequested) break;
|
|
2797
3151
|
let dispatchedThisRound = 0;
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
dispatchedThisRound++;
|
|
2804
|
-
}
|
|
3152
|
+
const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
|
|
3153
|
+
for (const task of ready) {
|
|
3154
|
+
if (running.size >= this.slots) break;
|
|
3155
|
+
dispatch(task);
|
|
3156
|
+
dispatchedThisRound++;
|
|
2805
3157
|
}
|
|
2806
3158
|
if (dispatchedThisRound > 0) {
|
|
2807
|
-
this.emit("sdd.wave", {
|
|
3159
|
+
this.emit("sdd.wave", {
|
|
3160
|
+
runId: this.runId,
|
|
3161
|
+
wave: this.round,
|
|
3162
|
+
batchSize: dispatchedThisRound
|
|
3163
|
+
});
|
|
2808
3164
|
this.round++;
|
|
2809
3165
|
}
|
|
2810
3166
|
if (running.size === 0) {
|
|
@@ -2835,7 +3191,6 @@ var SddParallelRun = class {
|
|
|
2835
3191
|
this.opts.onProgress?.(this.buildProgress());
|
|
2836
3192
|
}
|
|
2837
3193
|
}
|
|
2838
|
-
if (running.size > 0) await Promise.allSettled(running.values());
|
|
2839
3194
|
if (this.stopRequested) await this.teardown();
|
|
2840
3195
|
const finalProgress = this.opts.tracker.getProgress();
|
|
2841
3196
|
this.emit("sdd.run.finished", {
|
|
@@ -2966,7 +3321,7 @@ var SddParallelRun = class {
|
|
|
2966
3321
|
// -------------------------------------------------------------------
|
|
2967
3322
|
buildCoordinator() {
|
|
2968
3323
|
const config = {
|
|
2969
|
-
coordinatorId: `sdd-parallel-${
|
|
3324
|
+
coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
|
|
2970
3325
|
maxConcurrent: this.slots,
|
|
2971
3326
|
doneCondition: { type: "all_tasks_done" },
|
|
2972
3327
|
// Default budget guard for every spawned worker: idle reaper (resets on
|
|
@@ -3023,158 +3378,30 @@ var SddParallelRun = class {
|
|
|
3023
3378
|
* missing coordinator or failed spawn so callers can enforce all-or-nothing.
|
|
3024
3379
|
*/
|
|
3025
3380
|
async executeOne(task) {
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
this.usedNicknames
|
|
3031
|
-
agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
3032
|
-
this.opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
3033
|
-
}
|
|
3034
|
-
this.opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
3035
|
-
await this.allocateWorktrees([task]);
|
|
3036
|
-
if (!this.coordinator) throw new SddError3({
|
|
3037
|
-
message: "SDD parallel runner requires a coordinator",
|
|
3038
|
-
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3039
|
-
});
|
|
3040
|
-
const coordinator = this.coordinator;
|
|
3041
|
-
const subagentId = `sdd-d${this.dispatchSeq++}`;
|
|
3042
|
-
const correlationId = randomUUID2();
|
|
3043
|
-
const meta = task.metadata ?? {};
|
|
3044
|
-
const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
|
|
3045
|
-
const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
|
|
3046
|
-
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
|
|
3047
|
-
const spawnResult = await coordinator.spawn({
|
|
3048
|
-
id: subagentId,
|
|
3049
|
-
name: agentName ?? subagentId,
|
|
3050
|
-
role: "executor",
|
|
3051
|
-
// Idle reaper is always on; the hard wall-clock cap only when opted in.
|
|
3381
|
+
const outcome = await executeSddTask({
|
|
3382
|
+
task,
|
|
3383
|
+
opts: this.opts,
|
|
3384
|
+
coordinator: this.coordinator,
|
|
3385
|
+
usedNicknames: this.usedNicknames,
|
|
3052
3386
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
3053
|
-
|
|
3054
|
-
cwd: this.taskCwds.get(taskId),
|
|
3055
|
-
disabledTools: ["delegate"],
|
|
3056
|
-
...model ? { model } : {},
|
|
3057
|
-
...provider ? { provider } : {},
|
|
3058
|
-
...fallbackModels?.length ? { fallbackModels } : {}
|
|
3059
|
-
});
|
|
3060
|
-
if (!spawnResult.subagentId) {
|
|
3061
|
-
throw new SddError3({
|
|
3062
|
-
message: "One or more subagent spawns failed",
|
|
3063
|
-
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3064
|
-
});
|
|
3065
|
-
}
|
|
3066
|
-
this.taskSubagents.set(taskId, subagentId);
|
|
3067
|
-
this.emit("sdd.task.started", {
|
|
3387
|
+
timeoutMs: this.timeoutMs,
|
|
3068
3388
|
runId: this.runId,
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3389
|
+
nextSubagentId: () => `sdd-d${this.dispatchSeq++}`,
|
|
3390
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
3391
|
+
taskCwds: this.taskCwds,
|
|
3392
|
+
taskBranches: this.taskBranches,
|
|
3393
|
+
taskSubagents: this.taskSubagents,
|
|
3394
|
+
cancelledTasks: this.cancelledTasks,
|
|
3395
|
+
allocateWorktrees: (tasks) => this.allocateWorktrees(tasks),
|
|
3396
|
+
resolveWorktrees: (tasks) => this.resolveWorktrees(tasks),
|
|
3397
|
+
integrateWorktree: (taskNode, result) => this.integrateWorktree(taskNode, result),
|
|
3398
|
+
applyTaskFailure: (taskId, subagentId, errMsg) => this.applyTaskFailure(taskId, subagentId, errMsg)
|
|
3073
3399
|
});
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
`Graph: ${this.opts.graph.title}`,
|
|
3078
|
-
"",
|
|
3079
|
-
"\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
|
|
3080
|
-
"\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
|
|
3081
|
-
"\u2022 Mark the task [done] in the tracker when complete.",
|
|
3082
|
-
"\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
|
|
3083
|
-
"\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
|
|
3084
|
-
].join("\n");
|
|
3085
|
-
await coordinator.assign({
|
|
3086
|
-
id: correlationId,
|
|
3087
|
-
description: [
|
|
3088
|
-
directivePreamble,
|
|
3089
|
-
"",
|
|
3090
|
-
`\u2500\u2500 TASK \u2500\u2500`,
|
|
3091
|
-
`[${task.priority.toUpperCase()}] ${task.title}`,
|
|
3092
|
-
"",
|
|
3093
|
-
task.description
|
|
3094
|
-
].join("\n"),
|
|
3095
|
-
subagentId,
|
|
3096
|
-
...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
|
|
3097
|
-
context: {
|
|
3098
|
-
telemetryTaskId: taskId,
|
|
3099
|
-
telemetryRunId: this.runId,
|
|
3100
|
-
telemetryBoardId: this.opts.graph.id
|
|
3101
|
-
}
|
|
3102
|
-
});
|
|
3103
|
-
let result;
|
|
3104
|
-
try {
|
|
3105
|
-
const got = await coordinator.awaitTasks([correlationId]);
|
|
3106
|
-
result = expectDefined2(got[0]);
|
|
3107
|
-
} catch (err) {
|
|
3108
|
-
result = {
|
|
3109
|
-
subagentId,
|
|
3110
|
-
taskId: correlationId,
|
|
3111
|
-
status: "failed",
|
|
3112
|
-
error: { kind: "unknown", message: String(err), retryable: false },
|
|
3113
|
-
iterations: 0,
|
|
3114
|
-
toolCalls: 0,
|
|
3115
|
-
durationMs: 0
|
|
3116
|
-
};
|
|
3400
|
+
if (outcome.success) {
|
|
3401
|
+
this.retryMap.delete(task.id);
|
|
3402
|
+
this.persistRetries(task.id, 0);
|
|
3117
3403
|
}
|
|
3118
|
-
|
|
3119
|
-
if (this.cancelledTasks.has(taskId)) {
|
|
3120
|
-
await this.resolveWorktrees([task]);
|
|
3121
|
-
return { taskId, success: false, result };
|
|
3122
|
-
}
|
|
3123
|
-
let verificationFailReason;
|
|
3124
|
-
if (result.status === "success" && this.opts.verifyTask) {
|
|
3125
|
-
const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
|
|
3126
|
-
try {
|
|
3127
|
-
const verdict = await this.opts.verifyTask({ task, result, cwd });
|
|
3128
|
-
if (!verdict.ok) {
|
|
3129
|
-
verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
|
|
3130
|
-
}
|
|
3131
|
-
} catch (err) {
|
|
3132
|
-
verificationFailReason = `verification error: ${String(err)}`;
|
|
3133
|
-
}
|
|
3134
|
-
if (verificationFailReason) {
|
|
3135
|
-
this.emit("sdd.task.verification_failed", {
|
|
3136
|
-
runId: this.runId,
|
|
3137
|
-
taskId,
|
|
3138
|
-
reason: verificationFailReason
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
let success = false;
|
|
3143
|
-
if (result.status === "success" && !verificationFailReason) {
|
|
3144
|
-
const merged = await this.integrateWorktree(task, result);
|
|
3145
|
-
if (merged.ok) {
|
|
3146
|
-
success = true;
|
|
3147
|
-
this.opts.tracker.updateNodeStatus(taskId, "completed");
|
|
3148
|
-
this.retryMap.delete(taskId);
|
|
3149
|
-
this.persistRetries(taskId, 0);
|
|
3150
|
-
this.emit("sdd.task.completed", {
|
|
3151
|
-
runId: this.runId,
|
|
3152
|
-
taskId,
|
|
3153
|
-
subagentId,
|
|
3154
|
-
durationMs: result.durationMs
|
|
3155
|
-
});
|
|
3156
|
-
} else if (merged.reason) {
|
|
3157
|
-
this.emit("sdd.task.verification_failed", {
|
|
3158
|
-
runId: this.runId,
|
|
3159
|
-
taskId,
|
|
3160
|
-
reason: merged.reason
|
|
3161
|
-
});
|
|
3162
|
-
await this.applyTaskFailure(taskId, subagentId, merged.reason);
|
|
3163
|
-
} else {
|
|
3164
|
-
this.emit("sdd.task.conflict", {
|
|
3165
|
-
runId: this.runId,
|
|
3166
|
-
taskId,
|
|
3167
|
-
conflictFiles: merged.conflictFiles ?? []
|
|
3168
|
-
});
|
|
3169
|
-
const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
|
|
3170
|
-
await this.applyTaskFailure(taskId, subagentId, reason);
|
|
3171
|
-
}
|
|
3172
|
-
} else {
|
|
3173
|
-
const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
|
|
3174
|
-
await this.applyTaskFailure(taskId, subagentId, errMsg);
|
|
3175
|
-
await this.resolveWorktrees([task]);
|
|
3176
|
-
}
|
|
3177
|
-
return { taskId, success, result };
|
|
3404
|
+
return outcome;
|
|
3178
3405
|
}
|
|
3179
3406
|
/**
|
|
3180
3407
|
* Apply a task failure: retry (→ pending, bump retry count) while attempts
|
|
@@ -3269,7 +3496,11 @@ var SddParallelRun = class {
|
|
|
3269
3496
|
const res = await wt.merge(handle, {
|
|
3270
3497
|
squash: true,
|
|
3271
3498
|
...this.opts.conflictResolver ? {
|
|
3272
|
-
resolve: (info) => this.opts.conflictResolver({
|
|
3499
|
+
resolve: (info) => this.opts.conflictResolver({
|
|
3500
|
+
task,
|
|
3501
|
+
conflictFiles: info.conflictFiles,
|
|
3502
|
+
cwd: info.cwd
|
|
3503
|
+
})
|
|
3273
3504
|
} : {}
|
|
3274
3505
|
});
|
|
3275
3506
|
if (res.ok) {
|
|
@@ -3281,7 +3512,8 @@ var SddParallelRun = class {
|
|
|
3281
3512
|
result: result ?? {},
|
|
3282
3513
|
cwd: this.opts.projectRoot
|
|
3283
3514
|
});
|
|
3284
|
-
if (!verdict.ok)
|
|
3515
|
+
if (!verdict.ok)
|
|
3516
|
+
regressed = verdict.reason ?? "verification failed after conflict resolution";
|
|
3285
3517
|
} catch (err) {
|
|
3286
3518
|
regressed = `verification error after conflict resolution: ${String(err)}`;
|
|
3287
3519
|
}
|
|
@@ -3397,6 +3629,32 @@ var SddParallelRun = class {
|
|
|
3397
3629
|
};
|
|
3398
3630
|
|
|
3399
3631
|
// src/start-sdd-run.ts
|
|
3632
|
+
function applySddControlCommand(run, command) {
|
|
3633
|
+
const payload = command.payload ?? {};
|
|
3634
|
+
if (command.type === "pause") run.pause();
|
|
3635
|
+
else if (command.type === "resume") run.resume();
|
|
3636
|
+
else if (command.type === "stop") run.stop();
|
|
3637
|
+
else if (command.type === "retry" && payload.taskId) run.retryTask(payload.taskId);
|
|
3638
|
+
else if (command.type === "retry_all_failed") run.retryAllFailed();
|
|
3639
|
+
else if (command.type === "reassign" && payload.taskId)
|
|
3640
|
+
run.reassignTask(payload.taskId, payload.agentName ?? "");
|
|
3641
|
+
else if (command.type === "set_task_model" && payload.taskId)
|
|
3642
|
+
run.setTaskModel(payload.taskId, payload.model, payload.provider);
|
|
3643
|
+
else if (command.type === "set_task_fallbacks" && payload.taskId)
|
|
3644
|
+
run.setTaskFallbacks(payload.taskId, payload.fallbackModels);
|
|
3645
|
+
else if (command.type === "set_task_verification" && payload.taskId)
|
|
3646
|
+
run.setTaskVerification(payload.taskId, payload.verificationCommand);
|
|
3647
|
+
else if (command.type === "cancel_task" && payload.taskId)
|
|
3648
|
+
void run.cancelTask(payload.taskId).catch(() => {
|
|
3649
|
+
});
|
|
3650
|
+
else if (command.type === "delete_task" && payload.taskId) run.deleteTask(payload.taskId);
|
|
3651
|
+
else if (command.type === "split_task" && payload.taskId && payload.subtasks?.length)
|
|
3652
|
+
run.splitTask(payload.taskId, payload.subtasks);
|
|
3653
|
+
else if (command.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
|
|
3654
|
+
});
|
|
3655
|
+
else if (command.type === "rollback") void run.rollback().catch(() => {
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3400
3658
|
function startSddRun(opts) {
|
|
3401
3659
|
SddParallelRun.resetOrphans(opts.tracker);
|
|
3402
3660
|
const run = new SddParallelRun({
|
|
@@ -3460,25 +3718,7 @@ function startSddRun(opts) {
|
|
|
3460
3718
|
const controlTimer = setInterval(() => {
|
|
3461
3719
|
void opts.boardStore.drainControl(run.runId).then((cmds) => {
|
|
3462
3720
|
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
|
-
});
|
|
3721
|
+
applySddControlCommand(run, c);
|
|
3482
3722
|
}
|
|
3483
3723
|
}).catch(() => {
|
|
3484
3724
|
});
|
|
@@ -3506,7 +3746,10 @@ function startSddRun(opts) {
|
|
|
3506
3746
|
|
|
3507
3747
|
// src/sdd-lifecycle.ts
|
|
3508
3748
|
import * as fsp4 from "node:fs/promises";
|
|
3749
|
+
import * as path4 from "node:path";
|
|
3750
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
3509
3751
|
import { WorktreeManager } from "@wrongstack/core/worktree";
|
|
3752
|
+
import { listBoards, removeBoard } from "@wrongstack/kanban";
|
|
3510
3753
|
async function cleanupSddWorktrees(projectRoot) {
|
|
3511
3754
|
const wt = new WorktreeManager({ projectRoot });
|
|
3512
3755
|
return wt.cleanupAllManaged();
|
|
@@ -3517,19 +3760,21 @@ async function cleanupStaleWorktrees(projectRoot) {
|
|
|
3517
3760
|
}
|
|
3518
3761
|
async function cleanupStaleSddWorktrees(opts) {
|
|
3519
3762
|
const now = opts.now?.() ?? Date.now();
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
}
|
|
3763
|
+
const store = new SddBoardStore({ baseDir: opts.boardsDir });
|
|
3764
|
+
const latest = (await store.list())[0];
|
|
3765
|
+
if (latest) {
|
|
3766
|
+
const age = now - latest.updatedAt;
|
|
3767
|
+
if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
|
|
3768
|
+
return {
|
|
3769
|
+
swept: false,
|
|
3770
|
+
removed: 0,
|
|
3771
|
+
detected: 0,
|
|
3772
|
+
skippedReason: "a run appears live (running)"
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3775
|
+
if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
|
|
3776
|
+
return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
|
|
3531
3777
|
}
|
|
3532
|
-
} catch {
|
|
3533
3778
|
}
|
|
3534
3779
|
try {
|
|
3535
3780
|
const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
|
|
@@ -3546,7 +3791,11 @@ async function rollbackSddRunFromDisk(opts) {
|
|
|
3546
3791
|
const snap = await store.load(runId);
|
|
3547
3792
|
if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
|
|
3548
3793
|
if (!snap.baseBranch) {
|
|
3549
|
-
return {
|
|
3794
|
+
return {
|
|
3795
|
+
ok: false,
|
|
3796
|
+
reverted: 0,
|
|
3797
|
+
reason: "this run did not record a base branch (no worktree run)"
|
|
3798
|
+
};
|
|
3550
3799
|
}
|
|
3551
3800
|
const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
|
|
3552
3801
|
if (shas.length === 0) {
|
|
@@ -3564,7 +3813,7 @@ async function destroySddProject(opts) {
|
|
|
3564
3813
|
projectRoot: opts.projectRoot,
|
|
3565
3814
|
boardsDir: opts.paths.projectSddBoards,
|
|
3566
3815
|
runId: opts.runId
|
|
3567
|
-
}).catch((err) => ({ ok: false, reverted: 0, reason:
|
|
3816
|
+
}).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
|
|
3568
3817
|
reverted = r.reverted;
|
|
3569
3818
|
revertOk = r.ok;
|
|
3570
3819
|
revertReason = r.reason;
|
|
@@ -3586,14 +3835,24 @@ async function destroySddProject(opts) {
|
|
|
3586
3835
|
}
|
|
3587
3836
|
};
|
|
3588
3837
|
await rmFile(opts.paths.projectSddSession, "session");
|
|
3838
|
+
await rmFile(
|
|
3839
|
+
path4.join(path4.dirname(opts.paths.projectSddSession), "sdd-wizard-session.json"),
|
|
3840
|
+
"wizard-session"
|
|
3841
|
+
);
|
|
3589
3842
|
await rmDir(opts.paths.projectSpecs, "specs");
|
|
3590
3843
|
await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
|
|
3591
3844
|
await rmDir(opts.paths.projectSddBoards, "boards");
|
|
3845
|
+
try {
|
|
3846
|
+
const mirrors = (await listBoards(opts.projectRoot)).filter((b) => b.tags?.includes("sdd"));
|
|
3847
|
+
let mirrorsRemoved = 0;
|
|
3848
|
+
for (const b of mirrors) {
|
|
3849
|
+
if (await removeBoard(opts.projectRoot, b.id)) mirrorsRemoved++;
|
|
3850
|
+
}
|
|
3851
|
+
if (mirrorsRemoved > 0) deleted.push(`kanban-mirrors(${mirrorsRemoved})`);
|
|
3852
|
+
} catch {
|
|
3853
|
+
}
|
|
3592
3854
|
return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
|
|
3593
3855
|
}
|
|
3594
|
-
function toReason(err) {
|
|
3595
|
-
return err instanceof Error ? err.message : String(err);
|
|
3596
|
-
}
|
|
3597
3856
|
async function applySddLifecycle(op, opts) {
|
|
3598
3857
|
try {
|
|
3599
3858
|
if (op === "cleanup_worktrees") {
|
|
@@ -3625,8 +3884,58 @@ async function applySddLifecycle(op, opts) {
|
|
|
3625
3884
|
reason: r.revertOk === false ? r.revertReason : void 0
|
|
3626
3885
|
};
|
|
3627
3886
|
} catch (err) {
|
|
3628
|
-
return { op, ok: false, reason:
|
|
3887
|
+
return { op, ok: false, reason: toErrorMessage2(err) };
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// src/project-context.ts
|
|
3892
|
+
import * as fsp5 from "node:fs/promises";
|
|
3893
|
+
import * as path5 from "node:path";
|
|
3894
|
+
async function gatherProjectContext(projectRoot) {
|
|
3895
|
+
const parts = [];
|
|
3896
|
+
const root = projectRoot.trim() || process.cwd();
|
|
3897
|
+
try {
|
|
3898
|
+
const pkgPath = path5.join(root, "package.json");
|
|
3899
|
+
const pkgRaw = await fsp5.readFile(pkgPath, "utf8");
|
|
3900
|
+
const pkg = JSON.parse(pkgRaw);
|
|
3901
|
+
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
3902
|
+
parts.push(`Description: ${String(pkg.description ?? "none")}`);
|
|
3903
|
+
if (pkg.dependencies && typeof pkg.dependencies === "object") {
|
|
3904
|
+
const deps = Object.keys(pkg.dependencies);
|
|
3905
|
+
parts.push(`Dependencies: ${deps.slice(0, 20).join(", ")}${deps.length > 20 ? "..." : ""}`);
|
|
3906
|
+
}
|
|
3907
|
+
if (pkg.devDependencies && typeof pkg.devDependencies === "object") {
|
|
3908
|
+
const devDeps = Object.keys(pkg.devDependencies);
|
|
3909
|
+
parts.push(
|
|
3910
|
+
`Dev Dependencies: ${devDeps.slice(0, 15).join(", ")}${devDeps.length > 15 ? "..." : ""}`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
} catch {
|
|
3914
|
+
}
|
|
3915
|
+
try {
|
|
3916
|
+
await fsp5.access(path5.join(root, "tsconfig.json"));
|
|
3917
|
+
parts.push("Language: TypeScript");
|
|
3918
|
+
} catch {
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
const srcDir = path5.join(root, "src");
|
|
3922
|
+
const entries = await fsp5.readdir(srcDir, { withFileTypes: true });
|
|
3923
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3924
|
+
if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
|
|
3925
|
+
} catch {
|
|
3629
3926
|
}
|
|
3927
|
+
try {
|
|
3928
|
+
const packagesDir = path5.join(root, "packages");
|
|
3929
|
+
const entries = await fsp5.readdir(packagesDir, { withFileTypes: true });
|
|
3930
|
+
const pkgs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3931
|
+
if (pkgs.length > 0) {
|
|
3932
|
+
parts.push(
|
|
3933
|
+
`Packages: ${pkgs.slice(0, 25).join(", ")}${pkgs.length > 25 ? "..." : ""}`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
} catch {
|
|
3937
|
+
}
|
|
3938
|
+
return parts.join("\n");
|
|
3630
3939
|
}
|
|
3631
3940
|
|
|
3632
3941
|
// src/spec-templates.ts
|
|
@@ -3747,7 +4056,7 @@ function templateToMarkdown(template, title) {
|
|
|
3747
4056
|
|
|
3748
4057
|
// src/task-visualizer.ts
|
|
3749
4058
|
import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
|
|
3750
|
-
import {
|
|
4059
|
+
import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
|
|
3751
4060
|
var STATUS_ICON = {
|
|
3752
4061
|
pending: "\u25CB",
|
|
3753
4062
|
in_progress: "\u25D0",
|
|
@@ -3774,7 +4083,9 @@ function renderTaskGraph(graph, opts) {
|
|
|
3774
4083
|
const lines = [];
|
|
3775
4084
|
const compact = opts?.compact ?? false;
|
|
3776
4085
|
lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
|
|
3777
|
-
lines.push(
|
|
4086
|
+
lines.push(
|
|
4087
|
+
`\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`
|
|
4088
|
+
);
|
|
3778
4089
|
lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
|
|
3779
4090
|
lines.push("");
|
|
3780
4091
|
const progress = computeTaskProgress2(graph);
|
|
@@ -3809,19 +4120,18 @@ function renderTaskGraph(graph, opts) {
|
|
|
3809
4120
|
function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
|
|
3810
4121
|
if (rendered.has(nodeId)) return;
|
|
3811
4122
|
rendered.add(nodeId);
|
|
3812
|
-
const node = graph.nodes.get(nodeId);
|
|
3813
|
-
if (!node) return;
|
|
4123
|
+
const node = expectDefined4(graph.nodes.get(nodeId));
|
|
3814
4124
|
const icon = STATUS_ICON[node.status];
|
|
3815
4125
|
const prioIcon = PRIORITY_ICON[node.priority];
|
|
3816
4126
|
const typeIcon = TYPE_ICON[node.type];
|
|
3817
|
-
const title = compact ?
|
|
4127
|
+
const title = compact ? truncate(node.title, 40) : node.title;
|
|
3818
4128
|
const blockedBy = childrenMap.get(nodeId) ?? [];
|
|
3819
4129
|
const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
|
|
3820
4130
|
lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
|
|
3821
4131
|
if (!compact && node.description) {
|
|
3822
4132
|
const descLines = node.description.split("\n").slice(0, 3);
|
|
3823
4133
|
for (const dl of descLines) {
|
|
3824
|
-
lines.push(`${prefix} \u2514 ${
|
|
4134
|
+
lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
|
|
3825
4135
|
}
|
|
3826
4136
|
}
|
|
3827
4137
|
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 +4161,7 @@ function renderTaskList(graph) {
|
|
|
3851
4161
|
completed: []
|
|
3852
4162
|
};
|
|
3853
4163
|
for (const node of nodes) {
|
|
3854
|
-
groups[node.status]
|
|
4164
|
+
groups[node.status].push(node);
|
|
3855
4165
|
}
|
|
3856
4166
|
for (const [status, group] of Object.entries(groups)) {
|
|
3857
4167
|
if (group.length === 0) continue;
|
|
@@ -3899,8 +4209,8 @@ function renderSpecAnalysis(spec, analysis) {
|
|
|
3899
4209
|
}
|
|
3900
4210
|
|
|
3901
4211
|
// src/critical-path.ts
|
|
3902
|
-
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
3903
4212
|
import { topologicalSort } from "@wrongstack/core/tasking";
|
|
4213
|
+
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
3904
4214
|
function analyzeCriticalPath(graph) {
|
|
3905
4215
|
const nodes = Array.from(graph.nodes.values());
|
|
3906
4216
|
const topoOrder = topologicalSort(graph);
|
|
@@ -3954,8 +4264,7 @@ function analyzeCriticalPath(graph) {
|
|
|
3954
4264
|
bottlenecks.sort((a, b) => b.severity - a.severity);
|
|
3955
4265
|
const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
|
|
3956
4266
|
const totalHours = criticalPath.reduce((sum, id) => {
|
|
3957
|
-
|
|
3958
|
-
return sum + (n?.estimateHours ?? 0);
|
|
4267
|
+
return sum + (graph.nodes.get(id).estimateHours ?? 0);
|
|
3959
4268
|
}, 0);
|
|
3960
4269
|
const parallelGroups = computeParallelGroups(graph, blockedByMap);
|
|
3961
4270
|
const executionOrder = topoOrder.filter((id) => {
|
|
@@ -3976,7 +4285,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
|
|
|
3976
4285
|
const visited = /* @__PURE__ */ new Set();
|
|
3977
4286
|
const queue = [taskId];
|
|
3978
4287
|
while (queue.length > 0) {
|
|
3979
|
-
const current =
|
|
4288
|
+
const current = expectDefined5(queue.shift());
|
|
3980
4289
|
const blocked = blocksMap.get(current);
|
|
3981
4290
|
if (!blocked) continue;
|
|
3982
4291
|
for (const id of blocked) {
|
|
@@ -4011,7 +4320,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
4011
4320
|
const blocked = blocksMap.get(id);
|
|
4012
4321
|
if (!blocked) continue;
|
|
4013
4322
|
for (const blockedId of blocked) {
|
|
4014
|
-
const candidateDist =
|
|
4323
|
+
const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
|
|
4015
4324
|
if (candidateDist > (dist.get(blockedId) ?? 0)) {
|
|
4016
4325
|
dist.set(blockedId, candidateDist);
|
|
4017
4326
|
prev.set(blockedId, id);
|
|
@@ -4022,23 +4331,23 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
4022
4331
|
if (!changed) break;
|
|
4023
4332
|
}
|
|
4024
4333
|
let maxDist = 0;
|
|
4025
|
-
let maxId =
|
|
4334
|
+
let maxId = expectDefined5(allIds[0]);
|
|
4026
4335
|
for (const id of allIds) {
|
|
4027
|
-
const d = dist.get(id)
|
|
4336
|
+
const d = dist.get(id);
|
|
4028
4337
|
if (d > maxDist) {
|
|
4029
4338
|
maxDist = d;
|
|
4030
4339
|
maxId = id;
|
|
4031
4340
|
}
|
|
4032
4341
|
}
|
|
4033
|
-
const
|
|
4342
|
+
const path6 = [];
|
|
4034
4343
|
let current = maxId;
|
|
4035
4344
|
const visited = /* @__PURE__ */ new Set();
|
|
4036
4345
|
while (current && !visited.has(current)) {
|
|
4037
4346
|
visited.add(current);
|
|
4038
|
-
|
|
4347
|
+
path6.unshift(current);
|
|
4039
4348
|
current = prev.get(current) ?? null;
|
|
4040
4349
|
}
|
|
4041
|
-
return
|
|
4350
|
+
return path6;
|
|
4042
4351
|
}
|
|
4043
4352
|
function computeParallelGroups(graph, blockedByMap) {
|
|
4044
4353
|
const groups = [];
|
|
@@ -4059,8 +4368,7 @@ function computeParallelGroups(graph, blockedByMap) {
|
|
|
4059
4368
|
}
|
|
4060
4369
|
}
|
|
4061
4370
|
if (group.length === 0) {
|
|
4062
|
-
|
|
4063
|
-
if (first) group.push(first);
|
|
4371
|
+
group.push(expectDefined5(Array.from(remaining)[0]));
|
|
4064
4372
|
}
|
|
4065
4373
|
for (const id of group) {
|
|
4066
4374
|
assigned.add(id);
|
|
@@ -4274,7 +4582,6 @@ var AutoExecutor = class {
|
|
|
4274
4582
|
for (let i = 0; i < results.length; i++) {
|
|
4275
4583
|
const result = results[i];
|
|
4276
4584
|
const task = batch[i];
|
|
4277
|
-
if (!result || !task) continue;
|
|
4278
4585
|
if (result.status === "fulfilled") {
|
|
4279
4586
|
const { result: execResult, retries } = result.value;
|
|
4280
4587
|
if (execResult.success) {
|
|
@@ -4324,14 +4631,14 @@ var AutoExecutor = class {
|
|
|
4324
4631
|
}
|
|
4325
4632
|
}
|
|
4326
4633
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4327
|
-
ready.sort((a, b) =>
|
|
4634
|
+
ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
4328
4635
|
return ready;
|
|
4329
4636
|
}
|
|
4330
4637
|
/** Execute a single task with retry logic. */
|
|
4331
4638
|
async executeTaskWithRetry(task, graph, spec) {
|
|
4332
4639
|
const maxRetries = this.opts.maxRetries ?? 2;
|
|
4333
4640
|
let retryCount = this.retryMap.get(task.id) ?? 0;
|
|
4334
|
-
while (
|
|
4641
|
+
while (true) {
|
|
4335
4642
|
this.opts.tracker.updateNodeStatus(task.id, "in_progress");
|
|
4336
4643
|
this.opts.onTaskStart?.(task);
|
|
4337
4644
|
const dependencies = this.getTaskDependencies(task.id, graph);
|
|
@@ -4375,7 +4682,6 @@ var AutoExecutor = class {
|
|
|
4375
4682
|
};
|
|
4376
4683
|
}
|
|
4377
4684
|
}
|
|
4378
|
-
return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
|
|
4379
4685
|
}
|
|
4380
4686
|
/** Get tasks that this task depends on. */
|
|
4381
4687
|
getTaskDependencies(taskId, graph) {
|
|
@@ -4462,6 +4768,54 @@ Supervisor rescues already used: ${attempts}`,
|
|
|
4462
4768
|
|
|
4463
4769
|
// src/verify-task.ts
|
|
4464
4770
|
import { spawn } from "node:child_process";
|
|
4771
|
+
function verificationShell(platform) {
|
|
4772
|
+
return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4773
|
+
}
|
|
4774
|
+
function makeCompositeVerifier(parts) {
|
|
4775
|
+
return async function verifyTask(info) {
|
|
4776
|
+
for (const part of parts) {
|
|
4777
|
+
const outcome = await part(info);
|
|
4778
|
+
if (!outcome.ok) return outcome;
|
|
4779
|
+
}
|
|
4780
|
+
return { ok: true };
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
function makeAcceptanceCriteriaVerifier(options) {
|
|
4784
|
+
const maxResultChars = options.maxResultChars ?? 4e3;
|
|
4785
|
+
return async function verifyTask(info) {
|
|
4786
|
+
const description = info.task.description ?? "";
|
|
4787
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4788
|
+
if (marker === -1) return { ok: true };
|
|
4789
|
+
const criteria = description.slice(marker);
|
|
4790
|
+
const resultText = typeof info.result.result === "string" ? info.result.result.slice(0, maxResultChars) : JSON.stringify(info.result.result ?? "").slice(0, maxResultChars);
|
|
4791
|
+
let text;
|
|
4792
|
+
try {
|
|
4793
|
+
text = await options.run(
|
|
4794
|
+
[
|
|
4795
|
+
"You are a strict acceptance reviewer for one completed engineering task.",
|
|
4796
|
+
`Task: ${info.task.title}`,
|
|
4797
|
+
"",
|
|
4798
|
+
criteria,
|
|
4799
|
+
"",
|
|
4800
|
+
"Worker's reported result:",
|
|
4801
|
+
resultText || "(no result text)",
|
|
4802
|
+
"",
|
|
4803
|
+
"Does the reported result plausibly satisfy EVERY acceptance criterion?",
|
|
4804
|
+
'Answer with exactly one line: "VERDICT: PASS" or "VERDICT: FAIL \u2014 <short reason>".'
|
|
4805
|
+
].join("\n")
|
|
4806
|
+
);
|
|
4807
|
+
} catch {
|
|
4808
|
+
return { ok: true };
|
|
4809
|
+
}
|
|
4810
|
+
const match = text.match(/VERDICT:\s*(PASS|FAIL)(?:\s*[—-]\s*(.*))?/i);
|
|
4811
|
+
if (!match) return { ok: true };
|
|
4812
|
+
if (match[1].toUpperCase() === "PASS") return { ok: true };
|
|
4813
|
+
return {
|
|
4814
|
+
ok: false,
|
|
4815
|
+
reason: `acceptance criteria not met: ${match[2]?.trim() || "judge rejected the result"}`
|
|
4816
|
+
};
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4465
4819
|
function makeCommandVerifier(options = {}) {
|
|
4466
4820
|
const metadataKey = options.metadataKey ?? "verificationCommand";
|
|
4467
4821
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
@@ -4469,8 +4823,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4469
4823
|
const cmd = info.task.metadata?.[metadataKey];
|
|
4470
4824
|
if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
|
|
4471
4825
|
return await new Promise((resolve) => {
|
|
4472
|
-
const
|
|
4473
|
-
const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4826
|
+
const [shell, ...shellArgs] = verificationShell(process.platform);
|
|
4474
4827
|
const child = spawn(shell, [...shellArgs, cmd], {
|
|
4475
4828
|
cwd: info.cwd,
|
|
4476
4829
|
shell: false,
|
|
@@ -4492,7 +4845,6 @@ function makeCommandVerifier(options = {}) {
|
|
|
4492
4845
|
});
|
|
4493
4846
|
child.on("error", (err) => {
|
|
4494
4847
|
clearTimeout(timer);
|
|
4495
|
-
if (timedOut) return;
|
|
4496
4848
|
resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
|
|
4497
4849
|
});
|
|
4498
4850
|
});
|
|
@@ -4500,10 +4852,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4500
4852
|
}
|
|
4501
4853
|
|
|
4502
4854
|
// src/decompose-task.ts
|
|
4503
|
-
import {
|
|
4504
|
-
readBundledInstructionText,
|
|
4505
|
-
renderInstructionTemplate
|
|
4506
|
-
} from "@wrongstack/core/utils";
|
|
4855
|
+
import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
|
|
4507
4856
|
var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
|
|
4508
4857
|
var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
4509
4858
|
function extractJsonArray(text) {
|
|
@@ -4527,48 +4876,144 @@ function buildPrompt(task, error, min, max) {
|
|
|
4527
4876
|
error: error || "(none recorded)"
|
|
4528
4877
|
});
|
|
4529
4878
|
}
|
|
4530
|
-
function
|
|
4879
|
+
function parseSubtaskSpecs(text, min, max, options = {}) {
|
|
4880
|
+
const json = extractJsonArray(text ?? "");
|
|
4881
|
+
if (!json) return [];
|
|
4882
|
+
let raw;
|
|
4883
|
+
try {
|
|
4884
|
+
raw = JSON.parse(json);
|
|
4885
|
+
} catch {
|
|
4886
|
+
return [];
|
|
4887
|
+
}
|
|
4888
|
+
const items = raw;
|
|
4889
|
+
const specs = [];
|
|
4890
|
+
for (const item of items) {
|
|
4891
|
+
if (!item || typeof item !== "object") continue;
|
|
4892
|
+
const r = item;
|
|
4893
|
+
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
4894
|
+
const description = typeof r["description"] === "string" ? r["description"].trim() : "";
|
|
4895
|
+
if (!title || !description) continue;
|
|
4896
|
+
const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
|
|
4897
|
+
const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
|
|
4898
|
+
const successCriterion = options.acceptSuccessCriterion && typeof r["successCriterion"] === "string" ? r["successCriterion"].trim() || void 0 : void 0;
|
|
4899
|
+
specs.push({ title, description, type, priority, successCriterion });
|
|
4900
|
+
if (specs.length >= max) break;
|
|
4901
|
+
}
|
|
4902
|
+
return specs.length >= min ? specs : [];
|
|
4903
|
+
}
|
|
4904
|
+
function makePlanningDecomposer(opts) {
|
|
4531
4905
|
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4532
|
-
const max = Math.max(min, opts.maxSubtasks ??
|
|
4533
|
-
return async function
|
|
4906
|
+
const max = Math.max(min, opts.maxSubtasks ?? 5);
|
|
4907
|
+
return async function decompose(info) {
|
|
4534
4908
|
let text;
|
|
4535
4909
|
try {
|
|
4536
|
-
text = await opts.run(
|
|
4910
|
+
text = await opts.run(
|
|
4911
|
+
renderInstructionTemplate(readBundledInstructionText("sdd/decompose-task-planning.md"), {
|
|
4912
|
+
minSubtasks: String(min),
|
|
4913
|
+
maxSubtasks: String(max),
|
|
4914
|
+
title: info.title,
|
|
4915
|
+
description: info.description,
|
|
4916
|
+
reasons: info.reasons.length ? info.reasons.map((r) => `- ${r}`).join("\n") : "- (unspecified)"
|
|
4917
|
+
})
|
|
4918
|
+
);
|
|
4537
4919
|
} catch {
|
|
4538
4920
|
return [];
|
|
4539
4921
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4922
|
+
return parseSubtaskSpecs(text, min, max, { acceptSuccessCriterion: true });
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
function makeLlmSubtaskGenerator(opts) {
|
|
4926
|
+
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4927
|
+
const max = Math.max(min, opts.maxSubtasks ?? 4);
|
|
4928
|
+
return async function generateSubtasks(info) {
|
|
4929
|
+
let text;
|
|
4543
4930
|
try {
|
|
4544
|
-
|
|
4931
|
+
text = await opts.run(buildPrompt(info.task, info.error, min, max));
|
|
4545
4932
|
} catch {
|
|
4546
4933
|
return [];
|
|
4547
4934
|
}
|
|
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 : [];
|
|
4935
|
+
return parseSubtaskSpecs(text, min, max);
|
|
4562
4936
|
};
|
|
4563
4937
|
}
|
|
4564
4938
|
|
|
4565
|
-
// src/
|
|
4566
|
-
import { readFile as readFile4, writeFile } from "node:fs/promises";
|
|
4567
|
-
import { join as join4, isAbsolute } from "node:path";
|
|
4939
|
+
// src/plan-decompose.ts
|
|
4568
4940
|
import {
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4941
|
+
assessAtomicity as assessAtomicity2
|
|
4942
|
+
} from "@wrongstack/kanban";
|
|
4943
|
+
function countAcceptanceCriteria(description) {
|
|
4944
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4945
|
+
if (marker === -1) return 0;
|
|
4946
|
+
const tail = description.slice(marker);
|
|
4947
|
+
return (tail.match(/^\s*-\s+\S/gm) ?? []).length;
|
|
4948
|
+
}
|
|
4949
|
+
function assessTaskNodeAtomicity(tracker, node, config) {
|
|
4950
|
+
const criteriaCount = countAcceptanceCriteria(node.description ?? "");
|
|
4951
|
+
const verificationCommand = node.metadata?.["verificationCommand"] ?? extractVerificationCommand([node.description ?? ""]);
|
|
4952
|
+
return assessAtomicity2(
|
|
4953
|
+
{
|
|
4954
|
+
title: node.title,
|
|
4955
|
+
description: node.description,
|
|
4956
|
+
estimatedHours: node.estimateHours,
|
|
4957
|
+
dependencyCount: tracker.getBlockers(node.id).length,
|
|
4958
|
+
successCriteriaCount: criteriaCount,
|
|
4959
|
+
hasVerifiableOutput: Boolean(verificationCommand),
|
|
4960
|
+
childCount: tracker.getAllNodes().filter((n) => n.parentId === node.id).length
|
|
4961
|
+
},
|
|
4962
|
+
config
|
|
4963
|
+
);
|
|
4964
|
+
}
|
|
4965
|
+
async function decomposeNonAtomicTasks(opts) {
|
|
4966
|
+
const maxDecompositions = Math.max(1, opts.maxDecompositions ?? 10);
|
|
4967
|
+
const result = { applied: [], proposals: [], flagged: [] };
|
|
4968
|
+
const nodes = opts.tracker.getAllNodes();
|
|
4969
|
+
const childCounts = /* @__PURE__ */ new Map();
|
|
4970
|
+
for (const node of nodes) {
|
|
4971
|
+
if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
|
|
4972
|
+
}
|
|
4973
|
+
const candidates = nodes.filter(
|
|
4974
|
+
(node) => node.status === "pending" && !childCounts.get(node.id)
|
|
4975
|
+
);
|
|
4976
|
+
let spent = 0;
|
|
4977
|
+
for (const node of candidates) {
|
|
4978
|
+
if (spent >= maxDecompositions) break;
|
|
4979
|
+
const assessment = assessTaskNodeAtomicity(opts.tracker, node, opts.config);
|
|
4980
|
+
opts.tracker.patchMetadata(node.id, {
|
|
4981
|
+
atomicity: {
|
|
4982
|
+
verdict: assessment.verdict,
|
|
4983
|
+
score: assessment.score,
|
|
4984
|
+
reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
|
|
4985
|
+
}
|
|
4986
|
+
});
|
|
4987
|
+
if (assessment.verdict !== "needs_decomposition") continue;
|
|
4988
|
+
result.flagged.push(node.id);
|
|
4989
|
+
spent += 1;
|
|
4990
|
+
const reasons = assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason);
|
|
4991
|
+
const subtasks = await opts.decompose({
|
|
4992
|
+
title: node.title,
|
|
4993
|
+
description: node.description ?? "",
|
|
4994
|
+
reasons
|
|
4995
|
+
});
|
|
4996
|
+
if (!subtasks.length) continue;
|
|
4997
|
+
if (opts.mode === "auto") {
|
|
4998
|
+
const subtaskIds = splitGraphNode(opts.tracker, node.id, subtasks);
|
|
4999
|
+
if (subtaskIds.length) result.applied.push({ nodeId: node.id, subtaskIds });
|
|
5000
|
+
} else {
|
|
5001
|
+
result.proposals.push({ nodeId: node.id, title: node.title, reasons, subtasks });
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
return result;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
// src/conflict-resolver.ts
|
|
5008
|
+
import { readFile as readFile5, writeFile } from "node:fs/promises";
|
|
5009
|
+
import { isAbsolute, join as join6 } from "node:path";
|
|
5010
|
+
import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
|
|
5011
|
+
var defaultFileIO = {
|
|
5012
|
+
read: (path6) => readFile5(path6, "utf8"),
|
|
5013
|
+
write: async (path6, content) => {
|
|
5014
|
+
await writeFile(path6, content, "utf8");
|
|
5015
|
+
}
|
|
5016
|
+
};
|
|
4572
5017
|
var START = "<<<<<<<";
|
|
4573
5018
|
var BASE = "|||||||";
|
|
4574
5019
|
var SEP = "=======";
|
|
@@ -4606,21 +5051,21 @@ function hasConflictMarkers(text) {
|
|
|
4606
5051
|
return m === START || m === SEP || m === END || m === BASE;
|
|
4607
5052
|
});
|
|
4608
5053
|
}
|
|
4609
|
-
function makePreferSideConflictResolver(side) {
|
|
5054
|
+
function makePreferSideConflictResolver(side, io = defaultFileIO) {
|
|
4610
5055
|
return async function conflictResolver(info) {
|
|
4611
5056
|
if (info.conflictFiles.length === 0) return false;
|
|
4612
5057
|
for (const rel of info.conflictFiles) {
|
|
4613
|
-
const abs = isAbsolute(rel) ? rel :
|
|
5058
|
+
const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
|
|
4614
5059
|
let content;
|
|
4615
5060
|
try {
|
|
4616
|
-
content = await
|
|
5061
|
+
content = await io.read(abs);
|
|
4617
5062
|
} catch {
|
|
4618
5063
|
return false;
|
|
4619
5064
|
}
|
|
4620
5065
|
const resolved = resolveConflictText(content, side);
|
|
4621
5066
|
if (hasConflictMarkers(resolved)) return false;
|
|
4622
5067
|
try {
|
|
4623
|
-
await
|
|
5068
|
+
await io.write(abs, resolved);
|
|
4624
5069
|
} catch {
|
|
4625
5070
|
return false;
|
|
4626
5071
|
}
|
|
@@ -4640,13 +5085,14 @@ function nonMarkerLineCount(text) {
|
|
|
4640
5085
|
}
|
|
4641
5086
|
function makeLlmConflictResolver(opts) {
|
|
4642
5087
|
const minFraction = opts.minRetainedFraction ?? 0.5;
|
|
5088
|
+
const io = opts.io ?? defaultFileIO;
|
|
4643
5089
|
return async function conflictResolver(info) {
|
|
4644
5090
|
if (info.conflictFiles.length === 0) return false;
|
|
4645
5091
|
for (const rel of info.conflictFiles) {
|
|
4646
|
-
const abs = isAbsolute(rel) ? rel :
|
|
5092
|
+
const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
|
|
4647
5093
|
let content;
|
|
4648
5094
|
try {
|
|
4649
|
-
content = await
|
|
5095
|
+
content = await io.read(abs);
|
|
4650
5096
|
} catch {
|
|
4651
5097
|
return false;
|
|
4652
5098
|
}
|
|
@@ -4670,7 +5116,7 @@ function makeLlmConflictResolver(opts) {
|
|
|
4670
5116
|
return false;
|
|
4671
5117
|
}
|
|
4672
5118
|
try {
|
|
4673
|
-
await
|
|
5119
|
+
await io.write(abs, resolved);
|
|
4674
5120
|
} catch {
|
|
4675
5121
|
return false;
|
|
4676
5122
|
}
|
|
@@ -4700,21 +5146,28 @@ export {
|
|
|
4700
5146
|
TaskTracker3 as TaskTracker,
|
|
4701
5147
|
analyzeCriticalPath,
|
|
4702
5148
|
applySddLifecycle,
|
|
5149
|
+
assessGeneratedTaskAtomicity,
|
|
5150
|
+
assessTaskNodeAtomicity,
|
|
4703
5151
|
buildBoardSnapshot,
|
|
4704
5152
|
buildBoardTasks,
|
|
4705
5153
|
cleanupSddWorktrees,
|
|
4706
5154
|
cleanupStaleSddWorktrees,
|
|
4707
5155
|
cleanupStaleWorktrees,
|
|
4708
5156
|
createAutoExecutor,
|
|
5157
|
+
decomposeNonAtomicTasks,
|
|
4709
5158
|
destroySddProject,
|
|
4710
5159
|
extractVerificationCommand,
|
|
5160
|
+
gatherProjectContext,
|
|
4711
5161
|
getTemplate,
|
|
4712
5162
|
hasConflictMarkers,
|
|
4713
5163
|
isExplanatoryText,
|
|
4714
5164
|
listTemplates,
|
|
5165
|
+
makeAcceptanceCriteriaVerifier,
|
|
4715
5166
|
makeCommandVerifier,
|
|
5167
|
+
makeCompositeVerifier,
|
|
4716
5168
|
makeLlmConflictResolver,
|
|
4717
5169
|
makeLlmSubtaskGenerator,
|
|
5170
|
+
makePlanningDecomposer,
|
|
4718
5171
|
makePreferSideConflictResolver,
|
|
4719
5172
|
renderProgress,
|
|
4720
5173
|
renderSpecAnalysis,
|
|
@@ -4723,6 +5176,7 @@ export {
|
|
|
4723
5176
|
resolveConflictText,
|
|
4724
5177
|
rollbackSddRunFromDisk,
|
|
4725
5178
|
shortIdMap,
|
|
5179
|
+
splitGraphNode,
|
|
4726
5180
|
startSddRun,
|
|
4727
5181
|
templateToMarkdown
|
|
4728
5182
|
};
|