@wizdear/atlas-code 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-factory.d.ts +10 -5
  3. package/dist/agent-factory.d.ts.map +1 -1
  4. package/dist/agent-factory.js +50 -13
  5. package/dist/agent-factory.js.map +1 -1
  6. package/dist/cli.d.ts +7 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +72 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/discovery.d.ts +9 -2
  11. package/dist/discovery.d.ts.map +1 -1
  12. package/dist/discovery.js +4 -5
  13. package/dist/discovery.js.map +1 -1
  14. package/dist/extension.d.ts +9 -2
  15. package/dist/extension.d.ts.map +1 -1
  16. package/dist/extension.js +1103 -381
  17. package/dist/extension.js.map +1 -1
  18. package/dist/gate.d.ts +1 -1
  19. package/dist/gate.d.ts.map +1 -1
  20. package/dist/gate.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/orchestrator.d.ts +0 -4
  26. package/dist/orchestrator.d.ts.map +1 -1
  27. package/dist/orchestrator.js +0 -2
  28. package/dist/orchestrator.js.map +1 -1
  29. package/dist/pipeline-editor.d.ts +2 -0
  30. package/dist/pipeline-editor.d.ts.map +1 -1
  31. package/dist/pipeline-editor.js +36 -5
  32. package/dist/pipeline-editor.js.map +1 -1
  33. package/dist/pipeline.d.ts +2 -10
  34. package/dist/pipeline.d.ts.map +1 -1
  35. package/dist/pipeline.js +4 -6
  36. package/dist/pipeline.js.map +1 -1
  37. package/dist/planner.d.ts +9 -2
  38. package/dist/planner.d.ts.map +1 -1
  39. package/dist/planner.js +15 -11
  40. package/dist/planner.js.map +1 -1
  41. package/dist/roles/architect.d.ts +1 -1
  42. package/dist/roles/architect.d.ts.map +1 -1
  43. package/dist/roles/architect.js +1 -1
  44. package/dist/roles/architect.js.map +1 -1
  45. package/dist/roles/cicd.d.ts +1 -1
  46. package/dist/roles/cicd.d.ts.map +1 -1
  47. package/dist/roles/cicd.js +5 -0
  48. package/dist/roles/cicd.js.map +1 -1
  49. package/dist/roles/documenter.d.ts +1 -1
  50. package/dist/roles/documenter.d.ts.map +1 -1
  51. package/dist/roles/documenter.js +11 -0
  52. package/dist/roles/documenter.js.map +1 -1
  53. package/dist/roles/index.d.ts +1 -0
  54. package/dist/roles/index.d.ts.map +1 -1
  55. package/dist/roles/index.js +3 -0
  56. package/dist/roles/index.js.map +1 -1
  57. package/dist/roles/recover.d.ts +5 -0
  58. package/dist/roles/recover.d.ts.map +1 -0
  59. package/dist/roles/recover.js +82 -0
  60. package/dist/roles/recover.js.map +1 -0
  61. package/dist/roles/reviewer.d.ts +1 -1
  62. package/dist/roles/reviewer.d.ts.map +1 -1
  63. package/dist/roles/reviewer.js +7 -1
  64. package/dist/roles/reviewer.js.map +1 -1
  65. package/dist/roles/standards-enricher.d.ts +1 -1
  66. package/dist/roles/standards-enricher.d.ts.map +1 -1
  67. package/dist/roles/standards-enricher.js +8 -0
  68. package/dist/roles/standards-enricher.js.map +1 -1
  69. package/dist/roles/tester.d.ts +1 -1
  70. package/dist/roles/tester.d.ts.map +1 -1
  71. package/dist/roles/tester.js +7 -0
  72. package/dist/roles/tester.js.map +1 -1
  73. package/dist/router.d.ts.map +1 -1
  74. package/dist/router.js +6 -6
  75. package/dist/router.js.map +1 -1
  76. package/dist/standards.d.ts +37 -11
  77. package/dist/standards.d.ts.map +1 -1
  78. package/dist/standards.js +71 -89
  79. package/dist/standards.js.map +1 -1
  80. package/dist/step-executor.d.ts +15 -2
  81. package/dist/step-executor.d.ts.map +1 -1
  82. package/dist/step-executor.js +138 -30
  83. package/dist/step-executor.js.map +1 -1
  84. package/dist/store.d.ts +3 -10
  85. package/dist/store.d.ts.map +1 -1
  86. package/dist/store.js +45 -57
  87. package/dist/store.js.map +1 -1
  88. package/dist/system-architect.d.ts +9 -2
  89. package/dist/system-architect.d.ts.map +1 -1
  90. package/dist/system-architect.js +6 -10
  91. package/dist/system-architect.js.map +1 -1
  92. package/dist/telegram/bridge.d.ts +39 -0
  93. package/dist/telegram/bridge.d.ts.map +1 -0
  94. package/dist/telegram/bridge.js +380 -0
  95. package/dist/telegram/bridge.js.map +1 -0
  96. package/dist/telegram/formatter.d.ts +15 -0
  97. package/dist/telegram/formatter.d.ts.map +1 -0
  98. package/dist/telegram/formatter.js +86 -0
  99. package/dist/telegram/formatter.js.map +1 -0
  100. package/dist/telegram/renderer.d.ts +45 -0
  101. package/dist/telegram/renderer.d.ts.map +1 -0
  102. package/dist/telegram/renderer.js +150 -0
  103. package/dist/telegram/renderer.js.map +1 -0
  104. package/dist/telegram/telegram-api.d.ts +84 -0
  105. package/dist/telegram/telegram-api.d.ts.map +1 -0
  106. package/dist/telegram/telegram-api.js +134 -0
  107. package/dist/telegram/telegram-api.js.map +1 -0
  108. package/dist/types.d.ts +10 -1
  109. package/dist/types.d.ts.map +1 -1
  110. package/dist/types.js.map +1 -1
  111. package/dist/ui.d.ts +1 -1
  112. package/dist/ui.d.ts.map +1 -1
  113. package/dist/ui.js +2 -0
  114. package/dist/ui.js.map +1 -1
  115. 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 ──────────────────────────────────────────────────────
@@ -53,6 +55,16 @@ async function gitPush(cwd) {
53
55
  return false;
54
56
  }
55
57
  }
58
+ /** Check if the cwd is inside a git repository. */
59
+ async function isGitRepo(cwd) {
60
+ try {
61
+ await execFileAsync("git", ["rev-parse", "--git-dir"], { cwd });
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
56
68
  /** Check if a local git branch exists. */
57
69
  async function branchExists(cwd, branch) {
58
70
  try {
@@ -119,16 +131,21 @@ async function getFileMtime(filePath) {
119
131
  return null;
120
132
  }
121
133
  }
122
- // ─── QMD Cleanup ─────────────────────────────────────────────────────────────
123
- /** Remove stale QMD collections if qmd is installed. Silently skips if qmd is not available. */
124
- async function cleanupQmdCollections(cwd) {
134
+ // ─── QMD Lifecycle ───────────────────────────────────────────────────────────
135
+ /** Check if qmd CLI is available on the system. */
136
+ async function isQmdInstalled() {
125
137
  try {
126
138
  await execFileAsync("which", ["qmd"]);
139
+ return true;
127
140
  }
128
141
  catch {
129
- // qmd not installed — skip silently
130
- return;
142
+ return false;
131
143
  }
144
+ }
145
+ /** Remove stale QMD collections if qmd is installed. Silently skips if qmd is not available. */
146
+ async function cleanupQmdCollections(cwd) {
147
+ if (!(await isQmdInstalled()))
148
+ return;
132
149
  const indexName = basename(cwd);
133
150
  const collections = ["vibe-artifacts", "vibe-standards"];
134
151
  for (const col of collections) {
@@ -143,6 +160,52 @@ async function cleanupQmdCollections(cwd) {
143
160
  }
144
161
  }
145
162
  }
163
+ /** Initialize QMD collection for .vibe/features/. Idempotent. */
164
+ async function initQmdCollections(cwd) {
165
+ if (!(await isQmdInstalled()))
166
+ return;
167
+ const indexName = basename(cwd);
168
+ const env = { ...process.env, QMD_EMBED_MODEL: QMD_EMBED_MODEL_URI };
169
+ const featuresDir = join(cwd, ".vibe", "features");
170
+ try {
171
+ // Check if collection already exists
172
+ const { stdout } = await execFileAsync("qmd", ["--index", indexName, "collection", "list"], { cwd });
173
+ if (stdout.includes("vibe-artifacts"))
174
+ return;
175
+ }
176
+ catch {
177
+ // No collections yet — proceed with init
178
+ }
179
+ try {
180
+ await execFileAsync("qmd", ["--index", indexName, "collection", "add", featuresDir, "--name", "vibe-artifacts", "--mask", "**/*.md"], { cwd });
181
+ await execFileAsync("qmd", [
182
+ "--index",
183
+ indexName,
184
+ "context",
185
+ "add",
186
+ "qmd://vibe-artifacts",
187
+ "Feature artifacts: spec, design, review, test-report, diagnosis, impact-report",
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";
@@ -237,15 +310,13 @@ PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
237
310
  INDEX_NAME=$(basename "$PROJECT_ROOT")
238
311
  qmd --index "$INDEX_NAME" status
239
312
  \`\`\`
240
- If no collections exist, initialize both artifacts and standards:
313
+ If no collections exist, initialize the artifacts collection:
241
314
  \`\`\`bash
242
315
  export QMD_EMBED_MODEL="${QMD_EMBED_MODEL_URI}"
243
316
  PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
244
317
  INDEX_NAME=$(basename "$PROJECT_ROOT")
245
318
  qmd --index "$INDEX_NAME" collection add "$PROJECT_ROOT/.vibe/features" --name vibe-artifacts --mask "**/*.md"
246
319
  qmd --index "$INDEX_NAME" context add qmd://vibe-artifacts "Feature artifacts: spec, design, review, test-report, diagnosis, impact-report"
247
- qmd --index "$INDEX_NAME" collection add "$PROJECT_ROOT/.vibe/standards" --name vibe-standards --mask "**/*.md"
248
- qmd --index "$INDEX_NAME" context add qmd://vibe-standards "Project coding standards, architecture principles, testing policies"
249
320
  qmd --index "$INDEX_NAME" embed
250
321
  \`\`\`
251
322
 
@@ -258,12 +329,6 @@ export QMD_EMBED_MODEL="${QMD_EMBED_MODEL_URI}"
258
329
  INDEX_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
259
330
  qmd --index "$INDEX_NAME" search "<keywords>" -c vibe-artifacts -n 5 --min-score 0.3 --json
260
331
  \`\`\`
261
- Search coding standards:
262
- \`\`\`bash
263
- export QMD_EMBED_MODEL="${QMD_EMBED_MODEL_URI}"
264
- INDEX_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
265
- qmd --index "$INDEX_NAME" search "<keywords>" -c vibe-standards -n 5 --min-score 0.3 --json
266
- \`\`\`
267
332
  If results are returned, read the referenced files and factor past decisions into current work.
268
333
 
269
334
  ### During Review
@@ -291,6 +356,9 @@ qmd --index "$INDEX_NAME" embed
291
356
  // ─── Session Message Helper ──────────────────────────────────────────────────
292
357
  /** Extension API reference. Set by the vibeExtension factory. */
293
358
  let _pi;
359
+ /** Shared Telegram command registry. Commands are pushed here during factory setup
360
+ * and read by the bridge polling loop (which starts later during session_start). */
361
+ const _telegramCommands = [];
294
362
  // ─── Agent Response Preview ──────────────────────────────────────────────────
295
363
  /** Number of preview lines shown when agent-response is collapsed. */
296
364
  export const AGENT_RESPONSE_PREVIEW_LINES = 5;
@@ -307,13 +375,17 @@ export function computeAgentResponsePreview(fullText, maxLines = AGENT_RESPONSE_
307
375
  return { preview, remaining: allLines.length - maxLines };
308
376
  }
309
377
  /** Sends a categorized vibe message to the session. */
310
- function sendVibeMessage(content, category, fullText) {
378
+ function sendVibeMessage(content, category, fullText, usage) {
311
379
  _pi.sendMessage({
312
380
  customType: "vibe",
313
381
  content,
314
382
  display: true,
315
- details: { category, fullText },
383
+ details: { category, fullText, usage },
316
384
  });
385
+ // Also emit via EventBus so the Telegram bridge can receive it.
386
+ // pi.sendMessage() → sendCustomMessage() only fires _emit() (TUI listeners),
387
+ // not _emitExtensionEvent(), so pi.on("message_end") never fires for custom messages.
388
+ _pi.events?.emit("vibe:message", { content, category, fullText, usage });
317
389
  }
318
390
  function vibeCommand(text) {
319
391
  sendVibeMessage(text, "command");
@@ -333,8 +405,23 @@ function vibeGate(text) {
333
405
  function vibeEvent(text) {
334
406
  sendVibeMessage(text, "event");
335
407
  }
336
- function vibeAgentResponse(summary, fullText) {
337
- sendVibeMessage(summary, "agent-response", fullText);
408
+ function vibeAgentResponse(summary, fullText, usage) {
409
+ sendVibeMessage(summary, "agent-response", fullText, usage);
410
+ }
411
+ /**
412
+ * Wait for gate choice from TUI select() or Telegram inline keyboard.
413
+ * Whichever responds first wins; the other is dismissed.
414
+ */
415
+ async function waitForGateChoice(events, ui, summary, options) {
416
+ const abortController = new AbortController();
417
+ let telegramChoice;
418
+ const unsubGateResponse = events.on("vibe:gate_response", (raw) => {
419
+ telegramChoice = raw.choice;
420
+ abortController.abort();
421
+ });
422
+ const tuiChoice = await ui.select(summary, options, { signal: abortController.signal });
423
+ unsubGateResponse();
424
+ return tuiChoice ?? telegramChoice;
338
425
  }
339
426
  /**
340
427
  * Forwards only chat-visible events as vibeMessages.
@@ -463,8 +550,42 @@ function formatToolResult(toolName, args, result, isError, projectRoot) {
463
550
  * Creates a handler that displays agent event progress in PipelineEditorComponent (or status bar).
464
551
  * Uses the editor when active; falls back to the status bar otherwise.
465
552
  */
553
+ /** Extracts the last non-empty line from an AssistantMessage's text content. */
554
+ function getLastNonEmptyLine(partial) {
555
+ for (let i = partial.content.length - 1; i >= 0; i--) {
556
+ const c = partial.content[i];
557
+ if (c.type === "text") {
558
+ const lines = c.text.split("\n");
559
+ for (let j = lines.length - 1; j >= 0; j--) {
560
+ const trimmed = lines[j].trim();
561
+ if (trimmed)
562
+ return trimmed;
563
+ }
564
+ }
565
+ }
566
+ return undefined;
567
+ }
568
+ /** Extract the last N non-empty lines from a partial assistant message. */
569
+ function getLastNonEmptyLines(partial, maxLines) {
570
+ const allLines = [];
571
+ for (const c of partial.content) {
572
+ if (c.type === "text") {
573
+ for (const line of c.text.split("\n")) {
574
+ const trimmed = line.trim();
575
+ if (trimmed)
576
+ allLines.push(trimmed);
577
+ }
578
+ }
579
+ }
580
+ if (allLines.length === 0)
581
+ return undefined;
582
+ return allLines.slice(-maxLines).join("\n");
583
+ }
584
+ /** Throttle interval for LLM streaming detail updates (ms). */
585
+ const STREAMING_THROTTLE_MS = 100;
466
586
  function createAgentProgressHandler(ctx, label, pipelineEditor, projectRoot) {
467
587
  const pendingArgs = new Map();
588
+ let lastDetailUpdate = 0;
468
589
  return (event) => {
469
590
  if (event.type === "tool_execution_start") {
470
591
  const args = event.args;
@@ -495,40 +616,62 @@ function createAgentProgressHandler(ctx, label, pipelineEditor, projectRoot) {
495
616
  },
496
617
  });
497
618
  }
619
+ if (event.type === "message_update") {
620
+ const ame = event.assistantMessageEvent;
621
+ const now = Date.now();
622
+ if (ame.type === "text_delta" || ame.type === "text_end") {
623
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "text_end")
624
+ return;
625
+ lastDetailUpdate = now;
626
+ if (pipelineEditor) {
627
+ // Show last N lines in the pipeline editor box
628
+ const recentLines = getLastNonEmptyLines(ame.partial, 6);
629
+ if (recentLines) {
630
+ pipelineEditor.setDetail(`✎ ${recentLines}`);
631
+ }
632
+ }
633
+ else {
634
+ const lastLine = getLastNonEmptyLine(ame.partial);
635
+ if (lastLine) {
636
+ ctx.ui.setStatus("vibe", `${label}: ✎ ${lastLine}`);
637
+ }
638
+ }
639
+ // On text_end, send completed text block to chat
640
+ if (ame.type === "text_end") {
641
+ const completedText = ame.content.trim();
642
+ if (completedText) {
643
+ const lines = completedText.split("\n").length;
644
+ sendVibeMessage(`[${label}] ✎ (${lines} lines)`, "agent-text", completedText);
645
+ }
646
+ }
647
+ }
648
+ if (ame.type === "thinking_delta" || ame.type === "thinking_start") {
649
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "thinking_start")
650
+ return;
651
+ lastDetailUpdate = now;
652
+ if (pipelineEditor) {
653
+ pipelineEditor.setDetail("thinking...");
654
+ }
655
+ else {
656
+ ctx.ui.setStatus("vibe", `${label}: thinking...`);
657
+ }
658
+ }
659
+ if (ame.type === "done") {
660
+ const msg = ame.message;
661
+ if (msg.usage && msg.usage.totalTokens > 0) {
662
+ const fmt = (n) => n.toLocaleString("en-US");
663
+ const tokenInfo = `tokens: ${fmt(msg.usage.input)} in / ${fmt(msg.usage.output)} out`;
664
+ if (pipelineEditor) {
665
+ pipelineEditor.addActivity(tokenInfo);
666
+ }
667
+ }
668
+ }
669
+ }
498
670
  };
499
671
  }
500
672
  function createAbortHandle() {
501
673
  return { aborted: false };
502
674
  }
503
- // ─── Skill Discovery ─────────────────────────────────────────────────────────
504
- /**
505
- * Scans .vibe/skills/ and builds a lightweight index of available skills.
506
- * Contains only each skill's name, description, and file path.
507
- * Agents load the full content via the read tool when needed.
508
- */
509
- async function buildAvailableSkillsPrompt(store) {
510
- const skills = await store.listSkills();
511
- if (skills.length === 0)
512
- return "";
513
- const entries = [];
514
- for (const skillName of skills) {
515
- const description = await store.readSkillDescription(skillName);
516
- if (description) {
517
- const skillPath = `.vibe/skills/${skillName}/SKILL.md`;
518
- entries.push(`- **${skillName}** (${skillPath}): ${description}`);
519
- }
520
- }
521
- if (entries.length === 0)
522
- return "";
523
- return [
524
- "## Available Skills",
525
- "",
526
- "The following skills provide specialized instructions for this project.",
527
- "Use the read tool to load a skill's SKILL.md when your current task matches its description.",
528
- "",
529
- ...entries,
530
- ].join("\n");
531
- }
532
675
  // ─── Pipeline Editor Helpers ──────────────────────────────────────────────────
533
676
  /**
534
677
  * Activates the pipeline editor by replacing the default editor with PipelineEditorComponent.
@@ -585,10 +728,12 @@ function pipelineStepsToInfo(pipeline) {
585
728
  function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, stepContext) {
586
729
  let unsubscribe;
587
730
  const pendingArgs = new Map();
731
+ let lastDetailUpdate = 0;
588
732
  return {
589
733
  onAgentCreated: (agent) => {
590
734
  unsubscribe?.();
591
735
  pendingArgs.clear();
736
+ lastDetailUpdate = 0;
592
737
  if (abortHandle) {
593
738
  abortHandle.currentAgent = agent;
594
739
  }
@@ -598,12 +743,14 @@ function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, st
598
743
  pendingArgs.set(event.toolCallId, args);
599
744
  const detail = formatToolProgress(event.toolName, args, projectRoot);
600
745
  pipelineEditor.addActivity(detail);
746
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: detail, type: "tool" });
601
747
  }
602
748
  if (event.type === "tool_execution_end") {
603
749
  const args = pendingArgs.get(event.toolCallId) ?? {};
604
750
  pendingArgs.delete(event.toolCallId);
605
751
  const summary = formatToolResult(event.toolName, args, event.result, event.isError, projectRoot);
606
752
  pipelineEditor.addActivity(summary);
753
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: summary, type: "tool_result" });
607
754
  // Send tool execution to chat for ToolExecutionComponent rendering
608
755
  if (stepContext) {
609
756
  const label = formatToolProgress(event.toolName, args, projectRoot);
@@ -622,6 +769,46 @@ function createEditorAgentCallbacks(pipelineEditor, projectRoot, abortHandle, st
622
769
  });
623
770
  }
624
771
  }
772
+ if (event.type === "message_update") {
773
+ const ame = event.assistantMessageEvent;
774
+ const now = Date.now();
775
+ if (ame.type === "thinking_start") {
776
+ pipelineEditor.addActivity("thinking...");
777
+ }
778
+ if (ame.type === "text_delta" || ame.type === "text_end") {
779
+ if (now - lastDetailUpdate < STREAMING_THROTTLE_MS && ame.type !== "text_end")
780
+ return;
781
+ lastDetailUpdate = now;
782
+ const lastLine = getLastNonEmptyLine(ame.partial);
783
+ if (lastLine) {
784
+ const entry = `✎ ${lastLine}`;
785
+ const log = pipelineEditor.getActivityLog();
786
+ const last = log[log.length - 1];
787
+ if (last?.startsWith("✎")) {
788
+ pipelineEditor.updateLastActivity(entry);
789
+ }
790
+ else {
791
+ pipelineEditor.addActivity(entry);
792
+ }
793
+ _pi.events.emit("vibe:activity", { role: stepContext?.role ?? "", text: entry, type: "text" });
794
+ }
795
+ if (ame.type === "text_end" && stepContext) {
796
+ const completedText = ame.content.trim();
797
+ if (completedText) {
798
+ const lines = completedText.split("\n").length;
799
+ sendVibeMessage(`[${stepContext.role}] ✎ (${lines} lines)`, "agent-text", completedText);
800
+ }
801
+ }
802
+ }
803
+ if (ame.type === "done") {
804
+ const msg = ame.message;
805
+ if (msg.usage && msg.usage.totalTokens > 0) {
806
+ const fmt = (n) => n.toLocaleString("en-US");
807
+ const tokenInfo = `tokens: ${fmt(msg.usage.input)} in / ${fmt(msg.usage.output)} out`;
808
+ pipelineEditor.addActivity(tokenInfo);
809
+ }
810
+ }
811
+ }
625
812
  });
626
813
  },
627
814
  onAgentFinished: () => {
@@ -682,15 +869,19 @@ async function runStandardsEnrichment(store, ctx, config, source, abortHandle, p
682
869
  context = parts.join("\n\n---\n\n");
683
870
  }
684
871
  else {
685
- const requirements = await store.readRequirements();
872
+ const parts = [];
873
+ parts.push(await store.readRequirements());
686
874
  const plan = await store.loadPlan();
687
- const specs = [];
875
+ parts.push(JSON.stringify(plan, null, 2));
688
876
  for (const feature of plan.features) {
689
877
  if (await store.hasArtifact(feature.featureId, "spec.md")) {
690
- specs.push(await store.readArtifact(feature.featureId, "spec.md"));
878
+ parts.push(await store.readArtifact(feature.featureId, "spec.md"));
691
879
  }
692
880
  }
693
- context = [requirements, JSON.stringify(plan, null, 2), ...specs].join("\n\n---\n\n");
881
+ if (await store.hasSystemDesign()) {
882
+ parts.push(await store.readSystemDesign());
883
+ }
884
+ context = parts.join("\n\n---\n\n");
694
885
  }
695
886
  }
696
887
  catch {
@@ -718,6 +909,10 @@ async function runStandardsEnrichment(store, ctx, config, source, abortHandle, p
718
909
  }
719
910
  catch (error) {
720
911
  const msg = error instanceof Error ? error.message : String(error);
912
+ if (msg.includes("aborted")) {
913
+ // Abort errors are not re-thrown. The caller checks abortHandle.aborted.
914
+ return;
915
+ }
721
916
  vibeWarning(`Standards enrichment failed: ${msg}`);
722
917
  }
723
918
  }
@@ -747,26 +942,17 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
747
942
  const specList = specPaths.length > 0
748
943
  ? `The following feature specs were completed:\n${specPaths.map((p) => `- ${p}`).join("\n")}`
749
944
  : "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");
945
+ // Build feature metadata for Completed Features table
946
+ const featureMetadata = completedFeatures.map((fId) => `- ${fId} (${inferWorkflowType(fId)})`);
947
+ const featureInfo = featureMetadata.length > 0
948
+ ? `Add these features to the Completed Features table in project-context.md:\n${featureMetadata.join("\n")}`
949
+ : "";
950
+ const promptParts = [`Update the project documentation for the project at "${ctx.cwd}".`, "", specList];
951
+ if (featureInfo) {
952
+ promptParts.push("", featureInfo);
953
+ }
954
+ promptParts.push("", "Follow your instructions: update project-context.md, update/create README.md, and output a Getting Started summary.");
955
+ const prompt = promptParts.join("\n");
770
956
  // Create and run documenter agent
771
957
  const systemPrompt = getSystemPromptForRole("documenter");
772
958
  const agent = await createRoleAgent({
@@ -783,6 +969,9 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
783
969
  response = await runAgent(agent, prompt, {
784
970
  onEvent: createAgentProgressHandler(ctx, "Documenting", pipelineEditor, ctx.cwd),
785
971
  });
972
+ const documenterUsage = aggregateUsage(agent.state.messages);
973
+ const documenterLines = response.split("\n").length;
974
+ vibeAgentResponse(`Documenter response (${documenterLines} lines)`, response, documenterUsage);
786
975
  }
787
976
  catch (error) {
788
977
  const msg = error instanceof Error ? error.message : String(error);
@@ -794,12 +983,6 @@ async function runDocumenter(store, ctx, config, completedFeatures, pipelineEdit
794
983
  }
795
984
  if (abortHandle.aborted)
796
985
  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
986
  vibeMilestone("Documentation updated");
804
987
  }
805
988
  // ─── Workflow Titles ──────────────────────────────────────────────────────────
@@ -818,9 +1001,37 @@ const activePipelines = new Map();
818
1001
  function formatElapsed(ms) {
819
1002
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
820
1003
  }
1004
+ /** Formats token usage as a compact string for headers. */
1005
+ export function formatTokenUsage(usage) {
1006
+ const fmt = (n) => n.toLocaleString("en-US");
1007
+ return `[${fmt(usage.input)} in / ${fmt(usage.output)} out]`;
1008
+ }
1009
+ /** npm package name for update checks. */
1010
+ const AC_PACKAGE_NAME = "@wizdear/atlas-code";
1011
+ /** Check npm registry for a newer version of Atlas Code. */
1012
+ async function checkForAcUpdate(currentVersion) {
1013
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
1014
+ return undefined;
1015
+ try {
1016
+ const response = await fetch(`https://registry.npmjs.org/${AC_PACKAGE_NAME}/latest`, {
1017
+ signal: AbortSignal.timeout(10000),
1018
+ });
1019
+ if (!response.ok)
1020
+ return undefined;
1021
+ const data = (await response.json());
1022
+ if (data.version && data.version !== currentVersion)
1023
+ return data.version;
1024
+ return undefined;
1025
+ }
1026
+ catch {
1027
+ return undefined;
1028
+ }
1029
+ }
821
1030
  /** Vibe Engineering Extension factory. */
822
1031
  export const vibeExtension = (pi) => {
823
1032
  _pi = pi;
1033
+ // Suppress pi's built-in update check — Atlas Code has its own
1034
+ process.env.PI_SKIP_VERSION_CHECK = "1";
824
1035
  // Register custom renderer for vibe-tool messages (agent tool executions)
825
1036
  pi.registerMessageRenderer("vibe-tool", (message, { expanded }, _theme) => {
826
1037
  const details = message.details;
@@ -842,7 +1053,8 @@ export const vibeExtension = (pi) => {
842
1053
  const elapsed = formatElapsed(details.elapsed);
843
1054
  const marker = details.failed ? theme.fg("error", "✗") : theme.fg("success", "✓");
844
1055
  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}`;
1056
+ const tokenSuffix = details.usage && details.usage.totalTokens > 0 ? ` ${theme.fg("dim", formatTokenUsage(details.usage))}` : "";
1057
+ const header = `${marker} Step ${details.stepIndex + 1}/${details.totalSteps}: ${role} — ${details.action} ${suffix}${tokenSuffix}`;
846
1058
  const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
847
1059
  box.addChild(new Text(header, 0, 0));
848
1060
  if (expanded) {
@@ -866,6 +1078,16 @@ export const vibeExtension = (pi) => {
866
1078
  }
867
1079
  box.addChild(new Text(theme.fg("dim", tokenLine), 0, 0));
868
1080
  }
1081
+ if (details.responseText) {
1082
+ box.addChild(new Spacer(1));
1083
+ const { preview, remaining } = computeAgentResponsePreview(details.responseText);
1084
+ box.addChild(new Markdown(preview, 0, 0, mdTheme, {
1085
+ color: (t) => theme.fg("customMessageText", t),
1086
+ }));
1087
+ if (remaining > 0) {
1088
+ box.addChild(new Text(theme.fg("dim", `... (${remaining} more lines)`), 0, 0));
1089
+ }
1090
+ }
869
1091
  }
870
1092
  return box;
871
1093
  });
@@ -919,7 +1141,10 @@ export const vibeExtension = (pi) => {
919
1141
  }
920
1142
  case "agent-response": {
921
1143
  const marker = expanded ? "▼" : "▶";
922
- box.addChild(new Text(`${marker} ${content}`, 0, 0));
1144
+ const usageSuffix = details?.usage && details.usage.totalTokens > 0
1145
+ ? ` ${theme.fg("dim", formatTokenUsage(details.usage))}`
1146
+ : "";
1147
+ box.addChild(new Text(`${marker} ${content}${usageSuffix}`, 0, 0));
923
1148
  if (details?.fullText) {
924
1149
  box.addChild(new Spacer(1));
925
1150
  if (expanded) {
@@ -939,6 +1164,29 @@ export const vibeExtension = (pi) => {
939
1164
  }
940
1165
  break;
941
1166
  }
1167
+ case "agent-text": {
1168
+ const atMarker = expanded ? "▼" : "▶";
1169
+ box.addChild(new Text(theme.fg("dim", `${atMarker} ${content}`), 0, 0));
1170
+ if (details?.fullText) {
1171
+ if (expanded) {
1172
+ box.addChild(new Spacer(1));
1173
+ box.addChild(new Markdown(details.fullText, 0, 0, mdTheme, {
1174
+ color: (t) => theme.fg("customMessageText", t),
1175
+ }));
1176
+ }
1177
+ else {
1178
+ const { preview, remaining } = computeAgentResponsePreview(details.fullText);
1179
+ box.addChild(new Spacer(1));
1180
+ box.addChild(new Markdown(preview, 0, 0, mdTheme, {
1181
+ color: (t) => theme.fg("dim", t),
1182
+ }));
1183
+ if (remaining > 0) {
1184
+ box.addChild(new Text(theme.fg("dim", `... (${remaining} more lines)`), 0, 0));
1185
+ }
1186
+ }
1187
+ }
1188
+ break;
1189
+ }
942
1190
  case "event": {
943
1191
  box.addChild(new Text(theme.fg("dim", content), 0, 0));
944
1192
  break;
@@ -946,6 +1194,192 @@ export const vibeExtension = (pi) => {
946
1194
  }
947
1195
  return box;
948
1196
  });
1197
+ // Register custom renderer for vibe-update messages (Atlas Code update notification)
1198
+ pi.registerMessageRenderer("vibe-update", (message, _options, theme) => {
1199
+ const details = message.details;
1200
+ if (!details)
1201
+ return undefined;
1202
+ const w = (t) => theme.fg("warning", t);
1203
+ const a = (t) => theme.fg("accent", t);
1204
+ const m = (t) => theme.fg("muted", t);
1205
+ const border = w("─".repeat(75));
1206
+ const header = theme.bold(w("Update Available"));
1207
+ const instruction = `${m(`New version ${details.newVersion} is available. `)}${a(`Run: npm install -g ${AC_PACKAGE_NAME}`)}`;
1208
+ const box = new Box(0, 0);
1209
+ box.addChild(new Text(border, 0, 0));
1210
+ box.addChild(new Text(` ${header}`, 0, 0));
1211
+ box.addChild(new Text(` ${instruction}`, 0, 0));
1212
+ box.addChild(new Text(border, 0, 0));
1213
+ return box;
1214
+ });
1215
+ // ── Custom Header ────────────────────────────────────────────────────────
1216
+ pi.on("session_start", async (_event, ctx) => {
1217
+ if (ctx.hasUI) {
1218
+ // Read version from package.json
1219
+ let acVersion = "0.0.0";
1220
+ try {
1221
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1222
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
1223
+ acVersion = pkg.version ?? acVersion;
1224
+ }
1225
+ catch {
1226
+ // Fallback version
1227
+ }
1228
+ // Check for Atlas Code updates asynchronously
1229
+ checkForAcUpdate(acVersion).then((newVersion) => {
1230
+ if (newVersion) {
1231
+ _pi.sendMessage({
1232
+ customType: "vibe-update",
1233
+ content: newVersion,
1234
+ display: true,
1235
+ details: { currentVersion: acVersion, newVersion },
1236
+ });
1237
+ }
1238
+ });
1239
+ ctx.ui.setHeader((_tui, theme) => ({
1240
+ render(_width) {
1241
+ const b = (t) => theme.fg("accent", t);
1242
+ const d = (t) => theme.fg("dim", t);
1243
+ // Atlas Code logo — diamond with digital glitch
1244
+ const logo = [
1245
+ ` ${b("░▒▓")} ${b("╱╲")} ${b("▓▒░")}`,
1246
+ ` ${b("▓█")} ${b("╱ ╲")} ${b("█▓")}`,
1247
+ ` ${b("█▓")} ${b("╲ ╱")} ${b("▓█")}`,
1248
+ ` ${b("░▒▓")} ${b("╲╱")} ${b("▓▒░")}`,
1249
+ ];
1250
+ const title = ` ${theme.bold("Atlas Code")} ${d(`v${acVersion}`)}`;
1251
+ const vibeCommands = [
1252
+ ` ${d("── Vibe Commands ─────────────────────────────────────")}`,
1253
+ "",
1254
+ ` ${d("Setup")}`,
1255
+ ` ${d("/vibe init")} Initialize .vibe/ directory`,
1256
+ "",
1257
+ ` ${d("Start Workflow")}`,
1258
+ ` ${d("/vibe <requirements>")} Auto workflow`,
1259
+ ` ${d("/vibe new <requirements>")} New feature`,
1260
+ ` ${d("/vibe enhance <id> <req>")} Enhance existing feature`,
1261
+ ` ${d("/vibe fix <description>")} Fix a bug`,
1262
+ ` ${d("/vibe refactor <id> <purpose>")} Refactor a feature`,
1263
+ "",
1264
+ ` ${d("Monitor & Control")}`,
1265
+ ` ${d("/vibe status")} Show pipeline status`,
1266
+ ` ${d("/vibe resume <id>")} Resume paused pipeline`,
1267
+ ];
1268
+ const keybindings = [
1269
+ ` ${d("── Keybindings ───────────────────────────────────────")}`,
1270
+ ` ${d("ctrl+c to interrupt, ctrl+l to clear, ctrl+l twice to exit")}`,
1271
+ ` ${d("ctrl+d to exit (empty), ctrl+z to suspend")}`,
1272
+ ` ${d("ctrl+k to delete to end, ctrl+t to cycle thinking level")}`,
1273
+ ` ${d("ctrl+p/ctrl+n to cycle models, ctrl+shift+p to select model")}`,
1274
+ ` ${d("ctrl+e to expand tools, ctrl+shift+e to expand thinking")}`,
1275
+ ` ${d("ctrl+o for external editor, / for commands")}`,
1276
+ ` ${d("! to run bash, !! to run bash (no context)")}`,
1277
+ ` ${d("ctrl+f to queue follow-up, ctrl+shift+f to edit queued messages")}`,
1278
+ ` ${d("ctrl+v to paste image, drop files to attach")}`,
1279
+ ];
1280
+ return ["", ...logo, "", title, "", ...vibeCommands, "", ...keybindings, ""];
1281
+ },
1282
+ invalidate() { },
1283
+ }));
1284
+ }
1285
+ // Telegram bridge — auto-activate from env vars or .vibe/config.json
1286
+ const vibeDir = join(ctx.cwd, ".vibe");
1287
+ const bridgeManager = createTelegramBridgeManager(pi, ctx, vibeDir, _telegramCommands);
1288
+ bridgeManager.tryStart();
1289
+ pi.on("agent_end", () => {
1290
+ bridgeManager.tryStart();
1291
+ });
1292
+ pi.on("session_shutdown", () => {
1293
+ bridgeManager.stop();
1294
+ });
1295
+ });
1296
+ // Telegram bridge setup guidance in system prompt
1297
+ pi.on("before_agent_start", (event) => {
1298
+ const telegramGuideline = [
1299
+ "",
1300
+ "## Telegram Bridge",
1301
+ "This project has a built-in Telegram bridge. When the user asks to connect/setup Telegram:",
1302
+ '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.',
1303
+ "2. Do NOT create a Telegram bot project, install npm packages, or write bot code.",
1304
+ "3. The bridge activates automatically after the config is saved. Chat ID is auto-discovered from the first message sent to the bot.",
1305
+ ].join("\n");
1306
+ return { systemPrompt: `${event.systemPrompt}${telegramGuideline}` };
1307
+ });
1308
+ const vibeCommandHandler = async (args, ctx) => {
1309
+ try {
1310
+ const command = parseVibeCommand(args);
1311
+ // Record user command input in session
1312
+ if (command.type !== "help") {
1313
+ vibeCommand(`/vibe ${args.trim() || command.type}`);
1314
+ }
1315
+ const logger = createVibeLogger(ctx, join(ctx.cwd, ".vibe"));
1316
+ const store = new VibeStore(ctx.cwd, logger);
1317
+ switch (command.type) {
1318
+ case "help":
1319
+ ctx.ui.notify(VIBE_HELP, "info");
1320
+ break;
1321
+ case "init":
1322
+ await handleInit(store, ctx);
1323
+ break;
1324
+ case "reset":
1325
+ await handleReset(store, ctx);
1326
+ break;
1327
+ case "recover":
1328
+ await handleRecover(store, ctx);
1329
+ break;
1330
+ case "analyze":
1331
+ await handleAnalyze(store, ctx, ctx.model);
1332
+ break;
1333
+ case "config":
1334
+ await handleConfig(store, ctx);
1335
+ break;
1336
+ case "status":
1337
+ await handleStatus(store, ctx, command.featureId);
1338
+ break;
1339
+ case "log":
1340
+ await handleLog(store, ctx, command.featureId);
1341
+ break;
1342
+ case "auto":
1343
+ await handleWorkflow(store, ctx, "auto", command.requirement, undefined, logger);
1344
+ break;
1345
+ case "new":
1346
+ await handleWorkflow(store, ctx, "new_feature", command.requirement, undefined, logger);
1347
+ break;
1348
+ case "enhance":
1349
+ await handleWorkflow(store, ctx, "enhancement", command.requirement, { parentFeatureId: command.featureId }, logger);
1350
+ break;
1351
+ case "fix":
1352
+ await handleWorkflow(store, ctx, "bugfix", command.description, { issueRef: command.issueRef }, logger);
1353
+ break;
1354
+ case "refactor":
1355
+ await handleWorkflow(store, ctx, "refactor", command.purpose, { parentFeatureId: command.featureId }, logger);
1356
+ break;
1357
+ case "pause":
1358
+ await handlePause(command.featureId, ctx);
1359
+ break;
1360
+ case "resume":
1361
+ await handleResume(store, ctx, command.featureId, logger);
1362
+ break;
1363
+ case "steer": {
1364
+ const active = activePipelines.get(command.featureId);
1365
+ if (!active?.activeAgent) {
1366
+ ctx.ui.notify(`[Vibe] No running agent for ${command.featureId}`, "warning");
1367
+ break;
1368
+ }
1369
+ active.activeAgent.steer({
1370
+ role: "user",
1371
+ content: [{ type: "text", text: command.feedback }],
1372
+ timestamp: Date.now(),
1373
+ });
1374
+ ctx.ui.notify(`[Steer] Feedback sent to ${command.featureId}`, "info");
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1379
+ catch (error) {
1380
+ vibeError(error instanceof Error ? error.message : String(error));
1381
+ }
1382
+ };
949
1383
  pi.registerCommand("vibe", {
950
1384
  description: "AI Vibe Engineering System",
951
1385
  getArgumentCompletions(argumentPrefix) {
@@ -957,96 +1391,52 @@ export const vibeExtension = (pi) => {
957
1391
  }
958
1392
  return null;
959
1393
  },
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
- },
1394
+ handler: vibeCommandHandler,
1395
+ });
1396
+ // Register vibe command for Telegram dispatch (shared array, read by bridge polling loop)
1397
+ _telegramCommands.push({
1398
+ name: "vibe",
1399
+ description: "AI Vibe Engineering System",
1400
+ handler: vibeCommandHandler,
1401
+ subcommands: [
1402
+ { name: "new", description: "Start a new feature" },
1403
+ { name: "enhance", description: "Enhance existing feature" },
1404
+ { name: "fix", description: "Fix a bug" },
1405
+ { name: "refactor", description: "Refactor code" },
1406
+ { name: "status", description: "Show pipeline status" },
1407
+ { name: "resume", description: "Resume paused pipeline" },
1408
+ { name: "pause", description: "Pause current pipeline" },
1409
+ { name: "config", description: "Configure vibe settings" },
1410
+ { name: "log", description: "Show pipeline log" },
1411
+ { name: "steer", description: "Steer active pipeline" },
1412
+ { name: "analyze", description: "Analyze project" },
1413
+ { name: "init", description: "Initialize vibe project" },
1414
+ { name: "reset", description: "Reset vibe state" },
1415
+ { name: "recover", description: "Recover from failed state" },
1416
+ ],
1029
1417
  });
1030
1418
  // ── vibe_start tool ──────────────────────────────────────────────────────
1031
1419
  //
1032
1420
  // Allows the LLM to invoke the vibe pipeline programmatically.
1033
1421
  //
1034
- // sendUserMessage() bypasses command routing (expandPromptTemplates: false
1035
- // in agent-session.js), so we cannot delegate via "/vibe" as a user message.
1422
+ // The tool stores the requirement and defers execution to agent_end.
1423
+ // This ensures handleWorkflow() runs AFTER the main agent finishes streaming,
1424
+ // so _pi.sendMessage() calls go through appendMessage() instead of
1425
+ // agent.steer(). Without this, display-only progress messages would enter
1426
+ // the steering queue, skip remaining tool calls, and pollute the main
1427
+ // agent's LLM context (custom messages are converted to role:"user" by
1428
+ // convertToLlm, causing the main agent to respond to pipeline progress).
1036
1429
  //
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):
1430
+ // Duplicate execution guard:
1042
1431
  // 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
1432
+ // 2. agent_end clears the flag after workflow completes
1045
1433
  let pendingVibeRequirement;
1046
1434
  let vibeStartFired = false;
1047
- const VIBE_TRIGGER = "__vibe_pipeline_start__";
1048
- pi.on("input", async (event, ctx) => {
1049
- if (event.text === VIBE_TRIGGER && pendingVibeRequirement) {
1435
+ pi.on("agent_end", async (_event, ctx) => {
1436
+ // Start the pipeline after the main agent finishes streaming.
1437
+ // At this point isStreaming === false, so _pi.sendMessage() calls
1438
+ // use appendMessage() instead of agent.steer().
1439
+ if (pendingVibeRequirement) {
1050
1440
  const requirement = pendingVibeRequirement;
1051
1441
  pendingVibeRequirement = undefined;
1052
1442
  // Cast ctx to ExtensionCommandContext — safe because handleWorkflow
@@ -1065,14 +1455,8 @@ export const vibeExtension = (pi) => {
1065
1455
  finally {
1066
1456
  vibeStartFired = false;
1067
1457
  }
1068
- return { action: "handled" };
1069
- }
1070
- // Absorb duplicate trigger messages from batched tool calls
1071
- if (event.text === VIBE_TRIGGER) {
1072
- return { action: "handled" };
1458
+ return;
1073
1459
  }
1074
- });
1075
- pi.on("agent_end", async () => {
1076
1460
  // Re-activate vibe_start only after the pipeline has completed
1077
1461
  if (!vibeStartFired) {
1078
1462
  const activeTools = pi.getActiveTools();
@@ -1106,7 +1490,6 @@ export const vibeExtension = (pi) => {
1106
1490
  }
1107
1491
  vibeStartFired = true;
1108
1492
  pendingVibeRequirement = params.description;
1109
- pi.sendUserMessage(VIBE_TRIGGER, { deliverAs: "followUp" });
1110
1493
  const activeTools = pi.getActiveTools();
1111
1494
  pi.setActiveTools(activeTools.filter((t) => t !== "vibe_start"));
1112
1495
  return {
@@ -1135,17 +1518,19 @@ async function handleInit(store, ctx) {
1135
1518
  await writeFile(filePath, content, "utf-8");
1136
1519
  }
1137
1520
  }
1138
- // Create QMD memory skill template
1139
- const qmdSkillPath = join(store.getSkillsDir(), "qmd-memory", "SKILL.md");
1521
+ // Create QMD memory skill template in .pi/skills/ (pi's native skill system)
1522
+ const qmdSkillDir = join(store.getProjectRoot(), ".pi", "skills", "qmd-memory");
1523
+ const qmdSkillPath = join(qmdSkillDir, "SKILL.md");
1140
1524
  try {
1141
1525
  await access(qmdSkillPath);
1142
1526
  }
1143
1527
  catch {
1144
- await mkdir(join(store.getSkillsDir(), "qmd-memory"), { recursive: true });
1528
+ await mkdir(qmdSkillDir, { recursive: true });
1145
1529
  await writeFile(qmdSkillPath, QMD_SKILL_TEMPLATE, "utf-8");
1146
1530
  }
1147
- // Clean up stale QMD collections if qmd is installed
1531
+ // Clean up stale QMD collections and initialize fresh ones if qmd is installed
1148
1532
  await cleanupQmdCollections(ctx.cwd);
1533
+ await initQmdCollections(ctx.cwd);
1149
1534
  vibeMilestone("Initialized .vibe/ directory with 14 standard templates");
1150
1535
  // autoAnalyzeOnInit
1151
1536
  const config = await store.loadConfig();
@@ -1161,6 +1546,15 @@ async function handleInit(store, ctx) {
1161
1546
  }
1162
1547
  }
1163
1548
  }
1549
+ async function handleReset(store, ctx) {
1550
+ const confirm = await ctx.ui.select("This will delete all .vibe/ contents (standards, features, config, etc.). Continue?", ["No", "Yes"]);
1551
+ if (confirm !== "Yes") {
1552
+ ctx.ui.notify("[Vibe] Reset cancelled", "info");
1553
+ return;
1554
+ }
1555
+ await store.reset();
1556
+ await handleInit(store, ctx);
1557
+ }
1164
1558
  async function handleAnalyze(store, ctx, currentModel) {
1165
1559
  const config = await store.loadConfig();
1166
1560
  const configBackup = structuredClone(config);
@@ -1180,9 +1574,12 @@ async function handleAnalyze(store, ctx, currentModel) {
1180
1574
  const prompt = buildAnalyzePrompt(ctx.cwd, excludePaths);
1181
1575
  abortHandle.currentAgent = agent;
1182
1576
  try {
1183
- await runAgent(agent, prompt, {
1577
+ const analyzeResponse = await runAgent(agent, prompt, {
1184
1578
  onEvent: createAgentProgressHandler(ctx, "Analyzing", pipelineEditor, ctx.cwd),
1185
1579
  });
1580
+ const analyzeUsage = aggregateUsage(agent.state.messages);
1581
+ const analyzeLines = analyzeResponse.split("\n").length;
1582
+ vibeAgentResponse(`Project Analyzer response (${analyzeLines} lines)`, analyzeResponse, analyzeUsage);
1186
1583
  }
1187
1584
  catch (error) {
1188
1585
  if (abortHandle.aborted) {
@@ -1519,16 +1916,10 @@ export const PHASE_ORDER = [
1519
1916
  "done",
1520
1917
  ];
1521
1918
  /**
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.
1919
+ * Resolves the phase to resume from. Gate phases are kept as-is so that
1920
+ * resume skips the parent execution and goes directly to the gate prompt.
1524
1921
  */
1525
1922
  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
1923
  return phase;
1533
1924
  }
1534
1925
  async function handleWorkflow(store, ctx, workflowType, _requirement, options, logger, startFromPhase) {
@@ -1537,6 +1928,11 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1537
1928
  await handleInit(store, ctx);
1538
1929
  }
1539
1930
  let config = await store.loadConfig();
1931
+ // Auto-init git repository if not present
1932
+ if (!(await isGitRepo(ctx.cwd))) {
1933
+ await execFileAsync("git", ["init"], { cwd: ctx.cwd });
1934
+ vibeMilestone("Initialized git repository");
1935
+ }
1540
1936
  // Resolve baseBranch: config > existing orchestration state > prompt user
1541
1937
  if (!config.baseBranch) {
1542
1938
  const existingOrcState = (await store.hasOrchestrationState()) ? await store.loadOrchestrationState() : undefined;
@@ -1572,23 +1968,32 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1572
1968
  vibeMilestone(`Base branch set to \`${chosenBranch}\``);
1573
1969
  }
1574
1970
  }
1575
- // Stale project-context warning
1971
+ // Stale project-context check: auto-analyze if missing, warn if stale
1576
1972
  if (await store.isProjectContextStale(config.projectAnalysis.staleThresholdDays)) {
1577
1973
  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);
1974
+ if (!hasContext && config.projectAnalysis.autoAnalyzeOnInit) {
1975
+ try {
1976
+ await handleAnalyze(store, ctx, ctx.model);
1977
+ }
1978
+ catch (error) {
1979
+ vibeError(`Project analysis failed: ${error instanceof Error ? error.message : String(error)}\nRun /vibe analyze to retry.`);
1980
+ }
1981
+ }
1982
+ else {
1983
+ const msg = hasContext
1984
+ ? `project-context.md is older than ${config.projectAnalysis.staleThresholdDays} days. Run /vibe analyze to refresh.`
1985
+ : "No project-context.md found. Run /vibe analyze to generate.";
1986
+ vibeWarning(msg);
1987
+ }
1582
1988
  }
1583
1989
  const getApiKey = (provider) => ctx.modelRegistry.getApiKeyForProvider(provider);
1584
- const availableSkills = await buildAvailableSkillsPrompt(store);
1585
- const projectContext = (await store.hasProjectContext()) ? await store.readProjectContext() : undefined;
1586
1990
  const requestApproval = async (_step, _fId, summary) => {
1991
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
1587
1992
  // Auto-expand messages so the user can review full content before deciding
1588
1993
  const wasExpanded = ctx.ui.getToolsExpanded();
1589
1994
  if (!wasExpanded)
1590
1995
  ctx.ui.setToolsExpanded(true);
1591
- const choice = await ctx.ui.select(summary, ["Approve", "Reject", "Skip", "Abort"]);
1996
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
1592
1997
  // Restore previous expand state after gate resolves
1593
1998
  if (!wasExpanded)
1594
1999
  ctx.ui.setToolsExpanded(false);
@@ -1604,7 +2009,8 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1604
2009
  case "Abort":
1605
2010
  return { passed: false, action: "abort" };
1606
2011
  default:
1607
- return { passed: false, action: "rejected" };
2012
+ // ESC or undefined selection pause (preserve state for resume)
2013
+ return { passed: false, action: "pause" };
1608
2014
  }
1609
2015
  };
1610
2016
  /**
@@ -1620,6 +2026,9 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1620
2026
  if (result.action === "abort") {
1621
2027
  return { proceed: false, action: "abort" };
1622
2028
  }
2029
+ if (result.action === "pause") {
2030
+ return { proceed: false, action: "pause" };
2031
+ }
1623
2032
  if (result.action === "skip") {
1624
2033
  return { proceed: true, action: "skip" };
1625
2034
  }
@@ -1661,62 +2070,62 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1661
2070
  let requirementsApproved = false;
1662
2071
  let discoveryFeedback;
1663
2072
  // Discovery can be skipped on resume, but must run if requirements.md is missing
1664
- if (!shouldRunPhase("discovery") && (await store.hasRequirements())) {
2073
+ if (!shouldRunPhase("discovery") && !shouldRunPhase("requirements_gate") && (await store.hasRequirements())) {
1665
2074
  requirementsApproved = true;
1666
2075
  }
2076
+ // When resuming at requirements_gate, skip discovery and go straight to gate
2077
+ let skipDiscoveryForGate = startFromPhase === "requirements_gate" && (await store.hasRequirements());
1667
2078
  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")) {
2079
+ if (skipDiscoveryForGate) {
2080
+ skipDiscoveryForGate = false;
2081
+ }
2082
+ else {
2083
+ await saveOrcState("discovery");
2084
+ pipelineEditor.setPhase(discoveryFeedback ? "Discovery (retry)" : "Discovery");
2085
+ const discoveryResult = await runDiscovery({
2086
+ store,
2087
+ config,
2088
+ projectRoot: ctx.cwd,
2089
+ requirement: _requirement,
2090
+ requestUserInput: async (_questions) => {
2091
+ pipelineEditor.setDetail("waiting for input...");
2092
+ return ctx.ui.editor("Discovery: Answer the questions above");
2093
+ },
2094
+ getApiKey,
2095
+ model: ctx.model,
2096
+ onAgentEvent: createAgentProgressHandler(ctx, "Discovery", pipelineEditor, ctx.cwd),
2097
+ onMessage: (msg) => {
1684
2098
  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}`);
2099
+ if (msg.includes("[REQUIREMENTS_COMPLETE]") || msg.includes("```requirements.md")) {
2100
+ vibeAgentResponse(`Discovery complete — requirements.md (${lines} lines)`, msg);
2101
+ }
2102
+ else if (msg.includes("[QUESTIONS]")) {
2103
+ vibeAgentResponse(`Discovery: clarifying questions (${lines} lines)`, msg);
2104
+ }
2105
+ else if (msg.startsWith("[Discovery]")) {
2106
+ vibeAgentResponse(`Discovery response (${lines} lines)`, msg);
1691
2107
  }
1692
2108
  else {
1693
- vibeMilestone("Discovery: asking clarifying questions");
2109
+ vibeAgentResponse(`Discovery response (${lines} lines)`, msg);
1694
2110
  }
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
- }
2111
+ },
2112
+ feedback: discoveryFeedback,
2113
+ onAgentCreated: (agent) => {
2114
+ abortHandle.currentAgent = agent;
2115
+ },
2116
+ onAgentFinished: () => {
2117
+ abortHandle.currentAgent = undefined;
2118
+ },
2119
+ });
2120
+ if (discoveryResult.usage) {
2121
+ vibeMilestone(`Discovery tokens: ${formatTokenUsage(discoveryResult.usage)}`);
2122
+ }
2123
+ if (!discoveryResult.completed) {
2124
+ // Discovery cancelled (ESC): preserve state + agent history for resume
2125
+ vibeGate("Discovery paused — use `/vibe resume` to continue");
2126
+ return;
2127
+ }
2128
+ } // end: skip discovery for gate resume
1720
2129
  // Gate: requirements approval
1721
2130
  await saveOrcState("requirements_gate");
1722
2131
  const gateResult = await checkOrchestrationGate("requireRequirementsApproval", "[Gate] Requirements complete. Approve to proceed?");
@@ -1727,6 +2136,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1727
2136
  await store.clearOrchestrationState();
1728
2137
  return;
1729
2138
  }
2139
+ if (gateResult.action === "pause") {
2140
+ vibeGate("Paused at requirements gate — use `/vibe resume` to continue");
2141
+ return;
2142
+ }
1730
2143
  if (gateResult.action === "skip") {
1731
2144
  vibeGate("Requirements review skipped");
1732
2145
  requirementsApproved = true;
@@ -1741,81 +2154,83 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1741
2154
  }
1742
2155
  }
1743
2156
  // ── Standards enrichment (post-Discovery) ──
1744
- if (!abortHandle.aborted) {
2157
+ if (!abortHandle.aborted && shouldRunPhase("system_architecture")) {
1745
2158
  await runStandardsEnrichment(store, ctx, config, "discovery", abortHandle, pipelineEditor);
1746
2159
  }
2160
+ if (abortHandle.aborted) {
2161
+ vibeGate("Aborted by user (ESC)");
2162
+ return;
2163
+ }
1747
2164
  // ── Phase: System Architecture → system-design.md ──
1748
2165
  let systemDesignApproved = false;
1749
2166
  let systemDesignFeedback;
1750
- if (!shouldRunPhase("system_architecture") && (await store.hasSystemDesign())) {
2167
+ if (!shouldRunPhase("system_architecture") &&
2168
+ !shouldRunPhase("system_design_gate") &&
2169
+ (await store.hasSystemDesign())) {
1751
2170
  systemDesignApproved = true;
1752
2171
  }
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).");
2172
+ // When system-design.md exists and phase should run, fall through to while loop (always update).
2173
+ // System Architect handles incremental refinement automatically via "Refine and update" prompt.
2174
+ // When resuming at system_design_gate, skip system architecture and go straight to gate
2175
+ let skipSysArchForGate = startFromPhase === "system_design_gate" && (await store.hasSystemDesign());
2176
+ while (!systemDesignApproved && !abortHandle.aborted) {
2177
+ if (skipSysArchForGate) {
2178
+ skipSysArchForGate = false;
1758
2179
  }
1759
2180
  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.");
2181
+ await saveOrcState("system_architecture");
2182
+ pipelineEditor.setPhase(systemDesignFeedback ? "System Architecture (retry)" : "System Architecture");
2183
+ pipelineEditor.setDetail("");
2184
+ // Save old content for impact analysis on re-runs
2185
+ let oldSystemDesign;
2186
+ if (systemDesignFeedback && (await store.hasSystemDesign())) {
2187
+ oldSystemDesign = await store.readSystemDesign();
1764
2188
  }
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}`);
2189
+ const sysArchResult = await runSystemArchitect({
2190
+ store,
2191
+ config,
2192
+ projectRoot: ctx.cwd,
2193
+ getApiKey,
2194
+ model: ctx.model,
2195
+ onAgentEvent: createAgentProgressHandler(ctx, "System Architecture", pipelineEditor, ctx.cwd),
2196
+ onMessage: (msg) => {
2197
+ const lines = msg.split("\n").length;
2198
+ vibeAgentResponse(`System design (${lines} lines)`, msg);
2199
+ },
2200
+ feedback: systemDesignFeedback,
2201
+ onAgentCreated: (agent) => {
2202
+ abortHandle.currentAgent = agent;
2203
+ },
2204
+ onAgentFinished: () => {
2205
+ abortHandle.currentAgent = undefined;
2206
+ },
2207
+ });
2208
+ if (sysArchResult.usage) {
2209
+ vibeMilestone(`System Architect tokens: ${formatTokenUsage(sysArchResult.usage)}`);
2210
+ }
2211
+ if (abortHandle.aborted) {
2212
+ vibeGate("Aborted by user (ESC)");
2213
+ return;
2214
+ }
2215
+ if (!sysArchResult.completed) {
2216
+ vibeError("System architecture failed");
2217
+ return;
2218
+ }
2219
+ // Impact analysis on re-runs (compare old vs new system-design.md)
2220
+ if (oldSystemDesign && (await store.hasSystemDesign())) {
2221
+ const newSystemDesign = await store.readSystemDesign();
2222
+ const featureDesigns = await loadAllFeatureDesigns(store);
2223
+ if (featureDesigns.size > 0) {
2224
+ const impact = analyzeSystemDesignImpact(oldSystemDesign, newSystemDesign, featureDesigns);
2225
+ if (impact.affectedFeatures.length > 0) {
2226
+ const lines = impact.affectedFeatures
2227
+ .map((f) => `- **${f.featureId}**: ${f.reasons.join("; ")}`)
2228
+ .join("\n");
2229
+ vibeWarning(`System design changes may affect ${impact.affectedFeatures.length} feature(s):\n${lines}`);
2230
+ }
1816
2231
  }
1817
2232
  }
1818
- }
2233
+ } // end: skip system architecture for gate resume
1819
2234
  // Gate: system design approval
1820
2235
  await saveOrcState("system_design_gate");
1821
2236
  const sysDesignGate = await checkOrchestrationGate("requireSystemDesignApproval", "[Gate] System design complete. Approve to proceed?");
@@ -1828,6 +2243,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1828
2243
  await store.clearOrchestrationState();
1829
2244
  return;
1830
2245
  }
2246
+ if (sysDesignGate.action === "pause") {
2247
+ vibeGate("Paused at system design gate — use `/vibe resume` to continue");
2248
+ return;
2249
+ }
1831
2250
  if (sysDesignGate.action === "skip") {
1832
2251
  vibeGate("System design review skipped");
1833
2252
  systemDesignApproved = true;
@@ -1842,20 +2261,31 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1842
2261
  }
1843
2262
  }
1844
2263
  // ── Standards enrichment (post-System Architecture) ──
1845
- if (!abortHandle.aborted && (await store.hasSystemDesign())) {
2264
+ if (!abortHandle.aborted && shouldRunPhase("analyze") && (await store.hasSystemDesign())) {
1846
2265
  await runStandardsEnrichment(store, ctx, config, "system_architect", abortHandle, pipelineEditor);
1847
2266
  }
2267
+ if (abortHandle.aborted) {
2268
+ vibeGate("Aborted by user (ESC)");
2269
+ return;
2270
+ }
1848
2271
  // ── Multi-feature orchestration path (new_feature, enhancement, refactor, auto/mixed) ──
1849
2272
  if (workflowType === "new_feature" ||
1850
2273
  workflowType === "enhancement" ||
1851
2274
  workflowType === "refactor" ||
1852
2275
  workflowType === "auto" ||
1853
2276
  workflowType === "mixed") {
1854
- // Enhancement/Refactor: run Analyze at orchestration level before Planner
1855
- if ((workflowType === "enhancement" || workflowType === "refactor") && shouldRunPhase("analyze")) {
2277
+ // Run Analyze at orchestration level before Planner (all multi-feature types)
2278
+ if ((workflowType === "new_feature" || workflowType === "enhancement" || workflowType === "refactor") &&
2279
+ shouldRunPhase("analyze")) {
1856
2280
  await saveOrcState("analyze");
1857
2281
  pipelineEditor.setPhase("Analyzing");
1858
2282
  pipelineEditor.setDetail("");
2283
+ // Inject system-design.md for architectural context during impact analysis
2284
+ let analyzerFeatureContext;
2285
+ if (await store.hasSystemDesign()) {
2286
+ const sd = await store.readSystemDesign();
2287
+ analyzerFeatureContext = `## System Architecture\n\n${sd}`;
2288
+ }
1859
2289
  const analyzerSystemPrompt = getSystemPromptForRole("analyzer");
1860
2290
  const analyzerAgent = await createRoleAgent({
1861
2291
  role: "analyzer",
@@ -1864,11 +2294,14 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1864
2294
  projectRoot: ctx.cwd,
1865
2295
  model: ctx.model,
1866
2296
  getApiKey,
1867
- availableSkills,
2297
+ featureContext: analyzerFeatureContext,
1868
2298
  });
1869
2299
  abortHandle.currentAgent = analyzerAgent;
1870
2300
  try {
1871
2301
  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) });
2302
+ const analyzerUsage = aggregateUsage(analyzerAgent.state.messages);
2303
+ const analyzerLines = analyzerResponse.split("\n").length;
2304
+ vibeAgentResponse(`Analyzer response (${analyzerLines} lines)`, analyzerResponse, analyzerUsage);
1872
2305
  const impactContent = extractArtifactContent(analyzerResponse, "impact-report.md", true);
1873
2306
  if (impactContent) {
1874
2307
  await store.writeGlobalArtifact("impact-report.md", impactContent);
@@ -1895,62 +2328,69 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1895
2328
  let planFeedback;
1896
2329
  let lastFeatureCount = 0;
1897
2330
  // Planning can be skipped on resume, but must run if plan.json is missing
1898
- if (!shouldRunPhase("planning") && (await store.hasPlan())) {
2331
+ if (!shouldRunPhase("planning") && !shouldRunPhase("plan_gate") && (await store.hasPlan())) {
1899
2332
  planApproved = true;
1900
2333
  }
2334
+ // When resuming at plan_gate, skip planner and go straight to gate
2335
+ let skipPlannerForGate = startFromPhase === "plan_gate" && (await store.hasPlan());
1901
2336
  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) {
2337
+ if (skipPlannerForGate) {
2338
+ skipPlannerForGate = false;
2339
+ }
2340
+ else {
2341
+ // Clean up features/ from previous runs. The Planner reuses sequential
2342
+ // featureIds (feat-001, etc.), so leftover artifacts would conflict
2343
+ // with new orchestration. No data loss since the Planner regenerates
2344
+ // spec.md. Ensures a clean state on Reject re-runs as well.
2345
+ await store.clearAllFeatures();
2346
+ await saveOrcState("planning");
2347
+ pipelineEditor.setPhase(planFeedback ? "Planning (retry)" : "Planning");
2348
+ pipelineEditor.setDetail("");
2349
+ const plannerResult = await runPlanner({
2350
+ store,
2351
+ config,
2352
+ projectRoot: ctx.cwd,
2353
+ getApiKey,
2354
+ model: ctx.model,
2355
+ onAgentEvent: createAgentProgressHandler(ctx, "Planning", pipelineEditor, ctx.cwd),
2356
+ onMessage: (msg) => {
2357
+ const lines = msg.split("\n").length;
1920
2358
  vibeAgentResponse(`Planner response (${lines} lines)`, msg);
2359
+ },
2360
+ feedback: planFeedback,
2361
+ onAgentCreated: (agent) => {
2362
+ abortHandle.currentAgent = agent;
2363
+ },
2364
+ onAgentFinished: () => {
2365
+ abortHandle.currentAgent = undefined;
2366
+ },
2367
+ });
2368
+ if (plannerResult.usage) {
2369
+ vibeMilestone(`Planner tokens: ${formatTokenUsage(plannerResult.usage)}`);
2370
+ }
2371
+ if (abortHandle.aborted) {
2372
+ vibeGate("Aborted by user (ESC)");
2373
+ return;
2374
+ }
2375
+ if (!plannerResult.completed) {
2376
+ vibeError("Planning failed");
2377
+ return;
2378
+ }
2379
+ lastFeatureCount = plannerResult.featureCount;
2380
+ // Traceability check
2381
+ if (await store.hasRequirements()) {
2382
+ const requirements = await store.readRequirements();
2383
+ const plan = await store.loadPlan();
2384
+ const unmapped = findUnmappedRequirements(requirements, plan);
2385
+ if (unmapped.length > 0) {
2386
+ vibeWarning(`Unmapped requirements: ${unmapped.join(", ")}`);
1921
2387
  }
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
2388
  }
1952
- }
2389
+ } // end: skip planner for gate resume
1953
2390
  // Gate: plan approval
2391
+ if (lastFeatureCount === 0 && (await store.hasPlan())) {
2392
+ lastFeatureCount = (await store.loadPlan()).features.length;
2393
+ }
1954
2394
  await saveOrcState("plan_gate");
1955
2395
  const planGateResult = await checkOrchestrationGate("requirePlanApproval", `[Gate] Plan complete (${lastFeatureCount} features). Approve to proceed to orchestration?`);
1956
2396
  if (planGateResult.action === "abort") {
@@ -1965,6 +2405,10 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1965
2405
  await store.clearOrchestrationState();
1966
2406
  return;
1967
2407
  }
2408
+ if (planGateResult.action === "pause") {
2409
+ vibeGate("Paused at plan gate — use `/vibe resume` to continue");
2410
+ return;
2411
+ }
1968
2412
  if (planGateResult.action === "skip") {
1969
2413
  vibeGate("Plan review skipped");
1970
2414
  planApproved = true;
@@ -1983,9 +2427,13 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1983
2427
  const plan = await store.loadPlan();
1984
2428
  await saveOrcState("orchestrating");
1985
2429
  // ── Standards enrichment (post-Planner) ──
1986
- if (!abortHandle.aborted) {
2430
+ if (!abortHandle.aborted && shouldRunPhase("orchestrating")) {
1987
2431
  await runStandardsEnrichment(store, ctx, config, "planner", abortHandle, pipelineEditor);
1988
2432
  }
2433
+ if (abortHandle.aborted) {
2434
+ vibeGate("Aborted by user (ESC)");
2435
+ return;
2436
+ }
1989
2437
  // Auto/Mixed mode: run Analyze after Planner if enhance-*/refactor-* features exist
1990
2438
  if (!abortHandle.aborted &&
1991
2439
  (workflowType === "auto" || workflowType === "mixed") &&
@@ -1998,6 +2446,12 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
1998
2446
  if (needsAnalyze) {
1999
2447
  pipelineEditor.setPhase("Analyzing");
2000
2448
  pipelineEditor.setDetail("");
2449
+ // Inject system-design.md for architectural context during impact analysis
2450
+ let analyzerFeatureContext;
2451
+ if (await store.hasSystemDesign()) {
2452
+ const sd = await store.readSystemDesign();
2453
+ analyzerFeatureContext = `## System Architecture\n\n${sd}`;
2454
+ }
2001
2455
  const analyzerSystemPrompt = getSystemPromptForRole("analyzer");
2002
2456
  const analyzerAgent = await createRoleAgent({
2003
2457
  role: "analyzer",
@@ -2006,11 +2460,14 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2006
2460
  projectRoot: ctx.cwd,
2007
2461
  model: ctx.model,
2008
2462
  getApiKey,
2009
- availableSkills,
2463
+ featureContext: analyzerFeatureContext,
2010
2464
  });
2011
2465
  abortHandle.currentAgent = analyzerAgent;
2012
2466
  try {
2013
2467
  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) });
2468
+ const analyzerUsage2 = aggregateUsage(analyzerAgent.state.messages);
2469
+ const analyzerLines2 = analyzerResponse.split("\n").length;
2470
+ vibeAgentResponse(`Analyzer response (${analyzerLines2} lines)`, analyzerResponse, analyzerUsage2);
2014
2471
  const impactContent = extractArtifactContent(analyzerResponse, "impact-report.md", true);
2015
2472
  if (impactContent) {
2016
2473
  await store.writeGlobalArtifact("impact-report.md", impactContent);
@@ -2050,8 +2507,6 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2050
2507
  onAgentCreated: editorCallbacks.onAgentCreated,
2051
2508
  onAgentFinished: editorCallbacks.onAgentFinished,
2052
2509
  logger,
2053
- availableSkills,
2054
- projectContext,
2055
2510
  });
2056
2511
  for await (const event of orchestrationGen) {
2057
2512
  // Update tool step context for chat messages
@@ -2064,6 +2519,7 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2064
2519
  vibeEventToChat(ctx, event);
2065
2520
  updateEditorFromEvent(event, pipelineEditor, plan.workflowType, config, options?.parentFeatureId);
2066
2521
  sendStepResultMessage(event, pipelineEditor, event.data?.totalSteps ?? 0);
2522
+ _pi.events.emit("vibe:pipeline", event);
2067
2523
  // Update orchestration state on feature complete/fail/skip (runs before abort check)
2068
2524
  if (event.type === "orchestration_complete" && event.data) {
2069
2525
  await saveOrcState("done", {
@@ -2097,13 +2553,17 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2097
2553
  break;
2098
2554
  }
2099
2555
  }
2100
- // Preserve state when failed features exist to allow resume
2556
+ // Preserve state when failed features exist or user aborted to allow resume
2101
2557
  const finalState = await store.loadOrchestrationState();
2102
- if (finalState && finalState.failedFeatures.length === 0) {
2558
+ if (finalState && finalState.failedFeatures.length === 0 && !abortHandle.aborted) {
2103
2559
  // Run Documenter only when all features succeeded
2104
- if (!abortHandle.aborted && finalState.completedFeatures.length > 0) {
2560
+ if (finalState.completedFeatures.length > 0) {
2105
2561
  await runDocumenter(store, ctx, config, finalState.completedFeatures, pipelineEditor, abortHandle);
2106
2562
  }
2563
+ // Update QMD index with new artifacts
2564
+ if (!abortHandle.aborted) {
2565
+ await updateQmdIndex(ctx.cwd);
2566
+ }
2107
2567
  // Push baseBranch to remote after all features succeeded
2108
2568
  if (!abortHandle.aborted) {
2109
2569
  pipelineEditor.setDetail("pushing to remote...");
@@ -2151,13 +2611,17 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2151
2611
  singleEditorCallbacks.onAgentFinished();
2152
2612
  },
2153
2613
  logger,
2154
- availableSkills,
2155
- projectContext,
2156
2614
  });
2157
2615
  abortHandle.currentRunner = runner;
2158
2616
  activePipelines.set(featureId, { pipeline, runner });
2159
2617
  pipelineEditor.setPhase(`Running: ${featureId}`);
2160
2618
  pipelineEditor.setSteps(pipelineStepsToInfo(pipeline));
2619
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2620
+ _pi.events.emit("vibe:pipeline", {
2621
+ type: "feature_start",
2622
+ featureId,
2623
+ data: { title: featureId, totalSteps: pipeline.steps.length },
2624
+ });
2161
2625
  for await (const event of runner.run(pipeline)) {
2162
2626
  // Update tool step context for chat messages
2163
2627
  if (event.step) {
@@ -2172,6 +2636,7 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2172
2636
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2173
2637
  }
2174
2638
  sendStepResultMessage(event, pipelineEditor, pipeline.steps.length);
2639
+ _pi.events.emit("vibe:pipeline", event);
2175
2640
  if (abortHandle.aborted) {
2176
2641
  vibeGate("Aborted by user (ESC)");
2177
2642
  break;
@@ -2199,6 +2664,190 @@ async function handleWorkflow(store, ctx, workflowType, _requirement, options, l
2199
2664
  deactivatePipelineEditor(ctx);
2200
2665
  }
2201
2666
  }
2667
+ async function handleRecover(store, ctx) {
2668
+ // Precondition: state already exists
2669
+ if (await store.hasOrchestrationState()) {
2670
+ ctx.ui.notify("[Vibe] Orchestration state already exists. Use /vibe resume instead.", "warning");
2671
+ return;
2672
+ }
2673
+ // Precondition: .vibe/ initialized
2674
+ if (!(await store.isInitialized())) {
2675
+ ctx.ui.notify("[Vibe] No .vibe/ directory found. Run /vibe init first.", "warning");
2676
+ return;
2677
+ }
2678
+ // Precondition: plan.json exists
2679
+ let planJson;
2680
+ try {
2681
+ const plan = await store.loadPlan();
2682
+ planJson = JSON.stringify(plan, null, "\t");
2683
+ }
2684
+ catch {
2685
+ ctx.ui.notify("[Vibe] Cannot recover without plan.json. Run a new /vibe command instead.", "error");
2686
+ return;
2687
+ }
2688
+ // Gather context for the agent
2689
+ const config = await store.loadConfig();
2690
+ const configJson = JSON.stringify(config, null, "\t");
2691
+ // List artifacts per feature
2692
+ const features = await store.listFeatures();
2693
+ const featureArtifacts = [];
2694
+ for (const fId of features) {
2695
+ const featureDir = store.getFeatureDir(fId);
2696
+ try {
2697
+ const entries = await readdir(featureDir);
2698
+ featureArtifacts.push(`${fId}/: ${entries.join(", ")}`);
2699
+ }
2700
+ catch {
2701
+ featureArtifacts.push(`${fId}/: (empty or inaccessible)`);
2702
+ }
2703
+ }
2704
+ // Requirements first lines
2705
+ let requirementSnippet = "";
2706
+ try {
2707
+ const reqPath = join(ctx.cwd, ".vibe", "requirements.md");
2708
+ const reqContent = await readFile(reqPath, "utf-8");
2709
+ requirementSnippet = reqContent.split("\n").slice(0, 5).join("\n");
2710
+ }
2711
+ catch {
2712
+ requirementSnippet = "(requirements.md not found)";
2713
+ }
2714
+ // Git info
2715
+ let gitInfo = "";
2716
+ try {
2717
+ const execFileAsync = promisify(execFile);
2718
+ const { stdout: currentBranch } = await execFileAsync("git", ["branch", "--show-current"], { cwd: ctx.cwd });
2719
+ const { stdout: branches } = await execFileAsync("git", ["branch", "--list"], { cwd: ctx.cwd });
2720
+ const featureBranches = branches
2721
+ .split("\n")
2722
+ .map((b) => b.trim().replace("* ", ""))
2723
+ .filter((b) => b.startsWith("feat/") || b.startsWith("fix/") || b.startsWith("enhance/") || b.startsWith("refactor/"));
2724
+ gitInfo = `Current branch: ${currentBranch.trim()}\nFeature branches:\n${featureBranches.map((b) => ` - ${b}`).join("\n") || " (none)"}`;
2725
+ }
2726
+ catch {
2727
+ gitInfo = "(git info unavailable)";
2728
+ }
2729
+ const contextPrompt = `# Recovery Context
2730
+
2731
+ ## plan.json
2732
+ \`\`\`json
2733
+ ${planJson}
2734
+ \`\`\`
2735
+
2736
+ ## config.json
2737
+ \`\`\`json
2738
+ ${configJson}
2739
+ \`\`\`
2740
+
2741
+ ## Feature Artifacts
2742
+ ${featureArtifacts.join("\n")}
2743
+
2744
+ ## Requirements (first 5 lines)
2745
+ ${requirementSnippet}
2746
+
2747
+ ## Git Info
2748
+ ${gitInfo}
2749
+
2750
+ ## Task
2751
+ 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.`;
2752
+ // Run the recovery agent
2753
+ const abortHandle = createAbortHandle();
2754
+ const pipelineEditor = activatePipelineEditor(ctx, "Recovering state", abortHandle);
2755
+ pipelineEditor.setTitle("Vibe ─ Recover");
2756
+ pipelineEditor.setPhase("Analyzing artifacts");
2757
+ const systemPrompt = getSystemPromptForRole("recover");
2758
+ const agent = await createRoleAgent({
2759
+ role: "recover",
2760
+ systemPrompt,
2761
+ config,
2762
+ projectRoot: ctx.cwd,
2763
+ model: ctx.model,
2764
+ getApiKey: (provider) => ctx.modelRegistry.getApiKeyForProvider(provider),
2765
+ });
2766
+ abortHandle.currentAgent = agent;
2767
+ let responseText;
2768
+ try {
2769
+ responseText = await runAgent(agent, contextPrompt, {
2770
+ onEvent: createAgentProgressHandler(ctx, "Recovering", pipelineEditor, ctx.cwd),
2771
+ });
2772
+ }
2773
+ catch (error) {
2774
+ if (abortHandle.aborted) {
2775
+ vibeGate("Recovery aborted by user");
2776
+ }
2777
+ else {
2778
+ const msg = error instanceof Error ? error.message : String(error);
2779
+ vibeError(`Recovery agent failed: ${msg}`);
2780
+ }
2781
+ ctx.ui.setStatus("vibe", undefined);
2782
+ deactivatePipelineEditor(ctx);
2783
+ return;
2784
+ }
2785
+ finally {
2786
+ abortHandle.currentAgent = undefined;
2787
+ }
2788
+ if (abortHandle.aborted) {
2789
+ vibeGate("Recovery aborted by user");
2790
+ ctx.ui.setStatus("vibe", undefined);
2791
+ deactivatePipelineEditor(ctx);
2792
+ return;
2793
+ }
2794
+ ctx.ui.setStatus("vibe", undefined);
2795
+ deactivatePipelineEditor(ctx);
2796
+ // Extract JSON from response
2797
+ const jsonStr = extractRecoverJson(responseText);
2798
+ if (!jsonStr) {
2799
+ ctx.ui.notify("[Vibe] Could not extract orchestration state JSON from agent response.", "error");
2800
+ return;
2801
+ }
2802
+ let proposedState;
2803
+ try {
2804
+ proposedState = JSON.parse(jsonStr);
2805
+ }
2806
+ catch {
2807
+ ctx.ui.notify("[Vibe] Agent returned invalid JSON.", "error");
2808
+ return;
2809
+ }
2810
+ // Validate required fields
2811
+ if (!proposedState.phase || !proposedState.workflowType || !Array.isArray(proposedState.completedFeatures)) {
2812
+ ctx.ui.notify("[Vibe] Agent returned incomplete state (missing phase, workflowType, or completedFeatures).", "error");
2813
+ return;
2814
+ }
2815
+ // Ensure updatedAt
2816
+ if (!proposedState.updatedAt) {
2817
+ proposedState.updatedAt = new Date().toISOString();
2818
+ }
2819
+ // Ensure arrays
2820
+ if (!Array.isArray(proposedState.failedFeatures))
2821
+ proposedState.failedFeatures = [];
2822
+ if (!Array.isArray(proposedState.skippedFeatures))
2823
+ proposedState.skippedFeatures = [];
2824
+ // Display proposed state and ask for confirmation
2825
+ const summary = [
2826
+ `Workflow: ${proposedState.workflowType}`,
2827
+ `Phase: ${proposedState.phase}`,
2828
+ `Base branch: ${proposedState.baseBranch ?? "(not set)"}`,
2829
+ `Completed: ${proposedState.completedFeatures.length > 0 ? proposedState.completedFeatures.join(", ") : "(none)"}`,
2830
+ `Failed: ${proposedState.failedFeatures.length > 0 ? proposedState.failedFeatures.join(", ") : "(none)"}`,
2831
+ `Skipped: ${proposedState.skippedFeatures.length > 0 ? proposedState.skippedFeatures.join(", ") : "(none)"}`,
2832
+ ].join("\n");
2833
+ const choice = await ctx.ui.select(`Proposed recovery state:\n${summary}\n\nSave and enable /vibe resume?`, [
2834
+ "Approve",
2835
+ "Abort",
2836
+ ]);
2837
+ if (choice === "Approve") {
2838
+ await store.saveOrchestrationState(proposedState);
2839
+ ctx.ui.notify("[Vibe] Orchestration state recovered. Use /vibe resume to continue.", "info");
2840
+ vibeMilestone("Orchestration state recovered successfully");
2841
+ }
2842
+ else {
2843
+ vibeMilestone("Recovery aborted by user");
2844
+ }
2845
+ }
2846
+ /** Extract JSON code block from agent response text. */
2847
+ export function extractRecoverJson(text) {
2848
+ const match = text.match(/```json\s*\n([\s\S]*?)\n```/);
2849
+ return match ? match[1] : null;
2850
+ }
2202
2851
  async function handlePause(featureId, ctx) {
2203
2852
  const active = activePipelines.get(featureId);
2204
2853
  if (!active) {
@@ -2214,6 +2863,12 @@ async function handleResume(store, ctx, featureId, logger) {
2214
2863
  if (active) {
2215
2864
  const abortHandle = createAbortHandle();
2216
2865
  abortHandle.currentRunner = active.runner;
2866
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2867
+ _pi.events.emit("vibe:pipeline", {
2868
+ type: "feature_start",
2869
+ featureId,
2870
+ data: { totalSteps: active.pipeline.steps.length },
2871
+ });
2217
2872
  const pipelineEditor = activatePipelineEditor(ctx, featureId, abortHandle);
2218
2873
  pipelineEditor.setTitle(`Vibe ─ Resuming`);
2219
2874
  pipelineEditor.setPhase(featureId);
@@ -2229,6 +2884,7 @@ async function handleResume(store, ctx, featureId, logger) {
2229
2884
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2230
2885
  }
2231
2886
  sendStepResultMessage(event, pipelineEditor, active.pipeline.steps.length);
2887
+ _pi.events.emit("vibe:pipeline", event);
2232
2888
  if (abortHandle.aborted) {
2233
2889
  vibeGate("Aborted by user (ESC)");
2234
2890
  break;
@@ -2274,19 +2930,48 @@ async function handleResume(store, ctx, featureId, logger) {
2274
2930
  pipelineEditor.setPhase(persistedPhase);
2275
2931
  pipelineEditor.setSteps(pipelineStepsToInfo(pipeline));
2276
2932
  pipelineEditor.setCurrentStep(pipeline.currentStep);
2933
+ // Emit feature_start so the Telegram bridge enters pipeline mode
2934
+ _pi.events.emit("vibe:pipeline", {
2935
+ type: "feature_start",
2936
+ featureId,
2937
+ data: { title: persistedPhase, totalSteps: pipeline.steps.length },
2938
+ });
2939
+ // Build requestApproval for pipeline-internal gates (merge_approve, etc.)
2940
+ const persistedRequestApproval = async (_step, _fId, summary) => {
2941
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
2942
+ const wasExpanded = ctx.ui.getToolsExpanded();
2943
+ if (!wasExpanded)
2944
+ ctx.ui.setToolsExpanded(true);
2945
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
2946
+ if (!wasExpanded)
2947
+ ctx.ui.setToolsExpanded(false);
2948
+ switch (choice) {
2949
+ case "Approve":
2950
+ return { passed: true, action: "approved" };
2951
+ case "Reject": {
2952
+ const feedback = await ctx.ui.input("Feedback", "Enter reason for rejection");
2953
+ return { passed: false, action: "feedback", feedback: feedback ?? "" };
2954
+ }
2955
+ case "Skip":
2956
+ return { passed: true, action: "skip" };
2957
+ case "Abort":
2958
+ return { passed: false, action: "abort" };
2959
+ default:
2960
+ return { passed: false, action: "pause" };
2961
+ }
2962
+ };
2277
2963
  const persistedToolCtx = { featureId: featureId, role: "", action: "" };
2278
2964
  const persistedCallbacks = createEditorAgentCallbacks(pipelineEditor, ctx.cwd, persistedAbortHandle, persistedToolCtx);
2279
- const resumeProjectContext = (await store.hasProjectContext()) ? await store.readProjectContext() : undefined;
2280
2965
  const runner = new PipelineRunner({
2281
2966
  store,
2282
2967
  config,
2283
2968
  projectRoot: ctx.cwd,
2969
+ requestApproval: persistedRequestApproval,
2284
2970
  getApiKey,
2285
2971
  model: ctx.model,
2286
2972
  onAgentCreated: persistedCallbacks.onAgentCreated,
2287
2973
  onAgentFinished: persistedCallbacks.onAgentFinished,
2288
2974
  logger,
2289
- projectContext: resumeProjectContext,
2290
2975
  });
2291
2976
  persistedAbortHandle.currentRunner = runner;
2292
2977
  try {
@@ -2304,6 +2989,7 @@ async function handleResume(store, ctx, featureId, logger) {
2304
2989
  pipelineEditor.setDetail(`${event.step.agent}: ${event.step.action}`);
2305
2990
  }
2306
2991
  sendStepResultMessage(event, pipelineEditor, pipeline.steps.length);
2992
+ _pi.events.emit("vibe:pipeline", event);
2307
2993
  if (persistedAbortHandle.aborted) {
2308
2994
  vibeGate("Aborted by user (ESC)");
2309
2995
  break;
@@ -2335,7 +3021,12 @@ async function handleResume(store, ctx, featureId, logger) {
2335
3021
  const orcState = await store.loadOrchestrationState();
2336
3022
  if (orcState) {
2337
3023
  vibeMilestone(`Resuming orchestration from phase: ${orcState.phase}`);
2338
- if (orcState.phase === "single_pipeline" && orcState.singleFeatureId) {
3024
+ if (orcState.phase === "single_pipeline") {
3025
+ if (!orcState.singleFeatureId) {
3026
+ ctx.ui.notify("[Vibe] Cannot resume: single pipeline state is missing featureId. Run a new /vibe command.", "error");
3027
+ await store.clearOrchestrationState();
3028
+ return;
3029
+ }
2339
3030
  // Single-feature path: recursive call with the featureId
2340
3031
  await handleResume(store, ctx, orcState.singleFeatureId, logger);
2341
3032
  return;
@@ -2348,7 +3039,38 @@ async function handleResume(store, ctx, featureId, logger) {
2348
3039
  config = { ...config, baseBranch: orcState.baseBranch };
2349
3040
  }
2350
3041
  const getApiKey = (provider) => ctx.modelRegistry.getApiKeyForProvider(provider);
2351
- const plan = await store.loadPlan();
3042
+ let plan;
3043
+ try {
3044
+ plan = await store.loadPlan();
3045
+ }
3046
+ catch {
3047
+ ctx.ui.notify("[Vibe] Cannot resume: plan.json is missing or corrupted. Run a new /vibe command.", "error");
3048
+ return;
3049
+ }
3050
+ // Build requestApproval for pipeline-internal gates (merge_approve, etc.)
3051
+ const requestApproval = async (_step, _fId, summary) => {
3052
+ _pi.events.emit("vibe:gate", { featureId: _fId, summary });
3053
+ const wasExpanded = ctx.ui.getToolsExpanded();
3054
+ if (!wasExpanded)
3055
+ ctx.ui.setToolsExpanded(true);
3056
+ const choice = await waitForGateChoice(_pi.events, ctx.ui, summary, ["Approve", "Reject", "Skip", "Abort"]);
3057
+ if (!wasExpanded)
3058
+ ctx.ui.setToolsExpanded(false);
3059
+ switch (choice) {
3060
+ case "Approve":
3061
+ return { passed: true, action: "approved" };
3062
+ case "Reject": {
3063
+ const feedback = await ctx.ui.input("Feedback", "Enter reason for rejection");
3064
+ return { passed: false, action: "feedback", feedback: feedback ?? "" };
3065
+ }
3066
+ case "Skip":
3067
+ return { passed: true, action: "skip" };
3068
+ case "Abort":
3069
+ return { passed: false, action: "abort" };
3070
+ default:
3071
+ return { passed: false, action: "pause" };
3072
+ }
3073
+ };
2352
3074
  vibeMilestone(`Resuming orchestration: ${orcState.completedFeatures.length} completed, ${plan.features.length - orcState.completedFeatures.length} remaining`);
2353
3075
  // Reset phase to orchestrating on resume
2354
3076
  orcState.phase = "orchestrating";
@@ -2359,16 +3081,13 @@ async function handleResume(store, ctx, featureId, logger) {
2359
3081
  pipelineEditor.setTitle(`Vibe ─ ${WORKFLOW_TITLES[orcState.workflowType] ?? "Resuming"}`);
2360
3082
  const resumeToolCtx = { featureId: "", role: "", action: "" };
2361
3083
  const resumeOrcCallbacks = createEditorAgentCallbacks(pipelineEditor, ctx.cwd, resumeAbortHandle, resumeToolCtx);
2362
- const availableSkills = await buildAvailableSkillsPrompt(store);
2363
- const resumeOrcProjectContext = (await store.hasProjectContext())
2364
- ? await store.readProjectContext()
2365
- : undefined;
2366
3084
  try {
2367
3085
  for await (const event of runOrchestration({
2368
3086
  store,
2369
3087
  config,
2370
3088
  projectRoot: ctx.cwd,
2371
3089
  plan,
3090
+ requestApproval,
2372
3091
  getApiKey,
2373
3092
  model: ctx.model,
2374
3093
  parentFeatureId: orcState.options?.parentFeatureId,
@@ -2376,8 +3095,6 @@ async function handleResume(store, ctx, featureId, logger) {
2376
3095
  onAgentCreated: resumeOrcCallbacks.onAgentCreated,
2377
3096
  onAgentFinished: resumeOrcCallbacks.onAgentFinished,
2378
3097
  logger,
2379
- availableSkills,
2380
- projectContext: resumeOrcProjectContext,
2381
3098
  })) {
2382
3099
  // Update tool step context for chat messages
2383
3100
  if (event.type === "feature_start")
@@ -2389,6 +3106,7 @@ async function handleResume(store, ctx, featureId, logger) {
2389
3106
  vibeEventToChat(ctx, event);
2390
3107
  updateEditorFromEvent(event, pipelineEditor, plan.workflowType, config, orcState.options?.parentFeatureId);
2391
3108
  sendStepResultMessage(event, pipelineEditor, event.data?.totalSteps ?? 0);
3109
+ _pi.events.emit("vibe:pipeline", event);
2392
3110
  // Update orchestration state: overwrite with final result on orchestration_complete
2393
3111
  if (event.type === "orchestration_complete" && event.data) {
2394
3112
  const currentState = await store.loadOrchestrationState();
@@ -2426,13 +3144,17 @@ async function handleResume(store, ctx, featureId, logger) {
2426
3144
  break;
2427
3145
  }
2428
3146
  }
2429
- // Preserve state when failed features exist to allow resume
3147
+ // Preserve state when failed features exist or user aborted to allow resume
2430
3148
  const finalState = await store.loadOrchestrationState();
2431
- if (finalState && finalState.failedFeatures.length === 0) {
3149
+ if (finalState && finalState.failedFeatures.length === 0 && !resumeAbortHandle.aborted) {
2432
3150
  // Run Documenter only when all features succeeded
2433
- if (!resumeAbortHandle.aborted && finalState.completedFeatures.length > 0) {
3151
+ if (finalState.completedFeatures.length > 0) {
2434
3152
  await runDocumenter(store, ctx, config, finalState.completedFeatures, pipelineEditor, resumeAbortHandle);
2435
3153
  }
3154
+ // Update QMD index with new artifacts
3155
+ if (!resumeAbortHandle.aborted) {
3156
+ await updateQmdIndex(ctx.cwd);
3157
+ }
2436
3158
  // Push baseBranch to remote after all features succeeded
2437
3159
  if (!resumeAbortHandle.aborted) {
2438
3160
  pipelineEditor.setDetail("pushing to remote...");