@wrongstack/sdd 1.0.24 → 1.0.27

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
@@ -2049,10 +2049,10 @@ var AISpecBuilder = class {
2049
2049
  return;
2050
2050
  }
2051
2051
  const fsp7 = await import("node:fs/promises");
2052
- const path6 = await import("node:path");
2052
+ const path7 = await import("node:path");
2053
2053
  const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
2054
2054
  const sessionPath = expectDefined2(this.sessionPath);
2055
- await fsp7.mkdir(path6.dirname(sessionPath), { recursive: true });
2055
+ await fsp7.mkdir(path7.dirname(sessionPath), { recursive: true });
2056
2056
  await atomicWrite4(sessionPath, JSON.stringify(snapshot, null, 2));
2057
2057
  } catch (error) {
2058
2058
  console.warn(
@@ -2822,6 +2822,68 @@ import {
2822
2822
  writeKanbanWorkflowState
2823
2823
  } from "@wrongstack/kanban";
2824
2824
 
2825
+ // src/sdd-task-recovery.ts
2826
+ function computeDeadlockChains(host) {
2827
+ const tracker = host.opts.tracker;
2828
+ const chains = [];
2829
+ for (const node of tracker.getAllNodes()) {
2830
+ if (node.status === "completed" || node.status === "failed") continue;
2831
+ const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
2832
+ if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
2833
+ }
2834
+ return chains;
2835
+ }
2836
+ function recoverFailedBlockers(host) {
2837
+ const tracker = host.opts.tracker;
2838
+ let recovered = false;
2839
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
2840
+ const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
2841
+ const s = tracker.getNode(d)?.status;
2842
+ return s !== "completed" && s !== "failed";
2843
+ });
2844
+ if (blocksIncomplete) {
2845
+ host.retryMap.delete(node.id);
2846
+ host.persistRetries(node.id, 0);
2847
+ tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
2848
+ recovered = true;
2849
+ }
2850
+ }
2851
+ return recovered;
2852
+ }
2853
+ function requeueFailedTasks(host, reason = "retry failed sweep") {
2854
+ const tracker = host.opts.tracker;
2855
+ let n = 0;
2856
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
2857
+ if (host.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
2858
+ host.retryMap.delete(node.id);
2859
+ host.persistRetries(node.id, 0);
2860
+ tracker.updateNodeStatus(node.id, "pending", reason);
2861
+ host.emit("sdd.task.retrying", {
2862
+ runId: host.runId,
2863
+ taskId: node.id,
2864
+ attempt: 0,
2865
+ maxRetries: host.maxRetries
2866
+ });
2867
+ n++;
2868
+ }
2869
+ return n;
2870
+ }
2871
+ function restoreRetryMap(host) {
2872
+ host.retryMap.clear();
2873
+ for (const node of host.opts.tracker.getAllNodes()) {
2874
+ const r = node.metadata?.retries;
2875
+ if (typeof r === "number" && r > 0) host.retryMap.set(node.id, r);
2876
+ }
2877
+ }
2878
+ function resetOrphans(tracker) {
2879
+ let n = 0;
2880
+ for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
2881
+ tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
2882
+ n++;
2883
+ }
2884
+ return n;
2885
+ }
2886
+
2825
2887
  // src/sdd-parallel-run.ts
2826
2888
  import { randomUUID as randomUUID3 } from "node:crypto";
2827
2889
  import {
@@ -3363,10 +3425,10 @@ var SddParallelRun = class {
3363
3425
  /** Type-safe emit on the optional EventBus (no-op when unwired). */
3364
3426
  emit(event, payload) {
3365
3427
  const sessionId = this.currentSessionId();
3366
- this.events?.emit(
3367
- event,
3368
- { ...payload, sessionId }
3369
- );
3428
+ this.events?.emit(event, {
3429
+ ...payload,
3430
+ sessionId
3431
+ });
3370
3432
  }
3371
3433
  currentSessionId() {
3372
3434
  const value = typeof this.sessionIdSource === "function" ? this.sessionIdSource() : this.sessionIdSource;
@@ -3692,32 +3754,11 @@ var SddParallelRun = class {
3692
3754
  * included since they're the usual deadlock cause once retries are exhausted.
3693
3755
  */
3694
3756
  computeDeadlockChains() {
3695
- const tracker = this.opts.tracker;
3696
- const chains = [];
3697
- for (const node of tracker.getAllNodes()) {
3698
- if (node.status === "completed" || node.status === "failed") continue;
3699
- const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
3700
- if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
3701
- }
3702
- return chains;
3757
+ return computeDeadlockChains(this.sddTaskRecoveryHost());
3703
3758
  }
3704
3759
  /** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
3705
3760
  recoverFailedBlockers() {
3706
- const tracker = this.opts.tracker;
3707
- let recovered = false;
3708
- for (const node of tracker.getAllNodes({ status: ["failed"] })) {
3709
- const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
3710
- const s = tracker.getNode(d)?.status;
3711
- return s !== "completed" && s !== "failed";
3712
- });
3713
- if (blocksIncomplete) {
3714
- this.retryMap.delete(node.id);
3715
- this.persistRetries(node.id, 0);
3716
- tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
3717
- recovered = true;
3718
- }
3719
- }
3720
- return recovered;
3761
+ return recoverFailedBlockers(this.sddTaskRecoveryHost());
3721
3762
  }
3722
3763
  /**
3723
3764
  * Requeue every terminal-failed task that the user did NOT cancel, giving each
@@ -3725,22 +3766,7 @@ var SddParallelRun = class {
3725
3766
  * the manual "retry all failed" control. Returns the number requeued.
3726
3767
  */
3727
3768
  requeueFailedTasks(reason = "retry failed sweep") {
3728
- const tracker = this.opts.tracker;
3729
- let n = 0;
3730
- for (const node of tracker.getAllNodes({ status: ["failed"] })) {
3731
- if (this.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
3732
- this.retryMap.delete(node.id);
3733
- this.persistRetries(node.id, 0);
3734
- tracker.updateNodeStatus(node.id, "pending", reason);
3735
- this.emit("sdd.task.retrying", {
3736
- runId: this.runId,
3737
- taskId: node.id,
3738
- attempt: 0,
3739
- maxRetries: this.maxRetries
3740
- });
3741
- n++;
3742
- }
3743
- return n;
3769
+ return requeueFailedTasks(this.sddTaskRecoveryHost(), reason);
3744
3770
  }
3745
3771
  /**
3746
3772
  * Manually requeue all failed tasks to `pending` (board "Retry all failed").
@@ -3759,11 +3785,7 @@ var SddParallelRun = class {
3759
3785
  }
3760
3786
  /** Restore per-task retry counts persisted in node metadata (resume support). */
3761
3787
  restoreRetryMap() {
3762
- this.retryMap.clear();
3763
- for (const node of this.opts.tracker.getAllNodes()) {
3764
- const r = node.metadata?.retries;
3765
- if (typeof r === "number" && r > 0) this.retryMap.set(node.id, r);
3766
- }
3788
+ restoreRetryMap(this.sddTaskRecoveryHost());
3767
3789
  }
3768
3790
  /**
3769
3791
  * Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
@@ -3771,12 +3793,7 @@ var SddParallelRun = class {
3771
3793
  * from a reloaded graph. Static so callers don't need a run instance.
3772
3794
  */
3773
3795
  static resetOrphans(tracker) {
3774
- let n = 0;
3775
- for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
3776
- tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
3777
- n++;
3778
- }
3779
- return n;
3796
+ return resetOrphans(tracker);
3780
3797
  }
3781
3798
  /** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
3782
3799
  async teardown() {
@@ -4027,6 +4044,24 @@ var SddParallelRun = class {
4027
4044
  deadlocked: isDeadlocked
4028
4045
  };
4029
4046
  }
4047
+ sddTaskRecoveryHost() {
4048
+ const self = this;
4049
+ return {
4050
+ get opts() {
4051
+ return self.opts;
4052
+ },
4053
+ get retryMap() {
4054
+ return self.retryMap;
4055
+ },
4056
+ persistRetries: (...args) => this.persistRetries(...args),
4057
+ get cancelledTasks() {
4058
+ return self.cancelledTasks;
4059
+ },
4060
+ emit: (...args) => this.emit(...args),
4061
+ runId: this.runId,
4062
+ maxRetries: this.maxRetries
4063
+ };
4064
+ }
4030
4065
  };
4031
4066
 
4032
4067
  // src/start-sdd-run.ts
@@ -5048,15 +5083,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
5048
5083
  maxId = id;
5049
5084
  }
5050
5085
  }
5051
- const path6 = [];
5086
+ const path7 = [];
5052
5087
  let current = maxId;
5053
5088
  const visited = /* @__PURE__ */ new Set();
5054
5089
  while (current && !visited.has(current)) {
5055
5090
  visited.add(current);
5056
- path6.unshift(current);
5091
+ path7.unshift(current);
5057
5092
  current = prev.get(current) ?? null;
5058
5093
  }
5059
- return path6;
5094
+ return path7;
5060
5095
  }
5061
5096
  function computeParallelGroups(graph, blockedByMap) {
5062
5097
  const groups = [];
@@ -5277,6 +5312,14 @@ var SpecVersioning = class {
5277
5312
  };
5278
5313
 
5279
5314
  // src/auto-executor.ts
5315
+ function asDependentFirstGraph(graph) {
5316
+ return {
5317
+ ...graph,
5318
+ edges: graph.edges.map(
5319
+ (edge) => edge.type === "depends_on" ? { ...edge, from: edge.to, to: edge.from } : edge
5320
+ )
5321
+ };
5322
+ }
5280
5323
  var AutoExecutor = class {
5281
5324
  opts;
5282
5325
  stopped = false;
@@ -5293,7 +5336,7 @@ var AutoExecutor = class {
5293
5336
  this.stopped = false;
5294
5337
  this.retryMap.clear();
5295
5338
  const startTime = Date.now();
5296
- const critical = analyzeCriticalPath(graph);
5339
+ const critical = analyzeCriticalPath(asDependentFirstGraph(graph));
5297
5340
  let completed = 0;
5298
5341
  let failed = 0;
5299
5342
  const skipped = 0;
@@ -5325,6 +5368,8 @@ var AutoExecutor = class {
5325
5368
  this.opts.onTaskComplete?.(task, execResult);
5326
5369
  } else if (execResult.retry) {
5327
5370
  retried++;
5371
+ this.opts.tracker.updateNodeStatus(task.id, "failed", execResult.error);
5372
+ failed++;
5328
5373
  } else {
5329
5374
  this.opts.tracker.updateNodeStatus(task.id, "failed", execResult.error);
5330
5375
  failed++;
@@ -5464,6 +5509,7 @@ var SddSupervisor = class {
5464
5509
  const canSplit = Boolean(this.opts.generateSubtasks);
5465
5510
  const decision = await this.opts.brain.decide({
5466
5511
  id: `sdd-supervisor-${task.id}-${attempts}`,
5512
+ sessionId: this.opts.sessionId,
5467
5513
  source: "system",
5468
5514
  question: `SDD task "${task.title}" exhausted its retries. How should the run proceed?`,
5469
5515
  context: `Error: ${error}
@@ -5502,6 +5548,28 @@ Supervisor rescues already used: ${attempts}`,
5502
5548
 
5503
5549
  // src/verify-task.ts
5504
5550
  import { spawn } from "node:child_process";
5551
+ import { accessSync } from "node:fs";
5552
+ import * as path6 from "node:path";
5553
+ import { buildWin32CmdShimInvocation } from "@wrongstack/core/utils";
5554
+ function win32BatchTarget(executable) {
5555
+ if (/\.(?:cmd|bat)$/i.test(executable)) return executable;
5556
+ if (/[\\/]/.test(executable) || path6.extname(executable)) return void 0;
5557
+ const exts = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").toLowerCase().split(";");
5558
+ for (const dir of (process.env["PATH"] ?? "").split(path6.delimiter)) {
5559
+ if (!dir) continue;
5560
+ for (const ext of exts) {
5561
+ if (!ext) continue;
5562
+ const full = path6.join(dir, `${executable}${ext}`);
5563
+ try {
5564
+ accessSync(full);
5565
+ } catch {
5566
+ continue;
5567
+ }
5568
+ return ext === ".cmd" || ext === ".bat" ? full : void 0;
5569
+ }
5570
+ }
5571
+ return void 0;
5572
+ }
5505
5573
  function tokenizeCommand(command) {
5506
5574
  const trimmed = command.trim();
5507
5575
  if (!trimmed) return void 0;
@@ -5638,12 +5706,22 @@ function makeCommandVerifier(options = {}) {
5638
5706
  return { ok: false, reason: `verification command is malformed: ${rawCommand}` };
5639
5707
  }
5640
5708
  const [executable, ...args] = argv;
5709
+ const batch = process.platform === "win32" ? win32BatchTarget(executable) : void 0;
5710
+ let shim;
5711
+ if (batch) {
5712
+ try {
5713
+ shim = buildWin32CmdShimInvocation(batch, args);
5714
+ } catch (err) {
5715
+ return { ok: false, reason: `verification command refused: ${String(err)}` };
5716
+ }
5717
+ }
5641
5718
  return await new Promise((resolve3) => {
5642
- const child = spawn(executable, args, {
5719
+ const child = spawn(shim?.command ?? executable, shim?.args ?? args, {
5643
5720
  cwd: info.cwd,
5644
5721
  shell: false,
5645
5722
  windowsHide: true,
5646
- stdio: "ignore"
5723
+ stdio: "ignore",
5724
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
5647
5725
  });
5648
5726
  let timedOut = false;
5649
5727
  const timer = setTimeout(() => {
@@ -5819,12 +5897,12 @@ async function decomposeNonAtomicTasks(opts) {
5819
5897
 
5820
5898
  // src/conflict-resolver.ts
5821
5899
  import { readFile as readFile6, writeFile } from "node:fs/promises";
5822
- import { isAbsolute as isAbsolute3, join as join6 } from "node:path";
5900
+ import { isAbsolute as isAbsolute3, join as join7 } from "node:path";
5823
5901
  import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
5824
5902
  var defaultFileIO = {
5825
- read: (path6) => readFile6(path6, "utf8"),
5826
- write: async (path6, content) => {
5827
- await writeFile(path6, content, "utf8");
5903
+ read: (path7) => readFile6(path7, "utf8"),
5904
+ write: async (path7, content) => {
5905
+ await writeFile(path7, content, "utf8");
5828
5906
  }
5829
5907
  };
5830
5908
  var START = "<<<<<<<";
@@ -5868,7 +5946,7 @@ function makePreferSideConflictResolver(side, io = defaultFileIO) {
5868
5946
  return async function conflictResolver(info) {
5869
5947
  if (info.conflictFiles.length === 0) return false;
5870
5948
  for (const rel of info.conflictFiles) {
5871
- const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
5949
+ const abs = isAbsolute3(rel) ? rel : join7(info.cwd, rel);
5872
5950
  let content;
5873
5951
  try {
5874
5952
  content = await io.read(abs);
@@ -5902,7 +5980,7 @@ function makeLlmConflictResolver(opts) {
5902
5980
  return async function conflictResolver(info) {
5903
5981
  if (info.conflictFiles.length === 0) return false;
5904
5982
  for (const rel of info.conflictFiles) {
5905
- const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
5983
+ const abs = isAbsolute3(rel) ? rel : join7(info.cwd, rel);
5906
5984
  let content;
5907
5985
  try {
5908
5986
  content = await io.read(abs);
@@ -1,22 +1,3 @@
1
- /**
2
- * SddParallelRun
3
- *
4
- * Drives a TaskGraph through ParallelEternalEngine's infrastructure
5
- * (DefaultMultiAgentCoordinator + AgentSubagentRunner) but powered by
6
- * SddTaskDecomposer — producing dependency-aware waves instead of
7
- * goal-driven iterations.
8
- *
9
- * One-shot: completes when all tasks are done OR a deadlock is detected.
10
- * Does NOT loop — each run() call is a discrete execution.
11
- *
12
- * Usage:
13
- * ```
14
- * const run = new SddParallelRun({ tracker, graph, agent, projectRoot });
15
- * await run.run({ onWave });
16
- * // or with progress callback:
17
- * await run.run({ onProgress: (p) => console.log(renderProgress(p)) });
18
- * ```
19
- */
20
1
  import type { TaskTracker } from '@wrongstack/core/tasking';
21
2
  import type { TaskNode } from '@wrongstack/core/types';
22
3
  import type { RunResult, SddParallelRunOptions, SddSubtaskSpec, TaskOutcome, WaveResult } from './sdd-parallel-run-types.js';
@@ -270,5 +251,6 @@ export declare class SddParallelRun {
270
251
  /** Persist a task's retry count into node metadata (survives crash → resume). */
271
252
  private persistRetries;
272
253
  private buildProgress;
254
+ private sddTaskRecoveryHost;
273
255
  }
274
256
  //# sourceMappingURL=sdd-parallel-run.d.ts.map
@@ -4,6 +4,8 @@ import type { SddSubtaskSpec, SddSupervisorVerdict } from './sdd-parallel-run.js
4
4
  export interface SddSupervisorOptions {
5
5
  /** Decision authority (policy/LLM/human). Reuse the session's TOKENS.BrainArbiter. */
6
6
  brain: BrainArbiter;
7
+ /** Session that started this run, retained when another tab becomes foreground. */
8
+ sessionId?: string | undefined;
7
9
  /**
8
10
  * Models to rotate through on a `reassign` verdict (e.g. the run's fallback
9
11
  * chain). Omit to drop the reassign option entirely.
@@ -0,0 +1,20 @@
1
+ import type { TaskTracker } from '@wrongstack/core/tasking';
2
+ import type { SddParallelRunOptions } from './sdd-parallel-run-types.js';
3
+ export interface SddTaskRecoveryHost {
4
+ opts: SddParallelRunOptions;
5
+ retryMap: Map<string, number>;
6
+ persistRetries: (taskId: string, retries: number) => void;
7
+ cancelledTasks: Set<string>;
8
+ emit: <K extends keyof import('@wrongstack/core/kernel').EventMap>(event: K, payload: import('@wrongstack/core/kernel').EventMap[K]) => void;
9
+ runId: string;
10
+ maxRetries: number;
11
+ }
12
+ export declare function computeDeadlockChains(host: SddTaskRecoveryHost): Array<{
13
+ blocked: string;
14
+ blockedBy: string[];
15
+ }>;
16
+ export declare function recoverFailedBlockers(host: SddTaskRecoveryHost): boolean;
17
+ export declare function requeueFailedTasks(host: SddTaskRecoveryHost, reason?: string): number;
18
+ export declare function restoreRetryMap(host: SddTaskRecoveryHost): void;
19
+ export declare function resetOrphans(tracker: TaskTracker): number;
20
+ //# sourceMappingURL=sdd-task-recovery.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/sdd",
3
- "version": "1.0.24",
3
+ "version": "1.0.27",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Spec-Driven Development engine — standalone package extracted from @wrongstack/core. Task graph generation, tracking, execution, lifecycle management, and AI-driven spec building for SDD workflows.",
6
6
  "repository": {
@@ -27,10 +27,10 @@
27
27
  "!dist/**/*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@wrongstack/core": "1.0.24",
31
- "@wrongstack/kanban": "1.0.24",
32
- "@wrongstack/primitives": "1.0.24",
33
- "@wrongstack/requirement-intake": "1.0.24"
30
+ "@wrongstack/core": "1.0.27",
31
+ "@wrongstack/kanban": "1.0.27",
32
+ "@wrongstack/primitives": "1.0.27",
33
+ "@wrongstack/requirement-intake": "1.0.27"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.5.1",