vemora 0.1.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +851 -0
  2. package/dist/cli.d.ts +16 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +682 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/ask.d.ts +14 -0
  7. package/dist/commands/ask.d.ts.map +1 -0
  8. package/dist/commands/ask.js +137 -0
  9. package/dist/commands/ask.js.map +1 -0
  10. package/dist/commands/audit.d.ts +17 -0
  11. package/dist/commands/audit.d.ts.map +1 -0
  12. package/dist/commands/audit.js +398 -0
  13. package/dist/commands/audit.js.map +1 -0
  14. package/dist/commands/brief.d.ts +16 -0
  15. package/dist/commands/brief.d.ts.map +1 -0
  16. package/dist/commands/brief.js +84 -0
  17. package/dist/commands/brief.js.map +1 -0
  18. package/dist/commands/chat.d.ts +7 -0
  19. package/dist/commands/chat.d.ts.map +1 -0
  20. package/dist/commands/chat.js +155 -0
  21. package/dist/commands/chat.js.map +1 -0
  22. package/dist/commands/context.d.ts +63 -0
  23. package/dist/commands/context.d.ts.map +1 -0
  24. package/dist/commands/context.js +794 -0
  25. package/dist/commands/context.js.map +1 -0
  26. package/dist/commands/dead-code.d.ts +15 -0
  27. package/dist/commands/dead-code.d.ts.map +1 -0
  28. package/dist/commands/dead-code.js +206 -0
  29. package/dist/commands/dead-code.js.map +1 -0
  30. package/dist/commands/deps.d.ts +20 -0
  31. package/dist/commands/deps.d.ts.map +1 -0
  32. package/dist/commands/deps.js +138 -0
  33. package/dist/commands/deps.js.map +1 -0
  34. package/dist/commands/focus.d.ts +8 -0
  35. package/dist/commands/focus.d.ts.map +1 -0
  36. package/dist/commands/focus.js +310 -0
  37. package/dist/commands/focus.js.map +1 -0
  38. package/dist/commands/index.d.ts +10 -0
  39. package/dist/commands/index.d.ts.map +1 -0
  40. package/dist/commands/index.js +366 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/init-agent.d.ts +23 -0
  43. package/dist/commands/init-agent.d.ts.map +1 -0
  44. package/dist/commands/init-agent.js +384 -0
  45. package/dist/commands/init-agent.js.map +1 -0
  46. package/dist/commands/init.d.ts +2 -0
  47. package/dist/commands/init.d.ts.map +1 -0
  48. package/dist/commands/init.js +122 -0
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/knowledge.d.ts +14 -0
  51. package/dist/commands/knowledge.d.ts.map +1 -0
  52. package/dist/commands/knowledge.js +115 -0
  53. package/dist/commands/knowledge.js.map +1 -0
  54. package/dist/commands/plan.d.ts +24 -0
  55. package/dist/commands/plan.d.ts.map +1 -0
  56. package/dist/commands/plan.js +867 -0
  57. package/dist/commands/plan.js.map +1 -0
  58. package/dist/commands/query.d.ts +39 -0
  59. package/dist/commands/query.d.ts.map +1 -0
  60. package/dist/commands/query.js +392 -0
  61. package/dist/commands/query.js.map +1 -0
  62. package/dist/commands/remember.d.ts +11 -0
  63. package/dist/commands/remember.d.ts.map +1 -0
  64. package/dist/commands/remember.js +267 -0
  65. package/dist/commands/remember.js.map +1 -0
  66. package/dist/commands/report.d.ts +10 -0
  67. package/dist/commands/report.d.ts.map +1 -0
  68. package/dist/commands/report.js +243 -0
  69. package/dist/commands/report.js.map +1 -0
  70. package/dist/commands/status.d.ts +2 -0
  71. package/dist/commands/status.d.ts.map +1 -0
  72. package/dist/commands/status.js +127 -0
  73. package/dist/commands/status.js.map +1 -0
  74. package/dist/commands/summarize.d.ts +14 -0
  75. package/dist/commands/summarize.d.ts.map +1 -0
  76. package/dist/commands/summarize.js +170 -0
  77. package/dist/commands/summarize.js.map +1 -0
  78. package/dist/commands/triage.d.ts +33 -0
  79. package/dist/commands/triage.d.ts.map +1 -0
  80. package/dist/commands/triage.js +419 -0
  81. package/dist/commands/triage.js.map +1 -0
  82. package/dist/commands/usages.d.ts +14 -0
  83. package/dist/commands/usages.d.ts.map +1 -0
  84. package/dist/commands/usages.js +236 -0
  85. package/dist/commands/usages.js.map +1 -0
  86. package/dist/core/config.d.ts +35 -0
  87. package/dist/core/config.d.ts.map +1 -0
  88. package/dist/core/config.js +159 -0
  89. package/dist/core/config.js.map +1 -0
  90. package/dist/core/types.d.ts +287 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +4 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/embeddings/factory.d.ts +9 -0
  95. package/dist/embeddings/factory.d.ts.map +1 -0
  96. package/dist/embeddings/factory.js +26 -0
  97. package/dist/embeddings/factory.js.map +1 -0
  98. package/dist/embeddings/noop.d.ts +17 -0
  99. package/dist/embeddings/noop.d.ts.map +1 -0
  100. package/dist/embeddings/noop.js +22 -0
  101. package/dist/embeddings/noop.js.map +1 -0
  102. package/dist/embeddings/ollama.d.ts +11 -0
  103. package/dist/embeddings/ollama.d.ts.map +1 -0
  104. package/dist/embeddings/ollama.js +49 -0
  105. package/dist/embeddings/ollama.js.map +1 -0
  106. package/dist/embeddings/openai.d.ts +10 -0
  107. package/dist/embeddings/openai.d.ts.map +1 -0
  108. package/dist/embeddings/openai.js +67 -0
  109. package/dist/embeddings/openai.js.map +1 -0
  110. package/dist/embeddings/provider.d.ts +19 -0
  111. package/dist/embeddings/provider.d.ts.map +1 -0
  112. package/dist/embeddings/provider.js +3 -0
  113. package/dist/embeddings/provider.js.map +1 -0
  114. package/dist/indexer/callgraph.d.ts +16 -0
  115. package/dist/indexer/callgraph.d.ts.map +1 -0
  116. package/dist/indexer/callgraph.js +154 -0
  117. package/dist/indexer/callgraph.js.map +1 -0
  118. package/dist/indexer/chunkBySlidingWindow.d.ts +6 -0
  119. package/dist/indexer/chunkBySlidingWindow.d.ts.map +1 -0
  120. package/dist/indexer/chunkBySlidingWindow.js +30 -0
  121. package/dist/indexer/chunkBySlidingWindow.js.map +1 -0
  122. package/dist/indexer/chunkBySymbols.d.ts +7 -0
  123. package/dist/indexer/chunkBySymbols.d.ts.map +1 -0
  124. package/dist/indexer/chunkBySymbols.js +57 -0
  125. package/dist/indexer/chunkBySymbols.js.map +1 -0
  126. package/dist/indexer/chunker.d.ts +15 -0
  127. package/dist/indexer/chunker.d.ts.map +1 -0
  128. package/dist/indexer/chunker.js +26 -0
  129. package/dist/indexer/chunker.js.map +1 -0
  130. package/dist/indexer/classHeader.d.ts +7 -0
  131. package/dist/indexer/classHeader.d.ts.map +1 -0
  132. package/dist/indexer/classHeader.js +37 -0
  133. package/dist/indexer/classHeader.js.map +1 -0
  134. package/dist/indexer/deps.d.ts +66 -0
  135. package/dist/indexer/deps.d.ts.map +1 -0
  136. package/dist/indexer/deps.js +412 -0
  137. package/dist/indexer/deps.js.map +1 -0
  138. package/dist/indexer/hasher.d.ts +17 -0
  139. package/dist/indexer/hasher.d.ts.map +1 -0
  140. package/dist/indexer/hasher.js +38 -0
  141. package/dist/indexer/hasher.js.map +1 -0
  142. package/dist/indexer/parser.d.ts +18 -0
  143. package/dist/indexer/parser.d.ts.map +1 -0
  144. package/dist/indexer/parser.js +355 -0
  145. package/dist/indexer/parser.js.map +1 -0
  146. package/dist/indexer/scanner.d.ts +18 -0
  147. package/dist/indexer/scanner.d.ts.map +1 -0
  148. package/dist/indexer/scanner.js +37 -0
  149. package/dist/indexer/scanner.js.map +1 -0
  150. package/dist/indexer/strategy.d.ts +11 -0
  151. package/dist/indexer/strategy.d.ts.map +1 -0
  152. package/dist/indexer/strategy.js +15 -0
  153. package/dist/indexer/strategy.js.map +1 -0
  154. package/dist/indexer/tests.d.ts +15 -0
  155. package/dist/indexer/tests.d.ts.map +1 -0
  156. package/dist/indexer/tests.js +68 -0
  157. package/dist/indexer/tests.js.map +1 -0
  158. package/dist/indexer/todos.d.ts +9 -0
  159. package/dist/indexer/todos.d.ts.map +1 -0
  160. package/dist/indexer/todos.js +29 -0
  161. package/dist/indexer/todos.js.map +1 -0
  162. package/dist/llm/anthropic.d.ts +8 -0
  163. package/dist/llm/anthropic.d.ts.map +1 -0
  164. package/dist/llm/anthropic.js +76 -0
  165. package/dist/llm/anthropic.js.map +1 -0
  166. package/dist/llm/claude-code.d.ts +37 -0
  167. package/dist/llm/claude-code.d.ts.map +1 -0
  168. package/dist/llm/claude-code.js +97 -0
  169. package/dist/llm/claude-code.js.map +1 -0
  170. package/dist/llm/factory.d.ts +7 -0
  171. package/dist/llm/factory.d.ts.map +1 -0
  172. package/dist/llm/factory.js +47 -0
  173. package/dist/llm/factory.js.map +1 -0
  174. package/dist/llm/ollama.d.ts +8 -0
  175. package/dist/llm/ollama.d.ts.map +1 -0
  176. package/dist/llm/ollama.js +83 -0
  177. package/dist/llm/ollama.js.map +1 -0
  178. package/dist/llm/openai.d.ts +8 -0
  179. package/dist/llm/openai.d.ts.map +1 -0
  180. package/dist/llm/openai.js +68 -0
  181. package/dist/llm/openai.js.map +1 -0
  182. package/dist/llm/provider.d.ts +35 -0
  183. package/dist/llm/provider.d.ts.map +1 -0
  184. package/dist/llm/provider.js +3 -0
  185. package/dist/llm/provider.js.map +1 -0
  186. package/dist/search/bm25.d.ts +3 -0
  187. package/dist/search/bm25.d.ts.map +1 -0
  188. package/dist/search/bm25.js +104 -0
  189. package/dist/search/bm25.js.map +1 -0
  190. package/dist/search/formatter.d.ts +43 -0
  191. package/dist/search/formatter.d.ts.map +1 -0
  192. package/dist/search/formatter.js +208 -0
  193. package/dist/search/formatter.js.map +1 -0
  194. package/dist/search/hybrid.d.ts +10 -0
  195. package/dist/search/hybrid.d.ts.map +1 -0
  196. package/dist/search/hybrid.js +53 -0
  197. package/dist/search/hybrid.js.map +1 -0
  198. package/dist/search/merge.d.ts +33 -0
  199. package/dist/search/merge.d.ts.map +1 -0
  200. package/dist/search/merge.js +158 -0
  201. package/dist/search/merge.js.map +1 -0
  202. package/dist/search/mmr.d.ts +23 -0
  203. package/dist/search/mmr.d.ts.map +1 -0
  204. package/dist/search/mmr.js +95 -0
  205. package/dist/search/mmr.js.map +1 -0
  206. package/dist/search/rerank.d.ts +12 -0
  207. package/dist/search/rerank.d.ts.map +1 -0
  208. package/dist/search/rerank.js +113 -0
  209. package/dist/search/rerank.js.map +1 -0
  210. package/dist/search/signature.d.ts +42 -0
  211. package/dist/search/signature.d.ts.map +1 -0
  212. package/dist/search/signature.js +112 -0
  213. package/dist/search/signature.js.map +1 -0
  214. package/dist/search/vector.d.ts +41 -0
  215. package/dist/search/vector.d.ts.map +1 -0
  216. package/dist/search/vector.js +185 -0
  217. package/dist/search/vector.js.map +1 -0
  218. package/dist/storage/cache.d.ts +30 -0
  219. package/dist/storage/cache.d.ts.map +1 -0
  220. package/dist/storage/cache.js +160 -0
  221. package/dist/storage/cache.js.map +1 -0
  222. package/dist/storage/knowledge.d.ts +23 -0
  223. package/dist/storage/knowledge.d.ts.map +1 -0
  224. package/dist/storage/knowledge.js +81 -0
  225. package/dist/storage/knowledge.js.map +1 -0
  226. package/dist/storage/planSession.d.ts +39 -0
  227. package/dist/storage/planSession.d.ts.map +1 -0
  228. package/dist/storage/planSession.js +78 -0
  229. package/dist/storage/planSession.js.map +1 -0
  230. package/dist/storage/repository.d.ts +27 -0
  231. package/dist/storage/repository.d.ts.map +1 -0
  232. package/dist/storage/repository.js +95 -0
  233. package/dist/storage/repository.js.map +1 -0
  234. package/dist/storage/session.d.ts +38 -0
  235. package/dist/storage/session.d.ts.map +1 -0
  236. package/dist/storage/session.js +100 -0
  237. package/dist/storage/session.js.map +1 -0
  238. package/dist/storage/summaries.d.ts +19 -0
  239. package/dist/storage/summaries.d.ts.map +1 -0
  240. package/dist/storage/summaries.js +66 -0
  241. package/dist/storage/summaries.js.map +1 -0
  242. package/dist/storage/usage.d.ts +39 -0
  243. package/dist/storage/usage.d.ts.map +1 -0
  244. package/dist/storage/usage.js +55 -0
  245. package/dist/storage/usage.js.map +1 -0
  246. package/dist/utils/git.d.ts +20 -0
  247. package/dist/utils/git.d.ts.map +1 -0
  248. package/dist/utils/git.js +49 -0
  249. package/dist/utils/git.js.map +1 -0
  250. package/dist/utils/tokenizer.d.ts +32 -0
  251. package/dist/utils/tokenizer.d.ts.map +1 -0
  252. package/dist/utils/tokenizer.js +66 -0
  253. package/dist/utils/tokenizer.js.map +1 -0
  254. package/package.json +71 -0
@@ -0,0 +1,867 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPlan = runPlan;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const child_process_1 = require("child_process");
9
+ const crypto_1 = require("crypto");
10
+ const fs_1 = require("fs");
11
+ const os_1 = require("os");
12
+ const path_1 = __importDefault(require("path"));
13
+ const ora_1 = __importDefault(require("ora"));
14
+ const readline_1 = __importDefault(require("readline"));
15
+ const util_1 = require("util");
16
+ const config_1 = require("../core/config");
17
+ const factory_1 = require("../embeddings/factory");
18
+ const factory_2 = require("../llm/factory");
19
+ const bm25_1 = require("../search/bm25");
20
+ const vector_1 = require("../search/vector");
21
+ const cache_1 = require("../storage/cache");
22
+ const knowledge_1 = require("../storage/knowledge");
23
+ const planSession_1 = require("../storage/planSession");
24
+ const repository_1 = require("../storage/repository");
25
+ const summaries_1 = require("../storage/summaries");
26
+ const tokenizer_1 = require("../utils/tokenizer");
27
+ const context_1 = require("./context");
28
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
29
+ // ─── Prompts ──────────────────────────────────────────────────────────────────
30
+ const PLANNER_SYSTEM_PROMPT = `You are an expert software architect.
31
+ Decompose a complex codebase task into a concrete step-by-step plan for a smaller model to execute.
32
+
33
+ Available step actions:
34
+ read — pull code into context with no LLM call (free; use to share files across steps)
35
+ analyze — executor answers a question in prose (default)
36
+ write — executor produces a unified diff (use for code changes)
37
+ test — run a shell command and capture its output (use to verify changes)
38
+
39
+ Guidelines:
40
+ - Each step must be atomic and focused on a single concern.
41
+ - Prefer "files" and "symbols" over "query" — direct retrieval is faster and cheaper.
42
+ - Use "dependsOn" when a step needs the output of a prior step.
43
+ - Use action:"read" to share a file's content with later write/analyze steps.
44
+ - Use action:"write" for code changes; the executor will produce a unified diff.
45
+ - Use action:"test" with a "command" field to verify changes.
46
+ - If unsure of exact file paths, use "query" as fallback.
47
+ - Keep steps ≤ 7 unless truly necessary.
48
+
49
+ Return ONLY valid JSON — no markdown fences, no explanation:
50
+ {
51
+ "goal": "<one-line summary>",
52
+ "steps": [
53
+ {
54
+ "id": 1,
55
+ "action": "read|analyze|write|test",
56
+ "goal": "<what this step determines or produces>",
57
+ "instruction": "<precise instruction for the executor>",
58
+ "files": ["src/path/to/file.ts"],
59
+ "symbols": ["SymbolName"],
60
+ "query": "<fallback search query>",
61
+ "dependsOn": [],
62
+ "command": "<shell command, only for action:test>"
63
+ }
64
+ ]
65
+ }`;
66
+ const EXECUTOR_ANALYZE_PROMPT = "You are an expert software engineer. " +
67
+ "Answer the specific question using ONLY the provided code context. " +
68
+ "Be concise and technical. Reference file paths and symbol names when relevant.\n" +
69
+ "If context is insufficient, start your response with INSUFFICIENT: and describe what is missing.";
70
+ const EXECUTOR_WRITE_PROMPT = "You are an expert software engineer. " +
71
+ "Produce the code changes requested as a unified diff.\n\n" +
72
+ "Rules:\n" +
73
+ "- Output ONLY the diff, no explanation before or after.\n" +
74
+ "- Use standard unified diff format: --- a/path and +++ b/path headers.\n" +
75
+ "- Include 3 lines of context around each change.\n" +
76
+ "- If context is insufficient to produce a correct diff, start with INSUFFICIENT: and describe what is missing.";
77
+ const SYNTHESIZER_SYSTEM_PROMPT = "You are an expert software engineer. " +
78
+ "Synthesize the results from multiple analysis steps into a single, coherent, well-structured answer. " +
79
+ "Avoid repeating information. Be concise and precise.";
80
+ const REPLAN_SYSTEM_PROMPT = "You are an expert software architect. " +
81
+ "One or more steps in a plan failed because the executor had insufficient context. " +
82
+ "Provide 1-3 additional steps to recover the missing information.\n\n" +
83
+ "Return ONLY valid JSON — no markdown fences:\n" +
84
+ '{ "steps": [{ "id": <next_id>, "action": "read|analyze", "goal": "...", "instruction": "...", "files": [], "symbols": [], "query": "...", "dependsOn": [<failed_step_id>] }] }';
85
+ const VERIFIER_SYSTEM_PROMPT = "You are an expert software architect reviewing code changes produced by an AI executor. " +
86
+ "Your job is to verify that the executor's output correctly implements the requested goal.\n\n" +
87
+ "Review the diff for:\n" +
88
+ "- Correctness: does it faithfully implement the goal and instruction?\n" +
89
+ "- Completeness: are there obvious missing pieces?\n" +
90
+ "- Safety: are there apparent bugs, regressions, or unsafe changes?\n\n" +
91
+ "Respond with EXACTLY one of the following two options (no other text):\n" +
92
+ "APPROVED\n" +
93
+ "NEEDS_REVISION: <specific, actionable feedback for the executor to address>\n\n" +
94
+ "Be concise. The executor will retry with your feedback injected into its prompt.";
95
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
96
+ /** Topological sort into waves — each wave can run in parallel. */
97
+ function buildExecutionWaves(steps) {
98
+ const waves = [];
99
+ const resolved = new Set();
100
+ let remaining = [...steps];
101
+ while (remaining.length > 0) {
102
+ const wave = remaining.filter((s) => (s.dependsOn ?? []).every((id) => resolved.has(id)));
103
+ if (wave.length === 0)
104
+ break; // circular dependency guard
105
+ for (const s of wave)
106
+ resolved.add(s.id);
107
+ remaining = remaining.filter((s) => !wave.some((w) => w.id === s.id));
108
+ waves.push(wave);
109
+ }
110
+ return waves;
111
+ }
112
+ /** Cache key for context deduplication within a session. */
113
+ function contextCacheKey(step) {
114
+ return JSON.stringify({
115
+ f: [...(step.files ?? [])].sort(),
116
+ s: [...(step.symbols ?? [])].sort(),
117
+ q: step.query,
118
+ });
119
+ }
120
+ /** Prompt the user for y/n confirmation. */
121
+ async function askConfirm(question) {
122
+ return new Promise((resolve) => {
123
+ const rl = readline_1.default.createInterface({
124
+ input: process.stdin,
125
+ output: process.stdout,
126
+ });
127
+ rl.question(question, (answer) => {
128
+ rl.close();
129
+ const a = answer.trim().toLowerCase();
130
+ resolve(a === "y" || a === "yes" || a === "");
131
+ });
132
+ });
133
+ }
134
+ /**
135
+ * Extract the first valid JSON object from a response that may contain
136
+ * surrounding prose, markdown code fences, or both.
137
+ * Priority: code fence → brace-delimited object → raw string.
138
+ */
139
+ function extractJson(raw) {
140
+ // 1. Try markdown code fence (```json ... ``` or ``` ... ```)
141
+ const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
142
+ if (fenceMatch)
143
+ return fenceMatch[1].trim();
144
+ // 2. Try to find the outermost { ... } block
145
+ const start = raw.indexOf("{");
146
+ if (start !== -1) {
147
+ let depth = 0;
148
+ for (let i = start; i < raw.length; i++) {
149
+ if (raw[i] === "{")
150
+ depth++;
151
+ else if (raw[i] === "}") {
152
+ depth--;
153
+ if (depth === 0)
154
+ return raw.slice(start, i + 1);
155
+ }
156
+ }
157
+ }
158
+ // 3. Fallback — let JSON.parse produce the error
159
+ return raw;
160
+ }
161
+ /** Display the plan in a human-readable table before execution. */
162
+ function displayPlan(plan, steps) {
163
+ const actionColor = {
164
+ read: chalk_1.default.blue,
165
+ analyze: chalk_1.default.gray,
166
+ write: chalk_1.default.green,
167
+ test: chalk_1.default.magenta,
168
+ };
169
+ console.log(chalk_1.default.bold(`\nGoal: ${plan.goal}\n`));
170
+ for (const step of steps) {
171
+ const action = step.action ?? "analyze";
172
+ const badge = (actionColor[action] ?? chalk_1.default.gray)(`[${action}]`);
173
+ const targets = (step.files?.length ?? 0) + (step.symbols?.length ?? 0) > 0
174
+ ? chalk_1.default.gray(` → ${[...(step.files ?? []), ...(step.symbols ?? [])].join(", ")}`)
175
+ : step.query
176
+ ? chalk_1.default.gray(` → search: "${step.query}"`)
177
+ : "";
178
+ const deps = step.dependsOn?.length
179
+ ? chalk_1.default.gray(` (depends on: ${step.dependsOn.join(", ")})`)
180
+ : "";
181
+ console.log(` ${chalk_1.default.bold.yellow(String(step.id).padStart(2))} ${badge} ${chalk_1.default.bold(step.goal)}${targets}${deps}`);
182
+ if (action === "test" && step.command) {
183
+ console.log(chalk_1.default.gray(` $ ${step.command}`));
184
+ }
185
+ }
186
+ console.log();
187
+ }
188
+ /**
189
+ * Apply a unified diff to the project using `patch -p1`.
190
+ * Writes the diff to a temp file to avoid shell stdin complexity.
191
+ */
192
+ async function applyDiff(diff, rootDir, dryRun = false) {
193
+ const tmpFile = `${(0, os_1.tmpdir)()}/vemora-${Date.now()}.patch`;
194
+ try {
195
+ (0, fs_1.writeFileSync)(tmpFile, diff, "utf-8");
196
+ const flags = dryRun ? "--dry-run" : "";
197
+ const { stdout, stderr } = await execAsync(`patch -p1 ${flags} < "${tmpFile}"`, { cwd: rootDir });
198
+ return { success: true, output: (stdout + stderr).trim() };
199
+ }
200
+ catch (err) {
201
+ const e = err;
202
+ return {
203
+ success: false,
204
+ output: ((e.stdout ?? "") + (e.stderr ?? "") + (e.message ?? "")).trim(),
205
+ };
206
+ }
207
+ finally {
208
+ try {
209
+ (0, fs_1.unlinkSync)(tmpFile);
210
+ }
211
+ catch {
212
+ /* ignore */
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Normalize an executor's raw output into a clean unified diff:
218
+ * - Strips markdown code fences (```diff ... ```)
219
+ * - Strips explanation prose before the first `--- ` header
220
+ * Returns the cleaned diff and any warnings to surface to the user.
221
+ */
222
+ function normalizeDiff(raw) {
223
+ const warnings = [];
224
+ // Pass through INSUFFICIENT responses untouched
225
+ if (raw.trimStart().startsWith("INSUFFICIENT:")) {
226
+ return { diff: raw, warnings: [] };
227
+ }
228
+ let content = raw.trim();
229
+ // Strip markdown code fences
230
+ const fenceMatch = content.match(/^```(?:diff)?\n?([\s\S]*?)\n?```$/);
231
+ if (fenceMatch) {
232
+ content = fenceMatch[1].trim();
233
+ }
234
+ // Strip explanation prose before the first `--- ` diff header
235
+ const lines = content.split("\n");
236
+ const diffStartIdx = lines.findIndex((l) => l.startsWith("--- "));
237
+ if (diffStartIdx === -1) {
238
+ warnings.push("No diff header (--- a/file) found — executor may have produced prose instead of a unified diff");
239
+ return { diff: content, warnings };
240
+ }
241
+ if (diffStartIdx > 0) {
242
+ warnings.push(`Stripped ${diffStartIdx} line(s) of preamble before the diff`);
243
+ }
244
+ return { diff: lines.slice(diffStartIdx).join("\n"), warnings };
245
+ }
246
+ /**
247
+ * Call the planner to verify a write-step diff.
248
+ * Returns approved=true or approved=false with actionable feedback.
249
+ */
250
+ async function verifyOutput(step, answer, planner, plannerConfig, plannerContext, rootDir, isClaudeCodePlanner) {
251
+ const response = await planner.chat([
252
+ {
253
+ role: "system",
254
+ content: isClaudeCodePlanner
255
+ ? VERIFIER_SYSTEM_PROMPT
256
+ : `${VERIFIER_SYSTEM_PROMPT}\n\n${plannerContext}`,
257
+ },
258
+ {
259
+ role: "user",
260
+ content: `Goal: ${step.goal}\n\nInstruction: ${step.instruction}\n\n` +
261
+ `Executor output:\n\`\`\`diff\n${answer}\n\`\`\``,
262
+ },
263
+ ], { model: plannerConfig.model, temperature: 0.1, projectRoot: rootDir });
264
+ const raw = response.content.trim();
265
+ if (raw.startsWith("APPROVED")) {
266
+ return { approved: true, feedback: "" };
267
+ }
268
+ const feedback = raw.startsWith("NEEDS_REVISION:")
269
+ ? raw.slice("NEEDS_REVISION:".length).trim()
270
+ : raw;
271
+ return { approved: false, feedback };
272
+ }
273
+ /** Build the cheap planner context from summaries + symbol list (no raw code). */
274
+ function buildPlannerContext(projectName, projectOverview, fileSummaries, symbols) {
275
+ const lines = [`# Project: ${projectName}\n`];
276
+ if (projectOverview) {
277
+ lines.push("## Overview\n", projectOverview, "");
278
+ }
279
+ const fileEntries = Object.entries(fileSummaries);
280
+ if (fileEntries.length > 0) {
281
+ lines.push(`## Files (${fileEntries.length})\n`);
282
+ for (const [file, entry] of fileEntries) {
283
+ lines.push(`${file} — ${entry.summary}`);
284
+ }
285
+ lines.push("");
286
+ }
287
+ const symbolEntries = Object.entries(symbols);
288
+ if (symbolEntries.length > 0) {
289
+ lines.push(`## Symbols (${symbolEntries.length})\n`);
290
+ for (const [name, entry] of symbolEntries) {
291
+ lines.push(`${name} (${entry.type}) — ${entry.file}:${entry.startLine}`);
292
+ }
293
+ lines.push("");
294
+ }
295
+ return lines.join("\n");
296
+ }
297
+ /** Retrieve and format code context for an executor step, with session-level caching. */
298
+ async function retrieveStepContext(step, config, data, cacheStorage, rootDir, topK, budget, forceKeyword, contextCache) {
299
+ const cacheKey = contextCacheKey(step);
300
+ const cached = contextCache.get(cacheKey);
301
+ if (cached)
302
+ return cached;
303
+ // ── write steps: read targeted files live from disk (not stale index chunks) ─
304
+ if ((step.action ?? "analyze") === "write" && step.files?.length) {
305
+ const liveBlocks = step.files.map((relPath) => {
306
+ const absPath = path_1.default.join(rootDir, relPath);
307
+ if ((0, fs_1.existsSync)(absPath)) {
308
+ const content = (0, fs_1.readFileSync)(absPath, "utf-8");
309
+ return `## ${relPath} (live — read from disk)\n\`\`\`\n${content}\n\`\`\``;
310
+ }
311
+ return `## ${relPath} — FILE NOT FOUND ON DISK`;
312
+ });
313
+ const liveCtx = `# Current File Contents\n\n${liveBlocks.join("\n\n")}`;
314
+ contextCache.set(cacheKey, liveCtx);
315
+ return liveCtx;
316
+ }
317
+ const { chunks, symbols, depGraph, callGraph, fileSummaries, projectOverview, knowledgeEntries } = data;
318
+ let results = [];
319
+ const hasTargets = (step.files?.length ?? 0) + (step.symbols?.length ?? 0) > 0;
320
+ if (hasTargets) {
321
+ const fileSet = new Set(step.files ?? []);
322
+ const symbolSet = new Set(step.symbols ?? []);
323
+ const targeted = chunks.filter((c) => (fileSet.size > 0 && fileSet.has(c.file)) ||
324
+ (symbolSet.size > 0 && c.symbol !== undefined && symbolSet.has(c.symbol)));
325
+ results = targeted.map((chunk) => ({
326
+ chunk,
327
+ score: 1,
328
+ symbol: chunk.symbol ? symbols[chunk.symbol] : undefined,
329
+ }));
330
+ }
331
+ if (results.length === 0) {
332
+ // Guard: LLM may omit query; fall back to goal so BM25 never receives undefined
333
+ const searchQuery = step.query ?? step.goal;
334
+ if (forceKeyword) {
335
+ results = (0, bm25_1.computeBM25Scores)(searchQuery, chunks, symbols, topK);
336
+ }
337
+ else {
338
+ try {
339
+ const cache = cacheStorage.load();
340
+ const cachedCount = cache
341
+ ? (cache.chunkIds?.length ?? Object.keys(cache.embeddings ?? {}).length)
342
+ : 0;
343
+ if (!cache || cachedCount === 0) {
344
+ results = (0, bm25_1.computeBM25Scores)(searchQuery, chunks, symbols, topK);
345
+ }
346
+ else {
347
+ const provider = (0, factory_1.createEmbeddingProvider)(config.embedding);
348
+ const [queryEmbedding] = await provider.embed([searchQuery]);
349
+ results = (0, vector_1.vectorSearch)(queryEmbedding, chunks, cache, symbols, topK);
350
+ if (results.length === 0) {
351
+ results = (0, bm25_1.computeBM25Scores)(searchQuery, chunks, symbols, topK);
352
+ }
353
+ }
354
+ }
355
+ catch {
356
+ results = (0, bm25_1.computeBM25Scores)(searchQuery, chunks, symbols, topK);
357
+ }
358
+ }
359
+ }
360
+ results = (0, tokenizer_1.applyTokenBudget)(results, budget);
361
+ const contextFormat = config.display?.format === "terse" ? "terse" : "plain";
362
+ const contextStr = (0, context_1.generateContextString)(config, results, depGraph, callGraph, fileSummaries, projectOverview, { query: step.query, format: contextFormat }, rootDir, chunks, knowledgeEntries);
363
+ contextCache.set(cacheKey, contextStr);
364
+ return contextStr;
365
+ }
366
+ /** Execute a single step, returning its answer and whether context was insufficient. */
367
+ async function executeStep(step, config, data, cacheStorage, rootDir, executorConfig, executor, topK, budget, forceKeyword, stepResults, contextCache, showContext,
368
+ /** Planner feedback from a previous failed attempt — injected into the executor prompt */
369
+ feedback,
370
+ /** Stream tokens to stdout in real time (only for sequential, non-parallel steps) */
371
+ stream) {
372
+ const action = step.action ?? "analyze";
373
+ // ── action: test — run shell command, no LLM ──────────────────────────────
374
+ if (action === "test") {
375
+ if (!step.command) {
376
+ return { stepId: step.id, answer: "No command specified for test step.", insufficient: false };
377
+ }
378
+ try {
379
+ const { stdout, stderr } = await execAsync(step.command, {
380
+ cwd: rootDir,
381
+ timeout: 60_000,
382
+ });
383
+ const answer = `$ ${step.command}\n\n` +
384
+ (stdout ? `stdout:\n${stdout}` : "") +
385
+ (stderr ? `\nstderr:\n${stderr}` : "");
386
+ return { stepId: step.id, answer, insufficient: false };
387
+ }
388
+ catch (err) {
389
+ const e = err;
390
+ const answer = `$ ${step.command} — FAILED\n\n` +
391
+ (e.stdout ? `stdout:\n${e.stdout}\n` : "") +
392
+ (e.stderr ? `stderr:\n${e.stderr}\n` : "") +
393
+ (e.message ? `error: ${e.message}` : "");
394
+ return { stepId: step.id, answer, insufficient: false };
395
+ }
396
+ }
397
+ // ── retrieve context (shared cache) ──────────────────────────────────────
398
+ const contextStr = await retrieveStepContext(step, config, data, cacheStorage, rootDir, topK, budget, forceKeyword, contextCache);
399
+ if (showContext) {
400
+ console.log(chalk_1.default.gray(" ── context ──────────────────────────────────"));
401
+ console.log(chalk_1.default.gray(contextStr));
402
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────────\n"));
403
+ }
404
+ // ── action: read — return context as-is, no LLM call ─────────────────────
405
+ if (action === "read") {
406
+ return { stepId: step.id, answer: contextStr, insufficient: false };
407
+ }
408
+ // ── inject dependsOn results ──────────────────────────────────────────────
409
+ let dependencySection = "";
410
+ if (step.dependsOn?.length) {
411
+ const parts = [];
412
+ for (const depId of step.dependsOn) {
413
+ const depResult = stepResults.get(depId);
414
+ if (depResult)
415
+ parts.push(`### Step ${depId}\n${depResult}`);
416
+ }
417
+ if (parts.length > 0) {
418
+ dependencySection = `\n\n## Prior step results\n${parts.join("\n\n")}`;
419
+ }
420
+ }
421
+ // ── action: analyze | write — call executor LLM ───────────────────────────
422
+ const systemPrompt = action === "write" ? EXECUTOR_WRITE_PROMPT : EXECUTOR_ANALYZE_PROMPT;
423
+ const feedbackSection = feedback
424
+ ? `\n\n## Planner feedback from previous attempt\n${feedback}\n\nRevise your output to address this feedback.`
425
+ : "";
426
+ const messages = [
427
+ { role: "system", content: `${systemPrompt}\n\n${contextStr}` },
428
+ {
429
+ role: "user",
430
+ content: `Goal: ${step.goal}\n\nInstruction: ${step.instruction}${dependencySection}${feedbackSection}`,
431
+ },
432
+ ];
433
+ const chatOpts = {
434
+ model: executorConfig.model,
435
+ temperature: action === "write" ? 0.1 : 0.3,
436
+ };
437
+ if (stream) {
438
+ // Stream tokens directly to stdout — caller must not print the answer again
439
+ let answer = "";
440
+ process.stdout.write(chalk_1.default.cyan(" ↳ "));
441
+ await executor.chat(messages, {
442
+ ...chatOpts,
443
+ onToken: (token) => {
444
+ process.stdout.write(token);
445
+ answer += token;
446
+ },
447
+ });
448
+ process.stdout.write("\n\n");
449
+ const insufficient = answer.trimStart().startsWith("INSUFFICIENT:");
450
+ return { stepId: step.id, answer, insufficient, streamed: true };
451
+ }
452
+ const response = await executor.chat(messages, chatOpts);
453
+ const answer = response.content;
454
+ const insufficient = answer.trimStart().startsWith("INSUFFICIENT:");
455
+ return { stepId: step.id, answer, insufficient };
456
+ }
457
+ // ─── Main command ─────────────────────────────────────────────────────────────
458
+ async function runPlan(rootDir, task, options = {}) {
459
+ const config = (0, config_1.loadConfig)(rootDir);
460
+ if (!config.summarization && !config.executor) {
461
+ console.error(chalk_1.default.red('No executor LLM configured. Add a "summarization" or "executor" block to .vemora/config.json.'));
462
+ process.exit(1);
463
+ }
464
+ // executor > summarization fallback; planner > executor > summarization fallback
465
+ const executorConfig = (config.executor ?? config.summarization);
466
+ const plannerConfig = config.planner ?? executorConfig;
467
+ const isClaudeCodePlanner = plannerConfig.provider === "claude-code";
468
+ const topK = options.topK ?? 5;
469
+ const budget = options.budget ?? 4000;
470
+ const forceKeyword = options.keyword || config.embedding.provider === "none";
471
+ // ── Load index data once ───────────────────────────────────────────────────
472
+ const repo = new repository_1.RepositoryStorage(rootDir);
473
+ const cacheStorage = new cache_1.EmbeddingCacheStorage(config.projectId);
474
+ const summaryStorage = new summaries_1.SummaryStorage(rootDir);
475
+ const chunks = repo.loadChunks();
476
+ if (chunks.length === 0) {
477
+ console.error(chalk_1.default.red("No index found. Run `vemora index` first."));
478
+ process.exit(1);
479
+ }
480
+ const data = {
481
+ chunks,
482
+ symbols: repo.loadSymbols(),
483
+ depGraph: repo.loadDeps(),
484
+ callGraph: repo.loadCallGraph(),
485
+ fileSummaries: summaryStorage.hasFileSummaries()
486
+ ? summaryStorage.loadFileSummaries()
487
+ : {},
488
+ projectOverview: summaryStorage.loadProjectSummary()?.overview ?? null,
489
+ knowledgeEntries: new knowledge_1.KnowledgeStorage(rootDir).load(),
490
+ };
491
+ const planner = (0, factory_2.createLLMProvider)(plannerConfig);
492
+ const executor = (0, factory_2.createLLMProvider)(executorConfig);
493
+ const sameLLM = plannerConfig.provider === executorConfig.provider &&
494
+ plannerConfig.model === executorConfig.model;
495
+ console.log(chalk_1.default.bold.cyan("\n[vemora plan]"));
496
+ console.log(chalk_1.default.gray(` Planner: ${planner.name} · ${plannerConfig.model}${sameLLM ? " (also executor)" : ""}`));
497
+ if (!sameLLM) {
498
+ console.log(chalk_1.default.gray(` Executor: ${executor.name} · ${executorConfig.model}`));
499
+ }
500
+ const hasSummaries = data.projectOverview !== null || Object.keys(data.fileSummaries).length > 0;
501
+ if (!hasSummaries) {
502
+ console.log(chalk_1.default.yellow(" Tip: run `vemora summarize` for cheaper planning (summaries replace raw code)."));
503
+ }
504
+ console.log();
505
+ // ── Session setup (early — needed to skip planning on resume) ───────────────
506
+ const sessionStorage = new planSession_1.PlanSessionStorage(config.projectId);
507
+ // ── Phase 1 + 2: planner context + plan generation (skipped on resume) ──────
508
+ let plan;
509
+ let plannerContext = "";
510
+ if (options.resumeSession) {
511
+ // Load saved session — plan and results are already there
512
+ const loaded = sessionStorage.load(options.resumeSession);
513
+ if (!loaded) {
514
+ console.error(chalk_1.default.red(`Session "${options.resumeSession}" not found. List sessions with \`vemora sessions --root .\``));
515
+ process.exit(1);
516
+ }
517
+ plan = loaded.plan;
518
+ console.log(chalk_1.default.cyan(` Resuming session ${loaded.shortId} — reusing saved plan (${plan.steps.length} steps)`));
519
+ // Build plannerContext for verifier (still needed if --verify is set)
520
+ plannerContext = hasSummaries
521
+ ? buildPlannerContext(config.projectName, data.projectOverview, data.fileSummaries, data.symbols)
522
+ : "";
523
+ }
524
+ else {
525
+ // Build planner context from summaries + symbol list (no raw code)
526
+ plannerContext = hasSummaries
527
+ ? buildPlannerContext(config.projectName, data.projectOverview, data.fileSummaries, data.symbols)
528
+ : (0, context_1.generateContextString)(config, (0, tokenizer_1.applyTokenBudget)((0, bm25_1.computeBM25Scores)(task, chunks, data.symbols, topK), budget), data.depGraph, data.callGraph, data.fileSummaries, data.projectOverview, { query: task, format: "terse" }, rootDir, chunks, data.knowledgeEntries);
529
+ // When using claude-code, skip pre-built context — Claude explores the
530
+ // project autonomously with its file tools. For other providers, inject
531
+ // the summaries + symbol list so they don't need filesystem access.
532
+ const plannerMessages = isClaudeCodePlanner
533
+ ? [
534
+ { role: "system", content: PLANNER_SYSTEM_PROMPT },
535
+ {
536
+ role: "user",
537
+ content: `The project root is: ${rootDir}\n\n` +
538
+ `Use your file tools (Read, Grep, Glob) to explore the codebase ` +
539
+ `as needed, then decompose this task into concrete steps:\n\n${task}\n\n` +
540
+ `IMPORTANT: your entire response must be a single raw JSON object ` +
541
+ `starting with { and ending with }. No prose, no markdown, no code fences.`,
542
+ },
543
+ ]
544
+ : [
545
+ {
546
+ role: "system",
547
+ content: `${PLANNER_SYSTEM_PROMPT}\n\n${plannerContext}`,
548
+ },
549
+ {
550
+ role: "user",
551
+ content: `Decompose this task into concrete steps:\n\n${task}`,
552
+ },
553
+ ];
554
+ const plannerSpinner = (0, ora_1.default)(isClaudeCodePlanner
555
+ ? "Planning (claude-code exploring codebase)..."
556
+ : "Planning...").start();
557
+ try {
558
+ const plannerResponse = await planner.chat(plannerMessages, {
559
+ model: plannerConfig.model,
560
+ temperature: 0.2,
561
+ projectRoot: rootDir,
562
+ });
563
+ const raw = plannerResponse.content.trim();
564
+ const jsonStr = extractJson(raw);
565
+ plan = JSON.parse(jsonStr);
566
+ if (!Array.isArray(plan.steps) || plan.steps.length === 0) {
567
+ throw new Error("Plan has no steps");
568
+ }
569
+ plannerSpinner.succeed(`Plan ready — ${plan.steps.length} step${plan.steps.length !== 1 ? "s" : ""}`);
570
+ }
571
+ catch (err) {
572
+ plannerSpinner.fail(`Planning failed: ${err.message}`);
573
+ process.exit(1);
574
+ }
575
+ }
576
+ // ── Phase 3: display plan + optional confirmation (skipped on resume) ────────
577
+ if (!options.resumeSession) {
578
+ displayPlan(plan, plan.steps);
579
+ if (options.confirm) {
580
+ const ok = await askConfirm(chalk_1.default.bold("Proceed with this plan? [Y/n] "));
581
+ if (!ok) {
582
+ console.log(chalk_1.default.gray("Aborted."));
583
+ return;
584
+ }
585
+ console.log();
586
+ }
587
+ }
588
+ // ── Phase 4: execute in topological waves (parallel within each wave) ──────
589
+ const waves = buildExecutionWaves(plan.steps);
590
+ const stepResults = new Map();
591
+ const contextCache = new Map();
592
+ let nextId = Math.max(...plan.steps.map((s) => s.id)) + 1;
593
+ let session;
594
+ if (options.resumeSession) {
595
+ // Session was already loaded and plan restored in Phase 1/2 — just re-load to get state
596
+ session = sessionStorage.load(options.resumeSession);
597
+ for (const [k, v] of Object.entries(session.stepResults)) {
598
+ stepResults.set(Number(k), v);
599
+ }
600
+ nextId = session.nextId;
601
+ console.log(chalk_1.default.cyan(` ${session.completedStepIds.length} step(s) already complete — continuing from wave boundary\n`));
602
+ }
603
+ else {
604
+ const sessionId = (0, crypto_1.randomUUID)();
605
+ session = {
606
+ sessionId,
607
+ shortId: sessionId.slice(0, 8),
608
+ task,
609
+ rootDir,
610
+ createdAt: new Date().toISOString(),
611
+ updatedAt: new Date().toISOString(),
612
+ status: "running",
613
+ plan: plan,
614
+ stepResults: {},
615
+ completedStepIds: [],
616
+ nextId,
617
+ };
618
+ sessionStorage.save(session);
619
+ console.log(chalk_1.default.gray(` Session: ${session.shortId} (use --resume ${session.shortId} to continue if interrupted)\n`));
620
+ }
621
+ for (const wave of waves) {
622
+ // Skip waves whose steps are all already done (resume path)
623
+ if (wave.every((s) => stepResults.has(s.id))) {
624
+ const ids = wave.map((s) => s.id).join(", ");
625
+ console.log(chalk_1.default.gray(` ── step${wave.length > 1 ? "s" : ""} [${ids}] already complete — skipping ──`));
626
+ continue;
627
+ }
628
+ const isParallel = wave.length > 1;
629
+ if (isParallel) {
630
+ console.log(chalk_1.default.gray(`── parallel wave (${wave.length} steps: ${wave.map((s) => s.id).join(", ")}) ──`));
631
+ }
632
+ const waveSpinners = wave.map((step) => {
633
+ const action = step.action ?? "analyze";
634
+ const badge = { read: "📖", analyze: "🔍", write: "✏️", test: "🧪" }[action] ?? "•";
635
+ const label = `${badge} [${step.id}] ${step.goal}`;
636
+ return isParallel ? (0, ora_1.default)(` ${label}`).start() : null;
637
+ });
638
+ if (!isParallel) {
639
+ const step = wave[0];
640
+ const action = step.action ?? "analyze";
641
+ const badge = { read: chalk_1.default.blue, analyze: chalk_1.default.gray, write: chalk_1.default.green, test: chalk_1.default.magenta }[action] ?? chalk_1.default.gray;
642
+ const targets = (step.files?.length ?? 0) + (step.symbols?.length ?? 0) > 0
643
+ ? chalk_1.default.gray(` [${[...(step.files ?? []), ...(step.symbols ?? [])].join(", ")}]`)
644
+ : "";
645
+ console.log(`${chalk_1.default.bold.yellow(`[${step.id}/${plan.steps.length}]`)} ${badge(`[${action}]`)} ${chalk_1.default.bold(step.goal)}${targets}`);
646
+ }
647
+ const waveResults = await Promise.all(wave.map((step, i) => executeStep(step, config, data, cacheStorage, rootDir, executorConfig, executor, topK, budget, forceKeyword, stepResults, contextCache, options.showContext ?? false, undefined, // feedback — none on first attempt
648
+ !isParallel).then((result) => {
649
+ waveSpinners[i]?.succeed(` [${step.id}] ${step.goal}`);
650
+ return result;
651
+ }).catch((err) => {
652
+ waveSpinners[i]?.fail(` [${step.id}] ${step.goal}`);
653
+ return { stepId: step.id, answer: `Error: ${err.message}`, insufficient: false, streamed: false };
654
+ })));
655
+ for (const result of waveResults) {
656
+ stepResults.set(result.stepId, result.answer);
657
+ // Skip printing if already streamed to stdout
658
+ if (!isParallel && !result.streamed) {
659
+ const indented = result.answer.replace(/\n/g, "\n ");
660
+ console.log(` ${indented}\n`);
661
+ }
662
+ }
663
+ if (isParallel) {
664
+ for (const result of waveResults) {
665
+ const step = wave.find((s) => s.id === result.stepId);
666
+ console.log(chalk_1.default.bold.yellow(`\n [${step.id}] ${step.goal}`));
667
+ console.log(` ${result.answer.replace(/\n/g, "\n ")}\n`);
668
+ }
669
+ }
670
+ // ── Verify + retry + apply: handle write steps ────────────────────────────
671
+ if (options.verify || options.apply) {
672
+ const maxRetries = options.maxRetries ?? 2;
673
+ for (const result of waveResults) {
674
+ const step = wave.find((s) => s.id === result.stepId);
675
+ if ((step.action ?? "analyze") !== "write")
676
+ continue;
677
+ if (result.insufficient)
678
+ continue;
679
+ // Normalize the diff (strip code fences, preamble) before verify/apply
680
+ const { diff: normalizedDiff, warnings: diffWarnings } = normalizeDiff(result.answer);
681
+ if (diffWarnings.length > 0) {
682
+ for (const w of diffWarnings)
683
+ console.log(chalk_1.default.yellow(` ⚠ ${w}`));
684
+ }
685
+ let currentAnswer = normalizedDiff;
686
+ let approved = !options.verify; // auto-approve when not verifying
687
+ if (options.verify) {
688
+ const verifySpinner = (0, ora_1.default)(` Verifying [${step.id}] ${step.goal}...`).start();
689
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
690
+ let verdict;
691
+ try {
692
+ verdict = await verifyOutput(step, currentAnswer, planner, plannerConfig, plannerContext, rootDir, isClaudeCodePlanner);
693
+ }
694
+ catch (err) {
695
+ verifySpinner.warn(` [${step.id}] Verifier error: ${err.message}`);
696
+ break;
697
+ }
698
+ if (verdict.approved) {
699
+ verifySpinner.succeed(` [${step.id}] ${chalk_1.default.green("Approved")} by planner`);
700
+ approved = true;
701
+ break;
702
+ }
703
+ if (attempt === maxRetries) {
704
+ verifySpinner.warn(` [${step.id}] ${chalk_1.default.yellow("Rejected")} after ${maxRetries + 1} attempt(s) — not applying`);
705
+ console.log(chalk_1.default.yellow(` Planner feedback: ${verdict.feedback}`));
706
+ break;
707
+ }
708
+ verifySpinner.text = ` [${step.id}] Needs revision (attempt ${attempt + 2}/${maxRetries + 1})...`;
709
+ console.log(chalk_1.default.yellow(`\n Planner feedback: ${verdict.feedback}`));
710
+ // Re-run executor with planner feedback injected
711
+ const retryResult = await executeStep(step, config, data, cacheStorage, rootDir, executorConfig, executor, topK, budget, forceKeyword, stepResults, contextCache, options.showContext ?? false, verdict.feedback);
712
+ const { diff: retryDiff, warnings: retryWarnings } = normalizeDiff(retryResult.answer);
713
+ if (retryWarnings.length > 0) {
714
+ for (const w of retryWarnings)
715
+ console.log(chalk_1.default.yellow(` ⚠ ${w}`));
716
+ }
717
+ currentAnswer = retryDiff;
718
+ stepResults.set(retryResult.stepId, currentAnswer);
719
+ const indented = currentAnswer.replace(/\n/g, "\n ");
720
+ console.log(chalk_1.default.gray(`\n Revised output:\n ${indented}\n`));
721
+ }
722
+ }
723
+ // Apply diff to filesystem
724
+ if (options.apply) {
725
+ if (approved) {
726
+ const applySpinner = (0, ora_1.default)(` Applying diff for step [${step.id}]...`).start();
727
+ const { success, output } = await applyDiff(currentAnswer, rootDir);
728
+ if (success) {
729
+ applySpinner.succeed(` [${step.id}] Diff applied`);
730
+ if (output)
731
+ console.log(chalk_1.default.gray(` ${output}`));
732
+ }
733
+ else {
734
+ applySpinner.fail(` [${step.id}] Diff apply failed`);
735
+ console.log(chalk_1.default.red(` ${output}`));
736
+ }
737
+ }
738
+ else {
739
+ console.log(chalk_1.default.yellow(` [${step.id}] Diff NOT applied (verification rejected).`));
740
+ }
741
+ }
742
+ else if (approved) {
743
+ console.log(chalk_1.default.gray(` Tip: add --apply to automatically patch the filesystem.`));
744
+ }
745
+ }
746
+ }
747
+ // ── Adaptive re-planning: handle INSUFFICIENT steps ────────────────────
748
+ const insufficientSteps = waveResults.filter((r) => r.insufficient);
749
+ if (insufficientSteps.length > 0) {
750
+ const replanSpinner = (0, ora_1.default)(` Re-planning for ${insufficientSteps.length} insufficient step(s)...`).start();
751
+ const failedSummary = insufficientSteps
752
+ .map((r) => {
753
+ const step = wave.find((s) => s.id === r.stepId);
754
+ return `Step ${r.stepId} (${step.goal}):\n${r.answer}`;
755
+ })
756
+ .join("\n\n");
757
+ try {
758
+ const replanResponse = await planner.chat([
759
+ {
760
+ role: "system",
761
+ content: isClaudeCodePlanner
762
+ ? REPLAN_SYSTEM_PROMPT
763
+ : `${REPLAN_SYSTEM_PROMPT}\n\n${plannerContext}`,
764
+ },
765
+ {
766
+ role: "user",
767
+ content: `The following steps reported insufficient context:\n\n${failedSummary}\n\n` +
768
+ `Next available step ID: ${nextId}. ` +
769
+ `Provide remediation steps to gather the missing information.`,
770
+ },
771
+ ], { model: plannerConfig.model, temperature: 0.2, projectRoot: rootDir });
772
+ const raw = replanResponse.content.trim();
773
+ const jsonStr = raw.startsWith("```")
774
+ ? raw.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "")
775
+ : raw;
776
+ const replan = JSON.parse(jsonStr);
777
+ if (Array.isArray(replan.steps) && replan.steps.length > 0) {
778
+ // Reassign IDs to avoid collisions
779
+ const remediationSteps = replan.steps.map((s, i) => ({
780
+ ...s,
781
+ id: nextId + i,
782
+ }));
783
+ nextId += remediationSteps.length;
784
+ replanSpinner.succeed(` Re-plan: ${remediationSteps.length} remediation step(s) added`);
785
+ // Execute remediation steps immediately (as a new wave)
786
+ const remediationWaves = buildExecutionWaves(remediationSteps);
787
+ for (const remWave of remediationWaves) {
788
+ const remResults = await Promise.all(remWave.map((step) => executeStep(step, config, data, cacheStorage, rootDir, executorConfig, executor, topK, budget, forceKeyword, stepResults, contextCache, options.showContext ?? false)));
789
+ for (const r of remResults) {
790
+ stepResults.set(r.stepId, r.answer);
791
+ const step = remWave.find((s) => s.id === r.stepId);
792
+ console.log(chalk_1.default.bold.yellow(`\n [${step.id}ᴿ] ${step.goal}`));
793
+ console.log(` ${r.answer.replace(/\n/g, "\n ")}\n`);
794
+ }
795
+ }
796
+ }
797
+ else {
798
+ replanSpinner.warn(" Re-plan returned no steps.");
799
+ }
800
+ }
801
+ catch {
802
+ replanSpinner.warn(" Re-planning failed — continuing.");
803
+ }
804
+ }
805
+ // ── Persist session state after every wave ─────────────────────────────
806
+ session.stepResults = Object.fromEntries(Array.from(stepResults.entries()).map(([k, v]) => [String(k), v]));
807
+ session.completedStepIds = Array.from(stepResults.keys());
808
+ session.nextId = nextId;
809
+ session.updatedAt = new Date().toISOString();
810
+ sessionStorage.save(session);
811
+ }
812
+ // Mark session complete before synthesis
813
+ session.status = "completed";
814
+ session.updatedAt = new Date().toISOString();
815
+ sessionStorage.save(session);
816
+ // ── Phase 5: optional synthesis ────────────────────────────────────────────
817
+ let synthesisText = "";
818
+ if (options.synthesize && stepResults.size > 0) {
819
+ const synthSpinner = (0, ora_1.default)("Synthesizing final answer...").start();
820
+ const stepSummaries = plan.steps
821
+ .map((s) => {
822
+ const result = stepResults.get(s.id);
823
+ return result ? `### Step ${s.id}: ${s.goal}\n${result}` : null;
824
+ })
825
+ .filter(Boolean)
826
+ .join("\n\n");
827
+ try {
828
+ const response = await planner.chat([
829
+ { role: "system", content: SYNTHESIZER_SYSTEM_PROMPT },
830
+ {
831
+ role: "user",
832
+ content: `Original task: ${task}\n\n## Step results\n\n${stepSummaries}\n\nSynthesize a final, complete answer.`,
833
+ },
834
+ ], { model: plannerConfig.model, temperature: 0.3 });
835
+ synthesisText = response.content;
836
+ synthSpinner.succeed("Synthesis complete");
837
+ console.log(chalk_1.default.bold.cyan("\n── Final answer ────────────────────────────────\n"));
838
+ console.log(synthesisText);
839
+ console.log(chalk_1.default.gray("\n────────────────────────────────────────────────"));
840
+ }
841
+ catch (err) {
842
+ synthSpinner.fail(`Synthesis error: ${err.message}`);
843
+ }
844
+ }
845
+ // ── Phase 6: offer to save synthesis as knowledge entry ───────────────────
846
+ if (synthesisText && options.synthesize) {
847
+ const save = await askConfirm(chalk_1.default.bold("\nSave synthesis as a knowledge entry? [y/N] "));
848
+ if (save) {
849
+ const knowledge = new knowledge_1.KnowledgeStorage(rootDir);
850
+ const entries = knowledge.load();
851
+ entries.push({
852
+ id: (0, crypto_1.randomUUID)(),
853
+ category: "pattern",
854
+ title: plan.goal,
855
+ body: synthesisText,
856
+ createdAt: new Date().toISOString(),
857
+ createdBy: `llm:${plannerConfig.model}`,
858
+ confidence: "medium",
859
+ });
860
+ knowledge.save(entries);
861
+ console.log(chalk_1.default.green(" Saved."));
862
+ }
863
+ }
864
+ console.log(chalk_1.default.gray(`\nPlan complete: ${stepResults.size} step(s) executed` +
865
+ (options.synthesize ? " + synthesis" : "")));
866
+ }
867
+ //# sourceMappingURL=plan.js.map