scai 0.1.172 → 0.1.174
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 +891 -56
- 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 +121 -4
- package/dist/agents/researchPlanGenStep.js +123 -0
- package/dist/agents/resolveExecutionModeStep.js +40 -7
- package/dist/agents/routingDecisionStep.js +8 -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,276 @@ 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
|
+
this.recalibrateRoutingAfterVerify();
|
|
202
|
+
// Research gate is evaluated after runResearch() in run().
|
|
203
|
+
}
|
|
204
|
+
/* ───────────── research ───────────── */
|
|
205
|
+
/**
|
|
206
|
+
* Seeds explicit research task steps for complex repo-wide lanes.
|
|
207
|
+
* Example: enqueue research-impact-map, research-symbol-trace, and research-risk-check.
|
|
208
|
+
*/
|
|
209
|
+
async runResearch() {
|
|
210
|
+
var _a, _b;
|
|
211
|
+
if (!this.canExecuteRoute("research")) {
|
|
212
|
+
this.logLine("RESEARCH", "taskStepSeed", undefined, "skipped (route disallows research)");
|
|
213
|
+
return;
|
|
177
214
|
}
|
|
215
|
+
if (!this.context.task)
|
|
216
|
+
return;
|
|
217
|
+
(_a = this.context.task).taskSteps || (_a.taskSteps = []);
|
|
218
|
+
await researchPlanGenStep.run(this.context);
|
|
219
|
+
const generatedSteps = (this.context.analysis?.planSuggestion?.plan?.steps ?? [])
|
|
220
|
+
.filter(step => typeof step.action === "string" && step.action.startsWith("research-"))
|
|
221
|
+
.map(step => {
|
|
222
|
+
const action = step.action;
|
|
223
|
+
const defaultFilePath = action === "research-impact-map"
|
|
224
|
+
? "__research__/impact-map"
|
|
225
|
+
: action === "research-symbol-trace"
|
|
226
|
+
? "__research__/symbol-trace"
|
|
227
|
+
: action === "research-risk-check"
|
|
228
|
+
? "__research__/risk-check"
|
|
229
|
+
: "__research__/architecture-synthesis";
|
|
230
|
+
return {
|
|
231
|
+
action,
|
|
232
|
+
filePath: step.targetFile || defaultFilePath,
|
|
233
|
+
notes: step.description || `Run ${step.action}`,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
const fallbackResearchSteps = [
|
|
237
|
+
{
|
|
238
|
+
action: "research-impact-map",
|
|
239
|
+
filePath: "__research__/impact-map",
|
|
240
|
+
notes: "Map cross-file impact before code changes.",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
action: "research-symbol-trace",
|
|
244
|
+
filePath: "__research__/symbol-trace",
|
|
245
|
+
notes: "Trace key symbols across related files.",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
action: "research-risk-check",
|
|
249
|
+
filePath: "__research__/risk-check",
|
|
250
|
+
notes: "Record risks, assumptions, and constraints before edits.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
action: "research-architecture-synthesis",
|
|
254
|
+
filePath: "__research__/architecture-synthesis",
|
|
255
|
+
notes: "Synthesize architecture summary, shared patterns, hotspots, and coupling points.",
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
const researchSteps = generatedSteps.length > 0 ? generatedSteps : fallbackResearchSteps;
|
|
259
|
+
let seededCount = 0;
|
|
260
|
+
for (const step of researchSteps) {
|
|
261
|
+
const exists = this.context.task.taskSteps.some(s => s.filePath === step.filePath && s.action === step.action);
|
|
262
|
+
if (exists)
|
|
263
|
+
continue;
|
|
264
|
+
this.context.task.taskSteps.push({
|
|
265
|
+
taskId: this.context.task.id,
|
|
266
|
+
filePath: step.filePath,
|
|
267
|
+
action: step.action,
|
|
268
|
+
status: "pending",
|
|
269
|
+
notes: step.notes,
|
|
270
|
+
result: { phase: "research", seededBy: "runResearch" },
|
|
271
|
+
});
|
|
272
|
+
seededCount++;
|
|
273
|
+
}
|
|
274
|
+
const plannedResearchSteps = this.context.task.taskSteps
|
|
275
|
+
.filter(s => typeof s.action === "string" && s.action.startsWith("research-"))
|
|
276
|
+
.map(s => ({
|
|
277
|
+
action: s.action,
|
|
278
|
+
filePath: s.filePath,
|
|
279
|
+
status: s.status,
|
|
280
|
+
notes: s.notes,
|
|
281
|
+
}));
|
|
282
|
+
logInputOutput("runResearch", "output", {
|
|
283
|
+
source: generatedSteps.length > 0 ? "generated" : "fallback",
|
|
284
|
+
seededCount,
|
|
285
|
+
totalResearchSteps: plannedResearchSteps.length,
|
|
286
|
+
steps: plannedResearchSteps,
|
|
287
|
+
});
|
|
288
|
+
(_b = this.context).analysis || (_b.analysis = {});
|
|
289
|
+
this.context.analysis.planSuggestion = undefined;
|
|
290
|
+
this.logLine("RESEARCH", "taskStepSeed", undefined, `${seededCount} research step(s) added (${generatedSteps.length > 0 ? "generated" : "fallback"})`);
|
|
178
291
|
}
|
|
292
|
+
/* ───────────── plan ───────────── */
|
|
179
293
|
/**
|
|
180
|
-
*
|
|
181
|
-
* Example:
|
|
294
|
+
* Seeds ordered execution task steps from selected files + research/verify artifacts.
|
|
295
|
+
* Example: prioritize files that are both selected and research-touched.
|
|
182
296
|
*/
|
|
183
|
-
|
|
297
|
+
async runPlan() {
|
|
298
|
+
var _a, _b;
|
|
299
|
+
if (!this.context.task)
|
|
300
|
+
return;
|
|
301
|
+
if (!this.canExecutePhase("planning") || !this.canExecuteScope("planning"))
|
|
302
|
+
return;
|
|
303
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
304
|
+
(_b = this.context.task).taskSteps || (_b.taskSteps = []);
|
|
305
|
+
const existingExecutionPaths = new Set(this.context.task.taskSteps
|
|
306
|
+
.filter(step => !!step.filePath &&
|
|
307
|
+
!step.filePath.startsWith("__research__/"))
|
|
308
|
+
.map(step => step.filePath));
|
|
309
|
+
const selectedFiles = this.context.analysis.focus?.selectedFiles ?? [];
|
|
310
|
+
const touchedFromResearch = this.context.analysis.researchArtifacts?.touchedFiles ?? [];
|
|
311
|
+
const route = this.context.analysis.routingDecision;
|
|
312
|
+
const useFocusedSelectedPlanOnly = this.context.analysis.readiness?.decision === "ready" &&
|
|
313
|
+
(route?.decision === "has-info") &&
|
|
314
|
+
(route?.scopeLocked ?? false) &&
|
|
315
|
+
(route?.allowSearch === false) &&
|
|
316
|
+
selectedFiles.length > 0;
|
|
317
|
+
const verifyMinConfidence = this.getVerifyConfidenceThresholdForPlan();
|
|
318
|
+
const verifyEntries = Object.entries(this.context.analysis.verify?.byFile ?? {});
|
|
319
|
+
const verifyRelevantFiles = verifyEntries
|
|
320
|
+
.filter(([_, verify]) => verify?.isRelevant &&
|
|
321
|
+
(verify.fileConfidence ?? 0) >= verifyMinConfidence)
|
|
322
|
+
.map(([filePath]) => filePath);
|
|
323
|
+
const verifySkippedLowConfidenceCount = verifyEntries.filter(([_, verify]) => !!verify?.isRelevant &&
|
|
324
|
+
(verify.fileConfidence ?? 0) < verifyMinConfidence).length;
|
|
325
|
+
const rankPath = (filePath) => {
|
|
326
|
+
const inSelected = selectedFiles.includes(filePath);
|
|
327
|
+
const inResearchTouched = touchedFromResearch.includes(filePath);
|
|
328
|
+
const inVerify = verifyRelevantFiles.includes(filePath);
|
|
329
|
+
if (inSelected && inResearchTouched)
|
|
330
|
+
return 0;
|
|
331
|
+
if (inSelected)
|
|
332
|
+
return 1;
|
|
333
|
+
if (inResearchTouched)
|
|
334
|
+
return 2;
|
|
335
|
+
if (inVerify)
|
|
336
|
+
return 3;
|
|
337
|
+
return 4;
|
|
338
|
+
};
|
|
339
|
+
const plannedPathsSource = useFocusedSelectedPlanOnly
|
|
340
|
+
? Array.from(new Set(selectedFiles))
|
|
341
|
+
: Array.from(new Set([
|
|
342
|
+
...selectedFiles,
|
|
343
|
+
...touchedFromResearch,
|
|
344
|
+
...verifyRelevantFiles,
|
|
345
|
+
]));
|
|
346
|
+
const plannedPaths = plannedPathsSource
|
|
347
|
+
.filter(filePath => !!filePath && !filePath.startsWith("__research__/") && fs.existsSync(filePath))
|
|
348
|
+
.sort((a, b) => rankPath(a) - rankPath(b))
|
|
349
|
+
.slice(0, 16);
|
|
350
|
+
let seededCount = 0;
|
|
351
|
+
const seeded = [];
|
|
352
|
+
for (const filePath of plannedPaths) {
|
|
353
|
+
if (existingExecutionPaths.has(filePath))
|
|
354
|
+
continue;
|
|
355
|
+
const rank = rankPath(filePath);
|
|
356
|
+
const notes = rank === 0
|
|
357
|
+
? "Plan priority: selected + research-touched"
|
|
358
|
+
: rank === 1
|
|
359
|
+
? "Plan priority: selected file"
|
|
360
|
+
: rank === 2
|
|
361
|
+
? "Plan priority: research-touched file"
|
|
362
|
+
: "Plan priority: verify-relevant file";
|
|
363
|
+
this.context.task.taskSteps.push({
|
|
364
|
+
taskId: this.context.task.id,
|
|
365
|
+
filePath,
|
|
366
|
+
status: "pending",
|
|
367
|
+
notes,
|
|
368
|
+
result: {
|
|
369
|
+
phase: "plan",
|
|
370
|
+
seededBy: "runPlan",
|
|
371
|
+
priorityRank: rank,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
seeded.push({ filePath, rank, notes });
|
|
375
|
+
seededCount++;
|
|
376
|
+
}
|
|
377
|
+
logInputOutput("runPlan", "output", {
|
|
378
|
+
seededCount,
|
|
379
|
+
totalPlannedPaths: plannedPaths.length,
|
|
380
|
+
selectedFileCount: selectedFiles.length,
|
|
381
|
+
researchTouchedCount: touchedFromResearch.length,
|
|
382
|
+
verifyRelevantCount: verifyRelevantFiles.length,
|
|
383
|
+
focusedSelectedOnly: useFocusedSelectedPlanOnly,
|
|
384
|
+
verifyMinConfidence,
|
|
385
|
+
verifySkippedLowConfidenceCount,
|
|
386
|
+
seeded,
|
|
387
|
+
});
|
|
388
|
+
this.logLine("PLAN", "taskStepSeed", undefined, `${seededCount} execution step(s) planned`);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Sets minimum verify confidence before a file can be plan-seeded from verify-only signal.
|
|
392
|
+
* Example: single-file lanes require higher confidence than repo-wide lanes.
|
|
393
|
+
*/
|
|
394
|
+
getVerifyConfidenceThresholdForPlan() {
|
|
184
395
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
396
|
+
if (scope === "single-file")
|
|
397
|
+
return 0.45;
|
|
398
|
+
if (scope === "multi-file")
|
|
399
|
+
return 0.35;
|
|
400
|
+
return 0.3;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Re-routes after verify when evidence converges on selected files with high confidence.
|
|
404
|
+
* Example: selected files strongly verified => disable expansion/research and lock focused execution.
|
|
405
|
+
*/
|
|
406
|
+
recalibrateRoutingAfterVerify() {
|
|
407
|
+
var _a;
|
|
408
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
409
|
+
const routing = this.context.analysis.routingDecision;
|
|
410
|
+
if (!routing)
|
|
411
|
+
return;
|
|
412
|
+
const selectedFiles = this.context.analysis.focus?.selectedFiles ?? [];
|
|
413
|
+
if (selectedFiles.length === 0)
|
|
414
|
+
return;
|
|
415
|
+
const readinessConfidence = this.context.analysis.readiness?.confidence ?? 0;
|
|
416
|
+
const intentConfidence = this.context.analysis.intent?.confidence ?? 0;
|
|
417
|
+
const minFileConfidence = 0.28;
|
|
418
|
+
const strongSelected = selectedFiles.filter(filePath => {
|
|
419
|
+
const verify = this.context.analysis?.verify?.byFile?.[filePath];
|
|
420
|
+
return verify?.isRelevant === true && (verify.fileConfidence ?? 0) >= minFileConfidence;
|
|
421
|
+
});
|
|
422
|
+
const convergedSingle = selectedFiles.length === 1 &&
|
|
423
|
+
strongSelected.length === 1 &&
|
|
424
|
+
readinessConfidence >= 0.9 &&
|
|
425
|
+
intentConfidence >= 0.8;
|
|
426
|
+
const convergedMulti = selectedFiles.length >= 2 &&
|
|
427
|
+
strongSelected.length >= 2 &&
|
|
428
|
+
readinessConfidence >= 0.9 &&
|
|
429
|
+
intentConfidence >= 0.75;
|
|
430
|
+
if (!convergedSingle && !convergedMulti)
|
|
431
|
+
return;
|
|
432
|
+
routing.decision = "has-info";
|
|
433
|
+
routing.allowSearch = false;
|
|
434
|
+
routing.allowResearch = false;
|
|
435
|
+
routing.scopeLocked = true;
|
|
436
|
+
routing.rationale = `${routing.rationale}; postVerify=focused-selection(${strongSelected.length})`;
|
|
437
|
+
this.logLine("TASK", "Routing recalibrated", undefined, `focused=${selectedFiles.length} selected, strong=${strongSelected.length}`);
|
|
198
438
|
}
|
|
199
439
|
/* ───────────── work loop ───────────── */
|
|
200
440
|
async runWorkLoop() {
|
|
201
|
-
if (
|
|
441
|
+
if (this.context.task.status !== "active")
|
|
202
442
|
return;
|
|
203
443
|
this.ensureTaskForWorkLoop();
|
|
204
|
-
const MAX_TASK_STEPS =
|
|
444
|
+
const MAX_TASK_STEPS = this.getTaskStepBudget();
|
|
205
445
|
let stepCount = 0;
|
|
206
446
|
while (stepCount < MAX_TASK_STEPS &&
|
|
207
447
|
this.context.task.status === "active") {
|
|
@@ -232,7 +472,17 @@ export class MainAgent {
|
|
|
232
472
|
}
|
|
233
473
|
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
234
474
|
}
|
|
475
|
+
/* ───────────── finalize ───────────── */
|
|
476
|
+
async runFinalize() {
|
|
477
|
+
await finalAnswerModule.run({ query: this.query, context: this.context });
|
|
478
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
479
|
+
this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
|
|
480
|
+
}
|
|
235
481
|
/* ───────────── step iterations ───────────── */
|
|
482
|
+
/**
|
|
483
|
+
* Iterates one task step until it completes, needs feedback, or asks for redo.
|
|
484
|
+
* Example: validation failure sets nextAction=redo-step and re-runs iteration.
|
|
485
|
+
*/
|
|
236
486
|
async runStepIterations(taskStep) {
|
|
237
487
|
const MAX_ITERATIONS = 5;
|
|
238
488
|
let loopCount = 0;
|
|
@@ -261,9 +511,17 @@ export class MainAgent {
|
|
|
261
511
|
return "continue";
|
|
262
512
|
}
|
|
263
513
|
/* ───────────── work iteration ───────────── */
|
|
514
|
+
/**
|
|
515
|
+
* Executes one analyze/transform/validate pass for the current task step.
|
|
516
|
+
* Example: generate analysis plan, run one transform step, then validate.
|
|
517
|
+
*/
|
|
264
518
|
async runWorkIteration(taskStep) {
|
|
265
519
|
if (!this.context.analysis)
|
|
266
520
|
this.context.analysis = {};
|
|
521
|
+
if (taskStep.action?.startsWith("research-")) {
|
|
522
|
+
await this.executeResearchTaskStep(taskStep);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
267
525
|
if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
|
|
268
526
|
const tAnalysis = this.startTimer();
|
|
269
527
|
await analysisPlanGenStep.run(this.context);
|
|
@@ -309,6 +567,375 @@ export class MainAgent {
|
|
|
309
567
|
await integrateFeedbackStep.run(this.context);
|
|
310
568
|
this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
|
|
311
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Executes deterministic research steps and marks them complete.
|
|
572
|
+
* Example: research-impact-map summarizes affected files and seeds understanding notes.
|
|
573
|
+
*/
|
|
574
|
+
async executeResearchTaskStep(taskStep) {
|
|
575
|
+
var _a, _b;
|
|
576
|
+
const selectedFiles = this.context.analysis?.focus?.selectedFiles ?? [];
|
|
577
|
+
const candidateFiles = this.context.analysis?.focus?.candidateFiles ?? [];
|
|
578
|
+
const fileAnalysis = this.context.analysis?.fileAnalysis ?? {};
|
|
579
|
+
const researchTerms = this.buildResearchTerms();
|
|
580
|
+
const researchPaths = this.collectResearchPaths(24);
|
|
581
|
+
const corpus = this.loadResearchCorpus(researchPaths, 12, 12000);
|
|
582
|
+
const understanding = (_b = ((_a = this.context).analysis || (_a.analysis = {}))).understanding || (_b.understanding = {
|
|
583
|
+
assumptions: [],
|
|
584
|
+
constraints: [],
|
|
585
|
+
risks: [],
|
|
586
|
+
sharedPatterns: [],
|
|
587
|
+
hotspots: [],
|
|
588
|
+
couplingPoints: [],
|
|
589
|
+
});
|
|
590
|
+
const addUnique = (arr, value) => {
|
|
591
|
+
if (!arr)
|
|
592
|
+
return;
|
|
593
|
+
if (!arr.includes(value))
|
|
594
|
+
arr.push(value);
|
|
595
|
+
};
|
|
596
|
+
let summary = "";
|
|
597
|
+
let collectedData = {
|
|
598
|
+
selectedFiles: selectedFiles.slice(0, 12),
|
|
599
|
+
selectedFileCount: selectedFiles.length,
|
|
600
|
+
candidateFileCount: candidateFiles.length,
|
|
601
|
+
researchTerms,
|
|
602
|
+
corpusFilesRead: corpus.length,
|
|
603
|
+
corpusPaths: corpus.map(f => f.path).slice(0, 12),
|
|
604
|
+
};
|
|
605
|
+
switch (taskStep.action) {
|
|
606
|
+
case "research-impact-map": {
|
|
607
|
+
const touched = selectedFiles.length;
|
|
608
|
+
const impactRows = corpus
|
|
609
|
+
.map(file => {
|
|
610
|
+
const termHits = this.computeTermHits(file.content, researchTerms);
|
|
611
|
+
const termHitTotal = Object.values(termHits).reduce((acc, n) => acc + n, 0);
|
|
612
|
+
const importCount = this.countRegex(file.content, /\bimport\b|\brequire\s*\(/g);
|
|
613
|
+
const exportCount = this.countRegex(file.content, /\bexport\b|module\.exports/g);
|
|
614
|
+
const score = termHitTotal * 3 + importCount * 2 + exportCount;
|
|
615
|
+
return {
|
|
616
|
+
filePath: file.path,
|
|
617
|
+
score,
|
|
618
|
+
termHits,
|
|
619
|
+
importCount,
|
|
620
|
+
exportCount,
|
|
621
|
+
lineCount: file.lineCount,
|
|
622
|
+
};
|
|
623
|
+
})
|
|
624
|
+
.sort((a, b) => b.score - a.score)
|
|
625
|
+
.slice(0, 8);
|
|
626
|
+
summary = `Impact map across ${touched} selected file(s).`;
|
|
627
|
+
addUnique(understanding.constraints, `Refactor impact spans ${touched} file(s).`);
|
|
628
|
+
collectedData = {
|
|
629
|
+
...collectedData,
|
|
630
|
+
touchedFiles: selectedFiles.slice(0, 20),
|
|
631
|
+
impactSignals: [
|
|
632
|
+
`selected=${selectedFiles.length}`,
|
|
633
|
+
`candidates=${candidateFiles.length}`,
|
|
634
|
+
],
|
|
635
|
+
impactMap: impactRows,
|
|
636
|
+
};
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "research-symbol-trace": {
|
|
640
|
+
const structuralSymbols = Object.values(fileAnalysis)
|
|
641
|
+
.flatMap(fa => fa.structural?.functions?.map(fn => fn.name).filter(Boolean) ?? [])
|
|
642
|
+
.slice(0, 24);
|
|
643
|
+
const fallbackSymbols = corpus
|
|
644
|
+
.flatMap(file => Array.from(file.content.matchAll(/\b(function|class|const|let|var)\s+([A-Za-z_]\w*)/g)).map(m => m[2]))
|
|
645
|
+
.filter(Boolean);
|
|
646
|
+
const symbolPool = Array.from(new Set([...structuralSymbols, ...fallbackSymbols])).slice(0, 18);
|
|
647
|
+
const traceRows = symbolPool
|
|
648
|
+
.map(symbol => {
|
|
649
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
650
|
+
const re = new RegExp(`\\b${escaped}\\b`, "g");
|
|
651
|
+
const files = corpus
|
|
652
|
+
.map(file => ({ filePath: file.path, count: this.countRegex(file.content, re) }))
|
|
653
|
+
.filter(item => item.count > 0);
|
|
654
|
+
return {
|
|
655
|
+
symbol,
|
|
656
|
+
occurrenceCount: files.reduce((acc, f) => acc + f.count, 0),
|
|
657
|
+
files: files.slice(0, 8),
|
|
658
|
+
};
|
|
659
|
+
})
|
|
660
|
+
.filter(row => row.occurrenceCount > 0)
|
|
661
|
+
.sort((a, b) => b.occurrenceCount - a.occurrenceCount)
|
|
662
|
+
.slice(0, 10);
|
|
663
|
+
summary = traceRows.length
|
|
664
|
+
? `Traced ${traceRows.length} symbol(s) from corpus.`
|
|
665
|
+
: "No structural symbols found; symbol trace used filename-level anchors.";
|
|
666
|
+
addUnique(understanding.assumptions, "Symbol trace coverage is partial and based on current selected files.");
|
|
667
|
+
collectedData = {
|
|
668
|
+
...collectedData,
|
|
669
|
+
tracedSymbols: traceRows.map(s => s.symbol),
|
|
670
|
+
symbolTrace: traceRows,
|
|
671
|
+
analyzedFileCount: Object.values(fileAnalysis).filter(fa => fa?.semanticAnalyzed).length,
|
|
672
|
+
};
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case "research-risk-check": {
|
|
676
|
+
const riskPatterns = [
|
|
677
|
+
{ id: "empty-catch", description: "Empty catch blocks", pattern: /catch\s*\(\s*[^)]*\)\s*\{\s*\}/g },
|
|
678
|
+
{ id: "console-error", description: "Console error logging", pattern: /\bconsole\.error\s*\(/g },
|
|
679
|
+
{ id: "forced-exit", description: "Process exit usage", pattern: /\bprocess\.exit\s*\(/g },
|
|
680
|
+
{ id: "throws-string", description: "Throwing non-Error values", pattern: /\bthrow\s+['"`]/g },
|
|
681
|
+
];
|
|
682
|
+
const riskRows = riskPatterns
|
|
683
|
+
.map(risk => {
|
|
684
|
+
const perFile = corpus
|
|
685
|
+
.map(file => ({ filePath: file.path, count: this.countRegex(file.content, risk.pattern) }))
|
|
686
|
+
.filter(hit => hit.count > 0);
|
|
687
|
+
return {
|
|
688
|
+
id: risk.id,
|
|
689
|
+
description: risk.description,
|
|
690
|
+
totalHits: perFile.reduce((acc, hit) => acc + hit.count, 0),
|
|
691
|
+
files: perFile.slice(0, 8),
|
|
692
|
+
};
|
|
693
|
+
})
|
|
694
|
+
.filter(risk => risk.totalHits > 0);
|
|
695
|
+
summary = "Recorded baseline risks/assumptions/constraints before transformation.";
|
|
696
|
+
addUnique(understanding.risks, "Cross-file regressions are possible without full symbol coverage.");
|
|
697
|
+
addUnique(understanding.risks, "Validation should run after each transform step.");
|
|
698
|
+
for (const risk of riskRows) {
|
|
699
|
+
addUnique(understanding.risks, `${risk.description}: ${risk.totalHits} hit(s)`);
|
|
700
|
+
}
|
|
701
|
+
collectedData = {
|
|
702
|
+
...collectedData,
|
|
703
|
+
risks: understanding.risks?.slice(0, 12) ?? [],
|
|
704
|
+
assumptions: understanding.assumptions?.slice(0, 12) ?? [],
|
|
705
|
+
constraints: understanding.constraints?.slice(0, 12) ?? [],
|
|
706
|
+
riskSignals: riskRows,
|
|
707
|
+
};
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "research-architecture-synthesis": {
|
|
711
|
+
const analyzedPaths = Object.entries(fileAnalysis)
|
|
712
|
+
.filter(([_, fa]) => fa?.semanticAnalyzed)
|
|
713
|
+
.map(([filePath]) => filePath);
|
|
714
|
+
const architectureFiles = (analyzedPaths.length > 0 ? analyzedPaths : corpus.map(file => file.path)).slice(0, 8);
|
|
715
|
+
understanding.problemStatement =
|
|
716
|
+
`Summarize repository architecture and identify weak coupling points across ${selectedFiles.length} scoped file(s).`;
|
|
717
|
+
for (const p of architectureFiles) {
|
|
718
|
+
const base = path.basename(p);
|
|
719
|
+
if (base.toLowerCase().includes("registry")) {
|
|
720
|
+
addUnique(understanding.hotspots, `${base}: central registry point with broad module fan-in.`);
|
|
721
|
+
addUnique(understanding.couplingPoints, `${base}: centralized module registration coupling.`);
|
|
722
|
+
}
|
|
723
|
+
if (base.toLowerCase().includes("module")) {
|
|
724
|
+
addUnique(understanding.sharedPatterns, `${base}: module-oriented pipeline pattern.`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
addUnique(understanding.sharedPatterns, "Pipeline modules follow a shared Module/ModuleIO contract.");
|
|
728
|
+
addUnique(understanding.couplingPoints, "Shared config/model utilities create cross-module coupling.");
|
|
729
|
+
addUnique(understanding.hotspots, "Core orchestration and registry layers are high-impact change zones.");
|
|
730
|
+
summary = `Architecture synthesis completed from ${architectureFiles.length} analyzed file(s).`;
|
|
731
|
+
const priorResearch = (this.context.task?.taskSteps ?? [])
|
|
732
|
+
.filter(step => step.action?.startsWith("research-") && step.status === "completed")
|
|
733
|
+
.map(step => ({
|
|
734
|
+
action: step.action,
|
|
735
|
+
summary: step.result?.research?.summary,
|
|
736
|
+
}));
|
|
737
|
+
collectedData = {
|
|
738
|
+
...collectedData,
|
|
739
|
+
architectureInputFiles: architectureFiles,
|
|
740
|
+
priorResearchSummaries: priorResearch,
|
|
741
|
+
problemStatement: understanding.problemStatement ?? "",
|
|
742
|
+
sharedPatterns: understanding.sharedPatterns?.slice(0, 12) ?? [],
|
|
743
|
+
hotspots: understanding.hotspots?.slice(0, 12) ?? [],
|
|
744
|
+
couplingPoints: understanding.couplingPoints?.slice(0, 12) ?? [],
|
|
745
|
+
};
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
default: {
|
|
749
|
+
summary = `Unknown research action: ${taskStep.action}`;
|
|
750
|
+
collectedData = {
|
|
751
|
+
...collectedData,
|
|
752
|
+
warning: "No handler for research action",
|
|
753
|
+
};
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const completedAt = new Date().toISOString();
|
|
758
|
+
const researchEntry = {
|
|
759
|
+
action: taskStep.action,
|
|
760
|
+
summary,
|
|
761
|
+
collectedData,
|
|
762
|
+
selectedFileCount: selectedFiles.length,
|
|
763
|
+
completedAt,
|
|
764
|
+
};
|
|
765
|
+
taskStep.result || (taskStep.result = {});
|
|
766
|
+
taskStep.result.research = researchEntry;
|
|
767
|
+
taskStep.result.stepReasoning = {
|
|
768
|
+
nextAction: "complete",
|
|
769
|
+
rationale: `Research step completed: ${summary}`,
|
|
770
|
+
confidence: 0.95,
|
|
771
|
+
};
|
|
772
|
+
taskStep.status = "completed";
|
|
773
|
+
this.persistResearchArtifact(researchEntry);
|
|
774
|
+
logInputOutput("runResearchStep", "output", {
|
|
775
|
+
research: researchEntry,
|
|
776
|
+
stepReasoning: taskStep.result.stepReasoning,
|
|
777
|
+
status: taskStep.status,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Persists normalized research outputs into analysis.researchArtifacts.
|
|
782
|
+
* Example: latestByAction["research-risk-check"] stores current risk findings.
|
|
783
|
+
*/
|
|
784
|
+
persistResearchArtifact(entry) {
|
|
785
|
+
var _a, _b;
|
|
786
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
787
|
+
const store = (_b = this.context.analysis).researchArtifacts || (_b.researchArtifacts = {
|
|
788
|
+
latestByAction: {},
|
|
789
|
+
history: [],
|
|
790
|
+
touchedFiles: [],
|
|
791
|
+
lastUpdatedAt: entry.completedAt,
|
|
792
|
+
});
|
|
793
|
+
store.latestByAction || (store.latestByAction = {});
|
|
794
|
+
store.history || (store.history = []);
|
|
795
|
+
store.touchedFiles || (store.touchedFiles = []);
|
|
796
|
+
store.latestByAction[entry.action] = entry;
|
|
797
|
+
store.history.push(entry);
|
|
798
|
+
const data = entry.collectedData ?? {};
|
|
799
|
+
const touched = this.extractPathsFromResearchData(data);
|
|
800
|
+
const merged = new Set([...(store.touchedFiles ?? []), ...touched]);
|
|
801
|
+
store.touchedFiles = Array.from(merged);
|
|
802
|
+
store.lastUpdatedAt = entry.completedAt;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Extracts file paths from heterogeneous research payloads.
|
|
806
|
+
* Example: impactMap rows and architectureInputFiles are both merged into touchedFiles.
|
|
807
|
+
*/
|
|
808
|
+
extractPathsFromResearchData(data) {
|
|
809
|
+
const paths = new Set();
|
|
810
|
+
const addPath = (value) => {
|
|
811
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
812
|
+
paths.add(value);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
const addPathArray = (value) => {
|
|
816
|
+
if (!Array.isArray(value))
|
|
817
|
+
return;
|
|
818
|
+
for (const item of value) {
|
|
819
|
+
addPath(item);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
addPathArray(data.corpusPaths);
|
|
823
|
+
addPathArray(data.touchedFiles);
|
|
824
|
+
addPathArray(data.architectureInputFiles);
|
|
825
|
+
if (Array.isArray(data.impactMap)) {
|
|
826
|
+
for (const row of data.impactMap) {
|
|
827
|
+
addPath(row.filePath);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (Array.isArray(data.symbolTrace)) {
|
|
831
|
+
for (const row of data.symbolTrace) {
|
|
832
|
+
const files = row.files;
|
|
833
|
+
if (!Array.isArray(files))
|
|
834
|
+
continue;
|
|
835
|
+
for (const fileRow of files) {
|
|
836
|
+
addPath(fileRow.filePath);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (Array.isArray(data.riskSignals)) {
|
|
841
|
+
for (const row of data.riskSignals) {
|
|
842
|
+
const files = row.files;
|
|
843
|
+
if (!Array.isArray(files))
|
|
844
|
+
continue;
|
|
845
|
+
for (const fileRow of files) {
|
|
846
|
+
addPath(fileRow.filePath);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return Array.from(paths);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Builds lightweight query terms for deterministic research scanning.
|
|
854
|
+
* Example: "error handling test suite" -> ["error","handling","test","suite"].
|
|
855
|
+
*/
|
|
856
|
+
buildResearchTerms() {
|
|
857
|
+
const query = this.context.analysis?.intent?.normalizedQuery ??
|
|
858
|
+
this.context.initContext?.userQuery ??
|
|
859
|
+
this.query;
|
|
860
|
+
const stopWords = new Set([
|
|
861
|
+
"the", "and", "for", "with", "from", "this", "that", "what", "how",
|
|
862
|
+
"is", "are", "was", "were", "can", "could", "should", "would", "into",
|
|
863
|
+
"about", "across", "repo", "codebase", "please",
|
|
864
|
+
]);
|
|
865
|
+
return Array.from(new Set(query
|
|
866
|
+
.toLowerCase()
|
|
867
|
+
.split(/[^a-z0-9_]+/g)
|
|
868
|
+
.filter(token => token.length >= 3 && !stopWords.has(token)))).slice(0, 10);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Collects research candidate paths from selected, candidate, related, and working files.
|
|
872
|
+
* Example: selected files are prioritized before broader related file pool.
|
|
873
|
+
*/
|
|
874
|
+
collectResearchPaths(maxPaths) {
|
|
875
|
+
const focus = this.context.analysis?.focus;
|
|
876
|
+
const workingPaths = (this.context.workingFiles ?? []).map(file => file.path);
|
|
877
|
+
const related = this.context.initContext?.relatedFiles ?? [];
|
|
878
|
+
const combined = [
|
|
879
|
+
...(focus?.selectedFiles ?? []),
|
|
880
|
+
...(focus?.candidateFiles ?? []),
|
|
881
|
+
...workingPaths,
|
|
882
|
+
...related,
|
|
883
|
+
];
|
|
884
|
+
const unique = Array.from(new Set(combined));
|
|
885
|
+
return unique
|
|
886
|
+
.filter(filePath => !filePath.startsWith("__research__/") && fs.existsSync(filePath))
|
|
887
|
+
.slice(0, maxPaths);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Reads a bounded corpus from candidate paths.
|
|
891
|
+
* Example: read first 12 files, max 12k chars per file, skipping binary payloads.
|
|
892
|
+
*/
|
|
893
|
+
loadResearchCorpus(filePaths, maxFiles, maxCharsPerFile) {
|
|
894
|
+
const corpus = [];
|
|
895
|
+
for (const filePath of filePaths.slice(0, maxFiles)) {
|
|
896
|
+
try {
|
|
897
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
898
|
+
if (raw.includes("\u0000"))
|
|
899
|
+
continue;
|
|
900
|
+
const content = raw.slice(0, maxCharsPerFile);
|
|
901
|
+
corpus.push({
|
|
902
|
+
path: filePath,
|
|
903
|
+
content,
|
|
904
|
+
lineCount: content.split("\n").length,
|
|
905
|
+
charCount: content.length,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// Ignore unreadable files and continue.
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return corpus;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Counts regex matches safely.
|
|
916
|
+
* Example: countRegex(code, /import/g) -> number of import occurrences.
|
|
917
|
+
*/
|
|
918
|
+
countRegex(content, pattern) {
|
|
919
|
+
const source = pattern.source;
|
|
920
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
921
|
+
const re = new RegExp(source, flags);
|
|
922
|
+
return Array.from(content.matchAll(re)).length;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Computes per-term match counts for a file body.
|
|
926
|
+
* Example: terms ["error","test"] -> { error: 4, test: 2 }.
|
|
927
|
+
*/
|
|
928
|
+
computeTermHits(content, terms) {
|
|
929
|
+
const hits = {};
|
|
930
|
+
for (const term of terms) {
|
|
931
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
932
|
+
const count = this.countRegex(content, new RegExp(`\\b${escaped}\\b`, "gi"));
|
|
933
|
+
if (count > 0) {
|
|
934
|
+
hits[term] = count;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return hits;
|
|
938
|
+
}
|
|
312
939
|
/* ───────────── step executor ───────────── */
|
|
313
940
|
/**
|
|
314
941
|
* Executes a single step using its corresponding module.
|
|
@@ -344,13 +971,7 @@ export class MainAgent {
|
|
|
344
971
|
throw err;
|
|
345
972
|
}
|
|
346
973
|
}
|
|
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 ───────────── */
|
|
974
|
+
/* ───────────── extracted from runSearch ───────────── */
|
|
354
975
|
resolveInitialRetrievalQueries() {
|
|
355
976
|
const rawUserQuery = this.context.initContext?.userQuery ?? this.query;
|
|
356
977
|
const retrievalQuery = this.context.analysis?.intent?.normalizedQuery?.trim() || rawUserQuery;
|
|
@@ -391,6 +1012,8 @@ export class MainAgent {
|
|
|
391
1012
|
};
|
|
392
1013
|
}
|
|
393
1014
|
mergeSeededInitialContext(rawUserQuery, seededContext) {
|
|
1015
|
+
// Merge retrieval seed into initContext without losing previously discovered files.
|
|
1016
|
+
// Example: keep old relatedFiles and append newly seeded files from buildLightContext.
|
|
394
1017
|
const existingInit = this.context.initContext ?? { userQuery: rawUserQuery };
|
|
395
1018
|
const seededInit = seededContext.initContext;
|
|
396
1019
|
const mergedRelatedFiles = Array.from(new Set([
|
|
@@ -419,6 +1042,8 @@ export class MainAgent {
|
|
|
419
1042
|
return mergedRelatedFiles.length;
|
|
420
1043
|
}
|
|
421
1044
|
applyDeterministicPreGroundingPrefilter(retrievalQuery) {
|
|
1045
|
+
// Rank and cap retrieval candidates before grounding to reduce noisy evidence passes.
|
|
1046
|
+
// Example: explicit filename anchors are always kept even if BM25 score is low.
|
|
422
1047
|
const init = this.context.initContext;
|
|
423
1048
|
if (!init?.relatedFiles?.length)
|
|
424
1049
|
return { before: 0, after: 0 };
|
|
@@ -458,6 +1083,184 @@ export class MainAgent {
|
|
|
458
1083
|
});
|
|
459
1084
|
return { before, after: init.relatedFiles.length };
|
|
460
1085
|
}
|
|
1086
|
+
injectWellKnownRepoFiles(currentCount) {
|
|
1087
|
+
// Add high-signal repo root files when scope is broad or retrieval is sparse.
|
|
1088
|
+
// Example: on repo-wide queries, include package.json/README if present.
|
|
1089
|
+
const init = this.context.initContext;
|
|
1090
|
+
if (!init)
|
|
1091
|
+
return { added: 0, reason: "none" };
|
|
1092
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1093
|
+
const shouldInjectByScope = scope === "repo-wide";
|
|
1094
|
+
const shouldInjectByFallback = currentCount < 3;
|
|
1095
|
+
if (!shouldInjectByScope && !shouldInjectByFallback) {
|
|
1096
|
+
return { added: 0, reason: "none" };
|
|
1097
|
+
}
|
|
1098
|
+
const reason = shouldInjectByScope ? "scope" : "fallback";
|
|
1099
|
+
const candidates = WELL_KNOWN_REPO_FILE_BASENAMES
|
|
1100
|
+
.map(fileName => path.join(process.cwd(), fileName))
|
|
1101
|
+
.filter(filePath => fs.existsSync(filePath))
|
|
1102
|
+
.slice(0, MAX_WELL_KNOWN_REPO_FILES);
|
|
1103
|
+
if (candidates.length === 0)
|
|
1104
|
+
return { added: 0, reason };
|
|
1105
|
+
const existing = new Set(init.relatedFiles ?? []);
|
|
1106
|
+
let added = 0;
|
|
1107
|
+
for (const filePath of candidates) {
|
|
1108
|
+
if (existing.has(filePath))
|
|
1109
|
+
continue;
|
|
1110
|
+
existing.add(filePath);
|
|
1111
|
+
added++;
|
|
1112
|
+
}
|
|
1113
|
+
init.relatedFiles = Array.from(existing);
|
|
1114
|
+
return { added, reason };
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Captures focus sizes before/after a verify wave for growth tracking.
|
|
1118
|
+
* Example: selected=3,candidate=7 before wave; selected=4,candidate=9 after wave.
|
|
1119
|
+
*/
|
|
1120
|
+
captureVerifyFocusSnapshot() {
|
|
1121
|
+
return {
|
|
1122
|
+
selected: this.context.analysis?.focus?.selectedFiles?.length ?? 0,
|
|
1123
|
+
candidate: this.context.analysis?.focus?.candidateFiles?.length ?? 0,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Logs selected/candidate deltas for a verify wave and returns whether focus grew.
|
|
1128
|
+
* Example: selected 3->4 (+1), candidate 7->9 (+2) => growth=true.
|
|
1129
|
+
*/
|
|
1130
|
+
logVerifyFocusDelta(before, after) {
|
|
1131
|
+
const selectedDelta = after.selected - before.selected;
|
|
1132
|
+
const candidateDelta = after.candidate - before.candidate;
|
|
1133
|
+
this.logLine("ANALYSIS", "groundingDelta", undefined, `selected ${before.selected}->${after.selected} (${selectedDelta >= 0 ? "+" : ""}${selectedDelta}), candidate ${before.candidate}->${after.candidate} (${candidateDelta >= 0 ? "+" : ""}${candidateDelta})`);
|
|
1134
|
+
return selectedDelta > 0 || candidateDelta > 0;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Stops verify loop when focus has not grown for too many consecutive waves.
|
|
1138
|
+
* Example: 2 stagnant waves in a row => stop early to avoid useless loops.
|
|
1139
|
+
*/
|
|
1140
|
+
shouldStopVerifyForSaturation(stagnantWaves, maxStagnantWaves) {
|
|
1141
|
+
if (stagnantWaves < maxStagnantWaves)
|
|
1142
|
+
return false;
|
|
1143
|
+
this.logLine("ANALYSIS", "groundingSaturated", undefined, `No focus growth for ${stagnantWaves} consecutive wave(s); stopping early`);
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Drops missing files from retrieval/focus sets to avoid verifier ENOENT noise.
|
|
1148
|
+
* Example: if DB points to deleted explainModule.ts, remove it before evidence pass.
|
|
1149
|
+
*/
|
|
1150
|
+
pruneMissingVerifyPaths() {
|
|
1151
|
+
const init = this.context.initContext;
|
|
1152
|
+
const focus = this.context.analysis?.focus;
|
|
1153
|
+
if (!init && !focus)
|
|
1154
|
+
return;
|
|
1155
|
+
const existsOrResearch = (filePath) => {
|
|
1156
|
+
if (filePath.startsWith("__research__/"))
|
|
1157
|
+
return true;
|
|
1158
|
+
return fs.existsSync(filePath);
|
|
1159
|
+
};
|
|
1160
|
+
let removedRelated = 0;
|
|
1161
|
+
let removedSelected = 0;
|
|
1162
|
+
let removedCandidate = 0;
|
|
1163
|
+
if (init?.relatedFiles?.length) {
|
|
1164
|
+
const before = init.relatedFiles.length;
|
|
1165
|
+
init.relatedFiles = init.relatedFiles.filter(existsOrResearch);
|
|
1166
|
+
removedRelated = before - init.relatedFiles.length;
|
|
1167
|
+
if (removedRelated > 0 && init.relatedFileScores) {
|
|
1168
|
+
init.relatedFileScores = Object.fromEntries(Object.entries(init.relatedFileScores).filter(([filePath]) => init.relatedFiles?.includes(filePath)));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (focus?.selectedFiles?.length) {
|
|
1172
|
+
const before = focus.selectedFiles.length;
|
|
1173
|
+
focus.selectedFiles = focus.selectedFiles.filter(existsOrResearch);
|
|
1174
|
+
removedSelected = before - focus.selectedFiles.length;
|
|
1175
|
+
}
|
|
1176
|
+
if (focus?.candidateFiles?.length) {
|
|
1177
|
+
const before = focus.candidateFiles.length;
|
|
1178
|
+
focus.candidateFiles = focus.candidateFiles.filter(existsOrResearch);
|
|
1179
|
+
removedCandidate = before - focus.candidateFiles.length;
|
|
1180
|
+
}
|
|
1181
|
+
if (removedRelated + removedSelected + removedCandidate > 0) {
|
|
1182
|
+
this.logLine("ANALYSIS", "verifyPruneMissing", undefined, `removed related=${removedRelated}, selected=${removedSelected}, candidate=${removedCandidate}`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Route-aware grounding budget.
|
|
1187
|
+
* Example: repo-wide + needs-info => allow up to 4 verification waves.
|
|
1188
|
+
*/
|
|
1189
|
+
getGroundingWaveBudget() {
|
|
1190
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1191
|
+
const decision = this.context.analysis?.routingDecision?.decision ?? "has-info";
|
|
1192
|
+
const allowSearch = this.context.analysis?.routingDecision?.allowSearch ?? true;
|
|
1193
|
+
let budget = 2;
|
|
1194
|
+
if (!allowSearch || scope === "none")
|
|
1195
|
+
budget = 1;
|
|
1196
|
+
else if (scope === "single-file" && decision === "has-info")
|
|
1197
|
+
budget = 2;
|
|
1198
|
+
else if (scope === "multi-file")
|
|
1199
|
+
budget = 3;
|
|
1200
|
+
else if (scope === "repo-wide" && decision === "needs-info")
|
|
1201
|
+
budget = 4;
|
|
1202
|
+
this.logLine("ANALYSIS", "groundingBudget", undefined, `scope=${scope}, decision=${decision}, search=${allowSearch}, maxWaves=${budget}`);
|
|
1203
|
+
return budget;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Dynamic task-step cap by route complexity.
|
|
1207
|
+
* Example: research-required lanes get 10 steps instead of 5.
|
|
1208
|
+
*/
|
|
1209
|
+
getTaskStepBudget() {
|
|
1210
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1211
|
+
if (this.canExecuteRoute("research"))
|
|
1212
|
+
return 10;
|
|
1213
|
+
if (scope === "multi-file")
|
|
1214
|
+
return 7;
|
|
1215
|
+
if (scope === "single-file")
|
|
1216
|
+
return 5;
|
|
1217
|
+
return 6;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Blocks execution if repo-wide complex tasks lack minimum research signal.
|
|
1221
|
+
* Example: require at least two analyzed files plus one understanding signal.
|
|
1222
|
+
*/
|
|
1223
|
+
isResearchGateSatisfied() {
|
|
1224
|
+
if (!this.canExecuteRoute("research"))
|
|
1225
|
+
return true;
|
|
1226
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1227
|
+
const researchPlanCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" && s.action.startsWith("research-")).length ?? 0;
|
|
1228
|
+
const pendingResearchCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" &&
|
|
1229
|
+
s.action.startsWith("research-") &&
|
|
1230
|
+
s.status !== "completed").length ?? 0;
|
|
1231
|
+
const requiredResearchSteps = scope === "repo-wide"
|
|
1232
|
+
? 4
|
|
1233
|
+
: scope === "multi-file"
|
|
1234
|
+
? 3
|
|
1235
|
+
: 1;
|
|
1236
|
+
const hasResearchPlan = researchPlanCount >= requiredResearchSteps;
|
|
1237
|
+
if (!hasResearchPlan) {
|
|
1238
|
+
this.context.task.status = "deferred";
|
|
1239
|
+
this.context.task.reason =
|
|
1240
|
+
`Research phase required before execution ` +
|
|
1241
|
+
`(scope=${scope}, researchSteps=${researchPlanCount}, required=${requiredResearchSteps})`;
|
|
1242
|
+
this.persistTaskDataForRun();
|
|
1243
|
+
this.logLine("TASK", "Research gate blocked work loop", undefined, this.context.task.reason, { highlight: true });
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
if (pendingResearchCount > 0) {
|
|
1247
|
+
this.logLine("TASK", "Research gate queued", undefined, `researchSteps=${researchPlanCount}, pendingResearch=${pendingResearchCount}`);
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
const understanding = this.context.analysis?.understanding;
|
|
1251
|
+
const understandingSignals = (understanding?.assumptions?.length ?? 0) +
|
|
1252
|
+
(understanding?.constraints?.length ?? 0) +
|
|
1253
|
+
(understanding?.risks?.length ?? 0);
|
|
1254
|
+
if (understandingSignals > 0) {
|
|
1255
|
+
this.logLine("TASK", "Research gate passed", undefined, `researchSteps=${researchPlanCount}, understandingSignals=${understandingSignals}`);
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
this.context.task.status = "deferred";
|
|
1259
|
+
this.context.task.reason = `Research completed but produced insufficient understanding signals (${understandingSignals}).`;
|
|
1260
|
+
this.persistTaskDataForRun();
|
|
1261
|
+
this.logLine("TASK", "Research gate blocked work loop", undefined, this.context.task.reason, { highlight: true });
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
461
1264
|
/* ───────────── extracted from runWorkLoop ───────────── */
|
|
462
1265
|
isWorkLoopReady() {
|
|
463
1266
|
const readinessDecision = this.context.analysis?.readiness?.decision;
|
|
@@ -495,28 +1298,43 @@ export class MainAgent {
|
|
|
495
1298
|
taskStep.stepIndex = stepCount;
|
|
496
1299
|
taskStep.status = "pending";
|
|
497
1300
|
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
498
|
-
this.
|
|
1301
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1302
|
+
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
|
|
499
1303
|
taskStep.startTime = Date.now();
|
|
500
1304
|
persistTaskStepStart(taskStep, getDbForRepo());
|
|
501
1305
|
}
|
|
502
1306
|
finishTaskStep(taskStep, stepCount, stepAction) {
|
|
1307
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
503
1308
|
taskStep.endTime = Date.now();
|
|
504
1309
|
if (stepAction === "complete") {
|
|
505
1310
|
taskStep.status = "completed";
|
|
506
1311
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
507
|
-
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined,
|
|
1312
|
+
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, displayPath, { highlight: false });
|
|
508
1313
|
return;
|
|
509
1314
|
}
|
|
510
1315
|
taskStep.status = "pending";
|
|
511
1316
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
512
|
-
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined,
|
|
1317
|
+
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, displayPath);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Normalizes internal pseudo-paths for user-facing step logs.
|
|
1321
|
+
* Example: "__research__/symbol-trace" -> "research/symbol-trace".
|
|
1322
|
+
*/
|
|
1323
|
+
formatTaskStepDisplayPath(filePath) {
|
|
1324
|
+
return filePath.startsWith("__research__/")
|
|
1325
|
+
? filePath.replace("__research__/", "research/")
|
|
1326
|
+
: filePath;
|
|
513
1327
|
}
|
|
514
1328
|
/* ───────────── execution gates ───────────── */
|
|
515
1329
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
|
|
1330
|
+
* Gate model:
|
|
1331
|
+
* 1) Phase + scope gates decide coarse permissions (what broad work is allowed).
|
|
1332
|
+
* 2) Route gate decides finer sub-decisions within those allowed areas (what to do next).
|
|
1333
|
+
*/
|
|
1334
|
+
/**
|
|
1335
|
+
* Gate 1: Is this kind of work allowed at all?
|
|
1336
|
+
* Plain meaning: checks capability rules (e.g. read-only vs file-writing).
|
|
1337
|
+
* Example: for docs-only mode, analysis/planning are blocked, and writes are limited.
|
|
520
1338
|
*/
|
|
521
1339
|
canExecutePhase(phase) {
|
|
522
1340
|
const constraints = this.context.executionControl?.constraints;
|
|
@@ -536,10 +1354,9 @@ export class MainAgent {
|
|
|
536
1354
|
}
|
|
537
1355
|
/* ───────────── scope gates ───────────── */
|
|
538
1356
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* @returns True if the phase can be executed, false otherwise.
|
|
1357
|
+
* Gate 2: Is this work allowed for the current scope size?
|
|
1358
|
+
* Plain meaning: checks scope rules (none/single/multi/repo-wide).
|
|
1359
|
+
* Example: if scope is "analysis", only analysis/planning run and transform/write are blocked.
|
|
543
1360
|
*/
|
|
544
1361
|
canExecuteScope(phase) {
|
|
545
1362
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
@@ -553,6 +1370,24 @@ export class MainAgent {
|
|
|
553
1370
|
}
|
|
554
1371
|
return allowed;
|
|
555
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Gate 3: Does this request path want this action right now?
|
|
1375
|
+
* Plain meaning: checks route-specific intent from routingDecision.
|
|
1376
|
+
* Example: search expansion is skipped when routing says allowSearch=false.
|
|
1377
|
+
*/
|
|
1378
|
+
canExecuteRoute(action) {
|
|
1379
|
+
const routing = this.context.analysis?.routingDecision;
|
|
1380
|
+
switch (action) {
|
|
1381
|
+
case "search-expand":
|
|
1382
|
+
return routing?.allowSearch ?? true;
|
|
1383
|
+
case "transform":
|
|
1384
|
+
return routing?.allowTransform ?? true;
|
|
1385
|
+
case "research":
|
|
1386
|
+
return routing?.allowResearch ?? false;
|
|
1387
|
+
default:
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
556
1391
|
/* ----------------------------------- */
|
|
557
1392
|
/* ------------- helpers ------------- */
|
|
558
1393
|
/* ----------------------------------- */
|