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