@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 CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import * as path20 from 'path';
3
- 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, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
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
- if (!activeBuilder) return null;
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
- if (!activeBuilder) return null;
72
- return activeBuilder.getPhase();
81
+ return sddState.getPhase();
73
82
  }
74
83
  async function trySaveSpecFromAIOutput(aiOutput) {
75
- if (!activeBuilder) return false;
76
- const spec = activeBuilder.tryParseSpecFromOutput(aiOutput);
84
+ const builder = sddState.getBuilder();
85
+ if (!builder) return false;
86
+ const spec = builder.tryParseSpecFromOutput(aiOutput);
77
87
  if (!spec) return false;
78
- activeBuilder.setSpec(spec);
88
+ builder.setSpec(spec);
79
89
  return true;
80
90
  }
81
91
  async function trySaveTasksFromAIOutput(aiOutput) {
82
- if (!activeBuilder) return false;
83
- const session = activeBuilder.getSession();
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 = activeBuilder.extractJSONArray(aiOutput);
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
- activeTaskStore = store;
117
- activeTaskTracker = tracker;
118
- activeTaskGraphId = graph.id;
119
- activeBuilder.setTaskGraphId(graph.id);
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
- if (!activeTaskTracker) return null;
124
- const progress = activeTaskTracker.getProgress();
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
- if (!activeTaskTracker) return null;
134
- const nodes = activeTaskTracker.getAllNodes();
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
- if (!activeTaskTracker) return false;
144
- const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
163
+ tracker.updateNodeStatus(match.id, "completed");
150
164
  return true;
151
165
  }
152
166
  function autoDetectTaskCompletion(aiOutput) {
153
- if (!activeTaskTracker) return 0;
154
- const pending = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- if (!activeBuilder) return false;
221
- const session = activeBuilder.getSession();
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
- activeBuilder.setImplementation(plan);
243
+ builder.setImplementation(plan);
228
244
  return true;
229
245
  }
230
246
  }
231
247
  if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
232
- activeBuilder.setImplementation(aiOutput.trim());
248
+ builder.setImplementation(aiOutput.trim());
233
249
  return true;
234
250
  }
235
251
  return false;
236
252
  }
237
253
  function getActiveBuilder() {
238
- return activeBuilder;
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 (!activeBuilder && !forceFlag) {
280
+ if (!sddState.getBuilder() && !forceFlag) {
264
281
  const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
265
282
  try {
266
- const fsp = await import('fs/promises');
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
- activeTaskStore = null;
294
- activeTaskTracker = null;
295
- activeTaskGraphId = null;
309
+ sddState.clearTaskState();
296
310
  const projectContext = await gatherProjectContext(projectRoot);
297
- activeBuilder = new AISpecBuilder({
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
- activeBuilder.startSession(title);
305
- const aiPrompt = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 = activeBuilder.getSession().phase;
350
+ const phase = builder.getSession().phase;
335
351
  if (phase === "questioning") {
336
- const sddCtx = activeBuilder.getAIPrompt();
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 = activeBuilder.getSession().spec;
364
+ const spec = builder.getSession().spec;
349
365
  if (!spec) {
350
366
  return { message: "No spec to approve." };
351
367
  }
352
- await activeBuilder.saveSpec();
368
+ await builder.saveSpec();
353
369
  versioning.recordVersion(spec, "Initial spec approved");
354
- activeBuilder.approve();
355
- const implPrompt = activeBuilder.getAIPrompt();
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
- activeBuilder.approve();
374
- const execPrompt = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 = activeBuilder.getSession();
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 = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 session = activeBuilder.getSession();
420
- if (!session.implementation) {
437
+ const planSession = planBuilder.getSession();
438
+ if (!planSession.implementation) {
421
439
  return {
422
- message: session.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
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
- session.implementation
447
+ planSession.implementation
430
448
  ].join("\n")
431
449
  };
432
450
  }
433
451
  case "spec": {
434
- if (!activeBuilder) {
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 session = activeBuilder.getSession();
438
- if (!session.spec) {
456
+ const specSession = specBuilder.getSession();
457
+ if (!specSession.spec) {
439
458
  return {
440
- message: session.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
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 = session.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
- if (!activeTaskTracker) {
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 = activeTaskTracker.getAllNodes();
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 = activeTaskTracker.getProgress();
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
- if (!activeTaskTracker) {
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 = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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 = activeTaskTracker.getProgress();
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
- if (!activeBuilder) {
545
+ const statusBuilder = sddState.getBuilder();
546
+ if (!statusBuilder) {
525
547
  return { message: "No active SDD session." };
526
548
  }
527
- const session = activeBuilder.getSession();
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
- const fsp = await import('fs/promises');
566
- await fsp.unlink(sessionPath);
587
+ await fs3.unlink(sessionPath);
567
588
  deletedFromDisk = true;
568
589
  } catch {
569
590
  }
570
- if (activeBuilder) {
571
- const title = activeBuilder.getSession().title;
572
- await activeBuilder.deleteSession();
573
- activeBuilder = null;
574
- activeTaskStore = null;
575
- activeTaskTracker = null;
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 (activeBuilder) {
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
- activeBuilder = new AISpecBuilder({
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 loaded = await activeBuilder.loadSession();
616
+ }));
617
+ const resumeBuilder = sddState.getBuilder();
618
+ const loaded = await resumeBuilder.loadSession();
598
619
  if (!loaded) {
599
- activeBuilder = null;
620
+ sddState.setBuilder(null);
600
621
  return { message: "No saved SDD session found. Use /sdd new to start one." };
601
622
  }
602
- const session = activeBuilder.getSession();
623
+ const session = resumeBuilder.getSession();
603
624
  let taskCount = 0;
604
625
  let completedCount = 0;
605
- const taskGraphId = activeBuilder.getTaskGraphId();
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
- activeTaskStore = store;
613
- activeTaskTracker = tracker;
614
- activeTaskGraphId = taskGraphId;
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 = activeBuilder.getAIPrompt();
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 fsp.readFile(pkgPath, "utf8");
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 fsp.access(tsconfigPath);
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 fsp.readdir(srcDir, { withFileTypes: true });
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 activeBuilder, activeTaskStore, activeTaskTracker, activeTaskGraphId;
877
+ var SDD_META_KEY, SDDState, sddState;
860
878
  var init_sdd = __esm({
861
879
  "src/slash-commands/sdd.ts"() {
862
- activeBuilder = null;
863
- activeTaskStore = null;
864
- activeTaskTracker = null;
865
- activeTaskGraphId = null;
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 = [];