@wrongstack/cli 0.4.1 → 0.5.2

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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, DefaultSessionStore, DefaultSkillLoader, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, InputBuilder, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
2
+ import * as path18 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';
3
4
  import * as crypto from 'crypto';
4
5
  import { randomUUID } from 'crypto';
5
- import * as fs14 from 'fs/promises';
6
- import * as path15 from 'path';
7
- import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets } from '@wrongstack/core/security';
6
+ import * as fs5 from 'fs/promises';
7
+ import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
8
8
  import { WebSocketServer, WebSocket } from 'ws';
9
9
  import { writeFileSync } from 'fs';
10
10
  import { createRequire } from 'module';
@@ -12,12 +12,16 @@ import { MCPRegistry } from '@wrongstack/mcp';
12
12
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
13
13
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
14
14
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
15
- import * as os3 from 'os';
15
+ import * as os4 from 'os';
16
16
  import * as readline from 'readline';
17
+ import { spawn } from 'child_process';
18
+ import { SkillInstaller } from '@wrongstack/core/skills';
17
19
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
18
20
 
19
21
  var __defProp = Object.defineProperty;
22
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
20
23
  var __getOwnPropNames = Object.getOwnPropertyNames;
24
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
21
25
  var __esm = (fn, res) => function __init() {
22
26
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
23
27
  };
@@ -25,6 +29,835 @@ var __export = (target, all) => {
25
29
  for (var name in all)
26
30
  __defProp(target, name, { get: all[name], enumerable: true });
27
31
  };
32
+ var __copyProps = (to, from, except, desc) => {
33
+ if (from && typeof from === "object" || typeof from === "function") {
34
+ for (let key of __getOwnPropNames(from))
35
+ if (!__hasOwnProp.call(to, key) && key !== except)
36
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
37
+ }
38
+ return to;
39
+ };
40
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
41
+
42
+ // src/slash-commands/sdd.ts
43
+ var sdd_exports = {};
44
+ __export(sdd_exports, {
45
+ autoDetectTaskCompletion: () => autoDetectTaskCompletion,
46
+ buildSddCommand: () => buildSddCommand,
47
+ getActiveBuilder: () => getActiveBuilder,
48
+ getActiveSDDContext: () => getActiveSDDContext,
49
+ getActiveSDDPhase: () => getActiveSDDPhase,
50
+ getTaskListText: () => getTaskListText,
51
+ getTaskProgress: () => getTaskProgress,
52
+ markTaskCompleted: () => markTaskCompleted,
53
+ trySaveImplementationPlan: () => trySaveImplementationPlan,
54
+ trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
55
+ trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
56
+ });
57
+ function getActiveSDDContext() {
58
+ if (!activeBuilder) return null;
59
+ const session = activeBuilder.getSession();
60
+ if (session.phase === "done") return null;
61
+ return activeBuilder.getAIPrompt();
62
+ }
63
+ function getActiveSDDPhase() {
64
+ if (!activeBuilder) return null;
65
+ return activeBuilder.getPhase();
66
+ }
67
+ async function trySaveSpecFromAIOutput(aiOutput) {
68
+ if (!activeBuilder) return false;
69
+ const spec = activeBuilder.tryParseSpecFromOutput(aiOutput);
70
+ if (!spec) return false;
71
+ activeBuilder.setSpec(spec);
72
+ return true;
73
+ }
74
+ async function trySaveTasksFromAIOutput(aiOutput) {
75
+ if (!activeBuilder) return false;
76
+ const session = activeBuilder.getSession();
77
+ if (!session.spec) return false;
78
+ const json = activeBuilder.extractJSONArray(aiOutput);
79
+ if (!json) return false;
80
+ let tasks;
81
+ try {
82
+ tasks = JSON.parse(json);
83
+ } catch {
84
+ return false;
85
+ }
86
+ if (!Array.isArray(tasks) || tasks.length === 0) return false;
87
+ const validTasks = tasks.filter((t) => t && typeof t === "object" && typeof t.title === "string" && t.title.length > 0);
88
+ if (validTasks.length === 0) return false;
89
+ const store = new DefaultTaskStore();
90
+ const tracker = new TaskTracker({ store });
91
+ const graph = await tracker.createGraph(session.spec.id, session.spec.title);
92
+ for (const task of validTasks) {
93
+ const title = String(task.title);
94
+ const description = String(task.description ?? "");
95
+ const type = ["feature", "bugfix", "refactor", "docs", "test", "chore"].includes(String(task.type)) ? String(task.type) : "feature";
96
+ const priority = ["critical", "high", "medium", "low"].includes(String(task.priority)) ? String(task.priority) : "medium";
97
+ const estimateHours = Number(task.estimateHours) || 2;
98
+ const tags = Array.isArray(task.tags) ? task.tags.map(String) : [];
99
+ tracker.addNode({
100
+ title,
101
+ description,
102
+ type,
103
+ priority,
104
+ status: "pending",
105
+ estimateHours,
106
+ tags
107
+ });
108
+ }
109
+ activeTaskStore = store;
110
+ activeTaskTracker = tracker;
111
+ activeTaskGraphId = graph.id;
112
+ activeBuilder.setTaskGraphId(graph.id);
113
+ return true;
114
+ }
115
+ function getTaskProgress() {
116
+ if (!activeTaskTracker) return null;
117
+ const progress = activeTaskTracker.getProgress();
118
+ return {
119
+ total: progress.total,
120
+ completed: progress.completed,
121
+ pending: progress.pending,
122
+ percent: progress.percentComplete
123
+ };
124
+ }
125
+ function getTaskListText() {
126
+ if (!activeTaskTracker) return null;
127
+ const nodes = activeTaskTracker.getAllNodes();
128
+ if (nodes.length === 0) return null;
129
+ const lines = nodes.map((n, i) => {
130
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : "\u23F3";
131
+ return `${i + 1}. ${status} [${n.priority}] ${n.title}`;
132
+ });
133
+ return lines.join("\n");
134
+ }
135
+ function markTaskCompleted(taskTitle) {
136
+ if (!activeTaskTracker) return false;
137
+ const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
138
+ const match = nodes.find(
139
+ (n) => n.title.toLowerCase().includes(taskTitle.toLowerCase()) || taskTitle.toLowerCase().includes(n.title.toLowerCase())
140
+ );
141
+ if (!match) return false;
142
+ activeTaskTracker.updateNodeStatus(match.id, "completed");
143
+ return true;
144
+ }
145
+ function autoDetectTaskCompletion(aiOutput) {
146
+ if (!activeTaskTracker) return 0;
147
+ const pending = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
148
+ if (pending.length === 0) return 0;
149
+ let completed = 0;
150
+ const lines = aiOutput.split("\n");
151
+ for (const line of lines) {
152
+ const trimmed = line.trim();
153
+ const sddDoneMatch = trimmed.match(/\/sdd\s+done\s+(.+)/i);
154
+ if (sddDoneMatch?.[1]) {
155
+ const target = sddDoneMatch[1].trim();
156
+ const num = Number(target);
157
+ if (!Number.isNaN(num) && num >= 1 && num <= pending.length) {
158
+ const node = pending[num - 1];
159
+ if (node && node.status !== "completed") {
160
+ activeTaskTracker.updateNodeStatus(node.id, "completed");
161
+ completed++;
162
+ }
163
+ } else {
164
+ const match = pending.find(
165
+ (n) => n.title.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(n.title.toLowerCase())
166
+ );
167
+ if (match && match.status !== "completed") {
168
+ activeTaskTracker.updateNodeStatus(match.id, "completed");
169
+ completed++;
170
+ }
171
+ }
172
+ continue;
173
+ }
174
+ const checkmarkMatch = trimmed.match(/^✅\s*(?:Task:\s*)?(.+)/i);
175
+ if (checkmarkMatch?.[1]) {
176
+ const title = checkmarkMatch[1].trim();
177
+ const match = pending.find(
178
+ (n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
179
+ );
180
+ if (match && match.status !== "completed") {
181
+ activeTaskTracker.updateNodeStatus(match.id, "completed");
182
+ completed++;
183
+ }
184
+ continue;
185
+ }
186
+ const taskNumMatch = trimmed.match(/Task\s+(\d+)\s*[:]\s*(?:complete|done|finished)/i);
187
+ if (taskNumMatch?.[1]) {
188
+ const num = Number(taskNumMatch[1]);
189
+ if (num >= 1 && num <= pending.length) {
190
+ const node = pending[num - 1];
191
+ if (node && node.status !== "completed") {
192
+ activeTaskTracker.updateNodeStatus(node.id, "completed");
193
+ completed++;
194
+ }
195
+ }
196
+ continue;
197
+ }
198
+ const completedMatch = trimmed.match(/^(?:Completed|Done|Finished)\s*[:]\s*(.+)/i);
199
+ if (completedMatch?.[1]) {
200
+ const title = completedMatch[1].trim();
201
+ const match = pending.find(
202
+ (n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
203
+ );
204
+ if (match && match.status !== "completed") {
205
+ activeTaskTracker.updateNodeStatus(match.id, "completed");
206
+ completed++;
207
+ }
208
+ }
209
+ }
210
+ return completed;
211
+ }
212
+ function trySaveImplementationPlan(aiOutput) {
213
+ if (!activeBuilder) return false;
214
+ const session = activeBuilder.getSession();
215
+ if (session.phase !== "implementation") return false;
216
+ const jsonMatch = aiOutput.match(/```json\s*\[/);
217
+ if (jsonMatch?.index && jsonMatch.index > 0) {
218
+ const plan = aiOutput.substring(0, jsonMatch.index).trim();
219
+ if (plan.length > 50) {
220
+ activeBuilder.setImplementation(plan);
221
+ return true;
222
+ }
223
+ }
224
+ if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
225
+ activeBuilder.setImplementation(aiOutput.trim());
226
+ return true;
227
+ }
228
+ return false;
229
+ }
230
+ function getActiveBuilder() {
231
+ return activeBuilder;
232
+ }
233
+ function buildSddCommand(opts) {
234
+ return {
235
+ name: "sdd",
236
+ description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
237
+ async run(args) {
238
+ const ctx = opts.context;
239
+ const projectRoot = ctx?.projectRoot ?? process.cwd();
240
+ const specsDir = path18.join(projectRoot, ".wrongstack", "specs");
241
+ const graphsDir = path18.join(projectRoot, ".wrongstack", "task-graphs");
242
+ const specStore = new SpecStore({ baseDir: specsDir });
243
+ new TaskGraphStore({ baseDir: graphsDir });
244
+ const versioning = new SpecVersioning();
245
+ const [verb, ...rest] = args.trim().split(/\s+/);
246
+ const restJoined = rest.join(" ").trim();
247
+ switch (verb) {
248
+ case "":
249
+ case "help":
250
+ return { message: sddHelp() };
251
+ // ── AI-Driven Spec Session ─────────────────────────────────────────
252
+ case "new":
253
+ case "create": {
254
+ const forceFlag = rest.includes("--force") || rest.includes("-f");
255
+ const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
256
+ if (!activeBuilder && !forceFlag) {
257
+ const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
258
+ try {
259
+ const fsp = await import('fs/promises');
260
+ await fsp.access(sessionPath);
261
+ const projectContext2 = await gatherProjectContext(projectRoot);
262
+ const tempBuilder = new AISpecBuilder({
263
+ store: specStore,
264
+ projectContext: projectContext2,
265
+ sessionPath
266
+ });
267
+ const loaded = await tempBuilder.loadSession();
268
+ if (loaded) {
269
+ const existing = tempBuilder.getSession();
270
+ if (existing.phase !== "done") {
271
+ return {
272
+ message: [
273
+ `An existing SDD session was found:`,
274
+ ` Feature: "${existing.title}"`,
275
+ ` Phase: ${existing.phase}`,
276
+ ` Questions: ${existing.questionCount}`,
277
+ "",
278
+ "Use /sdd resume to continue, or /sdd new --force to start fresh."
279
+ ].join("\n")
280
+ };
281
+ }
282
+ }
283
+ } catch {
284
+ }
285
+ }
286
+ activeTaskStore = null;
287
+ activeTaskTracker = null;
288
+ activeTaskGraphId = null;
289
+ const projectContext = await gatherProjectContext(projectRoot);
290
+ activeBuilder = new AISpecBuilder({
291
+ store: specStore,
292
+ projectContext,
293
+ minQuestions: 2,
294
+ maxQuestions: 10,
295
+ sessionPath: path18.join(projectRoot, ".wrongstack", "sdd-session.json")
296
+ });
297
+ activeBuilder.startSession(title);
298
+ const aiPrompt = activeBuilder.getAIPrompt();
299
+ return {
300
+ message: [
301
+ `\u2554\u2550\u2550\u2550 SDD: AI Spec Builder \u2550\u2550\u2550\u2557`,
302
+ "",
303
+ `Feature: "${title}"`,
304
+ "",
305
+ "The AI will now ask you contextual questions.",
306
+ "Answer naturally \u2014 it will generate the spec when ready.",
307
+ "",
308
+ "Commands: /sdd approve \xB7 /sdd status \xB7 /sdd cancel"
309
+ ].join("\n"),
310
+ runText: `[SDD SESSION ACTIVE]
311
+ ${aiPrompt}
312
+
313
+ ---
314
+ User message:
315
+ Start the specification interview for "${title}". Ask your first contextual question.`
316
+ };
317
+ }
318
+ // ── Phase Transitions ──────────────────────────────────────────────
319
+ case "approve":
320
+ case "ok":
321
+ case "confirm": {
322
+ if (!activeBuilder) {
323
+ return {
324
+ message: "No active SDD session. Use /sdd new to start one."
325
+ };
326
+ }
327
+ const phase = activeBuilder.getSession().phase;
328
+ if (phase === "questioning") {
329
+ const sddCtx = activeBuilder.getAIPrompt();
330
+ return {
331
+ message: "No spec generated yet. Generating now...",
332
+ runText: `[SDD SESSION ACTIVE]
333
+ ${sddCtx}
334
+
335
+ ---
336
+ User message:
337
+ Generate the complete specification now based on the conversation so far.`
338
+ };
339
+ }
340
+ if (phase === "spec_review") {
341
+ const spec = activeBuilder.getSession().spec;
342
+ if (!spec) {
343
+ return { message: "No spec to approve." };
344
+ }
345
+ await activeBuilder.saveSpec();
346
+ versioning.recordVersion(spec, "Initial spec approved");
347
+ activeBuilder.approve();
348
+ const implPrompt = activeBuilder.getAIPrompt();
349
+ return {
350
+ message: [
351
+ `\u2705 Spec "${spec.title}" approved and saved!`,
352
+ `ID: ${spec.id}`,
353
+ `Requirements: ${spec.requirements.length}`,
354
+ "",
355
+ "The AI will now generate an implementation plan and tasks."
356
+ ].join("\n"),
357
+ runText: `[SDD SESSION ACTIVE]
358
+ ${implPrompt}
359
+
360
+ ---
361
+ User message:
362
+ Generate the implementation plan and tasks for the approved spec.`
363
+ };
364
+ }
365
+ if (phase === "task_review") {
366
+ activeBuilder.approve();
367
+ const execPrompt = activeBuilder.getAIPrompt();
368
+ return {
369
+ message: "\u2705 Tasks approved! The AI will now execute them one by one.",
370
+ runText: `[SDD SESSION ACTIVE]
371
+ ${execPrompt}
372
+
373
+ ---
374
+ User message:
375
+ Start executing the tasks one by one.`
376
+ };
377
+ }
378
+ return {
379
+ message: `Current phase is "${phase}". Use /sdd status to see details.`
380
+ };
381
+ }
382
+ // ── Task Execution ─────────────────────────────────────────────────
383
+ case "execute":
384
+ case "run": {
385
+ if (!activeBuilder) {
386
+ return {
387
+ message: "No active SDD session. Use /sdd new to start one."
388
+ };
389
+ }
390
+ const session = activeBuilder.getSession();
391
+ if (session.phase !== "executing" && session.phase !== "task_review") {
392
+ return {
393
+ message: `Cannot execute in phase "${session.phase}". Use /sdd approve first.`
394
+ };
395
+ }
396
+ const execPrompt = activeBuilder.getAIPrompt();
397
+ return {
398
+ message: "\u26A1 Starting task execution. The AI will execute tasks one by one.",
399
+ runText: `[SDD SESSION ACTIVE]
400
+ ${execPrompt}
401
+
402
+ ---
403
+ User message:
404
+ Start executing the tasks one by one.`
405
+ };
406
+ }
407
+ case "plan":
408
+ case "impl": {
409
+ if (!activeBuilder) {
410
+ return { message: "No active SDD session. Use /sdd new to start one." };
411
+ }
412
+ const session = activeBuilder.getSession();
413
+ if (!session.implementation) {
414
+ return {
415
+ message: session.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
416
+ };
417
+ }
418
+ return {
419
+ message: [
420
+ "\u2550\u2550\u2550 Implementation Plan \u2550\u2550\u2550",
421
+ "",
422
+ session.implementation
423
+ ].join("\n")
424
+ };
425
+ }
426
+ case "spec": {
427
+ if (!activeBuilder) {
428
+ return { message: "No active SDD session. Use /sdd new to start one." };
429
+ }
430
+ const session = activeBuilder.getSession();
431
+ if (!session.spec) {
432
+ return {
433
+ message: session.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
434
+ };
435
+ }
436
+ const spec = session.spec;
437
+ const lines = [
438
+ `\u2550\u2550\u2550 Current Spec \u2550\u2550\u2550`,
439
+ "",
440
+ `Title: ${spec.title}`,
441
+ `Version: ${spec.version}`,
442
+ `Status: ${spec.status}`,
443
+ "",
444
+ "## Overview",
445
+ spec.overview
446
+ ];
447
+ if (spec.requirements.length > 0) {
448
+ lines.push("", `## Requirements (${spec.requirements.length})`);
449
+ for (const r of spec.requirements) {
450
+ const ac = r.acceptanceCriteria.length > 0 ? ` \u2192 ${r.acceptanceCriteria.join(", ")}` : "";
451
+ lines.push(` [${r.priority}] ${r.description}${ac}`);
452
+ }
453
+ }
454
+ return { message: lines.join("\n") };
455
+ }
456
+ case "tasks":
457
+ case "task": {
458
+ if (!activeTaskTracker) {
459
+ return { message: "No tasks generated yet. Use /sdd new to start." };
460
+ }
461
+ const nodes = activeTaskTracker.getAllNodes();
462
+ if (nodes.length === 0) {
463
+ return { message: "No tasks in the current graph." };
464
+ }
465
+ const progress = activeTaskTracker.getProgress();
466
+ const lines = [
467
+ `\u2550\u2550\u2550 Task List (${progress.completed}/${progress.total} done) \u2550\u2550\u2550`,
468
+ ""
469
+ ];
470
+ for (let i = 0; i < nodes.length; i++) {
471
+ const n = nodes[i];
472
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : "\u23F3";
473
+ lines.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
474
+ if (n.description) {
475
+ lines.push(` ${n.description.split("\n")[0]}`);
476
+ }
477
+ }
478
+ return { message: lines.join("\n") };
479
+ }
480
+ case "done":
481
+ case "complete": {
482
+ if (!activeTaskTracker) {
483
+ return { message: "No tasks to complete." };
484
+ }
485
+ if (!restJoined) {
486
+ return { message: "Usage: /sdd done <task title or number>" };
487
+ }
488
+ const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
489
+ const num = Number(restJoined);
490
+ let matched = false;
491
+ if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
492
+ const node = nodes[num - 1];
493
+ if (node) {
494
+ activeTaskTracker.updateNodeStatus(node.id, "completed");
495
+ matched = true;
496
+ }
497
+ }
498
+ if (!matched) {
499
+ const match = nodes.find(
500
+ (n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
501
+ );
502
+ if (match) {
503
+ activeTaskTracker.updateNodeStatus(match.id, "completed");
504
+ matched = true;
505
+ }
506
+ }
507
+ if (!matched) {
508
+ return { message: `No pending task matching "${restJoined}".` };
509
+ }
510
+ const remaining = activeTaskTracker.getProgress();
511
+ return {
512
+ message: `\u2705 Task completed! ${remaining.completed}/${remaining.total} done (${remaining.percentComplete}%)`
513
+ };
514
+ }
515
+ // ── Session Management ─────────────────────────────────────────────
516
+ case "status": {
517
+ if (!activeBuilder) {
518
+ return { message: "No active SDD session." };
519
+ }
520
+ const session = activeBuilder.getSession();
521
+ const phaseEmoji = {
522
+ questioning: "\u2753",
523
+ spec_review: "\u{1F4CB}",
524
+ implementation: "\u{1F3D7}\uFE0F",
525
+ task_review: "\u{1F4DD}",
526
+ executing: "\u26A1",
527
+ done: "\u2705"
528
+ };
529
+ const progress = getTaskProgress();
530
+ const lines = [
531
+ "\u2550\u2550\u2550 SDD Session Status \u2550\u2550\u2550",
532
+ "",
533
+ `Feature: "${session.title}"`,
534
+ `Phase: ${phaseEmoji[session.phase]} ${session.phase}`,
535
+ `Questions asked: ${session.questionCount}`
536
+ ];
537
+ if (session.spec) {
538
+ lines.push(`Spec: ${session.spec.title} (${session.spec.requirements.length} requirements)`);
539
+ lines.push(` Requirements: ${session.spec.requirements.map((r) => r.description).join(", ")}`);
540
+ }
541
+ if (session.implementation) {
542
+ const planPreview = session.implementation.split("\n").slice(0, 3).join(" ");
543
+ lines.push(`Implementation: ${planPreview}${session.implementation.length > 100 ? "..." : ""}`);
544
+ }
545
+ if (progress && progress.total > 0) {
546
+ lines.push(`Tasks: ${progress.completed}/${progress.total} (${progress.percent}%)`);
547
+ }
548
+ lines.push("", `Session ID: ${session.id}`);
549
+ lines.push("Commands: /sdd plan \xB7 /sdd tasks \xB7 /sdd approve \xB7 /sdd cancel");
550
+ return {
551
+ message: lines.join("\n")
552
+ };
553
+ }
554
+ case "cancel": {
555
+ const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
556
+ let deletedFromDisk = false;
557
+ try {
558
+ const fsp = await import('fs/promises');
559
+ await fsp.unlink(sessionPath);
560
+ deletedFromDisk = true;
561
+ } catch {
562
+ }
563
+ if (activeBuilder) {
564
+ const title = activeBuilder.getSession().title;
565
+ await activeBuilder.deleteSession();
566
+ activeBuilder = null;
567
+ activeTaskStore = null;
568
+ activeTaskTracker = null;
569
+ activeTaskGraphId = null;
570
+ return { message: `SDD session for "${title}" cancelled.` };
571
+ }
572
+ if (deletedFromDisk) {
573
+ return { message: "Stale SDD session file deleted. You can now use /sdd new." };
574
+ }
575
+ return { message: "No active SDD session." };
576
+ }
577
+ case "resume": {
578
+ if (activeBuilder) {
579
+ return { message: "An SDD session is already active. Use /sdd cancel first." };
580
+ }
581
+ const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
582
+ const projectContext = await gatherProjectContext(projectRoot);
583
+ activeBuilder = new AISpecBuilder({
584
+ store: specStore,
585
+ projectContext,
586
+ minQuestions: 2,
587
+ maxQuestions: 10,
588
+ sessionPath
589
+ });
590
+ const loaded = await activeBuilder.loadSession();
591
+ if (!loaded) {
592
+ activeBuilder = null;
593
+ return { message: "No saved SDD session found. Use /sdd new to start one." };
594
+ }
595
+ const session = activeBuilder.getSession();
596
+ let taskCount = 0;
597
+ let completedCount = 0;
598
+ const taskGraphId = activeBuilder.getTaskGraphId();
599
+ if (taskGraphId) {
600
+ try {
601
+ const store = new DefaultTaskStore();
602
+ const tracker = new TaskTracker({ store });
603
+ const graph = await tracker.loadGraph(taskGraphId);
604
+ if (graph) {
605
+ activeTaskStore = store;
606
+ activeTaskTracker = tracker;
607
+ activeTaskGraphId = taskGraphId;
608
+ const progress = tracker.getProgress();
609
+ taskCount = progress.total;
610
+ completedCount = progress.completed;
611
+ }
612
+ } catch {
613
+ }
614
+ }
615
+ const resumePrompt = activeBuilder.getAIPrompt();
616
+ return {
617
+ message: [
618
+ `\u2554\u2550\u2550\u2550 SDD Session Resumed \u2550\u2550\u2550\u2557`,
619
+ "",
620
+ `Feature: "${session.title}"`,
621
+ `Phase: ${session.phase}`,
622
+ `Questions asked: ${session.questionCount}`,
623
+ session.spec ? `Spec: ${session.spec.title}` : "",
624
+ taskCount > 0 ? `Tasks: ${completedCount}/${taskCount} completed` : "",
625
+ "",
626
+ "The AI will continue from where you left off."
627
+ ].filter(Boolean).join("\n"),
628
+ runText: `[SDD SESSION ACTIVE]
629
+ ${resumePrompt}
630
+
631
+ ---
632
+ User message:
633
+ Continue from where we left off. Check the session status and proceed.`
634
+ };
635
+ }
636
+ // ── Spec Browsing ──────────────────────────────────────────────────
637
+ case "list":
638
+ case "ls": {
639
+ const entries = await specStore.list();
640
+ if (entries.length === 0) {
641
+ return { message: "No specs saved. Use /sdd new to create one." };
642
+ }
643
+ const lines = entries.map((e, i) => {
644
+ const status = e.status === "draft" ? "\u{1F4DD}" : e.status === "approved" ? "\u2705" : "\u{1F4CB}";
645
+ return `${i + 1}. ${status} ${e.title} (${e.version}) \u2014 ${e.id.slice(0, 8)}...`;
646
+ });
647
+ return { message: `Saved Specs:
648
+ ${lines.join("\n")}` };
649
+ }
650
+ case "show":
651
+ case "view": {
652
+ const spec = await findSpec(specStore, restJoined);
653
+ if (!spec) return { message: `Spec "${restJoined}" not found.` };
654
+ const parser = new SpecParser();
655
+ const analysis = parser.analyze(spec);
656
+ return {
657
+ message: [
658
+ `# ${spec.title}`,
659
+ `Version: ${spec.version} | Status: ${spec.status}`,
660
+ "",
661
+ "## Overview",
662
+ spec.overview,
663
+ "",
664
+ `## Requirements (${spec.requirements.length})`,
665
+ ...spec.requirements.map((r) => {
666
+ const tags = `[${r.type}][${r.priority}]`;
667
+ const ac = r.acceptanceCriteria.length > 0 ? `
668
+ AC: ${r.acceptanceCriteria.join(", ")}` : "";
669
+ return `- ${tags} ${r.description}${ac}`;
670
+ }),
671
+ "",
672
+ renderSpecAnalysis(spec, {
673
+ completeness: analysis.completeness,
674
+ gaps: analysis.gaps,
675
+ risks: analysis.risks.map((r) => r.risk),
676
+ suggestions: analysis.suggestions
677
+ })
678
+ ].join("\n")
679
+ };
680
+ }
681
+ case "templates": {
682
+ const templates = listTemplates();
683
+ const lines = templates.map(
684
+ (t) => ` ${t.id}: ${t.name} \u2014 ${t.description}`
685
+ );
686
+ return {
687
+ message: `Available Templates:
688
+ ${lines.join("\n")}`
689
+ };
690
+ }
691
+ case "from": {
692
+ const templateId = restJoined || "feature";
693
+ const template = getTemplate(templateId);
694
+ if (!template) {
695
+ return {
696
+ message: `Template "${templateId}" not found.
697
+ Available: ${listTemplates().map((t) => t.id).join(", ")}`
698
+ };
699
+ }
700
+ const skeleton = templateToMarkdown(template, "New Specification");
701
+ const spec = await specStore.createDraft("New Specification");
702
+ await specStore.update(spec.id, { sections: [] });
703
+ return {
704
+ message: [
705
+ `Created draft spec from template "${template.name}".`,
706
+ `ID: ${spec.id}`,
707
+ "",
708
+ "Edit the spec through the AI conversation or /sdd show to review.",
709
+ "",
710
+ skeleton
711
+ ].join("\n")
712
+ };
713
+ }
714
+ case "version":
715
+ case "history": {
716
+ const spec = await findSpec(specStore, restJoined);
717
+ if (!spec)
718
+ return { message: `Spec "${restJoined}" not found.` };
719
+ const history = versioning.getHistory(spec.id);
720
+ if (history.length === 0) {
721
+ return {
722
+ message: `No version history for "${spec.title}".`
723
+ };
724
+ }
725
+ const lines = history.map(
726
+ (v, i) => `${i + 1}. v${v.version} \u2014 ${new Date(v.timestamp).toISOString()}${v.changeDescription ? ` (${v.changeDescription})` : ""}`
727
+ );
728
+ return {
729
+ message: `Version History for "${spec.title}":
730
+ ${lines.join("\n")}`
731
+ };
732
+ }
733
+ default:
734
+ return {
735
+ message: `Unknown command "${verb}".
736
+
737
+ ${sddHelp()}`
738
+ };
739
+ }
740
+ }
741
+ };
742
+ }
743
+ function sddHelp() {
744
+ return [
745
+ "",
746
+ "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
747
+ "\u2551 \u{1F680} SDD \u2014 AI-Driven Spec Builder \u2551",
748
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
749
+ "",
750
+ " \u250C\u2500 \u{1F195} Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
751
+ " \u2502 /sdd new [title] Start a new spec session \u2502",
752
+ " \u2502 /sdd new --force Start fresh (skip resume check) \u2502",
753
+ " \u2502 /sdd resume Resume a saved session \u2502",
754
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
755
+ "",
756
+ " \u250C\u2500 \u{1F504} Flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
757
+ " \u2502 /sdd approve Approve current phase \u2502",
758
+ " \u2502 /sdd spec Show current session's spec \u2502",
759
+ " \u2502 /sdd plan Show implementation plan \u2502",
760
+ " \u2502 /sdd execute Execute generated tasks \u2502",
761
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
762
+ "",
763
+ " \u250C\u2500 \u{1F4CB} Task Management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
764
+ " \u2502 /sdd tasks Show current task list \u2502",
765
+ " \u2502 /sdd done <N> Mark task complete (by # or name) \u2502",
766
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
767
+ "",
768
+ " \u250C\u2500 \u{1F4CA} Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
769
+ " \u2502 /sdd status Show session status \u2502",
770
+ " \u2502 /sdd cancel Cancel session \u2502",
771
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
772
+ "",
773
+ " \u250C\u2500 \u{1F4C1} Spec History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
774
+ " \u2502 /sdd list List saved specs \u2502",
775
+ " \u2502 /sdd show <id> Show spec details \u2502",
776
+ " \u2502 /sdd templates List available templates \u2502",
777
+ " \u2502 /sdd from <tmpl> Create from template \u2502",
778
+ " \u2502 /sdd version <id> Show version history \u2502",
779
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
780
+ "",
781
+ " \u250C\u2500 \u{1F4A1} Quick Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
782
+ " \u2502 \u2502",
783
+ " \u2502 1. /sdd new Auth System \u2502",
784
+ " \u2502 \u2192 AI starts asking questions \u2502",
785
+ " \u2502 \u2502",
786
+ " \u2502 2. Just type your answers naturally \u2502",
787
+ " \u2502 \u2192 AI continues the interview \u2502",
788
+ " \u2502 \u2502",
789
+ " \u2502 3. AI generates spec (auto-detected) \u2502",
790
+ " \u2502 \u2192 /sdd approve \u2502",
791
+ " \u2502 \u2502",
792
+ " \u2502 3. AI generates implementation + tasks \u2502",
793
+ " \u2502 \u2192 /sdd approve \u2502",
794
+ " \u2502 \u2502",
795
+ " \u2502 4. AI executes tasks one by one \u2502",
796
+ " \u2502 \u2192 /sdd tasks (view progress) \u2502",
797
+ " \u2502 \u2192 /sdd done 1 (manual completion) \u2502",
798
+ " \u2502 \u2502",
799
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
800
+ ""
801
+ ].join("\n");
802
+ }
803
+ async function gatherProjectContext(projectRoot) {
804
+ const parts = [];
805
+ try {
806
+ const fsp = await import('fs/promises');
807
+ const pkgPath = path18.join(projectRoot, "package.json");
808
+ const pkgRaw = await fsp.readFile(pkgPath, "utf8");
809
+ const pkg = JSON.parse(pkgRaw);
810
+ parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
811
+ parts.push(`Description: ${String(pkg.description ?? "none")}`);
812
+ if (pkg.dependencies) {
813
+ const deps = Object.keys(pkg.dependencies);
814
+ parts.push(`Dependencies: ${deps.slice(0, 20).join(", ")}${deps.length > 20 ? "..." : ""}`);
815
+ }
816
+ if (pkg.devDependencies) {
817
+ const devDeps = Object.keys(pkg.devDependencies);
818
+ parts.push(`Dev Dependencies: ${devDeps.slice(0, 15).join(", ")}${devDeps.length > 15 ? "..." : ""}`);
819
+ }
820
+ } catch {
821
+ }
822
+ try {
823
+ const fsp = await import('fs/promises');
824
+ const tsconfigPath = path18.join(projectRoot, "tsconfig.json");
825
+ await fsp.access(tsconfigPath);
826
+ parts.push("Language: TypeScript");
827
+ } catch {
828
+ }
829
+ try {
830
+ const fsp = await import('fs/promises');
831
+ const srcDir = path18.join(projectRoot, "src");
832
+ const entries = await fsp.readdir(srcDir, { withFileTypes: true });
833
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
834
+ if (dirs.length > 0) {
835
+ parts.push(`Source structure: src/${dirs.join(", src/")}`);
836
+ }
837
+ } catch {
838
+ }
839
+ return parts.join("\n");
840
+ }
841
+ async function findSpec(store, idOrTitle) {
842
+ if (!idOrTitle) return null;
843
+ const byId = await store.load(idOrTitle);
844
+ if (byId) return byId;
845
+ const all = await store.list();
846
+ const match = all.find(
847
+ (e) => e.id.startsWith(idOrTitle) || e.title.toLowerCase().includes(idOrTitle.toLowerCase())
848
+ );
849
+ if (match) return store.load(match.id);
850
+ return null;
851
+ }
852
+ var activeBuilder, activeTaskStore, activeTaskTracker, activeTaskGraphId;
853
+ var init_sdd = __esm({
854
+ "src/slash-commands/sdd.ts"() {
855
+ activeBuilder = null;
856
+ activeTaskStore = null;
857
+ activeTaskTracker = null;
858
+ activeTaskGraphId = null;
859
+ }
860
+ });
28
861
  function normalizeKeys(cfg) {
29
862
  if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
30
863
  return cfg.apiKeys.map((k) => ({ ...k }));
@@ -549,7 +1382,7 @@ async function runWebUI(opts) {
549
1382
  if (!opts.globalConfigPath) return {};
550
1383
  let raw;
551
1384
  try {
552
- raw = await fs14.readFile(opts.globalConfigPath, "utf8");
1385
+ raw = await fs5.readFile(opts.globalConfigPath, "utf8");
553
1386
  } catch {
554
1387
  return {};
555
1388
  }
@@ -559,13 +1392,16 @@ async function runWebUI(opts) {
559
1392
  } catch {
560
1393
  return {};
561
1394
  }
562
- return parsed.providers ?? {};
1395
+ if (!parsed.providers) return {};
1396
+ const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
1397
+ const vault = new DefaultSecretVault$1({ keyFile });
1398
+ return decryptConfigSecrets$1(parsed.providers, vault);
563
1399
  }
564
1400
  async function saveProviders(providers) {
565
1401
  if (!opts.globalConfigPath) return;
566
1402
  let raw;
567
1403
  try {
568
- raw = await fs14.readFile(opts.globalConfigPath, "utf8");
1404
+ raw = await fs5.readFile(opts.globalConfigPath, "utf8");
569
1405
  } catch {
570
1406
  raw = "{}";
571
1407
  }
@@ -576,7 +1412,7 @@ async function runWebUI(opts) {
576
1412
  parsed = {};
577
1413
  }
578
1414
  parsed.providers = providers;
579
- const keyFile = path15.join(path15.dirname(opts.globalConfigPath), ".key");
1415
+ const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
580
1416
  const vault = new DefaultSecretVault$1({ keyFile });
581
1417
  const encrypted = encryptConfigSecrets(parsed, vault);
582
1418
  await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
@@ -728,10 +1564,10 @@ function parseSpawnFlags(input) {
728
1564
  return { description: rest.trim(), opts };
729
1565
  }
730
1566
  async function bootConfig(flags) {
731
- const cwd = typeof flags["cwd"] === "string" ? path15.resolve(flags["cwd"]) : process.cwd();
1567
+ const cwd = typeof flags["cwd"] === "string" ? path18.resolve(flags["cwd"]) : process.cwd();
732
1568
  const pathResolver = new DefaultPathResolver(cwd);
733
1569
  const projectRoot = pathResolver.projectRoot;
734
- const userHome = os3.homedir();
1570
+ const userHome = os4.homedir();
735
1571
  const wpaths = resolveWstackPaths({ projectRoot, userHome });
736
1572
  await ensureProjectMeta(wpaths, projectRoot);
737
1573
  const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
@@ -779,13 +1615,13 @@ function flagsToConfigPatch(flags) {
779
1615
  }
780
1616
  async function ensureProjectMeta(paths, projectRoot) {
781
1617
  try {
782
- await fs14.mkdir(paths.projectDir, { recursive: true });
1618
+ await fs5.mkdir(paths.projectDir, { recursive: true });
783
1619
  const meta = {
784
1620
  hash: paths.projectHash,
785
1621
  root: projectRoot,
786
1622
  lastSeen: (/* @__PURE__ */ new Date()).toISOString()
787
1623
  };
788
- await fs14.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
1624
+ await fs5.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
789
1625
  } catch {
790
1626
  }
791
1627
  }
@@ -795,11 +1631,11 @@ var ReadlineInputReader = class {
795
1631
  history = [];
796
1632
  pending = false;
797
1633
  constructor(opts = {}) {
798
- this.historyFile = opts.historyFile ?? path15.join(os3.homedir(), ".wrongstack", "history");
1634
+ this.historyFile = opts.historyFile ?? path18.join(os4.homedir(), ".wrongstack", "history");
799
1635
  }
800
1636
  async loadHistory() {
801
1637
  try {
802
- const raw = await fs14.readFile(this.historyFile, "utf8");
1638
+ const raw = await fs5.readFile(this.historyFile, "utf8");
803
1639
  this.history = raw.split("\n").filter(Boolean).slice(-1e3);
804
1640
  } catch {
805
1641
  this.history = [];
@@ -807,8 +1643,8 @@ var ReadlineInputReader = class {
807
1643
  }
808
1644
  async saveHistory() {
809
1645
  try {
810
- await fs14.mkdir(path15.dirname(this.historyFile), { recursive: true });
811
- await fs14.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
1646
+ await fs5.mkdir(path18.dirname(this.historyFile), { recursive: true });
1647
+ await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
812
1648
  } catch {
813
1649
  }
814
1650
  }
@@ -1278,7 +2114,7 @@ async function saveToGlobalConfig(configPath, provider, model) {
1278
2114
  }
1279
2115
  async function pathExists(file) {
1280
2116
  try {
1281
- await fs14.access(file);
2117
+ await fs5.access(file);
1282
2118
  return true;
1283
2119
  } catch {
1284
2120
  return false;
@@ -1289,10 +2125,10 @@ async function detectPackageManager(root, declared) {
1289
2125
  const name = declared.split("@")[0];
1290
2126
  if (name) return name;
1291
2127
  }
1292
- if (await pathExists(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
1293
- if (await pathExists(path15.join(root, "bun.lockb"))) return "bun";
1294
- if (await pathExists(path15.join(root, "bun.lock"))) return "bun";
1295
- if (await pathExists(path15.join(root, "yarn.lock"))) return "yarn";
2128
+ if (await pathExists(path18.join(root, "pnpm-lock.yaml"))) return "pnpm";
2129
+ if (await pathExists(path18.join(root, "bun.lockb"))) return "bun";
2130
+ if (await pathExists(path18.join(root, "bun.lock"))) return "bun";
2131
+ if (await pathExists(path18.join(root, "yarn.lock"))) return "yarn";
1296
2132
  return "npm";
1297
2133
  }
1298
2134
  function hasUsableScript(scripts, name) {
@@ -1313,7 +2149,7 @@ function parseMakeTargets(makefile) {
1313
2149
  async function detectProjectFacts(root) {
1314
2150
  const facts = { hints: [] };
1315
2151
  try {
1316
- const pkg = JSON.parse(await fs14.readFile(path15.join(root, "package.json"), "utf8"));
2152
+ const pkg = JSON.parse(await fs5.readFile(path18.join(root, "package.json"), "utf8"));
1317
2153
  const scripts = pkg.scripts ?? {};
1318
2154
  const pm = await detectPackageManager(root, pkg.packageManager);
1319
2155
  if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
@@ -1327,14 +2163,14 @@ async function detectProjectFacts(root) {
1327
2163
  } catch {
1328
2164
  }
1329
2165
  try {
1330
- if (!await pathExists(path15.join(root, "pyproject.toml"))) throw new Error("not python");
2166
+ if (!await pathExists(path18.join(root, "pyproject.toml"))) throw new Error("not python");
1331
2167
  facts.test ??= "pytest";
1332
2168
  facts.lint ??= "ruff check .";
1333
2169
  facts.hints.push("pyproject.toml");
1334
2170
  } catch {
1335
2171
  }
1336
2172
  try {
1337
- if (!await pathExists(path15.join(root, "go.mod"))) throw new Error("not go");
2173
+ if (!await pathExists(path18.join(root, "go.mod"))) throw new Error("not go");
1338
2174
  facts.build ??= "go build ./...";
1339
2175
  facts.test ??= "go test ./...";
1340
2176
  facts.run ??= "go run .";
@@ -1342,7 +2178,7 @@ async function detectProjectFacts(root) {
1342
2178
  } catch {
1343
2179
  }
1344
2180
  try {
1345
- if (!await pathExists(path15.join(root, "Cargo.toml"))) throw new Error("not rust");
2181
+ if (!await pathExists(path18.join(root, "Cargo.toml"))) throw new Error("not rust");
1346
2182
  facts.build ??= "cargo build";
1347
2183
  facts.test ??= "cargo test";
1348
2184
  facts.lint ??= "cargo clippy";
@@ -1351,7 +2187,7 @@ async function detectProjectFacts(root) {
1351
2187
  } catch {
1352
2188
  }
1353
2189
  try {
1354
- const makefile = await fs14.readFile(path15.join(root, "Makefile"), "utf8");
2190
+ const makefile = await fs5.readFile(path18.join(root, "Makefile"), "utf8");
1355
2191
  const targets = parseMakeTargets(makefile);
1356
2192
  facts.build ??= targets.has("build") ? "make build" : "make";
1357
2193
  if (targets.has("test")) facts.test ??= "make test";
@@ -1485,6 +2321,175 @@ function buildClearCommand(opts) {
1485
2321
  }
1486
2322
  };
1487
2323
  }
2324
+ async function runGit(args, cwd) {
2325
+ return new Promise((resolve3) => {
2326
+ const child = spawn("git", args, {
2327
+ cwd,
2328
+ stdio: ["ignore", "pipe", "pipe"]
2329
+ });
2330
+ let stdout = "";
2331
+ let stderr = "";
2332
+ child.stdout?.on("data", (d) => stdout += d);
2333
+ child.stderr?.on("data", (d) => stderr += d);
2334
+ child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
2335
+ });
2336
+ }
2337
+ function detectCommitType(stats) {
2338
+ const lines = stats.split("\n");
2339
+ const hasTestFiles = lines.some(
2340
+ (l) => l.includes("_test.") || l.includes(".test.") || l.includes(".spec.")
2341
+ );
2342
+ const hasDocs = lines.some(
2343
+ (l) => l.includes("README") || l.includes("CHANGELOG") || l.includes("docs/") || l.includes(".md")
2344
+ );
2345
+ const hasConfig = lines.some(
2346
+ (l) => l.includes("config") || l.includes("tsconfig") || l.includes(".json")
2347
+ );
2348
+ if (hasTestFiles) return "test";
2349
+ if (hasDocs) return "docs";
2350
+ if (hasConfig) return "chore";
2351
+ return "feat";
2352
+ }
2353
+ async function generateCommitMessage(cwd) {
2354
+ const statsResult = await runGit(["diff", "--stat"], cwd);
2355
+ if (statsResult.code !== 0) return "chore: update";
2356
+ const nameResult = await runGit(["diff", "--name-only"], cwd);
2357
+ const files = nameResult.stdout.split("\n").filter(Boolean);
2358
+ const commitType = detectCommitType(statsResult.stdout);
2359
+ let scope = "";
2360
+ if (files.length > 0) {
2361
+ const primary = files[0].split("/")[0];
2362
+ if (primary && primary !== "packages" && primary !== "apps" && primary !== "node_modules") {
2363
+ scope = `(${primary})`;
2364
+ }
2365
+ }
2366
+ if (files.length === 0) {
2367
+ return `${commitType}${scope}: update`;
2368
+ }
2369
+ if (files.length <= 3) {
2370
+ const summary2 = files.map((f) => f.split("/").pop()).join(", ");
2371
+ return `${commitType}${scope}: ${summary2}`;
2372
+ }
2373
+ const summary = files.slice(0, 3).map((f) => f.split("/").pop()).join(", ") + ` and ${files.length - 3} more`;
2374
+ return `${commitType}${scope}: ${summary}`;
2375
+ }
2376
+ async function hasUncommittedChanges(cwd) {
2377
+ const result = await runGit(["status", "--porcelain"], cwd);
2378
+ return result.stdout.trim().length > 0;
2379
+ }
2380
+ async function isGitRepo(cwd) {
2381
+ const result = await runGit(["rev-parse", "--git-dir"], cwd);
2382
+ return result.code === 0;
2383
+ }
2384
+ function buildCommitCommand(_opts) {
2385
+ return {
2386
+ name: "commit",
2387
+ description: "Stage all changes and commit with auto-generated message.",
2388
+ aliases: ["gc"],
2389
+ async run(args, ctx) {
2390
+ const cwd = ctx?.cwd ?? process.cwd();
2391
+ if (!await isGitRepo(cwd)) {
2392
+ return { message: "Not a git repository." };
2393
+ }
2394
+ if (!await hasUncommittedChanges(cwd)) {
2395
+ return { message: "Nothing to commit (working tree clean)." };
2396
+ }
2397
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
2398
+ const message = await generateCommitMessage(cwd);
2399
+ if (dryRun) {
2400
+ return {
2401
+ message: `Would commit:
2402
+
2403
+ ${color.green(message)}
2404
+
2405
+ ${color.dim("(dry-run \u2014 no actual commit)")}`
2406
+ };
2407
+ }
2408
+ const stageResult = await runGit(["add", "."], cwd);
2409
+ if (stageResult.code !== 0) {
2410
+ return { message: `Stage failed: ${stageResult.stderr}` };
2411
+ }
2412
+ const commitResult = await runGit(["commit", "-m", message], cwd);
2413
+ if (commitResult.code !== 0) {
2414
+ return { message: `Commit failed: ${commitResult.stderr}` };
2415
+ }
2416
+ const hashResult = await runGit(["rev-parse", "--short", "HEAD"], cwd);
2417
+ const hash = hashResult.stdout.trim();
2418
+ const pushResult = await runGit(["remote"], cwd);
2419
+ const hasRemote = pushResult.stdout.trim().length > 0;
2420
+ let pushMsg = "";
2421
+ if (hasRemote) {
2422
+ pushMsg = `
2423
+
2424
+ ${color.dim("Tip: Run /push to push to remote")}`;
2425
+ }
2426
+ return {
2427
+ message: `${color.green("\u2713")} Committed: ${color.bold(message)}
2428
+ ${color.dim(hash)}${pushMsg}`
2429
+ };
2430
+ }
2431
+ };
2432
+ }
2433
+ function buildGitcheckCommand(_opts) {
2434
+ return {
2435
+ name: "gitcheck",
2436
+ description: "Check for uncommitted changes (for system prompt integration).",
2437
+ aliases: ["gcstatus"],
2438
+ async run(_args, ctx) {
2439
+ const cwd = ctx?.cwd ?? process.cwd();
2440
+ if (!await isGitRepo(cwd)) {
2441
+ return { message: "" };
2442
+ }
2443
+ if (!await hasUncommittedChanges(cwd)) {
2444
+ return { message: "" };
2445
+ }
2446
+ const statusResult = await runGit(["status", "--porcelain"], cwd);
2447
+ const lines = statusResult.stdout.split("\n").filter(Boolean);
2448
+ const count = lines.length;
2449
+ if (count === 0) return { message: "" };
2450
+ return {
2451
+ message: `\u26A0 ${color.yellow(`${count} uncommitted change${count > 1 ? "s" : ""}`)} \u2014 consider /commit`
2452
+ };
2453
+ }
2454
+ };
2455
+ }
2456
+ function buildPushCommand(_opts) {
2457
+ return {
2458
+ name: "push",
2459
+ description: "Push to remote after commit.",
2460
+ async run(args, ctx) {
2461
+ const cwd = ctx?.cwd ?? process.cwd();
2462
+ if (!await isGitRepo(cwd)) {
2463
+ return { message: "Not a git repository." };
2464
+ }
2465
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
2466
+ const force = args.includes("--force") || args.includes("-f");
2467
+ const remoteResult = await runGit(["remote"], cwd);
2468
+ const remotes = remoteResult.stdout.split("\n").filter(Boolean);
2469
+ if (remotes.length === 0) {
2470
+ return { message: "No remote configured. Add one with: git remote add origin <url>" };
2471
+ }
2472
+ if (dryRun) {
2473
+ return {
2474
+ message: `Would push to ${remotes.join(", ")}${force ? " (force)" : ""}
2475
+ ${color.dim("(dry-run)")}`
2476
+ };
2477
+ }
2478
+ const branchResult = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
2479
+ const branch = branchResult.stdout.trim() || "main";
2480
+ const pushArgs = ["push"];
2481
+ if (force) pushArgs.push("--force");
2482
+ pushArgs.push(...remotes, branch);
2483
+ const pushResult = await runGit(pushArgs, cwd);
2484
+ if (pushResult.code !== 0) {
2485
+ return { message: `Push failed: ${pushResult.stderr}` };
2486
+ }
2487
+ return {
2488
+ message: `${color.green("\u2713")} Pushed to ${remotes.join(", ")} (${branch})`
2489
+ };
2490
+ }
2491
+ };
2492
+ }
1488
2493
 
1489
2494
  // src/slash-commands/compact.ts
1490
2495
  function buildCompactCommand(opts) {
@@ -1814,10 +2819,10 @@ function buildInitCommand(opts) {
1814
2819
  description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
1815
2820
  async run(args, ctx) {
1816
2821
  const force = args.trim() === "--force";
1817
- const dir = path15.join(ctx.projectRoot, ".wrongstack");
1818
- const file = path15.join(dir, "AGENTS.md");
2822
+ const dir = path18.join(ctx.projectRoot, ".wrongstack");
2823
+ const file = path18.join(dir, "AGENTS.md");
1819
2824
  try {
1820
- await fs14.access(file);
2825
+ await fs5.access(file);
1821
2826
  if (!force) {
1822
2827
  const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
1823
2828
  opts.renderer.writeWarning(msg2);
@@ -1827,8 +2832,8 @@ function buildInitCommand(opts) {
1827
2832
  }
1828
2833
  const detected = await detectProjectFacts(ctx.projectRoot);
1829
2834
  const body = renderAgentsTemplate(detected);
1830
- await fs14.mkdir(dir, { recursive: true });
1831
- await fs14.writeFile(file, body, "utf8");
2835
+ await fs5.mkdir(dir, { recursive: true });
2836
+ await fs5.writeFile(file, body, "utf8");
1832
2837
  if (detected.hints.length > 0) {
1833
2838
  const msg2 = `Wrote ${file}
1834
2839
  Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
@@ -2057,6 +3062,13 @@ function buildExitCommand(opts) {
2057
3062
  aliases: ["quit", "q"],
2058
3063
  description: "Exit the REPL.",
2059
3064
  async run() {
3065
+ if (opts.onBeforeExit) {
3066
+ const result = await opts.onBeforeExit();
3067
+ if (result?.abort) {
3068
+ opts.onExit?.();
3069
+ return { message: result.message ?? "", exit: true };
3070
+ }
3071
+ }
2060
3072
  opts.onExit?.();
2061
3073
  return { exit: true };
2062
3074
  }
@@ -2065,7 +3077,7 @@ function buildExitCommand(opts) {
2065
3077
  function buildSkillCommand(opts) {
2066
3078
  return {
2067
3079
  name: "skill",
2068
- description: "Show skill details or list available skills.",
3080
+ description: "Show skill details or list available skills. Use /skill-gen to create new skills.",
2069
3081
  async run(args) {
2070
3082
  if (!opts.skillLoader) return { message: "No skill loader configured." };
2071
3083
  if (!args.trim()) {
@@ -2130,76 +3142,462 @@ function buildDirectorCommand(opts) {
2130
3142
  message: "Cannot promote to director mode: subagents have already been spawned. Promote before using /spawn, or restart with --director."
2131
3143
  };
2132
3144
  }
2133
- return { message: result };
3145
+ return { message: result };
3146
+ }
3147
+ };
3148
+ }
3149
+ function buildTodosCommand(opts) {
3150
+ return {
3151
+ name: "todos",
3152
+ description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
3153
+ async run(args) {
3154
+ const ctx = opts.context;
3155
+ if (!ctx) return { message: "No active context." };
3156
+ const [verb, ...rest] = args.trim().split(/\s+/);
3157
+ const restJoined = rest.join(" ").trim();
3158
+ switch (verb) {
3159
+ case "":
3160
+ case "show":
3161
+ case "list": {
3162
+ return { message: formatTodosList(ctx.todos) };
3163
+ }
3164
+ case "clear": {
3165
+ const n = ctx.todos.length;
3166
+ ctx.todos.length = 0;
3167
+ return {
3168
+ message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.`
3169
+ };
3170
+ }
3171
+ case "add": {
3172
+ if (!restJoined) return { message: "Usage: /todos add <text>" };
3173
+ ctx.todos.push({
3174
+ id: `todo_${Date.now()}_${randomUUID().slice(0, 7)}`,
3175
+ content: restJoined,
3176
+ status: "pending"
3177
+ });
3178
+ return { message: `Added: ${restJoined}` };
3179
+ }
3180
+ case "done":
3181
+ case "complete": {
3182
+ if (!restJoined) return { message: "Usage: /todos done <id|index>" };
3183
+ const asIndex = Number.parseInt(restJoined, 10);
3184
+ let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
3185
+ if (!target)
3186
+ target = ctx.todos.find(
3187
+ (t) => t.content.toLowerCase().includes(restJoined.toLowerCase())
3188
+ );
3189
+ if (!target) return { message: `No todo matched "${restJoined}".` };
3190
+ target.status = "completed";
3191
+ return { message: `Marked done: ${target.content}` };
3192
+ }
3193
+ default:
3194
+ return {
3195
+ message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
3196
+ };
3197
+ }
3198
+ }
3199
+ };
3200
+ }
3201
+ function buildToolsCommand(opts) {
3202
+ return {
3203
+ name: "tools",
3204
+ description: "List registered tools.",
3205
+ async run() {
3206
+ const all = opts.toolRegistry.listWithOwner();
3207
+ const lines = all.map(
3208
+ ({ tool, owner }) => ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`
3209
+ );
3210
+ const msg = `${color.bold("Tools")} (${all.length}):
3211
+ ${lines.join("\n")}
3212
+ `;
3213
+ opts.renderer.write(msg);
3214
+ return { message: msg };
3215
+ }
3216
+ };
3217
+ }
3218
+ function buildYoloCommand(opts) {
3219
+ return {
3220
+ name: "yolo",
3221
+ description: "Toggle or query YOLO (auto-approve) mode.",
3222
+ help: [
3223
+ "Usage:",
3224
+ " /yolo Show current YOLO status",
3225
+ " /yolo on Enable YOLO mode (auto-approve every tool call)",
3226
+ " /yolo off Disable YOLO mode (restore permission prompts)",
3227
+ " /yolo toggle Toggle YOLO mode",
3228
+ "",
3229
+ "YOLO mode skips all permission prompts and auto-approves every tool call.",
3230
+ "Use with caution \u2014 the agent can execute any tool without asking."
3231
+ ].join("\n"),
3232
+ async run(args) {
3233
+ const arg = args.trim().toLowerCase();
3234
+ if (!opts.onYolo) {
3235
+ const msg2 = "YOLO toggle is not available in this session.";
3236
+ opts.renderer.writeWarning(msg2);
3237
+ return { message: msg2 };
3238
+ }
3239
+ if (!arg) {
3240
+ const current = opts.onYolo();
3241
+ const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving all tool calls)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
3242
+ const msg2 = `YOLO mode: ${status}`;
3243
+ opts.renderer.write(msg2);
3244
+ return { message: msg2 };
3245
+ }
3246
+ let newState;
3247
+ if (arg === "on" || arg === "enable" || arg === "true" || arg === "1") {
3248
+ newState = true;
3249
+ } else if (arg === "off" || arg === "disable" || arg === "false" || arg === "0") {
3250
+ newState = false;
3251
+ } else if (arg === "toggle") {
3252
+ newState = !opts.onYolo();
3253
+ } else {
3254
+ const msg2 = `Unknown argument: ${arg}. Use /yolo on, /yolo off, or /yolo toggle.`;
3255
+ opts.renderer.writeWarning(msg2);
3256
+ return { message: msg2 };
3257
+ }
3258
+ opts.onYolo(newState);
3259
+ const label = newState ? `${color.yellow("ENABLED")} \u2014 all tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
3260
+ const msg = `YOLO mode: ${label}`;
3261
+ opts.renderer.write(msg);
3262
+ return { message: msg };
3263
+ }
3264
+ };
3265
+ }
3266
+ function buildAutonomyCommand(opts) {
3267
+ return {
3268
+ name: "autonomy",
3269
+ description: "Toggle or query autonomy mode (self-driving agent).",
3270
+ help: [
3271
+ "Usage:",
3272
+ " /autonomy Show current autonomy status",
3273
+ " /autonomy off Disabled \u2014 agent stops after each turn (default)",
3274
+ " /autonomy suggest Show next-step suggestions after each turn",
3275
+ " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
3276
+ " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 off",
3277
+ "",
3278
+ "Modes:",
3279
+ " off \u2014 Normal interactive mode. Agent stops and waits.",
3280
+ " suggest \u2014 After each turn, agent suggests next steps. You pick.",
3281
+ " auto \u2014 After each turn, agent picks the best next step and continues.",
3282
+ " Runs indefinitely until you press Esc or Ctrl+C.",
3283
+ "",
3284
+ "In auto mode the agent works autonomously. Press Esc to redirect,",
3285
+ "Ctrl+C to stop. The agent suggests context-aware next steps based on",
3286
+ "the conversation history."
3287
+ ].join("\n"),
3288
+ async run(args) {
3289
+ const arg = args.trim().toLowerCase();
3290
+ if (!opts.onAutonomy) {
3291
+ const msg2 = "Autonomy mode is not available in this session.";
3292
+ opts.renderer.writeWarning(msg2);
3293
+ return { message: msg2 };
3294
+ }
3295
+ if (!arg) {
3296
+ const current = opts.onAutonomy();
3297
+ const labels2 = {
3298
+ off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
3299
+ suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
3300
+ auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`
3301
+ };
3302
+ const msg2 = `Autonomy mode: ${labels2[current]}`;
3303
+ opts.renderer.write(msg2);
3304
+ return { message: msg2 };
3305
+ }
3306
+ let newMode;
3307
+ if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
3308
+ newMode = "auto";
3309
+ } else if (arg === "off" || arg === "disable" || arg === "false") {
3310
+ newMode = "off";
3311
+ } else if (arg === "suggest" || arg === "suggestions") {
3312
+ newMode = "suggest";
3313
+ } else if (arg === "toggle" || arg === "cycle") {
3314
+ const current = opts.onAutonomy() ?? "off";
3315
+ const cycle = ["off", "suggest", "auto"];
3316
+ newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
3317
+ } else {
3318
+ const msg2 = `Unknown argument: ${arg}. Use /autonomy on, /autonomy off, /autonomy suggest, or /autonomy toggle.`;
3319
+ opts.renderer.writeWarning(msg2);
3320
+ return { message: msg2 };
3321
+ }
3322
+ opts.onAutonomy(newMode);
3323
+ const labels = {
3324
+ off: `${color.green("OFF")} \u2014 agent stops after each turn`,
3325
+ suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
3326
+ auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`
3327
+ };
3328
+ const msg = `Autonomy mode: ${labels[newMode]}`;
3329
+ opts.renderer.write(msg);
3330
+ return { message: msg };
3331
+ }
3332
+ };
3333
+ }
3334
+
3335
+ // src/slash-commands/mode.ts
3336
+ function buildModeCommand(opts) {
3337
+ return {
3338
+ name: "mode",
3339
+ description: "Switch or view the current mode",
3340
+ help: [
3341
+ "Usage:",
3342
+ " /mode Show current mode and available modes",
3343
+ " /mode <id> Switch to a different mode",
3344
+ "",
3345
+ "Available modes:",
3346
+ " default General-purpose coding assistant",
3347
+ " brief Fast, no-nonsense \u2014 get to the point",
3348
+ " teach Mentor mode \u2014 explains why, not just what",
3349
+ " code-reviewer, code-auditor, architect, debugger, tester, devops, refactorer",
3350
+ "",
3351
+ "Example:",
3352
+ " /mode brief Switch to brief mode",
3353
+ " /mode teach Switch to teach mode"
3354
+ ].join("\n"),
3355
+ async run(args) {
3356
+ const modeStore = opts.modeStore;
3357
+ if (!modeStore) {
3358
+ return { message: "Mode store not available in this context." };
3359
+ }
3360
+ const modes = await modeStore.listModes();
3361
+ const active = await modeStore.getActiveMode();
3362
+ if (!args.trim()) {
3363
+ const lines = [`Current mode: ${active?.name ?? "none"}`, "", "Available modes:"];
3364
+ for (const m of modes) {
3365
+ const mark = m.id === active?.id ? " [active]" : "";
3366
+ lines.push(` ${m.id} \u2014 ${m.description}${mark}`);
3367
+ }
3368
+ return { message: lines.join("\n") };
3369
+ }
3370
+ const target = args.trim().toLowerCase();
3371
+ const targetMode = modes.find((m) => m.id === target);
3372
+ if (!targetMode) {
3373
+ const available = modes.map((m) => m.id).join(", ");
3374
+ return { message: `Unknown mode "${target}". Available: ${available}` };
3375
+ }
3376
+ await modeStore.setActiveMode(targetMode.id);
3377
+ return {
3378
+ message: `Switched to "${targetMode.name}" mode.
3379
+ ${targetMode.description}`
3380
+ };
3381
+ }
3382
+ };
3383
+ }
3384
+
3385
+ // src/slash-commands/index.ts
3386
+ init_sdd();
3387
+
3388
+ // src/slash-commands/skill-generator.ts
3389
+ function buildSkillGeneratorCommand(opts) {
3390
+ return {
3391
+ name: "skill-gen",
3392
+ description: "Create a new AI skill interactively. The AI will guide you.",
3393
+ help: [
3394
+ "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557",
3395
+ "",
3396
+ "Create new AI skills with AI guidance.",
3397
+ "",
3398
+ "Usage:",
3399
+ " /skill-gen Start skill creation",
3400
+ " /skill-gen list List existing skills",
3401
+ " /skill-gen edit <name> View an existing skill",
3402
+ "",
3403
+ "The AI will ask you questions and create the skill file.",
3404
+ "Skills are saved to .wrongstack/skills/<name>/SKILL.md"
3405
+ ].join("\n"),
3406
+ async run(args) {
3407
+ const trimmed = args.trim();
3408
+ if (trimmed === "list" || trimmed === "ls") {
3409
+ if (!opts.skillLoader) return { message: "No skill loader configured." };
3410
+ const entries = await opts.skillLoader.listEntries();
3411
+ if (entries.length === 0) return { message: "No skills found." };
3412
+ const lines = entries.map((e) => {
3413
+ const src = e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : "\u{1F4E6}";
3414
+ return ` ${src} ${e.name}
3415
+ ${e.trigger}`;
3416
+ });
3417
+ return { message: `Available Skills:
3418
+ ${lines.join("\n\n")}
3419
+ ` };
3420
+ }
3421
+ if (trimmed.startsWith("edit ")) {
3422
+ const skillName = trimmed.slice(5).trim();
3423
+ if (!opts.skillLoader) return { message: "No skill loader configured." };
3424
+ const skill = await opts.skillLoader.find(skillName);
3425
+ if (!skill) return { message: `Skill "${skillName}" not found.` };
3426
+ const body = await opts.skillLoader.readBody(skillName);
3427
+ return {
3428
+ message: [
3429
+ `Skill: ${skillName}`,
3430
+ `Path: ${skill.path}`,
3431
+ "",
3432
+ body
3433
+ ].join("\n")
3434
+ };
3435
+ }
3436
+ return {
3437
+ message: "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557\n\nThe AI will guide you through creating a new skill.\nAnswer its questions naturally.",
3438
+ runText: "I want to create a new AI skill. Read the skill-creator skill and guide me through the process. Ask me questions one at a time \u2014 name, description, what to cover \u2014 then create the SKILL.md file."
3439
+ };
3440
+ }
3441
+ };
3442
+ }
3443
+ function makeInstaller(opts, projectRoot, global) {
3444
+ const globalRoot = path18.join(os4.homedir(), ".wrongstack");
3445
+ return new SkillInstaller({
3446
+ manifestPath: path18.join(globalRoot, "installed-skills.json"),
3447
+ projectSkillsDir: path18.join(projectRoot, ".wrongstack", "skills"),
3448
+ globalSkillsDir: path18.join(globalRoot, "skills"),
3449
+ projectHash: projectHash(projectRoot),
3450
+ skillLoader: opts.skillLoader
3451
+ });
3452
+ }
3453
+ function buildSkillInstallCommand(opts) {
3454
+ return {
3455
+ name: "skill-install",
3456
+ description: "Install skills from a GitHub repository.",
3457
+ argsHint: "<user/repo[@ref]> [--global]",
3458
+ help: [
3459
+ "\u2554\u2550\u2550\u2550 Skill Install \u2550\u2550\u2550\u2557",
3460
+ "",
3461
+ "Install skills from a GitHub repository.",
3462
+ "",
3463
+ "Usage:",
3464
+ " /skill-install <user/repo> Install from default branch (main)",
3465
+ " /skill-install <user/repo@ref> Install specific tag/branch/commit",
3466
+ " /skill-install <user/repo> --global Install to user-global skills",
3467
+ "",
3468
+ "Supports both single-skill repos (SKILL.md at root)",
3469
+ "and multi-skill repos (skills/ subdirectory).",
3470
+ "",
3471
+ "Examples:",
3472
+ " /skill-install wrongstack/awesome-skills",
3473
+ " /skill-install wrongstack/skills@v1.0",
3474
+ " /skill-install user/my-skills --global"
3475
+ ].join("\n"),
3476
+ async run(args, ctx) {
3477
+ const parts = args.trim().split(/\s+/);
3478
+ const ref = parts.find((p) => !p.startsWith("--"));
3479
+ const isGlobal = parts.includes("--global");
3480
+ if (!ref) {
3481
+ return { message: "Usage: /skill-install <user/repo[@ref]> [--global]" };
3482
+ }
3483
+ const installer = makeInstaller(opts, ctx.projectRoot);
3484
+ try {
3485
+ const results = await installer.install(ref, { global: isGlobal });
3486
+ if (results.length === 0) {
3487
+ return { message: "No skills found in the repository." };
3488
+ }
3489
+ const scope = isGlobal ? "user-global" : "project";
3490
+ const lines = [`Installed ${results.length} skill(s) [${scope}]:`];
3491
+ for (const r of results) {
3492
+ lines.push(` \u2713 ${r.name} (${r.source}@${r.ref})`);
3493
+ lines.push(` \u2192 ${r.path}`);
3494
+ }
3495
+ return { message: lines.join("\n") };
3496
+ } catch (err) {
3497
+ const msg = err instanceof Error ? err.message : String(err);
3498
+ opts.renderer.writeError(`Install failed: ${msg}`);
3499
+ return { message: `\u2717 Install failed: ${msg}` };
3500
+ }
2134
3501
  }
2135
3502
  };
2136
3503
  }
2137
- function buildTodosCommand(opts) {
3504
+ function buildSkillUpdateCommand(opts) {
2138
3505
  return {
2139
- name: "todos",
2140
- description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
2141
- async run(args) {
2142
- const ctx = opts.context;
2143
- if (!ctx) return { message: "No active context." };
2144
- const [verb, ...rest] = args.trim().split(/\s+/);
2145
- const restJoined = rest.join(" ").trim();
2146
- switch (verb) {
2147
- case "":
2148
- case "show":
2149
- case "list": {
2150
- return { message: formatTodosList(ctx.todos) };
3506
+ name: "skill-update",
3507
+ description: "Update installed skills from their GitHub source.",
3508
+ argsHint: "[name|ref] [--global]",
3509
+ help: [
3510
+ "\u2554\u2550\u2550\u2550 Skill Update \u2550\u2550\u2550\u2557",
3511
+ "",
3512
+ "Update installed skills from their GitHub source.",
3513
+ "",
3514
+ "Usage:",
3515
+ " /skill-update Update all installed skills",
3516
+ " /skill-update <name> Update a specific skill",
3517
+ " /skill-update <user/repo@ref> Update to a different ref",
3518
+ " /skill-update <name> --global Update a global skill"
3519
+ ].join("\n"),
3520
+ async run(args, ctx) {
3521
+ const parts = args.trim().split(/\s+/);
3522
+ const nameOrRef = parts.find((p) => !p.startsWith("--"));
3523
+ const isGlobal = parts.includes("--global");
3524
+ const installer = makeInstaller(opts, ctx.projectRoot);
3525
+ try {
3526
+ const result = await installer.update(nameOrRef, { global: isGlobal });
3527
+ const lines = [];
3528
+ if (result.updated.length > 0) {
3529
+ lines.push(`Updated ${result.updated.length} skill(s):`);
3530
+ for (const u of result.updated) {
3531
+ if (u.oldRef !== u.newRef) {
3532
+ lines.push(` \u2713 ${u.name} (${u.oldRef} \u2192 ${u.newRef})`);
3533
+ } else {
3534
+ lines.push(` \u2713 ${u.name} (refreshed)`);
3535
+ }
3536
+ }
2151
3537
  }
2152
- case "clear": {
2153
- const n = ctx.todos.length;
2154
- ctx.todos.length = 0;
2155
- return {
2156
- message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.`
2157
- };
3538
+ if (result.unchanged.length > 0) {
3539
+ lines.push(`Up to date: ${result.unchanged.join(", ")}`);
2158
3540
  }
2159
- case "add": {
2160
- if (!restJoined) return { message: "Usage: /todos add <text>" };
2161
- ctx.todos.push({
2162
- id: `todo_${Date.now()}_${randomUUID().slice(0, 7)}`,
2163
- content: restJoined,
2164
- status: "pending"
2165
- });
2166
- return { message: `Added: ${restJoined}` };
3541
+ if (result.errors.length > 0) {
3542
+ for (const e of result.errors) {
3543
+ lines.push(` \u2717 ${e.name}: ${e.error}`);
3544
+ }
2167
3545
  }
2168
- case "done":
2169
- case "complete": {
2170
- if (!restJoined) return { message: "Usage: /todos done <id|index>" };
2171
- const asIndex = Number.parseInt(restJoined, 10);
2172
- let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
2173
- if (!target)
2174
- target = ctx.todos.find(
2175
- (t) => t.content.toLowerCase().includes(restJoined.toLowerCase())
2176
- );
2177
- if (!target) return { message: `No todo matched "${restJoined}".` };
2178
- target.status = "completed";
2179
- return { message: `Marked done: ${target.content}` };
3546
+ if (lines.length === 0) {
3547
+ return { message: "No installed skills to update." };
2180
3548
  }
2181
- default:
2182
- return {
2183
- message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
2184
- };
3549
+ return { message: lines.join("\n") };
3550
+ } catch (err) {
3551
+ const msg = err instanceof Error ? err.message : String(err);
3552
+ return { message: `\u2717 Update failed: ${msg}` };
2185
3553
  }
2186
3554
  }
2187
3555
  };
2188
3556
  }
2189
- function buildToolsCommand(opts) {
3557
+ function buildSkillUninstallCommand(opts) {
2190
3558
  return {
2191
- name: "tools",
2192
- description: "List registered tools.",
2193
- async run() {
2194
- const all = opts.toolRegistry.listWithOwner();
2195
- const lines = all.map(
2196
- ({ tool, owner }) => ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`
2197
- );
2198
- const msg = `${color.bold("Tools")} (${all.length}):
2199
- ${lines.join("\n")}
2200
- `;
2201
- opts.renderer.write(msg);
2202
- return { message: msg };
3559
+ name: "skill-uninstall",
3560
+ description: "Remove an installed skill.",
3561
+ argsHint: "<name> [--global]",
3562
+ help: [
3563
+ "\u2554\u2550\u2550\u2550 Skill Uninstall \u2550\u2550\u2550\u2557",
3564
+ "",
3565
+ "Remove an installed skill and its files.",
3566
+ "",
3567
+ "Usage:",
3568
+ " /skill-uninstall <name> Remove from project skills",
3569
+ " /skill-uninstall <name> --global Remove from user-global skills"
3570
+ ].join("\n"),
3571
+ async run(args, ctx) {
3572
+ const parts = args.trim().split(/\s+/);
3573
+ const name = parts.find((p) => !p.startsWith("--"));
3574
+ const isGlobal = parts.includes("--global");
3575
+ if (!name) {
3576
+ const installer2 = makeInstaller(opts, ctx.projectRoot);
3577
+ const installed = await installer2.listInstalled();
3578
+ if (installed.length === 0) {
3579
+ return { message: "No installed skills found." };
3580
+ }
3581
+ const scope = isGlobal ? "user" : "project";
3582
+ const filtered = installed.filter((s) => s.scope === scope);
3583
+ if (filtered.length === 0) {
3584
+ return { message: `No installed skills found (${scope} scope).` };
3585
+ }
3586
+ const lines = [`Installed skills (${scope}):`];
3587
+ for (const s of filtered) {
3588
+ lines.push(` ${s.name} ${s.source}@${s.ref} (${s.installedAt.slice(0, 10)})`);
3589
+ }
3590
+ lines.push("", "Use /skill-uninstall <name> to remove.");
3591
+ return { message: lines.join("\n") };
3592
+ }
3593
+ const installer = makeInstaller(opts, ctx.projectRoot);
3594
+ try {
3595
+ await installer.uninstall(name, { global: isGlobal });
3596
+ return { message: `\u2713 Skill "${name}" uninstalled.` };
3597
+ } catch (err) {
3598
+ const msg = err instanceof Error ? err.message : String(err);
3599
+ return { message: `\u2717 Uninstall failed: ${msg}` };
3600
+ }
2203
3601
  }
2204
3602
  };
2205
3603
  }
@@ -2214,6 +3612,10 @@ function buildBuiltinSlashCommands(opts) {
2214
3612
  buildContextCommand(opts),
2215
3613
  buildToolsCommand(opts),
2216
3614
  buildSkillCommand(opts),
3615
+ buildSkillGeneratorCommand(opts),
3616
+ buildSkillInstallCommand(opts),
3617
+ buildSkillUpdateCommand(opts),
3618
+ buildSkillUninstallCommand(opts),
2217
3619
  buildPluginCommand(opts),
2218
3620
  buildDiagCommand(opts),
2219
3621
  buildStatsCommand(opts),
@@ -2226,9 +3628,16 @@ function buildBuiltinSlashCommands(opts) {
2226
3628
  buildMemoryCommand(opts),
2227
3629
  buildTodosCommand(opts),
2228
3630
  buildPlanCommand(opts),
3631
+ buildSddCommand(opts),
2229
3632
  buildSaveCommand(opts),
2230
3633
  buildLoadCommand(opts),
2231
- buildExitCommand(opts)
3634
+ buildYoloCommand(opts),
3635
+ buildAutonomyCommand(opts),
3636
+ buildModeCommand(opts),
3637
+ buildExitCommand(opts),
3638
+ buildCommitCommand(),
3639
+ buildGitcheckCommand(),
3640
+ buildPushCommand()
2232
3641
  ];
2233
3642
  }
2234
3643
 
@@ -2247,13 +3656,13 @@ var MANIFESTS = [
2247
3656
  ];
2248
3657
  async function detectProjectKind(projectRoot) {
2249
3658
  try {
2250
- await fs14.access(path15.join(projectRoot, ".wrongstack", "AGENTS.md"));
3659
+ await fs5.access(path18.join(projectRoot, ".wrongstack", "AGENTS.md"));
2251
3660
  return "initialized";
2252
3661
  } catch {
2253
3662
  }
2254
3663
  for (const m of MANIFESTS) {
2255
3664
  try {
2256
- await fs14.access(path15.join(projectRoot, m));
3665
+ await fs5.access(path18.join(projectRoot, m));
2257
3666
  return "project";
2258
3667
  } catch {
2259
3668
  }
@@ -2261,12 +3670,12 @@ async function detectProjectKind(projectRoot) {
2261
3670
  return "empty";
2262
3671
  }
2263
3672
  async function scaffoldAgentsMd(projectRoot) {
2264
- const dir = path15.join(projectRoot, ".wrongstack");
2265
- const file = path15.join(dir, "AGENTS.md");
3673
+ const dir = path18.join(projectRoot, ".wrongstack");
3674
+ const file = path18.join(dir, "AGENTS.md");
2266
3675
  const facts = await detectProjectFacts(projectRoot);
2267
3676
  const body = renderAgentsTemplate(facts);
2268
- await fs14.mkdir(dir, { recursive: true });
2269
- await fs14.writeFile(file, body, "utf8");
3677
+ await fs5.mkdir(dir, { recursive: true });
3678
+ await fs5.writeFile(file, body, "utf8");
2270
3679
  return file;
2271
3680
  }
2272
3681
  async function runProjectCheck(opts) {
@@ -2275,7 +3684,7 @@ async function runProjectCheck(opts) {
2275
3684
  if (kind === "initialized") {
2276
3685
  renderer.write(
2277
3686
  `
2278
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path15.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
3687
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path18.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
2279
3688
  `
2280
3689
  );
2281
3690
  return true;
@@ -2302,11 +3711,43 @@ async function runProjectCheck(opts) {
2302
3711
  }
2303
3712
  return true;
2304
3713
  }
2305
- renderer.write(
2306
- `
3714
+ const gitDir = path18.join(projectRoot, ".git");
3715
+ let hasGit = false;
3716
+ try {
3717
+ await fs5.access(gitDir);
3718
+ hasGit = true;
3719
+ } catch {
3720
+ }
3721
+ if (!hasGit) {
3722
+ renderer.write(
3723
+ `
2307
3724
  ${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
2308
3725
  `
2309
- );
3726
+ );
3727
+ const answer2 = (await reader.readLine(
3728
+ ` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
3729
+ )).trim().toLowerCase();
3730
+ if (answer2 === "y" || answer2 === "yes") {
3731
+ try {
3732
+ const { spawn: spawn2 } = await import('child_process');
3733
+ await new Promise((resolve3, reject) => {
3734
+ const child = spawn2("git", ["init"], { cwd: projectRoot });
3735
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`)));
3736
+ });
3737
+ renderer.write(` ${color.green("\u2713")} Git repository initialized
3738
+ `);
3739
+ } catch (err) {
3740
+ renderer.writeError(`git init failed: ${err instanceof Error ? err.message : String(err)}
3741
+ `);
3742
+ }
3743
+ }
3744
+ } else {
3745
+ renderer.write(
3746
+ `
3747
+ ${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
3748
+ `
3749
+ );
3750
+ }
2310
3751
  const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
2311
3752
  if (answer === "n" || answer === "no") {
2312
3753
  renderer.write(color.dim(" Cancelled.\n"));
@@ -2331,9 +3772,9 @@ async function runLaunchPrompts(opts) {
2331
3772
  yolo = yoloPinned;
2332
3773
  } else {
2333
3774
  const answer = (await reader.readLine(
2334
- ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[y/N]")} `
3775
+ ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n]")} `
2335
3776
  )).trim().toLowerCase();
2336
- yolo = answer === "y" || answer === "yes";
3777
+ yolo = answer !== "n" && answer !== "no";
2337
3778
  }
2338
3779
  renderer.write(
2339
3780
  `
@@ -2571,14 +4012,14 @@ function summarize(value, name) {
2571
4012
  if (typeof v === "object" && v !== null) {
2572
4013
  const o = v;
2573
4014
  if (name === "edit") {
2574
- const path16 = typeof o["path"] === "string" ? o["path"] : "";
4015
+ const path19 = typeof o["path"] === "string" ? o["path"] : "";
2575
4016
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
2576
- return `${path16} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
4017
+ return `${path19} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
2577
4018
  }
2578
4019
  if (name === "write") {
2579
- const path16 = typeof o["path"] === "string" ? o["path"] : "";
4020
+ const path19 = typeof o["path"] === "string" ? o["path"] : "";
2580
4021
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
2581
- return bytes !== void 0 ? `${path16} ${bytes}B` : path16;
4022
+ return bytes !== void 0 ? `${path19} ${bytes}B` : path19;
2582
4023
  }
2583
4024
  if (typeof o["count"] === "number") {
2584
4025
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -3174,7 +4615,7 @@ async function readKeyInput(deps, intent) {
3174
4615
  async function loadProviders(deps) {
3175
4616
  let raw;
3176
4617
  try {
3177
- raw = await fs14.readFile(deps.globalConfigPath, "utf8");
4618
+ raw = await fs5.readFile(deps.globalConfigPath, "utf8");
3178
4619
  } catch {
3179
4620
  return {};
3180
4621
  }
@@ -3190,7 +4631,7 @@ async function loadProviders(deps) {
3190
4631
  async function mutateProviders(deps, mutator) {
3191
4632
  let raw;
3192
4633
  try {
3193
- raw = await fs14.readFile(deps.globalConfigPath, "utf8");
4634
+ raw = await fs5.readFile(deps.globalConfigPath, "utf8");
3194
4635
  } catch {
3195
4636
  raw = "{}";
3196
4637
  }
@@ -3262,7 +4703,7 @@ var diagCmd = async (_args, deps) => {
3262
4703
  ` modelsCache: ${deps.paths.modelsCache}`,
3263
4704
  ` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
3264
4705
  ` node: ${process.version}`,
3265
- ` os: ${os3.platform()} ${os3.release()}`,
4706
+ ` os: ${os4.platform()} ${os4.release()}`,
3266
4707
  ` provider: ${cfg.provider ?? "<unset>"}`,
3267
4708
  ` model: ${cfg.model ?? "<unset>"}`,
3268
4709
  ` tools: ${deps.toolRegistry?.list().length ?? 0}`,
@@ -3330,7 +4771,7 @@ var doctorCmd = async (_args, deps) => {
3330
4771
  });
3331
4772
  }
3332
4773
  try {
3333
- await fs14.access(deps.paths.secretsKey);
4774
+ await fs5.access(deps.paths.secretsKey);
3334
4775
  checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
3335
4776
  } catch {
3336
4777
  checks.push({
@@ -3340,10 +4781,10 @@ var doctorCmd = async (_args, deps) => {
3340
4781
  });
3341
4782
  }
3342
4783
  try {
3343
- await fs14.mkdir(deps.paths.projectSessions, { recursive: true });
3344
- const probe = path15.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
3345
- await fs14.writeFile(probe, "");
3346
- await fs14.unlink(probe);
4784
+ await fs5.mkdir(deps.paths.projectSessions, { recursive: true });
4785
+ const probe = path18.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
4786
+ await fs5.writeFile(probe, "");
4787
+ await fs5.unlink(probe);
3347
4788
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
3348
4789
  } catch (err) {
3349
4790
  checks.push({
@@ -3444,8 +4885,8 @@ var exportCmd = async (args, deps) => {
3444
4885
  return 1;
3445
4886
  }
3446
4887
  if (output) {
3447
- await fs14.mkdir(path15.dirname(path15.resolve(deps.cwd, output)), { recursive: true });
3448
- await fs14.writeFile(path15.resolve(deps.cwd, output), rendered, "utf8");
4888
+ await fs5.mkdir(path18.dirname(path18.resolve(deps.cwd, output)), { recursive: true });
4889
+ await fs5.writeFile(path18.resolve(deps.cwd, output), rendered, "utf8");
3449
4890
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
3450
4891
  `);
3451
4892
  } else {
@@ -3502,17 +4943,17 @@ var initCmd = async (_args, deps) => {
3502
4943
  } else {
3503
4944
  deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
3504
4945
  }
3505
- await fs14.mkdir(deps.paths.globalRoot, { recursive: true });
4946
+ await fs5.mkdir(deps.paths.globalRoot, { recursive: true });
3506
4947
  const config = { version: 1, provider: providerId, model: modelId };
3507
4948
  if (apiKey) config.apiKey = apiKey;
3508
- const keyFile = path15.join(path15.dirname(deps.paths.globalConfig), ".key");
4949
+ const keyFile = path18.join(path18.dirname(deps.paths.globalConfig), ".key");
3509
4950
  const vault = new DefaultSecretVault$1({ keyFile });
3510
4951
  const encrypted = encryptConfigSecrets(config, vault);
3511
4952
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
3512
- await fs14.mkdir(path15.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3513
- const agentsFile = path15.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
4953
+ await fs5.mkdir(path18.join(deps.projectRoot, ".wrongstack"), { recursive: true });
4954
+ const agentsFile = path18.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
3514
4955
  try {
3515
- await fs14.access(agentsFile);
4956
+ await fs5.access(agentsFile);
3516
4957
  } catch {
3517
4958
  const detected2 = await detectProjectFacts(deps.projectRoot);
3518
4959
  await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
@@ -3588,7 +5029,7 @@ async function addMcpServer(args, deps) {
3588
5029
  serverCfg.enabled = enable;
3589
5030
  let existing = {};
3590
5031
  try {
3591
- existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
5032
+ existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
3592
5033
  } catch {
3593
5034
  }
3594
5035
  const mcpServers = existing.mcpServers ?? {};
@@ -3608,7 +5049,7 @@ async function addMcpServer(args, deps) {
3608
5049
  async function removeMcpServer(name, deps) {
3609
5050
  let existing = {};
3610
5051
  try {
3611
- existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
5052
+ existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
3612
5053
  } catch {
3613
5054
  deps.renderer.writeError("No config file found.\n");
3614
5055
  return 1;
@@ -3729,7 +5170,7 @@ function renderConfiguredPlugins(config) {
3729
5170
  }
3730
5171
  async function readConfig(file) {
3731
5172
  try {
3732
- return JSON.parse(await fs14.readFile(file, "utf8"));
5173
+ return JSON.parse(await fs5.readFile(file, "utf8"));
3733
5174
  } catch {
3734
5175
  return {};
3735
5176
  }
@@ -3820,9 +5261,9 @@ var usageCmd = async (_args, deps) => {
3820
5261
  return 0;
3821
5262
  };
3822
5263
  var projectsCmd = async (_args, deps) => {
3823
- const projectsRoot = path15.join(deps.paths.globalRoot, "projects");
5264
+ const projectsRoot = path18.join(deps.paths.globalRoot, "projects");
3824
5265
  try {
3825
- const entries = await fs14.readdir(projectsRoot);
5266
+ const entries = await fs5.readdir(projectsRoot);
3826
5267
  if (entries.length === 0) {
3827
5268
  deps.renderer.write("No projects tracked.\n");
3828
5269
  return 0;
@@ -3830,7 +5271,7 @@ var projectsCmd = async (_args, deps) => {
3830
5271
  for (const hash of entries) {
3831
5272
  try {
3832
5273
  const meta = JSON.parse(
3833
- await fs14.readFile(path15.join(projectsRoot, hash, "meta.json"), "utf8")
5274
+ await fs5.readFile(path18.join(projectsRoot, hash, "meta.json"), "utf8")
3834
5275
  );
3835
5276
  deps.renderer.write(
3836
5277
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -4007,6 +5448,131 @@ var configCmd = async (args, deps) => {
4007
5448
  deps.renderer.writeError(`Unknown config subcommand: ${sub}`);
4008
5449
  return 1;
4009
5450
  };
5451
+ function parseRewindFlags(args) {
5452
+ const flags = {};
5453
+ for (let i = 0; i < args.length; i++) {
5454
+ const a = args[i];
5455
+ if (a === "--all") flags.all = true;
5456
+ else if (a === "--last") flags.last = args[++i] ?? "1";
5457
+ else if (a === "--to") flags.to = args[++i] ?? "";
5458
+ else if (a === "--list") flags.list = true;
5459
+ else if (a === "--resume") flags.resume = true;
5460
+ }
5461
+ return flags;
5462
+ }
5463
+ var rewindCmd = async (args, deps) => {
5464
+ const flags = parseRewindFlags(args);
5465
+ const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
5466
+ const sessionsDir = path18.join(wpaths.globalRoot, "sessions");
5467
+ const rewind = new DefaultSessionRewinder(sessionsDir);
5468
+ let sessionId = args.find((a) => !a.startsWith("--"));
5469
+ if (!sessionId) {
5470
+ if (!deps.sessionStore) {
5471
+ deps.renderer.writeError("No session store available.");
5472
+ return 1;
5473
+ }
5474
+ const sessions = await deps.sessionStore.list(1);
5475
+ if (sessions.length === 0) {
5476
+ deps.renderer.writeError("No sessions found.");
5477
+ return 1;
5478
+ }
5479
+ sessionId = sessions[0].id;
5480
+ }
5481
+ if (flags.list) {
5482
+ deps.renderer.write(`Session: ${color.bold(sessionId)}
5483
+
5484
+ `);
5485
+ const checkpoints = await rewind.listCheckpoints(sessionId);
5486
+ if (checkpoints.length === 0) {
5487
+ deps.renderer.write("No checkpoints in this session.\n");
5488
+ return 0;
5489
+ }
5490
+ for (const cp of checkpoints) {
5491
+ deps.renderer.write(
5492
+ ` [${cp.promptIndex}] ${color.dim(cp.ts)} ${cp.promptPreview}${cp.fileCount > 0 ? color.dim(` (${cp.fileCount} file${cp.fileCount === 1 ? "" : "s"})`) : ""}
5493
+ `
5494
+ );
5495
+ }
5496
+ return 0;
5497
+ }
5498
+ try {
5499
+ let result;
5500
+ if (flags.all) {
5501
+ deps.renderer.write("Rewinding to session start...\n");
5502
+ result = await rewind.rewindToStart(sessionId);
5503
+ } else if (flags.last) {
5504
+ const n = parseInt(flags.last, 10);
5505
+ if (isNaN(n) || n < 1) {
5506
+ deps.renderer.writeError("--last requires a positive number");
5507
+ return 1;
5508
+ }
5509
+ deps.renderer.write(`Rewinding last ${n} prompt(s)...
5510
+ `);
5511
+ result = await rewind.rewindLastN(sessionId, n);
5512
+ } else if (flags.to) {
5513
+ const idx = parseInt(flags.to, 10);
5514
+ if (isNaN(idx) || idx < 0) {
5515
+ deps.renderer.writeError("--to requires a non-negative number");
5516
+ return 1;
5517
+ }
5518
+ deps.renderer.write(`Rewinding to checkpoint ${idx}...
5519
+ `);
5520
+ result = await rewind.rewindToCheckpoint(sessionId, idx);
5521
+ } else {
5522
+ deps.renderer.write("Usage: ws rewind --all | --last N | --to <index> [--list] [--resume]\n");
5523
+ deps.renderer.write(" --all Rewind to session start\n");
5524
+ deps.renderer.write(" --last N Rewind last N prompts\n");
5525
+ deps.renderer.write(" --to N Rewind to checkpoint N\n");
5526
+ deps.renderer.write(" --list List checkpoints\n");
5527
+ deps.renderer.write(" --resume After rewind, truncate session history at checkpoint\n");
5528
+ return 1;
5529
+ }
5530
+ if (result.revertedFiles.length === 0) {
5531
+ deps.renderer.write("No files to revert.\n");
5532
+ if (flags.resume) {
5533
+ const store = new DefaultSessionStore({ dir: sessionsDir });
5534
+ const resumed = await store.resume(sessionId);
5535
+ const toIdx = result.toPromptIndex;
5536
+ await resumed.writer.truncateToCheckpoint(toIdx);
5537
+ await resumed.writer.close();
5538
+ deps.renderer.write(` ${color.green("\u2713")} Session truncated at checkpoint ${toIdx}
5539
+ `);
5540
+ }
5541
+ return 0;
5542
+ }
5543
+ deps.renderer.write(`
5544
+ Reverted ${result.revertedFiles.length} file(s):
5545
+ `);
5546
+ for (const f of result.revertedFiles) {
5547
+ deps.renderer.write(` ${color.green("\u2713")} ${f}
5548
+ `);
5549
+ }
5550
+ if (flags.resume) {
5551
+ const store = new DefaultSessionStore({ dir: sessionsDir });
5552
+ const resumed = await store.resume(sessionId);
5553
+ const toIdx = result.toPromptIndex;
5554
+ const removed = await resumed.writer.truncateToCheckpoint(toIdx);
5555
+ await resumed.writer.close();
5556
+ deps.renderer.write(`
5557
+ ${color.green("\u2713")} Session truncated \u2014 ${removed} event(s) removed
5558
+ `);
5559
+ }
5560
+ if (result.errors.length > 0) {
5561
+ deps.renderer.write(`
5562
+ ${result.errors.length} error(s):
5563
+ `);
5564
+ for (const e of result.errors) {
5565
+ deps.renderer.write(` ${color.red("\u2717")} ${e}
5566
+ `);
5567
+ }
5568
+ return 1;
5569
+ }
5570
+ return 0;
5571
+ } catch (err) {
5572
+ deps.renderer.writeError(err instanceof Error ? err.message : String(err));
5573
+ return 1;
5574
+ }
5575
+ };
4010
5576
  var toolsCmd = async (_args, deps) => {
4011
5577
  const reg = deps.toolRegistry;
4012
5578
  if (!reg) return 0;
@@ -4029,7 +5595,7 @@ var skillsCmd = async (_args, deps) => {
4029
5595
  };
4030
5596
  var versionCmd = async (_args, deps) => {
4031
5597
  deps.renderer.write(
4032
- `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os3.platform()})
5598
+ `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os4.platform()})
4033
5599
  `
4034
5600
  );
4035
5601
  return 0;
@@ -4069,6 +5635,7 @@ var subcommands = {
4069
5635
  auth: authCmd,
4070
5636
  sessions: sessionsCmd,
4071
5637
  config: configCmd,
5638
+ rewind: rewindCmd,
4072
5639
  tools: toolsCmd,
4073
5640
  skills: skillsCmd,
4074
5641
  providers: providersCmd,
@@ -4105,31 +5672,29 @@ function fmtDuration(ms) {
4105
5672
  const remMin = m - h * 60;
4106
5673
  return `${h}h${remMin}m`;
4107
5674
  }
4108
- function fmtTaskResultLine(r, color28) {
5675
+ function fmtTaskResultLine(r, color32) {
4109
5676
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
4110
5677
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
4111
5678
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
4112
5679
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
4113
- const errKindChip = errKind ? color28.dim(` [${errKind}]`) : "";
4114
- const errSnip = errMsg || errKind ? `${errKindChip}${color28.dim(errTail)}` : "";
5680
+ const errKindChip = errKind ? color32.dim(` [${errKind}]`) : "";
5681
+ const errSnip = errMsg || errKind ? `${errKindChip}${color32.dim(errTail)}` : "";
4115
5682
  switch (r.status) {
4116
5683
  case "success":
4117
- return { mark: color28.green("\u2713"), stats, tail: "" };
5684
+ return { mark: color32.green("\u2713"), stats, tail: "" };
4118
5685
  case "timeout":
4119
- return { mark: color28.yellow("\u23F1"), stats: `${color28.yellow("timeout")} ${stats}`, tail: errSnip };
5686
+ return { mark: color32.yellow("\u23F1"), stats: `${color32.yellow("timeout")} ${stats}`, tail: errSnip };
4120
5687
  case "stopped":
4121
- return { mark: color28.dim("\u2298"), stats: `${color28.dim("stopped")} ${stats}`, tail: errSnip };
5688
+ return { mark: color32.dim("\u2298"), stats: `${color32.dim("stopped")} ${stats}`, tail: errSnip };
4122
5689
  case "failed":
4123
- return { mark: color28.red("\u2717"), stats: `${color28.red("failed")} ${stats}`, tail: errSnip };
5690
+ return { mark: color32.red("\u2717"), stats: `${color32.red("failed")} ${stats}`, tail: errSnip };
4124
5691
  }
4125
5692
  }
4126
-
4127
- // src/boot.ts
4128
5693
  function resolveBundledSkillsDir() {
4129
5694
  try {
4130
5695
  const req2 = createRequire(import.meta.url);
4131
5696
  const corePkg = req2.resolve("@wrongstack/core/package.json");
4132
- return path15.join(path15.dirname(corePkg), "skills");
5697
+ return path18.join(path18.dirname(corePkg), "skills");
4133
5698
  } catch {
4134
5699
  return void 0;
4135
5700
  }
@@ -4160,11 +5725,15 @@ async function boot(argv) {
4160
5725
  });
4161
5726
  const first = positional[0];
4162
5727
  if (first && subcommands[first]) {
4163
- const sessionStore = new DefaultSessionStore({ dir: wpaths.projectSessions });
4164
- const skillLoader = new DefaultSkillLoader({
4165
- paths: wpaths,
4166
- bundledDir: resolveBundledSkillsDir()
5728
+ const container = createDefaultContainer({
5729
+ config,
5730
+ wpaths,
5731
+ logger,
5732
+ modelsRegistry,
5733
+ bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir() : void 0
4167
5734
  });
5735
+ const sessionStore = container.resolve(TOKENS.SessionStore);
5736
+ const skillLoader = container.resolve(TOKENS.SkillLoader);
4168
5737
  const toolRegistryForSubcmd = new ToolRegistry();
4169
5738
  toolRegistryForSubcmd.registerAllOrThrow(
4170
5739
  [...builtinToolsPack.tools ?? []],
@@ -4265,6 +5834,9 @@ async function boot(argv) {
4265
5834
  logger
4266
5835
  };
4267
5836
  }
5837
+
5838
+ // src/repl.ts
5839
+ init_sdd();
4268
5840
  async function runRepl(opts) {
4269
5841
  if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
4270
5842
  let activeCtrl;
@@ -4308,6 +5880,58 @@ async function runRepl(opts) {
4308
5880
  if (res?.message) opts.renderer.write(`${res.message}
4309
5881
  `);
4310
5882
  if (res?.exit) break;
5883
+ if (res?.runText) {
5884
+ const runBlocks = [{ type: "text", text: res.runText }];
5885
+ const runCtrl2 = new AbortController();
5886
+ activeCtrl = runCtrl2;
5887
+ try {
5888
+ const runResult = await opts.agent.run(runBlocks, { signal: runCtrl2.signal });
5889
+ if (runResult.status === "done" && runResult.finalText) {
5890
+ const specSaved = await trySaveSpecFromAIOutput(runResult.finalText);
5891
+ if (specSaved) {
5892
+ opts.renderer.write(
5893
+ `
5894
+ ${color.cyan(" \u2713 Spec detected and saved! Use /sdd approve to continue.")}
5895
+ `
5896
+ );
5897
+ }
5898
+ const planSaved = trySaveImplementationPlan(runResult.finalText);
5899
+ if (planSaved) {
5900
+ opts.renderer.write(
5901
+ `
5902
+ ${color.cyan(" \u2713 Implementation plan saved!")}
5903
+ `
5904
+ );
5905
+ }
5906
+ const tasksSaved = await trySaveTasksFromAIOutput(runResult.finalText);
5907
+ if (tasksSaved) {
5908
+ const progress = getTaskProgress();
5909
+ const count = progress?.total ?? 0;
5910
+ opts.renderer.write(
5911
+ `
5912
+ ${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`)}
5913
+ `
5914
+ );
5915
+ }
5916
+ const sddPhase2 = getActiveSDDPhase();
5917
+ if (sddPhase2 === "executing") {
5918
+ const autoCompleted = autoDetectTaskCompletion(runResult.finalText);
5919
+ if (autoCompleted > 0) {
5920
+ const progress = getTaskProgress();
5921
+ if (progress) {
5922
+ opts.renderer.write(
5923
+ `
5924
+ ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
5925
+ `
5926
+ );
5927
+ }
5928
+ }
5929
+ }
5930
+ }
5931
+ } catch (runErr) {
5932
+ opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
5933
+ }
5934
+ }
4311
5935
  } catch (err) {
4312
5936
  opts.renderer.writeError(err instanceof Error ? err.message : String(err));
4313
5937
  }
@@ -4320,20 +5944,47 @@ async function runRepl(opts) {
4320
5944
  `));
4321
5945
  }
4322
5946
  const blocks = await builder.submit();
5947
+ const sddContext = getActiveSDDContext();
5948
+ const taskList = getTaskListText();
5949
+ const taskProgress = getTaskProgress();
5950
+ const sddPhase = getActiveSDDPhase();
5951
+ let sddPrefix = "";
5952
+ if (sddContext) {
5953
+ sddPrefix = `[SDD SESSION ACTIVE]
5954
+ ${sddContext}`;
5955
+ if (taskList) {
5956
+ sddPrefix += `
5957
+
5958
+ **Current Task List:**
5959
+ ${taskList}`;
5960
+ }
5961
+ if (taskProgress && taskProgress.total > 0) {
5962
+ sddPrefix += `
5963
+ **Progress:** ${taskProgress.completed}/${taskProgress.total} (${taskProgress.percent}%)`;
5964
+ }
5965
+ if (sddPhase === "executing" && taskProgress && taskProgress.percent === 100) {
5966
+ sddPrefix += "\n\n**All tasks completed! Provide a summary of everything implemented.**";
5967
+ }
5968
+ sddPrefix += "\n\n---\nUser message:\n";
5969
+ }
5970
+ const effectiveBlocks = sddPrefix ? [
5971
+ { type: "text", text: sddPrefix },
5972
+ ...blocks
5973
+ ] : blocks;
4323
5974
  const runCtrl = new AbortController();
4324
5975
  activeCtrl = runCtrl;
4325
5976
  try {
4326
5977
  const startedAt = Date.now();
4327
5978
  const before = opts.tokenCounter?.total();
4328
5979
  const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
4329
- const routed = blocks.some((block) => block.type === "image") ? await routeImagesForModel(blocks, {
5980
+ const routed = effectiveBlocks.some((block) => block.type === "image") ? await routeImagesForModel(effectiveBlocks, {
4330
5981
  supportsVision: opts.supportsVision ? await opts.supportsVision() : opts.agent.ctx.provider.capabilities.vision,
4331
5982
  adapters: opts.visionAdapters ?? [],
4332
5983
  ctx: opts.agent.ctx,
4333
5984
  signal: runCtrl.signal,
4334
5985
  providerId: opts.agent.ctx.provider.id,
4335
5986
  model: opts.agent.ctx.model
4336
- }) : { blocks, route: "none", convertedImages: 0 };
5987
+ }) : { blocks: effectiveBlocks, route: "none", convertedImages: 0 };
4337
5988
  if (routed.route === "adapter") {
4338
5989
  opts.renderer.write(
4339
5990
  color.dim(
@@ -4356,6 +6007,55 @@ async function runRepl(opts) {
4356
6007
  } else if (result.status === "max_iterations") {
4357
6008
  opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
4358
6009
  }
6010
+ if (result.status === "done" && result.finalText && sddContext) {
6011
+ const specSaved = await trySaveSpecFromAIOutput(result.finalText);
6012
+ if (specSaved) {
6013
+ opts.renderer.write(
6014
+ `
6015
+ ${color.cyan(" \u2713 Spec detected and saved! Use /sdd approve to continue.")}
6016
+ `
6017
+ );
6018
+ }
6019
+ const planSaved = trySaveImplementationPlan(result.finalText);
6020
+ if (planSaved) {
6021
+ opts.renderer.write(
6022
+ `
6023
+ ${color.cyan(" \u2713 Implementation plan saved!")}
6024
+ `
6025
+ );
6026
+ }
6027
+ const tasksSaved = await trySaveTasksFromAIOutput(result.finalText);
6028
+ if (tasksSaved) {
6029
+ const progress = getTaskProgress();
6030
+ const count = progress?.total ?? 0;
6031
+ opts.renderer.write(
6032
+ `
6033
+ ${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`)}
6034
+ `
6035
+ );
6036
+ }
6037
+ const phase = getActiveSDDPhase();
6038
+ if (phase === "executing") {
6039
+ const autoCompleted = autoDetectTaskCompletion(result.finalText);
6040
+ if (autoCompleted > 0) {
6041
+ const progress = getTaskProgress();
6042
+ if (progress) {
6043
+ opts.renderer.write(
6044
+ `
6045
+ ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
6046
+ `
6047
+ );
6048
+ if (progress.percent === 100) {
6049
+ opts.renderer.write(
6050
+ `
6051
+ ${color.green(" \u{1F389} All tasks completed! Use /sdd cancel to end the session.")}
6052
+ `
6053
+ );
6054
+ }
6055
+ }
6056
+ }
6057
+ }
6058
+ }
4359
6059
  if (opts.tokenCounter && before) {
4360
6060
  const after = opts.tokenCounter.total();
4361
6061
  const costAfter = opts.tokenCounter.estimateCost().total;
@@ -4368,6 +6068,49 @@ ${color.dim(
4368
6068
  `
4369
6069
  );
4370
6070
  }
6071
+ if (result.status === "done" && opts.getAutonomy) {
6072
+ const autonomy = opts.getAutonomy();
6073
+ if (autonomy === "auto") {
6074
+ const nextPrompt = 'Based on what you just did, what is the single most important next step? Just do it \u2014 execute the next logical step without asking for confirmation. If there is nothing meaningful left to do, say "DONE" and nothing else.';
6075
+ opts.renderer.write(color.dim("\n \u21B3 [autonomy] continuing\u2026\n"));
6076
+ const nextBlocks = [{ type: "text", text: nextPrompt }];
6077
+ const nextCtrl = new AbortController();
6078
+ activeCtrl = nextCtrl;
6079
+ try {
6080
+ const nextResult = await opts.agent.run(nextBlocks, { signal: nextCtrl.signal });
6081
+ if (nextResult.status === "done" && nextResult.finalText?.trim() === "DONE") {
6082
+ opts.renderer.write(color.dim("\n \u21B3 [autonomy] agent reports task complete.\n"));
6083
+ }
6084
+ if (opts.getAutonomy() === "auto" && nextResult.status === "done") {
6085
+ }
6086
+ } catch (err) {
6087
+ opts.renderer.writeError(
6088
+ `[autonomy] ${err instanceof Error ? err.message : String(err)}`
6089
+ );
6090
+ } finally {
6091
+ activeCtrl = void 0;
6092
+ }
6093
+ } else if (autonomy === "suggest") {
6094
+ const suggestPrompt = 'Based on what you just did, suggest 3 concrete next steps. Format: numbered list, one line each, no explanation. If there is nothing meaningful left, say "No further steps needed."';
6095
+ const suggestBlocks = [{ type: "text", text: suggestPrompt }];
6096
+ const suggestCtrl = new AbortController();
6097
+ activeCtrl = suggestCtrl;
6098
+ try {
6099
+ const suggestResult = await opts.agent.run(suggestBlocks, { signal: suggestCtrl.signal });
6100
+ if (suggestResult.status === "done" && suggestResult.finalText) {
6101
+ opts.renderer.write(
6102
+ `
6103
+ ${color.cyan(" Suggested next steps:")}
6104
+ ${suggestResult.finalText}
6105
+ `
6106
+ );
6107
+ }
6108
+ } catch {
6109
+ } finally {
6110
+ activeCtrl = void 0;
6111
+ }
6112
+ }
6113
+ }
4371
6114
  } catch (err) {
4372
6115
  opts.renderer.writeError(err instanceof Error ? err.message : String(err));
4373
6116
  } finally {
@@ -4480,7 +6223,10 @@ async function execute(deps) {
4480
6223
  switchProviderAndModel,
4481
6224
  director,
4482
6225
  fleetRoster,
4483
- fleetStreamController
6226
+ fleetStreamController,
6227
+ getYolo,
6228
+ getAutonomy,
6229
+ skillLoader
4484
6230
  } = deps;
4485
6231
  let code = 0;
4486
6232
  try {
@@ -4584,6 +6330,7 @@ async function execute(deps) {
4584
6330
  banner: !flags["no-banner"],
4585
6331
  queueStore,
4586
6332
  yolo: !!config.yolo,
6333
+ getYolo,
4587
6334
  appVersion: CLI_VERSION,
4588
6335
  provider: config.provider,
4589
6336
  family: banneredFamily,
@@ -4610,7 +6357,36 @@ async function execute(deps) {
4610
6357
  },
4611
6358
  fleetStreamController,
4612
6359
  initialGoal: goalFlag,
4613
- initialAsk: askFlag
6360
+ initialAsk: askFlag,
6361
+ getSDDContext: () => {
6362
+ const { getActiveSDDContext: getActiveSDDContext2 } = (init_sdd(), __toCommonJS(sdd_exports));
6363
+ return getActiveSDDContext2();
6364
+ },
6365
+ onSDDOutput: async (output) => {
6366
+ const { trySaveSpecFromAIOutput: trySaveSpecFromAIOutput2, trySaveImplementationPlan: trySaveImplementationPlan2, trySaveTasksFromAIOutput: trySaveTasksFromAIOutput2, autoDetectTaskCompletion: autoDetectTaskCompletion2, getTaskProgress: getTaskProgress2, getActiveSDDPhase: getActiveSDDPhase2 } = (init_sdd(), __toCommonJS(sdd_exports));
6367
+ const messages = [];
6368
+ const specSaved = await trySaveSpecFromAIOutput2(output);
6369
+ if (specSaved) messages.push("\u2713 Spec detected and saved! Use /sdd approve to continue.");
6370
+ const planSaved = trySaveImplementationPlan2(output);
6371
+ if (planSaved) messages.push("\u2713 Implementation plan saved!");
6372
+ const tasksSaved = await trySaveTasksFromAIOutput2(output);
6373
+ if (tasksSaved) {
6374
+ const progress = getTaskProgress2();
6375
+ const count = progress?.total ?? 0;
6376
+ messages.push(`\u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`);
6377
+ }
6378
+ const sddPhase = getActiveSDDPhase2();
6379
+ if (sddPhase === "executing") {
6380
+ const autoCompleted = autoDetectTaskCompletion2(output);
6381
+ if (autoCompleted > 0) {
6382
+ const progress = getTaskProgress2();
6383
+ if (progress) {
6384
+ messages.push(`\u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
6385
+ }
6386
+ }
6387
+ }
6388
+ return messages;
6389
+ }
4614
6390
  });
4615
6391
  } finally {
4616
6392
  renderer.setSilent(false);
@@ -4636,7 +6412,9 @@ async function execute(deps) {
4636
6412
  supportsVision,
4637
6413
  attachments,
4638
6414
  effectiveMaxContext,
4639
- projectName: path15.basename(projectRoot) || void 0
6415
+ projectName: path18.basename(projectRoot) || void 0,
6416
+ getAutonomy,
6417
+ skillLoader
4640
6418
  });
4641
6419
  } finally {
4642
6420
  await webuiPromise.catch(() => void 0);
@@ -4652,7 +6430,9 @@ async function execute(deps) {
4652
6430
  supportsVision,
4653
6431
  attachments,
4654
6432
  effectiveMaxContext,
4655
- projectName: path15.basename(projectRoot) || void 0
6433
+ projectName: path18.basename(projectRoot) || void 0,
6434
+ getAutonomy,
6435
+ skillLoader
4656
6436
  });
4657
6437
  }
4658
6438
  } finally {
@@ -4752,6 +6532,15 @@ var MultiAgentHost = class {
4752
6532
  this.pending.delete(task.id);
4753
6533
  this.emitLifecycleCompleted(task.id, result);
4754
6534
  });
6535
+ this.director.fleet.filter("budget.threshold_reached", (e) => {
6536
+ const payload = e.payload;
6537
+ this.deps.events.emit("subagent.budget_warning", {
6538
+ subagentId: e.subagentId,
6539
+ kind: payload.kind,
6540
+ used: payload.used,
6541
+ limit: payload.limit
6542
+ });
6543
+ });
4755
6544
  this.coordinator = this.director.coordinator;
4756
6545
  } else {
4757
6546
  this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, {});
@@ -4913,7 +6702,7 @@ var MultiAgentHost = class {
4913
6702
  model: opts?.model,
4914
6703
  tools: opts?.tools
4915
6704
  };
4916
- const transcriptPath = this.sessionFactory ? path15.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
6705
+ const transcriptPath = this.sessionFactory ? path18.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
4917
6706
  if (this.director) {
4918
6707
  const subagentId = await this.director.spawn(subagentConfig);
4919
6708
  const taskId2 = randomUUID();
@@ -4982,8 +6771,16 @@ var MultiAgentHost = class {
4982
6771
  description: v.description,
4983
6772
  subagentId: v.subagentId
4984
6773
  }));
4985
- const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
4986
- return { pending, completed: this.results, summary };
6774
+ const live = [];
6775
+ if (this.coordinator) {
6776
+ const s = this.coordinator.getStatus();
6777
+ for (const a of s.subagents) {
6778
+ live.push({ subagentId: a.id, status: a.status, task: a.currentTask });
6779
+ }
6780
+ }
6781
+ const liveCount = live.filter((s) => s.status === "running" || s.status === "idle").length;
6782
+ const summary = !this.coordinator ? "No subagents have been spawned." : liveCount > 0 ? `${pending.length} pending, ${liveCount} active, ${this.results.length} completed.` : `${pending.length} pending, ${this.results.length} completed.`;
6783
+ return { pending, completed: this.results, live, summary };
4987
6784
  }
4988
6785
  /**
4989
6786
  * Roll up per-subagent runtime cost from completed TaskResults. We don't
@@ -5065,16 +6862,16 @@ var MultiAgentHost = class {
5065
6862
  }
5066
6863
  this.opts.directorMode = true;
5067
6864
  if (this.opts.fleetRoot && !this.opts.manifestPath) {
5068
- this.opts.manifestPath = path15.join(this.opts.fleetRoot, "fleet.json");
6865
+ this.opts.manifestPath = path18.join(this.opts.fleetRoot, "fleet.json");
5069
6866
  }
5070
6867
  if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
5071
- this.opts.sharedScratchpadPath = path15.join(this.opts.fleetRoot, "shared");
6868
+ this.opts.sharedScratchpadPath = path18.join(this.opts.fleetRoot, "shared");
5072
6869
  }
5073
6870
  if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
5074
- this.opts.sessionsRoot = path15.join(this.opts.fleetRoot, "subagents");
6871
+ this.opts.sessionsRoot = path18.join(this.opts.fleetRoot, "subagents");
5075
6872
  }
5076
6873
  if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
5077
- this.opts.stateCheckpointPath = path15.join(this.opts.fleetRoot, "director-state.json");
6874
+ this.opts.stateCheckpointPath = path18.join(this.opts.fleetRoot, "director-state.json");
5078
6875
  }
5079
6876
  await this.ensureDirector();
5080
6877
  return this.director ?? null;
@@ -5195,11 +6992,11 @@ var SessionStats = class {
5195
6992
  if (e.name === "bash") this.bashCommands++;
5196
6993
  else if (e.name === "fetch") this.fetches++;
5197
6994
  if (!e.ok) return;
5198
- const path16 = typeof input?.path === "string" ? input.path : void 0;
5199
- if (e.name === "read" && path16) this.readPaths.add(path16);
5200
- else if (e.name === "edit" && path16) this.editedPaths.add(path16);
5201
- else if (e.name === "write" && path16) {
5202
- this.writtenPaths.add(path16);
6995
+ const path19 = typeof input?.path === "string" ? input.path : void 0;
6996
+ if (e.name === "read" && path19) this.readPaths.add(path19);
6997
+ else if (e.name === "edit" && path19) this.editedPaths.add(path19);
6998
+ else if (e.name === "write" && path19) {
6999
+ this.writtenPaths.add(path19);
5203
7000
  const content = typeof input?.content === "string" ? input.content : "";
5204
7001
  this.bytesWritten += Buffer.byteLength(content, "utf8");
5205
7002
  }
@@ -5405,36 +7202,14 @@ async function setupCompaction(params) {
5405
7202
  const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
5406
7203
  const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
5407
7204
  const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
5408
- console.error("[DEBUG] setupCompaction:", {
5409
- providerId: provider.id,
5410
- model: context.model,
5411
- resolvedCapsMaxContext: resolvedCaps?.maxContext,
5412
- providerCapMaxContext: provider.capabilities.maxContext,
5413
- configEffectiveMaxContext: config.context.effectiveMaxContext,
5414
- effectiveMaxContext,
5415
- resolvedCapsKeys: resolvedCaps ? Object.keys(resolvedCaps) : null
5416
- });
7205
+ let autoCompactor;
5417
7206
  if (config.context.autoCompact !== false) {
5418
- const autoCompactor = new AutoCompactionMiddleware(
7207
+ autoCompactor = new AutoCompactionMiddleware(
5419
7208
  compactor,
5420
7209
  effectiveMaxContext,
5421
- (ctx) => {
5422
- let total = 0;
5423
- for (const m of ctx.messages) {
5424
- if (typeof m.content === "string") {
5425
- total += Math.ceil(m.content.length / 4);
5426
- } else if (Array.isArray(m.content)) {
5427
- for (const b of m.content) {
5428
- if (b.type === "text") {
5429
- total += Math.ceil(b.text.length / 4);
5430
- } else if (b.type === "tool_use" || b.type === "tool_result") {
5431
- total += Math.ceil(JSON.stringify(b).length / 4);
5432
- }
5433
- }
5434
- }
5435
- }
5436
- return total;
5437
- },
7210
+ // Use the full API request estimator: messages + system prompt + tool definitions.
7211
+ // This matches what the provider actually counts as input tokens.
7212
+ (ctx) => estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
5438
7213
  {
5439
7214
  warn: config.context.warnThreshold,
5440
7215
  soft: config.context.softThreshold,
@@ -5444,7 +7219,7 @@ async function setupCompaction(params) {
5444
7219
  );
5445
7220
  pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
5446
7221
  }
5447
- return effectiveMaxContext;
7222
+ return { effectiveMaxContext, autoCompactor };
5448
7223
  }
5449
7224
  function createAgent(params) {
5450
7225
  return new Agent({
@@ -5552,12 +7327,12 @@ async function setupSession(params) {
5552
7327
  }
5553
7328
  const sessionRef = { current: session };
5554
7329
  await recoveryLock.write(session.id).catch(() => void 0);
5555
- const attachments = new DefaultAttachmentStore({ spoolDir: path15.join(wpaths.projectSessions, session.id, "attachments") });
5556
- const queueStore = new QueueStore({ dir: path15.join(wpaths.projectSessions, session.id) });
7330
+ const attachments = new DefaultAttachmentStore({ spoolDir: path18.join(wpaths.projectSessions, session.id, "attachments") });
7331
+ const queueStore = new QueueStore({ dir: path18.join(wpaths.projectSessions, session.id) });
5557
7332
  const ctxSignal = new AbortController().signal;
5558
7333
  const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
5559
7334
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
5560
- const todosCheckpointPath = path15.join(wpaths.projectSessions, `${session.id}.todos.json`);
7335
+ const todosCheckpointPath = path18.join(wpaths.projectSessions, `${session.id}.todos.json`);
5561
7336
  if (resumeId) {
5562
7337
  try {
5563
7338
  const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
@@ -5569,12 +7344,12 @@ async function setupSession(params) {
5569
7344
  }
5570
7345
  }
5571
7346
  const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
5572
- const planPath = path15.join(wpaths.projectSessions, `${session.id}.plan.json`);
7347
+ const planPath = path18.join(wpaths.projectSessions, `${session.id}.plan.json`);
5573
7348
  context.state.setMeta("plan.path", planPath);
5574
7349
  if (resumeId) {
5575
7350
  try {
5576
- const fleetRoot = path15.join(wpaths.projectSessions, session.id);
5577
- const dirState = await loadDirectorState(path15.join(fleetRoot, "director-state.json"));
7351
+ const fleetRoot = path18.join(wpaths.projectSessions, session.id);
7352
+ const dirState = await loadDirectorState(path18.join(fleetRoot, "director-state.json"));
5578
7353
  if (dirState) {
5579
7354
  const tCounts = {};
5580
7355
  for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
@@ -5601,7 +7376,7 @@ function resolveBundledSkillsDir2() {
5601
7376
  try {
5602
7377
  const req2 = createRequire(import.meta.url);
5603
7378
  const corePkg = req2.resolve("@wrongstack/core/package.json");
5604
- return path15.join(path15.dirname(corePkg), "skills");
7379
+ return path18.join(path18.dirname(corePkg), "skills");
5605
7380
  } catch {
5606
7381
  return void 0;
5607
7382
  }
@@ -5688,7 +7463,7 @@ async function main(argv) {
5688
7463
  modeId,
5689
7464
  modePrompt,
5690
7465
  modelCapabilities,
5691
- planPath: () => sessionRef.current ? path15.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
7466
+ planPath: () => sessionRef.current ? path18.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
5692
7467
  })
5693
7468
  );
5694
7469
  const toolRegistry = new ToolRegistry();
@@ -5716,7 +7491,7 @@ async function main(argv) {
5716
7491
  name: "session-store",
5717
7492
  check: async () => {
5718
7493
  try {
5719
- await fs14.access(wpaths.projectSessions);
7494
+ await fs5.access(wpaths.projectSessions);
5720
7495
  return { status: "healthy" };
5721
7496
  } catch (e) {
5722
7497
  return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
@@ -5733,7 +7508,7 @@ async function main(argv) {
5733
7508
  const dumpMetrics = () => {
5734
7509
  if (!metricsSink) return;
5735
7510
  try {
5736
- const out = path15.join(wpaths.projectSessions, "metrics.json");
7511
+ const out = path18.join(wpaths.projectSessions, "metrics.json");
5737
7512
  const snap = metricsSink.snapshot();
5738
7513
  writeFileSync(out, JSON.stringify(snap, null, 2));
5739
7514
  } catch {
@@ -5861,7 +7636,13 @@ async function main(argv) {
5861
7636
  });
5862
7637
  const pipelines = setupPipelines({ events, logger });
5863
7638
  const compactor = container.resolve(TOKENS.Compactor);
5864
- const effectiveMaxContext = await setupCompaction({ compactor, events, modelsRegistry, context, config, provider, pipelines });
7639
+ const { effectiveMaxContext, autoCompactor } = await setupCompaction({ compactor, events, modelsRegistry, context, config, provider, pipelines });
7640
+ const refreshMaxContext = async (providerId, modelId) => {
7641
+ if (!autoCompactor) return;
7642
+ const cap = await capabilitiesFor(modelsRegistry, providerId, modelId).catch(() => void 0);
7643
+ const mc = cap?.maxContext ?? config.context.effectiveMaxContext ?? 2e5;
7644
+ autoCompactor.setMaxContext(mc);
7645
+ };
5865
7646
  const updateSpinnerContext = () => {
5866
7647
  if (effectiveMaxContext > 0 && lastInputTokens > 0) {
5867
7648
  spinner.setContext({ used: lastInputTokens, max: effectiveMaxContext });
@@ -5924,23 +7705,20 @@ async function main(argv) {
5924
7705
  }
5925
7706
  const switchProviderAndModel = (providerId, modelId) => {
5926
7707
  try {
5927
- console.error("[DEBUG] switchProviderAndModel called with:", { providerId, modelId });
5928
7708
  const savedCfg = config.providers?.[providerId];
5929
7709
  const resolvedProviderId = savedCfg?.type ?? providerId;
5930
- console.error("[DEBUG] switchProviderAndModel: resolvedProviderId:", resolvedProviderId, "savedCfg.type:", savedCfg?.type);
5931
7710
  const newCfg = savedCfg ?? {
5932
7711
  type: providerId,
5933
7712
  apiKey: config.apiKey,
5934
7713
  baseUrl: config.baseUrl
5935
7714
  };
5936
7715
  const cfgWithType = { ...newCfg, type: resolvedProviderId };
5937
- console.error("[DEBUG] switchProviderAndModel: cfgWithType:", cfgWithType);
5938
7716
  const newProvider = config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
5939
- console.error("[DEBUG] switchProviderAndModel: new provider id:", newProvider.id, "maxContext:", newProvider.capabilities.maxContext);
5940
7717
  context.provider = newProvider;
5941
7718
  context.model = modelId;
5942
7719
  config = patchConfig(config, { provider: providerId, model: modelId });
5943
7720
  configStore.update({ provider: providerId, model: modelId });
7721
+ void refreshMaxContext(resolvedProviderId, modelId);
5944
7722
  return null;
5945
7723
  } catch (err) {
5946
7724
  return err instanceof Error ? err.message : String(err);
@@ -5948,12 +7726,13 @@ async function main(argv) {
5948
7726
  };
5949
7727
  const directorMode = flags["director"] === true;
5950
7728
  let director = null;
5951
- const fleetRoot = directorMode ? path15.join(wpaths.projectSessions, session.id) : void 0;
5952
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path15.join(fleetRoot, "fleet.json") : void 0;
5953
- const sharedScratchpadPath = directorMode ? path15.join(fleetRoot, "shared") : void 0;
5954
- const subagentSessionsRoot = directorMode ? path15.join(fleetRoot, "subagents") : void 0;
5955
- const stateCheckpointPath = directorMode ? path15.join(fleetRoot, "director-state.json") : void 0;
5956
- const fleetRootForPromotion = path15.join(wpaths.projectSessions, session.id);
7729
+ let autonomyMode = "off";
7730
+ const fleetRoot = directorMode ? path18.join(wpaths.projectSessions, session.id) : void 0;
7731
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path18.join(fleetRoot, "fleet.json") : void 0;
7732
+ const sharedScratchpadPath = directorMode ? path18.join(fleetRoot, "shared") : void 0;
7733
+ const subagentSessionsRoot = directorMode ? path18.join(fleetRoot, "subagents") : void 0;
7734
+ const stateCheckpointPath = directorMode ? path18.join(fleetRoot, "director-state.json") : void 0;
7735
+ const fleetRootForPromotion = path18.join(wpaths.projectSessions, session.id);
5957
7736
  const multiAgentHost = new MultiAgentHost(
5958
7737
  {
5959
7738
  container,
@@ -6024,6 +7803,7 @@ async function main(argv) {
6024
7803
  metricsSink,
6025
7804
  healthRegistry,
6026
7805
  planPath,
7806
+ modeStore,
6027
7807
  fleetStreamController,
6028
7808
  onSpawn: async (description, spawnOpts) => {
6029
7809
  const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
@@ -6037,8 +7817,19 @@ async function main(argv) {
6037
7817
  onAgents: () => {
6038
7818
  const s = multiAgentHost.status();
6039
7819
  const lines = [s.summary];
7820
+ const STATUS_ICON = {
7821
+ running: "\u25CF",
7822
+ idle: "\u25CB",
7823
+ stopped: "\u2298"
7824
+ };
7825
+ for (const a of s.live) {
7826
+ if (a.status === "running" || a.status === "idle") {
7827
+ const task = a.task ? ` \u2014 ${a.task.slice(0, 60)}` : "";
7828
+ lines.push(` ${STATUS_ICON[a.status] ?? "?"} ${a.subagentId.slice(0, 8)} ${a.status}${task}`);
7829
+ }
7830
+ }
6040
7831
  for (const p of s.pending) {
6041
- lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
7832
+ lines.push(` \xB7 pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
6042
7833
  }
6043
7834
  for (const r of s.completed) {
6044
7835
  const fmt = fmtTaskResultLine(r, color);
@@ -6050,11 +7841,26 @@ async function main(argv) {
6050
7841
  if (action === "status") {
6051
7842
  const s = multiAgentHost.status();
6052
7843
  const lines = [color.bold("Fleet status"), ` ${s.summary}`];
7844
+ const STATUS_ICON = {
7845
+ running: "\u25CF",
7846
+ idle: "\u25CB",
7847
+ stopped: "\u2298"
7848
+ };
7849
+ const liveActive = s.live.filter((a) => a.status === "running" || a.status === "idle");
7850
+ if (liveActive.length > 0) {
7851
+ lines.push("", color.dim(" Active"));
7852
+ for (const a of liveActive) {
7853
+ const task = a.task ? ` \xB7 ${a.task.slice(0, 50)}` : "";
7854
+ lines.push(
7855
+ ` ${STATUS_ICON[a.status] ?? "?"} ${a.subagentId.slice(0, 8)} ${a.status}${task}`
7856
+ );
7857
+ }
7858
+ }
6053
7859
  if (s.pending.length > 0) {
6054
7860
  lines.push("", color.dim(" Pending"));
6055
7861
  for (const p of s.pending) {
6056
7862
  lines.push(
6057
- ` ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`
7863
+ ` \xB7 ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`
6058
7864
  );
6059
7865
  }
6060
7866
  }
@@ -6105,27 +7911,27 @@ async function main(argv) {
6105
7911
  return `Unknown fleet action: ${action}`;
6106
7912
  },
6107
7913
  onFleetLog: async (subagentId, mode) => {
6108
- const subagentsRoot = path15.join(fleetRootForPromotion, "subagents");
7914
+ const subagentsRoot = path18.join(fleetRootForPromotion, "subagents");
6109
7915
  let runDirs;
6110
7916
  try {
6111
- runDirs = await fs14.readdir(subagentsRoot);
7917
+ runDirs = await fs5.readdir(subagentsRoot);
6112
7918
  } catch {
6113
7919
  return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
6114
7920
  }
6115
7921
  const found = [];
6116
7922
  for (const runId of runDirs) {
6117
- const runDir = path15.join(subagentsRoot, runId);
7923
+ const runDir = path18.join(subagentsRoot, runId);
6118
7924
  let files;
6119
7925
  try {
6120
- files = await fs14.readdir(runDir);
7926
+ files = await fs5.readdir(runDir);
6121
7927
  } catch {
6122
7928
  continue;
6123
7929
  }
6124
7930
  for (const f of files) {
6125
7931
  if (!f.endsWith(".jsonl")) continue;
6126
- const full = path15.join(runDir, f);
7932
+ const full = path18.join(runDir, f);
6127
7933
  try {
6128
- const stat2 = await fs14.stat(full);
7934
+ const stat2 = await fs5.stat(full);
6129
7935
  found.push({
6130
7936
  runId,
6131
7937
  subagentId: f.replace(/\.jsonl$/, ""),
@@ -6164,7 +7970,7 @@ async function main(argv) {
6164
7970
  ].join("\n");
6165
7971
  }
6166
7972
  const t = matches[0];
6167
- const raw = await fs14.readFile(t.file, "utf8");
7973
+ const raw = await fs5.readFile(t.file, "utf8");
6168
7974
  if (mode === "raw") return raw;
6169
7975
  const lines = raw.split("\n").filter((l) => l.trim());
6170
7976
  const counts = {};
@@ -6220,7 +8026,7 @@ async function main(argv) {
6220
8026
  }
6221
8027
  const dir = await multiAgentHost.ensureDirector();
6222
8028
  if (!dir) return "Director is not available.";
6223
- const dirStatePath = path15.join(fleetRootForPromotion, "director-state.json");
8029
+ const dirStatePath = path18.join(fleetRootForPromotion, "director-state.json");
6224
8030
  const prior = await loadDirectorState(dirStatePath);
6225
8031
  if (!prior) {
6226
8032
  return "No prior director-state.json found \u2014 nothing to retry.";
@@ -6291,9 +8097,9 @@ async function main(argv) {
6291
8097
  for (const tool of director2.tools(FLEET_ROSTER)) {
6292
8098
  toolRegistry.register(tool);
6293
8099
  }
6294
- const mp = path15.join(fleetRootForPromotion, "fleet.json");
6295
- const sp = path15.join(fleetRootForPromotion, "shared");
6296
- const ss = path15.join(fleetRootForPromotion, "subagents");
8100
+ const mp = path18.join(fleetRootForPromotion, "fleet.json");
8101
+ const sp = path18.join(fleetRootForPromotion, "shared");
8102
+ const ss = path18.join(fleetRootForPromotion, "subagents");
6297
8103
  const lines = [
6298
8104
  `${color.green("\u2713")} Promoted to director mode.`,
6299
8105
  ` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
@@ -6320,9 +8126,43 @@ Restart WrongStack to load or unload plugin code in this session.`;
6320
8126
  }
6321
8127
  return result.message;
6322
8128
  },
8129
+ onYolo: (setTo) => {
8130
+ const policy = container.resolve(TOKENS.PermissionPolicy);
8131
+ if (setTo !== void 0) {
8132
+ policy.setYolo(setTo);
8133
+ config = patchConfig(config, { yolo: setTo });
8134
+ return setTo;
8135
+ }
8136
+ return policy.getYolo();
8137
+ },
8138
+ onAutonomy: (setTo) => {
8139
+ if (setTo !== void 0) {
8140
+ autonomyMode = setTo;
8141
+ return setTo;
8142
+ }
8143
+ return autonomyMode;
8144
+ },
6323
8145
  onExit: () => {
6324
8146
  void mcpRegistry.stopAll();
6325
8147
  },
8148
+ onBeforeExit: async () => {
8149
+ const { spawn: spawn2 } = await import('child_process');
8150
+ const cwd2 = projectRoot;
8151
+ const statusResult = await new Promise((resolve3) => {
8152
+ const child = spawn2("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
8153
+ let stdout = "";
8154
+ child.stdout?.on("data", (d) => stdout += d);
8155
+ child.on("close", (code) => resolve3({ stdout, code: code ?? 0 }));
8156
+ });
8157
+ if (statusResult.stdout.trim().length > 0) {
8158
+ const lines = statusResult.stdout.split("\n").filter(Boolean);
8159
+ return {
8160
+ abort: true,
8161
+ // signals there are uncommitted changes (used only for the message)
8162
+ message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
8163
+ };
8164
+ }
8165
+ },
6326
8166
  onClear: () => {
6327
8167
  if (flags.tui && !flags["no-tui"]) return;
6328
8168
  try {
@@ -6382,7 +8222,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
6382
8222
  switchProviderAndModel,
6383
8223
  director: director ?? null,
6384
8224
  fleetRoster: FLEET_ROSTER,
6385
- fleetStreamController
8225
+ fleetStreamController,
8226
+ getYolo: () => {
8227
+ const policy = container.resolve(TOKENS.PermissionPolicy);
8228
+ return policy.getYolo();
8229
+ },
8230
+ getAutonomy: () => autonomyMode,
8231
+ skillLoader: config.features.skills ? skillLoader : void 0
6386
8232
  });
6387
8233
  }
6388
8234
  async function promptRecovery(reader, renderer, abandoned, autoRecover) {