@wizdear/atlas-code 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/agent-factory.d.ts +8 -1
  2. package/dist/agent-factory.d.ts.map +1 -1
  3. package/dist/agent-factory.js +42 -2
  4. package/dist/agent-factory.js.map +1 -1
  5. package/dist/cli.d.ts +7 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +8 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/discovery.d.ts +9 -0
  10. package/dist/discovery.d.ts.map +1 -1
  11. package/dist/discovery.js +4 -4
  12. package/dist/discovery.js.map +1 -1
  13. package/dist/extension.d.ts +9 -2
  14. package/dist/extension.d.ts.map +1 -1
  15. package/dist/extension.js +1096 -333
  16. package/dist/extension.js.map +1 -1
  17. package/dist/gate.d.ts +1 -1
  18. package/dist/gate.d.ts.map +1 -1
  19. package/dist/gate.js.map +1 -1
  20. package/dist/orchestrator.d.ts +0 -2
  21. package/dist/orchestrator.d.ts.map +1 -1
  22. package/dist/orchestrator.js +0 -1
  23. package/dist/orchestrator.js.map +1 -1
  24. package/dist/pipeline-editor.d.ts +2 -0
  25. package/dist/pipeline-editor.d.ts.map +1 -1
  26. package/dist/pipeline-editor.js +36 -5
  27. package/dist/pipeline-editor.js.map +1 -1
  28. package/dist/pipeline.d.ts +2 -5
  29. package/dist/pipeline.d.ts.map +1 -1
  30. package/dist/pipeline.js +4 -3
  31. package/dist/pipeline.js.map +1 -1
  32. package/dist/planner.d.ts +9 -0
  33. package/dist/planner.d.ts.map +1 -1
  34. package/dist/planner.js +20 -10
  35. package/dist/planner.js.map +1 -1
  36. package/dist/roles/architect.d.ts +1 -1
  37. package/dist/roles/architect.d.ts.map +1 -1
  38. package/dist/roles/architect.js +1 -1
  39. package/dist/roles/architect.js.map +1 -1
  40. package/dist/roles/documenter.d.ts +1 -1
  41. package/dist/roles/documenter.d.ts.map +1 -1
  42. package/dist/roles/documenter.js +11 -0
  43. package/dist/roles/documenter.js.map +1 -1
  44. package/dist/roles/index.d.ts +1 -0
  45. package/dist/roles/index.d.ts.map +1 -1
  46. package/dist/roles/index.js +3 -0
  47. package/dist/roles/index.js.map +1 -1
  48. package/dist/roles/recover.d.ts +5 -0
  49. package/dist/roles/recover.d.ts.map +1 -0
  50. package/dist/roles/recover.js +82 -0
  51. package/dist/roles/recover.js.map +1 -0
  52. package/dist/router.d.ts.map +1 -1
  53. package/dist/router.js +6 -6
  54. package/dist/router.js.map +1 -1
  55. package/dist/standards.d.ts.map +1 -1
  56. package/dist/standards.js +1 -0
  57. package/dist/standards.js.map +1 -1
  58. package/dist/step-executor.d.ts +2 -0
  59. package/dist/step-executor.d.ts.map +1 -1
  60. package/dist/step-executor.js +16 -4
  61. package/dist/step-executor.js.map +1 -1
  62. package/dist/store.d.ts +3 -0
  63. package/dist/store.d.ts.map +1 -1
  64. package/dist/store.js +48 -19
  65. package/dist/store.js.map +1 -1
  66. package/dist/system-architect.d.ts +9 -0
  67. package/dist/system-architect.d.ts.map +1 -1
  68. package/dist/system-architect.js +11 -9
  69. package/dist/system-architect.js.map +1 -1
  70. package/dist/telegram/bridge.d.ts +39 -0
  71. package/dist/telegram/bridge.d.ts.map +1 -0
  72. package/dist/telegram/bridge.js +380 -0
  73. package/dist/telegram/bridge.js.map +1 -0
  74. package/dist/telegram/formatter.d.ts +15 -0
  75. package/dist/telegram/formatter.d.ts.map +1 -0
  76. package/dist/telegram/formatter.js +86 -0
  77. package/dist/telegram/formatter.js.map +1 -0
  78. package/dist/telegram/renderer.d.ts +45 -0
  79. package/dist/telegram/renderer.d.ts.map +1 -0
  80. package/dist/telegram/renderer.js +150 -0
  81. package/dist/telegram/renderer.js.map +1 -0
  82. package/dist/telegram/telegram-api.d.ts +84 -0
  83. package/dist/telegram/telegram-api.d.ts.map +1 -0
  84. package/dist/telegram/telegram-api.js +134 -0
  85. package/dist/telegram/telegram-api.js.map +1 -0
  86. package/dist/types.d.ts +10 -1
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/ui.d.ts +1 -1
  90. package/dist/ui.d.ts.map +1 -1
  91. package/dist/ui.js +2 -0
  92. package/dist/ui.js.map +1 -1
  93. package/package.json +1 -1
package/dist/extension.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, appendFile, mkdir, readdir, stat, writeFile } from "node:fs/promises";
3
- import { basename, join, relative } from "node:path";
2
+ import { access, appendFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, relative } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { promisify } from "node:util";
5
6
  import { getMarkdownTheme, ToolExecutionComponent, } from "@mariozechner/pi-coding-agent";
6
7
  import { Box, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
7
8
  import { Type } from "@sinclair/typebox";
8
9
  import { createRoleAgent, runAgent } from "./agent-factory.js";
9
10
  import { parseVibeCommand, STANDARD_TEMPLATES, VIBE_SUBCOMMANDS } from "./cli.js";
10
- import { extractQuestions, runDiscovery } from "./discovery.js";
11
+ import { runDiscovery } from "./discovery.js";
11
12
  import { formatLogLine } from "./logger.js";
12
13
  import { runOrchestration } from "./orchestrator.js";
13
14
  import { PipelineRunner } from "./pipeline.js";
@@ -17,10 +18,11 @@ import { RetryManager } from "./retry.js";
17
18
  import { getSystemPromptForRole } from "./roles/index.js";
18
19
  import { createPipeline, inferWorkflowType, resolveFeatureWorkflowType } from "./router.js";
19
20
  import { enrichStandards } from "./standards-enricher.js";
20
- import { extractArtifactContent } from "./step-executor.js";
21
+ import { aggregateUsage, extractArtifactContent } from "./step-executor.js";
21
22
  import { VibeStore } from "./store.js";
22
23
  import { runSystemArchitect } from "./system-architect.js";
23
24
  import { analyzeSystemDesignImpact, loadAllFeatureDesigns } from "./system-design-impact.js";
25
+ import { createTelegramBridgeManager } from "./telegram/bridge.js";
24
26
  import { findUnmappedRequirements } from "./traceability.js";
25
27
  import { formatFeatureDetail, formatPipelineStatus, formatVibeEvent, VIBE_HELP } from "./ui.js";
26
28
  // ─── Chat Event Filter ──────────────────────────────────────────────────────
@@ -119,16 +121,21 @@ async function getFileMtime(filePath) {
119
121
  return null;
120
122
  }
121
123
  }
122
- // ─── QMD Cleanup ─────────────────────────────────────────────────────────────
123
- /** Remove stale QMD collections if qmd is installed. Silently skips if qmd is not available. */
124
- async function cleanupQmdCollections(cwd) {
124
+ // ─── QMD Lifecycle ───────────────────────────────────────────────────────────
125
+ /** Check if qmd CLI is available on the system. */
126
+ async function isQmdInstalled() {
125
127
  try {
126
128
  await execFileAsync("which", ["qmd"]);
129
+ return true;
127
130
  }
128
131
  catch {
129
- // qmd not installed — skip silently
130
- return;
132
+ return false;
131
133
  }
134
+ }
135
+ /** Remove stale QMD collections if qmd is installed. Silently skips if qmd is not available. */
136
+ async function cleanupQmdCollections(cwd) {
137
+ if (!(await isQmdInstalled()))
138
+ return;
132
139
  const indexName = basename(cwd);
133
140
  const collections = ["vibe-artifacts", "vibe-standards"];
134
141
  for (const col of collections) {
@@ -143,6 +150,62 @@ async function cleanupQmdCollections(cwd) {
143
150
  }
144
151
  }
145
152
  }
153
+ /** Initialize QMD collections for .vibe/features/ and .vibe/standards/. Idempotent. */
154
+ async function initQmdCollections(cwd) {
155
+ if (!(await isQmdInstalled()))
156
+ return;
157
+ const indexName = basename(cwd);
158
+ const env = { ...process.env, QMD_EMBED_MODEL: QMD_EMBED_MODEL_URI };
159
+ const featuresDir = join(cwd, ".vibe", "features");
160
+ const standardsDir = join(cwd, ".vibe", "standards");
161
+ try {
162
+ // Check if collections already exist
163
+ const { stdout } = await execFileAsync("qmd", ["--index", indexName, "collection", "list"], { cwd });
164
+ if (stdout.includes("vibe-artifacts") && stdout.includes("vibe-standards"))
165
+ return;
166
+ }
167
+ catch {
168
+ // No collections yet — proceed with init
169
+ }
170
+ try {
171
+ await execFileAsync("qmd", ["--index", indexName, "collection", "add", featuresDir, "--name", "vibe-artifacts", "--mask", "**/*.md"], { cwd });
172
+ await execFileAsync("qmd", [
173
+ "--index",
174
+ indexName,
175
+ "context",
176
+ "add",
177
+ "qmd://vibe-artifacts",
178
+ "Feature artifacts: spec, design, review, test-report, diagnosis, impact-report",
179
+ ], { cwd });
180
+ await execFileAsync("qmd", ["--index", indexName, "collection", "add", standardsDir, "--name", "vibe-standards", "--mask", "**/*.md"], { cwd });
181
+ await execFileAsync("qmd", [
182
+ "--index",
183
+ indexName,
184
+ "context",
185
+ "add",
186
+ "qmd://vibe-standards",
187
+ "Project coding standards, architecture principles, testing policies",
188
+ ], { cwd });
189
+ await execFileAsync("qmd", ["--index", indexName, "embed"], { cwd, env });
190
+ }
191
+ catch {
192
+ // Silent failure — QMD is optional
193
+ }
194
+ }
195
+ /** Update QMD index with new/changed artifacts and regenerate embeddings. */
196
+ async function updateQmdIndex(cwd) {
197
+ if (!(await isQmdInstalled()))
198
+ return;
199
+ const indexName = basename(cwd);
200
+ const env = { ...process.env, QMD_EMBED_MODEL: QMD_EMBED_MODEL_URI };
201
+ try {
202
+ await execFileAsync("qmd", ["--index", indexName, "update"], { cwd });
203
+ await execFileAsync("qmd", ["--index", indexName, "embed"], { cwd, env });
204
+ }
205
+ catch {
206
+ // Silent failure — QMD is optional
207
+ }
208
+ }
146
209
  /** Minimal TUI stub for ToolExecutionComponent in renderer context. */
147
210
  const rendererTui = { requestRender: () => { } };
148
211
  /** Normalize agent tool result to ToolExecutionComponent format. */
@@ -182,6 +245,7 @@ function sendStepResultMessage(event, pipelineEditor, totalSteps) {
182
245
  const activityLog = pipelineEditor ? [...pipelineEditor.getActivityLog()] : [];
183
246
  const rawUsage = event.data?.usage;
184
247
  const usage = rawUsage && rawUsage.totalTokens > 0 ? rawUsage : undefined;
248
+ const responseText = event.data?.responseText;
185
249
  const content = failed
186
250
  ? `Step ${stepIndex + 1}: ${role} — ${event.step.action} (failed)`
187
251
  : `Step ${stepIndex + 1}: ${role} — ${event.step.action}`;
@@ -201,8 +265,17 @@ function sendStepResultMessage(event, pipelineEditor, totalSteps) {
201
265
  failed,
202
266
  error,
203
267
  usage,
268
+ responseText,
204
269
  },
205
270
  });
271
+ // Send agent response as a separate chat message so LLM output is always visible.
272
+ // Per-turn agent-text messages (from text_end events) may not fire when the model
273
+ // uses thinking blocks + tool calls without producing text content blocks.
274
+ // This ensures the final response text is always available in chat.
275
+ if (responseText) {
276
+ const lines = responseText.split("\n").length;
277
+ vibeAgentResponse(`${role} response (${lines} lines)`, responseText, usage);
278
+ }
206
279
  }
207
280
  // ─── QMD Skill Template ──────────────────────────────────────────────────────
208
281
  const QMD_EMBED_MODEL_URI = "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf";
@@ -291,6 +364,9 @@ qmd --index "$INDEX_NAME" embed
291
364
  // ─── Session Message Helper ──────────────────────────────────────────────────
292
365
  /** Extension API reference. Set by the vibeExtension factory. */
293
366
  let _pi;
367
+ /** Shared Telegram command registry. Commands are pushed here during factory setup
368
+ * and read by the bridge polling loop (which starts later during session_start). */
369
+ const _telegramCommands = [];
294
370
  // ─── Agent Response Preview ──────────────────────────────────────────────────
295
371
  /** Number of preview lines shown when agent-response is collapsed. */
296
372
  export const AGENT_RESPONSE_PREVIEW_LINES = 5;
@@ -307,13 +383,17 @@ export function computeAgentResponsePreview(fullText, maxLines = AGENT_RESPONSE_
307
383
  return { preview, remaining: allLines.length - maxLines };
308
384
  }
309
385
  /** Sends a categorized vibe message to the session. */
310
- function sendVibeMessage(content, category, fullText) {
386
+ function sendVibeMessage(content, category, fullText, usage) {
311
387
  _pi.sendMessage({
312
388
  customType: "vibe",
313
389
  content,
314
390
  display: true,
315
- details: { category, fullText },
391
+ details: { category, fullText, usage },
316
392
  });
393
+ // Also emit via EventBus so the Telegram bridge can receive it.
394
+ // pi.sendMessage() → sendCustomMessage() only fires _emit() (TUI listeners),
395
+ // not _emitExtensionEvent(), so pi.on("message_end") never fires for custom messages.
396
+ _pi.events?.emit("vibe:message", { content, category, fullText, usage });
317
397
  }
318
398
  function vibeCommand(text) {
319
399
  sendVibeMessage(text, "command");
@@ -333,8 +413,23 @@ function vibeGate(text) {
333
413
  function vibeEvent(text) {
334
414
  sendVibeMessage(text, "event");
335
415
  }
336
- function vibeAgentResponse(summary, fullText) {
337
- sendVibeMessage(summary, "agent-response", fullText);
416
+ function vibeAgentResponse(summary, fullText, usage) {
417
+ sendVibeMessage(summary, "agent-response", fullText, usage);
418
+ }
419
+ /**
420
+ * Wait for gate choice from TUI select() or Telegram inline keyboard.
421
+ * Whichever responds first wins; the other is dismissed.
422
+ */
423
+ async function waitForGateChoice(events, ui, summary, options) {
424
+ const abortController = new AbortController();
425
+ let telegramChoice;
426
+ const unsubGateResponse = events.on("vibe:gate_response", (raw) => {
427
+ telegramChoice = raw.choice;
428
+ abortController.abort();
429
+ });
430
+ const tuiChoice = await ui.select(summary, options, { signal: abortController.signal });
431
+ unsubGateResponse();
432
+ return tuiChoice ?? telegramChoice;
338
433
  }
339
434
  /**
340
435
  * Forwards only chat-visible events as vibeMessages.
@@ -463,8 +558,42 @@ function formatToolResult(toolName, args, result, isError, projectRoot) {
463
558
  * Creates a handler that displays agent event progress in PipelineEditorComponent (or status bar).
464
559
  * Uses the editor when active; falls back to the status bar otherwise.
465
560
  */
561
+ /** Extracts the last non-empty line from an AssistantMessage's text content. */
562
+ function getLastNonEmptyLine(partial) {
563
+ for (let i = partial.content.length - 1; i >= 0; i--) {
564
+ const c = partial.content[i];
565
+ if (c.type === "text") {
566
+ const lines = c.text.split("\n");
567
+ for (let j = lines.length - 1; j >= 0; j--) {
568
+ const trimmed = lines[j].trim();
569
+ if (trimmed)
570
+ return trimmed;
571
+ }
572
+ }
573
+ }
574
+ return undefined;
575
+ }
576
+ /** Extract the last N non-empty lines from a partial assistant message. */
577
+ function getLastNonEmptyLines(partial, maxLines) {
578
+ const allLines = [];
579
+ for (const c of partial.content) {
580
+ if (c.type === "text") {
581
+ for (const line of c.text.split("\n")) {
582
+ const trimmed = line.trim();
583
+ if (trimmed)
584
+ allLines.push(trimmed);
585
+ }
586
+ }
587
+ }
588
+ if (allLines.length === 0)
589
+ return undefined;
590
+ return allLines.slice(-maxLines).join("\n");
591
+ }
592
+ /** Throttle interval for LLM streaming detail updates (ms). */
593
+ const STREAMING_THROTTLE_MS = 100;
466
594
  function createAgentProgressHandler(ctx, label, pipelineEditor, projectRoot) {
467
595
  const pendingArgs = new Map();
596
+ let lastDetailUpdate = 0;
468
597
  return (event) => {
469
598
  if (event.type === "tool_execution_start") {
470
599
  const args = event.args;
@@ -495,6 +624,57 @@ function createAgentProgressHandler(ctx, label, pipelineEditor, projectRoot) {
495
624
  },
496
625
  });
497
626
  }
627
+ if (event.type === "message_update") {
628
+ const ame = event.assistantMessageEvent;
629
+ const now = Date.now();
630
+ if (ame.type === "text_delta" || ame.type === "text_end") {
631
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "text_end")
632
+ return;
633
+ lastDetailUpdate = now;
634
+ if (pipelineEditor) {
635
+ // Show last N lines in the pipeline editor box
636
+ const recentLines = getLastNonEmptyLines(ame.partial, 6);
637
+ if (recentLines) {
638
+ pipelineEditor.setDetail(`✎ ${recentLines}`);
639
+ }
640
+ }
641
+ else {
642
+ const lastLine = getLastNonEmptyLine(ame.partial);
643
+ if (lastLine) {
644
+ ctx.ui.setStatus("vibe", `${label}: ✎ ${lastLine}`);
645
+ }
646
+ }
647
+ // On text_end, send completed text block to chat
648
+ if (ame.type === "text_end") {
649
+ const completedText = ame.content.trim();
650
+ if (completedText) {
651
+ const lines = completedText.split("\n").length;
652
+ sendVibeMessage(`[${label}] ✎ (${lines} lines)`, "agent-text", completedText);
653
+ }
654
+ }
655
+ }
656
+ if (ame.type === "thinking_delta" || ame.type === "thinking_start") {
657
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "thinking_start")
658
+ return;
659
+ lastDetailUpdate = now;
660
+ if (pipelineEditor) {
661
+ pipelineEditor.setDetail("thinking...");
662
+ }
663
+ else {
664
+ ctx.ui.setStatus("vibe", `${label}: thinking...`);
665
+ }
666
+ }
667
+ if (ame.type === "done") {
668
+ const msg = ame.message;
669
+ if (msg.usage && msg.usage.totalTokens > 0) {
670
+ const fmt = (n) => n.toLocaleString("en-US");
671
+ const tokenInfo = `tokens: ${fmt(msg.usage.input)} in / ${fmt(msg.usage.output)} out`;
672
+ if (pipelineEditor) {
673
+ pipelineEditor.addActivity(tokenInfo);
674
+ }
675
+ }
676
+ }
677
+ }
498
678
  };
499
679
  }
500
680
  function createAbortHandle() {
@@ -585,10 +765,12 @@ function pipelineStepsToInfo(pipeline) {
585
765
  function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, stepContext) {
586
766
  let unsubscribe;
587
767
  const pendingArgs = new Map();
768
+ let lastDetailUpdate = 0;
588
769
  return {
589
770
  onAgentCreated: (agent) => {
590
771
  unsubscribe?.();
591
772
  pendingArgs.clear();
773
+ lastDetailUpdate = 0;
592
774
  if (abortHandle) {
593
775
  abortHandle.currentAgent = agent;
594
776
  }
@@ -598,12 +780,14 @@ function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, st
598
780
  pendingArgs.set(event.toolCallId, args);
599
781
  const detail = formatToolProgress(event.toolName, args, projectRoot);
600
782
  pipelineEditor.addActivity(detail);
783
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: detail, type: "tool" });
601
784
  }
602
785
  if (event.type === "tool_execution_end") {
603
786
  const args = pendingArgs.get(event.toolCallId) ?? {};
604
787
  pendingArgs.delete(event.toolCallId);
605
788
  const summary = formatToolResult(event.toolName, args, event.result, event.isError, projectRoot);
606
789
  pipelineEditor.addActivity(summary);
790
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: summary, type: "tool_result" });
607
791
  // Send tool execution to chat for ToolExecutionComponent rendering
608
792
  if (stepContext) {
609
793
  const label = formatToolProgress(event.toolName, args, projectRoot);
@@ -622,6 +806,46 @@ function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, st
622
806
  });
623
807
  }
624
808
  }
809
+ if (event.type === "message_update") {
810
+ const ame = event.assistantMessageEvent;
811
+ const now = Date.now();
812
+ if (ame.type === "thinking_start") {
813
+ pipelineEditor.addActivity("thinking...");
814
+ }
815
+ if (ame.type === "text_delta" || ame.type === "text_end") {
816
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "text_end")
817
+ return;
818
+ lastDetailUpdate = now;
819
+ const lastLine = getLastNonEmptyLine(ame.partial);
820
+ if (lastLine) {
821
+ const entry = `✎ ${lastLine}`;
822
+ const log = pipelineEditor.getActivityLog();
823
+ const last = log[log.length - 1];
824
+ if (last?.startsWith("✎")) {
825
+ pipelineEditor.updateLastActivity(entry);
826
+ }
827
+ else {
828
+ pipelineEditor.addActivity(entry);
829
+ }
830
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: entry, type: "text" });
831
+ }
832
+ if (ame.type === "text_end" && stepContext) {
833
+ const completedText = ame.content.trim();
834
+ if (completedText) {
835
+ const lines = completedText.split("\n").length;
836
+ sendVibeMessage(`[${stepContext.role}] ✎ (${lines} lines)`, "agent-text", completedText);
837
+ }
838
+ }
839
+ }
840
+ if (ame.type === "done") {
841
+ const msg = ame.message;
842
+ if (msg.usage && msg.usage.totalTokens > 0) {
843
+ const fmt = (n) => n.toLocaleString("en-US");
844
+ const tokenInfo = `tokens: ${fmt(msg.usage.input)} in / ${fmt(msg.usage.output)} out`;
845
+ pipelineEditor.addActivity(tokenInfo);
846
+ }
847
+ }
848
+ }
625
849
  });
626
850
  },
627
851
  onAgentFinished: () => {
@@ -682,15 +906,19 @@ async function runStandardsEnrichment(store, ctx, config, source, abortHandle, p
682
906
  context = parts.join("\n\n---\n\n");
683
907
  }
684
908
  else {
685
- const requirements = await store.readRequirements();
909
+ const parts = [];
910
+ parts.push(await store.readRequirements());
686
911
  const plan = await store.loadPlan();
687
- const specs = [];
912
+ parts.push(JSON.stringify(plan, null, 2));
688
913
  for (const feature of plan.features) {
689
914
  if (await store.hasArtifact(feature.featureId, "spec.md")) {
690
- specs.push(await store.readArtifact(feature.featureId, "spec.md"));
915
+ parts.push(await store.readArtifact(feature.featureId, "spec.md"));
691
916
  }
692
917
  }
693
- context = [requirements, JSON.stringify(plan, null, 2), ...specs].join("\n\n---\n\n");
918
+ if (await store.hasSystemDesign()) {
919
+ parts.push(await store.readSystemDesign());
920
+ }
921
+ context = parts.join("\n\n---\n\n");
694
922
  }
695
923
  }
696
924
  catch {
@@ -718,6 +946,10 @@ async function runStandardsEnrichment(store, ctx, config, source, abortHandle, p
718
946
  }
719
947
  catch (error) {
720
948
  const msg = error instanceof Error ? error.message : String(error);
949
+ if (msg.includes("aborted")) {
950
+ // Abort errors are not re-thrown. The caller checks abortHandle.aborted.
951
+ return;
952
+ }
721
953
  vibeWarning(`Standards enrichment failed: ${msg}`);
722
954
  }
723
955
  }
@@ -747,26 +979,17 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
747
979
  const specList = specPaths.length > 0
748
980
  ? `The following feature specs were completed:\n${specPaths.map((p) => `- ${p}`).join("\n")}`
749
981
  : "No feature specs available.";
750
- const projectContextPath = join(ctx.cwd, ".vibe", "project-context.md");
751
- let existingContext = "";
752
- try {
753
- existingContext = await store.readProjectContext();
754
- }
755
- catch {
756
- // Empty string if project-context.md doesn't exist
757
- }
758
- const contextInfo = existingContext
759
- ? `Existing project-context.md:\n\`\`\`\n${existingContext}\n\`\`\``
760
- : `No existing project-context.md found at ${projectContextPath}. Create one from scratch by exploring the project.`;
761
- const prompt = [
762
- `Update the project documentation for the project at "${ctx.cwd}".`,
763
- "",
764
- specList,
765
- "",
766
- contextInfo,
767
- "",
768
- "Follow your instructions: update project-context.md, update/create README.md, and output a Getting Started summary.",
769
- ].join("\n");
982
+ // Build feature metadata for Completed Features table
983
+ const featureMetadata = completedFeatures.map((fId) => `- ${fId} (${inferWorkflowType(fId)})`);
984
+ const featureInfo = featureMetadata.length > 0
985
+ ? `Add these features to the Completed Features table in project-context.md:\n${featureMetadata.join("\n")}`
986
+ : "";
987
+ const promptParts = [`Update the project documentation for the project at "${ctx.cwd}".`, "", specList];
988
+ if (featureInfo) {
989
+ promptParts.push("", featureInfo);
990
+ }
991
+ promptParts.push("", "Follow your instructions: update project-context.md, update/create README.md, and output a Getting Started summary.");
992
+ const prompt = promptParts.join("\n");
770
993
  // Create and run documenter agent
771
994
  const systemPrompt = getSystemPromptForRole("documenter");
772
995
  const agent = await createRoleAgent({
@@ -783,6 +1006,9 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
783
1006
  response = await runAgent(agent, prompt, {
784
1007
  onEvent: createAgentProgressHandler(ctx, "Documenting", pipelineEditor, ctx.cwd),
785
1008
  });
1009
+ const documenterUsage = aggregateUsage(agent.state.messages);
1010
+ const documenterLines = response.split("\n").length;
1011
+ vibeAgentResponse(`Documenter response (${documenterLines} lines)`, response, documenterUsage);
786
1012
  }
787
1013
  catch (error) {
788
1014
  const msg = error instanceof Error ? error.message : String(error);
@@ -794,12 +1020,6 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
794
1020
  }
795
1021
  if (abortHandle.aborted)
796
1022
  return;
797
- // Extract Getting Started summary and display in chat
798
- const gettingStarted = extractArtifactContent(response, "getting-started", false);
799
- if (gettingStarted) {
800
- const lines = gettingStarted.split("\n").length;
801
- vibeAgentResponse(`Getting Started summary (${lines} lines)`, gettingStarted);
802
- }
803
1023
  vibeMilestone("Documentation updated");
804
1024
  }
805
1025
  // ─── Workflow Titles ──────────────────────────────────────────────────────────
@@ -818,9 +1038,37 @@ const activePipelines = new Map();
818
1038
  function formatElapsed(ms) {
819
1039
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
820
1040
  }
1041
+ /** Formats token usage as a compact string for headers. */
1042
+ export function formatTokenUsage(usage) {
1043
+ const fmt = (n) => n.toLocaleString("en-US");
1044
+ return `[${fmt(usage.input)} in / ${fmt(usage.output)} out]`;
1045
+ }
1046
+ /** npm package name for update checks. */
1047
+ const AC_PACKAGE_NAME = "@wizdear/atlas-code";
1048
+ /** Check npm registry for a newer version of Atlas Code. */
1049
+ async function checkForAcUpdate(currentVersion) {
1050
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
1051
+ return undefined;
1052
+ try {
1053
+ const response = await fetch(`https://registry.npmjs.org/${AC_PACKAGE_NAME}/latest`, {
1054
+ signal: AbortSignal.timeout(10000),
1055
+ });
1056
+ if (!response.ok)
1057
+ return undefined;
1058
+ const data = (await response.json());
1059
+ if (data.version && data.version !== currentVersion)
1060
+ return data.version;
1061
+ return undefined;
1062
+ }
1063
+ catch {
1064
+ return undefined;
1065
+ }
1066
+ }
821
1067
  /** Vibe Engineering Extension factory. */
822
1068
  export const vibeExtension = (pi) => {
823
1069
  _pi = pi;
1070
+ // Suppress pi's built-in update check — Atlas Code has its own
1071
+ process.env.PI_SKIP_VERSION_CHECK = "1";
824
1072
  // Register custom renderer for vibe-tool messages (agent tool executions)
825
1073
  pi.registerMessageRenderer("vibe-tool", (message, { expanded }, _theme) => {
826
1074
  const details = message.details;
@@ -842,7 +1090,8 @@ export const vibeExtension = (pi) => {
842
1090
  const elapsed = formatElapsed(details.elapsed);
843
1091
  const marker = details.failed ? theme.fg("error", "✗") : theme.fg("success", "✓");
844
1092
  const suffix = details.failed ? theme.fg("error", "(failed)") : theme.fg("dim", `(${elapsed})`);
845
- const header = `${marker} Step ${details.stepIndex + 1}/${details.totalSteps}: ${role} ${details.action} ${suffix}`;
1093
+ const tokenSuffix = details.usage && details.usage.totalTokens > 0 ? ` ${theme.fg("dim", formatTokenUsage(details.usage))}` : "";
1094
+ const header = `${marker} Step ${details.stepIndex + 1}/${details.totalSteps}: ${role} — ${details.action} ${suffix}${tokenSuffix}`;
846
1095
  const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
847
1096
  box.addChild(new Text(header, 0, 0));
848
1097
  if (expanded) {
@@ -866,6 +1115,16 @@ export const vibeExtension = (pi) => {
866
1115
  }
867
1116
  box.addChild(new Text(theme.fg("dim", tokenLine), 0, 0));
868
1117
  }
1118
+ if (details.responseText) {
1119
+ box.addChild(new Spacer(1));
1120
+ const { preview, remaining } = computeAgentResponsePreview(details.responseText);
1121
+ box.addChild(new Markdown(preview, 0, 0, mdTheme, {
1122
+ color: (t) => theme.fg("customMessageText", t),
1123
+ }));
1124
+ if (remaining > 0) {
1125
+ box.addChild(new Text(theme.fg("dim", `... (${remaining} more lines)`), 0, 0));
1126
+ }
1127
+ }
869
1128
  }
870
1129
  return box;
871
1130
  });
@@ -919,7 +1178,10 @@ export const vibeExtension = (pi) => {
919
1178
  }
920
1179
  case "agent-response": {
921
1180
  const marker = expanded ? "▼" : "▶";
922
- box.addChild(new Text(`${marker} ${content}`, 0, 0));
1181
+ const usageSuffix = details?.usage && details.usage.totalTokens > 0
1182
+ ? ` ${theme.fg("dim", formatTokenUsage(details.usage))}`
1183
+ : "";
1184
+ box.addChild(new Text(`${marker} ${content}${usageSuffix}`, 0, 0));
923
1185
  if (details?.fullText) {
924
1186
  box.addChild(new Spacer(1));
925
1187
  if (expanded) {
@@ -939,6 +1201,29 @@ export const vibeExtension = (pi) => {
939
1201
  }
940
1202
  break;
941
1203
  }
1204
+ case "agent-text": {
1205
+ const atMarker = expanded ? "▼" : "▶";
1206
+ box.addChild(new Text(theme.fg("dim", `${atMarker} ${content}`), 0, 0));
1207
+ if (details?.fullText) {
1208
+ if (expanded) {
1209
+ box.addChild(new Spacer(1));
1210
+ box.addChild(new Markdown(details.fullText, 0, 0, mdTheme, {
1211
+ color: (t) => theme.fg("customMessageText", t),
1212
+ }));
1213
+ }
1214
+ else {
1215
+ const { preview, remaining } = computeAgentResponsePreview(details.fullText);
1216
+ box.addChild(new Spacer(1));
1217
+ box.addChild(new Markdown(preview, 0, 0, mdTheme, {
1218
+ color: (t) => theme.fg("dim", t),
1219
+ }));
1220
+ if (remaining > 0) {
1221
+ box.addChild(new Text(theme.fg("dim", `... (${remaining} more lines)`), 0, 0));
1222
+ }
1223
+ }
1224
+ }
1225
+ break;
1226
+ }
942
1227
  case "event": {
943
1228
  box.addChild(new Text(theme.fg("dim", content), 0, 0));
944
1229
  break;
@@ -946,6 +1231,192 @@ export const vibeExtension = (pi) => {
946
1231
  }
947
1232
  return box;
948
1233
  });
1234
+ // Register custom renderer for vibe-update messages (Atlas Code update notification)
1235
+ pi.registerMessageRenderer("vibe-update", (message, _options, theme) => {
1236
+ const details = message.details;
1237
+ if (!details)
1238
+ return undefined;
1239
+ const w = (t) => theme.fg("warning", t);
1240
+ const a = (t) => theme.fg("accent", t);
1241
+ const m = (t) => theme.fg("muted", t);
1242
+ const border = w("─".repeat(75));
1243
+ const header = theme.bold(w("Update Available"));
1244
+ const instruction = `${m(`New version ${details.newVersion} is available. `)}${a(`Run: npm install -g ${AC_PACKAGE_NAME}`)}`;
1245
+ const box = new Box(0, 0);
1246
+ box.addChild(new Text(border, 0, 0));
1247
+ box.addChild(new Text(` ${header}`, 0, 0));
1248
+ box.addChild(new Text(` ${instruction}`, 0, 0));
1249
+ box.addChild(new Text(border, 0, 0));
1250
+ return box;
1251
+ });
1252
+ // ── Custom Header ────────────────────────────────────────────────────────
1253
+ pi.on("session_start", async (_event, ctx) => {
1254
+ if (ctx.hasUI) {
1255
+ // Read version from package.json
1256
+ let acVersion = "0.0.0";
1257
+ try {
1258
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1259
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
1260
+ acVersion = pkg.version ?? acVersion;
1261
+ }
1262
+ catch {
1263
+ // Fallback version
1264
+ }
1265
+ // Check for Atlas Code updates asynchronously
1266
+ checkForAcUpdate(acVersion).then((newVersion) => {
1267
+ if (newVersion) {
1268
+ _pi.sendMessage({
1269
+ customType: "vibe-update",
1270
+ content: newVersion,
1271
+ display: true,
1272
+ details: { currentVersion: acVersion, newVersion },
1273
+ });
1274
+ }
1275
+ });
1276
+ ctx.ui.setHeader((_tui, theme) => ({
1277
+ render(_width) {
1278
+ const b = (t) => theme.fg("accent", t);
1279
+ const d = (t) => theme.fg("dim", t);
1280
+ // Atlas Code logo — diamond with digital glitch
1281
+ const logo = [
1282
+ ` ${b("░▒▓")} ${b("╱╲")} ${b("▓▒░")}`,
1283
+ ` ${b("▓█")} ${b("╱ ╲")} ${b("█▓")}`,
1284
+ ` ${b("█▓")} ${b("╲ ╱")} ${b("▓█")}`,
1285
+ ` ${b("░▒▓")} ${b("╲╱")} ${b("▓▒░")}`,
1286
+ ];
1287
+ const title = ` ${theme.bold("Atlas Code")} ${d(`v${acVersion}`)}`;
1288
+ const vibeCommands = [
1289
+ ` ${d("── Vibe Commands ─────────────────────────────────────")}`,
1290
+ "",
1291
+ ` ${d("Setup")}`,
1292
+ ` ${d("/vibe init")} Initialize .vibe/ directory`,
1293
+ "",
1294
+ ` ${d("Start Workflow")}`,
1295
+ ` ${d("/vibe <requirements>")} Auto workflow`,
1296
+ ` ${d("/vibe new <requirements>")} New feature`,
1297
+ ` ${d("/vibe enhance <id> <req>")} Enhance existing feature`,
1298
+ ` ${d("/vibe fix <description>")} Fix a bug`,
1299
+ ` ${d("/vibe refactor <id> <purpose>")} Refactor a feature`,
1300
+ "",
1301
+ ` ${d("Monitor & Control")}`,
1302
+ ` ${d("/vibe status")} Show pipeline status`,
1303
+ ` ${d("/vibe resume <id>")} Resume paused pipeline`,
1304
+ ];
1305
+ const keybindings = [
1306
+ ` ${d("── Keybindings ───────────────────────────────────────")}`,
1307
+ ` ${d("ctrl+c to interrupt, ctrl+l to clear, ctrl+l twice to exit")}`,
1308
+ ` ${d("ctrl+d to exit (empty), ctrl+z to suspend")}`,
1309
+ ` ${d("ctrl+k to delete to end, ctrl+t to cycle thinking level")}`,
1310
+ ` ${d("ctrl+p/ctrl+n to cycle models, ctrl+shift+p to select model")}`,
1311
+ ` ${d("ctrl+e to expand tools, ctrl+shift+e to expand thinking")}`,
1312
+ ` ${d("ctrl+o for external editor, / for commands")}`,
1313
+ ` ${d("! to run bash, !! to run bash (no context)")}`,
1314
+ ` ${d("ctrl+f to queue follow-up, ctrl+shift+f to edit queued messages")}`,
1315
+ ` ${d("ctrl+v to paste image, drop files to attach")}`,
1316
+ ];
1317
+ return ["", ...logo, "", title, "", ...vibeCommands, "", ...keybindings, ""];
1318
+ },
1319
+ invalidate() { },
1320
+ }));
1321
+ }
1322
+ // Telegram bridge — auto-activate from env vars or .vibe/config.json
1323
+ const vibeDir = join(ctx.cwd, ".vibe");
1324
+ const bridgeManager = createTelegramBridgeManager(pi, ctx, vibeDir, _telegramCommands);
1325
+ bridgeManager.tryStart();
1326
+ pi.on("agent_end", () => {
1327
+ bridgeManager.tryStart();
1328
+ });
1329
+ pi.on("session_shutdown", () => {
1330
+ bridgeManager.stop();
1331
+ });
1332
+ });
1333
+ // Telegram bridge setup guidance in system prompt
1334
+ pi.on("before_agent_start", (event) => {
1335
+ const telegramGuideline = [
1336
+ "",
1337
+ "## Telegram Bridge",
1338
+ "This project has a built-in Telegram bridge. When the user asks to connect/setup Telegram:",
1339
+ '1. Write the bot token to `.vibe/config.json` under `telegram.token` (e.g., `{ "telegram": { "token": "..." } }`). Merge with existing config if the file already exists.',
1340
+ "2. Do NOT create a Telegram bot project, install npm packages, or write bot code.",
1341
+ "3. The bridge activates automatically after the config is saved. Chat ID is auto-discovered from the first message sent to the bot.",
1342
+ ].join("\n");
1343
+ return { systemPrompt: `${event.systemPrompt}${telegramGuideline}` };
1344
+ });
1345
+ const vibeCommandHandler = async (args, ctx) => {
1346
+ try {
1347
+ const command = parseVibeCommand(args);
1348
+ // Record user command input in session
1349
+ if (command.type !== "help") {
1350
+ vibeCommand(`/vibe ${args.trim() || command.type}`);
1351
+ }
1352
+ const logger = createVibeLogger(ctx, join(ctx.cwd, ".vibe"));
1353
+ const store = new VibeStore(ctx.cwd, logger);
1354
+ switch (command.type) {
1355
+ case "help":
1356
+ ctx.ui.notify(VIBE_HELP, "info");
1357
+ break;
1358
+ case "init":
1359
+ await handleInit(store, ctx);
1360
+ break;
1361
+ case "reset":
1362
+ await handleReset(store, ctx);
1363
+ break;
1364
+ case "recover":
1365
+ await handleRecover(store, ctx);
1366
+ break;
1367
+ case "analyze":
1368
+ await handleAnalyze(store, ctx, ctx.model);
1369
+ break;
1370
+ case "config":
1371
+ await handleConfig(store, ctx);
1372
+ break;
1373
+ case "status":
1374
+ await handleStatus(store, ctx, command.featureId);
1375
+ break;
1376
+ case "log":
1377
+ await handleLog(store, ctx, command.featureId);
1378
+ break;
1379
+ case "auto":
1380
+ await handleWorkflow(store, ctx, "auto", command.requirement, undefined, logger);
1381
+ break;
1382
+ case "new":
1383
+ await handleWorkflow(store, ctx, "new_feature", command.requirement, undefined, logger);
1384
+ break;
1385
+ case "enhance":
1386
+ await handleWorkflow(store, ctx, "enhancement", command.requirement, { parentFeatureId: command.featureId }, logger);
1387
+ break;
1388
+ case "fix":
1389
+ await handleWorkflow(store, ctx, "bugfix", command.description, { issueRef: command.issueRef }, logger);
1390
+ break;
1391
+ case "refactor":
1392
+ await handleWorkflow(store, ctx, "refactor", command.purpose, { parentFeatureId: command.featureId }, logger);
1393
+ break;
1394
+ case "pause":
1395
+ await handlePause(command.featureId, ctx);
1396
+ break;
1397
+ case "resume":
1398
+ await handleResume(store, ctx, command.featureId, logger);
1399
+ break;
1400
+ case "steer": {
1401
+ const active = activePipelines.get(command.featureId);
1402
+ if (!active?.activeAgent) {
1403
+ ctx.ui.notify(`[Vibe] No running agent for ${command.featureId}`, "warning");
1404
+ break;
1405
+ }
1406
+ active.activeAgent.steer({
1407
+ role: "user",
1408
+ content: [{ type: "text", text: command.feedback }],
1409
+ timestamp: Date.now(),
1410
+ });
1411
+ ctx.ui.notify(`[Steer] Feedback sent to ${command.featureId}`, "info");
1412
+ break;
1413
+ }
1414
+ }
1415
+ }
1416
+ catch (error) {
1417
+ vibeError(error instanceof Error ? error.message : String(error));
1418
+ }
1419
+ };
949
1420
  pi.registerCommand("vibe", {
950
1421
  description: "AI Vibe Engineering System",
951
1422
  getArgumentCompletions(argumentPrefix) {
@@ -957,96 +1428,52 @@ export const vibeExtension = (pi) => {
957
1428
  }
958
1429
  return null;
959
1430
  },
960
- async handler(args, ctx) {
961
- try {
962
- const command = parseVibeCommand(args);
963
- // Record user command input in session
964
- if (command.type !== "help") {
965
- vibeCommand(`/vibe ${args.trim() || command.type}`);
966
- }
967
- const logger = createVibeLogger(ctx, join(ctx.cwd, ".vibe"));
968
- const store = new VibeStore(ctx.cwd, logger);
969
- switch (command.type) {
970
- case "help":
971
- ctx.ui.notify(VIBE_HELP, "info");
972
- break;
973
- case "init":
974
- await handleInit(store, ctx);
975
- break;
976
- case "analyze":
977
- await handleAnalyze(store, ctx, ctx.model);
978
- break;
979
- case "config":
980
- await handleConfig(store, ctx);
981
- break;
982
- case "status":
983
- await handleStatus(store, ctx, command.featureId);
984
- break;
985
- case "log":
986
- await handleLog(store, ctx, command.featureId);
987
- break;
988
- case "auto":
989
- await handleWorkflow(store, ctx, "auto", command.requirement, undefined, logger);
990
- break;
991
- case "new":
992
- await handleWorkflow(store, ctx, "new_feature", command.requirement, undefined, logger);
993
- break;
994
- case "enhance":
995
- await handleWorkflow(store, ctx, "enhancement", command.requirement, { parentFeatureId: command.featureId }, logger);
996
- break;
997
- case "fix":
998
- await handleWorkflow(store, ctx, "bugfix", command.description, { issueRef: command.issueRef }, logger);
999
- break;
1000
- case "refactor":
1001
- await handleWorkflow(store, ctx, "refactor", command.purpose, { parentFeatureId: command.featureId }, logger);
1002
- break;
1003
- case "pause":
1004
- await handlePause(command.featureId, ctx);
1005
- break;
1006
- case "resume":
1007
- await handleResume(store, ctx, command.featureId, logger);
1008
- break;
1009
- case "steer": {
1010
- const active = activePipelines.get(command.featureId);
1011
- if (!active?.activeAgent) {
1012
- ctx.ui.notify(`[Vibe] No running agent for ${command.featureId}`, "warning");
1013
- break;
1014
- }
1015
- active.activeAgent.steer({
1016
- role: "user",
1017
- content: [{ type: "text", text: command.feedback }],
1018
- timestamp: Date.now(),
1019
- });
1020
- ctx.ui.notify(`[Steer] Feedback sent to ${command.featureId}`, "info");
1021
- break;
1022
- }
1023
- }
1024
- }
1025
- catch (error) {
1026
- vibeError(error instanceof Error ? error.message : String(error));
1027
- }
1028
- },
1431
+ handler: vibeCommandHandler,
1432
+ });
1433
+ // Register vibe command for Telegram dispatch (shared array, read by bridge polling loop)
1434
+ _telegramCommands.push({
1435
+ name: "vibe",
1436
+ description: "AI Vibe Engineering System",
1437
+ handler: vibeCommandHandler,
1438
+ subcommands: [
1439
+ { name: "new", description: "Start a new feature" },
1440
+ { name: "enhance", description: "Enhance existing feature" },
1441
+ { name: "fix", description: "Fix a bug" },
1442
+ { name: "refactor", description: "Refactor code" },
1443
+ { name: "status", description: "Show pipeline status" },
1444
+ { name: "resume", description: "Resume paused pipeline" },
1445
+ { name: "pause", description: "Pause current pipeline" },
1446
+ { name: "config", description: "Configure vibe settings" },
1447
+ { name: "log", description: "Show pipeline log" },
1448
+ { name: "steer", description: "Steer active pipeline" },
1449
+ { name: "analyze", description: "Analyze project" },
1450
+ { name: "init", description: "Initialize vibe project" },
1451
+ { name: "reset", description: "Reset vibe state" },
1452
+ { name: "recover", description: "Recover from failed state" },
1453
+ ],
1029
1454
  });
1030
1455
  // ── vibe_start tool ──────────────────────────────────────────────────────
1031
1456
  //
1032
1457
  // Allows the LLM to invoke the vibe pipeline programmatically.
1033
1458
  //
1034
- // sendUserMessage() bypasses command routing (expandPromptTemplates: false
1035
- // in agent-session.js), so we cannot delegate via "/vibe" as a user message.
1459
+ // The tool stores the requirement and defers execution to agent_end.
1460
+ // This ensures handleWorkflow() runs AFTER the main agent finishes streaming,
1461
+ // so _pi.sendMessage() calls go through appendMessage() instead of
1462
+ // agent.steer(). Without this, display-only progress messages would enter
1463
+ // the steering queue, skip remaining tool calls, and pollute the main
1464
+ // agent's LLM context (custom messages are converted to role:"user" by
1465
+ // convertToLlm, causing the main agent to respond to pipeline progress).
1036
1466
  //
1037
- // Instead: tool stores the requirement, sends a trigger phrase as a followUp.
1038
- // The input event handler intercepts the trigger before it reaches the queue,
1039
- // returns "handled", and calls handleWorkflow() directly.
1040
- //
1041
- // Triple guard against duplicate execution (LLM batches tool calls):
1467
+ // Duplicate execution guard:
1042
1468
  // 1. vibeStartFired flag in execute() — second call returns immediately
1043
- // 2. Input handler absorbs orphan triggers (no pending requirement)
1044
- // 3. agent_end skips tool re-activation while vibeStartFired is true
1469
+ // 2. agent_end clears the flag after workflow completes
1045
1470
  let pendingVibeRequirement;
1046
1471
  let vibeStartFired = false;
1047
- const VIBE_TRIGGER = "__vibe_pipeline_start__";
1048
- pi.on("input", async (event, ctx) => {
1049
- if (event.text === VIBE_TRIGGER && pendingVibeRequirement) {
1472
+ pi.on("agent_end", async (_event, ctx) => {
1473
+ // Start the pipeline after the main agent finishes streaming.
1474
+ // At this point isStreaming === false, so _pi.sendMessage() calls
1475
+ // use appendMessage() instead of agent.steer().
1476
+ if (pendingVibeRequirement) {
1050
1477
  const requirement = pendingVibeRequirement;
1051
1478
  pendingVibeRequirement = undefined;
1052
1479
  // Cast ctx to ExtensionCommandContext — safe because handleWorkflow
@@ -1065,14 +1492,8 @@ export const vibeExtension = (pi) => {
1065
1492
  finally {
1066
1493
  vibeStartFired = false;
1067
1494
  }
1068
- return { action: "handled" };
1069
- }
1070
- // Absorb duplicate trigger messages from batched tool calls
1071
- if (event.text === VIBE_TRIGGER) {
1072
- return { action: "handled" };
1495
+ return;
1073
1496
  }
1074
- });
1075
- pi.on("agent_end", async () => {
1076
1497
  // Re-activate vibe_start only after the pipeline has completed
1077
1498
  if (!vibeStartFired) {
1078
1499
  const activeTools = pi.getActiveTools();
@@ -1106,7 +1527,6 @@ export const vibeExtension = (pi) => {
1106
1527
  }
1107
1528
  vibeStartFired = true;
1108
1529
  pendingVibeRequirement = params.description;
1109
- pi.sendUserMessage(VIBE_TRIGGER, { deliverAs: "followUp" });
1110
1530
  const activeTools = pi.getActiveTools();
1111
1531
  pi.setActiveTools(activeTools.filter((t) => t !== "vibe_start"));
1112
1532
  return {
@@ -1144,8 +1564,9 @@ async function handleInit(store, ctx) {
1144
1564
  await mkdir(join(store.getSkillsDir(), "qmd-memory"), { recursive: true });
1145
1565
  await writeFile(qmdSkillPath, QMD_SKILL_TEMPLATE, "utf-8");
1146
1566
  }
1147
- // Clean up stale QMD collections if qmd is installed
1567
+ // Clean up stale QMD collections and initialize fresh ones if qmd is installed
1148
1568
  await cleanupQmdCollections(ctx.cwd);
1569
+ await initQmdCollections(ctx.cwd);
1149
1570
  vibeMilestone("Initialized .vibe/ directory with 14 standard templates");
1150
1571
  // autoAnalyzeOnInit
1151
1572
  const config = await store.loadConfig();
@@ -1161,6 +1582,15 @@ async function handleInit(store, ctx) {
1161
1582
  }
1162
1583
  }
1163
1584
  }
1585
+ async function handleReset(store, ctx) {
1586
+ const confirm = await ctx.ui.select("This will delete all .vibe/ contents (standards, features, config, etc.). Continue?", ["No", "Yes"]);
1587
+ if (confirm !== "Yes") {
1588
+ ctx.ui.notify("[Vibe] Reset cancelled", "info");
1589
+ return;
1590
+ }
1591
+ await store.reset();
1592
+ await handleInit(store, ctx);
1593
+ }
1164
1594
  async function handleAnalyze(store, ctx, currentModel) {
1165
1595
  const config = await store.loadConfig();
1166
1596
  const configBackup = structuredClone(config);
@@ -1180,9 +1610,12 @@ async function handleAnalyze(store, ctx, currentModel) {
1180
1610
  const prompt = buildAnalyzePrompt(ctx.cwd, excludePaths);
1181
1611
  abortHandle.currentAgent = agent;
1182
1612
  try {
1183
- await runAgent(agent, prompt, {
1613
+ const analyzeResponse = await runAgent(agent, prompt, {
1184
1614
  onEvent: createAgentProgressHandler(ctx, "Analyzing", pipelineEditor, ctx.cwd),
1185
1615
  });
1616
+ const analyzeUsage = aggregateUsage(agent.state.messages);
1617
+ const analyzeLines = analyzeResponse.split("\n").length;
1618
+ vibeAgentResponse(`Project Analyzer response (${analyzeLines} lines)`, analyzeResponse, analyzeUsage);
1186
1619
  }
1187
1620
  catch (error) {
1188
1621
  if (abortHandle.aborted) {
@@ -1519,16 +1952,10 @@ export const PHASE_ORDER = [
1519
1952
  "done",
1520
1953
  ];
1521
1954
  /**
1522
- * Maps gate phases to their parent execution phases on resume.
1523
- * When interrupted at a gate phase, re-execution starts from the block containing that gate.
1955
+ * Resolves the phase to resume from. Gate phases are kept as-is so that
1956
+ * resume skips the parent execution and goes directly to the gate prompt.
1524
1957
  */
1525
1958
  export function resolveResumePhase(phase) {
1526
- if (phase === "requirements_gate")
1527
- return "discovery";
1528
- if (phase === "system_design_gate")
1529
- return "system_architecture";
1530
- if (phase === "plan_gate")
1531
- return "planning";
1532
1959
  return phase;
1533
1960
  }
1534
1961
  async function handleWorkflow(store, ctx, workflowType, _requirement, options, logger, startFromPhase) {
@@ -1572,23 +1999,33 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1572
1999
  vibeMilestone(`Base branch set to \`${chosenBranch}\``);
1573
2000
  }
1574
2001
  }
1575
- // Stale project-context warning
2002
+ // Stale project-context check: auto-analyze if missing, warn if stale
1576
2003
  if (await store.isProjectContextStale(config.projectAnalysis.staleThresholdDays)) {
1577
2004
  const hasContext = await store.hasProjectContext();
1578
- const msg = hasContext
1579
- ? `project-context.md is older than ${config.projectAnalysis.staleThresholdDays} days. Run /vibe analyze to refresh.`
1580
- : "No project-context.md found. Run /vibe analyze to generate.";
1581
- vibeWarning(msg);
2005
+ if (!hasContext && config.projectAnalysis.autoAnalyzeOnInit) {
2006
+ try {
2007
+ await handleAnalyze(store, ctx, ctx.model);
2008
+ }
2009
+ catch (error) {
2010
+ vibeError(`Project analysis failed: ${error instanceof Error ? error.message : String(error)}\nRun /vibe analyze to retry.`);
2011
+ }
2012
+ }
2013
+ else {
2014
+ const msg = hasContext
2015
+ ? `project-context.md is older than ${config.projectAnalysis.staleThresholdDays} days. Run /vibe analyze to refresh.`
2016
+ : "No project-context.md found. Run /vibe analyze to generate.";
2017
+ vibeWarning(msg);
2018
+ }
1582
2019
  }
1583
2020
  const getApiKey = (provider) => ctx.modelRegistry.getApiKeyForProvider(provider);
1584
2021
  const availableSkills = await buildAvailableSkillsPrompt(store);
1585
- const projectContext = (await store.hasProjectContext()) ? await store.readProjectContext() : undefined;
1586
2022
  const requestApproval = async (_step, _fId, summary) => {
2023
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
1587
2024
  // Auto-expand messages so the user can review full content before deciding
1588
2025
  const wasExpanded = ctx.ui.getToolsExpanded();
1589
2026
  if (!wasExpanded)
1590
2027
  ctx.ui.setToolsExpanded(true);
1591
- const choice = await ctx.ui.select(summary, ["Approve", "Reject", "Skip", "Abort"]);
2028
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
1592
2029
  // Restore previous expand state after gate resolves
1593
2030
  if (!wasExpanded)
1594
2031
  ctx.ui.setToolsExpanded(false);
@@ -1604,7 +2041,8 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1604
2041
  case "Abort":
1605
2042
  return { passed: false, action: "abort" };
1606
2043
  default:
1607
- return { passed: false, action: "rejected" };
2044
+ // ESC or undefined selection pause (preserve state for resume)
2045
+ return { passed: false, action: "pause" };
1608
2046
  }
1609
2047
  };
1610
2048
  /**
@@ -1620,6 +2058,9 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1620
2058
  if (result.action === "abort") {
1621
2059
  return { proceed: false, action: "abort" };
1622
2060
  }
2061
+ if (result.action === "pause") {
2062
+ return { proceed: false, action: "pause" };
2063
+ }
1623
2064
  if (result.action === "skip") {
1624
2065
  return { proceed: true, action: "skip" };
1625
2066
  }
@@ -1661,62 +2102,63 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1661
2102
  let requirementsApproved = false;
1662
2103
  let discoveryFeedback;
1663
2104
  // Discovery can be skipped on resume, but must run if requirements.md is missing
1664
- if (!shouldRunPhase("discovery") && (await store.hasRequirements())) {
2105
+ if (!shouldRunPhase("discovery") && !shouldRunPhase("requirements_gate") && (await store.hasRequirements())) {
1665
2106
  requirementsApproved = true;
1666
2107
  }
2108
+ // When resuming at requirements_gate, skip discovery and go straight to gate
2109
+ let skipDiscoveryForGate = startFromPhase === "requirements_gate" && (await store.hasRequirements());
1667
2110
  while (!requirementsApproved) {
1668
- await saveOrcState("discovery");
1669
- pipelineEditor.setPhase(discoveryFeedback ? "Discovery (retry)" : "Discovery");
1670
- const discoveryResult = await runDiscovery({
1671
- store,
1672
- config,
1673
- projectRoot: ctx.cwd,
1674
- requirement: _requirement,
1675
- requestUserInput: async (_questions) => {
1676
- pipelineEditor.setDetail("waiting for input...");
1677
- return ctx.ui.editor("Discovery: Answer the questions above");
1678
- },
1679
- getApiKey,
1680
- model: ctx.model,
1681
- onAgentEvent: createAgentProgressHandler(ctx, "Discovery", pipelineEditor, ctx.cwd),
1682
- onMessage: (msg) => {
1683
- if (msg.includes("[REQUIREMENTS_COMPLETE]") || msg.includes("```requirements.md")) {
2111
+ if (skipDiscoveryForGate) {
2112
+ skipDiscoveryForGate = false;
2113
+ }
2114
+ else {
2115
+ await saveOrcState("discovery");
2116
+ pipelineEditor.setPhase(discoveryFeedback ? "Discovery (retry)" : "Discovery");
2117
+ const discoveryResult = await runDiscovery({
2118
+ store,
2119
+ config,
2120
+ projectRoot: ctx.cwd,
2121
+ requirement: _requirement,
2122
+ requestUserInput: async (_questions) => {
2123
+ pipelineEditor.setDetail("waiting for input...");
2124
+ return ctx.ui.editor("Discovery: Answer the questions above");
2125
+ },
2126
+ getApiKey,
2127
+ model: ctx.model,
2128
+ onAgentEvent: createAgentProgressHandler(ctx, "Discovery", pipelineEditor, ctx.cwd),
2129
+ onMessage: (msg) => {
1684
2130
  const lines = msg.split("\n").length;
1685
- vibeAgentResponse(`Discovery complete requirements.md (${lines} lines)`, msg);
1686
- }
1687
- else if (msg.includes("[QUESTIONS]")) {
1688
- const questions = extractQuestions(msg);
1689
- if (questions) {
1690
- vibeMilestone(`Discovery: clarifying questions\n\n${questions}`);
2131
+ if (msg.includes("[REQUIREMENTS_COMPLETE]") || msg.includes("```requirements.md")) {
2132
+ vibeAgentResponse(`Discovery complete — requirements.md (${lines} lines)`, msg);
2133
+ }
2134
+ else if (msg.includes("[QUESTIONS]")) {
2135
+ vibeAgentResponse(`Discovery: clarifying questions (${lines} lines)`, msg);
2136
+ }
2137
+ else if (msg.startsWith("[Discovery]")) {
2138
+ vibeAgentResponse(`Discovery response (${lines} lines)`, msg);
1691
2139
  }
1692
2140
  else {
1693
- vibeMilestone("Discovery: asking clarifying questions");
2141
+ vibeAgentResponse(`Discovery response (${lines} lines)`, msg);
1694
2142
  }
1695
- }
1696
- else if (msg.startsWith("[Discovery]")) {
1697
- vibeMilestone(msg.replace("[Discovery] ", ""));
1698
- }
1699
- else {
1700
- const lines = msg.split("\n").length;
1701
- vibeAgentResponse(`Discovery response (${lines} lines)`, msg);
1702
- }
1703
- },
1704
- feedback: discoveryFeedback,
1705
- availableSkills,
1706
- onAgentCreated: (agent) => {
1707
- abortHandle.currentAgent = agent;
1708
- },
1709
- onAgentFinished: () => {
1710
- abortHandle.currentAgent = undefined;
1711
- },
1712
- });
1713
- if (!discoveryResult.completed) {
1714
- // Discovery cancelled: clean exit
1715
- vibeGate("Discovery cancelled");
1716
- await store.clearGlobalAgentHistoryByRole("discovery");
1717
- await store.clearOrchestrationState();
1718
- return;
1719
- }
2143
+ },
2144
+ feedback: discoveryFeedback,
2145
+ availableSkills,
2146
+ onAgentCreated: (agent) => {
2147
+ abortHandle.currentAgent = agent;
2148
+ },
2149
+ onAgentFinished: () => {
2150
+ abortHandle.currentAgent = undefined;
2151
+ },
2152
+ });
2153
+ if (discoveryResult.usage) {
2154
+ vibeMilestone(`Discovery tokens: ${formatTokenUsage(discoveryResult.usage)}`);
2155
+ }
2156
+ if (!discoveryResult.completed) {
2157
+ // Discovery cancelled (ESC): preserve state + agent history for resume
2158
+ vibeGate("Discovery paused — use `/vibe resume` to continue");
2159
+ return;
2160
+ }
2161
+ } // end: skip discovery for gate resume
1720
2162
  // Gate: requirements approval
1721
2163
  await saveOrcState("requirements_gate");
1722
2164
  const gateResult = await checkOrchestrationGate("requireRequirementsApproval", "[Gate] Requirements complete. Approve to proceed?");
@@ -1727,6 +2169,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1727
2169
  await store.clearOrchestrationState();
1728
2170
  return;
1729
2171
  }
2172
+ if (gateResult.action === "pause") {
2173
+ vibeGate("Paused at requirements gate — use `/vibe resume` to continue");
2174
+ return;
2175
+ }
1730
2176
  if (gateResult.action === "skip") {
1731
2177
  vibeGate("Requirements review skipped");
1732
2178
  requirementsApproved = true;
@@ -1741,81 +2187,84 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1741
2187
  }
1742
2188
  }
1743
2189
  // ── Standards enrichment (post-Discovery) ──
1744
- if (!abortHandle.aborted) {
2190
+ if (!abortHandle.aborted && shouldRunPhase("system_architecture")) {
1745
2191
  await runStandardsEnrichment(store, ctx, config, "discovery", abortHandle, pipelineEditor);
1746
2192
  }
2193
+ if (abortHandle.aborted) {
2194
+ vibeGate("Aborted by user (ESC)");
2195
+ return;
2196
+ }
1747
2197
  // ── Phase: System Architecture → system-design.md ──
1748
2198
  let systemDesignApproved = false;
1749
2199
  let systemDesignFeedback;
1750
- if (!shouldRunPhase("system_architecture") && (await store.hasSystemDesign())) {
2200
+ if (!shouldRunPhase("system_architecture") &&
2201
+ !shouldRunPhase("system_design_gate") &&
2202
+ (await store.hasSystemDesign())) {
1751
2203
  systemDesignApproved = true;
1752
2204
  }
1753
- else if (shouldRunPhase("system_architecture") && (await store.hasSystemDesign())) {
1754
- // system-design.md exists ask user if architecture update is needed
1755
- if (config.autonomyLevel === "full_auto") {
1756
- systemDesignApproved = true;
1757
- vibeMilestone("System design exists. Skipping architecture update (full_auto).");
2205
+ // When system-design.md exists and phase should run, fall through to while loop (always update).
2206
+ // System Architect handles incremental refinement automatically via "Refine and update" prompt.
2207
+ // When resuming at system_design_gate, skip system architecture and go straight to gate
2208
+ let skipSysArchForGate = startFromPhase === "system_design_gate" && (await store.hasSystemDesign());
2209
+ while (!systemDesignApproved && !abortHandle.aborted) {
2210
+ if (skipSysArchForGate) {
2211
+ skipSysArchForGate = false;
1758
2212
  }
1759
2213
  else {
1760
- const choice = await ctx.ui.select("System design exists. Update architecture?", ["Skip", "Update"]);
1761
- if (choice === "Skip") {
1762
- systemDesignApproved = true;
1763
- vibeMilestone("System architecture skipped by user.");
2214
+ await saveOrcState("system_architecture");
2215
+ pipelineEditor.setPhase(systemDesignFeedback ? "System Architecture (retry)" : "System Architecture");
2216
+ pipelineEditor.setDetail("");
2217
+ // Save old content for impact analysis on re-runs
2218
+ let oldSystemDesign;
2219
+ if (systemDesignFeedback && (await store.hasSystemDesign())) {
2220
+ oldSystemDesign = await store.readSystemDesign();
1764
2221
  }
1765
- // "Update" falls through to while loop
1766
- }
1767
- }
1768
- while (!systemDesignApproved && !abortHandle.aborted) {
1769
- await saveOrcState("system_architecture");
1770
- pipelineEditor.setPhase(systemDesignFeedback ? "System Architecture (retry)" : "System Architecture");
1771
- pipelineEditor.setDetail("");
1772
- // Save old content for impact analysis on re-runs
1773
- let oldSystemDesign;
1774
- if (systemDesignFeedback && (await store.hasSystemDesign())) {
1775
- oldSystemDesign = await store.readSystemDesign();
1776
- }
1777
- const sysArchResult = await runSystemArchitect({
1778
- store,
1779
- config,
1780
- projectRoot: ctx.cwd,
1781
- getApiKey,
1782
- model: ctx.model,
1783
- onAgentEvent: createAgentProgressHandler(ctx, "System Architecture", pipelineEditor, ctx.cwd),
1784
- onMessage: (msg) => {
1785
- const lines = msg.split("\n").length;
1786
- vibeAgentResponse(`System design (${lines} lines)`, msg);
1787
- },
1788
- feedback: systemDesignFeedback,
1789
- availableSkills,
1790
- onAgentCreated: (agent) => {
1791
- abortHandle.currentAgent = agent;
1792
- },
1793
- onAgentFinished: () => {
1794
- abortHandle.currentAgent = undefined;
1795
- },
1796
- });
1797
- if (abortHandle.aborted) {
1798
- vibeGate("Aborted by user (ESC)");
1799
- return;
1800
- }
1801
- if (!sysArchResult.completed) {
1802
- vibeError("System architecture failed");
1803
- return;
1804
- }
1805
- // Impact analysis on re-runs (compare old vs new system-design.md)
1806
- if (oldSystemDesign && (await store.hasSystemDesign())) {
1807
- const newSystemDesign = await store.readSystemDesign();
1808
- const featureDesigns = await loadAllFeatureDesigns(store);
1809
- if (featureDesigns.size > 0) {
1810
- const impact = analyzeSystemDesignImpact(oldSystemDesign, newSystemDesign, featureDesigns);
1811
- if (impact.affectedFeatures.length > 0) {
1812
- const lines = impact.affectedFeatures
1813
- .map((f) => `- **${f.featureId}**: ${f.reasons.join("; ")}`)
1814
- .join("\n");
1815
- vibeWarning(`System design changes may affect ${impact.affectedFeatures.length} feature(s):\n${lines}`);
2222
+ const sysArchResult = await runSystemArchitect({
2223
+ store,
2224
+ config,
2225
+ projectRoot: ctx.cwd,
2226
+ getApiKey,
2227
+ model: ctx.model,
2228
+ onAgentEvent: createAgentProgressHandler(ctx, "System Architecture", pipelineEditor, ctx.cwd),
2229
+ onMessage: (msg) => {
2230
+ const lines = msg.split("\n").length;
2231
+ vibeAgentResponse(`System design (${lines} lines)`, msg);
2232
+ },
2233
+ feedback: systemDesignFeedback,
2234
+ availableSkills,
2235
+ onAgentCreated: (agent) => {
2236
+ abortHandle.currentAgent = agent;
2237
+ },
2238
+ onAgentFinished: () => {
2239
+ abortHandle.currentAgent = undefined;
2240
+ },
2241
+ });
2242
+ if (sysArchResult.usage) {
2243
+ vibeMilestone(`System Architect tokens: ${formatTokenUsage(sysArchResult.usage)}`);
2244
+ }
2245
+ if (abortHandle.aborted) {
2246
+ vibeGate("Aborted by user (ESC)");
2247
+ return;
2248
+ }
2249
+ if (!sysArchResult.completed) {
2250
+ vibeError("System architecture failed");
2251
+ return;
2252
+ }
2253
+ // Impact analysis on re-runs (compare old vs new system-design.md)
2254
+ if (oldSystemDesign && (await store.hasSystemDesign())) {
2255
+ const newSystemDesign = await store.readSystemDesign();
2256
+ const featureDesigns = await loadAllFeatureDesigns(store);
2257
+ if (featureDesigns.size > 0) {
2258
+ const impact = analyzeSystemDesignImpact(oldSystemDesign, newSystemDesign, featureDesigns);
2259
+ if (impact.affectedFeatures.length > 0) {
2260
+ const lines = impact.affectedFeatures
2261
+ .map((f) => `- **${f.featureId}**: ${f.reasons.join("; ")}`)
2262
+ .join("\n");
2263
+ vibeWarning(`System design changes may affect ${impact.affectedFeatures.length} feature(s):\n${lines}`);
2264
+ }
1816
2265
  }
1817
2266
  }
1818
- }
2267
+ } // end: skip system architecture for gate resume
1819
2268
  // Gate: system design approval
1820
2269
  await saveOrcState("system_design_gate");
1821
2270
  const sysDesignGate = await checkOrchestrationGate("requireSystemDesignApproval", "[Gate] System design complete. Approve to proceed?");
@@ -1828,6 +2277,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1828
2277
  await store.clearOrchestrationState();
1829
2278
  return;
1830
2279
  }
2280
+ if (sysDesignGate.action === "pause") {
2281
+ vibeGate("Paused at system design gate — use `/vibe resume` to continue");
2282
+ return;
2283
+ }
1831
2284
  if (sysDesignGate.action === "skip") {
1832
2285
  vibeGate("System design review skipped");
1833
2286
  systemDesignApproved = true;
@@ -1842,20 +2295,31 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1842
2295
  }
1843
2296
  }
1844
2297
  // ── Standards enrichment (post-System Architecture) ──
1845
- if (!abortHandle.aborted && (await store.hasSystemDesign())) {
2298
+ if (!abortHandle.aborted && shouldRunPhase("analyze") && (await store.hasSystemDesign())) {
1846
2299
  await runStandardsEnrichment(store, ctx, config, "system_architect", abortHandle, pipelineEditor);
1847
2300
  }
2301
+ if (abortHandle.aborted) {
2302
+ vibeGate("Aborted by user (ESC)");
2303
+ return;
2304
+ }
1848
2305
  // ── Multi-feature orchestration path (new_feature, enhancement, refactor, auto/mixed) ──
1849
2306
  if (workflowType === "new_feature" ||
1850
2307
  workflowType === "enhancement" ||
1851
2308
  workflowType === "refactor" ||
1852
2309
  workflowType === "auto" ||
1853
2310
  workflowType === "mixed") {
1854
- // Enhancement/Refactor: run Analyze at orchestration level before Planner
1855
- if ((workflowType === "enhancement" || workflowType === "refactor") && shouldRunPhase("analyze")) {
2311
+ // Run Analyze at orchestration level before Planner (all multi-feature types)
2312
+ if ((workflowType === "new_feature" || workflowType === "enhancement" || workflowType === "refactor") &&
2313
+ shouldRunPhase("analyze")) {
1856
2314
  await saveOrcState("analyze");
1857
2315
  pipelineEditor.setPhase("Analyzing");
1858
2316
  pipelineEditor.setDetail("");
2317
+ // Inject system-design.md for architectural context during impact analysis
2318
+ let analyzerFeatureContext;
2319
+ if (await store.hasSystemDesign()) {
2320
+ const sd = await store.readSystemDesign();
2321
+ analyzerFeatureContext = `## System Architecture\n\n${sd}`;
2322
+ }
1859
2323
  const analyzerSystemPrompt = getSystemPromptForRole("analyzer");
1860
2324
  const analyzerAgent = await createRoleAgent({
1861
2325
  role: "analyzer",
@@ -1865,10 +2329,14 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1865
2329
  model: ctx.model,
1866
2330
  getApiKey,
1867
2331
  availableSkills,
2332
+ featureContext: analyzerFeatureContext,
1868
2333
  });
1869
2334
  abortHandle.currentAgent = analyzerAgent;
1870
2335
  try {
1871
2336
  const analyzerResponse = await runAgent(analyzerAgent, `Analyze the impact of the following requirements on the existing codebase. Produce an impact-report.md.\n\n${await store.readRequirements()}`, { onEvent: createAgentProgressHandler(ctx, "Analyzing", pipelineEditor, ctx.cwd) });
2337
+ const analyzerUsage = aggregateUsage(analyzerAgent.state.messages);
2338
+ const analyzerLines = analyzerResponse.split("\n").length;
2339
+ vibeAgentResponse(`Analyzer response (${analyzerLines} lines)`, analyzerResponse, analyzerUsage);
1872
2340
  const impactContent = extractArtifactContent(analyzerResponse, "impact-report.md", true);
1873
2341
  if (impactContent) {
1874
2342
  await store.writeGlobalArtifact("impact-report.md", impactContent);
@@ -1895,62 +2363,70 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1895
2363
  let planFeedback;
1896
2364
  let lastFeatureCount = 0;
1897
2365
  // Planning can be skipped on resume, but must run if plan.json is missing
1898
- if (!shouldRunPhase("planning") && (await store.hasPlan())) {
2366
+ if (!shouldRunPhase("planning") && !shouldRunPhase("plan_gate") && (await store.hasPlan())) {
1899
2367
  planApproved = true;
1900
2368
  }
2369
+ // When resuming at plan_gate, skip planner and go straight to gate
2370
+ let skipPlannerForGate = startFromPhase === "plan_gate" && (await store.hasPlan());
1901
2371
  while (!planApproved) {
1902
- // Clean up features/ from previous runs. The Planner reuses sequential
1903
- // featureIds (feat-001, etc.), so leftover artifacts would conflict
1904
- // with new orchestration. No data loss since the Planner regenerates
1905
- // spec.md. Ensures a clean state on Reject re-runs as well.
1906
- await store.clearAllFeatures();
1907
- await saveOrcState("planning");
1908
- pipelineEditor.setPhase(planFeedback ? "Planning (retry)" : "Planning");
1909
- pipelineEditor.setDetail("");
1910
- const plannerResult = await runPlanner({
1911
- store,
1912
- config,
1913
- projectRoot: ctx.cwd,
1914
- getApiKey,
1915
- model: ctx.model,
1916
- onAgentEvent: createAgentProgressHandler(ctx, "Planning", pipelineEditor, ctx.cwd),
1917
- onMessage: (msg) => {
1918
- const lines = msg.split("\n").length;
1919
- if (lines > 5) {
2372
+ if (skipPlannerForGate) {
2373
+ skipPlannerForGate = false;
2374
+ }
2375
+ else {
2376
+ // Clean up features/ from previous runs. The Planner reuses sequential
2377
+ // featureIds (feat-001, etc.), so leftover artifacts would conflict
2378
+ // with new orchestration. No data loss since the Planner regenerates
2379
+ // spec.md. Ensures a clean state on Reject re-runs as well.
2380
+ await store.clearAllFeatures();
2381
+ await saveOrcState("planning");
2382
+ pipelineEditor.setPhase(planFeedback ? "Planning (retry)" : "Planning");
2383
+ pipelineEditor.setDetail("");
2384
+ const plannerResult = await runPlanner({
2385
+ store,
2386
+ config,
2387
+ projectRoot: ctx.cwd,
2388
+ getApiKey,
2389
+ model: ctx.model,
2390
+ onAgentEvent: createAgentProgressHandler(ctx, "Planning", pipelineEditor, ctx.cwd),
2391
+ onMessage: (msg) => {
2392
+ const lines = msg.split("\n").length;
1920
2393
  vibeAgentResponse(`Planner response (${lines} lines)`, msg);
2394
+ },
2395
+ feedback: planFeedback,
2396
+ availableSkills,
2397
+ onAgentCreated: (agent) => {
2398
+ abortHandle.currentAgent = agent;
2399
+ },
2400
+ onAgentFinished: () => {
2401
+ abortHandle.currentAgent = undefined;
2402
+ },
2403
+ });
2404
+ if (plannerResult.usage) {
2405
+ vibeMilestone(`Planner tokens: ${formatTokenUsage(plannerResult.usage)}`);
2406
+ }
2407
+ if (abortHandle.aborted) {
2408
+ vibeGate("Aborted by user (ESC)");
2409
+ return;
2410
+ }
2411
+ if (!plannerResult.completed) {
2412
+ vibeError("Planning failed");
2413
+ return;
2414
+ }
2415
+ lastFeatureCount = plannerResult.featureCount;
2416
+ // Traceability check
2417
+ if (await store.hasRequirements()) {
2418
+ const requirements = await store.readRequirements();
2419
+ const plan = await store.loadPlan();
2420
+ const unmapped = findUnmappedRequirements(requirements, plan);
2421
+ if (unmapped.length > 0) {
2422
+ vibeWarning(`Unmapped requirements: ${unmapped.join(", ")}`);
1921
2423
  }
1922
- else {
1923
- vibeMilestone(msg);
1924
- }
1925
- },
1926
- feedback: planFeedback,
1927
- availableSkills,
1928
- onAgentCreated: (agent) => {
1929
- abortHandle.currentAgent = agent;
1930
- },
1931
- onAgentFinished: () => {
1932
- abortHandle.currentAgent = undefined;
1933
- },
1934
- });
1935
- if (abortHandle.aborted) {
1936
- vibeGate("Aborted by user (ESC)");
1937
- return;
1938
- }
1939
- if (!plannerResult.completed) {
1940
- vibeError("Planning failed");
1941
- return;
1942
- }
1943
- lastFeatureCount = plannerResult.featureCount;
1944
- // Traceability check
1945
- if (await store.hasRequirements()) {
1946
- const requirements = await store.readRequirements();
1947
- const plan = await store.loadPlan();
1948
- const unmapped = findUnmappedRequirements(requirements, plan);
1949
- if (unmapped.length > 0) {
1950
- vibeWarning(`Unmapped requirements: ${unmapped.join(", ")}`);
1951
2424
  }
1952
- }
2425
+ } // end: skip planner for gate resume
1953
2426
  // Gate: plan approval
2427
+ if (lastFeatureCount === 0 && (await store.hasPlan())) {
2428
+ lastFeatureCount = (await store.loadPlan()).features.length;
2429
+ }
1954
2430
  await saveOrcState("plan_gate");
1955
2431
  const planGateResult = await checkOrchestrationGate("requirePlanApproval", `[Gate] Plan complete (${lastFeatureCount} features). Approve to proceed to orchestration?`);
1956
2432
  if (planGateResult.action === "abort") {
@@ -1965,6 +2441,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1965
2441
  await store.clearOrchestrationState();
1966
2442
  return;
1967
2443
  }
2444
+ if (planGateResult.action === "pause") {
2445
+ vibeGate("Paused at plan gate — use `/vibe resume` to continue");
2446
+ return;
2447
+ }
1968
2448
  if (planGateResult.action === "skip") {
1969
2449
  vibeGate("Plan review skipped");
1970
2450
  planApproved = true;
@@ -1983,9 +2463,13 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1983
2463
  const plan = await store.loadPlan();
1984
2464
  await saveOrcState("orchestrating");
1985
2465
  // ── Standards enrichment (post-Planner) ──
1986
- if (!abortHandle.aborted) {
2466
+ if (!abortHandle.aborted && shouldRunPhase("orchestrating")) {
1987
2467
  await runStandardsEnrichment(store, ctx, config, "planner", abortHandle, pipelineEditor);
1988
2468
  }
2469
+ if (abortHandle.aborted) {
2470
+ vibeGate("Aborted by user (ESC)");
2471
+ return;
2472
+ }
1989
2473
  // Auto/Mixed mode: run Analyze after Planner if enhance-*/refactor-* features exist
1990
2474
  if (!abortHandle.aborted &&
1991
2475
  (workflowType === "auto" || workflowType === "mixed") &&
@@ -1998,6 +2482,12 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1998
2482
  if (needsAnalyze) {
1999
2483
  pipelineEditor.setPhase("Analyzing");
2000
2484
  pipelineEditor.setDetail("");
2485
+ // Inject system-design.md for architectural context during impact analysis
2486
+ let analyzerFeatureContext;
2487
+ if (await store.hasSystemDesign()) {
2488
+ const sd = await store.readSystemDesign();
2489
+ analyzerFeatureContext = `## System Architecture\n\n${sd}`;
2490
+ }
2001
2491
  const analyzerSystemPrompt = getSystemPromptForRole("analyzer");
2002
2492
  const analyzerAgent = await createRoleAgent({
2003
2493
  role: "analyzer",
@@ -2007,10 +2497,14 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2007
2497
  model: ctx.model,
2008
2498
  getApiKey,
2009
2499
  availableSkills,
2500
+ featureContext: analyzerFeatureContext,
2010
2501
  });
2011
2502
  abortHandle.currentAgent = analyzerAgent;
2012
2503
  try {
2013
2504
  const analyzerResponse = await runAgent(analyzerAgent, `Analyze the impact of the following requirements on the existing codebase. Produce an impact-report.md.\n\n${await store.readRequirements()}`, { onEvent: createAgentProgressHandler(ctx, "Analyzing", pipelineEditor, ctx.cwd) });
2505
+ const analyzerUsage2 = aggregateUsage(analyzerAgent.state.messages);
2506
+ const analyzerLines2 = analyzerResponse.split("\n").length;
2507
+ vibeAgentResponse(`Analyzer response (${analyzerLines2} lines)`, analyzerResponse, analyzerUsage2);
2014
2508
  const impactContent = extractArtifactContent(analyzerResponse, "impact-report.md", true);
2015
2509
  if (impactContent) {
2016
2510
  await store.writeGlobalArtifact("impact-report.md", impactContent);
@@ -2051,7 +2545,6 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2051
2545
  onAgentFinished: editorCallbacks.onAgentFinished,
2052
2546
  logger,
2053
2547
  availableSkills,
2054
- projectContext,
2055
2548
  });
2056
2549
  for await (const event of orchestrationGen) {
2057
2550
  // Update tool step context for chat messages
@@ -2064,6 +2557,7 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2064
2557
  vibeEventToChat(ctx, event);
2065
2558
  updateEditorFromEvent(event, pipelineEditor, plan.workflowType, config, options?.parentFeatureId);
2066
2559
  sendStepResultMessage(event, pipelineEditor, event.data?.totalSteps ?? 0);
2560
+ _pi.events.emit("vibe:pipeline", event);
2067
2561
  // Update orchestration state on feature complete/fail/skip (runs before abort check)
2068
2562
  if (event.type === "orchestration_complete" && event.data) {
2069
2563
  await saveOrcState("done", {
@@ -2097,13 +2591,17 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2097
2591
  break;
2098
2592
  }
2099
2593
  }
2100
- // Preserve state when failed features exist to allow resume
2594
+ // Preserve state when failed features exist or user aborted to allow resume
2101
2595
  const finalState = await store.loadOrchestrationState();
2102
- if (finalState && finalState.failedFeatures.length === 0) {
2596
+ if (finalState && finalState.failedFeatures.length === 0 && !abortHandle.aborted) {
2103
2597
  // Run Documenter only when all features succeeded
2104
- if (!abortHandle.aborted && finalState.completedFeatures.length > 0) {
2598
+ if (finalState.completedFeatures.length > 0) {
2105
2599
  await runDocumenter(store, ctx, config, finalState.completedFeatures, pipelineEditor, abortHandle);
2106
2600
  }
2601
+ // Update QMD index with new artifacts
2602
+ if (!abortHandle.aborted) {
2603
+ await updateQmdIndex(ctx.cwd);
2604
+ }
2107
2605
  // Push baseBranch to remote after all features succeeded
2108
2606
  if (!abortHandle.aborted) {
2109
2607
  pipelineEditor.setDetail("pushing to remote...");
@@ -2152,12 +2650,17 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2152
2650
  },
2153
2651
  logger,
2154
2652
  availableSkills,
2155
- projectContext,
2156
2653
  });
2157
2654
  abortHandle.currentRunner = runner;
2158
2655
  activePipelines.set(featureId, { pipeline, runner });
2159
2656
  pipelineEditor.setPhase(`Running: ${featureId}`);
2160
2657
  pipelineEditor.setSteps(pipelineStepsToInfo(pipeline));
2658
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2659
+ _pi.events.emit("vibe:pipeline", {
2660
+ type: "feature_start",
2661
+ featureId,
2662
+ data: { title: featureId, totalSteps: pipeline.steps.length },
2663
+ });
2161
2664
  for await (const event of runner.run(pipeline)) {
2162
2665
  // Update tool step context for chat messages
2163
2666
  if (event.step) {
@@ -2172,6 +2675,7 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2172
2675
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2173
2676
  }
2174
2677
  sendStepResultMessage(event, pipelineEditor, pipeline.steps.length);
2678
+ _pi.events.emit("vibe:pipeline", event);
2175
2679
  if (abortHandle.aborted) {
2176
2680
  vibeGate("Aborted by user (ESC)");
2177
2681
  break;
@@ -2199,6 +2703,190 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2199
2703
  deactivatePipelineEditor(ctx);
2200
2704
  }
2201
2705
  }
2706
+ async function handleRecover(store, ctx) {
2707
+ // Precondition: state already exists
2708
+ if (await store.hasOrchestrationState()) {
2709
+ ctx.ui.notify("[Vibe] Orchestration state already exists. Use /vibe resume instead.", "warning");
2710
+ return;
2711
+ }
2712
+ // Precondition: .vibe/ initialized
2713
+ if (!(await store.isInitialized())) {
2714
+ ctx.ui.notify("[Vibe] No .vibe/ directory found. Run /vibe init first.", "warning");
2715
+ return;
2716
+ }
2717
+ // Precondition: plan.json exists
2718
+ let planJson;
2719
+ try {
2720
+ const plan = await store.loadPlan();
2721
+ planJson = JSON.stringify(plan, null, "\t");
2722
+ }
2723
+ catch {
2724
+ ctx.ui.notify("[Vibe] Cannot recover without plan.json. Run a new /vibe command instead.", "error");
2725
+ return;
2726
+ }
2727
+ // Gather context for the agent
2728
+ const config = await store.loadConfig();
2729
+ const configJson = JSON.stringify(config, null, "\t");
2730
+ // List artifacts per feature
2731
+ const features = await store.listFeatures();
2732
+ const featureArtifacts = [];
2733
+ for (const fId of features) {
2734
+ const featureDir = store.getFeatureDir(fId);
2735
+ try {
2736
+ const entries = await readdir(featureDir);
2737
+ featureArtifacts.push(`${fId}/: ${entries.join(", ")}`);
2738
+ }
2739
+ catch {
2740
+ featureArtifacts.push(`${fId}/: (empty or inaccessible)`);
2741
+ }
2742
+ }
2743
+ // Requirements first lines
2744
+ let requirementSnippet = "";
2745
+ try {
2746
+ const reqPath = join(ctx.cwd, ".vibe", "requirements.md");
2747
+ const reqContent = await readFile(reqPath, "utf-8");
2748
+ requirementSnippet = reqContent.split("\n").slice(0, 5).join("\n");
2749
+ }
2750
+ catch {
2751
+ requirementSnippet = "(requirements.md not found)";
2752
+ }
2753
+ // Git info
2754
+ let gitInfo = "";
2755
+ try {
2756
+ const execFileAsync = promisify(execFile);
2757
+ const { stdout: currentBranch } = await execFileAsync("git", ["branch", "--show-current"], { cwd: ctx.cwd });
2758
+ const { stdout: branches } = await execFileAsync("git", ["branch", "--list"], { cwd: ctx.cwd });
2759
+ const featureBranches = branches
2760
+ .split("\n")
2761
+ .map((b) => b.trim().replace("* ", ""))
2762
+ .filter((b) => b.startsWith("feat/") || b.startsWith("fix/") || b.startsWith("enhance/") || b.startsWith("refactor/"));
2763
+ gitInfo = `Current branch: ${currentBranch.trim()}\nFeature branches:\n${featureBranches.map((b) => ` - ${b}`).join("\n") || " (none)"}`;
2764
+ }
2765
+ catch {
2766
+ gitInfo = "(git info unavailable)";
2767
+ }
2768
+ const contextPrompt = `# Recovery Context
2769
+
2770
+ ## plan.json
2771
+ \`\`\`json
2772
+ ${planJson}
2773
+ \`\`\`
2774
+
2775
+ ## config.json
2776
+ \`\`\`json
2777
+ ${configJson}
2778
+ \`\`\`
2779
+
2780
+ ## Feature Artifacts
2781
+ ${featureArtifacts.join("\n")}
2782
+
2783
+ ## Requirements (first 5 lines)
2784
+ ${requirementSnippet}
2785
+
2786
+ ## Git Info
2787
+ ${gitInfo}
2788
+
2789
+ ## Task
2790
+ Analyze the above context. Use your tools to read artifact contents and check git state as needed. Then propose an orchestration-state.json to restore the workflow. Output the proposed state as a single JSON code block.`;
2791
+ // Run the recovery agent
2792
+ const abortHandle = createAbortHandle();
2793
+ const pipelineEditor = activatePipelineEditor(ctx, "Recovering state", abortHandle);
2794
+ pipelineEditor.setTitle("Vibe ─ Recover");
2795
+ pipelineEditor.setPhase("Analyzing artifacts");
2796
+ const systemPrompt = getSystemPromptForRole("recover");
2797
+ const agent = await createRoleAgent({
2798
+ role: "recover",
2799
+ systemPrompt,
2800
+ config,
2801
+ projectRoot: ctx.cwd,
2802
+ model: ctx.model,
2803
+ getApiKey: (provider) => ctx.modelRegistry.getApiKeyForProvider(provider),
2804
+ });
2805
+ abortHandle.currentAgent = agent;
2806
+ let responseText;
2807
+ try {
2808
+ responseText = await runAgent(agent, contextPrompt, {
2809
+ onEvent: createAgentProgressHandler(ctx, "Recovering", pipelineEditor, ctx.cwd),
2810
+ });
2811
+ }
2812
+ catch (error) {
2813
+ if (abortHandle.aborted) {
2814
+ vibeGate("Recovery aborted by user");
2815
+ }
2816
+ else {
2817
+ const msg = error instanceof Error ? error.message : String(error);
2818
+ vibeError(`Recovery agent failed: ${msg}`);
2819
+ }
2820
+ ctx.ui.setStatus("vibe", undefined);
2821
+ deactivatePipelineEditor(ctx);
2822
+ return;
2823
+ }
2824
+ finally {
2825
+ abortHandle.currentAgent = undefined;
2826
+ }
2827
+ if (abortHandle.aborted) {
2828
+ vibeGate("Recovery aborted by user");
2829
+ ctx.ui.setStatus("vibe", undefined);
2830
+ deactivatePipelineEditor(ctx);
2831
+ return;
2832
+ }
2833
+ ctx.ui.setStatus("vibe", undefined);
2834
+ deactivatePipelineEditor(ctx);
2835
+ // Extract JSON from response
2836
+ const jsonStr = extractRecoverJson(responseText);
2837
+ if (!jsonStr) {
2838
+ ctx.ui.notify("[Vibe] Could not extract orchestration state JSON from agent response.", "error");
2839
+ return;
2840
+ }
2841
+ let proposedState;
2842
+ try {
2843
+ proposedState = JSON.parse(jsonStr);
2844
+ }
2845
+ catch {
2846
+ ctx.ui.notify("[Vibe] Agent returned invalid JSON.", "error");
2847
+ return;
2848
+ }
2849
+ // Validate required fields
2850
+ if (!proposedState.phase || !proposedState.workflowType || !Array.isArray(proposedState.completedFeatures)) {
2851
+ ctx.ui.notify("[Vibe] Agent returned incomplete state (missing phase, workflowType, or completedFeatures).", "error");
2852
+ return;
2853
+ }
2854
+ // Ensure updatedAt
2855
+ if (!proposedState.updatedAt) {
2856
+ proposedState.updatedAt = new Date().toISOString();
2857
+ }
2858
+ // Ensure arrays
2859
+ if (!Array.isArray(proposedState.failedFeatures))
2860
+ proposedState.failedFeatures = [];
2861
+ if (!Array.isArray(proposedState.skippedFeatures))
2862
+ proposedState.skippedFeatures = [];
2863
+ // Display proposed state and ask for confirmation
2864
+ const summary = [
2865
+ `Workflow: ${proposedState.workflowType}`,
2866
+ `Phase: ${proposedState.phase}`,
2867
+ `Base branch: ${proposedState.baseBranch ?? "(not set)"}`,
2868
+ `Completed: ${proposedState.completedFeatures.length > 0 ? proposedState.completedFeatures.join(", ") : "(none)"}`,
2869
+ `Failed: ${proposedState.failedFeatures.length > 0 ? proposedState.failedFeatures.join(", ") : "(none)"}`,
2870
+ `Skipped: ${proposedState.skippedFeatures.length > 0 ? proposedState.skippedFeatures.join(", ") : "(none)"}`,
2871
+ ].join("\n");
2872
+ const choice = await ctx.ui.select(`Proposed recovery state:\n${summary}\n\nSave and enable /vibe resume?`, [
2873
+ "Approve",
2874
+ "Abort",
2875
+ ]);
2876
+ if (choice === "Approve") {
2877
+ await store.saveOrchestrationState(proposedState);
2878
+ ctx.ui.notify("[Vibe] Orchestration state recovered. Use /vibe resume to continue.", "info");
2879
+ vibeMilestone("Orchestration state recovered successfully");
2880
+ }
2881
+ else {
2882
+ vibeMilestone("Recovery aborted by user");
2883
+ }
2884
+ }
2885
+ /** Extract JSON code block from agent response text. */
2886
+ export function extractRecoverJson(text) {
2887
+ const match = text.match(/```json\s*\n([\s\S]*?)\n```/);
2888
+ return match ? match[1] : null;
2889
+ }
2202
2890
  async function handlePause(featureId, ctx) {
2203
2891
  const active = activePipelines.get(featureId);
2204
2892
  if (!active) {
@@ -2214,6 +2902,12 @@ async function handleResume(store, ctx, featureId, logger) {
2214
2902
  if (active) {
2215
2903
  const abortHandle = createAbortHandle();
2216
2904
  abortHandle.currentRunner = active.runner;
2905
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2906
+ _pi.events.emit("vibe:pipeline", {
2907
+ type: "feature_start",
2908
+ featureId,
2909
+ data: { totalSteps: active.pipeline.steps.length },
2910
+ });
2217
2911
  const pipelineEditor = activatePipelineEditor(ctx, featureId, abortHandle);
2218
2912
  pipelineEditor.setTitle(`Vibe ─ Resuming`);
2219
2913
  pipelineEditor.setPhase(featureId);
@@ -2229,6 +2923,7 @@ async function handleResume(store, ctx, featureId, logger) {
2229
2923
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2230
2924
  }
2231
2925
  sendStepResultMessage(event, pipelineEditor, active.pipeline.steps.length);
2926
+ _pi.events.emit("vibe:pipeline", event);
2232
2927
  if (abortHandle.aborted) {
2233
2928
  vibeGate("Aborted by user (ESC)");
2234
2929
  break;
@@ -2274,19 +2969,48 @@ async function handleResume(store, ctx, featureId, logger) {
2274
2969
  pipelineEditor.setPhase(persistedPhase);
2275
2970
  pipelineEditor.setSteps(pipelineStepsToInfo(pipeline));
2276
2971
  pipelineEditor.setCurrentStep(pipeline.currentStep);
2972
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2973
+ _pi.events.emit("vibe:pipeline", {
2974
+ type: "feature_start",
2975
+ featureId,
2976
+ data: { title: persistedPhase, totalSteps: pipeline.steps.length },
2977
+ });
2978
+ // Build requestApproval for pipeline-internal gates (merge_approve, etc.)
2979
+ const persistedRequestApproval = async (_step, _fId, summary) => {
2980
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
2981
+ const wasExpanded = ctx.ui.getToolsExpanded();
2982
+ if (!wasExpanded)
2983
+ ctx.ui.setToolsExpanded(true);
2984
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
2985
+ if (!wasExpanded)
2986
+ ctx.ui.setToolsExpanded(false);
2987
+ switch (choice) {
2988
+ case "Approve":
2989
+ return { passed: true, action: "approved" };
2990
+ case "Reject": {
2991
+ const feedback = await ctx.ui.input("Feedback", "Enter reason for rejection");
2992
+ return { passed: false, action: "feedback", feedback: feedback ?? "" };
2993
+ }
2994
+ case "Skip":
2995
+ return { passed: true, action: "skip" };
2996
+ case "Abort":
2997
+ return { passed: false, action: "abort" };
2998
+ default:
2999
+ return { passed: false, action: "pause" };
3000
+ }
3001
+ };
2277
3002
  const persistedToolCtx = { featureId: featureId, role: "", action: "" };
2278
3003
  const persistedCallbacks = createEditorAgentCallbacks(pipelineEditor, ctx.cwd, persistedAbortHandle, persistedToolCtx);
2279
- const resumeProjectContext = (await store.hasProjectContext()) ? await store.readProjectContext() : undefined;
2280
3004
  const runner = new PipelineRunner({
2281
3005
  store,
2282
3006
  config,
2283
3007
  projectRoot: ctx.cwd,
3008
+ requestApproval: persistedRequestApproval,
2284
3009
  getApiKey,
2285
3010
  model: ctx.model,
2286
3011
  onAgentCreated: persistedCallbacks.onAgentCreated,
2287
3012
  onAgentFinished: persistedCallbacks.onAgentFinished,
2288
3013
  logger,
2289
- projectContext: resumeProjectContext,
2290
3014
  });
2291
3015
  persistedAbortHandle.currentRunner = runner;
2292
3016
  try {
@@ -2304,6 +3028,7 @@ async function handleResume(store, ctx, featureId, logger) {
2304
3028
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2305
3029
  }
2306
3030
  sendStepResultMessage(event, pipelineEditor, pipeline.steps.length);
3031
+ _pi.events.emit("vibe:pipeline", event);
2307
3032
  if (persistedAbortHandle.aborted) {
2308
3033
  vibeGate("Aborted by user (ESC)");
2309
3034
  break;
@@ -2335,7 +3060,12 @@ async function handleResume(store, ctx, featureId, logger) {
2335
3060
  const orcState = await store.loadOrchestrationState();
2336
3061
  if (orcState) {
2337
3062
  vibeMilestone(`Resuming orchestration from phase: ${orcState.phase}`);
2338
- if (orcState.phase === "single_pipeline" && orcState.singleFeatureId) {
3063
+ if (orcState.phase === "single_pipeline") {
3064
+ if (!orcState.singleFeatureId) {
3065
+ ctx.ui.notify("[Vibe] Cannot resume: single pipeline state is missing featureId. Run a new /vibe command.", "error");
3066
+ await store.clearOrchestrationState();
3067
+ return;
3068
+ }
2339
3069
  // Single-feature path: recursive call with the featureId
2340
3070
  await handleResume(store, ctx, orcState.singleFeatureId, logger);
2341
3071
  return;
@@ -2348,7 +3078,38 @@ async function handleResume(store, ctx, featureId, logger) {
2348
3078
  config = { ...config, baseBranch: orcState.baseBranch };
2349
3079
  }
2350
3080
  const getApiKey = (provider) => ctx.modelRegistry.getApiKeyForProvider(provider);
2351
- const plan = await store.loadPlan();
3081
+ let plan;
3082
+ try {
3083
+ plan = await store.loadPlan();
3084
+ }
3085
+ catch {
3086
+ ctx.ui.notify("[Vibe] Cannot resume: plan.json is missing or corrupted. Run a new /vibe command.", "error");
3087
+ return;
3088
+ }
3089
+ // Build requestApproval for pipeline-internal gates (merge_approve, etc.)
3090
+ const requestApproval = async (_step, _fId, summary) => {
3091
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
3092
+ const wasExpanded = ctx.ui.getToolsExpanded();
3093
+ if (!wasExpanded)
3094
+ ctx.ui.setToolsExpanded(true);
3095
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
3096
+ if (!wasExpanded)
3097
+ ctx.ui.setToolsExpanded(false);
3098
+ switch (choice) {
3099
+ case "Approve":
3100
+ return { passed: true, action: "approved" };
3101
+ case "Reject": {
3102
+ const feedback = await ctx.ui.input("Feedback", "Enter reason for rejection");
3103
+ return { passed: false, action: "feedback", feedback: feedback ?? "" };
3104
+ }
3105
+ case "Skip":
3106
+ return { passed: true, action: "skip" };
3107
+ case "Abort":
3108
+ return { passed: false, action: "abort" };
3109
+ default:
3110
+ return { passed: false, action: "pause" };
3111
+ }
3112
+ };
2352
3113
  vibeMilestone(`Resuming orchestration: ${orcState.completedFeatures.length} completed, ${plan.features.length - orcState.completedFeatures.length} remaining`);
2353
3114
  // Reset phase to orchestrating on resume
2354
3115
  orcState.phase = "orchestrating";
@@ -2360,15 +3121,13 @@ async function handleResume(store, ctx, featureId, logger) {
2360
3121
  const resumeToolCtx = { featureId: "", role: "", action: "" };
2361
3122
  const resumeOrcCallbacks = createEditorAgentCallbacks(pipelineEditor, ctx.cwd, resumeAbortHandle, resumeToolCtx);
2362
3123
  const availableSkills = await buildAvailableSkillsPrompt(store);
2363
- const resumeOrcProjectContext = (await store.hasProjectContext())
2364
- ? await store.readProjectContext()
2365
- : undefined;
2366
3124
  try {
2367
3125
  for await (const event of runOrchestration({
2368
3126
  store,
2369
3127
  config,
2370
3128
  projectRoot: ctx.cwd,
2371
3129
  plan,
3130
+ requestApproval,
2372
3131
  getApiKey,
2373
3132
  model: ctx.model,
2374
3133
  parentFeatureId: orcState.options?.parentFeatureId,
@@ -2377,7 +3136,6 @@ async function handleResume(store, ctx, featureId, logger) {
2377
3136
  onAgentFinished: resumeOrcCallbacks.onAgentFinished,
2378
3137
  logger,
2379
3138
  availableSkills,
2380
- projectContext: resumeOrcProjectContext,
2381
3139
  })) {
2382
3140
  // Update tool step context for chat messages
2383
3141
  if (event.type === "feature_start")
@@ -2389,6 +3147,7 @@ async function handleResume(store, ctx, featureId, logger) {
2389
3147
  vibeEventToChat(ctx, event);
2390
3148
  updateEditorFromEvent(event, pipelineEditor, plan.workflowType, config, orcState.options?.parentFeatureId);
2391
3149
  sendStepResultMessage(event, pipelineEditor, event.data?.totalSteps ?? 0);
3150
+ _pi.events.emit("vibe:pipeline", event);
2392
3151
  // Update orchestration state: overwrite with final result on orchestration_complete
2393
3152
  if (event.type === "orchestration_complete" && event.data) {
2394
3153
  const currentState = await store.loadOrchestrationState();
@@ -2426,13 +3185,17 @@ async function handleResume(store, ctx, featureId, logger) {
2426
3185
  break;
2427
3186
  }
2428
3187
  }
2429
- // Preserve state when failed features exist to allow resume
3188
+ // Preserve state when failed features exist or user aborted to allow resume
2430
3189
  const finalState = await store.loadOrchestrationState();
2431
- if (finalState && finalState.failedFeatures.length === 0) {
3190
+ if (finalState && finalState.failedFeatures.length === 0 && !resumeAbortHandle.aborted) {
2432
3191
  // Run Documenter only when all features succeeded
2433
- if (!resumeAbortHandle.aborted && finalState.completedFeatures.length > 0) {
3192
+ if (finalState.completedFeatures.length > 0) {
2434
3193
  await runDocumenter(store, ctx, config, finalState.completedFeatures, pipelineEditor, resumeAbortHandle);
2435
3194
  }
3195
+ // Update QMD index with new artifacts
3196
+ if (!resumeAbortHandle.aborted) {
3197
+ await updateQmdIndex(ctx.cwd);
3198
+ }
2436
3199
  // Push baseBranch to remote after all features succeeded
2437
3200
  if (!resumeAbortHandle.aborted) {
2438
3201
  pipelineEditor.setDetail("pushing to remote...");