scai 0.1.172 → 0.1.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/MainAgent.js +823 -57
- package/dist/agents/evidenceVerifierStep.js +32 -16
- package/dist/agents/fileCheckStep.js +19 -8
- package/dist/agents/readinessGateStep.js +8 -8
- package/dist/agents/reasonNextStep.js +16 -1
- package/dist/agents/reasonNextTaskStep.js +118 -4
- package/dist/agents/researchPlanGenStep.js +123 -0
- package/dist/agents/resolveExecutionModeStep.js +27 -7
- package/dist/agents/routingDecisionStep.js +5 -0
- package/dist/agents/understandIntentStep.js +25 -0
- package/dist/commands/AskCmd.js +2 -0
- package/dist/db/fileIndex.js +116 -1
- package/dist/fileRules/wellKnownRepoFiles.js +8 -0
- package/dist/index.js +4 -33
- package/dist/pipeline/modules/finalAnswerModule.js +100 -3
- package/dist/pipeline/modules/semanticAnalysisModule.js +1 -64
- package/dist/pipeline/registry/moduleRegistry.js +1 -3
- package/dist/testing/testCommands.js +310 -0
- package/dist/utils/promptLogHelper.js +15 -4
- package/package.json +1 -1
- package/dist/pipeline/modules/explainModule.js +0 -169
package/dist/agents/MainAgent.js
CHANGED
|
@@ -16,6 +16,7 @@ import { validateChangesStep } from './validateChangesStep.js';
|
|
|
16
16
|
import { reasonNextTaskStep } from './reasonNextTaskStep.js';
|
|
17
17
|
import { collaboratorStep } from './collaboratorStep.js';
|
|
18
18
|
import { integrateFeedbackStep } from './integrateFeedbackStep.js';
|
|
19
|
+
import { researchPlanGenStep } from "./researchPlanGenStep.js";
|
|
19
20
|
import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
|
|
20
21
|
import { iterationFileSelector } from "./iterationFileSelector.js";
|
|
21
22
|
import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
|
|
@@ -26,8 +27,10 @@ import { NUM_TOPFILES, RELATED_FILES_LIMIT } from "../constants.js";
|
|
|
26
27
|
import { structuralPreloadStep } from "./structuralPreloadStep.js";
|
|
27
28
|
import { extractFileReferences } from "../utils/extractFileReferences.js";
|
|
28
29
|
import { PREFILTER_STOP_WORDS } from "../fileRules/stopWords.js";
|
|
30
|
+
import { MAX_WELL_KNOWN_REPO_FILES, WELL_KNOWN_REPO_FILE_BASENAMES } from "../fileRules/wellKnownRepoFiles.js";
|
|
29
31
|
import chalk from "chalk";
|
|
30
32
|
import path from "path";
|
|
33
|
+
import fs from "fs";
|
|
31
34
|
/* ───────────────────────── registry ───────────────────────── */
|
|
32
35
|
const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
|
|
33
36
|
function resolveModuleForAction(action) {
|
|
@@ -66,9 +69,14 @@ export class MainAgent {
|
|
|
66
69
|
this.runCount = 0;
|
|
67
70
|
await this.runBoot();
|
|
68
71
|
await this.runScope();
|
|
69
|
-
await this.
|
|
70
|
-
await this.
|
|
71
|
-
await this.
|
|
72
|
+
await this.runSearch();
|
|
73
|
+
await this.runVerify();
|
|
74
|
+
await this.runResearch();
|
|
75
|
+
const canProceedToExecution = this.isResearchGateSatisfied();
|
|
76
|
+
if (canProceedToExecution) {
|
|
77
|
+
await this.runPlan();
|
|
78
|
+
await this.runWorkLoop();
|
|
79
|
+
}
|
|
72
80
|
await this.runFinalize();
|
|
73
81
|
}
|
|
74
82
|
finally {
|
|
@@ -100,12 +108,16 @@ export class MainAgent {
|
|
|
100
108
|
await routingDecisionStep.run(this.context);
|
|
101
109
|
const routing = this.context.analysis?.routingDecision;
|
|
102
110
|
if (routing) {
|
|
103
|
-
this.logLine("TASK", "Routing decision", undefined, `${routing.decision} | search=${routing.allowSearch} | transform=${routing.allowTransform} | scopeLocked=${routing.scopeLocked}`);
|
|
111
|
+
this.logLine("TASK", "Routing decision", undefined, `${routing.decision} | search=${routing.allowSearch} | research=${routing.allowResearch} | transform=${routing.allowTransform} | scopeLocked=${routing.scopeLocked}`);
|
|
104
112
|
}
|
|
105
113
|
this.logLine("TASK", "Scope classification complete");
|
|
106
114
|
}
|
|
107
|
-
/* ─────────────
|
|
108
|
-
|
|
115
|
+
/* ───────────── search ───────────── */
|
|
116
|
+
/**
|
|
117
|
+
* Seeds initial candidate files using semantic retrieval + deterministic prefilter.
|
|
118
|
+
* Example: query mentions "MainAgent" -> relatedFiles are narrowed before grounding.
|
|
119
|
+
*/
|
|
120
|
+
async runSearch() {
|
|
109
121
|
const { rawUserQuery, retrievalQuery } = this.resolveInitialRetrievalQueries();
|
|
110
122
|
const t = this.startTimer();
|
|
111
123
|
try {
|
|
@@ -114,20 +126,29 @@ export class MainAgent {
|
|
|
114
126
|
const seededContext = await buildLightContext(promptArgs);
|
|
115
127
|
const mergedRelatedCount = this.mergeSeededInitialContext(rawUserQuery, seededContext);
|
|
116
128
|
const prefilter = this.applyDeterministicPreGroundingPrefilter(retrievalQuery);
|
|
117
|
-
|
|
129
|
+
const repoDefaults = this.injectWellKnownRepoFiles(prefilter.after);
|
|
130
|
+
this.logLine("ANALYSIS", "initialRetrieval", t(), `${results.length} result(s), ${mergedRelatedCount} candidate file(s), prefilter ${prefilter.before} -> ${prefilter.after}, defaults +${repoDefaults.added} (${repoDefaults.reason})`);
|
|
118
131
|
}
|
|
119
132
|
catch (err) {
|
|
120
133
|
this.logLine("ANALYSIS", "initialRetrieval", t(), `failed: ${String(err)}`);
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
|
-
/* ─────────────
|
|
124
|
-
|
|
136
|
+
/* ───────────── verify ───────────── */
|
|
137
|
+
/**
|
|
138
|
+
* Wave-based verify loop (evidence -> readiness -> optional info acquisition).
|
|
139
|
+
* Example: if readiness stays not-ready, run an info plan and try another wave.
|
|
140
|
+
*/
|
|
141
|
+
async runVerify() {
|
|
125
142
|
let ready = false;
|
|
126
143
|
const maxGroundingWaves = this.getGroundingWaveBudget();
|
|
127
144
|
let groundingWave = 0;
|
|
145
|
+
let stagnantWaves = 0;
|
|
146
|
+
const MAX_STAGNANT_WAVES = 2;
|
|
128
147
|
while (groundingWave < maxGroundingWaves) {
|
|
129
148
|
groundingWave++;
|
|
149
|
+
this.pruneMissingVerifyPaths();
|
|
130
150
|
this.logLine("ANALYSIS", "groundingWave", undefined, `wave ${groundingWave}/${maxGroundingWaves}`);
|
|
151
|
+
const beforeFocus = this.captureVerifyFocusSnapshot();
|
|
131
152
|
// ---------------- EVIDENCE PIPELINE ----------------
|
|
132
153
|
// -------- STRUCTURAL PRELOAD --------
|
|
133
154
|
const t0 = this.startTimer();
|
|
@@ -151,57 +172,207 @@ export class MainAgent {
|
|
|
151
172
|
break;
|
|
152
173
|
}
|
|
153
174
|
// ---------------- INFORMATION ACQUISITION ----------------
|
|
154
|
-
|
|
175
|
+
const canRouteSearchExpansion = this.canExecuteRoute("search-expand");
|
|
176
|
+
if (!canRouteSearchExpansion) {
|
|
177
|
+
this.logLine("PLAN", "infoPlanGen", undefined, "skipped (routing disallows search expansion)", { highlight: false });
|
|
178
|
+
}
|
|
179
|
+
else if (this.canExecutePhase("planning") &&
|
|
155
180
|
this.canExecuteScope("planning")) {
|
|
156
181
|
const t = this.startTimer();
|
|
157
182
|
await infoPlanGenStep.run(this.context);
|
|
158
183
|
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
159
|
-
// If we are about to execute a new info acquisition wave,
|
|
160
|
-
// wipe previous search results.
|
|
161
|
-
if (infoPlan.steps.length > 0 && this.context.initContext && this.context.analysis?.focus) {
|
|
162
|
-
this.context.initContext.relatedFiles = [];
|
|
163
|
-
this.context.analysis.focus.candidateFiles = [];
|
|
164
|
-
}
|
|
165
184
|
for (const step of infoPlan.steps) {
|
|
166
185
|
const stepIO = { query: this.query };
|
|
167
186
|
await this.executeStep(step, stepIO);
|
|
168
187
|
}
|
|
169
188
|
this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
|
|
170
189
|
}
|
|
190
|
+
const afterFocus = this.captureVerifyFocusSnapshot();
|
|
191
|
+
const hasFocusGrowth = this.logVerifyFocusDelta(beforeFocus, afterFocus);
|
|
192
|
+
stagnantWaves = hasFocusGrowth ? 0 : stagnantWaves + 1;
|
|
193
|
+
if (this.shouldStopVerifyForSaturation(stagnantWaves, MAX_STAGNANT_WAVES))
|
|
194
|
+
break;
|
|
171
195
|
this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
|
|
172
196
|
}
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
// Grounding is the phase boundary that decides whether execution may start.
|
|
198
|
+
if (!this.isWorkLoopReady())
|
|
199
|
+
return;
|
|
200
|
+
this.ensureTaskForWorkLoop();
|
|
201
|
+
// Research gate is evaluated after runResearch() in run().
|
|
202
|
+
}
|
|
203
|
+
/* ───────────── research ───────────── */
|
|
204
|
+
/**
|
|
205
|
+
* Seeds explicit research task steps for complex repo-wide lanes.
|
|
206
|
+
* Example: enqueue research-impact-map, research-symbol-trace, and research-risk-check.
|
|
207
|
+
*/
|
|
208
|
+
async runResearch() {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
if (!this.canExecuteRoute("research"))
|
|
211
|
+
return;
|
|
212
|
+
if (!this.context.task)
|
|
213
|
+
return;
|
|
214
|
+
(_a = this.context.task).taskSteps || (_a.taskSteps = []);
|
|
215
|
+
await researchPlanGenStep.run(this.context);
|
|
216
|
+
const generatedSteps = (this.context.analysis?.planSuggestion?.plan?.steps ?? [])
|
|
217
|
+
.filter(step => typeof step.action === "string" && step.action.startsWith("research-"))
|
|
218
|
+
.map(step => {
|
|
219
|
+
const action = step.action;
|
|
220
|
+
const defaultFilePath = action === "research-impact-map"
|
|
221
|
+
? "__research__/impact-map"
|
|
222
|
+
: action === "research-symbol-trace"
|
|
223
|
+
? "__research__/symbol-trace"
|
|
224
|
+
: action === "research-risk-check"
|
|
225
|
+
? "__research__/risk-check"
|
|
226
|
+
: "__research__/architecture-synthesis";
|
|
227
|
+
return {
|
|
228
|
+
action,
|
|
229
|
+
filePath: step.targetFile || defaultFilePath,
|
|
230
|
+
notes: step.description || `Run ${step.action}`,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
const fallbackResearchSteps = [
|
|
234
|
+
{
|
|
235
|
+
action: "research-impact-map",
|
|
236
|
+
filePath: "__research__/impact-map",
|
|
237
|
+
notes: "Map cross-file impact before code changes.",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
action: "research-symbol-trace",
|
|
241
|
+
filePath: "__research__/symbol-trace",
|
|
242
|
+
notes: "Trace key symbols across related files.",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
action: "research-risk-check",
|
|
246
|
+
filePath: "__research__/risk-check",
|
|
247
|
+
notes: "Record risks, assumptions, and constraints before edits.",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
action: "research-architecture-synthesis",
|
|
251
|
+
filePath: "__research__/architecture-synthesis",
|
|
252
|
+
notes: "Synthesize architecture summary, shared patterns, hotspots, and coupling points.",
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
const researchSteps = generatedSteps.length > 0 ? generatedSteps : fallbackResearchSteps;
|
|
256
|
+
let seededCount = 0;
|
|
257
|
+
for (const step of researchSteps) {
|
|
258
|
+
const exists = this.context.task.taskSteps.some(s => s.filePath === step.filePath && s.action === step.action);
|
|
259
|
+
if (exists)
|
|
260
|
+
continue;
|
|
261
|
+
this.context.task.taskSteps.push({
|
|
262
|
+
taskId: this.context.task.id,
|
|
263
|
+
filePath: step.filePath,
|
|
264
|
+
action: step.action,
|
|
265
|
+
status: "pending",
|
|
266
|
+
notes: step.notes,
|
|
267
|
+
result: { phase: "research", seededBy: "runResearch" },
|
|
268
|
+
});
|
|
269
|
+
seededCount++;
|
|
177
270
|
}
|
|
271
|
+
const plannedResearchSteps = this.context.task.taskSteps
|
|
272
|
+
.filter(s => typeof s.action === "string" && s.action.startsWith("research-"))
|
|
273
|
+
.map(s => ({
|
|
274
|
+
action: s.action,
|
|
275
|
+
filePath: s.filePath,
|
|
276
|
+
status: s.status,
|
|
277
|
+
notes: s.notes,
|
|
278
|
+
}));
|
|
279
|
+
logInputOutput("runResearch", "output", {
|
|
280
|
+
source: generatedSteps.length > 0 ? "generated" : "fallback",
|
|
281
|
+
seededCount,
|
|
282
|
+
totalResearchSteps: plannedResearchSteps.length,
|
|
283
|
+
steps: plannedResearchSteps,
|
|
284
|
+
});
|
|
285
|
+
(_b = this.context).analysis || (_b.analysis = {});
|
|
286
|
+
this.context.analysis.planSuggestion = undefined;
|
|
287
|
+
this.logLine("RESEARCH", "taskStepSeed", undefined, `${seededCount} research step(s) added (${generatedSteps.length > 0 ? "generated" : "fallback"})`);
|
|
178
288
|
}
|
|
289
|
+
/* ───────────── plan ───────────── */
|
|
179
290
|
/**
|
|
180
|
-
*
|
|
181
|
-
* Example:
|
|
291
|
+
* Seeds ordered execution task steps from selected files + research/verify artifacts.
|
|
292
|
+
* Example: prioritize files that are both selected and research-touched.
|
|
182
293
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
294
|
+
async runPlan() {
|
|
295
|
+
var _a, _b;
|
|
296
|
+
if (!this.context.task)
|
|
297
|
+
return;
|
|
298
|
+
if (!this.canExecutePhase("planning") || !this.canExecuteScope("planning"))
|
|
299
|
+
return;
|
|
300
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
301
|
+
(_b = this.context.task).taskSteps || (_b.taskSteps = []);
|
|
302
|
+
const existingExecutionPaths = new Set(this.context.task.taskSteps
|
|
303
|
+
.filter(step => !!step.filePath &&
|
|
304
|
+
!step.filePath.startsWith("__research__/"))
|
|
305
|
+
.map(step => step.filePath));
|
|
306
|
+
const selectedFiles = this.context.analysis.focus?.selectedFiles ?? [];
|
|
307
|
+
const touchedFromResearch = this.context.analysis.researchArtifacts?.touchedFiles ?? [];
|
|
308
|
+
const verifyRelevantFiles = Object.entries(this.context.analysis.verify?.byFile ?? {})
|
|
309
|
+
.filter(([_, verify]) => verify?.isRelevant)
|
|
310
|
+
.map(([filePath]) => filePath);
|
|
311
|
+
const rankPath = (filePath) => {
|
|
312
|
+
const inSelected = selectedFiles.includes(filePath);
|
|
313
|
+
const inResearchTouched = touchedFromResearch.includes(filePath);
|
|
314
|
+
const inVerify = verifyRelevantFiles.includes(filePath);
|
|
315
|
+
if (inSelected && inResearchTouched)
|
|
316
|
+
return 0;
|
|
317
|
+
if (inSelected)
|
|
318
|
+
return 1;
|
|
319
|
+
if (inResearchTouched)
|
|
320
|
+
return 2;
|
|
321
|
+
if (inVerify)
|
|
322
|
+
return 3;
|
|
323
|
+
return 4;
|
|
324
|
+
};
|
|
325
|
+
const plannedPaths = Array.from(new Set([
|
|
326
|
+
...selectedFiles,
|
|
327
|
+
...touchedFromResearch,
|
|
328
|
+
...verifyRelevantFiles,
|
|
329
|
+
]))
|
|
330
|
+
.filter(filePath => !!filePath && !filePath.startsWith("__research__/") && fs.existsSync(filePath))
|
|
331
|
+
.sort((a, b) => rankPath(a) - rankPath(b))
|
|
332
|
+
.slice(0, 16);
|
|
333
|
+
let seededCount = 0;
|
|
334
|
+
const seeded = [];
|
|
335
|
+
for (const filePath of plannedPaths) {
|
|
336
|
+
if (existingExecutionPaths.has(filePath))
|
|
337
|
+
continue;
|
|
338
|
+
const rank = rankPath(filePath);
|
|
339
|
+
const notes = rank === 0
|
|
340
|
+
? "Plan priority: selected + research-touched"
|
|
341
|
+
: rank === 1
|
|
342
|
+
? "Plan priority: selected file"
|
|
343
|
+
: rank === 2
|
|
344
|
+
? "Plan priority: research-touched file"
|
|
345
|
+
: "Plan priority: verify-relevant file";
|
|
346
|
+
this.context.task.taskSteps.push({
|
|
347
|
+
taskId: this.context.task.id,
|
|
348
|
+
filePath,
|
|
349
|
+
status: "pending",
|
|
350
|
+
notes,
|
|
351
|
+
result: {
|
|
352
|
+
phase: "plan",
|
|
353
|
+
seededBy: "runPlan",
|
|
354
|
+
priorityRank: rank,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
seeded.push({ filePath, rank, notes });
|
|
358
|
+
seededCount++;
|
|
359
|
+
}
|
|
360
|
+
logInputOutput("runPlan", "output", {
|
|
361
|
+
seededCount,
|
|
362
|
+
totalPlannedPaths: plannedPaths.length,
|
|
363
|
+
selectedFileCount: selectedFiles.length,
|
|
364
|
+
researchTouchedCount: touchedFromResearch.length,
|
|
365
|
+
verifyRelevantCount: verifyRelevantFiles.length,
|
|
366
|
+
seeded,
|
|
367
|
+
});
|
|
368
|
+
this.logLine("PLAN", "taskStepSeed", undefined, `${seededCount} execution step(s) planned`);
|
|
198
369
|
}
|
|
199
370
|
/* ───────────── work loop ───────────── */
|
|
200
371
|
async runWorkLoop() {
|
|
201
|
-
if (
|
|
372
|
+
if (this.context.task.status !== "active")
|
|
202
373
|
return;
|
|
203
374
|
this.ensureTaskForWorkLoop();
|
|
204
|
-
const MAX_TASK_STEPS =
|
|
375
|
+
const MAX_TASK_STEPS = this.getTaskStepBudget();
|
|
205
376
|
let stepCount = 0;
|
|
206
377
|
while (stepCount < MAX_TASK_STEPS &&
|
|
207
378
|
this.context.task.status === "active") {
|
|
@@ -232,7 +403,17 @@ export class MainAgent {
|
|
|
232
403
|
}
|
|
233
404
|
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
234
405
|
}
|
|
406
|
+
/* ───────────── finalize ───────────── */
|
|
407
|
+
async runFinalize() {
|
|
408
|
+
await finalAnswerModule.run({ query: this.query, context: this.context });
|
|
409
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
410
|
+
this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
|
|
411
|
+
}
|
|
235
412
|
/* ───────────── step iterations ───────────── */
|
|
413
|
+
/**
|
|
414
|
+
* Iterates one task step until it completes, needs feedback, or asks for redo.
|
|
415
|
+
* Example: validation failure sets nextAction=redo-step and re-runs iteration.
|
|
416
|
+
*/
|
|
236
417
|
async runStepIterations(taskStep) {
|
|
237
418
|
const MAX_ITERATIONS = 5;
|
|
238
419
|
let loopCount = 0;
|
|
@@ -261,9 +442,17 @@ export class MainAgent {
|
|
|
261
442
|
return "continue";
|
|
262
443
|
}
|
|
263
444
|
/* ───────────── work iteration ───────────── */
|
|
445
|
+
/**
|
|
446
|
+
* Executes one analyze/transform/validate pass for the current task step.
|
|
447
|
+
* Example: generate analysis plan, run one transform step, then validate.
|
|
448
|
+
*/
|
|
264
449
|
async runWorkIteration(taskStep) {
|
|
265
450
|
if (!this.context.analysis)
|
|
266
451
|
this.context.analysis = {};
|
|
452
|
+
if (taskStep.action?.startsWith("research-")) {
|
|
453
|
+
await this.executeResearchTaskStep(taskStep);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
267
456
|
if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
|
|
268
457
|
const tAnalysis = this.startTimer();
|
|
269
458
|
await analysisPlanGenStep.run(this.context);
|
|
@@ -309,6 +498,375 @@ export class MainAgent {
|
|
|
309
498
|
await integrateFeedbackStep.run(this.context);
|
|
310
499
|
this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
|
|
311
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Executes deterministic research steps and marks them complete.
|
|
503
|
+
* Example: research-impact-map summarizes affected files and seeds understanding notes.
|
|
504
|
+
*/
|
|
505
|
+
async executeResearchTaskStep(taskStep) {
|
|
506
|
+
var _a, _b;
|
|
507
|
+
const selectedFiles = this.context.analysis?.focus?.selectedFiles ?? [];
|
|
508
|
+
const candidateFiles = this.context.analysis?.focus?.candidateFiles ?? [];
|
|
509
|
+
const fileAnalysis = this.context.analysis?.fileAnalysis ?? {};
|
|
510
|
+
const researchTerms = this.buildResearchTerms();
|
|
511
|
+
const researchPaths = this.collectResearchPaths(24);
|
|
512
|
+
const corpus = this.loadResearchCorpus(researchPaths, 12, 12000);
|
|
513
|
+
const understanding = (_b = ((_a = this.context).analysis || (_a.analysis = {}))).understanding || (_b.understanding = {
|
|
514
|
+
assumptions: [],
|
|
515
|
+
constraints: [],
|
|
516
|
+
risks: [],
|
|
517
|
+
sharedPatterns: [],
|
|
518
|
+
hotspots: [],
|
|
519
|
+
couplingPoints: [],
|
|
520
|
+
});
|
|
521
|
+
const addUnique = (arr, value) => {
|
|
522
|
+
if (!arr)
|
|
523
|
+
return;
|
|
524
|
+
if (!arr.includes(value))
|
|
525
|
+
arr.push(value);
|
|
526
|
+
};
|
|
527
|
+
let summary = "";
|
|
528
|
+
let collectedData = {
|
|
529
|
+
selectedFiles: selectedFiles.slice(0, 12),
|
|
530
|
+
selectedFileCount: selectedFiles.length,
|
|
531
|
+
candidateFileCount: candidateFiles.length,
|
|
532
|
+
researchTerms,
|
|
533
|
+
corpusFilesRead: corpus.length,
|
|
534
|
+
corpusPaths: corpus.map(f => f.path).slice(0, 12),
|
|
535
|
+
};
|
|
536
|
+
switch (taskStep.action) {
|
|
537
|
+
case "research-impact-map": {
|
|
538
|
+
const touched = selectedFiles.length;
|
|
539
|
+
const impactRows = corpus
|
|
540
|
+
.map(file => {
|
|
541
|
+
const termHits = this.computeTermHits(file.content, researchTerms);
|
|
542
|
+
const termHitTotal = Object.values(termHits).reduce((acc, n) => acc + n, 0);
|
|
543
|
+
const importCount = this.countRegex(file.content, /\bimport\b|\brequire\s*\(/g);
|
|
544
|
+
const exportCount = this.countRegex(file.content, /\bexport\b|module\.exports/g);
|
|
545
|
+
const score = termHitTotal * 3 + importCount * 2 + exportCount;
|
|
546
|
+
return {
|
|
547
|
+
filePath: file.path,
|
|
548
|
+
score,
|
|
549
|
+
termHits,
|
|
550
|
+
importCount,
|
|
551
|
+
exportCount,
|
|
552
|
+
lineCount: file.lineCount,
|
|
553
|
+
};
|
|
554
|
+
})
|
|
555
|
+
.sort((a, b) => b.score - a.score)
|
|
556
|
+
.slice(0, 8);
|
|
557
|
+
summary = `Impact map across ${touched} selected file(s).`;
|
|
558
|
+
addUnique(understanding.constraints, `Refactor impact spans ${touched} file(s).`);
|
|
559
|
+
collectedData = {
|
|
560
|
+
...collectedData,
|
|
561
|
+
touchedFiles: selectedFiles.slice(0, 20),
|
|
562
|
+
impactSignals: [
|
|
563
|
+
`selected=${selectedFiles.length}`,
|
|
564
|
+
`candidates=${candidateFiles.length}`,
|
|
565
|
+
],
|
|
566
|
+
impactMap: impactRows,
|
|
567
|
+
};
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "research-symbol-trace": {
|
|
571
|
+
const structuralSymbols = Object.values(fileAnalysis)
|
|
572
|
+
.flatMap(fa => fa.structural?.functions?.map(fn => fn.name).filter(Boolean) ?? [])
|
|
573
|
+
.slice(0, 24);
|
|
574
|
+
const fallbackSymbols = corpus
|
|
575
|
+
.flatMap(file => Array.from(file.content.matchAll(/\b(function|class|const|let|var)\s+([A-Za-z_]\w*)/g)).map(m => m[2]))
|
|
576
|
+
.filter(Boolean);
|
|
577
|
+
const symbolPool = Array.from(new Set([...structuralSymbols, ...fallbackSymbols])).slice(0, 18);
|
|
578
|
+
const traceRows = symbolPool
|
|
579
|
+
.map(symbol => {
|
|
580
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
581
|
+
const re = new RegExp(`\\b${escaped}\\b`, "g");
|
|
582
|
+
const files = corpus
|
|
583
|
+
.map(file => ({ filePath: file.path, count: this.countRegex(file.content, re) }))
|
|
584
|
+
.filter(item => item.count > 0);
|
|
585
|
+
return {
|
|
586
|
+
symbol,
|
|
587
|
+
occurrenceCount: files.reduce((acc, f) => acc + f.count, 0),
|
|
588
|
+
files: files.slice(0, 8),
|
|
589
|
+
};
|
|
590
|
+
})
|
|
591
|
+
.filter(row => row.occurrenceCount > 0)
|
|
592
|
+
.sort((a, b) => b.occurrenceCount - a.occurrenceCount)
|
|
593
|
+
.slice(0, 10);
|
|
594
|
+
summary = traceRows.length
|
|
595
|
+
? `Traced ${traceRows.length} symbol(s) from corpus.`
|
|
596
|
+
: "No structural symbols found; symbol trace used filename-level anchors.";
|
|
597
|
+
addUnique(understanding.assumptions, "Symbol trace coverage is partial and based on current selected files.");
|
|
598
|
+
collectedData = {
|
|
599
|
+
...collectedData,
|
|
600
|
+
tracedSymbols: traceRows.map(s => s.symbol),
|
|
601
|
+
symbolTrace: traceRows,
|
|
602
|
+
analyzedFileCount: Object.values(fileAnalysis).filter(fa => fa?.semanticAnalyzed).length,
|
|
603
|
+
};
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "research-risk-check": {
|
|
607
|
+
const riskPatterns = [
|
|
608
|
+
{ id: "empty-catch", description: "Empty catch blocks", pattern: /catch\s*\(\s*[^)]*\)\s*\{\s*\}/g },
|
|
609
|
+
{ id: "console-error", description: "Console error logging", pattern: /\bconsole\.error\s*\(/g },
|
|
610
|
+
{ id: "forced-exit", description: "Process exit usage", pattern: /\bprocess\.exit\s*\(/g },
|
|
611
|
+
{ id: "throws-string", description: "Throwing non-Error values", pattern: /\bthrow\s+['"`]/g },
|
|
612
|
+
];
|
|
613
|
+
const riskRows = riskPatterns
|
|
614
|
+
.map(risk => {
|
|
615
|
+
const perFile = corpus
|
|
616
|
+
.map(file => ({ filePath: file.path, count: this.countRegex(file.content, risk.pattern) }))
|
|
617
|
+
.filter(hit => hit.count > 0);
|
|
618
|
+
return {
|
|
619
|
+
id: risk.id,
|
|
620
|
+
description: risk.description,
|
|
621
|
+
totalHits: perFile.reduce((acc, hit) => acc + hit.count, 0),
|
|
622
|
+
files: perFile.slice(0, 8),
|
|
623
|
+
};
|
|
624
|
+
})
|
|
625
|
+
.filter(risk => risk.totalHits > 0);
|
|
626
|
+
summary = "Recorded baseline risks/assumptions/constraints before transformation.";
|
|
627
|
+
addUnique(understanding.risks, "Cross-file regressions are possible without full symbol coverage.");
|
|
628
|
+
addUnique(understanding.risks, "Validation should run after each transform step.");
|
|
629
|
+
for (const risk of riskRows) {
|
|
630
|
+
addUnique(understanding.risks, `${risk.description}: ${risk.totalHits} hit(s)`);
|
|
631
|
+
}
|
|
632
|
+
collectedData = {
|
|
633
|
+
...collectedData,
|
|
634
|
+
risks: understanding.risks?.slice(0, 12) ?? [],
|
|
635
|
+
assumptions: understanding.assumptions?.slice(0, 12) ?? [],
|
|
636
|
+
constraints: understanding.constraints?.slice(0, 12) ?? [],
|
|
637
|
+
riskSignals: riskRows,
|
|
638
|
+
};
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "research-architecture-synthesis": {
|
|
642
|
+
const analyzedPaths = Object.entries(fileAnalysis)
|
|
643
|
+
.filter(([_, fa]) => fa?.semanticAnalyzed)
|
|
644
|
+
.map(([filePath]) => filePath);
|
|
645
|
+
const architectureFiles = (analyzedPaths.length > 0 ? analyzedPaths : corpus.map(file => file.path)).slice(0, 8);
|
|
646
|
+
understanding.problemStatement =
|
|
647
|
+
`Summarize repository architecture and identify weak coupling points across ${selectedFiles.length} scoped file(s).`;
|
|
648
|
+
for (const p of architectureFiles) {
|
|
649
|
+
const base = path.basename(p);
|
|
650
|
+
if (base.toLowerCase().includes("registry")) {
|
|
651
|
+
addUnique(understanding.hotspots, `${base}: central registry point with broad module fan-in.`);
|
|
652
|
+
addUnique(understanding.couplingPoints, `${base}: centralized module registration coupling.`);
|
|
653
|
+
}
|
|
654
|
+
if (base.toLowerCase().includes("module")) {
|
|
655
|
+
addUnique(understanding.sharedPatterns, `${base}: module-oriented pipeline pattern.`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
addUnique(understanding.sharedPatterns, "Pipeline modules follow a shared Module/ModuleIO contract.");
|
|
659
|
+
addUnique(understanding.couplingPoints, "Shared config/model utilities create cross-module coupling.");
|
|
660
|
+
addUnique(understanding.hotspots, "Core orchestration and registry layers are high-impact change zones.");
|
|
661
|
+
summary = `Architecture synthesis completed from ${architectureFiles.length} analyzed file(s).`;
|
|
662
|
+
const priorResearch = (this.context.task?.taskSteps ?? [])
|
|
663
|
+
.filter(step => step.action?.startsWith("research-") && step.status === "completed")
|
|
664
|
+
.map(step => ({
|
|
665
|
+
action: step.action,
|
|
666
|
+
summary: step.result?.research?.summary,
|
|
667
|
+
}));
|
|
668
|
+
collectedData = {
|
|
669
|
+
...collectedData,
|
|
670
|
+
architectureInputFiles: architectureFiles,
|
|
671
|
+
priorResearchSummaries: priorResearch,
|
|
672
|
+
problemStatement: understanding.problemStatement ?? "",
|
|
673
|
+
sharedPatterns: understanding.sharedPatterns?.slice(0, 12) ?? [],
|
|
674
|
+
hotspots: understanding.hotspots?.slice(0, 12) ?? [],
|
|
675
|
+
couplingPoints: understanding.couplingPoints?.slice(0, 12) ?? [],
|
|
676
|
+
};
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
default: {
|
|
680
|
+
summary = `Unknown research action: ${taskStep.action}`;
|
|
681
|
+
collectedData = {
|
|
682
|
+
...collectedData,
|
|
683
|
+
warning: "No handler for research action",
|
|
684
|
+
};
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const completedAt = new Date().toISOString();
|
|
689
|
+
const researchEntry = {
|
|
690
|
+
action: taskStep.action,
|
|
691
|
+
summary,
|
|
692
|
+
collectedData,
|
|
693
|
+
selectedFileCount: selectedFiles.length,
|
|
694
|
+
completedAt,
|
|
695
|
+
};
|
|
696
|
+
taskStep.result || (taskStep.result = {});
|
|
697
|
+
taskStep.result.research = researchEntry;
|
|
698
|
+
taskStep.result.stepReasoning = {
|
|
699
|
+
nextAction: "complete",
|
|
700
|
+
rationale: `Research step completed: ${summary}`,
|
|
701
|
+
confidence: 0.95,
|
|
702
|
+
};
|
|
703
|
+
taskStep.status = "completed";
|
|
704
|
+
this.persistResearchArtifact(researchEntry);
|
|
705
|
+
logInputOutput("runResearchStep", "output", {
|
|
706
|
+
research: researchEntry,
|
|
707
|
+
stepReasoning: taskStep.result.stepReasoning,
|
|
708
|
+
status: taskStep.status,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Persists normalized research outputs into analysis.researchArtifacts.
|
|
713
|
+
* Example: latestByAction["research-risk-check"] stores current risk findings.
|
|
714
|
+
*/
|
|
715
|
+
persistResearchArtifact(entry) {
|
|
716
|
+
var _a, _b;
|
|
717
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
718
|
+
const store = (_b = this.context.analysis).researchArtifacts || (_b.researchArtifacts = {
|
|
719
|
+
latestByAction: {},
|
|
720
|
+
history: [],
|
|
721
|
+
touchedFiles: [],
|
|
722
|
+
lastUpdatedAt: entry.completedAt,
|
|
723
|
+
});
|
|
724
|
+
store.latestByAction || (store.latestByAction = {});
|
|
725
|
+
store.history || (store.history = []);
|
|
726
|
+
store.touchedFiles || (store.touchedFiles = []);
|
|
727
|
+
store.latestByAction[entry.action] = entry;
|
|
728
|
+
store.history.push(entry);
|
|
729
|
+
const data = entry.collectedData ?? {};
|
|
730
|
+
const touched = this.extractPathsFromResearchData(data);
|
|
731
|
+
const merged = new Set([...(store.touchedFiles ?? []), ...touched]);
|
|
732
|
+
store.touchedFiles = Array.from(merged);
|
|
733
|
+
store.lastUpdatedAt = entry.completedAt;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Extracts file paths from heterogeneous research payloads.
|
|
737
|
+
* Example: impactMap rows and architectureInputFiles are both merged into touchedFiles.
|
|
738
|
+
*/
|
|
739
|
+
extractPathsFromResearchData(data) {
|
|
740
|
+
const paths = new Set();
|
|
741
|
+
const addPath = (value) => {
|
|
742
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
743
|
+
paths.add(value);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const addPathArray = (value) => {
|
|
747
|
+
if (!Array.isArray(value))
|
|
748
|
+
return;
|
|
749
|
+
for (const item of value) {
|
|
750
|
+
addPath(item);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
addPathArray(data.corpusPaths);
|
|
754
|
+
addPathArray(data.touchedFiles);
|
|
755
|
+
addPathArray(data.architectureInputFiles);
|
|
756
|
+
if (Array.isArray(data.impactMap)) {
|
|
757
|
+
for (const row of data.impactMap) {
|
|
758
|
+
addPath(row.filePath);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (Array.isArray(data.symbolTrace)) {
|
|
762
|
+
for (const row of data.symbolTrace) {
|
|
763
|
+
const files = row.files;
|
|
764
|
+
if (!Array.isArray(files))
|
|
765
|
+
continue;
|
|
766
|
+
for (const fileRow of files) {
|
|
767
|
+
addPath(fileRow.filePath);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (Array.isArray(data.riskSignals)) {
|
|
772
|
+
for (const row of data.riskSignals) {
|
|
773
|
+
const files = row.files;
|
|
774
|
+
if (!Array.isArray(files))
|
|
775
|
+
continue;
|
|
776
|
+
for (const fileRow of files) {
|
|
777
|
+
addPath(fileRow.filePath);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return Array.from(paths);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Builds lightweight query terms for deterministic research scanning.
|
|
785
|
+
* Example: "error handling test suite" -> ["error","handling","test","suite"].
|
|
786
|
+
*/
|
|
787
|
+
buildResearchTerms() {
|
|
788
|
+
const query = this.context.analysis?.intent?.normalizedQuery ??
|
|
789
|
+
this.context.initContext?.userQuery ??
|
|
790
|
+
this.query;
|
|
791
|
+
const stopWords = new Set([
|
|
792
|
+
"the", "and", "for", "with", "from", "this", "that", "what", "how",
|
|
793
|
+
"is", "are", "was", "were", "can", "could", "should", "would", "into",
|
|
794
|
+
"about", "across", "repo", "codebase", "please",
|
|
795
|
+
]);
|
|
796
|
+
return Array.from(new Set(query
|
|
797
|
+
.toLowerCase()
|
|
798
|
+
.split(/[^a-z0-9_]+/g)
|
|
799
|
+
.filter(token => token.length >= 3 && !stopWords.has(token)))).slice(0, 10);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Collects research candidate paths from selected, candidate, related, and working files.
|
|
803
|
+
* Example: selected files are prioritized before broader related file pool.
|
|
804
|
+
*/
|
|
805
|
+
collectResearchPaths(maxPaths) {
|
|
806
|
+
const focus = this.context.analysis?.focus;
|
|
807
|
+
const workingPaths = (this.context.workingFiles ?? []).map(file => file.path);
|
|
808
|
+
const related = this.context.initContext?.relatedFiles ?? [];
|
|
809
|
+
const combined = [
|
|
810
|
+
...(focus?.selectedFiles ?? []),
|
|
811
|
+
...(focus?.candidateFiles ?? []),
|
|
812
|
+
...workingPaths,
|
|
813
|
+
...related,
|
|
814
|
+
];
|
|
815
|
+
const unique = Array.from(new Set(combined));
|
|
816
|
+
return unique
|
|
817
|
+
.filter(filePath => !filePath.startsWith("__research__/") && fs.existsSync(filePath))
|
|
818
|
+
.slice(0, maxPaths);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Reads a bounded corpus from candidate paths.
|
|
822
|
+
* Example: read first 12 files, max 12k chars per file, skipping binary payloads.
|
|
823
|
+
*/
|
|
824
|
+
loadResearchCorpus(filePaths, maxFiles, maxCharsPerFile) {
|
|
825
|
+
const corpus = [];
|
|
826
|
+
for (const filePath of filePaths.slice(0, maxFiles)) {
|
|
827
|
+
try {
|
|
828
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
829
|
+
if (raw.includes("\u0000"))
|
|
830
|
+
continue;
|
|
831
|
+
const content = raw.slice(0, maxCharsPerFile);
|
|
832
|
+
corpus.push({
|
|
833
|
+
path: filePath,
|
|
834
|
+
content,
|
|
835
|
+
lineCount: content.split("\n").length,
|
|
836
|
+
charCount: content.length,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
// Ignore unreadable files and continue.
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return corpus;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Counts regex matches safely.
|
|
847
|
+
* Example: countRegex(code, /import/g) -> number of import occurrences.
|
|
848
|
+
*/
|
|
849
|
+
countRegex(content, pattern) {
|
|
850
|
+
const source = pattern.source;
|
|
851
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
852
|
+
const re = new RegExp(source, flags);
|
|
853
|
+
return Array.from(content.matchAll(re)).length;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Computes per-term match counts for a file body.
|
|
857
|
+
* Example: terms ["error","test"] -> { error: 4, test: 2 }.
|
|
858
|
+
*/
|
|
859
|
+
computeTermHits(content, terms) {
|
|
860
|
+
const hits = {};
|
|
861
|
+
for (const term of terms) {
|
|
862
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
863
|
+
const count = this.countRegex(content, new RegExp(`\\b${escaped}\\b`, "gi"));
|
|
864
|
+
if (count > 0) {
|
|
865
|
+
hits[term] = count;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return hits;
|
|
869
|
+
}
|
|
312
870
|
/* ───────────── step executor ───────────── */
|
|
313
871
|
/**
|
|
314
872
|
* Executes a single step using its corresponding module.
|
|
@@ -344,13 +902,7 @@ export class MainAgent {
|
|
|
344
902
|
throw err;
|
|
345
903
|
}
|
|
346
904
|
}
|
|
347
|
-
/* ─────────────
|
|
348
|
-
async runFinalize() {
|
|
349
|
-
await finalAnswerModule.run({ query: this.query, context: this.context });
|
|
350
|
-
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
351
|
-
this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
|
|
352
|
-
}
|
|
353
|
-
/* ───────────── extracted from runInitialRetrieval ───────────── */
|
|
905
|
+
/* ───────────── extracted from runSearch ───────────── */
|
|
354
906
|
resolveInitialRetrievalQueries() {
|
|
355
907
|
const rawUserQuery = this.context.initContext?.userQuery ?? this.query;
|
|
356
908
|
const retrievalQuery = this.context.analysis?.intent?.normalizedQuery?.trim() || rawUserQuery;
|
|
@@ -391,6 +943,8 @@ export class MainAgent {
|
|
|
391
943
|
};
|
|
392
944
|
}
|
|
393
945
|
mergeSeededInitialContext(rawUserQuery, seededContext) {
|
|
946
|
+
// Merge retrieval seed into initContext without losing previously discovered files.
|
|
947
|
+
// Example: keep old relatedFiles and append newly seeded files from buildLightContext.
|
|
394
948
|
const existingInit = this.context.initContext ?? { userQuery: rawUserQuery };
|
|
395
949
|
const seededInit = seededContext.initContext;
|
|
396
950
|
const mergedRelatedFiles = Array.from(new Set([
|
|
@@ -419,6 +973,8 @@ export class MainAgent {
|
|
|
419
973
|
return mergedRelatedFiles.length;
|
|
420
974
|
}
|
|
421
975
|
applyDeterministicPreGroundingPrefilter(retrievalQuery) {
|
|
976
|
+
// Rank and cap retrieval candidates before grounding to reduce noisy evidence passes.
|
|
977
|
+
// Example: explicit filename anchors are always kept even if BM25 score is low.
|
|
422
978
|
const init = this.context.initContext;
|
|
423
979
|
if (!init?.relatedFiles?.length)
|
|
424
980
|
return { before: 0, after: 0 };
|
|
@@ -458,6 +1014,184 @@ export class MainAgent {
|
|
|
458
1014
|
});
|
|
459
1015
|
return { before, after: init.relatedFiles.length };
|
|
460
1016
|
}
|
|
1017
|
+
injectWellKnownRepoFiles(currentCount) {
|
|
1018
|
+
// Add high-signal repo root files when scope is broad or retrieval is sparse.
|
|
1019
|
+
// Example: on repo-wide queries, include package.json/README if present.
|
|
1020
|
+
const init = this.context.initContext;
|
|
1021
|
+
if (!init)
|
|
1022
|
+
return { added: 0, reason: "none" };
|
|
1023
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1024
|
+
const shouldInjectByScope = scope === "repo-wide";
|
|
1025
|
+
const shouldInjectByFallback = currentCount < 3;
|
|
1026
|
+
if (!shouldInjectByScope && !shouldInjectByFallback) {
|
|
1027
|
+
return { added: 0, reason: "none" };
|
|
1028
|
+
}
|
|
1029
|
+
const reason = shouldInjectByScope ? "scope" : "fallback";
|
|
1030
|
+
const candidates = WELL_KNOWN_REPO_FILE_BASENAMES
|
|
1031
|
+
.map(fileName => path.join(process.cwd(), fileName))
|
|
1032
|
+
.filter(filePath => fs.existsSync(filePath))
|
|
1033
|
+
.slice(0, MAX_WELL_KNOWN_REPO_FILES);
|
|
1034
|
+
if (candidates.length === 0)
|
|
1035
|
+
return { added: 0, reason };
|
|
1036
|
+
const existing = new Set(init.relatedFiles ?? []);
|
|
1037
|
+
let added = 0;
|
|
1038
|
+
for (const filePath of candidates) {
|
|
1039
|
+
if (existing.has(filePath))
|
|
1040
|
+
continue;
|
|
1041
|
+
existing.add(filePath);
|
|
1042
|
+
added++;
|
|
1043
|
+
}
|
|
1044
|
+
init.relatedFiles = Array.from(existing);
|
|
1045
|
+
return { added, reason };
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Captures focus sizes before/after a verify wave for growth tracking.
|
|
1049
|
+
* Example: selected=3,candidate=7 before wave; selected=4,candidate=9 after wave.
|
|
1050
|
+
*/
|
|
1051
|
+
captureVerifyFocusSnapshot() {
|
|
1052
|
+
return {
|
|
1053
|
+
selected: this.context.analysis?.focus?.selectedFiles?.length ?? 0,
|
|
1054
|
+
candidate: this.context.analysis?.focus?.candidateFiles?.length ?? 0,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Logs selected/candidate deltas for a verify wave and returns whether focus grew.
|
|
1059
|
+
* Example: selected 3->4 (+1), candidate 7->9 (+2) => growth=true.
|
|
1060
|
+
*/
|
|
1061
|
+
logVerifyFocusDelta(before, after) {
|
|
1062
|
+
const selectedDelta = after.selected - before.selected;
|
|
1063
|
+
const candidateDelta = after.candidate - before.candidate;
|
|
1064
|
+
this.logLine("ANALYSIS", "groundingDelta", undefined, `selected ${before.selected}->${after.selected} (${selectedDelta >= 0 ? "+" : ""}${selectedDelta}), candidate ${before.candidate}->${after.candidate} (${candidateDelta >= 0 ? "+" : ""}${candidateDelta})`);
|
|
1065
|
+
return selectedDelta > 0 || candidateDelta > 0;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Stops verify loop when focus has not grown for too many consecutive waves.
|
|
1069
|
+
* Example: 2 stagnant waves in a row => stop early to avoid useless loops.
|
|
1070
|
+
*/
|
|
1071
|
+
shouldStopVerifyForSaturation(stagnantWaves, maxStagnantWaves) {
|
|
1072
|
+
if (stagnantWaves < maxStagnantWaves)
|
|
1073
|
+
return false;
|
|
1074
|
+
this.logLine("ANALYSIS", "groundingSaturated", undefined, `No focus growth for ${stagnantWaves} consecutive wave(s); stopping early`);
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Drops missing files from retrieval/focus sets to avoid verifier ENOENT noise.
|
|
1079
|
+
* Example: if DB points to deleted explainModule.ts, remove it before evidence pass.
|
|
1080
|
+
*/
|
|
1081
|
+
pruneMissingVerifyPaths() {
|
|
1082
|
+
const init = this.context.initContext;
|
|
1083
|
+
const focus = this.context.analysis?.focus;
|
|
1084
|
+
if (!init && !focus)
|
|
1085
|
+
return;
|
|
1086
|
+
const existsOrResearch = (filePath) => {
|
|
1087
|
+
if (filePath.startsWith("__research__/"))
|
|
1088
|
+
return true;
|
|
1089
|
+
return fs.existsSync(filePath);
|
|
1090
|
+
};
|
|
1091
|
+
let removedRelated = 0;
|
|
1092
|
+
let removedSelected = 0;
|
|
1093
|
+
let removedCandidate = 0;
|
|
1094
|
+
if (init?.relatedFiles?.length) {
|
|
1095
|
+
const before = init.relatedFiles.length;
|
|
1096
|
+
init.relatedFiles = init.relatedFiles.filter(existsOrResearch);
|
|
1097
|
+
removedRelated = before - init.relatedFiles.length;
|
|
1098
|
+
if (removedRelated > 0 && init.relatedFileScores) {
|
|
1099
|
+
init.relatedFileScores = Object.fromEntries(Object.entries(init.relatedFileScores).filter(([filePath]) => init.relatedFiles?.includes(filePath)));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (focus?.selectedFiles?.length) {
|
|
1103
|
+
const before = focus.selectedFiles.length;
|
|
1104
|
+
focus.selectedFiles = focus.selectedFiles.filter(existsOrResearch);
|
|
1105
|
+
removedSelected = before - focus.selectedFiles.length;
|
|
1106
|
+
}
|
|
1107
|
+
if (focus?.candidateFiles?.length) {
|
|
1108
|
+
const before = focus.candidateFiles.length;
|
|
1109
|
+
focus.candidateFiles = focus.candidateFiles.filter(existsOrResearch);
|
|
1110
|
+
removedCandidate = before - focus.candidateFiles.length;
|
|
1111
|
+
}
|
|
1112
|
+
if (removedRelated + removedSelected + removedCandidate > 0) {
|
|
1113
|
+
this.logLine("ANALYSIS", "verifyPruneMissing", undefined, `removed related=${removedRelated}, selected=${removedSelected}, candidate=${removedCandidate}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Route-aware grounding budget.
|
|
1118
|
+
* Example: repo-wide + needs-info => allow up to 4 verification waves.
|
|
1119
|
+
*/
|
|
1120
|
+
getGroundingWaveBudget() {
|
|
1121
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1122
|
+
const decision = this.context.analysis?.routingDecision?.decision ?? "has-info";
|
|
1123
|
+
const allowSearch = this.context.analysis?.routingDecision?.allowSearch ?? true;
|
|
1124
|
+
let budget = 2;
|
|
1125
|
+
if (!allowSearch || scope === "none")
|
|
1126
|
+
budget = 1;
|
|
1127
|
+
else if (scope === "single-file" && decision === "has-info")
|
|
1128
|
+
budget = 2;
|
|
1129
|
+
else if (scope === "multi-file")
|
|
1130
|
+
budget = 3;
|
|
1131
|
+
else if (scope === "repo-wide" && decision === "needs-info")
|
|
1132
|
+
budget = 4;
|
|
1133
|
+
this.logLine("ANALYSIS", "groundingBudget", undefined, `scope=${scope}, decision=${decision}, search=${allowSearch}, maxWaves=${budget}`);
|
|
1134
|
+
return budget;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Dynamic task-step cap by route complexity.
|
|
1138
|
+
* Example: research-required lanes get 10 steps instead of 5.
|
|
1139
|
+
*/
|
|
1140
|
+
getTaskStepBudget() {
|
|
1141
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1142
|
+
if (this.canExecuteRoute("research"))
|
|
1143
|
+
return 10;
|
|
1144
|
+
if (scope === "multi-file")
|
|
1145
|
+
return 7;
|
|
1146
|
+
if (scope === "single-file")
|
|
1147
|
+
return 5;
|
|
1148
|
+
return 6;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Blocks execution if repo-wide complex tasks lack minimum research signal.
|
|
1152
|
+
* Example: require at least two analyzed files plus one understanding signal.
|
|
1153
|
+
*/
|
|
1154
|
+
isResearchGateSatisfied() {
|
|
1155
|
+
if (!this.canExecuteRoute("research"))
|
|
1156
|
+
return true;
|
|
1157
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1158
|
+
const researchPlanCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" && s.action.startsWith("research-")).length ?? 0;
|
|
1159
|
+
const pendingResearchCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" &&
|
|
1160
|
+
s.action.startsWith("research-") &&
|
|
1161
|
+
s.status !== "completed").length ?? 0;
|
|
1162
|
+
const requiredResearchSteps = scope === "repo-wide"
|
|
1163
|
+
? 4
|
|
1164
|
+
: scope === "multi-file"
|
|
1165
|
+
? 3
|
|
1166
|
+
: 1;
|
|
1167
|
+
const hasResearchPlan = researchPlanCount >= requiredResearchSteps;
|
|
1168
|
+
if (!hasResearchPlan) {
|
|
1169
|
+
this.context.task.status = "deferred";
|
|
1170
|
+
this.context.task.reason =
|
|
1171
|
+
`Research phase required before execution ` +
|
|
1172
|
+
`(scope=${scope}, researchSteps=${researchPlanCount}, required=${requiredResearchSteps})`;
|
|
1173
|
+
this.persistTaskDataForRun();
|
|
1174
|
+
this.logLine("TASK", "Research gate blocked work loop", undefined, this.context.task.reason, { highlight: true });
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
if (pendingResearchCount > 0) {
|
|
1178
|
+
this.logLine("TASK", "Research gate queued", undefined, `researchSteps=${researchPlanCount}, pendingResearch=${pendingResearchCount}`);
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
const understanding = this.context.analysis?.understanding;
|
|
1182
|
+
const understandingSignals = (understanding?.assumptions?.length ?? 0) +
|
|
1183
|
+
(understanding?.constraints?.length ?? 0) +
|
|
1184
|
+
(understanding?.risks?.length ?? 0);
|
|
1185
|
+
if (understandingSignals > 0) {
|
|
1186
|
+
this.logLine("TASK", "Research gate passed", undefined, `researchSteps=${researchPlanCount}, understandingSignals=${understandingSignals}`);
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
this.context.task.status = "deferred";
|
|
1190
|
+
this.context.task.reason = `Research completed but produced insufficient understanding signals (${understandingSignals}).`;
|
|
1191
|
+
this.persistTaskDataForRun();
|
|
1192
|
+
this.logLine("TASK", "Research gate blocked work loop", undefined, this.context.task.reason, { highlight: true });
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
461
1195
|
/* ───────────── extracted from runWorkLoop ───────────── */
|
|
462
1196
|
isWorkLoopReady() {
|
|
463
1197
|
const readinessDecision = this.context.analysis?.readiness?.decision;
|
|
@@ -495,28 +1229,43 @@ export class MainAgent {
|
|
|
495
1229
|
taskStep.stepIndex = stepCount;
|
|
496
1230
|
taskStep.status = "pending";
|
|
497
1231
|
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
498
|
-
this.
|
|
1232
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1233
|
+
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
|
|
499
1234
|
taskStep.startTime = Date.now();
|
|
500
1235
|
persistTaskStepStart(taskStep, getDbForRepo());
|
|
501
1236
|
}
|
|
502
1237
|
finishTaskStep(taskStep, stepCount, stepAction) {
|
|
1238
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
503
1239
|
taskStep.endTime = Date.now();
|
|
504
1240
|
if (stepAction === "complete") {
|
|
505
1241
|
taskStep.status = "completed";
|
|
506
1242
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
507
|
-
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined,
|
|
1243
|
+
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, displayPath, { highlight: false });
|
|
508
1244
|
return;
|
|
509
1245
|
}
|
|
510
1246
|
taskStep.status = "pending";
|
|
511
1247
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
512
|
-
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined,
|
|
1248
|
+
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, displayPath);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Normalizes internal pseudo-paths for user-facing step logs.
|
|
1252
|
+
* Example: "__research__/symbol-trace" -> "research/symbol-trace".
|
|
1253
|
+
*/
|
|
1254
|
+
formatTaskStepDisplayPath(filePath) {
|
|
1255
|
+
return filePath.startsWith("__research__/")
|
|
1256
|
+
? filePath.replace("__research__/", "research/")
|
|
1257
|
+
: filePath;
|
|
513
1258
|
}
|
|
514
1259
|
/* ───────────── execution gates ───────────── */
|
|
515
1260
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
|
|
1261
|
+
* Gate model:
|
|
1262
|
+
* 1) Phase + scope gates decide coarse permissions (what broad work is allowed).
|
|
1263
|
+
* 2) Route gate decides finer sub-decisions within those allowed areas (what to do next).
|
|
1264
|
+
*/
|
|
1265
|
+
/**
|
|
1266
|
+
* Gate 1: Is this kind of work allowed at all?
|
|
1267
|
+
* Plain meaning: checks capability rules (e.g. read-only vs file-writing).
|
|
1268
|
+
* Example: for docs-only mode, analysis/planning are blocked, and writes are limited.
|
|
520
1269
|
*/
|
|
521
1270
|
canExecutePhase(phase) {
|
|
522
1271
|
const constraints = this.context.executionControl?.constraints;
|
|
@@ -536,10 +1285,9 @@ export class MainAgent {
|
|
|
536
1285
|
}
|
|
537
1286
|
/* ───────────── scope gates ───────────── */
|
|
538
1287
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* @returns True if the phase can be executed, false otherwise.
|
|
1288
|
+
* Gate 2: Is this work allowed for the current scope size?
|
|
1289
|
+
* Plain meaning: checks scope rules (none/single/multi/repo-wide).
|
|
1290
|
+
* Example: if scope is "analysis", only analysis/planning run and transform/write are blocked.
|
|
543
1291
|
*/
|
|
544
1292
|
canExecuteScope(phase) {
|
|
545
1293
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
@@ -553,6 +1301,24 @@ export class MainAgent {
|
|
|
553
1301
|
}
|
|
554
1302
|
return allowed;
|
|
555
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Gate 3: Does this request path want this action right now?
|
|
1306
|
+
* Plain meaning: checks route-specific intent from routingDecision.
|
|
1307
|
+
* Example: search expansion is skipped when routing says allowSearch=false.
|
|
1308
|
+
*/
|
|
1309
|
+
canExecuteRoute(action) {
|
|
1310
|
+
const routing = this.context.analysis?.routingDecision;
|
|
1311
|
+
switch (action) {
|
|
1312
|
+
case "search-expand":
|
|
1313
|
+
return routing?.allowSearch ?? true;
|
|
1314
|
+
case "transform":
|
|
1315
|
+
return routing?.allowTransform ?? true;
|
|
1316
|
+
case "research":
|
|
1317
|
+
return routing?.allowResearch ?? false;
|
|
1318
|
+
default:
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
556
1322
|
/* ----------------------------------- */
|
|
557
1323
|
/* ------------- helpers ------------- */
|
|
558
1324
|
/* ----------------------------------- */
|