@wrongstack/sdd 0.295.1 → 0.296.3

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,20 +1836,21 @@ 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
- } catch {
1844
+ } catch (error) {
1845
+ console.error("[sdd] Failed to persist session", error);
1845
1846
  }
1846
1847
  }
1847
1848
  /** Load session state from disk. Returns true if a session was loaded. */
1848
1849
  async loadSession() {
1849
1850
  if (!this.sessionPath) return false;
1850
1851
  try {
1851
- const fsp5 = await import("node:fs/promises");
1852
- const raw = await fsp5.readFile(this.sessionPath, "utf8");
1852
+ const fsp6 = await import("node:fs/promises");
1853
+ const raw = await fsp6.readFile(this.sessionPath, "utf8");
1853
1854
  const loaded = JSON.parse(raw);
1854
1855
  if (loaded?.id && loaded?.phase && loaded?.title) {
1855
1856
  this.session = loaded;
@@ -1863,8 +1864,8 @@ var AISpecBuilder = class {
1863
1864
  async deleteSession() {
1864
1865
  if (!this.sessionPath) return;
1865
1866
  try {
1866
- const fsp5 = await import("node:fs/promises");
1867
- await fsp5.unlink(this.sessionPath);
1867
+ const fsp6 = await import("node:fs/promises");
1868
+ await fsp6.unlink(this.sessionPath);
1868
1869
  } catch {
1869
1870
  }
1870
1871
  }
@@ -2003,11 +2004,15 @@ var AISpecBuilder = class {
2003
2004
  this.autoSave();
2004
2005
  }
2005
2006
  /**
2006
- * Set the task graph ID for this session.
2007
+ * Set the task graph ID for this session. Awaits the save so a caller that
2008
+ * immediately follows with `await saveSession()` cannot end up with the
2009
+ * awaited write committing first and the fire-and-forget rename reverting
2010
+ * the persisted `taskGraphId` to its pre-set value. Same race window that
2011
+ * broke `setLastAgentText`/`setLastRunId` on the resume test.
2007
2012
  */
2008
- setTaskGraphId(graphId) {
2013
+ async setTaskGraphId(graphId) {
2009
2014
  this.session.taskGraphId = graphId;
2010
- this.autoSave();
2015
+ await this.saveSession();
2011
2016
  }
2012
2017
  /**
2013
2018
  * Get the task graph ID for this session.
@@ -2015,6 +2020,50 @@ var AISpecBuilder = class {
2015
2020
  getTaskGraphId() {
2016
2021
  return this.session.taskGraphId;
2017
2022
  }
2023
+ /**
2024
+ * Persist the last agent utterance so resume can rehydrate the UI + Q/A
2025
+ * pairing. Awaits the save so the next mutation in the call chain (e.g.
2026
+ * `setLastRunId`) cannot fire a concurrent save that overwrites this one
2027
+ * with a stale snapshot — the fire-and-forget `autoSave()` pattern leaves
2028
+ * a race window where an earlier queued save may commit its rename after
2029
+ * a later one, silently reverting the persisted state.
2030
+ */
2031
+ async setLastAgentText(text) {
2032
+ this.session.lastAgentText = text;
2033
+ this.session.updatedAt = Date.now();
2034
+ await this.saveSession();
2035
+ }
2036
+ getLastAgentText() {
2037
+ return this.session.lastAgentText;
2038
+ }
2039
+ /** Record a run kicked off from this interview (board deep-link after restart). See {@link setLastAgentText} for the awaited-save rationale. */
2040
+ async setLastRunId(runId) {
2041
+ this.session.lastRunId = runId;
2042
+ this.session.updatedAt = Date.now();
2043
+ await this.saveSession();
2044
+ }
2045
+ getLastRunId() {
2046
+ return this.session.lastRunId;
2047
+ }
2048
+ /**
2049
+ * Hard-reset in-memory session fields while keeping the same session id /
2050
+ * store binding. Used when the operator abandons a resumed interview and
2051
+ * starts a brand-new goal (the next save overwrites the session file).
2052
+ */
2053
+ resetForNewInterview() {
2054
+ this.session.phase = "questioning";
2055
+ this.session.title = "";
2056
+ this.session.userIntent = "";
2057
+ this.session.answers = [];
2058
+ this.session.questionCount = 0;
2059
+ this.session.spec = void 0;
2060
+ this.session.implementation = void 0;
2061
+ this.session.taskGraphId = void 0;
2062
+ this.session.lastAgentText = void 0;
2063
+ this.session.lastRunId = void 0;
2064
+ this.session.approved = false;
2065
+ this.session.updatedAt = Date.now();
2066
+ }
2018
2067
  // ── Spec Persistence ──────────────────────────────────────────────────────
2019
2068
  /**
2020
2069
  * Save the current spec to the store.
@@ -2181,6 +2230,8 @@ var SddInterviewDriver = class {
2181
2230
  maxQuestions;
2182
2231
  tracker = null;
2183
2232
  graph = null;
2233
+ /** Set when {@link loadExisting} successfully rehydrated a session from disk. */
2234
+ resumedFromDisk = false;
2184
2235
  constructor(opts) {
2185
2236
  this.o = opts;
2186
2237
  this.minQuestions = opts.minQuestions ?? 2;
@@ -2195,9 +2246,11 @@ var SddInterviewDriver = class {
2195
2246
  }
2196
2247
  /** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
2197
2248
  start(title, intent) {
2249
+ this.builder.resetForNewInterview();
2198
2250
  this.builder.startSession(title, intent);
2199
2251
  this.tracker = null;
2200
2252
  this.graph = null;
2253
+ this.resumedFromDisk = false;
2201
2254
  return this.builder.getAIPrompt();
2202
2255
  }
2203
2256
  /**
@@ -2217,8 +2270,32 @@ var SddInterviewDriver = class {
2217
2270
  this.tracker = tracker;
2218
2271
  }
2219
2272
  }
2273
+ this.resumedFromDisk = true;
2220
2274
  return true;
2221
2275
  }
2276
+ /** Drop the on-disk session (if any) and clear in-memory interview state. */
2277
+ async discard() {
2278
+ await this.builder.deleteSession();
2279
+ this.builder.resetForNewInterview();
2280
+ this.tracker = null;
2281
+ this.graph = null;
2282
+ this.resumedFromDisk = false;
2283
+ }
2284
+ setLastAgentText(text) {
2285
+ return this.builder.setLastAgentText(text);
2286
+ }
2287
+ getLastAgentText() {
2288
+ return this.builder.getLastAgentText();
2289
+ }
2290
+ setLastRunId(runId) {
2291
+ return this.builder.setLastRunId(runId);
2292
+ }
2293
+ getLastRunId() {
2294
+ return this.builder.getLastRunId();
2295
+ }
2296
+ wasResumed() {
2297
+ return this.resumedFromDisk;
2298
+ }
2222
2299
  phase() {
2223
2300
  return this.builder.getPhase();
2224
2301
  }
@@ -2303,7 +2380,7 @@ var SddInterviewDriver = class {
2303
2380
  this.tracker = tracker;
2304
2381
  this.graph = graph;
2305
2382
  await this.persistGraph(graph);
2306
- this.builder.setTaskGraphId(graph.id);
2383
+ await this.builder.setTaskGraphId(graph.id);
2307
2384
  await this.builder.saveSession();
2308
2385
  return graph;
2309
2386
  }
@@ -2319,6 +2396,9 @@ var SddInterviewDriver = class {
2319
2396
  minQuestions: this.minQuestions,
2320
2397
  maxQuestions: this.maxQuestions,
2321
2398
  answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
2399
+ lastAgentText: s.lastAgentText,
2400
+ lastRunId: s.lastRunId,
2401
+ resumed: this.resumedFromDisk || void 0,
2322
2402
  spec: spec ? {
2323
2403
  id: spec.id,
2324
2404
  title: spec.title,
@@ -2413,7 +2493,7 @@ var SddInterviewDriver = class {
2413
2493
  }
2414
2494
  }
2415
2495
  await this.persistGraph(graph);
2416
- this.builder.setTaskGraphId(graph.id);
2496
+ await this.builder.setTaskGraphId(graph.id);
2417
2497
  await this.builder.saveSession();
2418
2498
  return graph.id;
2419
2499
  }
@@ -2443,15 +2523,12 @@ function isExplanatoryText(text) {
2443
2523
  import { TOKENS } from "@wrongstack/core/kernel";
2444
2524
 
2445
2525
  // src/sdd-parallel-run.ts
2446
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2447
- import { randomUUID as randomUUID2 } from "node:crypto";
2526
+ import { randomUUID as randomUUID3 } from "node:crypto";
2448
2527
  import {
2449
- assignNickname,
2450
2528
  DefaultMultiAgentCoordinator,
2451
2529
  makeAgentSubagentRunner,
2452
2530
  withDisabledToolFiltering
2453
2531
  } from "@wrongstack/core/coordination";
2454
- import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2455
2532
 
2456
2533
  // src/graph-split.ts
2457
2534
  function splitGraphNode(tracker, taskId, subtasks, options = {}) {
@@ -2463,8 +2540,7 @@ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2463
2540
  const dependents = tracker.getDependents(taskId);
2464
2541
  const leafIds = subtasks.map((s) => {
2465
2542
  const criterion = s.successCriterion?.trim();
2466
- const verificationCommand = criterion ? extractVerificationCommand([criterion]) : void 0;
2467
- const description = criterion && !verificationCommand ? `${s.description}
2543
+ const description = criterion ? `${s.description}
2468
2544
 
2469
2545
  **Acceptance Criteria:**
2470
2546
  - ${criterion}` : s.description;
@@ -2474,8 +2550,7 @@ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2474
2550
  type: s.type ?? node.type,
2475
2551
  priority: s.priority ?? node.priority,
2476
2552
  status: "pending",
2477
- parentId: taskId,
2478
- ...verificationCommand ? { metadata: { verificationCommand } } : {}
2553
+ parentId: taskId
2479
2554
  }).id;
2480
2555
  });
2481
2556
  for (const leaf of leafIds) {
@@ -2486,6 +2561,180 @@ function splitGraphNode(tracker, taskId, subtasks, options = {}) {
2486
2561
  return leafIds;
2487
2562
  }
2488
2563
 
2564
+ // src/sdd-task-execution.ts
2565
+ import { randomUUID as randomUUID2 } from "node:crypto";
2566
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
2567
+ import { assignNickname } from "@wrongstack/core/coordination";
2568
+ import { ERROR_CODES as ERROR_CODES3, SddError as SddError3 } from "@wrongstack/core/types";
2569
+ async function executeSddTask(params) {
2570
+ const { task, opts } = params;
2571
+ const taskId = task.id;
2572
+ let agentName = task.assignee;
2573
+ if (!agentName) {
2574
+ const nick = assignNickname("executor", params.usedNicknames);
2575
+ params.usedNicknames.add(nick.key);
2576
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
2577
+ opts.tracker.updateNode(taskId, { assignee: agentName });
2578
+ }
2579
+ opts.tracker.updateNodeStatus(taskId, "in_progress");
2580
+ await params.allocateWorktrees([task]);
2581
+ if (!params.coordinator)
2582
+ throw new SddError3({
2583
+ message: "SDD parallel runner requires a coordinator",
2584
+ code: ERROR_CODES3.SDD_INVALID_STATE
2585
+ });
2586
+ const coordinator = params.coordinator;
2587
+ const subagentId = params.nextSubagentId();
2588
+ const correlationId = randomUUID2();
2589
+ const meta = task.metadata ?? {};
2590
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? opts.defaultModel;
2591
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? opts.defaultProvider;
2592
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : opts.fallbackModels;
2593
+ const spawnResult = await coordinator.spawn({
2594
+ id: subagentId,
2595
+ name: agentName,
2596
+ role: "executor",
2597
+ idleTimeoutMs: params.idleTimeoutMs,
2598
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
2599
+ cwd: params.taskCwds.get(taskId),
2600
+ disabledTools: ["delegate"],
2601
+ ...model ? { model } : {},
2602
+ ...provider ? { provider } : {},
2603
+ ...fallbackModels?.length ? { fallbackModels } : {}
2604
+ });
2605
+ if (!spawnResult.subagentId) {
2606
+ throw new SddError3({
2607
+ message: "One or more subagent spawns failed",
2608
+ code: ERROR_CODES3.SDD_INVALID_STATE
2609
+ });
2610
+ }
2611
+ params.taskSubagents.set(taskId, subagentId);
2612
+ params.emit("sdd.task.started", {
2613
+ runId: params.runId,
2614
+ taskId,
2615
+ subagentId,
2616
+ agentName,
2617
+ worktreeBranch: params.taskBranches.get(taskId)
2618
+ });
2619
+ await coordinator.assign({
2620
+ id: correlationId,
2621
+ description: buildTaskDirective(opts.graph.title, task),
2622
+ subagentId,
2623
+ ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
2624
+ context: {
2625
+ telemetryTaskId: taskId,
2626
+ telemetryRunId: params.runId,
2627
+ telemetryBoardId: opts.graph.id
2628
+ }
2629
+ });
2630
+ let result;
2631
+ try {
2632
+ const got = await coordinator.awaitTasks([correlationId]);
2633
+ result = expectDefined3(got[0]);
2634
+ } catch (err) {
2635
+ result = {
2636
+ subagentId,
2637
+ taskId: correlationId,
2638
+ status: "failed",
2639
+ error: { kind: "unknown", message: String(err), retryable: false },
2640
+ iterations: 0,
2641
+ toolCalls: 0,
2642
+ durationMs: 0
2643
+ };
2644
+ }
2645
+ params.taskSubagents.delete(taskId);
2646
+ if (params.cancelledTasks.has(taskId)) {
2647
+ await params.resolveWorktrees([task]);
2648
+ return { taskId, success: false, result };
2649
+ }
2650
+ const verificationFailReason = await verifyTaskResult(params, result);
2651
+ let success = false;
2652
+ if (result.status === "success" && !verificationFailReason) {
2653
+ const merged = await params.integrateWorktree(task, result);
2654
+ if (merged.ok) {
2655
+ success = true;
2656
+ opts.tracker.updateNodeStatus(taskId, "completed");
2657
+ params.emit("sdd.task.completed", {
2658
+ runId: params.runId,
2659
+ taskId,
2660
+ subagentId,
2661
+ durationMs: result.durationMs
2662
+ });
2663
+ } else if (merged.reason) {
2664
+ params.emit("sdd.task.verification_failed", {
2665
+ runId: params.runId,
2666
+ taskId,
2667
+ reason: merged.reason
2668
+ });
2669
+ await params.applyTaskFailure(taskId, subagentId, merged.reason);
2670
+ } else {
2671
+ const conflictFiles = merged.conflictFiles ?? [];
2672
+ params.emit("sdd.task.conflict", { runId: params.runId, taskId, conflictFiles });
2673
+ const reason = `merge conflict${conflictFiles.length ? `: ${conflictFiles.join(", ")}` : ""}`;
2674
+ await params.applyTaskFailure(taskId, subagentId, reason);
2675
+ }
2676
+ } else {
2677
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
2678
+ await params.applyTaskFailure(taskId, subagentId, errMsg);
2679
+ await params.resolveWorktrees([task]);
2680
+ }
2681
+ return { taskId, success, result };
2682
+ }
2683
+ function buildTaskDirective(graphTitle, task) {
2684
+ const directivePreamble = [
2685
+ "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
2686
+ "",
2687
+ `Graph: ${graphTitle}`,
2688
+ "",
2689
+ "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
2690
+ "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
2691
+ "\u2022 Mark the task [done] in the tracker when complete.",
2692
+ "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
2693
+ "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
2694
+ ].join("\n");
2695
+ return [
2696
+ directivePreamble,
2697
+ "",
2698
+ `\u2500\u2500 TASK \u2500\u2500`,
2699
+ `[${task.priority.toUpperCase()}] ${task.title}`,
2700
+ "",
2701
+ task.description
2702
+ ].join("\n");
2703
+ }
2704
+ async function verifyTaskResult(params, result) {
2705
+ const { task, opts, taskCwds } = params;
2706
+ if (result.status !== "success" || !opts.verifyTask) return void 0;
2707
+ const taskId = task.id;
2708
+ const cwd = taskCwds.get(taskId) ?? opts.projectRoot;
2709
+ let verificationFailReason;
2710
+ try {
2711
+ const verdict = await opts.verifyTask({ task, result, cwd });
2712
+ if (!verdict.ok) {
2713
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
2714
+ }
2715
+ } catch (err) {
2716
+ verificationFailReason = `verification error: ${String(err)}`;
2717
+ }
2718
+ const hadVerifiable = typeof task.metadata?.["verificationCommand"] === "string" || task.description.includes("**Acceptance Criteria:**");
2719
+ if (verificationFailReason) {
2720
+ opts.tracker.patchMetadata(taskId, {
2721
+ verificationState: "failed",
2722
+ verificationDetail: verificationFailReason
2723
+ });
2724
+ params.emit("sdd.task.verification_failed", {
2725
+ runId: params.runId,
2726
+ taskId,
2727
+ reason: verificationFailReason
2728
+ });
2729
+ } else if (hadVerifiable) {
2730
+ opts.tracker.patchMetadata(taskId, {
2731
+ verificationState: "passed",
2732
+ verificationDetail: void 0
2733
+ });
2734
+ }
2735
+ return verificationFailReason;
2736
+ }
2737
+
2489
2738
  // src/sdd-task-decomposer.ts
2490
2739
  var SddTaskDecomposer = class {
2491
2740
  constructor(tracker, _graph, opts = {}) {
@@ -2602,7 +2851,7 @@ var SddParallelRun = class {
2602
2851
  this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
2603
2852
  this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
2604
2853
  this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
2605
- this.runId = opts.runId ?? `sdd-${randomUUID2().slice(0, 8)}`;
2854
+ this.runId = opts.runId ?? `sdd-${randomUUID3().slice(0, 8)}`;
2606
2855
  this.events = opts.events;
2607
2856
  this.sessionIdSource = opts.sessionId;
2608
2857
  this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
@@ -3082,7 +3331,7 @@ var SddParallelRun = class {
3082
3331
  // -------------------------------------------------------------------
3083
3332
  buildCoordinator() {
3084
3333
  const config = {
3085
- coordinatorId: `sdd-parallel-${randomUUID2().slice(0, 8)}`,
3334
+ coordinatorId: `sdd-parallel-${randomUUID3().slice(0, 8)}`,
3086
3335
  maxConcurrent: this.slots,
3087
3336
  doneCondition: { type: "all_tasks_done" },
3088
3337
  // Default budget guard for every spawned worker: idle reaper (resets on
@@ -3139,170 +3388,30 @@ var SddParallelRun = class {
3139
3388
  * missing coordinator or failed spawn so callers can enforce all-or-nothing.
3140
3389
  */
3141
3390
  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.
3391
+ const outcome = await executeSddTask({
3392
+ task,
3393
+ opts: this.opts,
3394
+ coordinator: this.coordinator,
3395
+ usedNicknames: this.usedNicknames,
3169
3396
  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", {
3397
+ timeoutMs: this.timeoutMs,
3185
3398
  runId: this.runId,
3186
- taskId,
3187
- subagentId,
3188
- agentName,
3189
- worktreeBranch: this.taskBranches.get(taskId)
3399
+ nextSubagentId: () => `sdd-d${this.dispatchSeq++}`,
3400
+ emit: (event, payload) => this.emit(event, payload),
3401
+ taskCwds: this.taskCwds,
3402
+ taskBranches: this.taskBranches,
3403
+ taskSubagents: this.taskSubagents,
3404
+ cancelledTasks: this.cancelledTasks,
3405
+ allocateWorktrees: (tasks) => this.allocateWorktrees(tasks),
3406
+ resolveWorktrees: (tasks) => this.resolveWorktrees(tasks),
3407
+ integrateWorktree: (taskNode, result) => this.integrateWorktree(taskNode, result),
3408
+ applyTaskFailure: (taskId, subagentId, errMsg) => this.applyTaskFailure(taskId, subagentId, errMsg)
3190
3409
  });
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
- }
3219
- });
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
- };
3234
- }
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]);
3410
+ if (outcome.success) {
3411
+ this.retryMap.delete(task.id);
3412
+ this.persistRetries(task.id, 0);
3304
3413
  }
3305
- return { taskId, success, result };
3414
+ return outcome;
3306
3415
  }
3307
3416
  /**
3308
3417
  * Apply a task failure: retry (→ pending, bump retry count) while attempts
@@ -3647,8 +3756,10 @@ function startSddRun(opts) {
3647
3756
 
3648
3757
  // src/sdd-lifecycle.ts
3649
3758
  import * as fsp4 from "node:fs/promises";
3759
+ import * as path4 from "node:path";
3650
3760
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3651
3761
  import { WorktreeManager } from "@wrongstack/core/worktree";
3762
+ import { listBoards, removeBoard } from "@wrongstack/kanban";
3652
3763
  async function cleanupSddWorktrees(projectRoot) {
3653
3764
  const wt = new WorktreeManager({ projectRoot });
3654
3765
  return wt.cleanupAllManaged();
@@ -3734,9 +3845,22 @@ async function destroySddProject(opts) {
3734
3845
  }
3735
3846
  };
3736
3847
  await rmFile(opts.paths.projectSddSession, "session");
3848
+ await rmFile(
3849
+ path4.join(path4.dirname(opts.paths.projectSddSession), "sdd-wizard-session.json"),
3850
+ "wizard-session"
3851
+ );
3737
3852
  await rmDir(opts.paths.projectSpecs, "specs");
3738
3853
  await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
3739
3854
  await rmDir(opts.paths.projectSddBoards, "boards");
3855
+ try {
3856
+ const mirrors = (await listBoards(opts.projectRoot)).filter((b) => b.tags?.includes("sdd"));
3857
+ let mirrorsRemoved = 0;
3858
+ for (const b of mirrors) {
3859
+ if (await removeBoard(opts.projectRoot, b.id)) mirrorsRemoved++;
3860
+ }
3861
+ if (mirrorsRemoved > 0) deleted.push(`kanban-mirrors(${mirrorsRemoved})`);
3862
+ } catch {
3863
+ }
3740
3864
  return { worktreesRemoved: removed, deleted, reverted, revertOk, revertReason };
3741
3865
  }
3742
3866
  async function applySddLifecycle(op, opts) {
@@ -3774,6 +3898,56 @@ async function applySddLifecycle(op, opts) {
3774
3898
  }
3775
3899
  }
3776
3900
 
3901
+ // src/project-context.ts
3902
+ import * as fsp5 from "node:fs/promises";
3903
+ import * as path5 from "node:path";
3904
+ async function gatherProjectContext(projectRoot) {
3905
+ const parts = [];
3906
+ const root = projectRoot.trim() || process.cwd();
3907
+ try {
3908
+ const pkgPath = path5.join(root, "package.json");
3909
+ const pkgRaw = await fsp5.readFile(pkgPath, "utf8");
3910
+ const pkg = JSON.parse(pkgRaw);
3911
+ parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
3912
+ parts.push(`Description: ${String(pkg.description ?? "none")}`);
3913
+ if (pkg.dependencies && typeof pkg.dependencies === "object") {
3914
+ const deps = Object.keys(pkg.dependencies);
3915
+ parts.push(`Dependencies: ${deps.slice(0, 20).join(", ")}${deps.length > 20 ? "..." : ""}`);
3916
+ }
3917
+ if (pkg.devDependencies && typeof pkg.devDependencies === "object") {
3918
+ const devDeps = Object.keys(pkg.devDependencies);
3919
+ parts.push(
3920
+ `Dev Dependencies: ${devDeps.slice(0, 15).join(", ")}${devDeps.length > 15 ? "..." : ""}`
3921
+ );
3922
+ }
3923
+ } catch {
3924
+ }
3925
+ try {
3926
+ await fsp5.access(path5.join(root, "tsconfig.json"));
3927
+ parts.push("Language: TypeScript");
3928
+ } catch {
3929
+ }
3930
+ try {
3931
+ const srcDir = path5.join(root, "src");
3932
+ const entries = await fsp5.readdir(srcDir, { withFileTypes: true });
3933
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3934
+ if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
3935
+ } catch {
3936
+ }
3937
+ try {
3938
+ const packagesDir = path5.join(root, "packages");
3939
+ const entries = await fsp5.readdir(packagesDir, { withFileTypes: true });
3940
+ const pkgs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3941
+ if (pkgs.length > 0) {
3942
+ parts.push(
3943
+ `Packages: ${pkgs.slice(0, 25).join(", ")}${pkgs.length > 25 ? "..." : ""}`
3944
+ );
3945
+ }
3946
+ } catch {
3947
+ }
3948
+ return parts.join("\n");
3949
+ }
3950
+
3777
3951
  // src/spec-templates.ts
3778
3952
  var SPEC_TEMPLATES = [
3779
3953
  {
@@ -4175,15 +4349,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
4175
4349
  maxId = id;
4176
4350
  }
4177
4351
  }
4178
- const path4 = [];
4352
+ const path6 = [];
4179
4353
  let current = maxId;
4180
4354
  const visited = /* @__PURE__ */ new Set();
4181
4355
  while (current && !visited.has(current)) {
4182
4356
  visited.add(current);
4183
- path4.unshift(current);
4357
+ path6.unshift(current);
4184
4358
  current = prev.get(current) ?? null;
4185
4359
  }
4186
- return path4;
4360
+ return path6;
4187
4361
  }
4188
4362
  function computeParallelGroups(graph, blockedByMap) {
4189
4363
  const groups = [];
@@ -4604,8 +4778,70 @@ Supervisor rescues already used: ${attempts}`,
4604
4778
 
4605
4779
  // src/verify-task.ts
4606
4780
  import { spawn } from "node:child_process";
4607
- function verificationShell(platform) {
4608
- return platform === "win32" ? ["cmd", "/d", "/c"] : ["sh", "-c"];
4781
+ function tokenizeCommand(command) {
4782
+ const trimmed = command.trim();
4783
+ if (!trimmed) return void 0;
4784
+ const argv = [];
4785
+ let current = "";
4786
+ let inSingle = false;
4787
+ let inDouble = false;
4788
+ let hasToken = false;
4789
+ for (let i = 0; i < trimmed.length; i++) {
4790
+ const ch = trimmed[i];
4791
+ if (inSingle) {
4792
+ if (ch === "'") {
4793
+ inSingle = false;
4794
+ } else {
4795
+ current += ch;
4796
+ }
4797
+ continue;
4798
+ }
4799
+ if (inDouble) {
4800
+ if (ch === '"') {
4801
+ inDouble = false;
4802
+ } else if (ch === "\\" && i + 1 < trimmed.length) {
4803
+ const next = trimmed[i + 1];
4804
+ if (next === '"' || next === "\\" || next === "$" || next === "`") {
4805
+ current += next;
4806
+ i++;
4807
+ } else {
4808
+ current += ch;
4809
+ }
4810
+ } else {
4811
+ current += ch;
4812
+ }
4813
+ continue;
4814
+ }
4815
+ if (ch === "'") {
4816
+ inSingle = true;
4817
+ hasToken = true;
4818
+ continue;
4819
+ }
4820
+ if (ch === '"') {
4821
+ inDouble = true;
4822
+ hasToken = true;
4823
+ continue;
4824
+ }
4825
+ if (ch === "\\" && i + 1 < trimmed.length) {
4826
+ current += trimmed[i + 1];
4827
+ i++;
4828
+ hasToken = true;
4829
+ continue;
4830
+ }
4831
+ if (ch === " " || ch === " ") {
4832
+ if (hasToken) {
4833
+ argv.push(current);
4834
+ current = "";
4835
+ hasToken = false;
4836
+ }
4837
+ continue;
4838
+ }
4839
+ current += ch;
4840
+ hasToken = true;
4841
+ }
4842
+ if (inSingle || inDouble) return void 0;
4843
+ if (hasToken) argv.push(current);
4844
+ return argv.length > 0 ? argv : void 0;
4609
4845
  }
4610
4846
  function makeCompositeVerifier(parts) {
4611
4847
  return async function verifyTask(info) {
@@ -4656,11 +4892,15 @@ function makeCommandVerifier(options = {}) {
4656
4892
  const metadataKey = options.metadataKey ?? "verificationCommand";
4657
4893
  const timeoutMs = options.timeoutMs ?? 18e4;
4658
4894
  return async function verifyTask(info) {
4659
- const cmd = info.task.metadata?.[metadataKey];
4660
- if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
4895
+ const rawCommand = info.task.metadata?.[metadataKey];
4896
+ if (typeof rawCommand !== "string" || !rawCommand.trim()) return { ok: true };
4897
+ const argv = tokenizeCommand(rawCommand);
4898
+ if (!argv || argv.length === 0) {
4899
+ return { ok: false, reason: `verification command is malformed: ${rawCommand}` };
4900
+ }
4901
+ const [executable, ...args] = argv;
4661
4902
  return await new Promise((resolve) => {
4662
- const [shell, ...shellArgs] = verificationShell(process.platform);
4663
- const child = spawn(shell, [...shellArgs, cmd], {
4903
+ const child = spawn(executable, args, {
4664
4904
  cwd: info.cwd,
4665
4905
  shell: false,
4666
4906
  windowsHide: true,
@@ -4670,13 +4910,13 @@ function makeCommandVerifier(options = {}) {
4670
4910
  const timer = setTimeout(() => {
4671
4911
  timedOut = true;
4672
4912
  child.kill();
4673
- resolve({ ok: false, reason: `verification timed out: ${cmd}` });
4913
+ resolve({ ok: false, reason: `verification timed out: ${rawCommand}` });
4674
4914
  }, timeoutMs);
4675
4915
  child.on("exit", (code) => {
4676
4916
  clearTimeout(timer);
4677
4917
  if (timedOut) return;
4678
4918
  resolve(
4679
- code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${cmd}` }
4919
+ code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${rawCommand}` }
4680
4920
  );
4681
4921
  });
4682
4922
  child.on("error", (err) => {
@@ -4841,13 +5081,13 @@ async function decomposeNonAtomicTasks(opts) {
4841
5081
  }
4842
5082
 
4843
5083
  // src/conflict-resolver.ts
4844
- import { readFile as readFile4, writeFile } from "node:fs/promises";
4845
- import { isAbsolute, join as join4 } from "node:path";
5084
+ import { readFile as readFile5, writeFile } from "node:fs/promises";
5085
+ import { isAbsolute, join as join6 } from "node:path";
4846
5086
  import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
4847
5087
  var defaultFileIO = {
4848
- read: (path4) => readFile4(path4, "utf8"),
4849
- write: async (path4, content) => {
4850
- await writeFile(path4, content, "utf8");
5088
+ read: (path6) => readFile5(path6, "utf8"),
5089
+ write: async (path6, content) => {
5090
+ await writeFile(path6, content, "utf8");
4851
5091
  }
4852
5092
  };
4853
5093
  var START = "<<<<<<<";
@@ -4891,7 +5131,7 @@ function makePreferSideConflictResolver(side, io = defaultFileIO) {
4891
5131
  return async function conflictResolver(info) {
4892
5132
  if (info.conflictFiles.length === 0) return false;
4893
5133
  for (const rel of info.conflictFiles) {
4894
- const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
5134
+ const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
4895
5135
  let content;
4896
5136
  try {
4897
5137
  content = await io.read(abs);
@@ -4925,7 +5165,7 @@ function makeLlmConflictResolver(opts) {
4925
5165
  return async function conflictResolver(info) {
4926
5166
  if (info.conflictFiles.length === 0) return false;
4927
5167
  for (const rel of info.conflictFiles) {
4928
- const abs = isAbsolute(rel) ? rel : join4(info.cwd, rel);
5168
+ const abs = isAbsolute(rel) ? rel : join6(info.cwd, rel);
4929
5169
  let content;
4930
5170
  try {
4931
5171
  content = await io.read(abs);
@@ -4993,6 +5233,7 @@ export {
4993
5233
  decomposeNonAtomicTasks,
4994
5234
  destroySddProject,
4995
5235
  extractVerificationCommand,
5236
+ gatherProjectContext,
4996
5237
  getTemplate,
4997
5238
  hasConflictMarkers,
4998
5239
  isExplanatoryText,
@@ -5013,6 +5254,7 @@ export {
5013
5254
  shortIdMap,
5014
5255
  splitGraphNode,
5015
5256
  startSddRun,
5016
- templateToMarkdown
5257
+ templateToMarkdown,
5258
+ tokenizeCommand
5017
5259
  };
5018
5260
  //# sourceMappingURL=index.js.map