@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.js CHANGED
@@ -1836,10 +1836,10 @@ var AISpecBuilder = class {
1836
1836
  async saveSession() {
1837
1837
  if (!this.sessionPath) return;
1838
1838
  try {
1839
- const fsp5 = await import("node:fs/promises");
1840
- const path4 = await import("node:path");
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 fsp5.mkdir(path4.dirname(this.sessionPath), { recursive: true });
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 fsp5 = await import("node:fs/promises");
1852
- const raw = await fsp5.readFile(this.sessionPath, "utf8");
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 fsp5 = await import("node:fs/promises");
1867
- await fsp5.unlink(this.sessionPath);
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 { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
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-${randomUUID2().slice(0, 8)}`;
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-${randomUUID2().slice(0, 8)}`,
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 taskId = task.id;
3143
- let agentName = task.assignee;
3144
- if (!agentName) {
3145
- const nick = assignNickname("executor", this.usedNicknames);
3146
- this.usedNicknames.add(nick.key);
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
- ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
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
- taskId,
3187
- subagentId,
3188
- agentName,
3189
- worktreeBranch: this.taskBranches.get(taskId)
3190
- });
3191
- const directivePreamble = [
3192
- "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
3193
- "",
3194
- `Graph: ${this.opts.graph.title}`,
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
- let result;
3221
- try {
3222
- const got = await coordinator.awaitTasks([correlationId]);
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
- this.taskSubagents.delete(taskId);
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 path4 = [];
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
- path4.unshift(current);
4347
+ path6.unshift(current);
4184
4348
  current = prev.get(current) ?? null;
4185
4349
  }
4186
- return path4;
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 readFile4, writeFile } from "node:fs/promises";
4845
- import { isAbsolute, join as join4 } from "node:path";
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: (path4) => readFile4(path4, "utf8"),
4849
- write: async (path4, content) => {
4850
- await writeFile(path4, content, "utf8");
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 : join4(info.cwd, 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 : join4(info.cwd, 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,