@wrongstack/cli 0.5.3 → 0.5.5
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 +395 -111
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as path20 from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import { createRequire } from 'module';
|
|
3
|
+
import { join } from 'path';
|
|
5
4
|
import * as fs3 from 'fs/promises';
|
|
5
|
+
import { readdir, readFile } from 'fs/promises';
|
|
6
|
+
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
7
|
+
import { createRequire } from 'module';
|
|
6
8
|
import * as os6 from 'os';
|
|
7
9
|
import os6__default from 'os';
|
|
8
10
|
import * as crypto from 'crypto';
|
|
@@ -61,28 +63,37 @@ __export(sdd_exports, {
|
|
|
61
63
|
trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
|
|
62
64
|
trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
|
|
63
65
|
});
|
|
66
|
+
function getSessionState(ctx) {
|
|
67
|
+
if (!ctx) {
|
|
68
|
+
return sddState;
|
|
69
|
+
}
|
|
70
|
+
let state = ctx.meta[SDD_META_KEY];
|
|
71
|
+
if (!state) {
|
|
72
|
+
state = new SDDState();
|
|
73
|
+
ctx.meta[SDD_META_KEY] = state;
|
|
74
|
+
}
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
64
77
|
function getActiveSDDContext() {
|
|
65
|
-
|
|
66
|
-
const session = activeBuilder.getSession();
|
|
67
|
-
if (session.phase === "done") return null;
|
|
68
|
-
return activeBuilder.getAIPrompt();
|
|
78
|
+
return sddState.getContext();
|
|
69
79
|
}
|
|
70
80
|
function getActiveSDDPhase() {
|
|
71
|
-
|
|
72
|
-
return activeBuilder.getPhase();
|
|
81
|
+
return sddState.getPhase();
|
|
73
82
|
}
|
|
74
83
|
async function trySaveSpecFromAIOutput(aiOutput) {
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
const builder = sddState.getBuilder();
|
|
85
|
+
if (!builder) return false;
|
|
86
|
+
const spec = builder.tryParseSpecFromOutput(aiOutput);
|
|
77
87
|
if (!spec) return false;
|
|
78
|
-
|
|
88
|
+
builder.setSpec(spec);
|
|
79
89
|
return true;
|
|
80
90
|
}
|
|
81
91
|
async function trySaveTasksFromAIOutput(aiOutput) {
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
const builder = sddState.getBuilder();
|
|
93
|
+
if (!builder) return false;
|
|
94
|
+
const session = builder.getSession();
|
|
84
95
|
if (!session.spec) return false;
|
|
85
|
-
const json =
|
|
96
|
+
const json = builder.extractJSONArray(aiOutput);
|
|
86
97
|
if (!json) return false;
|
|
87
98
|
let tasks;
|
|
88
99
|
try {
|
|
@@ -113,15 +124,16 @@ async function trySaveTasksFromAIOutput(aiOutput) {
|
|
|
113
124
|
tags
|
|
114
125
|
});
|
|
115
126
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
sddState.setTaskStore(store);
|
|
128
|
+
sddState.setTaskTracker(tracker);
|
|
129
|
+
sddState.setTaskGraphId(graph.id);
|
|
130
|
+
builder.setTaskGraphId(graph.id);
|
|
120
131
|
return true;
|
|
121
132
|
}
|
|
122
133
|
function getTaskProgress() {
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
const tracker = sddState.getTaskTracker();
|
|
135
|
+
if (!tracker) return null;
|
|
136
|
+
const progress = tracker.getProgress();
|
|
125
137
|
return {
|
|
126
138
|
total: progress.total,
|
|
127
139
|
completed: progress.completed,
|
|
@@ -130,8 +142,9 @@ function getTaskProgress() {
|
|
|
130
142
|
};
|
|
131
143
|
}
|
|
132
144
|
function getTaskListText() {
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
const tracker = sddState.getTaskTracker();
|
|
146
|
+
if (!tracker) return null;
|
|
147
|
+
const nodes = tracker.getAllNodes();
|
|
135
148
|
if (nodes.length === 0) return null;
|
|
136
149
|
const lines = nodes.map((n, i) => {
|
|
137
150
|
const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : "\u23F3";
|
|
@@ -140,18 +153,20 @@ function getTaskListText() {
|
|
|
140
153
|
return lines.join("\n");
|
|
141
154
|
}
|
|
142
155
|
function markTaskCompleted(taskTitle) {
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
const tracker = sddState.getTaskTracker();
|
|
157
|
+
if (!tracker) return false;
|
|
158
|
+
const nodes = tracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
145
159
|
const match = nodes.find(
|
|
146
160
|
(n) => n.title.toLowerCase().includes(taskTitle.toLowerCase()) || taskTitle.toLowerCase().includes(n.title.toLowerCase())
|
|
147
161
|
);
|
|
148
162
|
if (!match) return false;
|
|
149
|
-
|
|
163
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
150
164
|
return true;
|
|
151
165
|
}
|
|
152
166
|
function autoDetectTaskCompletion(aiOutput) {
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
const tracker = sddState.getTaskTracker();
|
|
168
|
+
if (!tracker) return 0;
|
|
169
|
+
const pending = tracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
155
170
|
if (pending.length === 0) return 0;
|
|
156
171
|
let completed = 0;
|
|
157
172
|
const lines = aiOutput.split("\n");
|
|
@@ -164,7 +179,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
164
179
|
if (!Number.isNaN(num) && num >= 1 && num <= pending.length) {
|
|
165
180
|
const node = pending[num - 1];
|
|
166
181
|
if (node && node.status !== "completed") {
|
|
167
|
-
|
|
182
|
+
tracker.updateNodeStatus(node.id, "completed");
|
|
168
183
|
completed++;
|
|
169
184
|
}
|
|
170
185
|
} else {
|
|
@@ -172,7 +187,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
172
187
|
(n) => n.title.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(n.title.toLowerCase())
|
|
173
188
|
);
|
|
174
189
|
if (match && match.status !== "completed") {
|
|
175
|
-
|
|
190
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
176
191
|
completed++;
|
|
177
192
|
}
|
|
178
193
|
}
|
|
@@ -185,7 +200,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
185
200
|
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
186
201
|
);
|
|
187
202
|
if (match && match.status !== "completed") {
|
|
188
|
-
|
|
203
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
189
204
|
completed++;
|
|
190
205
|
}
|
|
191
206
|
continue;
|
|
@@ -196,7 +211,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
196
211
|
if (num >= 1 && num <= pending.length) {
|
|
197
212
|
const node = pending[num - 1];
|
|
198
213
|
if (node && node.status !== "completed") {
|
|
199
|
-
|
|
214
|
+
tracker.updateNodeStatus(node.id, "completed");
|
|
200
215
|
completed++;
|
|
201
216
|
}
|
|
202
217
|
}
|
|
@@ -209,7 +224,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
209
224
|
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
210
225
|
);
|
|
211
226
|
if (match && match.status !== "completed") {
|
|
212
|
-
|
|
227
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
213
228
|
completed++;
|
|
214
229
|
}
|
|
215
230
|
}
|
|
@@ -217,27 +232,29 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
217
232
|
return completed;
|
|
218
233
|
}
|
|
219
234
|
function trySaveImplementationPlan(aiOutput) {
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
const builder = sddState.getBuilder();
|
|
236
|
+
if (!builder) return false;
|
|
237
|
+
const session = builder.getSession();
|
|
222
238
|
if (session.phase !== "implementation") return false;
|
|
223
239
|
const jsonMatch = aiOutput.match(/```json\s*\[/);
|
|
224
240
|
if (jsonMatch?.index && jsonMatch.index > 0) {
|
|
225
241
|
const plan = aiOutput.substring(0, jsonMatch.index).trim();
|
|
226
242
|
if (plan.length > 50) {
|
|
227
|
-
|
|
243
|
+
builder.setImplementation(plan);
|
|
228
244
|
return true;
|
|
229
245
|
}
|
|
230
246
|
}
|
|
231
247
|
if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
|
|
232
|
-
|
|
248
|
+
builder.setImplementation(aiOutput.trim());
|
|
233
249
|
return true;
|
|
234
250
|
}
|
|
235
251
|
return false;
|
|
236
252
|
}
|
|
237
253
|
function getActiveBuilder() {
|
|
238
|
-
return
|
|
254
|
+
return sddState.getBuilder();
|
|
239
255
|
}
|
|
240
256
|
function buildSddCommand(opts) {
|
|
257
|
+
getSessionState(opts.context);
|
|
241
258
|
return {
|
|
242
259
|
name: "sdd",
|
|
243
260
|
description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
|
|
@@ -260,11 +277,10 @@ function buildSddCommand(opts) {
|
|
|
260
277
|
case "create": {
|
|
261
278
|
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
262
279
|
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
263
|
-
if (!
|
|
280
|
+
if (!sddState.getBuilder() && !forceFlag) {
|
|
264
281
|
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
265
282
|
try {
|
|
266
|
-
|
|
267
|
-
await fsp.access(sessionPath);
|
|
283
|
+
await fs3.access(sessionPath);
|
|
268
284
|
const projectContext2 = await gatherProjectContext(projectRoot);
|
|
269
285
|
const tempBuilder = new AISpecBuilder({
|
|
270
286
|
store: specStore,
|
|
@@ -290,19 +306,18 @@ function buildSddCommand(opts) {
|
|
|
290
306
|
} catch {
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
|
-
|
|
294
|
-
activeTaskTracker = null;
|
|
295
|
-
activeTaskGraphId = null;
|
|
309
|
+
sddState.clearTaskState();
|
|
296
310
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
297
|
-
|
|
311
|
+
sddState.setBuilder(new AISpecBuilder({
|
|
298
312
|
store: specStore,
|
|
299
313
|
projectContext,
|
|
300
314
|
minQuestions: 2,
|
|
301
315
|
maxQuestions: 10,
|
|
302
316
|
sessionPath: path20.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
}));
|
|
318
|
+
const builder = sddState.getBuilder();
|
|
319
|
+
builder.startSession(title);
|
|
320
|
+
const aiPrompt = builder.getAIPrompt();
|
|
306
321
|
return {
|
|
307
322
|
message: [
|
|
308
323
|
`\u2554\u2550\u2550\u2550 SDD: AI Spec Builder \u2550\u2550\u2550\u2557`,
|
|
@@ -326,14 +341,15 @@ Start the specification interview for "${title}". Ask your first contextual ques
|
|
|
326
341
|
case "approve":
|
|
327
342
|
case "ok":
|
|
328
343
|
case "confirm": {
|
|
329
|
-
|
|
344
|
+
const builder = sddState.getBuilder();
|
|
345
|
+
if (!builder) {
|
|
330
346
|
return {
|
|
331
347
|
message: "No active SDD session. Use /sdd new to start one."
|
|
332
348
|
};
|
|
333
349
|
}
|
|
334
|
-
const phase =
|
|
350
|
+
const phase = builder.getSession().phase;
|
|
335
351
|
if (phase === "questioning") {
|
|
336
|
-
const sddCtx =
|
|
352
|
+
const sddCtx = builder.getAIPrompt();
|
|
337
353
|
return {
|
|
338
354
|
message: "No spec generated yet. Generating now...",
|
|
339
355
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -345,14 +361,14 @@ Generate the complete specification now based on the conversation so far.`
|
|
|
345
361
|
};
|
|
346
362
|
}
|
|
347
363
|
if (phase === "spec_review") {
|
|
348
|
-
const spec =
|
|
364
|
+
const spec = builder.getSession().spec;
|
|
349
365
|
if (!spec) {
|
|
350
366
|
return { message: "No spec to approve." };
|
|
351
367
|
}
|
|
352
|
-
await
|
|
368
|
+
await builder.saveSpec();
|
|
353
369
|
versioning.recordVersion(spec, "Initial spec approved");
|
|
354
|
-
|
|
355
|
-
const implPrompt =
|
|
370
|
+
builder.approve();
|
|
371
|
+
const implPrompt = builder.getAIPrompt();
|
|
356
372
|
return {
|
|
357
373
|
message: [
|
|
358
374
|
`\u2705 Spec "${spec.title}" approved and saved!`,
|
|
@@ -370,8 +386,8 @@ Generate the implementation plan and tasks for the approved spec.`
|
|
|
370
386
|
};
|
|
371
387
|
}
|
|
372
388
|
if (phase === "task_review") {
|
|
373
|
-
|
|
374
|
-
const execPrompt =
|
|
389
|
+
builder.approve();
|
|
390
|
+
const execPrompt = builder.getAIPrompt();
|
|
375
391
|
return {
|
|
376
392
|
message: "\u2705 Tasks approved! The AI will now execute them one by one.",
|
|
377
393
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -389,18 +405,19 @@ Start executing the tasks one by one.`
|
|
|
389
405
|
// ── Task Execution ─────────────────────────────────────────────────
|
|
390
406
|
case "execute":
|
|
391
407
|
case "run": {
|
|
392
|
-
|
|
408
|
+
const runBuilder = sddState.getBuilder();
|
|
409
|
+
if (!runBuilder) {
|
|
393
410
|
return {
|
|
394
411
|
message: "No active SDD session. Use /sdd new to start one."
|
|
395
412
|
};
|
|
396
413
|
}
|
|
397
|
-
const session =
|
|
414
|
+
const session = runBuilder.getSession();
|
|
398
415
|
if (session.phase !== "executing" && session.phase !== "task_review") {
|
|
399
416
|
return {
|
|
400
417
|
message: `Cannot execute in phase "${session.phase}". Use /sdd approve first.`
|
|
401
418
|
};
|
|
402
419
|
}
|
|
403
|
-
const execPrompt =
|
|
420
|
+
const execPrompt = runBuilder.getAIPrompt();
|
|
404
421
|
return {
|
|
405
422
|
message: "\u26A1 Starting task execution. The AI will execute tasks one by one.",
|
|
406
423
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -413,34 +430,36 @@ Start executing the tasks one by one.`
|
|
|
413
430
|
}
|
|
414
431
|
case "plan":
|
|
415
432
|
case "impl": {
|
|
416
|
-
|
|
433
|
+
const planBuilder = sddState.getBuilder();
|
|
434
|
+
if (!planBuilder) {
|
|
417
435
|
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
418
436
|
}
|
|
419
|
-
const
|
|
420
|
-
if (!
|
|
437
|
+
const planSession = planBuilder.getSession();
|
|
438
|
+
if (!planSession.implementation) {
|
|
421
439
|
return {
|
|
422
|
-
message:
|
|
440
|
+
message: planSession.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
|
|
423
441
|
};
|
|
424
442
|
}
|
|
425
443
|
return {
|
|
426
444
|
message: [
|
|
427
445
|
"\u2550\u2550\u2550 Implementation Plan \u2550\u2550\u2550",
|
|
428
446
|
"",
|
|
429
|
-
|
|
447
|
+
planSession.implementation
|
|
430
448
|
].join("\n")
|
|
431
449
|
};
|
|
432
450
|
}
|
|
433
451
|
case "spec": {
|
|
434
|
-
|
|
452
|
+
const specBuilder = sddState.getBuilder();
|
|
453
|
+
if (!specBuilder) {
|
|
435
454
|
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
436
455
|
}
|
|
437
|
-
const
|
|
438
|
-
if (!
|
|
456
|
+
const specSession = specBuilder.getSession();
|
|
457
|
+
if (!specSession.spec) {
|
|
439
458
|
return {
|
|
440
|
-
message:
|
|
459
|
+
message: specSession.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
|
|
441
460
|
};
|
|
442
461
|
}
|
|
443
|
-
const spec =
|
|
462
|
+
const spec = specSession.spec;
|
|
444
463
|
const lines = [
|
|
445
464
|
`\u2550\u2550\u2550 Current Spec \u2550\u2550\u2550`,
|
|
446
465
|
"",
|
|
@@ -462,14 +481,15 @@ Start executing the tasks one by one.`
|
|
|
462
481
|
}
|
|
463
482
|
case "tasks":
|
|
464
483
|
case "task": {
|
|
465
|
-
|
|
484
|
+
const taskTracker = sddState.getTaskTracker();
|
|
485
|
+
if (!taskTracker) {
|
|
466
486
|
return { message: "No tasks generated yet. Use /sdd new to start." };
|
|
467
487
|
}
|
|
468
|
-
const nodes =
|
|
488
|
+
const nodes = taskTracker.getAllNodes();
|
|
469
489
|
if (nodes.length === 0) {
|
|
470
490
|
return { message: "No tasks in the current graph." };
|
|
471
491
|
}
|
|
472
|
-
const progress =
|
|
492
|
+
const progress = taskTracker.getProgress();
|
|
473
493
|
const lines = [
|
|
474
494
|
`\u2550\u2550\u2550 Task List (${progress.completed}/${progress.total} done) \u2550\u2550\u2550`,
|
|
475
495
|
""
|
|
@@ -486,19 +506,20 @@ Start executing the tasks one by one.`
|
|
|
486
506
|
}
|
|
487
507
|
case "done":
|
|
488
508
|
case "complete": {
|
|
489
|
-
|
|
509
|
+
const doneTracker = sddState.getTaskTracker();
|
|
510
|
+
if (!doneTracker) {
|
|
490
511
|
return { message: "No tasks to complete." };
|
|
491
512
|
}
|
|
492
513
|
if (!restJoined) {
|
|
493
514
|
return { message: "Usage: /sdd done <task title or number>" };
|
|
494
515
|
}
|
|
495
|
-
const nodes =
|
|
516
|
+
const nodes = doneTracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
496
517
|
const num = Number(restJoined);
|
|
497
518
|
let matched = false;
|
|
498
519
|
if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
|
|
499
520
|
const node = nodes[num - 1];
|
|
500
521
|
if (node) {
|
|
501
|
-
|
|
522
|
+
doneTracker.updateNodeStatus(node.id, "completed");
|
|
502
523
|
matched = true;
|
|
503
524
|
}
|
|
504
525
|
}
|
|
@@ -507,24 +528,25 @@ Start executing the tasks one by one.`
|
|
|
507
528
|
(n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
|
|
508
529
|
);
|
|
509
530
|
if (match) {
|
|
510
|
-
|
|
531
|
+
doneTracker.updateNodeStatus(match.id, "completed");
|
|
511
532
|
matched = true;
|
|
512
533
|
}
|
|
513
534
|
}
|
|
514
535
|
if (!matched) {
|
|
515
536
|
return { message: `No pending task matching "${restJoined}".` };
|
|
516
537
|
}
|
|
517
|
-
const remaining =
|
|
538
|
+
const remaining = doneTracker.getProgress();
|
|
518
539
|
return {
|
|
519
540
|
message: `\u2705 Task completed! ${remaining.completed}/${remaining.total} done (${remaining.percentComplete}%)`
|
|
520
541
|
};
|
|
521
542
|
}
|
|
522
543
|
// ── Session Management ─────────────────────────────────────────────
|
|
523
544
|
case "status": {
|
|
524
|
-
|
|
545
|
+
const statusBuilder = sddState.getBuilder();
|
|
546
|
+
if (!statusBuilder) {
|
|
525
547
|
return { message: "No active SDD session." };
|
|
526
548
|
}
|
|
527
|
-
const session =
|
|
549
|
+
const session = statusBuilder.getSession();
|
|
528
550
|
const phaseEmoji = {
|
|
529
551
|
questioning: "\u2753",
|
|
530
552
|
spec_review: "\u{1F4CB}",
|
|
@@ -562,18 +584,16 @@ Start executing the tasks one by one.`
|
|
|
562
584
|
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
563
585
|
let deletedFromDisk = false;
|
|
564
586
|
try {
|
|
565
|
-
|
|
566
|
-
await fsp.unlink(sessionPath);
|
|
587
|
+
await fs3.unlink(sessionPath);
|
|
567
588
|
deletedFromDisk = true;
|
|
568
589
|
} catch {
|
|
569
590
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
activeTaskGraphId = null;
|
|
591
|
+
const cancelBuilder = sddState.getBuilder();
|
|
592
|
+
if (cancelBuilder) {
|
|
593
|
+
const title = cancelBuilder.getSession().title;
|
|
594
|
+
await cancelBuilder.deleteSession();
|
|
595
|
+
sddState.setBuilder(null);
|
|
596
|
+
sddState.clearTaskState();
|
|
577
597
|
return { message: `SDD session for "${title}" cancelled.` };
|
|
578
598
|
}
|
|
579
599
|
if (deletedFromDisk) {
|
|
@@ -582,36 +602,37 @@ Start executing the tasks one by one.`
|
|
|
582
602
|
return { message: "No active SDD session." };
|
|
583
603
|
}
|
|
584
604
|
case "resume": {
|
|
585
|
-
if (
|
|
605
|
+
if (sddState.getBuilder()) {
|
|
586
606
|
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
587
607
|
}
|
|
588
608
|
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
589
609
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
590
|
-
|
|
610
|
+
sddState.setBuilder(new AISpecBuilder({
|
|
591
611
|
store: specStore,
|
|
592
612
|
projectContext,
|
|
593
613
|
minQuestions: 2,
|
|
594
614
|
maxQuestions: 10,
|
|
595
615
|
sessionPath
|
|
596
|
-
});
|
|
597
|
-
const
|
|
616
|
+
}));
|
|
617
|
+
const resumeBuilder = sddState.getBuilder();
|
|
618
|
+
const loaded = await resumeBuilder.loadSession();
|
|
598
619
|
if (!loaded) {
|
|
599
|
-
|
|
620
|
+
sddState.setBuilder(null);
|
|
600
621
|
return { message: "No saved SDD session found. Use /sdd new to start one." };
|
|
601
622
|
}
|
|
602
|
-
const session =
|
|
623
|
+
const session = resumeBuilder.getSession();
|
|
603
624
|
let taskCount = 0;
|
|
604
625
|
let completedCount = 0;
|
|
605
|
-
const taskGraphId =
|
|
626
|
+
const taskGraphId = resumeBuilder.getTaskGraphId();
|
|
606
627
|
if (taskGraphId) {
|
|
607
628
|
try {
|
|
608
629
|
const store = new DefaultTaskStore();
|
|
609
630
|
const tracker = new TaskTracker({ store });
|
|
610
631
|
const graph = await tracker.loadGraph(taskGraphId);
|
|
611
632
|
if (graph) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
633
|
+
sddState.setTaskStore(store);
|
|
634
|
+
sddState.setTaskTracker(tracker);
|
|
635
|
+
sddState.setTaskGraphId(taskGraphId);
|
|
615
636
|
const progress = tracker.getProgress();
|
|
616
637
|
taskCount = progress.total;
|
|
617
638
|
completedCount = progress.completed;
|
|
@@ -619,7 +640,7 @@ Start executing the tasks one by one.`
|
|
|
619
640
|
} catch {
|
|
620
641
|
}
|
|
621
642
|
}
|
|
622
|
-
const resumePrompt =
|
|
643
|
+
const resumePrompt = resumeBuilder.getAIPrompt();
|
|
623
644
|
return {
|
|
624
645
|
message: [
|
|
625
646
|
`\u2554\u2550\u2550\u2550 SDD Session Resumed \u2550\u2550\u2550\u2557`,
|
|
@@ -810,9 +831,8 @@ function sddHelp() {
|
|
|
810
831
|
async function gatherProjectContext(projectRoot) {
|
|
811
832
|
const parts = [];
|
|
812
833
|
try {
|
|
813
|
-
const fsp = await import('fs/promises');
|
|
814
834
|
const pkgPath = path20.join(projectRoot, "package.json");
|
|
815
|
-
const pkgRaw = await
|
|
835
|
+
const pkgRaw = await fs3.readFile(pkgPath, "utf8");
|
|
816
836
|
const pkg = JSON.parse(pkgRaw);
|
|
817
837
|
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
818
838
|
parts.push(`Description: ${String(pkg.description ?? "none")}`);
|
|
@@ -827,16 +847,14 @@ async function gatherProjectContext(projectRoot) {
|
|
|
827
847
|
} catch {
|
|
828
848
|
}
|
|
829
849
|
try {
|
|
830
|
-
const fsp = await import('fs/promises');
|
|
831
850
|
const tsconfigPath = path20.join(projectRoot, "tsconfig.json");
|
|
832
|
-
await
|
|
851
|
+
await fs3.access(tsconfigPath);
|
|
833
852
|
parts.push("Language: TypeScript");
|
|
834
853
|
} catch {
|
|
835
854
|
}
|
|
836
855
|
try {
|
|
837
|
-
const fsp = await import('fs/promises');
|
|
838
856
|
const srcDir = path20.join(projectRoot, "src");
|
|
839
|
-
const entries = await
|
|
857
|
+
const entries = await fs3.readdir(srcDir, { withFileTypes: true });
|
|
840
858
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
841
859
|
if (dirs.length > 0) {
|
|
842
860
|
parts.push(`Source structure: src/${dirs.join(", src/")}`);
|
|
@@ -856,13 +874,55 @@ async function findSpec(store, idOrTitle) {
|
|
|
856
874
|
if (match) return store.load(match.id);
|
|
857
875
|
return null;
|
|
858
876
|
}
|
|
859
|
-
var
|
|
877
|
+
var SDD_META_KEY, SDDState, sddState;
|
|
860
878
|
var init_sdd = __esm({
|
|
861
879
|
"src/slash-commands/sdd.ts"() {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
880
|
+
SDD_META_KEY = "sdd.state";
|
|
881
|
+
SDDState = class {
|
|
882
|
+
builder = null;
|
|
883
|
+
taskStore = null;
|
|
884
|
+
taskTracker = null;
|
|
885
|
+
taskGraphId = null;
|
|
886
|
+
getBuilder() {
|
|
887
|
+
return this.builder;
|
|
888
|
+
}
|
|
889
|
+
setBuilder(b) {
|
|
890
|
+
this.builder = b;
|
|
891
|
+
}
|
|
892
|
+
getTaskStore() {
|
|
893
|
+
return this.taskStore;
|
|
894
|
+
}
|
|
895
|
+
setTaskStore(s) {
|
|
896
|
+
this.taskStore = s;
|
|
897
|
+
}
|
|
898
|
+
getTaskTracker() {
|
|
899
|
+
return this.taskTracker;
|
|
900
|
+
}
|
|
901
|
+
setTaskTracker(t) {
|
|
902
|
+
this.taskTracker = t;
|
|
903
|
+
}
|
|
904
|
+
getTaskGraphId() {
|
|
905
|
+
return this.taskGraphId;
|
|
906
|
+
}
|
|
907
|
+
setTaskGraphId(id) {
|
|
908
|
+
this.taskGraphId = id;
|
|
909
|
+
}
|
|
910
|
+
clearTaskState() {
|
|
911
|
+
this.taskStore = null;
|
|
912
|
+
this.taskTracker = null;
|
|
913
|
+
this.taskGraphId = null;
|
|
914
|
+
}
|
|
915
|
+
getContext() {
|
|
916
|
+
if (!this.builder) return null;
|
|
917
|
+
const session = this.builder.getSession();
|
|
918
|
+
if (session.phase === "done") return null;
|
|
919
|
+
return this.builder.getAIPrompt();
|
|
920
|
+
}
|
|
921
|
+
getPhase() {
|
|
922
|
+
return this.builder?.getPhase() ?? null;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
sddState = new SDDState();
|
|
866
926
|
}
|
|
867
927
|
});
|
|
868
928
|
function normalizeKeys(cfg) {
|
|
@@ -3827,6 +3887,225 @@ ${lines.join("\n\n")}
|
|
|
3827
3887
|
}
|
|
3828
3888
|
};
|
|
3829
3889
|
}
|
|
3890
|
+
function buildSecurityCommand(opts) {
|
|
3891
|
+
return {
|
|
3892
|
+
name: "security",
|
|
3893
|
+
description: "Security scanning: scan, audit, report",
|
|
3894
|
+
argsHint: "[scan|audit|report] [options]",
|
|
3895
|
+
help: `
|
|
3896
|
+
# /security \u2014 Security Scanner
|
|
3897
|
+
|
|
3898
|
+
Automated security scanning with tech stack detection.
|
|
3899
|
+
|
|
3900
|
+
## Commands
|
|
3901
|
+
|
|
3902
|
+
### /security scan [options]
|
|
3903
|
+
Run a full security scan on the current project.
|
|
3904
|
+
Options:
|
|
3905
|
+
--depth quick|standard|deep Scan depth (default: standard)
|
|
3906
|
+
--format markdown|json|html Report format (default: markdown)
|
|
3907
|
+
|
|
3908
|
+
### /security audit
|
|
3909
|
+
Run dependency audit + security scan.
|
|
3910
|
+
|
|
3911
|
+
### /security report [id]
|
|
3912
|
+
List or view security reports.
|
|
3913
|
+
|
|
3914
|
+
## Examples
|
|
3915
|
+
|
|
3916
|
+
/security scan
|
|
3917
|
+
/security scan --depth deep --format html
|
|
3918
|
+
/security audit
|
|
3919
|
+
/security report
|
|
3920
|
+
`,
|
|
3921
|
+
async run(args, ctx) {
|
|
3922
|
+
const parts = args.trim().split(/\s+/);
|
|
3923
|
+
const subcommand = parts[0] || "";
|
|
3924
|
+
switch (subcommand) {
|
|
3925
|
+
case "scan":
|
|
3926
|
+
return handleScan(parts.slice(1).join(" "), ctx, opts);
|
|
3927
|
+
case "audit":
|
|
3928
|
+
return handleAudit(ctx, opts);
|
|
3929
|
+
case "report":
|
|
3930
|
+
return handleReport(parts[1] || "");
|
|
3931
|
+
default:
|
|
3932
|
+
return { message: getHelpMessage() };
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
async function handleScan(args, ctx, opts) {
|
|
3938
|
+
const options = parseArgs2(args);
|
|
3939
|
+
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
3940
|
+
try {
|
|
3941
|
+
const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
|
|
3942
|
+
if (!orchestratorContext.provider) {
|
|
3943
|
+
return { message: "\u274C Security scan requires an active LLM provider. No provider configured." };
|
|
3944
|
+
}
|
|
3945
|
+
const result = await defaultOrchestrator.run(orchestratorContext, {
|
|
3946
|
+
projectRoot,
|
|
3947
|
+
scanOptions: {
|
|
3948
|
+
depth: options.depth || "standard",
|
|
3949
|
+
includeSecrets: true,
|
|
3950
|
+
includeInjection: true,
|
|
3951
|
+
includeConfig: true
|
|
3952
|
+
},
|
|
3953
|
+
reportOptions: {
|
|
3954
|
+
format: options.format || "markdown"
|
|
3955
|
+
}
|
|
3956
|
+
});
|
|
3957
|
+
const summary = result.scanResult.summary;
|
|
3958
|
+
const status = summary.total === 0 ? "\u2705 No issues found" : `\u26A0\uFE0F Found ${summary.total} issues`;
|
|
3959
|
+
const reportContent = result.synthesizedReport || `# Security Scan Complete
|
|
3960
|
+
|
|
3961
|
+
**Project:** ${projectRoot}
|
|
3962
|
+
**Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
|
|
3963
|
+
**Scanned Files:** ${result.scanResult.scannedFiles}
|
|
3964
|
+
**Duration:** ${result.scanResult.scanDurationMs}ms
|
|
3965
|
+
|
|
3966
|
+
## Summary
|
|
3967
|
+
|
|
3968
|
+
| Severity | Count |
|
|
3969
|
+
|----------|-------|
|
|
3970
|
+
| \u{1F534} Critical | ${summary.critical} |
|
|
3971
|
+
| \u{1F7E0} High | ${summary.high} |
|
|
3972
|
+
| \u{1F7E1} Medium | ${summary.medium} |
|
|
3973
|
+
| \u{1F7E2} Low | ${summary.low} |
|
|
3974
|
+
|
|
3975
|
+
**Status:** ${status}
|
|
3976
|
+
|
|
3977
|
+
**Report:** ${result.reportPath}
|
|
3978
|
+
`;
|
|
3979
|
+
return {
|
|
3980
|
+
message: reportContent,
|
|
3981
|
+
metadata: {
|
|
3982
|
+
scanResult: result.scanResult,
|
|
3983
|
+
reportPath: result.reportPath,
|
|
3984
|
+
techStack: result.detectionResult.detectedStacks[0]
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
return { message: `\u274C Scan failed: ${error}` };
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
async function handleAudit(ctx, opts) {
|
|
3992
|
+
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
3993
|
+
try {
|
|
3994
|
+
const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
|
|
3995
|
+
if (!orchestratorContext.provider) {
|
|
3996
|
+
return { message: "\u274C Security audit requires an active LLM provider. No provider configured." };
|
|
3997
|
+
}
|
|
3998
|
+
const result = await defaultOrchestrator.run(orchestratorContext, {
|
|
3999
|
+
projectRoot,
|
|
4000
|
+
reportOptions: { format: "markdown" }
|
|
4001
|
+
});
|
|
4002
|
+
const summary = result.scanResult.summary;
|
|
4003
|
+
const depIssues = summary.critical + summary.high;
|
|
4004
|
+
const reportContent = result.synthesizedReport || `# Security Audit Complete
|
|
4005
|
+
|
|
4006
|
+
**Project:** ${projectRoot}
|
|
4007
|
+
**Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
|
|
4008
|
+
|
|
4009
|
+
## Dependency Health
|
|
4010
|
+
|
|
4011
|
+
| Status | Count |
|
|
4012
|
+
|--------|-------|
|
|
4013
|
+
| Critical Issues | ${summary.critical} |
|
|
4014
|
+
| High Priority | ${summary.high} |
|
|
4015
|
+
| Medium Priority | ${summary.medium} |
|
|
4016
|
+
| Low Priority | ${summary.low} |
|
|
4017
|
+
|
|
4018
|
+
${depIssues === 0 ? "\u2705 No known vulnerabilities detected" : `\u26A0\uFE0F ${depIssues} vulnerabilities need attention`}
|
|
4019
|
+
|
|
4020
|
+
**Full Report:** ${result.reportPath}
|
|
4021
|
+
`;
|
|
4022
|
+
return {
|
|
4023
|
+
message: reportContent,
|
|
4024
|
+
metadata: {
|
|
4025
|
+
scanResult: result.scanResult,
|
|
4026
|
+
reportPath: result.reportPath
|
|
4027
|
+
}
|
|
4028
|
+
};
|
|
4029
|
+
} catch (error) {
|
|
4030
|
+
return { message: `\u274C Audit failed: ${error}` };
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
async function handleReport(reportId) {
|
|
4034
|
+
const reportsDir = "security-reports";
|
|
4035
|
+
try {
|
|
4036
|
+
const files = await readdir(reportsDir);
|
|
4037
|
+
const reports = files.filter((f) => f.startsWith("security-report-") && (f.endsWith(".md") || f.endsWith(".json"))).sort().reverse();
|
|
4038
|
+
if (!reportId) {
|
|
4039
|
+
if (reports.length === 0) {
|
|
4040
|
+
return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
|
|
4041
|
+
}
|
|
4042
|
+
const list = reports.map((r, i) => {
|
|
4043
|
+
const date = r.replace("security-report-", "").replace(/\.(md|json)$/, "");
|
|
4044
|
+
return ` ${i + 1}. ${date}`;
|
|
4045
|
+
}).join("\n");
|
|
4046
|
+
return { message: `# Available Security Reports
|
|
4047
|
+
|
|
4048
|
+
${list}
|
|
4049
|
+
|
|
4050
|
+
Use \`/security report <number>\` to view a specific report.` };
|
|
4051
|
+
}
|
|
4052
|
+
const index = parseInt(reportId, 10) - 1;
|
|
4053
|
+
if (!isNaN(index) && reports[index]) {
|
|
4054
|
+
const content = await readFile(join(reportsDir, reports[index]), "utf-8");
|
|
4055
|
+
return { message: `# Security Report
|
|
4056
|
+
|
|
4057
|
+
${content}` };
|
|
4058
|
+
}
|
|
4059
|
+
const match = reports.find((r) => r.includes(reportId));
|
|
4060
|
+
if (match) {
|
|
4061
|
+
const content = await readFile(join(reportsDir, match), "utf-8");
|
|
4062
|
+
return { message: `# Security Report
|
|
4063
|
+
|
|
4064
|
+
${content}` };
|
|
4065
|
+
}
|
|
4066
|
+
return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
|
|
4067
|
+
} catch {
|
|
4068
|
+
return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function parseArgs2(args) {
|
|
4072
|
+
const result = {};
|
|
4073
|
+
const parts = args.split(/\s+/);
|
|
4074
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4075
|
+
const part = parts[i];
|
|
4076
|
+
if (!part || !part.startsWith("--")) continue;
|
|
4077
|
+
const key = part.slice(2);
|
|
4078
|
+
const next = parts[i + 1];
|
|
4079
|
+
if (next && !next.startsWith("--")) {
|
|
4080
|
+
result[key] = next;
|
|
4081
|
+
i++;
|
|
4082
|
+
} else {
|
|
4083
|
+
result[key] = "true";
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
return result;
|
|
4087
|
+
}
|
|
4088
|
+
function getHelpMessage() {
|
|
4089
|
+
return `# /security \u2014 Security Scanner
|
|
4090
|
+
|
|
4091
|
+
**Available Commands:**
|
|
4092
|
+
|
|
4093
|
+
1. **/security scan** \u2014 Run full security scan
|
|
4094
|
+
\`/security scan --depth deep --format html\`
|
|
4095
|
+
|
|
4096
|
+
2. **/security audit** \u2014 Run dependency audit + security scan
|
|
4097
|
+
|
|
4098
|
+
3. **/security report** \u2014 List available reports
|
|
4099
|
+
|
|
4100
|
+
**Features:**
|
|
4101
|
+
- Automatic tech stack detection
|
|
4102
|
+
- Dynamic security skill generation
|
|
4103
|
+
- Secrets, injection, and config vulnerability scanning
|
|
4104
|
+
- Markdown/JSON/HTML reports
|
|
4105
|
+
- .gitignore auto-update
|
|
4106
|
+
|
|
4107
|
+
Run \`/security scan\` to start.`;
|
|
4108
|
+
}
|
|
3830
4109
|
function makeInstaller(opts, projectRoot, global) {
|
|
3831
4110
|
const globalRoot = path20.join(os6.homedir(), ".wrongstack");
|
|
3832
4111
|
return new SkillInstaller({
|
|
@@ -4024,7 +4303,8 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
4024
4303
|
buildExitCommand(opts),
|
|
4025
4304
|
buildCommitCommand(),
|
|
4026
4305
|
buildGitcheckCommand(),
|
|
4027
|
-
buildPushCommand()
|
|
4306
|
+
buildPushCommand(),
|
|
4307
|
+
buildSecurityCommand(opts)
|
|
4028
4308
|
];
|
|
4029
4309
|
}
|
|
4030
4310
|
|
|
@@ -8364,11 +8644,15 @@ async function main(argv) {
|
|
|
8364
8644
|
renderer,
|
|
8365
8645
|
memoryStore,
|
|
8366
8646
|
context,
|
|
8647
|
+
cwd,
|
|
8648
|
+
projectRoot,
|
|
8367
8649
|
metricsSink,
|
|
8368
8650
|
healthRegistry,
|
|
8369
8651
|
planPath,
|
|
8370
8652
|
modeStore,
|
|
8371
8653
|
fleetStreamController,
|
|
8654
|
+
llmProvider: provider,
|
|
8655
|
+
llmModel: config.model,
|
|
8372
8656
|
onSpawn: async (description, spawnOpts) => {
|
|
8373
8657
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
8374
8658
|
const tags = [];
|