@wrongstack/sdd 0.295.1 → 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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +348 -183
- package/dist/index.js.map +4 -4
- package/dist/project-context.d.ts +6 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/sdd-interview-driver.d.ts +15 -0
- 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 -198
- 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 +20 -0
- package/dist/spec-builder.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1836,10 +1836,10 @@ var AISpecBuilder = class {
|
|
|
1836
1836
|
async saveSession() {
|
|
1837
1837
|
if (!this.sessionPath) return;
|
|
1838
1838
|
try {
|
|
1839
|
-
const
|
|
1840
|
-
const
|
|
1839
|
+
const fsp6 = await import("node:fs/promises");
|
|
1840
|
+
const path6 = await import("node:path");
|
|
1841
1841
|
const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
|
|
1842
|
-
await
|
|
1842
|
+
await fsp6.mkdir(path6.dirname(this.sessionPath), { recursive: true });
|
|
1843
1843
|
await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
1844
1844
|
} catch {
|
|
1845
1845
|
}
|
|
@@ -1848,8 +1848,8 @@ var AISpecBuilder = class {
|
|
|
1848
1848
|
async loadSession() {
|
|
1849
1849
|
if (!this.sessionPath) return false;
|
|
1850
1850
|
try {
|
|
1851
|
-
const
|
|
1852
|
-
const raw = await
|
|
1851
|
+
const fsp6 = await import("node:fs/promises");
|
|
1852
|
+
const raw = await fsp6.readFile(this.sessionPath, "utf8");
|
|
1853
1853
|
const loaded = JSON.parse(raw);
|
|
1854
1854
|
if (loaded?.id && loaded?.phase && loaded?.title) {
|
|
1855
1855
|
this.session = loaded;
|
|
@@ -1863,8 +1863,8 @@ var AISpecBuilder = class {
|
|
|
1863
1863
|
async deleteSession() {
|
|
1864
1864
|
if (!this.sessionPath) return;
|
|
1865
1865
|
try {
|
|
1866
|
-
const
|
|
1867
|
-
await
|
|
1866
|
+
const fsp6 = await import("node:fs/promises");
|
|
1867
|
+
await fsp6.unlink(this.sessionPath);
|
|
1868
1868
|
} catch {
|
|
1869
1869
|
}
|
|
1870
1870
|
}
|
|
@@ -2015,6 +2015,43 @@ var AISpecBuilder = class {
|
|
|
2015
2015
|
getTaskGraphId() {
|
|
2016
2016
|
return this.session.taskGraphId;
|
|
2017
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
|
+
}
|
|
2018
2055
|
// ── Spec Persistence ──────────────────────────────────────────────────────
|
|
2019
2056
|
/**
|
|
2020
2057
|
* Save the current spec to the store.
|
|
@@ -2181,6 +2218,8 @@ var SddInterviewDriver = class {
|
|
|
2181
2218
|
maxQuestions;
|
|
2182
2219
|
tracker = null;
|
|
2183
2220
|
graph = null;
|
|
2221
|
+
/** Set when {@link loadExisting} successfully rehydrated a session from disk. */
|
|
2222
|
+
resumedFromDisk = false;
|
|
2184
2223
|
constructor(opts) {
|
|
2185
2224
|
this.o = opts;
|
|
2186
2225
|
this.minQuestions = opts.minQuestions ?? 2;
|
|
@@ -2195,9 +2234,11 @@ var SddInterviewDriver = class {
|
|
|
2195
2234
|
}
|
|
2196
2235
|
/** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
|
|
2197
2236
|
start(title, intent) {
|
|
2237
|
+
this.builder.resetForNewInterview();
|
|
2198
2238
|
this.builder.startSession(title, intent);
|
|
2199
2239
|
this.tracker = null;
|
|
2200
2240
|
this.graph = null;
|
|
2241
|
+
this.resumedFromDisk = false;
|
|
2201
2242
|
return this.builder.getAIPrompt();
|
|
2202
2243
|
}
|
|
2203
2244
|
/**
|
|
@@ -2217,8 +2258,32 @@ var SddInterviewDriver = class {
|
|
|
2217
2258
|
this.tracker = tracker;
|
|
2218
2259
|
}
|
|
2219
2260
|
}
|
|
2261
|
+
this.resumedFromDisk = true;
|
|
2220
2262
|
return true;
|
|
2221
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
|
+
}
|
|
2222
2287
|
phase() {
|
|
2223
2288
|
return this.builder.getPhase();
|
|
2224
2289
|
}
|
|
@@ -2319,6 +2384,9 @@ var SddInterviewDriver = class {
|
|
|
2319
2384
|
minQuestions: this.minQuestions,
|
|
2320
2385
|
maxQuestions: this.maxQuestions,
|
|
2321
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,
|
|
2322
2390
|
spec: spec ? {
|
|
2323
2391
|
id: spec.id,
|
|
2324
2392
|
title: spec.title,
|
|
@@ -2443,15 +2511,12 @@ function isExplanatoryText(text) {
|
|
|
2443
2511
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
2444
2512
|
|
|
2445
2513
|
// src/sdd-parallel-run.ts
|
|
2446
|
-
import {
|
|
2447
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2514
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2448
2515
|
import {
|
|
2449
|
-
assignNickname,
|
|
2450
2516
|
DefaultMultiAgentCoordinator,
|
|
2451
2517
|
makeAgentSubagentRunner,
|
|
2452
2518
|
withDisabledToolFiltering
|
|
2453
2519
|
} from "@wrongstack/core/coordination";
|
|
2454
|
-
import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
|
|
2455
2520
|
|
|
2456
2521
|
// src/graph-split.ts
|
|
2457
2522
|
function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
@@ -2486,6 +2551,180 @@ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
|
|
|
2486
2551
|
return leafIds;
|
|
2487
2552
|
}
|
|
2488
2553
|
|
|
2554
|
+
// src/sdd-task-execution.ts
|
|
2555
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
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
|
+
}
|
|
2727
|
+
|
|
2489
2728
|
// src/sdd-task-decomposer.ts
|
|
2490
2729
|
var SddTaskDecomposer = class {
|
|
2491
2730
|
constructor(tracker, _graph, opts = {}) {
|
|
@@ -2602,7 +2841,7 @@ var SddParallelRun = class {
|
|
|
2602
2841
|
this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
|
|
2603
2842
|
this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
|
|
2604
2843
|
this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
|
|
2605
|
-
this.runId = opts.runId ?? `sdd-${
|
|
2844
|
+
this.runId = opts.runId ?? `sdd-${randomUUID3().slice(0, 8)}`;
|
|
2606
2845
|
this.events = opts.events;
|
|
2607
2846
|
this.sessionIdSource = opts.sessionId;
|
|
2608
2847
|
this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
|
|
@@ -3082,7 +3321,7 @@ var SddParallelRun = class {
|
|
|
3082
3321
|
// -------------------------------------------------------------------
|
|
3083
3322
|
buildCoordinator() {
|
|
3084
3323
|
const config = {
|
|
3085
|
-
coordinatorId: `sdd-parallel-${
|
|
3324
|
+
coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
|
|
3086
3325
|
maxConcurrent: this.slots,
|
|
3087
3326
|
doneCondition: { type: "all_tasks_done" },
|
|
3088
3327
|
// Default budget guard for every spawned worker: idle reaper (resets on
|
|
@@ -3139,170 +3378,30 @@ var SddParallelRun = class {
|
|
|
3139
3378
|
* missing coordinator or failed spawn so callers can enforce all-or-nothing.
|
|
3140
3379
|
*/
|
|
3141
3380
|
async executeOne(task) {
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
this.usedNicknames
|
|
3147
|
-
agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
3148
|
-
this.opts.tracker.updateNode(taskId, { assignee: agentName });
|
|
3149
|
-
}
|
|
3150
|
-
this.opts.tracker.updateNodeStatus(taskId, "in_progress");
|
|
3151
|
-
await this.allocateWorktrees([task]);
|
|
3152
|
-
if (!this.coordinator)
|
|
3153
|
-
throw new SddError3({
|
|
3154
|
-
message: "SDD parallel runner requires a coordinator",
|
|
3155
|
-
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3156
|
-
});
|
|
3157
|
-
const coordinator = this.coordinator;
|
|
3158
|
-
const subagentId = `sdd-d${this.dispatchSeq++}`;
|
|
3159
|
-
const correlationId = randomUUID2();
|
|
3160
|
-
const meta = task.metadata ?? {};
|
|
3161
|
-
const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
|
|
3162
|
-
const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
|
|
3163
|
-
const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
|
|
3164
|
-
const spawnResult = await coordinator.spawn({
|
|
3165
|
-
id: subagentId,
|
|
3166
|
-
name: agentName,
|
|
3167
|
-
role: "executor",
|
|
3168
|
-
// 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,
|
|
3169
3386
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
3170
|
-
|
|
3171
|
-
cwd: this.taskCwds.get(taskId),
|
|
3172
|
-
disabledTools: ["delegate"],
|
|
3173
|
-
...model ? { model } : {},
|
|
3174
|
-
...provider ? { provider } : {},
|
|
3175
|
-
...fallbackModels?.length ? { fallbackModels } : {}
|
|
3176
|
-
});
|
|
3177
|
-
if (!spawnResult.subagentId) {
|
|
3178
|
-
throw new SddError3({
|
|
3179
|
-
message: "One or more subagent spawns failed",
|
|
3180
|
-
code: ERROR_CODES3.SDD_INVALID_STATE
|
|
3181
|
-
});
|
|
3182
|
-
}
|
|
3183
|
-
this.taskSubagents.set(taskId, subagentId);
|
|
3184
|
-
this.emit("sdd.task.started", {
|
|
3387
|
+
timeoutMs: this.timeoutMs,
|
|
3185
3388
|
runId: this.runId,
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
"\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
|
|
3197
|
-
"\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
|
|
3198
|
-
"\u2022 Mark the task [done] in the tracker when complete.",
|
|
3199
|
-
"\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
|
|
3200
|
-
"\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
|
|
3201
|
-
].join("\n");
|
|
3202
|
-
await coordinator.assign({
|
|
3203
|
-
id: correlationId,
|
|
3204
|
-
description: [
|
|
3205
|
-
directivePreamble,
|
|
3206
|
-
"",
|
|
3207
|
-
`\u2500\u2500 TASK \u2500\u2500`,
|
|
3208
|
-
`[${task.priority.toUpperCase()}] ${task.title}`,
|
|
3209
|
-
"",
|
|
3210
|
-
task.description
|
|
3211
|
-
].join("\n"),
|
|
3212
|
-
subagentId,
|
|
3213
|
-
...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
|
|
3214
|
-
context: {
|
|
3215
|
-
telemetryTaskId: taskId,
|
|
3216
|
-
telemetryRunId: this.runId,
|
|
3217
|
-
telemetryBoardId: this.opts.graph.id
|
|
3218
|
-
}
|
|
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)
|
|
3219
3399
|
});
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
result = expectDefined3(got[0]);
|
|
3224
|
-
} catch (err) {
|
|
3225
|
-
result = {
|
|
3226
|
-
subagentId,
|
|
3227
|
-
taskId: correlationId,
|
|
3228
|
-
status: "failed",
|
|
3229
|
-
error: { kind: "unknown", message: String(err), retryable: false },
|
|
3230
|
-
iterations: 0,
|
|
3231
|
-
toolCalls: 0,
|
|
3232
|
-
durationMs: 0
|
|
3233
|
-
};
|
|
3400
|
+
if (outcome.success) {
|
|
3401
|
+
this.retryMap.delete(task.id);
|
|
3402
|
+
this.persistRetries(task.id, 0);
|
|
3234
3403
|
}
|
|
3235
|
-
|
|
3236
|
-
if (this.cancelledTasks.has(taskId)) {
|
|
3237
|
-
await this.resolveWorktrees([task]);
|
|
3238
|
-
return { taskId, success: false, result };
|
|
3239
|
-
}
|
|
3240
|
-
let verificationFailReason;
|
|
3241
|
-
if (result.status === "success" && this.opts.verifyTask) {
|
|
3242
|
-
const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
|
|
3243
|
-
try {
|
|
3244
|
-
const verdict = await this.opts.verifyTask({ task, result, cwd });
|
|
3245
|
-
if (!verdict.ok) {
|
|
3246
|
-
verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
|
|
3247
|
-
}
|
|
3248
|
-
} catch (err) {
|
|
3249
|
-
verificationFailReason = `verification error: ${String(err)}`;
|
|
3250
|
-
}
|
|
3251
|
-
const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
|
|
3252
|
-
if (verificationFailReason) {
|
|
3253
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
3254
|
-
verificationState: "failed",
|
|
3255
|
-
verificationDetail: verificationFailReason
|
|
3256
|
-
});
|
|
3257
|
-
this.emit("sdd.task.verification_failed", {
|
|
3258
|
-
runId: this.runId,
|
|
3259
|
-
taskId,
|
|
3260
|
-
reason: verificationFailReason
|
|
3261
|
-
});
|
|
3262
|
-
} else if (hadVerifiable) {
|
|
3263
|
-
this.opts.tracker.patchMetadata(taskId, {
|
|
3264
|
-
verificationState: "passed",
|
|
3265
|
-
verificationDetail: void 0
|
|
3266
|
-
});
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
let success = false;
|
|
3270
|
-
if (result.status === "success" && !verificationFailReason) {
|
|
3271
|
-
const merged = await this.integrateWorktree(task, result);
|
|
3272
|
-
if (merged.ok) {
|
|
3273
|
-
success = true;
|
|
3274
|
-
this.opts.tracker.updateNodeStatus(taskId, "completed");
|
|
3275
|
-
this.retryMap.delete(taskId);
|
|
3276
|
-
this.persistRetries(taskId, 0);
|
|
3277
|
-
this.emit("sdd.task.completed", {
|
|
3278
|
-
runId: this.runId,
|
|
3279
|
-
taskId,
|
|
3280
|
-
subagentId,
|
|
3281
|
-
durationMs: result.durationMs
|
|
3282
|
-
});
|
|
3283
|
-
} else if (merged.reason) {
|
|
3284
|
-
this.emit("sdd.task.verification_failed", {
|
|
3285
|
-
runId: this.runId,
|
|
3286
|
-
taskId,
|
|
3287
|
-
reason: merged.reason
|
|
3288
|
-
});
|
|
3289
|
-
await this.applyTaskFailure(taskId, subagentId, merged.reason);
|
|
3290
|
-
} else {
|
|
3291
|
-
const conflictFiles = merged.conflictFiles ?? [];
|
|
3292
|
-
this.emit("sdd.task.conflict", {
|
|
3293
|
-
runId: this.runId,
|
|
3294
|
-
taskId,
|
|
3295
|
-
conflictFiles
|
|
3296
|
-
});
|
|
3297
|
-
const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
|
|
3298
|
-
await this.applyTaskFailure(taskId, subagentId, reason);
|
|
3299
|
-
}
|
|
3300
|
-
} else {
|
|
3301
|
-
const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
|
|
3302
|
-
await this.applyTaskFailure(taskId, subagentId, errMsg);
|
|
3303
|
-
await this.resolveWorktrees([task]);
|
|
3304
|
-
}
|
|
3305
|
-
return { taskId, success, result };
|
|
3404
|
+
return outcome;
|
|
3306
3405
|
}
|
|
3307
3406
|
/**
|
|
3308
3407
|
* Apply a task failure: retry (→ pending, bump retry count) while attempts
|
|
@@ -3647,8 +3746,10 @@ function startSddRun(opts) {
|
|
|
3647
3746
|
|
|
3648
3747
|
// src/sdd-lifecycle.ts
|
|
3649
3748
|
import * as fsp4 from "node:fs/promises";
|
|
3749
|
+
import * as path4 from "node:path";
|
|
3650
3750
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
3651
3751
|
import { WorktreeManager } from "@wrongstack/core/worktree";
|
|
3752
|
+
import { listBoards, removeBoard } from "@wrongstack/kanban";
|
|
3652
3753
|
async function cleanupSddWorktrees(projectRoot) {
|
|
3653
3754
|
const wt = new WorktreeManager({ projectRoot });
|
|
3654
3755
|
return wt.cleanupAllManaged();
|
|
@@ -3734,9 +3835,22 @@ async function destroySddProject(opts) {
|
|
|
3734
3835
|
}
|
|
3735
3836
|
};
|
|
3736
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
|
+
);
|
|
3737
3842
|
await rmDir(opts.paths.projectSpecs, "specs");
|
|
3738
3843
|
await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
|
|
3739
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
|
+
}
|
|
3740
3854
|
return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
|
|
3741
3855
|
}
|
|
3742
3856
|
async function applySddLifecycle(op, opts) {
|
|
@@ -3774,6 +3888,56 @@ async function applySddLifecycle(op, opts) {
|
|
|
3774
3888
|
}
|
|
3775
3889
|
}
|
|
3776
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 {
|
|
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");
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3777
3941
|
// src/spec-templates.ts
|
|
3778
3942
|
var SPEC_TEMPLATES = [
|
|
3779
3943
|
{
|
|
@@ -4175,15 +4339,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
4175
4339
|
maxId = id;
|
|
4176
4340
|
}
|
|
4177
4341
|
}
|
|
4178
|
-
const
|
|
4342
|
+
const path6 = [];
|
|
4179
4343
|
let current = maxId;
|
|
4180
4344
|
const visited = /* @__PURE__ */ new Set();
|
|
4181
4345
|
while (current && !visited.has(current)) {
|
|
4182
4346
|
visited.add(current);
|
|
4183
|
-
|
|
4347
|
+
path6.unshift(current);
|
|
4184
4348
|
current = prev.get(current) ?? null;
|
|
4185
4349
|
}
|
|
4186
|
-
return
|
|
4350
|
+
return path6;
|
|
4187
4351
|
}
|
|
4188
4352
|
function computeParallelGroups(graph, blockedByMap) {
|
|
4189
4353
|
const groups = [];
|
|
@@ -4841,13 +5005,13 @@ async function decomposeNonAtomicTasks(opts) {
|
|
|
4841
5005
|
}
|
|
4842
5006
|
|
|
4843
5007
|
// src/conflict-resolver.ts
|
|
4844
|
-
import { readFile as
|
|
4845
|
-
import { isAbsolute, join as
|
|
5008
|
+
import { readFile as readFile5, writeFile } from "node:fs/promises";
|
|
5009
|
+
import { isAbsolute, join as join6 } from "node:path";
|
|
4846
5010
|
import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
|
|
4847
5011
|
var defaultFileIO = {
|
|
4848
|
-
read: (
|
|
4849
|
-
write: async (
|
|
4850
|
-
await writeFile(
|
|
5012
|
+
read: (path6) => readFile5(path6, "utf8"),
|
|
5013
|
+
write: async (path6, content) => {
|
|
5014
|
+
await writeFile(path6, content, "utf8");
|
|
4851
5015
|
}
|
|
4852
5016
|
};
|
|
4853
5017
|
var START = "<<<<<<<";
|
|
@@ -4891,7 +5055,7 @@ function makePreferSideConflictResolver(side, io = defaultFileIO) {
|
|
|
4891
5055
|
return async function conflictResolver(info) {
|
|
4892
5056
|
if (info.conflictFiles.length === 0) return false;
|
|
4893
5057
|
for (const rel of info.conflictFiles) {
|
|
4894
|
-
const abs = isAbsolute(rel) ? rel :
|
|
5058
|
+
const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
|
|
4895
5059
|
let content;
|
|
4896
5060
|
try {
|
|
4897
5061
|
content = await io.read(abs);
|
|
@@ -4925,7 +5089,7 @@ function makeLlmConflictResolver(opts) {
|
|
|
4925
5089
|
return async function conflictResolver(info) {
|
|
4926
5090
|
if (info.conflictFiles.length === 0) return false;
|
|
4927
5091
|
for (const rel of info.conflictFiles) {
|
|
4928
|
-
const abs = isAbsolute(rel) ? rel :
|
|
5092
|
+
const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
|
|
4929
5093
|
let content;
|
|
4930
5094
|
try {
|
|
4931
5095
|
content = await io.read(abs);
|
|
@@ -4993,6 +5157,7 @@ export {
|
|
|
4993
5157
|
decomposeNonAtomicTasks,
|
|
4994
5158
|
destroySddProject,
|
|
4995
5159
|
extractVerificationCommand,
|
|
5160
|
+
gatherProjectContext,
|
|
4996
5161
|
getTemplate,
|
|
4997
5162
|
hasConflictMarkers,
|
|
4998
5163
|
isExplanatoryText,
|