@wrongstack/sdd 0.296.3 → 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.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +331 -51
- package/dist/index.js.map +4 -4
- package/dist/kanban-sdd-session.d.ts +7 -0
- package/dist/kanban-sdd-session.d.ts.map +1 -0
- package/dist/sdd-board-projector.d.ts +7 -3
- package/dist/sdd-board-projector.d.ts.map +1 -1
- package/dist/sdd-board-store.d.ts +7 -6
- package/dist/sdd-board-store.d.ts.map +1 -1
- package/dist/sdd-interview-driver.d.ts +6 -4
- package/dist/sdd-interview-driver.d.ts.map +1 -1
- package/dist/sdd-lifecycle.d.ts +11 -1
- package/dist/sdd-lifecycle.d.ts.map +1 -1
- package/dist/sdd-session-types.d.ts +34 -0
- package/dist/sdd-session-types.d.ts.map +1 -0
- package/dist/spec-builder.d.ts +9 -33
- package/dist/spec-builder.d.ts.map +1 -1
- package/dist/start-sdd-run.d.ts +6 -2
- package/dist/start-sdd-run.d.ts.map +1 -1
- package/dist/task-graph-store.d.ts +11 -1
- package/dist/task-graph-store.d.ts.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1843
|
-
await
|
|
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.
|
|
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
|
|
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
|
|
1853
|
-
const raw = await
|
|
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
|
|
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
|
|
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
|
|
1868
|
-
await
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
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 {
|
|
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
|
-
|
|
3774
|
-
|
|
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
|
|
3799
|
-
const runId = opts.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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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,
|