@wrongstack/sdd 0.296.4 → 0.297.0

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
@@ -424,7 +424,7 @@ ${acLines}` : "") + blockedLine;
424
424
  // src/index.ts
425
425
  import {
426
426
  TaskTracker as TaskTracker3,
427
- DefaultTaskStore as DefaultTaskStore3
427
+ DefaultTaskStore as DefaultTaskStore2
428
428
  } from "@wrongstack/core/tasking";
429
429
 
430
430
  // src/task-flow.ts
@@ -755,17 +755,24 @@ function graphFromJSON(raw) {
755
755
  var TaskGraphStore = class {
756
756
  baseDir;
757
757
  indexPath;
758
+ writeChain = Promise.resolve();
758
759
  constructor(opts) {
759
760
  this.baseDir = opts.baseDir;
760
761
  this.indexPath = path2.join(this.baseDir, "_index.json");
761
762
  }
762
763
  async save(graph) {
763
- await ensureDir2(this.baseDir);
764
- const filePath = this.filePath(graph.id);
765
- await atomicWrite2(filePath, graphToJSON(graph), { mode: 384 });
766
- await this.updateIndex(graph);
764
+ const snapshot = graphFromJSON(graphToJSON(graph));
765
+ const pending = this.writeChain.then(async () => {
766
+ await ensureDir2(this.baseDir);
767
+ const filePath = this.filePath(snapshot.id);
768
+ await atomicWrite2(filePath, graphToJSON(snapshot), { mode: 384 });
769
+ await this.updateIndex(snapshot);
770
+ });
771
+ this.writeChain = pending.catch(() => void 0);
772
+ await pending;
767
773
  }
768
774
  async load(id) {
775
+ await this.writeChain;
769
776
  try {
770
777
  const raw = await fsp2.readFile(this.filePath(id), "utf8");
771
778
  return graphFromJSON(raw);
@@ -774,10 +781,12 @@ var TaskGraphStore = class {
774
781
  }
775
782
  }
776
783
  async list() {
784
+ await this.writeChain;
777
785
  const index = await this.readIndex();
778
786
  return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
779
787
  }
780
788
  async delete(id) {
789
+ await this.writeChain;
781
790
  try {
782
791
  await fsp2.unlink(this.filePath(id));
783
792
  await this.removeFromIndex(id);
@@ -787,6 +796,7 @@ var TaskGraphStore = class {
787
796
  }
788
797
  }
789
798
  async exists(id) {
799
+ await this.writeChain;
790
800
  try {
791
801
  await fsp2.access(this.filePath(id));
792
802
  return true;
@@ -794,6 +804,18 @@ var TaskGraphStore = class {
794
804
  return false;
795
805
  }
796
806
  }
807
+ saveGraph(graph) {
808
+ return this.save(graph);
809
+ }
810
+ loadGraph(id) {
811
+ return this.load(id);
812
+ }
813
+ async listGraphs() {
814
+ return (await this.list()).map(({ id, title, updatedAt }) => ({ id, title, updatedAt }));
815
+ }
816
+ async deleteGraph(id) {
817
+ await this.delete(id);
818
+ }
797
819
  filePath(id) {
798
820
  return path2.join(this.baseDir, `${id}.json`);
799
821
  }
@@ -1007,7 +1029,7 @@ var SddBoardStore = class {
1007
1029
  await write;
1008
1030
  if (this.eventChains.get(filePath) === write) this.eventChains.delete(filePath);
1009
1031
  }
1010
- /** Append a control command (used by readers to steer a CLI-owned run). */
1032
+ /** Append a legacy control command. Production readers use Kanban IPC. */
1011
1033
  async appendControl(runId, command) {
1012
1034
  await this.ensureBaseDir();
1013
1035
  const filePath = this.controlPath(runId);
@@ -1017,7 +1039,7 @@ var SddBoardStore = class {
1017
1039
  `, { mode: 384 })
1018
1040
  );
1019
1041
  }
1020
- /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
1042
+ /** Read + truncate the legacy control queue for one-time migration. */
1021
1043
  async drainControl(runId) {
1022
1044
  const filePath = this.controlPath(runId);
1023
1045
  const active = this.controlDrains.get(filePath);
@@ -1612,11 +1634,20 @@ var SddRunRegistry = class {
1612
1634
  };
1613
1635
 
1614
1636
  // src/sdd-interview-driver.ts
1615
- import { DefaultTaskStore as DefaultTaskStore2, TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
1637
+ import { TaskTracker as TaskTracker2 } from "@wrongstack/core/tasking";
1616
1638
 
1617
1639
  // src/spec-builder.ts
1618
1640
  import { ERROR_CODES as ERROR_CODES2, SddError as SddError2 } from "@wrongstack/core/types";
1619
1641
  import { expectDefined as expectDefined2, toErrorMessage } from "@wrongstack/core/utils";
1642
+
1643
+ // src/sdd-session-types.ts
1644
+ function isAISpecSession(value) {
1645
+ if (!value || typeof value !== "object") return false;
1646
+ const session = value;
1647
+ return typeof session.id === "string" && typeof session.phase === "string" && typeof session.title === "string" && typeof session.userIntent === "string" && Array.isArray(session.answers) && typeof session.updatedAt === "number";
1648
+ }
1649
+
1650
+ // src/spec-builder.ts
1620
1651
  function buildQuestioningPrompt(session, min, max) {
1621
1652
  const answered = session.answers.length;
1622
1653
  const remaining = Math.max(0, min - answered);
@@ -1813,11 +1844,13 @@ var AISpecBuilder = class {
1813
1844
  minQuestions;
1814
1845
  maxQuestions;
1815
1846
  sessionPath;
1847
+ sessionPersistence;
1816
1848
  constructor(opts) {
1817
1849
  this.store = opts.store;
1818
1850
  this.minQuestions = opts.minQuestions ?? 2;
1819
1851
  this.maxQuestions = opts.maxQuestions ?? 10;
1820
1852
  this.sessionPath = opts.sessionPath;
1853
+ this.sessionPersistence = opts.sessionPersistence;
1821
1854
  this.session = {
1822
1855
  id: crypto.randomUUID(),
1823
1856
  phase: "questioning",
@@ -1832,27 +1865,47 @@ var AISpecBuilder = class {
1832
1865
  };
1833
1866
  }
1834
1867
  // ── Session Persistence ──────────────────────────────────────────────────
1835
- /** Save session state to disk. */
1868
+ /** Save session state to the configured durable owner. */
1836
1869
  async saveSession() {
1837
- if (!this.sessionPath) return;
1870
+ if (!this.sessionPersistence && !this.sessionPath) return;
1838
1871
  try {
1839
- const fsp6 = await import("node:fs/promises");
1872
+ if (this.sessionPersistence) {
1873
+ await this.sessionPersistence.save(structuredClone(this.session));
1874
+ return;
1875
+ }
1876
+ const fsp7 = await import("node:fs/promises");
1840
1877
  const path6 = await import("node:path");
1841
1878
  const { atomicWrite: atomicWrite4 } = await import("@wrongstack/core/utils");
1842
- await fsp6.mkdir(path6.dirname(this.sessionPath), { recursive: true });
1843
- await atomicWrite4(this.sessionPath, JSON.stringify(this.session, null, 2));
1879
+ const sessionPath = expectDefined2(this.sessionPath);
1880
+ await fsp7.mkdir(path6.dirname(sessionPath), { recursive: true });
1881
+ await atomicWrite4(sessionPath, JSON.stringify(this.session, null, 2));
1844
1882
  } catch (error) {
1845
- console.warn(JSON.stringify({ level: "warn", event: "sdd.persist.failed", message: String(error), timestamp: Date.now() }));
1883
+ console.warn(
1884
+ JSON.stringify({
1885
+ level: "warn",
1886
+ event: "sdd.persist.failed",
1887
+ message: String(error),
1888
+ timestamp: Date.now()
1889
+ })
1890
+ );
1846
1891
  }
1847
1892
  }
1848
- /** Load session state from disk. Returns true if a session was loaded. */
1893
+ /** Load session state from the configured durable owner. */
1849
1894
  async loadSession() {
1895
+ if (this.sessionPersistence) {
1896
+ const loaded = await this.sessionPersistence.load();
1897
+ if (isAISpecSession(loaded)) {
1898
+ this.session = loaded;
1899
+ return true;
1900
+ }
1901
+ return false;
1902
+ }
1850
1903
  if (!this.sessionPath) return false;
1851
1904
  try {
1852
- const fsp6 = await import("node:fs/promises");
1853
- const raw = await fsp6.readFile(this.sessionPath, "utf8");
1905
+ const fsp7 = await import("node:fs/promises");
1906
+ const raw = await fsp7.readFile(this.sessionPath, "utf8");
1854
1907
  const loaded = JSON.parse(raw);
1855
- if (loaded?.id && loaded?.phase && loaded?.title) {
1908
+ if (isAISpecSession(loaded)) {
1856
1909
  this.session = loaded;
1857
1910
  return true;
1858
1911
  }
@@ -1860,12 +1913,16 @@ var AISpecBuilder = class {
1860
1913
  }
1861
1914
  return false;
1862
1915
  }
1863
- /** Delete saved session from disk. */
1916
+ /** Delete the saved session from the configured durable owner. */
1864
1917
  async deleteSession() {
1918
+ if (this.sessionPersistence) {
1919
+ await this.sessionPersistence.delete();
1920
+ return;
1921
+ }
1865
1922
  if (!this.sessionPath) return;
1866
1923
  try {
1867
- const fsp6 = await import("node:fs/promises");
1868
- await fsp6.unlink(this.sessionPath);
1924
+ const fsp7 = await import("node:fs/promises");
1925
+ await fsp7.unlink(this.sessionPath);
1869
1926
  } catch {
1870
1927
  }
1871
1928
  }
@@ -2230,7 +2287,7 @@ var SddInterviewDriver = class {
2230
2287
  maxQuestions;
2231
2288
  tracker = null;
2232
2289
  graph = null;
2233
- /** Set when {@link loadExisting} successfully rehydrated a session from disk. */
2290
+ /** Set when {@link loadExisting} successfully rehydrated a durable session. */
2234
2291
  resumedFromDisk = false;
2235
2292
  constructor(opts) {
2236
2293
  this.o = opts;
@@ -2239,6 +2296,7 @@ var SddInterviewDriver = class {
2239
2296
  this.builder = new AISpecBuilder({
2240
2297
  store: opts.specStore,
2241
2298
  sessionPath: opts.sessionPath,
2299
+ sessionPersistence: opts.sessionPersistence,
2242
2300
  projectContext: opts.projectContext,
2243
2301
  minQuestions: this.minQuestions,
2244
2302
  maxQuestions: this.maxQuestions
@@ -2254,7 +2312,7 @@ var SddInterviewDriver = class {
2254
2312
  return this.builder.getAIPrompt();
2255
2313
  }
2256
2314
  /**
2257
- * Resume a previously-persisted interview from disk. Re-hydrates the task
2315
+ * Resume a previously-persisted interview. Re-hydrates the task
2258
2316
  * graph too when one was already produced. Returns true if a session loaded.
2259
2317
  */
2260
2318
  async loadExisting() {
@@ -2265,7 +2323,7 @@ var SddInterviewDriver = class {
2265
2323
  const graph = await this.o.graphStore.load(graphId);
2266
2324
  if (graph) {
2267
2325
  this.graph = graph;
2268
- const tracker = new TaskTracker2({ store: new DefaultTaskStore2() });
2326
+ const tracker = new TaskTracker2({ store: this.o.graphStore });
2269
2327
  tracker.setGraph(graph);
2270
2328
  this.tracker = tracker;
2271
2329
  }
@@ -2273,7 +2331,7 @@ var SddInterviewDriver = class {
2273
2331
  this.resumedFromDisk = true;
2274
2332
  return true;
2275
2333
  }
2276
- /** Drop the on-disk session (if any) and clear in-memory interview state. */
2334
+ /** Drop the durable session (if any) and clear in-memory interview state. */
2277
2335
  async discard() {
2278
2336
  await this.builder.deleteSession();
2279
2337
  this.builder.resetForNewInterview();
@@ -2371,7 +2429,7 @@ var SddInterviewDriver = class {
2371
2429
  if (this.graph) return this.graph;
2372
2430
  const spec = this.builder.getSession().spec;
2373
2431
  if (!spec) return null;
2374
- const tracker = new TaskTracker2({ store: new DefaultTaskStore2() });
2432
+ const tracker = new TaskTracker2({ store: this.o.graphStore });
2375
2433
  const generator = new TaskGenerator({
2376
2434
  taskTracker: tracker,
2377
2435
  verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
@@ -2467,7 +2525,7 @@ var SddInterviewDriver = class {
2467
2525
  if (valid.length === 0) return void 0;
2468
2526
  const spec = this.builder.getSession().spec;
2469
2527
  if (!this.tracker) {
2470
- const tracker2 = new TaskTracker2({ store: new DefaultTaskStore2() });
2528
+ const tracker2 = new TaskTracker2({ store: this.o.graphStore });
2471
2529
  this.graph = await tracker2.createGraph(spec.id, spec.title);
2472
2530
  this.tracker = tracker2;
2473
2531
  }
@@ -2521,6 +2579,12 @@ function isExplanatoryText(text) {
2521
2579
 
2522
2580
  // src/start-sdd-run.ts
2523
2581
  import { TOKENS } from "@wrongstack/core/kernel";
2582
+ import {
2583
+ drainKanbanWorkflowCommands,
2584
+ kanbanWorkflowId,
2585
+ subscribeKanbanWorkflowCommands,
2586
+ writeKanbanWorkflowState
2587
+ } from "@wrongstack/kanban";
2524
2588
 
2525
2589
  // src/sdd-parallel-run.ts
2526
2590
  import { randomUUID as randomUUID3 } from "node:crypto";
@@ -3689,12 +3753,23 @@ function startSddRun(opts) {
3689
3753
  defaultProvider: opts.defaultProvider,
3690
3754
  fallbackModels: opts.fallbackModels
3691
3755
  });
3756
+ const workflowId = kanbanWorkflowId("sdd", run.runId);
3757
+ const legacyControl = opts.controlTransport === "legacy-file";
3758
+ const legacyBoardState = opts.boardStateTransport === "legacy-file" || opts.boardStateTransport === void 0 && legacyControl;
3759
+ const boardPersistence = legacyBoardState ? opts.boardStore : {
3760
+ saveSnapshot: async (snapshot) => {
3761
+ await writeKanbanWorkflowState(opts.projectRoot, workflowId, snapshot);
3762
+ },
3763
+ // Detailed events remain append-only audit artifacts; they are not read
3764
+ // back as workflow authority.
3765
+ appendEvent: (runId, event) => opts.boardStore.appendEvent(runId, event)
3766
+ };
3692
3767
  const projector = new SddBoardProjector({
3693
3768
  runId: run.runId,
3694
3769
  graph: opts.graph,
3695
3770
  tracker: opts.tracker,
3696
3771
  events: opts.events,
3697
- store: opts.boardStore,
3772
+ store: boardPersistence,
3698
3773
  sessionId: opts.sessionId,
3699
3774
  specId: opts.graph.specId,
3700
3775
  defaultModel: opts.defaultModel,
@@ -3724,21 +3799,82 @@ function startSddRun(opts) {
3724
3799
  snapshot: () => projector.snapshot(),
3725
3800
  isRunning: () => run.isRunning()
3726
3801
  });
3727
- const drainMs = opts.controlDrainMs ?? 500;
3728
- const controlTimer = setInterval(() => {
3729
- void opts.boardStore.drainControl(run.runId).then((cmds) => {
3730
- for (const c of cmds) {
3731
- applySddControlCommand(run, c);
3802
+ let controlDrainInFlight = false;
3803
+ let controlDisposed = false;
3804
+ let unsubscribeControl;
3805
+ const drainControl = async () => {
3806
+ if (controlDrainInFlight || controlDisposed) return;
3807
+ controlDrainInFlight = true;
3808
+ try {
3809
+ const commands = legacyControl ? await opts.boardStore.drainControl(run.runId) : await drainKanbanWorkflowCommands(opts.projectRoot, workflowId);
3810
+ for (const command of commands) applySddControlCommand(run, command);
3811
+ } catch (error) {
3812
+ const message = error instanceof Error ? error.message : String(error);
3813
+ console.warn(
3814
+ JSON.stringify({
3815
+ level: "warn",
3816
+ event: "sdd.control_drain_failed",
3817
+ runId: run.runId,
3818
+ workflowId,
3819
+ transport: legacyControl ? "legacy-file" : "kanban",
3820
+ message,
3821
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3822
+ })
3823
+ );
3824
+ } finally {
3825
+ controlDrainInFlight = false;
3826
+ }
3827
+ };
3828
+ if (!legacyControl) {
3829
+ void subscribeKanbanWorkflowCommands(opts.projectRoot, workflowId, () => {
3830
+ void drainControl().catch(() => void 0);
3831
+ }).then((unsubscribe) => {
3832
+ if (controlDisposed) unsubscribe();
3833
+ else {
3834
+ unsubscribeControl = unsubscribe;
3835
+ void drainControl().catch(() => void 0);
3732
3836
  }
3733
- }).catch(() => {
3837
+ }).catch((error) => {
3838
+ console.warn(
3839
+ JSON.stringify({
3840
+ level: "warn",
3841
+ event: "sdd.control_subscribe_failed",
3842
+ runId: run.runId,
3843
+ workflowId,
3844
+ message: error instanceof Error ? error.message : String(error),
3845
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3846
+ })
3847
+ );
3848
+ });
3849
+ } else {
3850
+ void opts.boardStore.drainControl(run.runId).then((commands) => {
3851
+ for (const command of commands) applySddControlCommand(run, command);
3852
+ }).catch((error) => {
3853
+ console.warn(
3854
+ JSON.stringify({
3855
+ level: "warn",
3856
+ event: "sdd.control_drain_failed",
3857
+ runId: run.runId,
3858
+ workflowId,
3859
+ transport: "legacy-file",
3860
+ message: error instanceof Error ? error.message : String(error),
3861
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3862
+ })
3863
+ );
3734
3864
  });
3865
+ }
3866
+ const drainMs = opts.controlDrainMs ?? 500;
3867
+ const controlTimer = setInterval(() => {
3868
+ void drainControl().catch(() => void 0);
3735
3869
  }, drainMs);
3736
3870
  controlTimer.unref?.();
3737
3871
  const completion = (async () => {
3738
3872
  try {
3739
3873
  return await run.run();
3740
3874
  } finally {
3875
+ controlDisposed = true;
3741
3876
  clearInterval(controlTimer);
3877
+ unsubscribeControl?.();
3742
3878
  await projector.drain().catch(() => {
3743
3879
  });
3744
3880
  projector.dispose();
@@ -3759,7 +3895,57 @@ import * as fsp4 from "node:fs/promises";
3759
3895
  import * as path4 from "node:path";
3760
3896
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3761
3897
  import { WorktreeManager } from "@wrongstack/core/worktree";
3762
- import { listBoards, removeBoard } from "@wrongstack/kanban";
3898
+ import {
3899
+ deleteKanbanWorkflowState,
3900
+ kanbanWorkflowId as kanbanWorkflowId2,
3901
+ listBoards,
3902
+ listKanbanWorkflowStates,
3903
+ readKanbanWorkflowState,
3904
+ removeBoard,
3905
+ writeKanbanWorkflowState as writeKanbanWorkflowState2
3906
+ } from "@wrongstack/kanban";
3907
+ async function listSddSnapshots(projectRoot, boardsDir, transport) {
3908
+ const legacyStore = new SddBoardStore({ baseDir: boardsDir });
3909
+ if (transport === "kanban") {
3910
+ const states = await listKanbanWorkflowStates(projectRoot, "sdd:");
3911
+ const snapshots = states.map((state) => state.value).filter(isSddBoardSnapshot).sort((a, b) => b.updatedAt - a.updatedAt);
3912
+ if (snapshots.length > 0) return snapshots;
3913
+ const legacy = await loadLegacySnapshots(legacyStore);
3914
+ for (const snapshot of legacy) {
3915
+ await writeKanbanWorkflowState2(
3916
+ projectRoot,
3917
+ kanbanWorkflowId2("sdd", snapshot.runId),
3918
+ snapshot
3919
+ );
3920
+ }
3921
+ return legacy;
3922
+ }
3923
+ return loadLegacySnapshots(legacyStore);
3924
+ }
3925
+ async function loadSddSnapshot(projectRoot, boardsDir, runId, transport) {
3926
+ const legacyStore = new SddBoardStore({ baseDir: boardsDir });
3927
+ if (transport !== "kanban") return legacyStore.load(runId);
3928
+ const state = await readKanbanWorkflowState(projectRoot, kanbanWorkflowId2("sdd", runId));
3929
+ if (isSddBoardSnapshot(state?.value)) return state.value;
3930
+ const legacy = await legacyStore.load(runId);
3931
+ if (legacy) {
3932
+ await writeKanbanWorkflowState2(projectRoot, kanbanWorkflowId2("sdd", runId), legacy);
3933
+ }
3934
+ return legacy;
3935
+ }
3936
+ async function loadLegacySnapshots(store) {
3937
+ const snapshots = [];
3938
+ for (const entry of await store.list()) {
3939
+ const snapshot = await store.load(entry.runId);
3940
+ if (snapshot) snapshots.push(snapshot);
3941
+ }
3942
+ return snapshots.sort((a, b) => b.updatedAt - a.updatedAt);
3943
+ }
3944
+ function isSddBoardSnapshot(value) {
3945
+ if (!value || typeof value !== "object") return false;
3946
+ const snapshot = value;
3947
+ return typeof snapshot.runId === "string" && typeof snapshot.updatedAt === "number" && typeof snapshot.status === "string" && Array.isArray(snapshot.tasks);
3948
+ }
3763
3949
  async function cleanupSddWorktrees(projectRoot) {
3764
3950
  const wt = new WorktreeManager({ projectRoot });
3765
3951
  return wt.cleanupAllManaged();
@@ -3770,8 +3956,17 @@ async function cleanupStaleWorktrees(projectRoot) {
3770
3956
  }
3771
3957
  async function cleanupStaleSddWorktrees(opts) {
3772
3958
  const now = opts.now?.() ?? Date.now();
3773
- const store = new SddBoardStore({ baseDir: opts.boardsDir });
3774
- const latest = (await store.list())[0];
3959
+ let latest;
3960
+ try {
3961
+ latest = (await listSddSnapshots(opts.projectRoot, opts.boardsDir, opts.stateTransport))[0];
3962
+ } catch {
3963
+ return {
3964
+ swept: false,
3965
+ removed: 0,
3966
+ detected: 0,
3967
+ skippedReason: "SDD workflow state is unavailable"
3968
+ };
3969
+ }
3775
3970
  if (latest) {
3776
3971
  const age = now - latest.updatedAt;
3777
3972
  if (latest.status === "running" && age < (opts.runningLiveMs ?? 12e4)) {
@@ -3795,10 +3990,10 @@ async function cleanupStaleSddWorktrees(opts) {
3795
3990
  }
3796
3991
  }
3797
3992
  async function rollbackSddRunFromDisk(opts) {
3798
- const store = new SddBoardStore({ baseDir: opts.boardsDir });
3799
- const runId = opts.runId ?? (await store.list())[0]?.runId;
3993
+ const snapshots = await listSddSnapshots(opts.projectRoot, opts.boardsDir, opts.stateTransport);
3994
+ const runId = opts.runId ?? snapshots[0]?.runId;
3800
3995
  if (!runId) return { ok: false, reverted: 0, reason: "no SDD board found to roll back" };
3801
- const snap = await store.load(runId);
3996
+ const snap = snapshots.find((snapshot) => snapshot.runId === runId) ?? await loadSddSnapshot(opts.projectRoot, opts.boardsDir, runId, opts.stateTransport);
3802
3997
  if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
3803
3998
  if (!snap.baseBranch) {
3804
3999
  return {
@@ -3822,7 +4017,8 @@ async function destroySddProject(opts) {
3822
4017
  const r = await rollbackSddRunFromDisk({
3823
4018
  projectRoot: opts.projectRoot,
3824
4019
  boardsDir: opts.paths.projectSddBoards,
3825
- runId: opts.runId
4020
+ runId: opts.runId,
4021
+ stateTransport: opts.stateTransport
3826
4022
  }).catch((err) => ({ ok: false, reverted: 0, reason: toErrorMessage2(err) }));
3827
4023
  reverted = r.reverted;
3828
4024
  revertOk = r.ok;
@@ -3852,6 +4048,16 @@ async function destroySddProject(opts) {
3852
4048
  await rmDir(opts.paths.projectSpecs, "specs");
3853
4049
  await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
3854
4050
  await rmDir(opts.paths.projectSddBoards, "boards");
4051
+ if (opts.stateTransport === "kanban") {
4052
+ const states = await listKanbanWorkflowStates(opts.projectRoot, "sdd:").catch(() => []);
4053
+ let removedStates = 0;
4054
+ for (const state of states) {
4055
+ if (await deleteKanbanWorkflowState(opts.projectRoot, state.workflowId).catch(() => false)) {
4056
+ removedStates++;
4057
+ }
4058
+ }
4059
+ if (removedStates > 0) deleted.push(`workflow-states(${removedStates})`);
4060
+ }
3855
4061
  try {
3856
4062
  const mirrors = (await listBoards(opts.projectRoot)).filter((b) => b.tags?.includes("sdd"));
3857
4063
  let mirrorsRemoved = 0;
@@ -3873,7 +4079,8 @@ async function applySddLifecycle(op, opts) {
3873
4079
  const r2 = await rollbackSddRunFromDisk({
3874
4080
  projectRoot: opts.projectRoot,
3875
4081
  boardsDir: opts.paths.projectSddBoards,
3876
- runId: opts.runId
4082
+ runId: opts.runId,
4083
+ stateTransport: opts.stateTransport
3877
4084
  });
3878
4085
  return { op, ok: r2.ok, reverted: r2.reverted, reason: r2.reason };
3879
4086
  }
@@ -3881,7 +4088,8 @@ async function applySddLifecycle(op, opts) {
3881
4088
  projectRoot: opts.projectRoot,
3882
4089
  paths: opts.paths,
3883
4090
  revertMerged: opts.revertMerged,
3884
- runId: opts.runId
4091
+ runId: opts.runId,
4092
+ stateTransport: opts.stateTransport
3885
4093
  });
3886
4094
  return {
3887
4095
  op,
@@ -3898,15 +4106,85 @@ async function applySddLifecycle(op, opts) {
3898
4106
  }
3899
4107
  }
3900
4108
 
3901
- // src/project-context.ts
4109
+ // src/kanban-sdd-session.ts
3902
4110
  import * as fsp5 from "node:fs/promises";
4111
+ import {
4112
+ deleteKanbanWorkflowState as deleteKanbanWorkflowState2,
4113
+ kanbanWorkflowId as kanbanWorkflowId3,
4114
+ readKanbanWorkflowState as readKanbanWorkflowState2,
4115
+ writeKanbanWorkflowState as writeKanbanWorkflowState3
4116
+ } from "@wrongstack/kanban";
4117
+ var SDD_SESSION_WORKFLOW_ID = kanbanWorkflowId3("sdd", "session");
4118
+ function createKanbanSddSessionPersistence(projectRoot, legacySessionPath) {
4119
+ let revision;
4120
+ let writeChain = Promise.resolve();
4121
+ return {
4122
+ async load() {
4123
+ await writeChain;
4124
+ const state = await readKanbanWorkflowState2(projectRoot, SDD_SESSION_WORKFLOW_ID);
4125
+ if (state) {
4126
+ revision = state.revision;
4127
+ return isAISpecSession(state.value) ? structuredClone(state.value) : null;
4128
+ }
4129
+ const legacy = await readLegacySession(legacySessionPath);
4130
+ if (!legacy) {
4131
+ revision = 0;
4132
+ return null;
4133
+ }
4134
+ const imported = await writeKanbanWorkflowState3(
4135
+ projectRoot,
4136
+ SDD_SESSION_WORKFLOW_ID,
4137
+ legacy,
4138
+ 0
4139
+ );
4140
+ revision = imported.revision;
4141
+ if (legacySessionPath) await fsp5.unlink(legacySessionPath).catch(() => void 0);
4142
+ return structuredClone(legacy);
4143
+ },
4144
+ async save(session) {
4145
+ const pending = writeChain.then(async () => {
4146
+ if (revision === void 0) {
4147
+ const current = await readKanbanWorkflowState2(projectRoot, SDD_SESSION_WORKFLOW_ID);
4148
+ revision = current?.revision ?? 0;
4149
+ }
4150
+ const saved = await writeKanbanWorkflowState3(
4151
+ projectRoot,
4152
+ SDD_SESSION_WORKFLOW_ID,
4153
+ session,
4154
+ revision
4155
+ );
4156
+ revision = saved.revision;
4157
+ });
4158
+ writeChain = pending.catch(() => void 0);
4159
+ await pending;
4160
+ },
4161
+ async delete() {
4162
+ await writeChain;
4163
+ await deleteKanbanWorkflowState2(projectRoot, SDD_SESSION_WORKFLOW_ID);
4164
+ revision = 0;
4165
+ if (legacySessionPath) await fsp5.unlink(legacySessionPath).catch(() => void 0);
4166
+ }
4167
+ };
4168
+ }
4169
+ async function readLegacySession(sessionPath) {
4170
+ if (!sessionPath) return null;
4171
+ try {
4172
+ const value = JSON.parse(await fsp5.readFile(sessionPath, "utf8"));
4173
+ return isAISpecSession(value) ? value : null;
4174
+ } catch {
4175
+ return null;
4176
+ }
4177
+ }
4178
+
4179
+ // src/project-context.ts
4180
+ import * as fsp6 from "node:fs/promises";
3903
4181
  import * as path5 from "node:path";
3904
4182
  async function gatherProjectContext(projectRoot) {
3905
4183
  const parts = [];
3906
4184
  const root = projectRoot.trim() || process.cwd();
3907
4185
  try {
3908
4186
  const pkgPath = path5.join(root, "package.json");
3909
- const pkgRaw = await fsp5.readFile(pkgPath, "utf8");
4187
+ const pkgRaw = await fsp6.readFile(pkgPath, "utf8");
3910
4188
  const pkg = JSON.parse(pkgRaw);
3911
4189
  parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
3912
4190
  parts.push(`Description: ${String(pkg.description ?? "none")}`);
@@ -3923,20 +4201,20 @@ async function gatherProjectContext(projectRoot) {
3923
4201
  } catch {
3924
4202
  }
3925
4203
  try {
3926
- await fsp5.access(path5.join(root, "tsconfig.json"));
4204
+ await fsp6.access(path5.join(root, "tsconfig.json"));
3927
4205
  parts.push("Language: TypeScript");
3928
4206
  } catch {
3929
4207
  }
3930
4208
  try {
3931
4209
  const srcDir = path5.join(root, "src");
3932
- const entries = await fsp5.readdir(srcDir, { withFileTypes: true });
4210
+ const entries = await fsp6.readdir(srcDir, { withFileTypes: true });
3933
4211
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3934
4212
  if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
3935
4213
  } catch {
3936
4214
  }
3937
4215
  try {
3938
4216
  const packagesDir = path5.join(root, "packages");
3939
- const entries = await fsp5.readdir(packagesDir, { withFileTypes: true });
4217
+ const entries = await fsp6.readdir(packagesDir, { withFileTypes: true });
3940
4218
  const pkgs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3941
4219
  if (pkgs.length > 0) {
3942
4220
  parts.push(
@@ -5081,11 +5359,11 @@ async function decomposeNonAtomicTasks(opts) {
5081
5359
  }
5082
5360
 
5083
5361
  // src/conflict-resolver.ts
5084
- import { readFile as readFile5, writeFile } from "node:fs/promises";
5362
+ import { readFile as readFile6, writeFile } from "node:fs/promises";
5085
5363
  import { isAbsolute, join as join6 } from "node:path";
5086
5364
  import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
5087
5365
  var defaultFileIO = {
5088
- read: (path6) => readFile5(path6, "utf8"),
5366
+ read: (path6) => readFile6(path6, "utf8"),
5089
5367
  write: async (path6, content) => {
5090
5368
  await writeFile(path6, content, "utf8");
5091
5369
  }
@@ -5203,7 +5481,7 @@ function makeLlmConflictResolver(opts) {
5203
5481
  export {
5204
5482
  AISpecBuilder,
5205
5483
  AutoExecutor,
5206
- DefaultTaskStore3 as DefaultTaskStore,
5484
+ DefaultTaskStore2 as DefaultTaskStore,
5207
5485
  SPEC_TEMPLATES,
5208
5486
  SddBoardProjector,
5209
5487
  SddBoardStore,
@@ -5230,12 +5508,14 @@ export {
5230
5508
  cleanupStaleSddWorktrees,
5231
5509
  cleanupStaleWorktrees,
5232
5510
  createAutoExecutor,
5511
+ createKanbanSddSessionPersistence,
5233
5512
  decomposeNonAtomicTasks,
5234
5513
  destroySddProject,
5235
5514
  extractVerificationCommand,
5236
5515
  gatherProjectContext,
5237
5516
  getTemplate,
5238
5517
  hasConflictMarkers,
5518
+ isAISpecSession,
5239
5519
  isExplanatoryText,
5240
5520
  listTemplates,
5241
5521
  makeAcceptanceCriteriaVerifier,