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