@wrongstack/sdd 0.293.0 → 0.295.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-executor.d.ts +3 -4
- package/dist/auto-executor.d.ts.map +1 -1
- package/dist/board-types.d.ts +4 -0
- package/dist/board-types.d.ts.map +1 -1
- package/dist/conflict-resolver.d.ts +7 -1
- package/dist/conflict-resolver.d.ts.map +1 -1
- package/dist/critical-path.d.ts.map +1 -1
- package/dist/decompose-task.d.ts +21 -0
- package/dist/decompose-task.d.ts.map +1 -1
- package/dist/graph-split.d.ts +25 -0
- package/dist/graph-split.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +722 -268
- package/dist/index.js.map +4 -4
- package/dist/plan-decompose.d.ts +47 -0
- package/dist/plan-decompose.d.ts.map +1 -0
- package/dist/sdd-board-projector.d.ts +9 -6
- package/dist/sdd-board-projector.d.ts.map +1 -1
- package/dist/sdd-board-store.d.ts +39 -1
- package/dist/sdd-board-store.d.ts.map +1 -1
- package/dist/sdd-interview-driver.d.ts +2 -2
- package/dist/sdd-interview-driver.d.ts.map +1 -1
- package/dist/sdd-lifecycle.d.ts.map +1 -1
- package/dist/sdd-parallel-run.d.ts +11 -5
- package/dist/sdd-parallel-run.d.ts.map +1 -1
- package/dist/sdd-supervisor.d.ts +1 -1
- package/dist/sdd-supervisor.d.ts.map +1 -1
- package/dist/spec-builder.d.ts +1 -3
- package/dist/spec-builder.d.ts.map +1 -1
- package/dist/spec-parser.d.ts +1 -1
- package/dist/spec-parser.d.ts.map +1 -1
- package/dist/spec-versioning.d.ts +1 -2
- package/dist/spec-versioning.d.ts.map +1 -1
- package/dist/start-sdd-run.d.ts +11 -5
- 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-tracker.d.ts +1 -1
- package/dist/task-tracker.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";
|
|
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) {
|
|
@@ -549,7 +623,7 @@ var SpecDrivenDev = class {
|
|
|
549
623
|
import * as fsp from "node:fs/promises";
|
|
550
624
|
import * as path from "node:path";
|
|
551
625
|
import { randomUUID } from "node:crypto";
|
|
552
|
-
import { atomicWrite, ensureDir } from "@wrongstack/core";
|
|
626
|
+
import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
|
|
553
627
|
var SpecStore = class {
|
|
554
628
|
baseDir;
|
|
555
629
|
indexPath;
|
|
@@ -663,7 +737,7 @@ var SpecStore = class {
|
|
|
663
737
|
// src/task-graph-store.ts
|
|
664
738
|
import * as fsp2 from "node:fs/promises";
|
|
665
739
|
import * as path2 from "node:path";
|
|
666
|
-
import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core";
|
|
740
|
+
import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
|
|
667
741
|
function graphToJSON(graph) {
|
|
668
742
|
const serialisable = {
|
|
669
743
|
...graph,
|
|
@@ -762,7 +836,7 @@ var TaskGraphStore = class {
|
|
|
762
836
|
};
|
|
763
837
|
|
|
764
838
|
// src/board-types.ts
|
|
765
|
-
import { computeTaskProgress } from "@wrongstack/core/
|
|
839
|
+
import { computeTaskProgress } from "@wrongstack/core/tasking";
|
|
766
840
|
function shortIdMap(graph) {
|
|
767
841
|
const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
|
|
768
842
|
const m = /* @__PURE__ */ new Map();
|
|
@@ -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) {
|
|
@@ -854,13 +930,36 @@ function buildBoardSnapshot(graph, run, now) {
|
|
|
854
930
|
// src/sdd-board-store.ts
|
|
855
931
|
import * as fsp3 from "node:fs/promises";
|
|
856
932
|
import * as path3 from "node:path";
|
|
857
|
-
import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core";
|
|
933
|
+
import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock } from "@wrongstack/core/utils";
|
|
934
|
+
var DEFAULT_EVENT_MAX_BYTES = 16 * 1024 * 1024;
|
|
935
|
+
var DEFAULT_EVENT_KEEP_BYTES = 8 * 1024 * 1024;
|
|
936
|
+
var DEFAULT_EVENT_SIZE_CHECK_EVERY = 100;
|
|
858
937
|
var SddBoardStore = class {
|
|
859
938
|
baseDir;
|
|
860
939
|
indexPath;
|
|
940
|
+
eventMaxBytes;
|
|
941
|
+
eventKeepBytes;
|
|
942
|
+
eventSizeCheckEvery;
|
|
943
|
+
controlFileIO;
|
|
944
|
+
eventChains = /* @__PURE__ */ new Map();
|
|
945
|
+
eventWritesSinceCheck = /* @__PURE__ */ new Map();
|
|
946
|
+
controlDrains = /* @__PURE__ */ new Map();
|
|
947
|
+
baseDirReady;
|
|
948
|
+
cachedIndex;
|
|
949
|
+
cachedIndexSignature = null;
|
|
861
950
|
constructor(opts) {
|
|
862
951
|
this.baseDir = opts.baseDir;
|
|
863
952
|
this.indexPath = path3.join(this.baseDir, "_index.json");
|
|
953
|
+
this.eventMaxBytes = Math.max(1024, Math.floor(opts.eventMaxBytes ?? DEFAULT_EVENT_MAX_BYTES));
|
|
954
|
+
this.eventKeepBytes = Math.min(
|
|
955
|
+
this.eventMaxBytes,
|
|
956
|
+
Math.max(0, Math.floor(opts.eventKeepBytes ?? DEFAULT_EVENT_KEEP_BYTES))
|
|
957
|
+
);
|
|
958
|
+
this.eventSizeCheckEvery = Math.max(
|
|
959
|
+
1,
|
|
960
|
+
Math.floor(opts.eventSizeCheckEvery ?? DEFAULT_EVENT_SIZE_CHECK_EVERY)
|
|
961
|
+
);
|
|
962
|
+
this.controlFileIO = opts.controlFileIO ?? fsp3;
|
|
864
963
|
}
|
|
865
964
|
snapshotPath(runId) {
|
|
866
965
|
return path3.join(this.baseDir, `${this.safe(runId)}.json`);
|
|
@@ -872,7 +971,7 @@ var SddBoardStore = class {
|
|
|
872
971
|
return path3.join(this.baseDir, `${this.safe(runId)}.control.jsonl`);
|
|
873
972
|
}
|
|
874
973
|
async saveSnapshot(snapshot) {
|
|
875
|
-
await
|
|
974
|
+
await this.ensureBaseDir();
|
|
876
975
|
await atomicWrite3(this.snapshotPath(snapshot.runId), JSON.stringify(snapshot, null, 2), {
|
|
877
976
|
mode: 384
|
|
878
977
|
});
|
|
@@ -888,7 +987,12 @@ var SddBoardStore = class {
|
|
|
888
987
|
}
|
|
889
988
|
async list() {
|
|
890
989
|
const index = await this.readIndex();
|
|
891
|
-
return index.entries.
|
|
990
|
+
return index.entries.map((entry) => ({ ...entry }));
|
|
991
|
+
}
|
|
992
|
+
/** Latest board metadata without cloning/sorting the complete index. */
|
|
993
|
+
async latest() {
|
|
994
|
+
const entry = (await this.readIndex()).entries[0];
|
|
995
|
+
return entry ? { ...entry } : void 0;
|
|
892
996
|
}
|
|
893
997
|
async loadLatestForSpec(specId) {
|
|
894
998
|
const entry = (await this.list()).find((e) => e.specId === specId);
|
|
@@ -896,63 +1000,157 @@ var SddBoardStore = class {
|
|
|
896
1000
|
}
|
|
897
1001
|
/** Append one line to the board's JSONL event log (best-effort, never throws). */
|
|
898
1002
|
async appendEvent(runId, event) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1003
|
+
const filePath = this.eventsPath(runId);
|
|
1004
|
+
const previous = this.eventChains.get(filePath) ?? Promise.resolve();
|
|
1005
|
+
const write = previous.then(() => this.appendEventInternal(filePath, event)).catch(() => void 0);
|
|
1006
|
+
this.eventChains.set(filePath, write);
|
|
1007
|
+
await write;
|
|
1008
|
+
if (this.eventChains.get(filePath) === write) this.eventChains.delete(filePath);
|
|
905
1009
|
}
|
|
906
1010
|
/** Append a control command (used by readers to steer a CLI-owned run). */
|
|
907
1011
|
async appendControl(runId, command) {
|
|
908
|
-
await
|
|
909
|
-
|
|
910
|
-
|
|
1012
|
+
await this.ensureBaseDir();
|
|
1013
|
+
const filePath = this.controlPath(runId);
|
|
1014
|
+
await withFileLock(
|
|
1015
|
+
filePath,
|
|
1016
|
+
() => fsp3.appendFile(filePath, `${JSON.stringify(command)}
|
|
1017
|
+
`, { mode: 384 })
|
|
1018
|
+
);
|
|
911
1019
|
}
|
|
912
1020
|
/** Read + truncate the control queue (the run drains it). Returns parsed commands. */
|
|
913
1021
|
async drainControl(runId) {
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
} catch {
|
|
1022
|
+
const filePath = this.controlPath(runId);
|
|
1023
|
+
const active = this.controlDrains.get(filePath);
|
|
1024
|
+
if (active) {
|
|
1025
|
+
await active;
|
|
919
1026
|
return [];
|
|
920
1027
|
}
|
|
1028
|
+
const drain = this.drainControlInternal(filePath);
|
|
1029
|
+
this.controlDrains.set(filePath, drain);
|
|
921
1030
|
try {
|
|
922
|
-
await
|
|
923
|
-
}
|
|
1031
|
+
return await drain;
|
|
1032
|
+
} finally {
|
|
1033
|
+
this.controlDrains.delete(filePath);
|
|
924
1034
|
}
|
|
925
|
-
return raw.split("\n").filter((l) => l.trim()).map((l) => {
|
|
926
|
-
try {
|
|
927
|
-
return JSON.parse(l);
|
|
928
|
-
} catch {
|
|
929
|
-
return null;
|
|
930
|
-
}
|
|
931
|
-
}).filter((c) => c !== null);
|
|
932
1035
|
}
|
|
933
1036
|
async delete(runId) {
|
|
1037
|
+
const eventPath = this.eventsPath(runId);
|
|
1038
|
+
await this.eventChains.get(eventPath);
|
|
1039
|
+
this.eventChains.delete(eventPath);
|
|
934
1040
|
await Promise.allSettled([
|
|
935
1041
|
fsp3.unlink(this.snapshotPath(runId)),
|
|
936
|
-
fsp3.unlink(
|
|
1042
|
+
fsp3.unlink(eventPath),
|
|
937
1043
|
fsp3.unlink(this.controlPath(runId))
|
|
938
1044
|
]);
|
|
1045
|
+
this.eventWritesSinceCheck.delete(eventPath);
|
|
939
1046
|
await this.removeFromIndex(runId);
|
|
940
1047
|
}
|
|
941
1048
|
// ── internal ────────────────────────────────────────────────────────────
|
|
942
1049
|
safe(runId) {
|
|
943
1050
|
return runId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
944
1051
|
}
|
|
1052
|
+
async ensureBaseDir() {
|
|
1053
|
+
this.baseDirReady ??= ensureDir3(this.baseDir).catch((error) => {
|
|
1054
|
+
this.baseDirReady = void 0;
|
|
1055
|
+
throw error;
|
|
1056
|
+
});
|
|
1057
|
+
await this.baseDirReady;
|
|
1058
|
+
}
|
|
1059
|
+
async appendEventInternal(filePath, event) {
|
|
1060
|
+
await this.ensureBaseDir();
|
|
1061
|
+
await fsp3.appendFile(filePath, `${JSON.stringify(event)}
|
|
1062
|
+
`, { mode: 384 });
|
|
1063
|
+
const writes = (this.eventWritesSinceCheck.get(filePath) ?? 0) + 1;
|
|
1064
|
+
if (writes < this.eventSizeCheckEvery) {
|
|
1065
|
+
this.eventWritesSinceCheck.set(filePath, writes);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
this.eventWritesSinceCheck.set(filePath, 0);
|
|
1069
|
+
const stat2 = await fsp3.stat(filePath);
|
|
1070
|
+
if (stat2.size <= this.eventMaxBytes) return;
|
|
1071
|
+
await this.compactEventTail(filePath, stat2.size);
|
|
1072
|
+
}
|
|
1073
|
+
async compactEventTail(filePath, size) {
|
|
1074
|
+
if (this.eventKeepBytes === 0) {
|
|
1075
|
+
await atomicWrite3(filePath, "", { mode: 384 });
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const handle = await fsp3.open(filePath, "r");
|
|
1079
|
+
let retained;
|
|
1080
|
+
try {
|
|
1081
|
+
const length = Math.min(size, this.eventKeepBytes);
|
|
1082
|
+
const start = size - length;
|
|
1083
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
1084
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
1085
|
+
retained = buffer.subarray(0, bytesRead);
|
|
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);
|
|
1090
|
+
}
|
|
1091
|
+
} finally {
|
|
1092
|
+
await handle.close();
|
|
1093
|
+
}
|
|
1094
|
+
await atomicWrite3(filePath, retained, { mode: 384 });
|
|
1095
|
+
}
|
|
1096
|
+
async drainControlInternal(filePath) {
|
|
1097
|
+
try {
|
|
1098
|
+
const stat2 = await this.controlFileIO.stat(filePath);
|
|
1099
|
+
if (stat2.size === 0) return [];
|
|
1100
|
+
} catch {
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
return withFileLock(filePath, async () => {
|
|
1104
|
+
let raw;
|
|
1105
|
+
try {
|
|
1106
|
+
const stat2 = await this.controlFileIO.stat(filePath);
|
|
1107
|
+
if (stat2.size === 0) return [];
|
|
1108
|
+
raw = await this.controlFileIO.readFile(filePath, "utf8");
|
|
1109
|
+
} catch {
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
await this.controlFileIO.truncate(filePath, 0);
|
|
1114
|
+
} catch {
|
|
1115
|
+
return [];
|
|
1116
|
+
}
|
|
1117
|
+
return raw.split("\n").filter((line) => line.trim()).map((line) => {
|
|
1118
|
+
try {
|
|
1119
|
+
return JSON.parse(line);
|
|
1120
|
+
} catch {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
}).filter(
|
|
1124
|
+
(command) => command !== null
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
945
1128
|
async readIndex() {
|
|
1129
|
+
const signature = await this.indexSignature();
|
|
1130
|
+
if (this.cachedIndex && sameIndexSignature(signature, this.cachedIndexSignature)) {
|
|
1131
|
+
return this.cachedIndex;
|
|
1132
|
+
}
|
|
946
1133
|
try {
|
|
947
1134
|
const raw = await fsp3.readFile(this.indexPath, "utf8");
|
|
948
1135
|
const parsed = JSON.parse(raw);
|
|
949
|
-
if (parsed?.version === 1)
|
|
1136
|
+
if (parsed?.version === 1) {
|
|
1137
|
+
parsed.entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1138
|
+
this.cachedIndex = parsed;
|
|
1139
|
+
this.cachedIndexSignature = signature;
|
|
1140
|
+
return parsed;
|
|
1141
|
+
}
|
|
950
1142
|
} catch {
|
|
951
1143
|
}
|
|
952
|
-
|
|
1144
|
+
this.cachedIndex = { version: 1, entries: [] };
|
|
1145
|
+
this.cachedIndexSignature = signature;
|
|
1146
|
+
return this.cachedIndex;
|
|
953
1147
|
}
|
|
954
1148
|
async updateIndex(snapshot) {
|
|
955
|
-
const
|
|
1149
|
+
const current = await this.readIndex();
|
|
1150
|
+
const index = {
|
|
1151
|
+
version: 1,
|
|
1152
|
+
entries: current.entries.map((entry2) => ({ ...entry2 }))
|
|
1153
|
+
};
|
|
956
1154
|
const entry = {
|
|
957
1155
|
runId: snapshot.runId,
|
|
958
1156
|
specId: snapshot.specId,
|
|
@@ -965,17 +1163,37 @@ var SddBoardStore = class {
|
|
|
965
1163
|
const idx = index.entries.findIndex((e) => e.runId === snapshot.runId);
|
|
966
1164
|
if (idx >= 0) index.entries[idx] = entry;
|
|
967
1165
|
else index.entries.push(entry);
|
|
1166
|
+
index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
968
1167
|
await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1168
|
+
this.cachedIndex = index;
|
|
1169
|
+
this.cachedIndexSignature = await this.indexSignature();
|
|
969
1170
|
}
|
|
970
1171
|
async removeFromIndex(runId) {
|
|
971
|
-
const
|
|
972
|
-
index
|
|
1172
|
+
const current = await this.readIndex();
|
|
1173
|
+
const index = {
|
|
1174
|
+
version: 1,
|
|
1175
|
+
entries: current.entries.filter((entry) => entry.runId !== runId).map((entry) => ({ ...entry }))
|
|
1176
|
+
};
|
|
973
1177
|
await atomicWrite3(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
|
|
1178
|
+
this.cachedIndex = index;
|
|
1179
|
+
this.cachedIndexSignature = await this.indexSignature();
|
|
1180
|
+
}
|
|
1181
|
+
async indexSignature() {
|
|
1182
|
+
try {
|
|
1183
|
+
const stat2 = await fsp3.stat(this.indexPath);
|
|
1184
|
+
return { size: stat2.size, mtimeMs: stat2.mtimeMs, ctimeMs: stat2.ctimeMs };
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
974
1188
|
}
|
|
975
1189
|
};
|
|
1190
|
+
function sameIndexSignature(a, b) {
|
|
1191
|
+
if (a === null || b === null) return a === b;
|
|
1192
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
1193
|
+
}
|
|
976
1194
|
|
|
977
1195
|
// src/sdd-board-projector.ts
|
|
978
|
-
import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
1196
|
+
import { DefaultSecretScrubber } from "@wrongstack/core/security";
|
|
979
1197
|
function summarizeToolInput(input, scrubber) {
|
|
980
1198
|
if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
|
|
981
1199
|
const record = input;
|
|
@@ -1017,11 +1235,12 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1017
1235
|
mergedCommits = [];
|
|
1018
1236
|
/** Base branch reported by the run at start (overrides the constructor option). */
|
|
1019
1237
|
runBaseBranch;
|
|
1020
|
-
dirty = false;
|
|
1021
1238
|
timer = null;
|
|
1022
1239
|
unsubs = [];
|
|
1023
|
-
/**
|
|
1024
|
-
|
|
1240
|
+
/** Latest snapshot waiting behind an in-flight disk write. */
|
|
1241
|
+
pendingSnapshot;
|
|
1242
|
+
/** At most one persistence loop runs; intermediate snapshots are coalesced. */
|
|
1243
|
+
saveLoop;
|
|
1025
1244
|
constructor(opts) {
|
|
1026
1245
|
this.o = opts;
|
|
1027
1246
|
this.now = opts.now ?? Date.now;
|
|
@@ -1044,7 +1263,11 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1044
1263
|
});
|
|
1045
1264
|
this.onRun("sdd.wave", (e) => {
|
|
1046
1265
|
this.wave = e.wave;
|
|
1047
|
-
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
|
+
});
|
|
1048
1271
|
this.markDirty();
|
|
1049
1272
|
});
|
|
1050
1273
|
this.onRun("sdd.deadlock", (e) => {
|
|
@@ -1052,7 +1275,11 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1052
1275
|
blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
|
|
1053
1276
|
blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
|
|
1054
1277
|
}));
|
|
1055
|
-
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
|
+
});
|
|
1056
1283
|
this.markDirty();
|
|
1057
1284
|
});
|
|
1058
1285
|
this.onRun("sdd.task.started", (e) => {
|
|
@@ -1232,7 +1459,8 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1232
1459
|
}
|
|
1233
1460
|
pushFeed(entry) {
|
|
1234
1461
|
this.feed.unshift(entry);
|
|
1235
|
-
if (this.feed.length > _SddBoardProjector.FEED_CAP)
|
|
1462
|
+
if (this.feed.length > _SddBoardProjector.FEED_CAP)
|
|
1463
|
+
this.feed.length = _SddBoardProjector.FEED_CAP;
|
|
1236
1464
|
if (entry.taskId) {
|
|
1237
1465
|
const taskFeed = this.taskEvents.get(entry.taskId) ?? [];
|
|
1238
1466
|
taskFeed.unshift(entry);
|
|
@@ -1257,7 +1485,7 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1257
1485
|
}
|
|
1258
1486
|
/** Resolve once all in-flight snapshot persistence has settled. */
|
|
1259
1487
|
async drain() {
|
|
1260
|
-
await this.
|
|
1488
|
+
while (this.saveLoop) await this.saveLoop;
|
|
1261
1489
|
}
|
|
1262
1490
|
/** Stop projecting and release subscriptions. */
|
|
1263
1491
|
dispose() {
|
|
@@ -1324,15 +1552,13 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1324
1552
|
return snap;
|
|
1325
1553
|
}
|
|
1326
1554
|
markDirty() {
|
|
1327
|
-
this.dirty = true;
|
|
1328
1555
|
if (this.timer || this.finished) return;
|
|
1329
1556
|
this.timer = setTimeout(() => {
|
|
1330
1557
|
this.timer = null;
|
|
1331
|
-
|
|
1558
|
+
this.flush();
|
|
1332
1559
|
}, this.throttleMs);
|
|
1333
1560
|
}
|
|
1334
1561
|
flush() {
|
|
1335
|
-
this.dirty = false;
|
|
1336
1562
|
if (this.timer) {
|
|
1337
1563
|
clearTimeout(this.timer);
|
|
1338
1564
|
this.timer = null;
|
|
@@ -1345,8 +1571,23 @@ var SddBoardProjector = class _SddBoardProjector {
|
|
|
1345
1571
|
snapshot: snap
|
|
1346
1572
|
});
|
|
1347
1573
|
if (this.o.store) {
|
|
1348
|
-
|
|
1349
|
-
this.
|
|
1574
|
+
this.pendingSnapshot = snap;
|
|
1575
|
+
this.startSaveLoop(this.o.store);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
startSaveLoop(store) {
|
|
1579
|
+
if (this.saveLoop) return;
|
|
1580
|
+
const loop = this.persistPendingSnapshots(store);
|
|
1581
|
+
this.saveLoop = loop;
|
|
1582
|
+
void loop.finally(() => {
|
|
1583
|
+
this.saveLoop = void 0;
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
async persistPendingSnapshots(store) {
|
|
1587
|
+
while (this.pendingSnapshot) {
|
|
1588
|
+
const snapshot = this.pendingSnapshot;
|
|
1589
|
+
this.pendingSnapshot = void 0;
|
|
1590
|
+
await store.saveSnapshot(snapshot).catch(() => {
|
|
1350
1591
|
});
|
|
1351
1592
|
}
|
|
1352
1593
|
}
|
|
@@ -1370,10 +1611,12 @@ var SddRunRegistry = class {
|
|
|
1370
1611
|
}
|
|
1371
1612
|
};
|
|
1372
1613
|
|
|
1614
|
+
// src/sdd-interview-driver.ts
|
|
1615
|
+
import { DefaultTaskStore as DefaultTaskStore2, TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
|
|
1616
|
+
|
|
1373
1617
|
// src/spec-builder.ts
|
|
1374
|
-
import {
|
|
1375
|
-
import { toErrorMessage } from "@wrongstack/core";
|
|
1376
|
-
import { SddError as SddError2, ERROR_CODES as ERROR_CODES2 } from "@wrongstack/core";
|
|
1618
|
+
import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
|
|
1619
|
+
import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
|
|
1377
1620
|
function buildQuestioningPrompt(session, min, max) {
|
|
1378
1621
|
const answered = session.answers.length;
|
|
1379
1622
|
const remaining = Math.max(0, min - answered);
|
|
@@ -1422,7 +1665,7 @@ function buildQuestioningPrompt(session, min, max) {
|
|
|
1422
1665
|
if (answered > 0) {
|
|
1423
1666
|
lines.push("", "**Conversation so far:**");
|
|
1424
1667
|
for (let i = 0; i < answered; i++) {
|
|
1425
|
-
const a =
|
|
1668
|
+
const a = expectDefined2(session.answers[i]);
|
|
1426
1669
|
lines.push(``, `Q${i + 1}: ${a.question}`, `A${i + 1}: ${a.answer}`);
|
|
1427
1670
|
}
|
|
1428
1671
|
}
|
|
@@ -1595,7 +1838,7 @@ var AISpecBuilder = class {
|
|
|
1595
1838
|
try {
|
|
1596
1839
|
const fsp5 = await import("node:fs/promises");
|
|
1597
1840
|
const path4 = await import("node:path");
|
|
1598
|
-
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core");
|
|
1841
|
+
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
|
|
1599
1842
|
await fsp5.mkdir(path4.dirname(this.sessionPath), { recursive: true });
|
|
1600
1843
|
await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
1601
1844
|
} catch {
|
|
@@ -1625,17 +1868,9 @@ var AISpecBuilder = class {
|
|
|
1625
1868
|
} catch {
|
|
1626
1869
|
}
|
|
1627
1870
|
}
|
|
1628
|
-
/** Auto-save helper
|
|
1629
|
-
* Failures are surfaced via process.emitWarning so a persistent
|
|
1630
|
-
* ENOSPC / EACCES doesn't silently strand session edits in memory. */
|
|
1871
|
+
/** Auto-save helper. saveSession() already handles best-effort persistence. */
|
|
1631
1872
|
autoSave() {
|
|
1632
|
-
this.saveSession()
|
|
1633
|
-
const detail = toErrorMessage(err);
|
|
1634
|
-
process.emitWarning(
|
|
1635
|
-
`SpecBuilder autoSave failed: ${detail}`,
|
|
1636
|
-
"SpecBuilderWarning"
|
|
1637
|
-
);
|
|
1638
|
-
});
|
|
1873
|
+
void this.saveSession();
|
|
1639
1874
|
}
|
|
1640
1875
|
// ── Session Lifecycle ─────────────────────────────────────────────────────
|
|
1641
1876
|
/** Start a new session with a title and optional intent. */
|
|
@@ -1809,7 +2044,7 @@ var AISpecBuilder = class {
|
|
|
1809
2044
|
message: "Invalid JSON for spec",
|
|
1810
2045
|
code: ERROR_CODES2.SDD_PARSE_FAILED,
|
|
1811
2046
|
cause: e,
|
|
1812
|
-
context: { detail: e
|
|
2047
|
+
context: { detail: toErrorMessage(e) }
|
|
1813
2048
|
});
|
|
1814
2049
|
}
|
|
1815
2050
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -1821,7 +2056,7 @@ var AISpecBuilder = class {
|
|
|
1821
2056
|
}
|
|
1822
2057
|
const raw = parsed;
|
|
1823
2058
|
const now = Date.now();
|
|
1824
|
-
const title = String(raw.title ?? this.session.title
|
|
2059
|
+
const title = String(raw.title ?? this.session.title);
|
|
1825
2060
|
const overview = String(raw.overview ?? "");
|
|
1826
2061
|
if (!overview || overview === "undefined") {
|
|
1827
2062
|
throw new SddError2({
|
|
@@ -1832,7 +2067,15 @@ var AISpecBuilder = class {
|
|
|
1832
2067
|
}
|
|
1833
2068
|
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
1834
2069
|
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
1835
|
-
type: [
|
|
2070
|
+
type: [
|
|
2071
|
+
"overview",
|
|
2072
|
+
"requirements",
|
|
2073
|
+
"architecture",
|
|
2074
|
+
"api",
|
|
2075
|
+
"data",
|
|
2076
|
+
"security",
|
|
2077
|
+
"acceptance"
|
|
2078
|
+
].includes(String(s.type)) ? String(s.type) : "overview",
|
|
1836
2079
|
title: String(s.title ?? ""),
|
|
1837
2080
|
content: String(s.content ?? ""),
|
|
1838
2081
|
level: Number(s.level) || 1
|
|
@@ -1840,7 +2083,9 @@ var AISpecBuilder = class {
|
|
|
1840
2083
|
const rawReqs = Array.isArray(raw.requirements) ? raw.requirements : [];
|
|
1841
2084
|
const requirements = rawReqs.filter((r) => r && typeof r === "object").map((r, i) => ({
|
|
1842
2085
|
id: String(r.id ?? `REQ-${i + 1}`),
|
|
1843
|
-
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2086
|
+
type: ["functional", "non-functional", "security", "performance", "ux"].includes(
|
|
2087
|
+
String(r.type)
|
|
2088
|
+
) ? String(r.type) : "functional",
|
|
1844
2089
|
priority: ["critical", "high", "medium", "low"].includes(String(r.priority)) ? String(r.priority) : "medium",
|
|
1845
2090
|
description: String(r.description ?? ""),
|
|
1846
2091
|
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria.map(String) : []
|
|
@@ -1929,7 +2174,6 @@ var AISpecBuilder = class {
|
|
|
1929
2174
|
};
|
|
1930
2175
|
|
|
1931
2176
|
// src/sdd-interview-driver.ts
|
|
1932
|
-
import { TaskTracker as TaskTracker2, DefaultTaskStore as DefaultTaskStore2 } from "@wrongstack/core/tasking";
|
|
1933
2177
|
var SddInterviewDriver = class {
|
|
1934
2178
|
builder;
|
|
1935
2179
|
o;
|
|
@@ -2142,16 +2386,17 @@ var SddInterviewDriver = class {
|
|
|
2142
2386
|
);
|
|
2143
2387
|
if (valid.length === 0) return void 0;
|
|
2144
2388
|
const spec = this.builder.getSession().spec;
|
|
2145
|
-
if (!
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
this.
|
|
2149
|
-
this.tracker = tracker;
|
|
2389
|
+
if (!this.tracker) {
|
|
2390
|
+
const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
|
|
2391
|
+
this.graph = await tracker2.createGraph(spec.id, spec.title);
|
|
2392
|
+
this.tracker = tracker2;
|
|
2150
2393
|
}
|
|
2394
|
+
const tracker = this.tracker;
|
|
2395
|
+
const graph = this.graph;
|
|
2151
2396
|
const refMap = /* @__PURE__ */ new Map();
|
|
2152
2397
|
const created = [];
|
|
2153
2398
|
valid.forEach((task, i) => {
|
|
2154
|
-
const node = addTaskToTracker(
|
|
2399
|
+
const node = addTaskToTracker(tracker, task);
|
|
2155
2400
|
created.push({ nodeId: node.id, task });
|
|
2156
2401
|
if (typeof task.id === "string" && task.id.trim()) {
|
|
2157
2402
|
refMap.set(task.id.trim().toLowerCase(), node.id);
|
|
@@ -2164,13 +2409,13 @@ var SddInterviewDriver = class {
|
|
|
2164
2409
|
const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
|
|
2165
2410
|
for (const ref of deps) {
|
|
2166
2411
|
const depId = refMap.get(normalizeTaskRef(String(ref)));
|
|
2167
|
-
if (depId && depId !== nodeId)
|
|
2412
|
+
if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
|
|
2168
2413
|
}
|
|
2169
2414
|
}
|
|
2170
|
-
await this.persistGraph(
|
|
2171
|
-
this.builder.setTaskGraphId(
|
|
2415
|
+
await this.persistGraph(graph);
|
|
2416
|
+
this.builder.setTaskGraphId(graph.id);
|
|
2172
2417
|
await this.builder.saveSession();
|
|
2173
|
-
return
|
|
2418
|
+
return graph.id;
|
|
2174
2419
|
}
|
|
2175
2420
|
};
|
|
2176
2421
|
var TASK_TYPES = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
|
|
@@ -2195,14 +2440,51 @@ function isExplanatoryText(text) {
|
|
|
2195
2440
|
}
|
|
2196
2441
|
|
|
2197
2442
|
// src/start-sdd-run.ts
|
|
2198
|
-
import { TOKENS } from "@wrongstack/core";
|
|
2443
|
+
import { TOKENS } from "@wrongstack/core/kernel";
|
|
2199
2444
|
|
|
2200
2445
|
// src/sdd-parallel-run.ts
|
|
2201
|
-
import { expectDefined as
|
|
2446
|
+
import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
|
|
2202
2447
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2203
|
-
import {
|
|
2204
|
-
|
|
2205
|
-
|
|
2448
|
+
import {
|
|
2449
|
+
assignNickname,
|
|
2450
|
+
DefaultMultiAgentCoordinator,
|
|
2451
|
+
makeAgentSubagentRunner,
|
|
2452
|
+
withDisabledToolFiltering
|
|
2453
|
+
} from "@wrongstack/core/coordination";
|
|
2454
|
+
import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
|
|
2455
|
+
|
|
2456
|
+
// src/graph-split.ts
|
|
2457
|
+
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
2458
|
+
const node = tracker.getNode(taskId);
|
|
2459
|
+
if (!node) return [];
|
|
2460
|
+
if (node.status === "in_progress" || options.isRunning?.(taskId)) return [];
|
|
2461
|
+
if (!subtasks.length) return [];
|
|
2462
|
+
const blockers = tracker.getBlockers(taskId);
|
|
2463
|
+
const dependents = tracker.getDependents(taskId);
|
|
2464
|
+
const leafIds = subtasks.map((s) => {
|
|
2465
|
+
const criterion = s.successCriterion?.trim();
|
|
2466
|
+
const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
|
|
2467
|
+
const description = criterion && !verificationCommand ? `${s.description}
|
|
2468
|
+
|
|
2469
|
+
**Acceptance Criteria:**
|
|
2470
|
+
- ${criterion}` : s.description;
|
|
2471
|
+
return tracker.addNode({
|
|
2472
|
+
title: s.title,
|
|
2473
|
+
description,
|
|
2474
|
+
type: s.type ?? node.type,
|
|
2475
|
+
priority: s.priority ?? node.priority,
|
|
2476
|
+
status: "pending",
|
|
2477
|
+
parentId: taskId,
|
|
2478
|
+
...verificationCommand ? { metadata: { verificationCommand } } : {}
|
|
2479
|
+
}).id;
|
|
2480
|
+
});
|
|
2481
|
+
for (const leaf of leafIds) {
|
|
2482
|
+
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
2483
|
+
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
2484
|
+
}
|
|
2485
|
+
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2486
|
+
return leafIds;
|
|
2487
|
+
}
|
|
2206
2488
|
|
|
2207
2489
|
// src/sdd-task-decomposer.ts
|
|
2208
2490
|
var SddTaskDecomposer = class {
|
|
@@ -2326,7 +2608,9 @@ var SddParallelRun = class {
|
|
|
2326
2608
|
this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
|
|
2327
2609
|
this.maxWallClockMs = opts.maxWallClockMs;
|
|
2328
2610
|
this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
|
|
2329
|
-
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2611
|
+
this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, {
|
|
2612
|
+
parallelSlots: this.slots
|
|
2613
|
+
});
|
|
2330
2614
|
}
|
|
2331
2615
|
opts;
|
|
2332
2616
|
slots;
|
|
@@ -2449,7 +2733,8 @@ var SddParallelRun = class {
|
|
|
2449
2733
|
* revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
|
|
2450
2734
|
*/
|
|
2451
2735
|
async rollback() {
|
|
2452
|
-
if (this.isRunning())
|
|
2736
|
+
if (this.isRunning())
|
|
2737
|
+
return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
|
|
2453
2738
|
const wt = this.opts.worktrees;
|
|
2454
2739
|
if (!wt || !this.baseBranch) {
|
|
2455
2740
|
return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
|
|
@@ -2482,7 +2767,10 @@ var SddParallelRun = class {
|
|
|
2482
2767
|
*/
|
|
2483
2768
|
setTaskModel(taskId, model, provider) {
|
|
2484
2769
|
if (!this.opts.tracker.getNode(taskId)) return false;
|
|
2485
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
2770
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
2771
|
+
model,
|
|
2772
|
+
...provider !== void 0 ? { provider } : {}
|
|
2773
|
+
});
|
|
2486
2774
|
return true;
|
|
2487
2775
|
}
|
|
2488
2776
|
/** Set/override a task's fallback model chain (applied on its next dispatch). */
|
|
@@ -2514,7 +2802,12 @@ var SddParallelRun = class {
|
|
|
2514
2802
|
this.cancelledTasks.add(taskId);
|
|
2515
2803
|
this.opts.tracker.patchMetadata(taskId, { cancelled: true });
|
|
2516
2804
|
this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
|
|
2517
|
-
this.emit("sdd.task.failed", {
|
|
2805
|
+
this.emit("sdd.task.failed", {
|
|
2806
|
+
runId: this.runId,
|
|
2807
|
+
taskId,
|
|
2808
|
+
subagentId: "",
|
|
2809
|
+
error: "cancelled by user"
|
|
2810
|
+
});
|
|
2518
2811
|
const subagentId = this.taskSubagents.get(taskId);
|
|
2519
2812
|
if (subagentId && this.coordinator) {
|
|
2520
2813
|
await this.coordinator.stop(subagentId).catch(() => {
|
|
@@ -2545,30 +2838,12 @@ var SddParallelRun = class {
|
|
|
2545
2838
|
* The scheduler picks the new pending leaves up on its next dispatch pass.
|
|
2546
2839
|
*/
|
|
2547
2840
|
splitTask(taskId, subtasks) {
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
if (
|
|
2552
|
-
if (!subtasks.length) return [];
|
|
2553
|
-
const blockers = tracker.getBlockers(taskId);
|
|
2554
|
-
const dependents = tracker.getDependents(taskId);
|
|
2555
|
-
const leafIds = subtasks.map(
|
|
2556
|
-
(s) => tracker.addNode({
|
|
2557
|
-
title: s.title,
|
|
2558
|
-
description: s.description,
|
|
2559
|
-
type: s.type ?? node.type,
|
|
2560
|
-
priority: s.priority ?? node.priority,
|
|
2561
|
-
status: "pending",
|
|
2562
|
-
parentId: taskId
|
|
2563
|
-
}).id
|
|
2564
|
-
);
|
|
2565
|
-
for (const leaf of leafIds) {
|
|
2566
|
-
for (const b of blockers) tracker.addDependency(b, leaf);
|
|
2567
|
-
for (const dep of dependents) tracker.addDependency(leaf, dep);
|
|
2568
|
-
}
|
|
2841
|
+
const leafIds = splitGraphNode(this.opts.tracker, taskId, subtasks, {
|
|
2842
|
+
isRunning: (id) => this.taskSubagents.has(id)
|
|
2843
|
+
});
|
|
2844
|
+
if (!leafIds.length) return [];
|
|
2569
2845
|
this.retryMap.delete(taskId);
|
|
2570
2846
|
this.persistRetries(taskId, 0);
|
|
2571
|
-
tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
|
|
2572
2847
|
this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
|
|
2573
2848
|
return leafIds;
|
|
2574
2849
|
}
|
|
@@ -2616,7 +2891,12 @@ var SddParallelRun = class {
|
|
|
2616
2891
|
return await this.executeOne(task);
|
|
2617
2892
|
} catch (err) {
|
|
2618
2893
|
this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
|
|
2619
|
-
this.emit("sdd.task.failed", {
|
|
2894
|
+
this.emit("sdd.task.failed", {
|
|
2895
|
+
runId: this.runId,
|
|
2896
|
+
taskId: task.id,
|
|
2897
|
+
subagentId: "",
|
|
2898
|
+
error: String(err)
|
|
2899
|
+
});
|
|
2620
2900
|
return { taskId: task.id, success: false };
|
|
2621
2901
|
} finally {
|
|
2622
2902
|
running.delete(task.id);
|
|
@@ -2630,16 +2910,18 @@ var SddParallelRun = class {
|
|
|
2630
2910
|
await this.waitWhilePaused();
|
|
2631
2911
|
if (this.stopRequested) break;
|
|
2632
2912
|
let dispatchedThisRound = 0;
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
dispatchedThisRound++;
|
|
2639
|
-
}
|
|
2913
|
+
const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
|
|
2914
|
+
for (const task of ready) {
|
|
2915
|
+
if (running.size >= this.slots) break;
|
|
2916
|
+
dispatch(task);
|
|
2917
|
+
dispatchedThisRound++;
|
|
2640
2918
|
}
|
|
2641
2919
|
if (dispatchedThisRound > 0) {
|
|
2642
|
-
this.emit("sdd.wave", {
|
|
2920
|
+
this.emit("sdd.wave", {
|
|
2921
|
+
runId: this.runId,
|
|
2922
|
+
wave: this.round,
|
|
2923
|
+
batchSize: dispatchedThisRound
|
|
2924
|
+
});
|
|
2643
2925
|
this.round++;
|
|
2644
2926
|
}
|
|
2645
2927
|
if (running.size === 0) {
|
|
@@ -2670,7 +2952,6 @@ var SddParallelRun = class {
|
|
|
2670
2952
|
this.opts.onProgress?.(this.buildProgress());
|
|
2671
2953
|
}
|
|
2672
2954
|
}
|
|
2673
|
-
if (running.size > 0) await Promise.allSettled(running.values());
|
|
2674
2955
|
if (this.stopRequested) await this.teardown();
|
|
2675
2956
|
const finalProgress = this.opts.tracker.getProgress();
|
|
2676
2957
|
this.emit("sdd.run.finished", {
|
|
@@ -2868,10 +3149,11 @@ var SddParallelRun = class {
|
|
|
2868
3149
|
}
|
|
2869
3150
|
this.opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
2870
3151
|
await this.allocateWorktrees([task]);
|
|
2871
|
-
if (!this.coordinator)
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3152
|
+
if (!this.coordinator)
|
|
3153
|
+
throw new SddError3({
|
|
3154
|
+
message: "SDD parallel runner requires a coordinator",
|
|
3155
|
+
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3156
|
+
});
|
|
2875
3157
|
const coordinator = this.coordinator;
|
|
2876
3158
|
const subagentId = `sdd-d${this.dispatchSeq++}`;
|
|
2877
3159
|
const correlationId = randomUUID2();
|
|
@@ -2881,7 +3163,7 @@ var SddParallelRun = class {
|
|
|
2881
3163
|
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
|
|
2882
3164
|
const spawnResult = await coordinator.spawn({
|
|
2883
3165
|
id: subagentId,
|
|
2884
|
-
name: agentName
|
|
3166
|
+
name: agentName,
|
|
2885
3167
|
role: "executor",
|
|
2886
3168
|
// Idle reaper is always on; the hard wall-clock cap only when opted in.
|
|
2887
3169
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
@@ -2903,7 +3185,7 @@ var SddParallelRun = class {
|
|
|
2903
3185
|
runId: this.runId,
|
|
2904
3186
|
taskId,
|
|
2905
3187
|
subagentId,
|
|
2906
|
-
agentName
|
|
3188
|
+
agentName,
|
|
2907
3189
|
worktreeBranch: this.taskBranches.get(taskId)
|
|
2908
3190
|
});
|
|
2909
3191
|
const directivePreamble = [
|
|
@@ -2938,7 +3220,7 @@ var SddParallelRun = class {
|
|
|
2938
3220
|
let result;
|
|
2939
3221
|
try {
|
|
2940
3222
|
const got = await coordinator.awaitTasks([correlationId]);
|
|
2941
|
-
result =
|
|
3223
|
+
result = expectDefined3(got[0]);
|
|
2942
3224
|
} catch (err) {
|
|
2943
3225
|
result = {
|
|
2944
3226
|
subagentId,
|
|
@@ -2966,12 +3248,22 @@ var SddParallelRun = class {
|
|
|
2966
3248
|
} catch (err) {
|
|
2967
3249
|
verificationFailReason = `verification error: ${String(err)}`;
|
|
2968
3250
|
}
|
|
3251
|
+
const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
|
|
2969
3252
|
if (verificationFailReason) {
|
|
3253
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
3254
|
+
verificationState: "failed",
|
|
3255
|
+
verificationDetail: verificationFailReason
|
|
3256
|
+
});
|
|
2970
3257
|
this.emit("sdd.task.verification_failed", {
|
|
2971
3258
|
runId: this.runId,
|
|
2972
3259
|
taskId,
|
|
2973
3260
|
reason: verificationFailReason
|
|
2974
3261
|
});
|
|
3262
|
+
} else if (hadVerifiable) {
|
|
3263
|
+
this.opts.tracker.patchMetadata(taskId, {
|
|
3264
|
+
verificationState: "passed",
|
|
3265
|
+
verificationDetail: void 0
|
|
3266
|
+
});
|
|
2975
3267
|
}
|
|
2976
3268
|
}
|
|
2977
3269
|
let success = false;
|
|
@@ -2996,12 +3288,13 @@ var SddParallelRun = class {
|
|
|
2996
3288
|
});
|
|
2997
3289
|
await this.applyTaskFailure(taskId, subagentId, merged.reason);
|
|
2998
3290
|
} else {
|
|
3291
|
+
const conflictFiles = merged.conflictFiles ?? [];
|
|
2999
3292
|
this.emit("sdd.task.conflict", {
|
|
3000
3293
|
runId: this.runId,
|
|
3001
3294
|
taskId,
|
|
3002
|
-
conflictFiles
|
|
3295
|
+
conflictFiles
|
|
3003
3296
|
});
|
|
3004
|
-
const reason = `merge conflict${
|
|
3297
|
+
const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
|
|
3005
3298
|
await this.applyTaskFailure(taskId, subagentId, reason);
|
|
3006
3299
|
}
|
|
3007
3300
|
} else {
|
|
@@ -3104,7 +3397,11 @@ var SddParallelRun = class {
|
|
|
3104
3397
|
const res = await wt.merge(handle, {
|
|
3105
3398
|
squash: true,
|
|
3106
3399
|
...this.opts.conflictResolver ? {
|
|
3107
|
-
resolve: (info) => this.opts.conflictResolver({
|
|
3400
|
+
resolve: (info) => this.opts.conflictResolver({
|
|
3401
|
+
task,
|
|
3402
|
+
conflictFiles: info.conflictFiles,
|
|
3403
|
+
cwd: info.cwd
|
|
3404
|
+
})
|
|
3108
3405
|
} : {}
|
|
3109
3406
|
});
|
|
3110
3407
|
if (res.ok) {
|
|
@@ -3116,7 +3413,8 @@ var SddParallelRun = class {
|
|
|
3116
3413
|
result: result ?? {},
|
|
3117
3414
|
cwd: this.opts.projectRoot
|
|
3118
3415
|
});
|
|
3119
|
-
if (!verdict.ok)
|
|
3416
|
+
if (!verdict.ok)
|
|
3417
|
+
regressed = verdict.reason ?? "verification failed after conflict resolution";
|
|
3120
3418
|
} catch (err) {
|
|
3121
3419
|
regressed = `verification error after conflict resolution: ${String(err)}`;
|
|
3122
3420
|
}
|
|
@@ -3232,6 +3530,32 @@ var SddParallelRun = class {
|
|
|
3232
3530
|
};
|
|
3233
3531
|
|
|
3234
3532
|
// src/start-sdd-run.ts
|
|
3533
|
+
function applySddControlCommand(run, command) {
|
|
3534
|
+
const payload = command.payload ?? {};
|
|
3535
|
+
if (command.type === "pause") run.pause();
|
|
3536
|
+
else if (command.type === "resume") run.resume();
|
|
3537
|
+
else if (command.type === "stop") run.stop();
|
|
3538
|
+
else if (command.type === "retry" && payload.taskId) run.retryTask(payload.taskId);
|
|
3539
|
+
else if (command.type === "retry_all_failed") run.retryAllFailed();
|
|
3540
|
+
else if (command.type === "reassign" && payload.taskId)
|
|
3541
|
+
run.reassignTask(payload.taskId, payload.agentName ?? "");
|
|
3542
|
+
else if (command.type === "set_task_model" && payload.taskId)
|
|
3543
|
+
run.setTaskModel(payload.taskId, payload.model, payload.provider);
|
|
3544
|
+
else if (command.type === "set_task_fallbacks" && payload.taskId)
|
|
3545
|
+
run.setTaskFallbacks(payload.taskId, payload.fallbackModels);
|
|
3546
|
+
else if (command.type === "set_task_verification" && payload.taskId)
|
|
3547
|
+
run.setTaskVerification(payload.taskId, payload.verificationCommand);
|
|
3548
|
+
else if (command.type === "cancel_task" && payload.taskId)
|
|
3549
|
+
void run.cancelTask(payload.taskId).catch(() => {
|
|
3550
|
+
});
|
|
3551
|
+
else if (command.type === "delete_task" && payload.taskId) run.deleteTask(payload.taskId);
|
|
3552
|
+
else if (command.type === "split_task" && payload.taskId && payload.subtasks?.length)
|
|
3553
|
+
run.splitTask(payload.taskId, payload.subtasks);
|
|
3554
|
+
else if (command.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
|
|
3555
|
+
});
|
|
3556
|
+
else if (command.type === "rollback") void run.rollback().catch(() => {
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3235
3559
|
function startSddRun(opts) {
|
|
3236
3560
|
SddParallelRun.resetOrphans(opts.tracker);
|
|
3237
3561
|
const run = new SddParallelRun({
|
|
@@ -3295,25 +3619,7 @@ function startSddRun(opts) {
|
|
|
3295
3619
|
const controlTimer = setInterval(() => {
|
|
3296
3620
|
void opts.boardStore.drainControl(run.runId).then((cmds) => {
|
|
3297
3621
|
for (const c of cmds) {
|
|
3298
|
-
|
|
3299
|
-
if (c.type === "pause") run.pause();
|
|
3300
|
-
else if (c.type === "resume") run.resume();
|
|
3301
|
-
else if (c.type === "stop") run.stop();
|
|
3302
|
-
else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
|
|
3303
|
-
else if (c.type === "retry_all_failed") run.retryAllFailed();
|
|
3304
|
-
else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
|
|
3305
|
-
else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
|
|
3306
|
-
else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
|
|
3307
|
-
else if (c.type === "set_task_verification" && p.taskId)
|
|
3308
|
-
run.setTaskVerification(p.taskId, p.verificationCommand);
|
|
3309
|
-
else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId).catch(() => {
|
|
3310
|
-
});
|
|
3311
|
-
else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
|
|
3312
|
-
else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
|
|
3313
|
-
else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees().catch(() => {
|
|
3314
|
-
});
|
|
3315
|
-
else if (c.type === "rollback") void run.rollback().catch(() => {
|
|
3316
|
-
});
|
|
3622
|
+
applySddControlCommand(run, c);
|
|
3317
3623
|
}
|
|
3318
3624
|
}).catch(() => {
|
|
3319
3625
|
});
|
|
@@ -3341,7 +3647,8 @@ function startSddRun(opts) {
|
|
|
3341
3647
|
|
|
3342
3648
|
// src/sdd-lifecycle.ts
|
|
3343
3649
|
import * as fsp4 from "node:fs/promises";
|
|
3344
|
-
import {
|
|
3650
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
3651
|
+
import { WorktreeManager } from "@wrongstack/core/worktree";
|
|
3345
3652
|
async function cleanupSddWorktrees(projectRoot) {
|
|
3346
3653
|
const wt = new WorktreeManager({ projectRoot });
|
|
3347
3654
|
return wt.cleanupAllManaged();
|
|
@@ -3352,19 +3659,21 @@ async function cleanupStaleWorktrees(projectRoot) {
|
|
|
3352
3659
|
}
|
|
3353
3660
|
async function cleanupStaleSddWorktrees(opts) {
|
|
3354
3661
|
const now = opts.now?.() ?? Date.now();
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
}
|
|
3662
|
+
const store = new SddBoardStore({ baseDir: opts.boardsDir });
|
|
3663
|
+
const latest = (await store.list())[0];
|
|
3664
|
+
if (latest) {
|
|
3665
|
+
const age = now - latest.updatedAt;
|
|
3666
|
+
if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
|
|
3667
|
+
return {
|
|
3668
|
+
swept: false,
|
|
3669
|
+
removed: 0,
|
|
3670
|
+
detected: 0,
|
|
3671
|
+
skippedReason: "a run appears live (running)"
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
if (latest.status === "paused" && age < (opts.pausedLiveMs ?? 18e5)) {
|
|
3675
|
+
return { swept: false, removed: 0, detected: 0, skippedReason: "a run is paused" };
|
|
3366
3676
|
}
|
|
3367
|
-
} catch {
|
|
3368
3677
|
}
|
|
3369
3678
|
try {
|
|
3370
3679
|
const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
|
|
@@ -3381,7 +3690,11 @@ async function rollbackSddRunFromDisk(opts) {
|
|
|
3381
3690
|
const snap = await store.load(runId);
|
|
3382
3691
|
if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
|
|
3383
3692
|
if (!snap.baseBranch) {
|
|
3384
|
-
return {
|
|
3693
|
+
return {
|
|
3694
|
+
ok: false,
|
|
3695
|
+
reverted: 0,
|
|
3696
|
+
reason: "this run did not record a base branch (no worktree run)"
|
|
3697
|
+
};
|
|
3385
3698
|
}
|
|
3386
3699
|
const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
|
|
3387
3700
|
if (shas.length === 0) {
|
|
@@ -3399,7 +3712,7 @@ async function destroySddProject(opts) {
|
|
|
3399
3712
|
projectRoot: opts.projectRoot,
|
|
3400
3713
|
boardsDir: opts.paths.projectSddBoards,
|
|
3401
3714
|
runId: opts.runId
|
|
3402
|
-
}).catch((err) => ({ ok: false, reverted: 0, reason:
|
|
3715
|
+
}).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
|
|
3403
3716
|
reverted = r.reverted;
|
|
3404
3717
|
revertOk = r.ok;
|
|
3405
3718
|
revertReason = r.reason;
|
|
@@ -3426,9 +3739,6 @@ async function destroySddProject(opts) {
|
|
|
3426
3739
|
await rmDir(opts.paths.projectSddBoards, "boards");
|
|
3427
3740
|
return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
|
|
3428
3741
|
}
|
|
3429
|
-
function toReason(err) {
|
|
3430
|
-
return err instanceof Error ? err.message : String(err);
|
|
3431
|
-
}
|
|
3432
3742
|
async function applySddLifecycle(op, opts) {
|
|
3433
3743
|
try {
|
|
3434
3744
|
if (op === "cleanup_worktrees") {
|
|
@@ -3460,7 +3770,7 @@ async function applySddLifecycle(op, opts) {
|
|
|
3460
3770
|
reason: r.revertOk === false ? r.revertReason : void 0
|
|
3461
3771
|
};
|
|
3462
3772
|
} catch (err) {
|
|
3463
|
-
return { op, ok: false, reason:
|
|
3773
|
+
return { op, ok: false, reason: toErrorMessage2(err) };
|
|
3464
3774
|
}
|
|
3465
3775
|
}
|
|
3466
3776
|
|
|
@@ -3581,8 +3891,8 @@ function templateToMarkdown(template, title) {
|
|
|
3581
3891
|
}
|
|
3582
3892
|
|
|
3583
3893
|
// src/task-visualizer.ts
|
|
3584
|
-
import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/
|
|
3585
|
-
import { truncate } from "@wrongstack/core";
|
|
3894
|
+
import { computeTaskProgress as computeTaskProgress2 } from "@wrongstack/core/tasking";
|
|
3895
|
+
import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
|
|
3586
3896
|
var STATUS_ICON = {
|
|
3587
3897
|
pending: "\u25CB",
|
|
3588
3898
|
in_progress: "\u25D0",
|
|
@@ -3609,7 +3919,9 @@ function renderTaskGraph(graph, opts) {
|
|
|
3609
3919
|
const lines = [];
|
|
3610
3920
|
const compact = opts?.compact ?? false;
|
|
3611
3921
|
lines.push(`\u256D\u2500 Task Graph: ${graph.title} \u2500\u256E`);
|
|
3612
|
-
lines.push(
|
|
3922
|
+
lines.push(
|
|
3923
|
+
`\u2502 Spec: ${graph.specId.slice(0, 8)}... \u2502 Nodes: ${graph.nodes.size} \u2502 Edges: ${graph.edges.length} \u2502`
|
|
3924
|
+
);
|
|
3613
3925
|
lines.push("\u2570" + "\u2500".repeat(Math.max(50, graph.title.length + 30)) + "\u256F");
|
|
3614
3926
|
lines.push("");
|
|
3615
3927
|
const progress = computeTaskProgress2(graph);
|
|
@@ -3644,8 +3956,7 @@ function renderTaskGraph(graph, opts) {
|
|
|
3644
3956
|
function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix) {
|
|
3645
3957
|
if (rendered.has(nodeId)) return;
|
|
3646
3958
|
rendered.add(nodeId);
|
|
3647
|
-
const node = graph.nodes.get(nodeId);
|
|
3648
|
-
if (!node) return;
|
|
3959
|
+
const node = expectDefined4(graph.nodes.get(nodeId));
|
|
3649
3960
|
const icon = STATUS_ICON[node.status];
|
|
3650
3961
|
const prioIcon = PRIORITY_ICON[node.priority];
|
|
3651
3962
|
const typeIcon = TYPE_ICON[node.type];
|
|
@@ -3686,7 +3997,7 @@ function renderTaskList(graph) {
|
|
|
3686
3997
|
completed: []
|
|
3687
3998
|
};
|
|
3688
3999
|
for (const node of nodes) {
|
|
3689
|
-
groups[node.status]
|
|
4000
|
+
groups[node.status].push(node);
|
|
3690
4001
|
}
|
|
3691
4002
|
for (const [status, group] of Object.entries(groups)) {
|
|
3692
4003
|
if (group.length === 0) continue;
|
|
@@ -3734,8 +4045,8 @@ function renderSpecAnalysis(spec, analysis) {
|
|
|
3734
4045
|
}
|
|
3735
4046
|
|
|
3736
4047
|
// src/critical-path.ts
|
|
3737
|
-
import {
|
|
3738
|
-
import {
|
|
4048
|
+
import { topologicalSort } from "@wrongstack/core/tasking";
|
|
4049
|
+
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
3739
4050
|
function analyzeCriticalPath(graph) {
|
|
3740
4051
|
const nodes = Array.from(graph.nodes.values());
|
|
3741
4052
|
const topoOrder = topologicalSort(graph);
|
|
@@ -3789,8 +4100,7 @@ function analyzeCriticalPath(graph) {
|
|
|
3789
4100
|
bottlenecks.sort((a, b) => b.severity - a.severity);
|
|
3790
4101
|
const criticalPath = computeCriticalPath(graph, topoOrder, blockedByMap);
|
|
3791
4102
|
const totalHours = criticalPath.reduce((sum, id) => {
|
|
3792
|
-
|
|
3793
|
-
return sum + (n?.estimateHours ?? 0);
|
|
4103
|
+
return sum + (graph.nodes.get(id).estimateHours ?? 0);
|
|
3794
4104
|
}, 0);
|
|
3795
4105
|
const parallelGroups = computeParallelGroups(graph, blockedByMap);
|
|
3796
4106
|
const executionOrder = topoOrder.filter((id) => {
|
|
@@ -3811,7 +4121,7 @@ function getTransitiveBlocked(_graph, taskId, blocksMap) {
|
|
|
3811
4121
|
const visited = /* @__PURE__ */ new Set();
|
|
3812
4122
|
const queue = [taskId];
|
|
3813
4123
|
while (queue.length > 0) {
|
|
3814
|
-
const current =
|
|
4124
|
+
const current = expectDefined5(queue.shift());
|
|
3815
4125
|
const blocked = blocksMap.get(current);
|
|
3816
4126
|
if (!blocked) continue;
|
|
3817
4127
|
for (const id of blocked) {
|
|
@@ -3846,7 +4156,7 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
3846
4156
|
const blocked = blocksMap.get(id);
|
|
3847
4157
|
if (!blocked) continue;
|
|
3848
4158
|
for (const blockedId of blocked) {
|
|
3849
|
-
const candidateDist =
|
|
4159
|
+
const candidateDist = dist.get(id) + (graph.nodes.get(blockedId)?.estimateHours ?? 1);
|
|
3850
4160
|
if (candidateDist > (dist.get(blockedId) ?? 0)) {
|
|
3851
4161
|
dist.set(blockedId, candidateDist);
|
|
3852
4162
|
prev.set(blockedId, id);
|
|
@@ -3857,9 +4167,9 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
3857
4167
|
if (!changed) break;
|
|
3858
4168
|
}
|
|
3859
4169
|
let maxDist = 0;
|
|
3860
|
-
let maxId =
|
|
4170
|
+
let maxId = expectDefined5(allIds[0]);
|
|
3861
4171
|
for (const id of allIds) {
|
|
3862
|
-
const d = dist.get(id)
|
|
4172
|
+
const d = dist.get(id);
|
|
3863
4173
|
if (d > maxDist) {
|
|
3864
4174
|
maxDist = d;
|
|
3865
4175
|
maxId = id;
|
|
@@ -3894,8 +4204,7 @@ function computeParallelGroups(graph, blockedByMap) {
|
|
|
3894
4204
|
}
|
|
3895
4205
|
}
|
|
3896
4206
|
if (group.length === 0) {
|
|
3897
|
-
|
|
3898
|
-
if (first) group.push(first);
|
|
4207
|
+
group.push(expectDefined5(Array.from(remaining)[0]));
|
|
3899
4208
|
}
|
|
3900
4209
|
for (const id of group) {
|
|
3901
4210
|
assigned.add(id);
|
|
@@ -3907,7 +4216,7 @@ function computeParallelGroups(graph, blockedByMap) {
|
|
|
3907
4216
|
}
|
|
3908
4217
|
|
|
3909
4218
|
// src/spec-versioning.ts
|
|
3910
|
-
import { assertNever } from "@wrongstack/core";
|
|
4219
|
+
import { assertNever } from "@wrongstack/core/utils";
|
|
3911
4220
|
var SpecVersioning = class {
|
|
3912
4221
|
versions = /* @__PURE__ */ new Map();
|
|
3913
4222
|
/** Record a new version of a spec. */
|
|
@@ -4109,7 +4418,6 @@ var AutoExecutor = class {
|
|
|
4109
4418
|
for (let i = 0; i < results.length; i++) {
|
|
4110
4419
|
const result = results[i];
|
|
4111
4420
|
const task = batch[i];
|
|
4112
|
-
if (!result || !task) continue;
|
|
4113
4421
|
if (result.status === "fulfilled") {
|
|
4114
4422
|
const { result: execResult, retries } = result.value;
|
|
4115
4423
|
if (execResult.success) {
|
|
@@ -4159,14 +4467,14 @@ var AutoExecutor = class {
|
|
|
4159
4467
|
}
|
|
4160
4468
|
}
|
|
4161
4469
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4162
|
-
ready.sort((a, b) =>
|
|
4470
|
+
ready.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
4163
4471
|
return ready;
|
|
4164
4472
|
}
|
|
4165
4473
|
/** Execute a single task with retry logic. */
|
|
4166
4474
|
async executeTaskWithRetry(task, graph, spec) {
|
|
4167
4475
|
const maxRetries = this.opts.maxRetries ?? 2;
|
|
4168
4476
|
let retryCount = this.retryMap.get(task.id) ?? 0;
|
|
4169
|
-
while (
|
|
4477
|
+
while (true) {
|
|
4170
4478
|
this.opts.tracker.updateNodeStatus(task.id, "in_progress");
|
|
4171
4479
|
this.opts.onTaskStart?.(task);
|
|
4172
4480
|
const dependencies = this.getTaskDependencies(task.id, graph);
|
|
@@ -4210,7 +4518,6 @@ var AutoExecutor = class {
|
|
|
4210
4518
|
};
|
|
4211
4519
|
}
|
|
4212
4520
|
}
|
|
4213
|
-
return { result: { success: false, error: "Max retries exceeded" }, retries: retryCount };
|
|
4214
4521
|
}
|
|
4215
4522
|
/** Get tasks that this task depends on. */
|
|
4216
4523
|
getTaskDependencies(taskId, graph) {
|
|
@@ -4243,7 +4550,7 @@ function createAutoExecutor(opts) {
|
|
|
4243
4550
|
}
|
|
4244
4551
|
|
|
4245
4552
|
// src/sdd-supervisor.ts
|
|
4246
|
-
import { parseModelRef } from "@wrongstack/core";
|
|
4553
|
+
import { parseModelRef } from "@wrongstack/core/agent";
|
|
4247
4554
|
var SddSupervisor = class {
|
|
4248
4555
|
constructor(opts) {
|
|
4249
4556
|
this.opts = opts;
|
|
@@ -4297,6 +4604,54 @@ Supervisor rescues already used: ${attempts}`,
|
|
|
4297
4604
|
|
|
4298
4605
|
// src/verify-task.ts
|
|
4299
4606
|
import { spawn } from "node:child_process";
|
|
4607
|
+
function verificationShell(platform) {
|
|
4608
|
+
return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4609
|
+
}
|
|
4610
|
+
function makeCompositeVerifier(parts) {
|
|
4611
|
+
return async function verifyTask(info) {
|
|
4612
|
+
for (const part of parts) {
|
|
4613
|
+
const outcome = await part(info);
|
|
4614
|
+
if (!outcome.ok) return outcome;
|
|
4615
|
+
}
|
|
4616
|
+
return { ok: true };
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
function makeAcceptanceCriteriaVerifier(options) {
|
|
4620
|
+
const maxResultChars = options.maxResultChars ?? 4e3;
|
|
4621
|
+
return async function verifyTask(info) {
|
|
4622
|
+
const description = info.task.description ?? "";
|
|
4623
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4624
|
+
if (marker === -1) return { ok: true };
|
|
4625
|
+
const criteria = description.slice(marker);
|
|
4626
|
+
const resultText = typeof info.result.result === "string" ? info.result.result.slice(0, maxResultChars) : JSON.stringify(info.result.result ?? "").slice(0, maxResultChars);
|
|
4627
|
+
let text;
|
|
4628
|
+
try {
|
|
4629
|
+
text = await options.run(
|
|
4630
|
+
[
|
|
4631
|
+
"You are a strict acceptance reviewer for one completed engineering task.",
|
|
4632
|
+
`Task: ${info.task.title}`,
|
|
4633
|
+
"",
|
|
4634
|
+
criteria,
|
|
4635
|
+
"",
|
|
4636
|
+
"Worker's reported result:",
|
|
4637
|
+
resultText || "(no result text)",
|
|
4638
|
+
"",
|
|
4639
|
+
"Does the reported result plausibly satisfy EVERY acceptance criterion?",
|
|
4640
|
+
'Answer with exactly one line: "VERDICT: PASS" or "VERDICT: FAIL \u2014 <short reason>".'
|
|
4641
|
+
].join("\n")
|
|
4642
|
+
);
|
|
4643
|
+
} catch {
|
|
4644
|
+
return { ok: true };
|
|
4645
|
+
}
|
|
4646
|
+
const match = text.match(/VERDICT:\s*(PASS|FAIL)(?:\s*[—-]\s*(.*))?/i);
|
|
4647
|
+
if (!match) return { ok: true };
|
|
4648
|
+
if (match[1].toUpperCase() === "PASS") return { ok: true };
|
|
4649
|
+
return {
|
|
4650
|
+
ok: false,
|
|
4651
|
+
reason: `acceptance criteria not met: ${match[2]?.trim() || "judge rejected the result"}`
|
|
4652
|
+
};
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4300
4655
|
function makeCommandVerifier(options = {}) {
|
|
4301
4656
|
const metadataKey = options.metadataKey ?? "verificationCommand";
|
|
4302
4657
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
@@ -4304,8 +4659,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4304
4659
|
const cmd = info.task.metadata?.[metadataKey];
|
|
4305
4660
|
if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
|
|
4306
4661
|
return await new Promise((resolve) => {
|
|
4307
|
-
const
|
|
4308
|
-
const [shell, ...shellArgs] = isWindows ? ["cmd", "/d", "/c"] : ["sh", "-c"];
|
|
4662
|
+
const [shell, ...shellArgs] = verificationShell(process.platform);
|
|
4309
4663
|
const child = spawn(shell, [...shellArgs, cmd], {
|
|
4310
4664
|
cwd: info.cwd,
|
|
4311
4665
|
shell: false,
|
|
@@ -4327,7 +4681,6 @@ function makeCommandVerifier(options = {}) {
|
|
|
4327
4681
|
});
|
|
4328
4682
|
child.on("error", (err) => {
|
|
4329
4683
|
clearTimeout(timer);
|
|
4330
|
-
if (timedOut) return;
|
|
4331
4684
|
resolve({ ok: false, reason: `verification spawn error: ${String(err)}` });
|
|
4332
4685
|
});
|
|
4333
4686
|
});
|
|
@@ -4335,10 +4688,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
4335
4688
|
}
|
|
4336
4689
|
|
|
4337
4690
|
// src/decompose-task.ts
|
|
4338
|
-
import {
|
|
4339
|
-
readBundledInstructionText,
|
|
4340
|
-
renderInstructionTemplate
|
|
4341
|
-
} from "@wrongstack/core";
|
|
4691
|
+
import { readBundledInstructionText, renderInstructionTemplate } from "@wrongstack/core/utils";
|
|
4342
4692
|
var TASK_TYPES2 = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
|
|
4343
4693
|
var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
4344
4694
|
function extractJsonArray(text) {
|
|
@@ -4362,48 +4712,144 @@ function buildPrompt(task, error, min, max) {
|
|
|
4362
4712
|
error: error || "(none recorded)"
|
|
4363
4713
|
});
|
|
4364
4714
|
}
|
|
4365
|
-
function
|
|
4715
|
+
function parseSubtaskSpecs(text, min, max, options = {}) {
|
|
4716
|
+
const json = extractJsonArray(text ?? "");
|
|
4717
|
+
if (!json) return [];
|
|
4718
|
+
let raw;
|
|
4719
|
+
try {
|
|
4720
|
+
raw = JSON.parse(json);
|
|
4721
|
+
} catch {
|
|
4722
|
+
return [];
|
|
4723
|
+
}
|
|
4724
|
+
const items = raw;
|
|
4725
|
+
const specs = [];
|
|
4726
|
+
for (const item of items) {
|
|
4727
|
+
if (!item || typeof item !== "object") continue;
|
|
4728
|
+
const r = item;
|
|
4729
|
+
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
4730
|
+
const description = typeof r["description"] === "string" ? r["description"].trim() : "";
|
|
4731
|
+
if (!title || !description) continue;
|
|
4732
|
+
const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
|
|
4733
|
+
const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
|
|
4734
|
+
const successCriterion = options.acceptSuccessCriterion && typeof r["successCriterion"] === "string" ? r["successCriterion"].trim() || void 0 : void 0;
|
|
4735
|
+
specs.push({ title, description, type, priority, successCriterion });
|
|
4736
|
+
if (specs.length >= max) break;
|
|
4737
|
+
}
|
|
4738
|
+
return specs.length >= min ? specs : [];
|
|
4739
|
+
}
|
|
4740
|
+
function makePlanningDecomposer(opts) {
|
|
4366
4741
|
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4367
|
-
const max = Math.max(min, opts.maxSubtasks ??
|
|
4368
|
-
return async function
|
|
4742
|
+
const max = Math.max(min, opts.maxSubtasks ?? 5);
|
|
4743
|
+
return async function decompose(info) {
|
|
4369
4744
|
let text;
|
|
4370
4745
|
try {
|
|
4371
|
-
text = await opts.run(
|
|
4746
|
+
text = await opts.run(
|
|
4747
|
+
renderInstructionTemplate(readBundledInstructionText("sdd/decompose-task-planning.md"), {
|
|
4748
|
+
minSubtasks: String(min),
|
|
4749
|
+
maxSubtasks: String(max),
|
|
4750
|
+
title: info.title,
|
|
4751
|
+
description: info.description,
|
|
4752
|
+
reasons: info.reasons.length ? info.reasons.map((r) => `- ${r}`).join("\n") : "- (unspecified)"
|
|
4753
|
+
})
|
|
4754
|
+
);
|
|
4372
4755
|
} catch {
|
|
4373
4756
|
return [];
|
|
4374
4757
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4758
|
+
return parseSubtaskSpecs(text, min, max, { acceptSuccessCriterion: true });
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
function makeLlmSubtaskGenerator(opts) {
|
|
4762
|
+
const min = Math.max(2, opts.minSubtasks ?? 2);
|
|
4763
|
+
const max = Math.max(min, opts.maxSubtasks ?? 4);
|
|
4764
|
+
return async function generateSubtasks(info) {
|
|
4765
|
+
let text;
|
|
4378
4766
|
try {
|
|
4379
|
-
|
|
4767
|
+
text = await opts.run(buildPrompt(info.task, info.error, min, max));
|
|
4380
4768
|
} catch {
|
|
4381
4769
|
return [];
|
|
4382
4770
|
}
|
|
4383
|
-
|
|
4384
|
-
const specs = [];
|
|
4385
|
-
for (const item of raw) {
|
|
4386
|
-
if (!item || typeof item !== "object") continue;
|
|
4387
|
-
const r = item;
|
|
4388
|
-
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
4389
|
-
const description = typeof r["description"] === "string" ? r["description"].trim() : "";
|
|
4390
|
-
if (!title || !description) continue;
|
|
4391
|
-
const type = TASK_TYPES2.has(r["type"]) ? r["type"] : void 0;
|
|
4392
|
-
const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
|
|
4393
|
-
specs.push({ title, description, type, priority });
|
|
4394
|
-
if (specs.length >= max) break;
|
|
4395
|
-
}
|
|
4396
|
-
return specs.length >= min ? specs : [];
|
|
4771
|
+
return parseSubtaskSpecs(text, min, max);
|
|
4397
4772
|
};
|
|
4398
4773
|
}
|
|
4399
4774
|
|
|
4400
|
-
// src/
|
|
4401
|
-
import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
4402
|
-
import { join as join4, isAbsolute } from "node:path";
|
|
4775
|
+
// src/plan-decompose.ts
|
|
4403
4776
|
import {
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4777
|
+
assessAtomicity as assessAtomicity2
|
|
4778
|
+
} from "@wrongstack/kanban";
|
|
4779
|
+
function countAcceptanceCriteria(description) {
|
|
4780
|
+
const marker = description.indexOf("**Acceptance Criteria:**");
|
|
4781
|
+
if (marker === -1) return 0;
|
|
4782
|
+
const tail = description.slice(marker);
|
|
4783
|
+
return (tail.match(/^\s*-\s+\S/gm) ?? []).length;
|
|
4784
|
+
}
|
|
4785
|
+
function assessTaskNodeAtomicity(tracker, node, config) {
|
|
4786
|
+
const criteriaCount = countAcceptanceCriteria(node.description ?? "");
|
|
4787
|
+
const verificationCommand = node.metadata?.["verificationCommand"] ?? extractVerificationCommand([node.description ?? ""]);
|
|
4788
|
+
return assessAtomicity2(
|
|
4789
|
+
{
|
|
4790
|
+
title: node.title,
|
|
4791
|
+
description: node.description,
|
|
4792
|
+
estimatedHours: node.estimateHours,
|
|
4793
|
+
dependencyCount: tracker.getBlockers(node.id).length,
|
|
4794
|
+
successCriteriaCount: criteriaCount,
|
|
4795
|
+
hasVerifiableOutput: Boolean(verificationCommand),
|
|
4796
|
+
childCount: tracker.getAllNodes().filter((n) => n.parentId === node.id).length
|
|
4797
|
+
},
|
|
4798
|
+
config
|
|
4799
|
+
);
|
|
4800
|
+
}
|
|
4801
|
+
async function decomposeNonAtomicTasks(opts) {
|
|
4802
|
+
const maxDecompositions = Math.max(1, opts.maxDecompositions ?? 10);
|
|
4803
|
+
const result = { applied: [], proposals: [], flagged: [] };
|
|
4804
|
+
const nodes = opts.tracker.getAllNodes();
|
|
4805
|
+
const childCounts = /* @__PURE__ */ new Map();
|
|
4806
|
+
for (const node of nodes) {
|
|
4807
|
+
if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
|
|
4808
|
+
}
|
|
4809
|
+
const candidates = nodes.filter(
|
|
4810
|
+
(node) => node.status === "pending" && !childCounts.get(node.id)
|
|
4811
|
+
);
|
|
4812
|
+
let spent = 0;
|
|
4813
|
+
for (const node of candidates) {
|
|
4814
|
+
if (spent >= maxDecompositions) break;
|
|
4815
|
+
const assessment = assessTaskNodeAtomicity(opts.tracker, node, opts.config);
|
|
4816
|
+
opts.tracker.patchMetadata(node.id, {
|
|
4817
|
+
atomicity: {
|
|
4818
|
+
verdict: assessment.verdict,
|
|
4819
|
+
score: assessment.score,
|
|
4820
|
+
reasons: assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason)
|
|
4821
|
+
}
|
|
4822
|
+
});
|
|
4823
|
+
if (assessment.verdict !== "needs_decomposition") continue;
|
|
4824
|
+
result.flagged.push(node.id);
|
|
4825
|
+
spent += 1;
|
|
4826
|
+
const reasons = assessment.criteria.filter((c) => c.score < 1).map((c) => c.reason);
|
|
4827
|
+
const subtasks = await opts.decompose({
|
|
4828
|
+
title: node.title,
|
|
4829
|
+
description: node.description ?? "",
|
|
4830
|
+
reasons
|
|
4831
|
+
});
|
|
4832
|
+
if (!subtasks.length) continue;
|
|
4833
|
+
if (opts.mode === "auto") {
|
|
4834
|
+
const subtaskIds = splitGraphNode(opts.tracker, node.id, subtasks);
|
|
4835
|
+
if (subtaskIds.length) result.applied.push({ nodeId: node.id, subtaskIds });
|
|
4836
|
+
} else {
|
|
4837
|
+
result.proposals.push({ nodeId: node.id, title: node.title, reasons, subtasks });
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
return result;
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
// src/conflict-resolver.ts
|
|
4844
|
+
import { readFile as readFile4, writeFile } from "node:fs/promises";
|
|
4845
|
+
import { isAbsolute, join as join4 } from "node:path";
|
|
4846
|
+
import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
|
|
4847
|
+
var defaultFileIO = {
|
|
4848
|
+
read: (path4) => readFile4(path4, "utf8"),
|
|
4849
|
+
write: async (path4, content) => {
|
|
4850
|
+
await writeFile(path4, content, "utf8");
|
|
4851
|
+
}
|
|
4852
|
+
};
|
|
4407
4853
|
var START = "<<<<<<<";
|
|
4408
4854
|
var BASE = "|||||||";
|
|
4409
4855
|
var SEP = "=======";
|
|
@@ -4441,21 +4887,21 @@ function hasConflictMarkers(text) {
|
|
|
4441
4887
|
return m === START || m === SEP || m === END || m === BASE;
|
|
4442
4888
|
});
|
|
4443
4889
|
}
|
|
4444
|
-
function makePreferSideConflictResolver(side) {
|
|
4890
|
+
function makePreferSideConflictResolver(side, io = defaultFileIO) {
|
|
4445
4891
|
return async function conflictResolver(info) {
|
|
4446
4892
|
if (info.conflictFiles.length === 0) return false;
|
|
4447
4893
|
for (const rel of info.conflictFiles) {
|
|
4448
4894
|
const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
|
|
4449
4895
|
let content;
|
|
4450
4896
|
try {
|
|
4451
|
-
content = await
|
|
4897
|
+
content = await io.read(abs);
|
|
4452
4898
|
} catch {
|
|
4453
4899
|
return false;
|
|
4454
4900
|
}
|
|
4455
4901
|
const resolved = resolveConflictText(content, side);
|
|
4456
4902
|
if (hasConflictMarkers(resolved)) return false;
|
|
4457
4903
|
try {
|
|
4458
|
-
await
|
|
4904
|
+
await io.write(abs, resolved);
|
|
4459
4905
|
} catch {
|
|
4460
4906
|
return false;
|
|
4461
4907
|
}
|
|
@@ -4475,13 +4921,14 @@ function nonMarkerLineCount(text) {
|
|
|
4475
4921
|
}
|
|
4476
4922
|
function makeLlmConflictResolver(opts) {
|
|
4477
4923
|
const minFraction = opts.minRetainedFraction ?? 0.5;
|
|
4924
|
+
const io = opts.io ?? defaultFileIO;
|
|
4478
4925
|
return async function conflictResolver(info) {
|
|
4479
4926
|
if (info.conflictFiles.length === 0) return false;
|
|
4480
4927
|
for (const rel of info.conflictFiles) {
|
|
4481
4928
|
const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
|
|
4482
4929
|
let content;
|
|
4483
4930
|
try {
|
|
4484
|
-
content = await
|
|
4931
|
+
content = await io.read(abs);
|
|
4485
4932
|
} catch {
|
|
4486
4933
|
return false;
|
|
4487
4934
|
}
|
|
@@ -4505,7 +4952,7 @@ function makeLlmConflictResolver(opts) {
|
|
|
4505
4952
|
return false;
|
|
4506
4953
|
}
|
|
4507
4954
|
try {
|
|
4508
|
-
await
|
|
4955
|
+
await io.write(abs, resolved);
|
|
4509
4956
|
} catch {
|
|
4510
4957
|
return false;
|
|
4511
4958
|
}
|
|
@@ -4535,21 +4982,27 @@ export {
|
|
|
4535
4982
|
TaskTracker3 as TaskTracker,
|
|
4536
4983
|
analyzeCriticalPath,
|
|
4537
4984
|
applySddLifecycle,
|
|
4985
|
+
assessGeneratedTaskAtomicity,
|
|
4986
|
+
assessTaskNodeAtomicity,
|
|
4538
4987
|
buildBoardSnapshot,
|
|
4539
4988
|
buildBoardTasks,
|
|
4540
4989
|
cleanupSddWorktrees,
|
|
4541
4990
|
cleanupStaleSddWorktrees,
|
|
4542
4991
|
cleanupStaleWorktrees,
|
|
4543
4992
|
createAutoExecutor,
|
|
4993
|
+
decomposeNonAtomicTasks,
|
|
4544
4994
|
destroySddProject,
|
|
4545
4995
|
extractVerificationCommand,
|
|
4546
4996
|
getTemplate,
|
|
4547
4997
|
hasConflictMarkers,
|
|
4548
4998
|
isExplanatoryText,
|
|
4549
4999
|
listTemplates,
|
|
5000
|
+
makeAcceptanceCriteriaVerifier,
|
|
4550
5001
|
makeCommandVerifier,
|
|
5002
|
+
makeCompositeVerifier,
|
|
4551
5003
|
makeLlmConflictResolver,
|
|
4552
5004
|
makeLlmSubtaskGenerator,
|
|
5005
|
+
makePlanningDecomposer,
|
|
4553
5006
|
makePreferSideConflictResolver,
|
|
4554
5007
|
renderProgress,
|
|
4555
5008
|
renderSpecAnalysis,
|
|
@@ -4558,6 +5011,7 @@ export {
|
|
|
4558
5011
|
resolveConflictText,
|
|
4559
5012
|
rollbackSddRunFromDisk,
|
|
4560
5013
|
shortIdMap,
|
|
5014
|
+
splitGraphNode,
|
|
4561
5015
|
startSddRun,
|
|
4562
5016
|
templateToMarkdown
|
|
4563
5017
|
};
|