openreport 0.1.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/openreport.ts +6 -0
- package/package.json +61 -0
- package/src/agents/api-documentation.ts +66 -0
- package/src/agents/architecture-analyst.ts +46 -0
- package/src/agents/code-quality-reviewer.ts +59 -0
- package/src/agents/dependency-analyzer.ts +51 -0
- package/src/agents/onboarding-guide.ts +59 -0
- package/src/agents/orchestrator.ts +41 -0
- package/src/agents/performance-analyzer.ts +57 -0
- package/src/agents/registry.ts +50 -0
- package/src/agents/security-auditor.ts +61 -0
- package/src/agents/test-coverage-analyst.ts +58 -0
- package/src/agents/todo-generator.ts +50 -0
- package/src/app/App.tsx +151 -0
- package/src/app/theme.ts +54 -0
- package/src/cli.ts +145 -0
- package/src/commands/init.ts +81 -0
- package/src/commands/interactive.tsx +29 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/run.ts +168 -0
- package/src/commands/view.tsx +52 -0
- package/src/components/generation/AgentStatusItem.tsx +125 -0
- package/src/components/generation/AgentStatusList.tsx +70 -0
- package/src/components/generation/ProgressSummary.tsx +107 -0
- package/src/components/generation/StreamingOutput.tsx +154 -0
- package/src/components/layout/Container.tsx +24 -0
- package/src/components/layout/Footer.tsx +52 -0
- package/src/components/layout/Header.tsx +50 -0
- package/src/components/report/MarkdownRenderer.tsx +50 -0
- package/src/components/report/ReportCard.tsx +31 -0
- package/src/components/report/ScrollableView.tsx +164 -0
- package/src/config/cli-detection.ts +130 -0
- package/src/config/cli-model.ts +397 -0
- package/src/config/cli-prompt-formatter.ts +129 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +168 -0
- package/src/config/ollama.ts +48 -0
- package/src/config/providers.ts +199 -0
- package/src/config/resolve-provider.ts +62 -0
- package/src/config/saver.ts +50 -0
- package/src/config/schema.ts +51 -0
- package/src/errors.ts +34 -0
- package/src/hooks/useReportGeneration.ts +199 -0
- package/src/hooks/useTerminalSize.ts +35 -0
- package/src/ingestion/context-selector.ts +247 -0
- package/src/ingestion/file-tree.ts +227 -0
- package/src/ingestion/token-budget.ts +52 -0
- package/src/pipeline/agent-runner.ts +360 -0
- package/src/pipeline/combiner.ts +199 -0
- package/src/pipeline/context.ts +108 -0
- package/src/pipeline/extraction.ts +153 -0
- package/src/pipeline/progress.ts +192 -0
- package/src/pipeline/runner.ts +526 -0
- package/src/report/html-renderer.ts +294 -0
- package/src/report/html-script.ts +123 -0
- package/src/report/html-styles.ts +1127 -0
- package/src/report/md-to-html.ts +153 -0
- package/src/report/open-browser.ts +22 -0
- package/src/schemas/findings.ts +48 -0
- package/src/schemas/report.ts +64 -0
- package/src/screens/ConfigScreen.tsx +271 -0
- package/src/screens/GenerationScreen.tsx +278 -0
- package/src/screens/HistoryScreen.tsx +108 -0
- package/src/screens/HomeScreen.tsx +143 -0
- package/src/screens/ViewerScreen.tsx +82 -0
- package/src/storage/metadata.ts +69 -0
- package/src/storage/report-store.ts +128 -0
- package/src/tools/get-file-tree.ts +157 -0
- package/src/tools/get-git-info.ts +123 -0
- package/src/tools/glob.ts +48 -0
- package/src/tools/grep.ts +149 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-directory.ts +57 -0
- package/src/tools/read-file.ts +52 -0
- package/src/tools/read-package-json.ts +48 -0
- package/src/tools/run-command.ts +154 -0
- package/src/tools/shared-ignore.ts +58 -0
- package/src/types/index.ts +127 -0
- package/src/types/marked-terminal.d.ts +17 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/file-utils.ts +77 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/grade-colors.ts +43 -0
- package/src/utils/project-detector.ts +296 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
import type { LanguageModelV1 } from "ai";
|
|
3
|
+
import { ProgressTracker } from "./progress.js";
|
|
4
|
+
import { combineReport, countFindings, type CombineOptions } from "./combiner.js";
|
|
5
|
+
import { getAgentById, getAgentsForReportType } from "../agents/registry.js";
|
|
6
|
+
import { buildFileTree, fileTreeToString, flattenFileTree } from "../ingestion/file-tree.js";
|
|
7
|
+
import { getRelevantFilePaths } from "../ingestion/context-selector.js";
|
|
8
|
+
import { detectProject } from "../utils/project-detector.js";
|
|
9
|
+
import { PipelineError } from "../errors.js";
|
|
10
|
+
import { debugLog } from "../utils/debug.js";
|
|
11
|
+
import { getErrorMessage } from "../utils/format.js";
|
|
12
|
+
import type {
|
|
13
|
+
AgentId,
|
|
14
|
+
SubReport,
|
|
15
|
+
FullReport,
|
|
16
|
+
FileTreeNode,
|
|
17
|
+
ProjectClassification,
|
|
18
|
+
} from "../types/index.js";
|
|
19
|
+
import type { OpenReportConfig } from "../config/schema.js";
|
|
20
|
+
import { preReadAllFiles, buildContextInfo, type SharedContext } from "./context.js";
|
|
21
|
+
import { createBaseTools, createExtendedTools } from "../tools/index.js";
|
|
22
|
+
import {
|
|
23
|
+
runOrchestratorAgent,
|
|
24
|
+
runAgentWithTools,
|
|
25
|
+
runAgentWithCli,
|
|
26
|
+
withRetry,
|
|
27
|
+
} from "./agent-runner.js";
|
|
28
|
+
import { extractSubReport } from "./extraction.js";
|
|
29
|
+
import { isCliModel } from "../config/cli-model.js";
|
|
30
|
+
|
|
31
|
+
export interface RunPipelineOptions {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
reportType: string;
|
|
34
|
+
model: LanguageModelV1;
|
|
35
|
+
modelId: string;
|
|
36
|
+
config: OpenReportConfig;
|
|
37
|
+
progress: ProgressTracker;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
generateTodoList?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Yield to event loop so React/Ink can render between phases
|
|
43
|
+
const tick = () => new Promise<void>((r) => setTimeout(r, 0));
|
|
44
|
+
|
|
45
|
+
// ── Phase 1: Scan project ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface ScanResult {
|
|
48
|
+
fileTree: FileTreeNode[];
|
|
49
|
+
fileTreeString: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function scanProject(
|
|
53
|
+
projectRoot: string,
|
|
54
|
+
config: OpenReportConfig,
|
|
55
|
+
progress: ProgressTracker
|
|
56
|
+
): Promise<ScanResult> {
|
|
57
|
+
progress.setPhase("scanning");
|
|
58
|
+
await tick();
|
|
59
|
+
|
|
60
|
+
const fileTree = await buildFileTree(projectRoot, {
|
|
61
|
+
maxDepth: config.scan.maxDepth,
|
|
62
|
+
extraIgnore: config.scan.exclude,
|
|
63
|
+
maxFileSize: config.scan.maxFileSize,
|
|
64
|
+
});
|
|
65
|
+
const fileTreeString = fileTreeToString(fileTree);
|
|
66
|
+
|
|
67
|
+
return { fileTree, fileTreeString };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Phase 2: Classify project ─────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
interface ClassifyResult {
|
|
73
|
+
classification: ProjectClassification;
|
|
74
|
+
projectName: string;
|
|
75
|
+
applicableAgents: AgentId[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function classifyProject(
|
|
79
|
+
projectRoot: string,
|
|
80
|
+
reportType: string,
|
|
81
|
+
progress: ProgressTracker
|
|
82
|
+
): Promise<ClassifyResult> {
|
|
83
|
+
progress.setPhase("classifying");
|
|
84
|
+
await tick();
|
|
85
|
+
|
|
86
|
+
const classification = await detectProject(projectRoot);
|
|
87
|
+
const projectName = classification.framework
|
|
88
|
+
? `${classification.framework} ${classification.projectType}`
|
|
89
|
+
: classification.language;
|
|
90
|
+
|
|
91
|
+
const agentIds = getAgentsForReportType(reportType);
|
|
92
|
+
const applicableAgents = agentIds.filter((id) => {
|
|
93
|
+
const def = getAgentById(id);
|
|
94
|
+
return def ? def.relevantFor(classification) : false;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { classification, projectName, applicableAgents };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Phase 3: Pre-read project files ───────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function preReadFiles(
|
|
103
|
+
projectRoot: string,
|
|
104
|
+
applicableAgents: AgentId[],
|
|
105
|
+
fileTree: FileTreeNode[],
|
|
106
|
+
fileTreeString: string,
|
|
107
|
+
classification: ProjectClassification,
|
|
108
|
+
config: OpenReportConfig,
|
|
109
|
+
progress: ProgressTracker
|
|
110
|
+
): Promise<SharedContext> {
|
|
111
|
+
progress.setPhase("pre-reading");
|
|
112
|
+
await tick();
|
|
113
|
+
|
|
114
|
+
const allRelevantPaths = new Set<string>();
|
|
115
|
+
const agentFilePaths = new Map<string, string[]>();
|
|
116
|
+
for (const agentId of applicableAgents) {
|
|
117
|
+
const paths = getRelevantFilePaths(agentId, fileTree);
|
|
118
|
+
agentFilePaths.set(agentId, paths);
|
|
119
|
+
for (const p of paths) {
|
|
120
|
+
allRelevantPaths.add(p);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
progress.setPhaseDetail(`${allRelevantPaths.size} unique files for ${applicableAgents.length} agents`);
|
|
125
|
+
const fileSizes = new Map<string, number>();
|
|
126
|
+
for (const node of flattenFileTree(fileTree)) {
|
|
127
|
+
if (node.size !== undefined) {
|
|
128
|
+
fileSizes.set(node.path, node.size);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const fileContents = await preReadAllFiles(projectRoot, [...allRelevantPaths], 30, 8000, fileSizes);
|
|
132
|
+
progress.setPhaseDetail(`${fileContents.size} files loaded, shared across ${applicableAgents.length} agents`);
|
|
133
|
+
await tick();
|
|
134
|
+
|
|
135
|
+
// Create tool instances once, shared across all agents
|
|
136
|
+
const baseTools = createBaseTools(projectRoot);
|
|
137
|
+
const extendedTools = createExtendedTools(projectRoot);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
classification,
|
|
141
|
+
fileTreeString,
|
|
142
|
+
fileTree,
|
|
143
|
+
fileContents,
|
|
144
|
+
contextInfo: buildContextInfo(classification, fileTreeString, [...allRelevantPaths]),
|
|
145
|
+
orchestratorAnalysis: "",
|
|
146
|
+
baseTools,
|
|
147
|
+
extendedTools,
|
|
148
|
+
agentFilePaths,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Phase 4: Orchestrator global analysis ─────────────────────────────
|
|
153
|
+
|
|
154
|
+
async function runOrchestrator(
|
|
155
|
+
model: LanguageModelV1,
|
|
156
|
+
sharedContext: SharedContext,
|
|
157
|
+
config: OpenReportConfig,
|
|
158
|
+
progress: ProgressTracker,
|
|
159
|
+
signal?: AbortSignal
|
|
160
|
+
): Promise<SharedContext> {
|
|
161
|
+
progress.setPhase("analyzing");
|
|
162
|
+
await tick();
|
|
163
|
+
progress.setPhaseDetail("Producing project overview...");
|
|
164
|
+
|
|
165
|
+
const orchestratorAnalysis = await runOrchestratorAgent(
|
|
166
|
+
model,
|
|
167
|
+
sharedContext,
|
|
168
|
+
config,
|
|
169
|
+
progress,
|
|
170
|
+
signal
|
|
171
|
+
);
|
|
172
|
+
progress.setPhaseDetail(`Done (${orchestratorAnalysis.length} chars)`);
|
|
173
|
+
await tick();
|
|
174
|
+
|
|
175
|
+
return { ...sharedContext, orchestratorAnalysis };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Phase 5: Run agents ───────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
async function executeAgents(
|
|
181
|
+
applicableAgents: AgentId[],
|
|
182
|
+
model: LanguageModelV1,
|
|
183
|
+
projectRoot: string,
|
|
184
|
+
sharedContext: SharedContext,
|
|
185
|
+
config: OpenReportConfig,
|
|
186
|
+
progress: ProgressTracker,
|
|
187
|
+
signal?: AbortSignal
|
|
188
|
+
): Promise<SubReport[]> {
|
|
189
|
+
progress.setPhase("running-agents");
|
|
190
|
+
await tick();
|
|
191
|
+
|
|
192
|
+
// Register agents in progress tracker
|
|
193
|
+
for (const id of applicableAgents) {
|
|
194
|
+
const def = getAgentById(id);
|
|
195
|
+
if (def) {
|
|
196
|
+
progress.registerAgent(id, def.name);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await tick();
|
|
200
|
+
|
|
201
|
+
const useCli = isCliModel(model);
|
|
202
|
+
const maxConcurrency = config.agents.maxConcurrency;
|
|
203
|
+
|
|
204
|
+
// Choose the right runner function based on provider type
|
|
205
|
+
const runAgent = useCli ? runAgentWithCli : runAgentWithTools;
|
|
206
|
+
|
|
207
|
+
// Pool-based concurrency with indexed results for deterministic ordering
|
|
208
|
+
const results = new Map<string, SubReport>();
|
|
209
|
+
const executing = new Set<Promise<void>>();
|
|
210
|
+
for (const agentId of applicableAgents) {
|
|
211
|
+
if (signal?.aborted) break;
|
|
212
|
+
|
|
213
|
+
const p = runAgent(
|
|
214
|
+
agentId,
|
|
215
|
+
model,
|
|
216
|
+
projectRoot,
|
|
217
|
+
sharedContext,
|
|
218
|
+
config,
|
|
219
|
+
progress,
|
|
220
|
+
signal
|
|
221
|
+
).then((result) => {
|
|
222
|
+
if (result) {
|
|
223
|
+
results.set(agentId, result);
|
|
224
|
+
}
|
|
225
|
+
}).catch((err) => {
|
|
226
|
+
debugLog("pipeline:agentPool", err);
|
|
227
|
+
}).finally(() => {
|
|
228
|
+
executing.delete(p);
|
|
229
|
+
});
|
|
230
|
+
executing.add(p);
|
|
231
|
+
|
|
232
|
+
if (executing.size >= maxConcurrency) {
|
|
233
|
+
await Promise.race(executing);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
await Promise.all(executing);
|
|
237
|
+
|
|
238
|
+
// Return results in the original agent order
|
|
239
|
+
const subReports: SubReport[] = [];
|
|
240
|
+
for (const agentId of applicableAgents) {
|
|
241
|
+
const report = results.get(agentId);
|
|
242
|
+
if (report) {
|
|
243
|
+
subReports.push(report);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return subReports;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Phase 5.5: Generate todo list from findings ───────────────────────
|
|
250
|
+
|
|
251
|
+
async function executeTodoAgent(
|
|
252
|
+
model: LanguageModelV1,
|
|
253
|
+
subReports: SubReport[],
|
|
254
|
+
config: OpenReportConfig,
|
|
255
|
+
progress: ProgressTracker,
|
|
256
|
+
signal?: AbortSignal
|
|
257
|
+
): Promise<SubReport | null> {
|
|
258
|
+
const allFindings = subReports.flatMap((r) => r.findings);
|
|
259
|
+
if (allFindings.length === 0) {
|
|
260
|
+
debugLog("pipeline:todo", "No findings to generate todo list from, skipping");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
progress.setPhase("generating-todo");
|
|
265
|
+
await tick();
|
|
266
|
+
|
|
267
|
+
const agentDef = getAgentById("todo-generator");
|
|
268
|
+
if (!agentDef) {
|
|
269
|
+
debugLog("pipeline:todo", "todo-generator agent not found in registry");
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
progress.registerAgent("todo-generator", agentDef.name);
|
|
274
|
+
progress.updateAgentStatus("todo-generator", "running", "Generating todo list...");
|
|
275
|
+
|
|
276
|
+
const findingsText = allFindings
|
|
277
|
+
.map(
|
|
278
|
+
(f, i) =>
|
|
279
|
+
`${i + 1}. [${f.severity.toUpperCase()}] (${f.category}) ${f.title}: ${f.description}${f.files?.length ? ` | Files: ${f.files.join(", ")}` : ""}${f.recommendation ? ` | Recommendation: ${f.recommendation}` : ""} | Effort: ${f.effort}`
|
|
280
|
+
)
|
|
281
|
+
.join("\n");
|
|
282
|
+
|
|
283
|
+
const prompt = `${agentDef.systemPrompt}
|
|
284
|
+
|
|
285
|
+
## Findings from Analysis (${allFindings.length} total)
|
|
286
|
+
|
|
287
|
+
${findingsText}
|
|
288
|
+
|
|
289
|
+
## Instructions
|
|
290
|
+
Generate the prioritized todo list based on these findings. Wrap your entire response in <report> tags with valid JSON matching this structure:
|
|
291
|
+
{
|
|
292
|
+
"agentId": "todo-generator",
|
|
293
|
+
"title": "Action Plan & Todo List",
|
|
294
|
+
"summary": "<brief summary of the action plan>",
|
|
295
|
+
"sections": [
|
|
296
|
+
{ "heading": "<section title>", "content": "<markdown content>" }
|
|
297
|
+
],
|
|
298
|
+
"findings": []
|
|
299
|
+
}`;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const result = await withRetry(async () => {
|
|
303
|
+
const response = await streamText({
|
|
304
|
+
model,
|
|
305
|
+
prompt,
|
|
306
|
+
maxSteps: 1,
|
|
307
|
+
maxTokens: config.agents.maxTokens,
|
|
308
|
+
temperature: config.agents.temperature,
|
|
309
|
+
abortSignal: signal,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const chunks: string[] = [];
|
|
313
|
+
for await (const chunk of response.textStream) {
|
|
314
|
+
chunks.push(chunk);
|
|
315
|
+
progress.addStreamText(chunk, "todo-generator");
|
|
316
|
+
}
|
|
317
|
+
return chunks.join("");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const report = extractSubReport("todo-generator", agentDef.name, result, 0);
|
|
321
|
+
progress.updateAgentStatus("todo-generator", "completed", "Done");
|
|
322
|
+
return report;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
debugLog("pipeline:todo", err);
|
|
325
|
+
progress.updateAgentStatus("todo-generator", "failed", getErrorMessage(err));
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Phase 6: Generate executive summary ───────────────────────────────
|
|
331
|
+
|
|
332
|
+
async function generateExecutiveSummary(
|
|
333
|
+
model: LanguageModelV1,
|
|
334
|
+
subReports: SubReport[],
|
|
335
|
+
projectName: string,
|
|
336
|
+
classification: ProjectClassification,
|
|
337
|
+
progress: ProgressTracker,
|
|
338
|
+
signal?: AbortSignal
|
|
339
|
+
): Promise<string> {
|
|
340
|
+
await tick();
|
|
341
|
+
progress.setPhase("combining");
|
|
342
|
+
|
|
343
|
+
if (subReports.length === 0) {
|
|
344
|
+
return "Report generation completed.";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const allFindings = subReports.flatMap((r) => r.findings);
|
|
348
|
+
const findingSummary = countFindings(allFindings);
|
|
349
|
+
progress.setPhaseDetail(`${subReports.length} sub-reports, ${allFindings.length} findings`);
|
|
350
|
+
|
|
351
|
+
const summaryPrompt = `You are a senior technical lead writing an executive summary for a code analysis report.
|
|
352
|
+
|
|
353
|
+
## Project: ${projectName}
|
|
354
|
+
## Classification
|
|
355
|
+
${JSON.stringify(classification, null, 2)}
|
|
356
|
+
|
|
357
|
+
## Sub-Report Summaries
|
|
358
|
+
${subReports.map((r) => `### ${r.title}\n${r.summary}\n\nKey sections: ${r.sections.map((s) => s.heading).join(", ")}`).join("\n\n")}
|
|
359
|
+
|
|
360
|
+
## All Findings (${allFindings.length} total)
|
|
361
|
+
${allFindings.map((f) => `- [${f.severity.toUpperCase()}] ${f.title}: ${f.description} (effort: ${f.effort})`).join("\n")}
|
|
362
|
+
|
|
363
|
+
## Finding Breakdown
|
|
364
|
+
- Critical: ${findingSummary.critical}
|
|
365
|
+
- Warning: ${findingSummary.warning}
|
|
366
|
+
- Info: ${findingSummary.info}
|
|
367
|
+
- Suggestion: ${findingSummary.suggestion}
|
|
368
|
+
|
|
369
|
+
Write a comprehensive executive summary in markdown with these sections:
|
|
370
|
+
1. **Project Health Overview** - Overall assessment, grade justification
|
|
371
|
+
2. **Key Strengths** - What the project does well
|
|
372
|
+
3. **Critical Issues** - Top priority items requiring immediate attention
|
|
373
|
+
4. **Cross-Cutting Concerns** - Patterns/issues found by multiple analysis angles
|
|
374
|
+
5. **Top Recommendations** - Prioritized action items by impact/effort ratio
|
|
375
|
+
|
|
376
|
+
Be specific, reference actual findings, and provide actionable insights. Write 4-6 paragraphs.`;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const result = await streamText({
|
|
380
|
+
model,
|
|
381
|
+
prompt: summaryPrompt,
|
|
382
|
+
maxSteps: 1,
|
|
383
|
+
maxTokens: 2000,
|
|
384
|
+
temperature: 0.3,
|
|
385
|
+
abortSignal: signal,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const chunks: string[] = [];
|
|
389
|
+
for await (const chunk of result.textStream) {
|
|
390
|
+
chunks.push(chunk);
|
|
391
|
+
}
|
|
392
|
+
return chunks.join("");
|
|
393
|
+
} catch (e) {
|
|
394
|
+
debugLog("pipeline:executiveSummary", e);
|
|
395
|
+
return "Executive summary generation failed. Please review individual sections below.";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Phase 7: Combine and save ─────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function combineAndFinalize(
|
|
402
|
+
projectName: string,
|
|
403
|
+
projectRoot: string,
|
|
404
|
+
reportType: string,
|
|
405
|
+
modelId: string,
|
|
406
|
+
executiveSummary: string,
|
|
407
|
+
subReports: SubReport[],
|
|
408
|
+
progress: ProgressTracker
|
|
409
|
+
): FullReport {
|
|
410
|
+
progress.setPhase("saving");
|
|
411
|
+
|
|
412
|
+
const agentStatuses = progress.getProgress().agents;
|
|
413
|
+
const totalTokens = progress.getProgress().totalTokens;
|
|
414
|
+
|
|
415
|
+
const fullReport = combineReport({
|
|
416
|
+
projectName,
|
|
417
|
+
projectPath: projectRoot,
|
|
418
|
+
reportType,
|
|
419
|
+
model: modelId,
|
|
420
|
+
executiveSummary,
|
|
421
|
+
subReports,
|
|
422
|
+
agentStatuses,
|
|
423
|
+
startedAt: progress.getProgress().startedAt,
|
|
424
|
+
totalTokens,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
progress.setPhase("done");
|
|
428
|
+
progress.emit("complete", progress.getProgress());
|
|
429
|
+
|
|
430
|
+
return fullReport;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Main pipeline ───────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
export async function runPipeline(
|
|
436
|
+
options: RunPipelineOptions
|
|
437
|
+
): Promise<FullReport> {
|
|
438
|
+
const { projectRoot, reportType, model, modelId, config, progress, signal, generateTodoList } =
|
|
439
|
+
options;
|
|
440
|
+
|
|
441
|
+
const PIPELINE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
442
|
+
const timeoutController = new AbortController();
|
|
443
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), PIPELINE_TIMEOUT_MS);
|
|
444
|
+
|
|
445
|
+
const combinedSignal = signal
|
|
446
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
447
|
+
: timeoutController.signal;
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
// Phase 1 & 2: Scan and classify project in parallel
|
|
451
|
+
const [scanResult, classifyResult] = await Promise.all([
|
|
452
|
+
scanProject(projectRoot, config, progress),
|
|
453
|
+
classifyProject(projectRoot, reportType, progress),
|
|
454
|
+
]);
|
|
455
|
+
const { fileTree, fileTreeString } = scanResult;
|
|
456
|
+
const { classification, projectName, applicableAgents } = classifyResult;
|
|
457
|
+
|
|
458
|
+
// Phase 3: Pre-read project files
|
|
459
|
+
const sharedContext = await preReadFiles(
|
|
460
|
+
projectRoot,
|
|
461
|
+
applicableAgents,
|
|
462
|
+
fileTree,
|
|
463
|
+
fileTreeString,
|
|
464
|
+
classification,
|
|
465
|
+
config,
|
|
466
|
+
progress
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Phase 4: Orchestrator global analysis
|
|
470
|
+
const fullContext = await runOrchestrator(model, sharedContext, config, progress, combinedSignal);
|
|
471
|
+
|
|
472
|
+
// Phase 5: Run agents
|
|
473
|
+
const subReports = await executeAgents(
|
|
474
|
+
applicableAgents,
|
|
475
|
+
model,
|
|
476
|
+
projectRoot,
|
|
477
|
+
fullContext,
|
|
478
|
+
config,
|
|
479
|
+
progress,
|
|
480
|
+
combinedSignal
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
if (subReports.length === 0 && applicableAgents.length > 0) {
|
|
484
|
+
throw new PipelineError(
|
|
485
|
+
`All ${applicableAgents.length} agents failed to produce results. Check API keys and provider configuration.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Phase 5.5: Generate todo list (if enabled)
|
|
490
|
+
if (generateTodoList) {
|
|
491
|
+
const todoReport = await executeTodoAgent(
|
|
492
|
+
model,
|
|
493
|
+
subReports,
|
|
494
|
+
config,
|
|
495
|
+
progress,
|
|
496
|
+
combinedSignal
|
|
497
|
+
);
|
|
498
|
+
if (todoReport) {
|
|
499
|
+
subReports.push(todoReport);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Phase 6: Generate executive summary
|
|
504
|
+
const executiveSummary = await generateExecutiveSummary(
|
|
505
|
+
model,
|
|
506
|
+
subReports,
|
|
507
|
+
projectName,
|
|
508
|
+
classification,
|
|
509
|
+
progress,
|
|
510
|
+
combinedSignal
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
// Phase 7: Combine and finalize
|
|
514
|
+
return combineAndFinalize(
|
|
515
|
+
projectName,
|
|
516
|
+
projectRoot,
|
|
517
|
+
reportType,
|
|
518
|
+
modelId,
|
|
519
|
+
executiveSummary,
|
|
520
|
+
subReports,
|
|
521
|
+
progress
|
|
522
|
+
);
|
|
523
|
+
} finally {
|
|
524
|
+
clearTimeout(timeoutId);
|
|
525
|
+
}
|
|
526
|
+
}
|