@wrongstack/cli 0.6.5 → 0.6.6

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
@@ -3,7 +3,7 @@ import * as path23 from 'path';
3
3
  import { join } from 'path';
4
4
  import * as fsp2 from 'fs/promises';
5
5
  import { readdir, readFile } from 'fs/promises';
6
- import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeAgentSubagentRunner, NULL_FLEET_BUS, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
6
+ import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeAgentSubagentRunner, NULL_FLEET_BUS, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
7
7
  import { createRequire } from 'module';
8
8
  import * as os6 from 'os';
9
9
  import os6__default from 'os';
@@ -53,14 +53,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
53
53
  // src/slash-commands/sdd.ts
54
54
  var sdd_exports = {};
55
55
  __export(sdd_exports, {
56
+ advanceToNextTask: () => advanceToNextTask,
56
57
  autoDetectTaskCompletion: () => autoDetectTaskCompletion,
57
58
  buildSddCommand: () => buildSddCommand,
58
59
  getActiveBuilder: () => getActiveBuilder,
59
60
  getActiveSDDContext: () => getActiveSDDContext,
60
61
  getActiveSDDPhase: () => getActiveSDDPhase,
62
+ getCurrentExecutingContext: () => getCurrentExecutingContext,
63
+ getCurrentTask: () => getCurrentTask,
61
64
  getTaskListText: () => getTaskListText,
62
65
  getTaskProgress: () => getTaskProgress,
63
66
  markTaskCompleted: () => markTaskCompleted,
67
+ renderTaskListWithProgress: () => renderTaskListWithProgress,
64
68
  trySaveImplementationPlan: () => trySaveImplementationPlan,
65
69
  trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
66
70
  trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
@@ -106,6 +110,27 @@ async function trySaveTasksFromAIOutput(aiOutput) {
106
110
  if (!Array.isArray(tasks) || tasks.length === 0) return false;
107
111
  const validTasks = tasks.filter((t) => t && typeof t === "object" && typeof t.title === "string" && t.title.length > 0);
108
112
  if (validTasks.length === 0) return false;
113
+ const existingTracker = sddState.getTaskTracker();
114
+ if (existingTracker) {
115
+ for (const task of validTasks) {
116
+ const title = String(task.title);
117
+ const description = String(task.description ?? "");
118
+ const type = ["feature", "bugfix", "refactor", "docs", "test", "chore"].includes(String(task.type)) ? String(task.type) : "feature";
119
+ const priority = ["critical", "high", "medium", "low"].includes(String(task.priority)) ? String(task.priority) : "medium";
120
+ const estimateHours = Number(task.estimateHours) || 2;
121
+ const tags = Array.isArray(task.tags) ? task.tags.map(String) : [];
122
+ existingTracker.addNode({
123
+ title,
124
+ description,
125
+ type,
126
+ priority,
127
+ status: "pending",
128
+ estimateHours,
129
+ tags
130
+ });
131
+ }
132
+ return true;
133
+ }
109
134
  const store = new DefaultTaskStore();
110
135
  const tracker = new TaskTracker({ store });
111
136
  const graph = await tracker.createGraph(session.spec.id, session.spec.title);
@@ -135,14 +160,47 @@ async function trySaveTasksFromAIOutput(aiOutput) {
135
160
  function getTaskProgress() {
136
161
  const tracker = sddState.getTaskTracker();
137
162
  if (!tracker) return null;
138
- const progress = tracker.getProgress();
163
+ return tracker.getProgress();
164
+ }
165
+ function getCurrentTask() {
166
+ const tracker = sddState.getTaskTracker();
167
+ if (!tracker) return null;
168
+ const nodes = tracker.getAllNodes({ status: ["in_progress"] });
169
+ if (nodes.length === 0) return null;
170
+ const n = nodes[0];
139
171
  return {
140
- total: progress.total,
141
- completed: progress.completed,
142
- pending: progress.pending,
143
- percent: progress.percentComplete
172
+ id: n.id,
173
+ title: n.title,
174
+ description: n.description,
175
+ priority: n.priority,
176
+ estimateHours: n.estimateHours ?? 0,
177
+ tags: n.tags ?? [],
178
+ startedAt: n.startedAt
144
179
  };
145
180
  }
181
+ function advanceToNextTask() {
182
+ const tracker = sddState.getTaskTracker();
183
+ if (!tracker) return false;
184
+ const pending = tracker.getAllNodes({ status: ["pending"] });
185
+ for (const n of pending) {
186
+ if (tracker.canStart(n.id)) {
187
+ tracker.updateNodeStatus(n.id, "in_progress");
188
+ return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
193
+ function formatElapsed(ms) {
194
+ if (ms < 1e3) return `${ms}ms`;
195
+ const s = Math.floor(ms / 1e3);
196
+ if (s < 60) return `${s}s`;
197
+ const m = Math.floor(s / 60);
198
+ const remS = s % 60;
199
+ if (m < 60) return remS > 0 ? `${m}m ${remS}s` : `${m}m`;
200
+ const h = Math.floor(m / 60);
201
+ const remM = m % 60;
202
+ return `${h}h ${remM}m`;
203
+ }
146
204
  function getTaskListText() {
147
205
  const tracker = sddState.getTaskTracker();
148
206
  if (!tracker) return null;
@@ -154,6 +212,58 @@ function getTaskListText() {
154
212
  });
155
213
  return lines.join("\n");
156
214
  }
215
+ function renderTaskListWithProgress() {
216
+ const tracker = sddState.getTaskTracker();
217
+ if (!tracker) return null;
218
+ const nodes = tracker.getAllNodes();
219
+ if (nodes.length === 0) return null;
220
+ const progress = tracker.getProgress();
221
+ const phase = sddState.getPhase();
222
+ const phaseLabel = {
223
+ questioning: "\u2753 Questioning",
224
+ spec_review: "\u{1F4CB} Spec Review",
225
+ implementation: "\u{1F3D7}\uFE0F Implementation",
226
+ task_review: "\u{1F4DD} Task Review",
227
+ executing: "\u26A1 Executing",
228
+ done: "\u2705 Done"
229
+ };
230
+ const lines = [
231
+ `**${phaseLabel[phase ?? ""] ?? phase} \u2014 Task Status**`,
232
+ "",
233
+ renderProgress(progress),
234
+ ""
235
+ ];
236
+ const sorted = [...nodes].sort((a, b) => {
237
+ const order = { in_progress: 0, pending: 1, review: 2, blocked: 3, failed: 4, completed: 5 };
238
+ return (order[a.status] ?? 6) - (order[b.status] ?? 6);
239
+ });
240
+ for (let i = 0; i < sorted.length; i++) {
241
+ const n = sorted[i];
242
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
243
+ const title = n.title.length > 50 ? n.title.slice(0, 49) + "\u2026" : n.title;
244
+ let elapsed = "";
245
+ if (n.status === "in_progress" && n.startedAt) {
246
+ elapsed = ` \xB7 ${formatElapsed(Date.now() - n.startedAt)}`;
247
+ }
248
+ lines.push(`${i + 1}. ${status} ${title}${elapsed}`);
249
+ }
250
+ return lines.join("\n");
251
+ }
252
+ function getCurrentExecutingContext() {
253
+ const tracker = sddState.getTaskTracker();
254
+ if (!tracker) return null;
255
+ const nodes = tracker.getAllNodes({ status: ["in_progress"] });
256
+ if (nodes.length === 0) return null;
257
+ const n = nodes[0];
258
+ const elapsed = n.startedAt ? ` \xB7 elapsed: ${formatElapsed(Date.now() - n.startedAt)}` : "";
259
+ const progress = tracker.getProgress();
260
+ return [
261
+ `**NOW EXECUTING:** "${n.title}"${elapsed}`,
262
+ `Description: ${n.description.split("\n")[0] ?? "(none)"}`,
263
+ `Priority: ${n.priority} \xB7 Est: ${n.estimateHours ?? 0}h \xB7 Tags: ${(n.tags ?? []).join(", ") || "none"}`,
264
+ `Progress: ${progress.completed}/${progress.total} tasks (${progress.percentComplete}%)`
265
+ ].join("\n");
266
+ }
157
267
  function markTaskCompleted(taskTitle) {
158
268
  const tracker = sddState.getTaskTracker();
159
269
  if (!tracker) return false;
@@ -238,20 +348,26 @@ function trySaveImplementationPlan(aiOutput) {
238
348
  if (!builder) return false;
239
349
  const session = builder.getSession();
240
350
  if (session.phase !== "implementation") return false;
351
+ const current = session.implementation ?? "";
241
352
  const jsonMatch = aiOutput.match(/```json\s*\[/);
242
353
  if (jsonMatch?.index && jsonMatch.index > 0) {
243
354
  const plan = aiOutput.substring(0, jsonMatch.index).trim();
244
- if (plan.length > 50) {
355
+ if (plan.length > 50 && plan !== current && !isExplanatoryText(plan)) {
245
356
  builder.setImplementation(plan);
246
357
  return true;
247
358
  }
248
359
  }
249
- if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
360
+ if (aiOutput.length > 100 && !aiOutput.includes("```json") && aiOutput !== current && !isExplanatoryText(aiOutput)) {
250
361
  builder.setImplementation(aiOutput.trim());
251
362
  return true;
252
363
  }
253
364
  return false;
254
365
  }
366
+ function isExplanatoryText(text) {
367
+ const lower = text.toLowerCase();
368
+ return lower.startsWith("i'") || lower.startsWith("i will") || lower.startsWith("let me") || lower.startsWith("here's my") || lower.startsWith("here is my") || lower.startsWith("i'm going to") || lower.startsWith("first, let me") || lower.startsWith("sure") || lower.startsWith("of course") || lower.startsWith("okay") || lower.startsWith("ok,") || lower.startsWith("sounds good") || lower.startsWith("no problem") || // Skip if mostly code-like with minimal prose
369
+ text.split("\n").length < 3 && !text.includes(".");
370
+ }
255
371
  function getActiveBuilder() {
256
372
  return sddState.getBuilder();
257
373
  }
@@ -267,7 +383,7 @@ function buildSddCommand(opts) {
267
383
  const graphsDir = path23.join(projectRoot, ".wrongstack", "task-graphs");
268
384
  const specStore = new SpecStore({ baseDir: specsDir });
269
385
  new TaskGraphStore({ baseDir: graphsDir });
270
- const versioning = new SpecVersioning();
386
+ const versioning = sddState.getVersioning();
271
387
  const [verb, ...rest] = args.trim().split(/\s+/);
272
388
  const restJoined = rest.join(" ").trim();
273
389
  switch (verb) {
@@ -317,6 +433,8 @@ function buildSddCommand(opts) {
317
433
  maxQuestions: 10,
318
434
  sessionPath: path23.join(projectRoot, ".wrongstack", "sdd-session.json")
319
435
  }));
436
+ sddState.setSessionStartTime(Date.now());
437
+ sddState.setPhaseStartTime(Date.now());
320
438
  const builder = sddState.getBuilder();
321
439
  builder.startSession(title);
322
440
  const aiPrompt = builder.getAIPrompt();
@@ -370,6 +488,7 @@ Generate the complete specification now based on the conversation so far.`
370
488
  await builder.saveSpec();
371
489
  versioning.recordVersion(spec, "Initial spec approved");
372
490
  builder.approve();
491
+ sddState.setPhaseStartTime(Date.now());
373
492
  const implPrompt = builder.getAIPrompt();
374
493
  return {
375
494
  message: [
@@ -389,6 +508,8 @@ Generate the implementation plan and tasks for the approved spec.`
389
508
  }
390
509
  if (phase === "task_review") {
391
510
  builder.approve();
511
+ sddState.setPhaseStartTime(Date.now());
512
+ advanceToNextTask();
392
513
  const execPrompt = builder.getAIPrompt();
393
514
  return {
394
515
  message: "\u2705 Tasks approved! The AI will now execute them one by one.",
@@ -400,6 +521,24 @@ User message:
400
521
  Start executing the tasks one by one.`
401
522
  };
402
523
  }
524
+ if (phase === "implementation") {
525
+ const session = builder.getSession();
526
+ const plan = session.implementation;
527
+ if (!plan) {
528
+ return {
529
+ message: "No implementation plan yet. The AI is still generating it. Try again shortly."
530
+ };
531
+ }
532
+ return {
533
+ message: [
534
+ `\u256D\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E`,
535
+ "",
536
+ ...plan.split("\n").map((l) => ` ${l}`),
537
+ "",
538
+ `\u2570${"\u2500".repeat(55)}\u256F`
539
+ ].join("\n")
540
+ };
541
+ }
403
542
  return {
404
543
  message: `Current phase is "${phase}". Use /sdd status to see details.`
405
544
  };
@@ -492,18 +631,46 @@ Start executing the tasks one by one.`
492
631
  return { message: "No tasks in the current graph." };
493
632
  }
494
633
  const progress = taskTracker.getProgress();
634
+ const builder = sddState.getBuilder();
635
+ const phase = builder?.getPhase() ?? "unknown";
636
+ const phaseLabel = {
637
+ questioning: "\u2753 Questioning",
638
+ spec_review: "\u{1F4CB} Spec Review",
639
+ implementation: "\u{1F3D7}\uFE0F Implementation",
640
+ task_review: "\u{1F4DD} Task Review",
641
+ executing: "\u26A1 Executing",
642
+ done: "\u2705 Done"
643
+ };
495
644
  const lines = [
496
- `\u2550\u2550\u2550 Task List (${progress.completed}/${progress.total} done) \u2550\u2550\u2550`,
497
- ""
645
+ `\u256D\u2500\u2500\u2500 ${phaseLabel[phase] ?? phase} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E`,
646
+ "",
647
+ renderProgress(progress),
648
+ "",
649
+ ` # Status Priority Task`,
650
+ ` ${"\u2500".repeat(49)}`
498
651
  ];
499
- for (let i = 0; i < nodes.length; i++) {
500
- const n = nodes[i];
501
- const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : "\u23F3";
502
- lines.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
503
- if (n.description) {
504
- lines.push(` ${n.description.split("\n")[0]}`);
652
+ const sorted = [...nodes].sort((a, b) => {
653
+ const order = { in_progress: 0, pending: 1, review: 2, blocked: 3, failed: 4, completed: 5 };
654
+ return (order[a.status] ?? 6) - (order[b.status] ?? 6);
655
+ });
656
+ for (let i = 0; i < sorted.length; i++) {
657
+ const n = sorted[i];
658
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
659
+ const num = `${i + 1}`.padStart(3);
660
+ const prio = n.priority.slice(0, 4).padEnd(5);
661
+ const title = n.title.length > 36 ? n.title.slice(0, 35) + "\u2026" : n.title;
662
+ const elapsed = n.status === "in_progress" && n.startedAt ? ` (${formatElapsed(Date.now() - n.startedAt)})` : "";
663
+ lines.push(` ${num} ${status} ${prio} ${title}${elapsed}`);
664
+ if (n.description && n.status !== "completed") {
665
+ const first = n.description.split("\n")[0];
666
+ const truncated = first.length > 42 ? first.slice(0, 41) + "\u2026" : first;
667
+ lines.push(` \u21B3 ${truncated}`);
505
668
  }
506
669
  }
670
+ lines.push("");
671
+ lines.push(` Commands: /sdd done <N> \xB7 /sdd skip <N> \xB7 /sdd fail <N> \xB7 /sdd review <N>`);
672
+ lines.push(` /sdd next \xB7 /sdd status \xB7 /sdd edit <N> \xB7 /sdd approve`);
673
+ lines.push(`\u2570${"\u2500".repeat(54)}\u256F`);
507
674
  return { message: lines.join("\n") };
508
675
  }
509
676
  case "done":
@@ -539,9 +706,194 @@ Start executing the tasks one by one.`
539
706
  }
540
707
  const remaining = doneTracker.getProgress();
541
708
  return {
542
- message: `\u2705 Task completed! ${remaining.completed}/${remaining.total} done (${remaining.percentComplete}%)`
709
+ message: `\u2705 Task marked done! (${remaining.completed}/${remaining.total} \u2014 ${remaining.percentComplete}%)`
710
+ };
711
+ }
712
+ case "skip": {
713
+ const skipTracker = sddState.getTaskTracker();
714
+ if (!skipTracker) return { message: "No tasks to skip." };
715
+ if (!restJoined) return { message: "Usage: /sdd skip <task title or number>" };
716
+ const nodes = skipTracker.getAllNodes({ status: ["pending", "in_progress", "blocked"] });
717
+ const num = Number(restJoined);
718
+ let matched = false;
719
+ if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
720
+ const node = nodes[num - 1];
721
+ if (node) {
722
+ skipTracker.updateNodeStatus(node.id, "pending");
723
+ matched = true;
724
+ }
725
+ }
726
+ if (!matched) {
727
+ const match = nodes.find(
728
+ (n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
729
+ );
730
+ if (match) {
731
+ skipTracker.updateNodeStatus(match.id, "pending");
732
+ matched = true;
733
+ }
734
+ }
735
+ if (!matched) return { message: `No task matching "${restJoined}".` };
736
+ const progress = skipTracker.getProgress();
737
+ return {
738
+ message: `\u23ED Task skipped \u2014 moved to pending. (${progress.completed}/${progress.total} \u2014 ${progress.percentComplete}%)`
739
+ };
740
+ }
741
+ case "fail": {
742
+ const failTracker = sddState.getTaskTracker();
743
+ if (!failTracker) return { message: "No tasks to fail." };
744
+ if (!restJoined) return { message: "Usage: /sdd fail <task title or number>" };
745
+ const nodes = failTracker.getAllNodes({ status: ["pending", "in_progress"] });
746
+ const num = Number(restJoined);
747
+ let matched = false;
748
+ if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
749
+ const node = nodes[num - 1];
750
+ if (node) {
751
+ failTracker.updateNodeStatus(node.id, "failed");
752
+ matched = true;
753
+ }
754
+ }
755
+ if (!matched) {
756
+ const match = nodes.find(
757
+ (n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
758
+ );
759
+ if (match) {
760
+ failTracker.updateNodeStatus(match.id, "failed");
761
+ matched = true;
762
+ }
763
+ }
764
+ if (!matched) return { message: `No pending/in-progress task matching "${restJoined}".` };
765
+ const progress = failTracker.getProgress();
766
+ return {
767
+ message: `\u274C Task marked as failed. (${progress.failed} failed \xB7 ${progress.completed}/${progress.total} done)`
768
+ };
769
+ }
770
+ case "review": {
771
+ const reviewTracker = sddState.getTaskTracker();
772
+ if (!reviewTracker) return { message: "No tasks to review." };
773
+ if (!restJoined) return { message: "Usage: /sdd review <task title or number>" };
774
+ const nodes = reviewTracker.getAllNodes();
775
+ const num = Number(restJoined);
776
+ let matched = false;
777
+ const sorted = [...nodes].sort((a, b) => {
778
+ const order = { in_progress: 0, pending: 1, review: 2, blocked: 3, failed: 4, completed: 5 };
779
+ return (order[a.status] ?? 6) - (order[b.status] ?? 6);
780
+ });
781
+ if (!Number.isNaN(num) && num >= 1 && num <= sorted.length) {
782
+ const node = sorted[num - 1];
783
+ if (node) {
784
+ reviewTracker.updateNodeStatus(node.id, "review");
785
+ matched = true;
786
+ }
787
+ }
788
+ if (!matched) {
789
+ const match = nodes.find(
790
+ (n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
791
+ );
792
+ if (match) {
793
+ reviewTracker.updateNodeStatus(match.id, "review");
794
+ matched = true;
795
+ }
796
+ }
797
+ if (!matched) return { message: `No task matching "${restJoined}".` };
798
+ const progress = reviewTracker.getProgress();
799
+ return {
800
+ message: `\u{1F441} Task sent to review. (${progress.review} in review)`
801
+ };
802
+ }
803
+ case "edit": {
804
+ const editTracker = sddState.getTaskTracker();
805
+ if (!editTracker) return { message: "No tasks to edit." };
806
+ if (!restJoined) return { message: "Usage: /sdd edit <N> <new title or description>" };
807
+ const parts = restJoined.split(/\s+/);
808
+ const num = Number(parts[0]);
809
+ if (Number.isNaN(num)) return { message: "Usage: /sdd edit <N> <new title or description>" };
810
+ const nodes = editTracker.getAllNodes();
811
+ if (num < 1 || num > nodes.length) return { message: `Task #${num} not found.` };
812
+ const node = nodes[num - 1];
813
+ if (!node) return { message: `Task #${num} not found.` };
814
+ const newContent = parts.slice(1).join(" ");
815
+ if (!newContent) return { message: "Provide new title or description content." };
816
+ if (newContent.length < 60) {
817
+ editTracker.updateNode(node.id, { title: newContent });
818
+ } else {
819
+ editTracker.updateNode(node.id, { description: newContent });
820
+ }
821
+ return { message: `\u270F\uFE0F Task #${num} updated: "${newContent.slice(0, 50)}${newContent.length > 50 ? "\u2026" : ""}"` };
822
+ }
823
+ case "undo": {
824
+ const undoTracker = sddState.getTaskTracker();
825
+ if (!undoTracker) {
826
+ return { message: "No tasks to undo." };
827
+ }
828
+ const completed = undoTracker.getAllNodes({ status: ["completed"] });
829
+ if (completed.length === 0) {
830
+ return { message: "No completed tasks to undo." };
831
+ }
832
+ const last = completed[completed.length - 1];
833
+ undoTracker.updateNodeStatus(last.id, "pending");
834
+ const progress = undoTracker.getProgress();
835
+ return {
836
+ message: `\u21A9 Undo: "${last.title}" back to pending. (${progress.completed}/${progress.total} \u2014 ${progress.percentComplete}%)`
543
837
  };
544
838
  }
839
+ // ── Next Task Preview ─────────────────────────────────────────────
840
+ case "next": {
841
+ const nextTracker = sddState.getTaskTracker();
842
+ if (!nextTracker) {
843
+ return { message: "No tasks generated yet. Use /sdd new to start." };
844
+ }
845
+ const pending = nextTracker.getAllNodes({ status: ["pending", "in_progress"] });
846
+ if (pending.length === 0) {
847
+ const allDone = nextTracker.getProgress();
848
+ if (allDone.completed === allDone.total) {
849
+ return { message: "\u{1F389} All tasks completed! Run /sdd status for the full summary." };
850
+ }
851
+ return { message: "No pending tasks." };
852
+ }
853
+ const next = pending.find((n) => nextTracker.canStart(n.id));
854
+ if (!next) {
855
+ const blocked = pending.filter((n) => {
856
+ const blockers2 = nextTracker.getBlockers(n.id);
857
+ return blockers2.some((id) => nextTracker.getNode(id)?.status !== "completed");
858
+ });
859
+ if (blocked.length > 0) {
860
+ return {
861
+ message: [
862
+ `\u{1F6AB} ${blocked.length} task(s) blocked \u2014 waiting on dependencies:`,
863
+ ...blocked.map((b, i) => {
864
+ const blockers2 = nextTracker.getBlockers(b.id);
865
+ const blockerNames = blockers2.map((id) => nextTracker.getNode(id)?.title ?? "?").join(", ");
866
+ return ` ${i + 1}. ${b.title} (blocked by: ${blockerNames})`;
867
+ })
868
+ ].join("\n")
869
+ };
870
+ }
871
+ return { message: "No next task found." };
872
+ }
873
+ const progress = nextTracker.getProgress();
874
+ const blockers = nextTracker.getBlockers(next.id);
875
+ const blockedBy = blockers.filter((id) => nextTracker.getNode(id)?.status !== "completed").map((id) => nextTracker.getNode(id)?.title ?? "?").join(", ");
876
+ const lines = [
877
+ `\u256D\u2500\u2500\u2500 NEXT TASK \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E`,
878
+ "",
879
+ ` \u{1F504} ${next.title}`
880
+ ];
881
+ if (next.description) {
882
+ const first = next.description.split("\n")[0];
883
+ lines.push(` \u21B3 ${first}`);
884
+ }
885
+ const taskElapsed = next.startedAt ? ` \u23F1 ${formatElapsed(Date.now() - next.startedAt)}` : "";
886
+ lines.push(` Priority: ${next.priority} | Est: ${next.estimateHours}h | Tags: ${(next.tags ?? []).join(", ") || "none"}${taskElapsed}`);
887
+ if (blockedBy) {
888
+ lines.push(` Blocked by: ${blockedBy}`);
889
+ }
890
+ lines.push("");
891
+ lines.push(` \u2500\u2500 Progress: ${progress.completed}/${progress.total} (${progress.percentComplete}%) \u2500\u2500`);
892
+ lines.push("");
893
+ lines.push(` Run /sdd done <task title or number> when done.`);
894
+ lines.push(`\u2570${"\u2500".repeat(55)}\u256F`);
895
+ return { message: lines.join("\n") };
896
+ }
545
897
  // ── Session Management ─────────────────────────────────────────────
546
898
  case "status": {
547
899
  const statusBuilder = sddState.getBuilder();
@@ -557,31 +909,119 @@ Start executing the tasks one by one.`
557
909
  executing: "\u26A1",
558
910
  done: "\u2705"
559
911
  };
912
+ const phaseLabel = {
913
+ questioning: "Questioning",
914
+ spec_review: "Spec Review",
915
+ implementation: "Implementation",
916
+ task_review: "Task Review",
917
+ executing: "Executing",
918
+ done: "Done"
919
+ };
560
920
  const progress = getTaskProgress();
921
+ const sessionElapsed = sddState.getSessionElapsed();
922
+ const phaseElapsed = sddState.getPhaseElapsed();
561
923
  const lines = [
562
- "\u2550\u2550\u2550 SDD Session Status \u2550\u2550\u2550",
924
+ `\u256D\u2500\u2500\u2500 SDD: ${session.title} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E`,
563
925
  "",
564
- `Feature: "${session.title}"`,
565
- `Phase: ${phaseEmoji[session.phase]} ${session.phase}`,
566
- `Questions asked: ${session.questionCount}`
926
+ ` ${phaseEmoji[session.phase]} Phase: ${phaseLabel[session.phase]} \u23F1 ${formatElapsed(phaseElapsed)}`,
927
+ ` \u23F1 Session: ${formatElapsed(sessionElapsed)} | \u275D Questions: ${session.questionCount}`
567
928
  ];
568
929
  if (session.spec) {
569
- lines.push(`Spec: ${session.spec.title} (${session.spec.requirements.length} requirements)`);
570
- lines.push(` Requirements: ${session.spec.requirements.map((r) => r.description).join(", ")}`);
571
- }
572
- if (session.implementation) {
573
- const planPreview = session.implementation.split("\n").slice(0, 3).join(" ");
574
- lines.push(`Implementation: ${planPreview}${session.implementation.length > 100 ? "..." : ""}`);
930
+ lines.push("");
931
+ lines.push(` \u{1F4CB} Spec: ${session.spec.title}`);
932
+ lines.push(` ${session.spec.requirements.length} requirements`);
933
+ const reqs = session.spec.requirements.slice(0, 4);
934
+ for (const r of reqs) {
935
+ lines.push(` \u2022 [${r.priority}] ${r.description.length > 42 ? r.description.slice(0, 41) + "\u2026" : r.description}`);
936
+ }
937
+ if (session.spec.requirements.length > 4) {
938
+ lines.push(` + ${session.spec.requirements.length - 4} more requirements`);
939
+ }
575
940
  }
576
941
  if (progress && progress.total > 0) {
577
- lines.push(`Tasks: ${progress.completed}/${progress.total} (${progress.percent}%)`);
942
+ lines.push("");
943
+ lines.push(renderProgress(progress));
944
+ lines.push(` Task breakdown:`);
945
+ if (progress.inProgress > 0) lines.push(` \u{1F504} ${progress.inProgress} in progress`);
946
+ if (progress.pending > 0) lines.push(` \u23F3 ${progress.pending} pending`);
947
+ if (progress.blocked > 0) lines.push(` \u{1F6AB} ${progress.blocked} blocked`);
948
+ if (progress.failed > 0) lines.push(` \u274C ${progress.failed} failed`);
949
+ if (progress.review > 0) lines.push(` \u{1F441} ${progress.review} in review`);
950
+ const tracker = sddState.getTaskTracker();
951
+ if (tracker) {
952
+ const pending = tracker.getAllNodes({ status: ["pending", "in_progress"] });
953
+ const nextTasks = pending.filter((n) => n.status === "pending" && tracker.canStart(n.id)).slice(0, 3);
954
+ if (nextTasks.length > 0) {
955
+ lines.push("");
956
+ lines.push(` Up next:`);
957
+ nextTasks.forEach((t, i) => {
958
+ lines.push(` ${i + 1}. ${t.title}`);
959
+ });
960
+ }
961
+ }
962
+ lines.push("");
963
+ lines.push(` Commands: /sdd tasks \xB7 /sdd next \xB7 /sdd approve \xB7 /sdd cancel`);
964
+ } else {
965
+ lines.push("");
966
+ lines.push(` Commands: /sdd plan \xB7 /sdd approve \xB7 /sdd cancel`);
578
967
  }
579
- lines.push("", `Session ID: ${session.id}`);
580
- lines.push("Commands: /sdd plan \xB7 /sdd tasks \xB7 /sdd approve \xB7 /sdd cancel");
968
+ lines.push(`\u2570${"\u2500".repeat(56)}\u256F`);
969
+ lines.push("");
970
+ lines.push(` Session ID: ${session.id.slice(0, 8)}\u2026`);
581
971
  return {
582
972
  message: lines.join("\n")
583
973
  };
584
974
  }
975
+ // ── Task Graph Visualization ──────────────────────────────────────
976
+ case "graph": {
977
+ const graphTracker = sddState.getTaskTracker();
978
+ if (!graphTracker) {
979
+ return { message: "No tasks generated yet. Use /sdd new to start." };
980
+ }
981
+ const graphId = sddState.getTaskGraphId();
982
+ if (!graphId) {
983
+ const nodes2 = graphTracker.getAllNodes();
984
+ if (nodes2.length === 0) {
985
+ return { message: "No tasks in the current graph." };
986
+ }
987
+ const progress2 = graphTracker.getProgress();
988
+ const lines2 = [renderProgress(progress2), ""];
989
+ const sorted2 = [...nodes2].sort((a, b) => {
990
+ const order = { in_progress: 0, pending: 1, review: 2, blocked: 3, failed: 4, completed: 5 };
991
+ return (order[a.status] ?? 6) - (order[b.status] ?? 6);
992
+ });
993
+ for (let i = 0; i < sorted2.length; i++) {
994
+ const n = sorted2[i];
995
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
996
+ lines2.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
997
+ }
998
+ return { message: lines2.join("\n") };
999
+ }
1000
+ try {
1001
+ const graphStore2 = new TaskGraphStore({ baseDir: path23.join(projectRoot, ".wrongstack", "task-graphs") });
1002
+ const stored = await graphStore2.load(graphId);
1003
+ if (stored) {
1004
+ return { message: renderTaskGraph(stored, { compact: false }) };
1005
+ }
1006
+ } catch {
1007
+ }
1008
+ const nodes = graphTracker.getAllNodes();
1009
+ if (nodes.length === 0) {
1010
+ return { message: "No tasks in the current graph." };
1011
+ }
1012
+ const progress = graphTracker.getProgress();
1013
+ const lines = [renderProgress(progress), ""];
1014
+ const sorted = [...nodes].sort((a, b) => {
1015
+ const order = { in_progress: 0, pending: 1, review: 2, blocked: 3, failed: 4, completed: 5 };
1016
+ return (order[a.status] ?? 6) - (order[b.status] ?? 6);
1017
+ });
1018
+ for (let i = 0; i < sorted.length; i++) {
1019
+ const n = sorted[i];
1020
+ const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
1021
+ lines.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
1022
+ }
1023
+ return { message: lines.join("\n") };
1024
+ }
585
1025
  case "cancel": {
586
1026
  const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
587
1027
  let deletedFromDisk = false;
@@ -760,6 +1200,73 @@ Available: ${listTemplates().map((t) => t.id).join(", ")}`
760
1200
  ${lines.join("\n")}`
761
1201
  };
762
1202
  }
1203
+ case "critical":
1204
+ case "bottleneck": {
1205
+ const critTracker = sddState.getTaskTracker();
1206
+ if (!critTracker) {
1207
+ return { message: "No tasks generated yet. Use /sdd new to start." };
1208
+ }
1209
+ const graphId = sddState.getTaskGraphId();
1210
+ if (!graphId) {
1211
+ return { message: "No task graph found. Generate tasks first." };
1212
+ }
1213
+ try {
1214
+ const graphStore2 = new TaskGraphStore({ baseDir: path23.join(projectRoot, ".wrongstack", "task-graphs") });
1215
+ const graph = await graphStore2.load(graphId);
1216
+ if (!graph) {
1217
+ return { message: "Could not load task graph." };
1218
+ }
1219
+ const analysis = analyzeCriticalPath(graph);
1220
+ const lines = [
1221
+ `\u256D\u2500\u2500\u2500 Critical Path Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E`,
1222
+ "",
1223
+ ` Critical path length: ${analysis.criticalPath.length} tasks`,
1224
+ ` Estimated total time: ${analysis.totalHours}h`,
1225
+ ""
1226
+ ];
1227
+ if (analysis.criticalPath.length > 0) {
1228
+ lines.push(` \u{1F534} Critical path:`);
1229
+ analysis.criticalPath.forEach((taskId, i) => {
1230
+ const node = graph.nodes.get(taskId);
1231
+ if (node) {
1232
+ lines.push(` ${i + 1}. ${node.title} [${node.priority}] \u2014 ${node.estimateHours}h`);
1233
+ }
1234
+ });
1235
+ }
1236
+ if (analysis.bottlenecks.length > 0) {
1237
+ lines.push("");
1238
+ lines.push(` \u{1F6AB} Bottlenecks (blocking most downstream):`);
1239
+ analysis.bottlenecks.forEach((bt) => {
1240
+ const node = graph.nodes.get(bt.taskId);
1241
+ if (node) {
1242
+ lines.push(` \u2022 ${node.title} (blocks ${bt.blockedCount} task(s))`);
1243
+ }
1244
+ });
1245
+ }
1246
+ if (analysis.parallelGroups.length > 0) {
1247
+ lines.push("");
1248
+ lines.push(` \u26A1 Parallel groups (can run concurrently):`);
1249
+ analysis.parallelGroups.forEach((group, i) => {
1250
+ const names = group.map((id) => graph.nodes.get(id)?.title ?? "?").join(" | ");
1251
+ lines.push(` Group ${i + 1}: ${names}`);
1252
+ });
1253
+ }
1254
+ if (analysis.readyTasks.length > 0) {
1255
+ lines.push("");
1256
+ lines.push(` \u2705 Ready to start now:`);
1257
+ analysis.readyTasks.forEach((taskId) => {
1258
+ const node = graph.nodes.get(taskId);
1259
+ if (node) {
1260
+ lines.push(` \u2022 ${node.title}`);
1261
+ }
1262
+ });
1263
+ }
1264
+ lines.push(`\u2570${"\u2500".repeat(55)}\u256F`);
1265
+ return { message: lines.join("\n") };
1266
+ } catch {
1267
+ return { message: "Could not analyze critical path." };
1268
+ }
1269
+ }
763
1270
  default:
764
1271
  return {
765
1272
  message: `Unknown command "${verb}".
@@ -788,45 +1295,67 @@ function sddHelp() {
788
1295
  " \u2502 /sdd spec Show current session's spec \u2502",
789
1296
  " \u2502 /sdd plan Show implementation plan \u2502",
790
1297
  " \u2502 /sdd execute Execute generated tasks \u2502",
791
- " \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",
1298
+ " \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\u2518",
792
1299
  "",
793
- " \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",
794
- " \u2502 /sdd tasks Show current task list \u2502",
795
- " \u2502 /sdd done <N> Mark task complete (by # or name) \u2502",
796
- " \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",
1300
+ " \u250C\u2500 \u23F8 Goal Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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",
1301
+ " \u2502 /sdd goal Show current goal + journal \u2502",
1302
+ " \u2502 /sdd goal set <text> Set autonomous mission \u2502",
1303
+ " \u2502 /sdd goal pause Pause at end of current iteration \u2502",
1304
+ " \u2502 /sdd goal resume Resume a paused goal \u2502",
1305
+ " \u2502 /sdd goal journal [N] Show recent journal entries \u2502",
1306
+ " \u2502 /sdd goal clear Clear goal + stop eternal mode \u2502",
1307
+ " \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\u2518",
1308
+ "",
1309
+ " \u250C\u2500 \u{1F4E1} Eternal Stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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",
1310
+ " \u2502 decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped \u2502",
1311
+ " \u2502 Stage shown in real-time during /sdd goal mode \u2502",
1312
+ " \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\u2518",
1313
+ "",
1314
+ " \u250C\u2500 \u{1F527} Task Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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",
1315
+ " \u2502 /sdd tasks Show task list + progress bar \u2502",
1316
+ " \u2502 /sdd next Show next executable task \u2502",
1317
+ " \u2502 /sdd done <N> Complete a task \u2502",
1318
+ " \u2502 /sdd skip <N> Skip a task (back to pending) \u2502",
1319
+ " \u2502 /sdd fail <N> Mark task as failed \u2502",
1320
+ " \u2502 /sdd review <N> Send task to review \u2502",
1321
+ " \u2502 /sdd edit <N> <txt> Edit task title or description \u2502",
1322
+ " \u2502 /sdd undo Undo last completion \u2502",
1323
+ " \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\u2518",
797
1324
  "",
798
- " \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",
799
- " \u2502 /sdd status Show session status \u2502",
1325
+ " \u250C\u2500 \u{1F4CA} Session 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\u2510",
1326
+ " \u2502 /sdd status Full session status + tasks preview \u2502",
800
1327
  " \u2502 /sdd cancel Cancel session \u2502",
801
- " \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",
1328
+ " \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\u2518",
802
1329
  "",
803
- " \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",
1330
+ " \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\u2510",
804
1331
  " \u2502 /sdd list List saved specs \u2502",
805
1332
  " \u2502 /sdd show <id> Show spec details \u2502",
806
1333
  " \u2502 /sdd templates List available templates \u2502",
807
1334
  " \u2502 /sdd from <tmpl> Create from template \u2502",
808
1335
  " \u2502 /sdd version <id> Show version history \u2502",
809
- " \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",
1336
+ " \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\u2518",
810
1337
  "",
811
- " \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",
812
- " \u2502 \u2502",
813
- " \u2502 1. /sdd new Auth System \u2502",
814
- " \u2502 \u2192 AI starts asking questions \u2502",
815
- " \u2502 \u2502",
816
- " \u2502 2. Just type your answers naturally \u2502",
817
- " \u2502 \u2192 AI continues the interview \u2502",
818
- " \u2502 \u2502",
819
- " \u2502 3. AI generates spec (auto-detected) \u2502",
820
- " \u2502 \u2192 /sdd approve \u2502",
821
- " \u2502 \u2502",
822
- " \u2502 3. AI generates implementation + tasks \u2502",
823
- " \u2502 \u2192 /sdd approve \u2502",
824
- " \u2502 \u2502",
825
- " \u2502 4. AI executes tasks one by one \u2502",
826
- " \u2502 \u2192 /sdd tasks (view progress) \u2502",
827
- " \u2502 \u2192 /sdd done 1 (manual completion) \u2502",
828
- " \u2502 \u2502",
829
- " \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",
1338
+ " \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\u2510",
1339
+ " \u2502 \u2502",
1340
+ " \u2502 1. /sdd new Auth System \u2502",
1341
+ " \u2502 \u2192 AI starts asking questions \u2502",
1342
+ " \u2502 \u2502",
1343
+ " \u2502 2. Just type your answers naturally \u2502",
1344
+ " \u2502 \u2192 AI continues the interview \u2502",
1345
+ " \u2502 \u2502",
1346
+ " \u2502 3. AI generates spec (auto-detected) \u2502",
1347
+ " \u2502 \u2192 /sdd approve \u2502",
1348
+ " \u2502 \u2502",
1349
+ " \u2502 3. AI generates implementation + tasks \u2502",
1350
+ " \u2502 \u2192 /sdd approve \u2502",
1351
+ " \u2502 \u2502",
1352
+ " \u2502 4. AI executes tasks one by one \u2502",
1353
+ " \u2502 \u2192 /sdd tasks (view progress) \u2502",
1354
+ " \u2502 \u2192 /sdd done 1 (mark task complete) \u2502",
1355
+ " \u2502 \u2502",
1356
+ " \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\u2518",
1357
+ "",
1358
+ " Tip: tasks are shown with progress bar after each AI turn.",
830
1359
  ""
831
1360
  ].join("\n");
832
1361
  }
@@ -885,6 +1414,9 @@ var init_sdd = __esm({
885
1414
  taskStore = null;
886
1415
  taskTracker = null;
887
1416
  taskGraphId = null;
1417
+ sessionStartTime = Date.now();
1418
+ phaseStartTime = Date.now();
1419
+ versioning = null;
888
1420
  getBuilder() {
889
1421
  return this.builder;
890
1422
  }
@@ -909,6 +1441,27 @@ var init_sdd = __esm({
909
1441
  setTaskGraphId(id) {
910
1442
  this.taskGraphId = id;
911
1443
  }
1444
+ getSessionStartTime() {
1445
+ return this.sessionStartTime;
1446
+ }
1447
+ setSessionStartTime(t) {
1448
+ this.sessionStartTime = t;
1449
+ }
1450
+ setPhaseStartTime(t) {
1451
+ this.phaseStartTime = t;
1452
+ }
1453
+ getPhaseStartTime() {
1454
+ return this.phaseStartTime;
1455
+ }
1456
+ getSessionElapsed() {
1457
+ return Date.now() - this.sessionStartTime;
1458
+ }
1459
+ getPhaseElapsed() {
1460
+ return Date.now() - this.phaseStartTime;
1461
+ }
1462
+ getVersioning() {
1463
+ return this.versioning ?? (this.versioning = new SpecVersioning());
1464
+ }
912
1465
  clearTaskState() {
913
1466
  this.taskStore = null;
914
1467
  this.taskTracker = null;
@@ -4262,7 +4815,7 @@ function buildAutonomyCommand(opts) {
4262
4815
  " /autonomy off Disabled \u2014 agent stops after each turn (default)",
4263
4816
  " /autonomy suggest Show next-step suggestions after each turn",
4264
4817
  " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
4265
- " /autonomy eternal Sittin-sene mode \u2014 runs forever against /goal",
4818
+ " /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
4266
4819
  " /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
4267
4820
  " /autonomy stop Stop eternal mode (no-op for other modes)",
4268
4821
  " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
@@ -4278,6 +4831,9 @@ function buildAutonomyCommand(opts) {
4278
4831
  " spawns N agents, awaits results, aggregates. Requires /goal.",
4279
4832
  " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
4280
4833
  "",
4834
+ "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
4835
+ "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
4836
+ "",
4281
4837
  "In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
4282
4838
  "Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
4283
4839
  ].join("\n"),
@@ -4294,7 +4850,7 @@ function buildAutonomyCommand(opts) {
4294
4850
  off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
4295
4851
  suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
4296
4852
  auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
4297
- eternal: `${color.red("ETERNAL")} ${color.dim("(sittin-sene \u2014 goal-driven, YOLO, until /autonomy stop)")}`,
4853
+ eternal: `${color.red("ETERNAL")} ${color.dim("(goal-driven loop \u2014 YOLO, until /autonomy stop)")}`,
4298
4854
  "eternal-parallel": `${color.magenta("PARALLEL")} ${color.dim("(4-8 subagents per tick \u2014 fan-out, until /autonomy stop)")}`
4299
4855
  };
4300
4856
  const lines = [`Autonomy mode: ${labels2[current] ?? current}`];
@@ -4426,7 +4982,9 @@ var KNOWN_VERBS = /* @__PURE__ */ new Set([
4426
4982
  "clear",
4427
4983
  "reset",
4428
4984
  "journal",
4429
- "log"
4985
+ "log",
4986
+ "pause",
4987
+ "resume"
4430
4988
  ]);
4431
4989
  function buildGoalCommand(opts) {
4432
4990
  return {
@@ -4437,9 +4995,14 @@ function buildGoalCommand(opts) {
4437
4995
  " /goal Show current goal + recent journal",
4438
4996
  " /goal set <text> Set a new goal (overwrites previous)",
4439
4997
  " /goal clear Clear the goal (stops eternal mode if running)",
4998
+ " /goal pause Pause at end of current iteration (no-op if already paused)",
4999
+ " /goal resume Resume a paused goal (no-op if not paused)",
4440
5000
  " /goal status Same as /goal (alias)",
4441
5001
  " /goal journal [N] Show last N journal entries (default 25)",
4442
5002
  "",
5003
+ "Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
5004
+ "Pausing stops after current iteration completes. Resume continues from next iteration.",
5005
+ "",
4443
5006
  "Goals live in <projectRoot>/.wrongstack/goal.json and persist across sessions.",
4444
5007
  "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
4445
5008
  "every iteration to decide what to do next."
@@ -4528,6 +5091,42 @@ ${lines.join("\n")}`;
4528
5091
  opts.renderer.write(msg);
4529
5092
  return { message: msg };
4530
5093
  }
5094
+ case "pause": {
5095
+ const current = await loadGoal(goalPath);
5096
+ if (!current) {
5097
+ const msg2 = "No goal set \u2014 nothing to pause.";
5098
+ opts.renderer.writeWarning(msg2);
5099
+ return { message: msg2 };
5100
+ }
5101
+ if (current.goalState === "paused") {
5102
+ const msg2 = `${color.dim("Already paused.")} Use /goal resume to continue.`;
5103
+ opts.renderer.write(msg2);
5104
+ return { message: msg2 };
5105
+ }
5106
+ const paused = { ...current, goalState: "paused" };
5107
+ await saveGoal(goalPath, paused);
5108
+ const msg = `${color.cyan("Goal paused.")} Current iteration will finish, then the loop stops. Use /goal resume to continue.`;
5109
+ opts.renderer.write(msg);
5110
+ return { message: msg };
5111
+ }
5112
+ case "resume": {
5113
+ const current = await loadGoal(goalPath);
5114
+ if (!current) {
5115
+ const msg2 = "No goal set \u2014 cannot resume.";
5116
+ opts.renderer.writeWarning(msg2);
5117
+ return { message: msg2 };
5118
+ }
5119
+ if (current.goalState !== "paused") {
5120
+ const msg2 = `${color.dim("Not paused.")} Use /goal set <text> to create or update a goal first.`;
5121
+ opts.renderer.writeWarning(msg2);
5122
+ return { message: msg2 };
5123
+ }
5124
+ const resumed = { ...current, goalState: "active" };
5125
+ await saveGoal(goalPath, resumed);
5126
+ const msg = `${color.green("Goal resumed.")} Loop will continue from the next iteration.`;
5127
+ opts.renderer.write(msg);
5128
+ return { message: msg };
5129
+ }
4531
5130
  default: {
4532
5131
  const msg = `Unknown subcommand "${verb}". Try: show | set <text> | clear | journal [N]`;
4533
5132
  opts.renderer.writeWarning(msg);
@@ -8787,9 +9386,22 @@ ${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to ex
8787
9386
  if (progress) {
8788
9387
  opts.renderer.write(
8789
9388
  `
8790
- ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
9389
+ ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percentComplete}%)`)}
8791
9390
  `
8792
9391
  );
9392
+ const taskList2 = renderTaskListWithProgress();
9393
+ if (taskList2) {
9394
+ opts.renderer.write(`
9395
+ ${color.dim(taskList2)}
9396
+ `);
9397
+ }
9398
+ }
9399
+ } else {
9400
+ const taskList2 = renderTaskListWithProgress();
9401
+ if (taskList2) {
9402
+ opts.renderer.write(`
9403
+ ${color.dim(taskList2)}
9404
+ `);
8793
9405
  }
8794
9406
  }
8795
9407
  }
@@ -8818,6 +9430,14 @@ ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${prog
8818
9430
  if (sddContext) {
8819
9431
  sddPrefix = `[SDD SESSION ACTIVE]
8820
9432
  ${sddContext}`;
9433
+ if (sddPhase === "executing") {
9434
+ const currentCtx = getCurrentExecutingContext();
9435
+ if (currentCtx) {
9436
+ sddPrefix += `
9437
+
9438
+ ${currentCtx}`;
9439
+ }
9440
+ }
8821
9441
  if (taskList) {
8822
9442
  sddPrefix += `
8823
9443
 
@@ -8826,9 +9446,9 @@ ${taskList}`;
8826
9446
  }
8827
9447
  if (taskProgress && taskProgress.total > 0) {
8828
9448
  sddPrefix += `
8829
- **Progress:** ${taskProgress.completed}/${taskProgress.total} (${taskProgress.percent}%)`;
9449
+ **Progress:** ${taskProgress.completed}/${taskProgress.total} (${taskProgress.percentComplete}%)`;
8830
9450
  }
8831
- if (sddPhase === "executing" && taskProgress && taskProgress.percent === 100) {
9451
+ if (sddPhase === "executing" && taskProgress && taskProgress.percentComplete === 100) {
8832
9452
  sddPrefix += "\n\n**All tasks completed! Provide a summary of everything implemented.**";
8833
9453
  }
8834
9454
  sddPrefix += "\n\n---\nUser message:\n";
@@ -8908,10 +9528,10 @@ ${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to ex
8908
9528
  if (progress) {
8909
9529
  opts.renderer.write(
8910
9530
  `
8911
- ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
9531
+ ${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percentComplete}%)`)}
8912
9532
  `
8913
9533
  );
8914
- if (progress.percent === 100) {
9534
+ if (progress.percentComplete === 100) {
8915
9535
  opts.renderer.write(
8916
9536
  `
8917
9537
  ${color.green(" \u{1F389} All tasks completed! Use /sdd cancel to end the session.")}
@@ -8919,6 +9539,20 @@ ${color.green(" \u{1F389} All tasks completed! Use /sdd cancel to end the sessi
8919
9539
  );
8920
9540
  }
8921
9541
  }
9542
+ advanceToNextTask();
9543
+ const taskList2 = renderTaskListWithProgress();
9544
+ if (taskList2) {
9545
+ opts.renderer.write(`
9546
+ ${color.dim(taskList2)}
9547
+ `);
9548
+ }
9549
+ } else {
9550
+ const taskList2 = renderTaskListWithProgress();
9551
+ if (taskList2) {
9552
+ opts.renderer.write(`
9553
+ ${color.dim(taskList2)}
9554
+ `);
9555
+ }
8922
9556
  }
8923
9557
  }
8924
9558
  }
@@ -9079,10 +9713,10 @@ var EMPTY = "\u2591";
9079
9713
  function renderContextChip(used, max) {
9080
9714
  const ratio = Math.max(0, Math.min(1, used / max));
9081
9715
  const pct2 = Math.round(ratio * 100);
9082
- const bar = renderProgress(ratio, 6);
9716
+ const bar = renderProgress2(ratio, 6);
9083
9717
  return `${bar} ${pct2}% (${fmtTok(used)}/${fmtTok(max)})`;
9084
9718
  }
9085
- function renderProgress(ratio, width) {
9719
+ function renderProgress2(ratio, width) {
9086
9720
  const clamped = Math.max(0, Math.min(1, ratio));
9087
9721
  const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
9088
9722
  const capped = Math.min(width, filled);
@@ -9140,6 +9774,7 @@ async function execute(deps) {
9140
9774
  getEternalEngine,
9141
9775
  getParallelEngine,
9142
9776
  subscribeEternalIteration,
9777
+ subscribeEternalStage,
9143
9778
  skillLoader
9144
9779
  } = deps;
9145
9780
  let code = 0;
@@ -9250,12 +9885,17 @@ async function execute(deps) {
9250
9885
  getAutonomy,
9251
9886
  getEternalEngine,
9252
9887
  subscribeEternalIteration,
9888
+ subscribeEternalStage,
9253
9889
  appVersion: CLI_VERSION,
9254
9890
  provider: config.provider,
9255
9891
  family: banneredFamily,
9256
9892
  keyTail: banneredKeyTail,
9257
9893
  getPickableProviders,
9258
9894
  switchProviderAndModel,
9895
+ switchAutonomy: (mode) => {
9896
+ onAutonomy?.(mode);
9897
+ return null;
9898
+ },
9259
9899
  effectiveMaxContext,
9260
9900
  // Default OFF so the terminal's native scrollback works for chat
9261
9901
  // history out of the box (mouse wheel / Shift+PgUp). Users who hit
@@ -9302,7 +9942,7 @@ async function execute(deps) {
9302
9942
  if (autoCompleted > 0) {
9303
9943
  const progress = getTaskProgress2();
9304
9944
  if (progress) {
9305
- messages.push(`\u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
9945
+ messages.push(`\u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percentComplete}%)`);
9306
9946
  }
9307
9947
  }
9308
9948
  }
@@ -10117,10 +10757,10 @@ function renderContextChip2(ctx) {
10117
10757
  const ratio = Math.max(0, Math.min(1, ctx.used / ctx.max));
10118
10758
  const pct2 = Math.round(ratio * 100);
10119
10759
  const chipColor = ratio >= 0.85 ? color.red : ratio >= 0.65 ? color.yellow : color.cyan;
10120
- const bar = renderProgress2(ratio, 8);
10760
+ const bar = renderProgress3(ratio, 8);
10121
10761
  return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct2}%`) + color.dim(` (${fmtTok(ctx.used)}/${fmtTok(ctx.max)})`);
10122
10762
  }
10123
- function renderProgress2(ratio, width) {
10763
+ function renderProgress3(ratio, width) {
10124
10764
  const clamped = Math.max(0, Math.min(1, ratio));
10125
10765
  const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
10126
10766
  const capped = Math.min(width, filled);
@@ -10483,6 +11123,7 @@ function resolveBundledSkillsDir2() {
10483
11123
  return void 0;
10484
11124
  }
10485
11125
  }
11126
+ var stageListeners = /* @__PURE__ */ new Set();
10486
11127
  async function main(argv) {
10487
11128
  const ctx = await boot(argv);
10488
11129
  if (typeof ctx === "number") return ctx;
@@ -11488,6 +12129,10 @@ Restart WrongStack to load or unload plugin code in this session.`;
11488
12129
  eternalListeners.add(fn);
11489
12130
  return () => eternalListeners.delete(fn);
11490
12131
  },
12132
+ subscribeEternalStage: (fn) => {
12133
+ stageListeners.add(fn);
12134
+ return () => stageListeners.delete(fn);
12135
+ },
11491
12136
  skillLoader: config.features.skills ? skillLoader : void 0
11492
12137
  });
11493
12138
  }