scai 0.1.171 → 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 +1099 -77
- package/dist/agents/evidenceVerifierStep.js +150 -18
- package/dist/agents/fileCheckStep.js +19 -8
- package/dist/agents/iterationFileSelector.js +18 -45
- package/dist/agents/readinessGateStep.js +45 -14
- package/dist/agents/reasonNextStep.js +27 -2
- package/dist/agents/reasonNextTaskStep.js +126 -4
- package/dist/agents/researchPlanGenStep.js +123 -0
- package/dist/agents/resolveExecutionModeStep.js +27 -7
- package/dist/agents/routingDecisionStep.js +75 -0
- package/dist/agents/scopeClassificationStep.js +11 -3
- package/dist/agents/selectRelevantSourcesStep.js +1 -2
- package/dist/agents/understandIntentStep.js +102 -6
- package/dist/commands/AskCmd.js +22 -46
- package/dist/commands/factory.js +2 -2
- package/dist/db/fileIndex.js +638 -18
- package/dist/fileRules/stopWords.js +10 -0
- package/dist/fileRules/wellKnownRepoFiles.js +8 -0
- package/dist/index.js +9 -93
- package/dist/lib/generate.js +26 -15
- package/dist/pipeline/modules/fileSearchModule.js +10 -0
- 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/buildContextualPrompt.js +18 -11
- package/dist/utils/extractFileReferences.js +13 -0
- package/dist/utils/promptLogHelper.js +15 -4
- package/dist/utils/runQueryWithDaemonControl.js +26 -0
- package/package.json +1 -1
- package/dist/pipeline/modules/explainModule.js +0 -169
package/dist/agents/MainAgent.js
CHANGED
|
@@ -10,17 +10,27 @@ import { fileCheckStep } from "./fileCheckStep.js";
|
|
|
10
10
|
import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
|
|
11
11
|
import { readinessGateStep } from "./readinessGateStep.js";
|
|
12
12
|
import { scopeClassificationStep } from "./scopeClassificationStep.js";
|
|
13
|
+
import { routingDecisionStep } from "./routingDecisionStep.js";
|
|
13
14
|
import { evidenceVerifierStep } from "./evidenceVerifierStep.js";
|
|
14
15
|
import { validateChangesStep } from './validateChangesStep.js';
|
|
15
16
|
import { reasonNextTaskStep } from './reasonNextTaskStep.js';
|
|
16
17
|
import { collaboratorStep } from './collaboratorStep.js';
|
|
17
18
|
import { integrateFeedbackStep } from './integrateFeedbackStep.js';
|
|
19
|
+
import { researchPlanGenStep } from "./researchPlanGenStep.js";
|
|
18
20
|
import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
|
|
19
21
|
import { iterationFileSelector } from "./iterationFileSelector.js";
|
|
20
22
|
import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
|
|
21
23
|
import { reasonNextStep } from "./reasonNextStep.js";
|
|
24
|
+
import { buildLightContext } from "../utils/buildContextualPrompt.js";
|
|
25
|
+
import { semanticSearchFiles } from "../db/fileIndex.js";
|
|
26
|
+
import { NUM_TOPFILES, RELATED_FILES_LIMIT } from "../constants.js";
|
|
22
27
|
import { structuralPreloadStep } from "./structuralPreloadStep.js";
|
|
28
|
+
import { extractFileReferences } from "../utils/extractFileReferences.js";
|
|
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";
|
|
23
31
|
import chalk from "chalk";
|
|
32
|
+
import path from "path";
|
|
33
|
+
import fs from "fs";
|
|
24
34
|
/* ───────────────────────── registry ───────────────────────── */
|
|
25
35
|
const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
|
|
26
36
|
function resolveModuleForAction(action) {
|
|
@@ -49,7 +59,6 @@ export class MainAgent {
|
|
|
49
59
|
*/
|
|
50
60
|
constructor(context, ui) {
|
|
51
61
|
this.runCount = 0;
|
|
52
|
-
this.maxRuns = 2;
|
|
53
62
|
this.context = context;
|
|
54
63
|
this.query = context.initContext?.userQuery ?? "";
|
|
55
64
|
this.ui = ui;
|
|
@@ -60,8 +69,14 @@ export class MainAgent {
|
|
|
60
69
|
this.runCount = 0;
|
|
61
70
|
await this.runBoot();
|
|
62
71
|
await this.runScope();
|
|
63
|
-
await this.
|
|
64
|
-
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
|
+
}
|
|
65
80
|
await this.runFinalize();
|
|
66
81
|
}
|
|
67
82
|
finally {
|
|
@@ -72,7 +87,6 @@ export class MainAgent {
|
|
|
72
87
|
async runBoot() {
|
|
73
88
|
var _a;
|
|
74
89
|
await understandIntentStep.run({ context: this.context });
|
|
75
|
-
await resolveExecutionModeStep.run(this.context);
|
|
76
90
|
// Boot the task and get the real DB taskId
|
|
77
91
|
this.taskId = bootTaskForRepo(this.context, getDbForRepo(), (phase, step, ms, desc) => this.logLine(phase, step, ms, desc, { highlight: true }));
|
|
78
92
|
(_a = this.context).task || (_a.task = {
|
|
@@ -90,11 +104,51 @@ export class MainAgent {
|
|
|
90
104
|
/* ───────────── scope ───────────── */
|
|
91
105
|
async runScope() {
|
|
92
106
|
await scopeClassificationStep.run(this.context);
|
|
107
|
+
await resolveExecutionModeStep.run(this.context);
|
|
108
|
+
await routingDecisionStep.run(this.context);
|
|
109
|
+
const routing = this.context.analysis?.routingDecision;
|
|
110
|
+
if (routing) {
|
|
111
|
+
this.logLine("TASK", "Routing decision", undefined, `${routing.decision} | search=${routing.allowSearch} | research=${routing.allowResearch} | transform=${routing.allowTransform} | scopeLocked=${routing.scopeLocked}`);
|
|
112
|
+
}
|
|
93
113
|
this.logLine("TASK", "Scope classification complete");
|
|
94
114
|
}
|
|
95
|
-
|
|
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() {
|
|
121
|
+
const { rawUserQuery, retrievalQuery } = this.resolveInitialRetrievalQueries();
|
|
122
|
+
const t = this.startTimer();
|
|
123
|
+
try {
|
|
124
|
+
const results = await this.fetchInitialRetrievalResults(retrievalQuery);
|
|
125
|
+
const promptArgs = this.buildInitialRetrievalPromptArgs(results, retrievalQuery);
|
|
126
|
+
const seededContext = await buildLightContext(promptArgs);
|
|
127
|
+
const mergedRelatedCount = this.mergeSeededInitialContext(rawUserQuery, seededContext);
|
|
128
|
+
const prefilter = this.applyDeterministicPreGroundingPrefilter(retrievalQuery);
|
|
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})`);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
this.logLine("ANALYSIS", "initialRetrieval", t(), `failed: ${String(err)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
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() {
|
|
96
142
|
let ready = false;
|
|
97
|
-
|
|
143
|
+
const maxGroundingWaves = this.getGroundingWaveBudget();
|
|
144
|
+
let groundingWave = 0;
|
|
145
|
+
let stagnantWaves = 0;
|
|
146
|
+
const MAX_STAGNANT_WAVES = 2;
|
|
147
|
+
while (groundingWave < maxGroundingWaves) {
|
|
148
|
+
groundingWave++;
|
|
149
|
+
this.pruneMissingVerifyPaths();
|
|
150
|
+
this.logLine("ANALYSIS", "groundingWave", undefined, `wave ${groundingWave}/${maxGroundingWaves}`);
|
|
151
|
+
const beforeFocus = this.captureVerifyFocusSnapshot();
|
|
98
152
|
// ---------------- EVIDENCE PIPELINE ----------------
|
|
99
153
|
// -------- STRUCTURAL PRELOAD --------
|
|
100
154
|
const t0 = this.startTimer();
|
|
@@ -112,123 +166,269 @@ export class MainAgent {
|
|
|
112
166
|
// ---------------- READINESS GATE ----------------
|
|
113
167
|
const t4 = this.startTimer();
|
|
114
168
|
await readinessGateStep.run(this.context);
|
|
115
|
-
this.logLine("
|
|
169
|
+
this.logLine("ANALYSIS", "readinessGate", t4());
|
|
116
170
|
ready = this.context.analysis?.readiness?.decision === "ready";
|
|
117
171
|
if (ready) {
|
|
118
172
|
break;
|
|
119
173
|
}
|
|
120
174
|
// ---------------- INFORMATION ACQUISITION ----------------
|
|
121
|
-
|
|
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") &&
|
|
122
180
|
this.canExecuteScope("planning")) {
|
|
123
181
|
const t = this.startTimer();
|
|
124
182
|
await infoPlanGenStep.run(this.context);
|
|
125
183
|
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
126
|
-
// If we are about to execute a new info acquisition wave,
|
|
127
|
-
// wipe previous search results.
|
|
128
|
-
if (infoPlan.steps.length > 0 && this.context.initContext && this.context.analysis?.focus) {
|
|
129
|
-
this.context.initContext.relatedFiles = [];
|
|
130
|
-
this.context.analysis.focus.candidateFiles = [];
|
|
131
|
-
}
|
|
132
184
|
for (const step of infoPlan.steps) {
|
|
133
185
|
const stepIO = { query: this.query };
|
|
134
186
|
await this.executeStep(step, stepIO);
|
|
135
187
|
}
|
|
136
188
|
this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
|
|
137
189
|
}
|
|
138
|
-
this.
|
|
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;
|
|
139
195
|
this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
|
|
140
196
|
}
|
|
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().
|
|
141
202
|
}
|
|
142
|
-
/* ─────────────
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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++;
|
|
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"})`);
|
|
288
|
+
}
|
|
289
|
+
/* ───────────── plan ───────────── */
|
|
290
|
+
/**
|
|
291
|
+
* Seeds ordered execution task steps from selected files + research/verify artifacts.
|
|
292
|
+
* Example: prioritize files that are both selected and research-touched.
|
|
293
|
+
*/
|
|
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`);
|
|
147
369
|
}
|
|
148
370
|
/* ───────────── work loop ───────────── */
|
|
149
371
|
async runWorkLoop() {
|
|
150
|
-
|
|
151
|
-
const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
|
|
152
|
-
if (readinessDecision !== "ready") {
|
|
153
|
-
// ❌ Graceful fallback instead of throwing
|
|
154
|
-
this.context.task.status = "deferred";
|
|
155
|
-
this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
|
|
156
|
-
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
157
|
-
this.logLine("TASK", `Cannot start work loop — agent needs more evidence to safely proceed`, undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
|
|
372
|
+
if (this.context.task.status !== "active")
|
|
158
373
|
return;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new Error("runWorkLoop: missing task");
|
|
162
|
-
}
|
|
163
|
-
const MAX_TASK_STEPS = 5;
|
|
374
|
+
this.ensureTaskForWorkLoop();
|
|
375
|
+
const MAX_TASK_STEPS = this.getTaskStepBudget();
|
|
164
376
|
let stepCount = 0;
|
|
165
377
|
while (stepCount < MAX_TASK_STEPS &&
|
|
166
378
|
this.context.task.status === "active") {
|
|
167
|
-
await
|
|
168
|
-
const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
|
|
169
|
-
// 🟡 Pause for user clarification
|
|
379
|
+
const nextAction = await this.resolveNextTaskAction();
|
|
170
380
|
if (nextAction === "request-feedback") {
|
|
171
|
-
this.
|
|
172
|
-
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
381
|
+
this.persistTaskStatus("paused");
|
|
173
382
|
this.logLine("TASK", "Execution paused — awaiting user clarification", undefined, undefined, { highlight: false });
|
|
174
383
|
return;
|
|
175
384
|
}
|
|
176
|
-
// 🟢 Completed task
|
|
177
385
|
if (nextAction === "complete") {
|
|
178
|
-
this.
|
|
179
|
-
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
386
|
+
this.persistTaskStatus("completed");
|
|
180
387
|
this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
|
|
181
388
|
return;
|
|
182
389
|
}
|
|
183
390
|
const taskStep = await iterationFileSelector.run(this.context);
|
|
184
391
|
if (!taskStep) {
|
|
185
|
-
this.
|
|
186
|
-
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
392
|
+
this.persistTaskStatus("completed");
|
|
187
393
|
this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
|
|
188
394
|
return;
|
|
189
395
|
}
|
|
190
|
-
this.context.task.currentStep = taskStep;
|
|
191
396
|
stepCount++;
|
|
192
|
-
taskStep
|
|
193
|
-
taskStep.stepIndex = stepCount;
|
|
194
|
-
taskStep.status = "pending";
|
|
195
|
-
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
196
|
-
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
|
|
197
|
-
taskStep.startTime = Date.now();
|
|
198
|
-
persistTaskStepStart(taskStep, getDbForRepo());
|
|
397
|
+
this.startTaskStep(taskStep, stepCount);
|
|
199
398
|
// ---------------------------
|
|
200
399
|
// Step-level iterations
|
|
201
400
|
// ---------------------------
|
|
202
401
|
const stepAction = await this.runStepIterations(taskStep);
|
|
203
|
-
|
|
204
|
-
taskStep.status = "completed";
|
|
205
|
-
taskStep.endTime = Date.now();
|
|
206
|
-
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
207
|
-
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
taskStep.status = "pending";
|
|
211
|
-
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
212
|
-
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
|
|
213
|
-
}
|
|
402
|
+
this.finishTaskStep(taskStep, stepCount, stepAction);
|
|
214
403
|
}
|
|
215
404
|
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
216
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
|
+
}
|
|
217
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
|
+
*/
|
|
218
417
|
async runStepIterations(taskStep) {
|
|
219
418
|
const MAX_ITERATIONS = 5;
|
|
220
419
|
let loopCount = 0;
|
|
221
420
|
const getNextIterationAction = () => {
|
|
222
|
-
const nextAction =
|
|
223
|
-
if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? ""))
|
|
224
|
-
return "
|
|
421
|
+
const nextAction = taskStep.result?.stepReasoning?.nextAction;
|
|
422
|
+
if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? "")) {
|
|
423
|
+
return "continue";
|
|
424
|
+
}
|
|
225
425
|
return nextAction;
|
|
226
426
|
};
|
|
227
427
|
while (loopCount < MAX_ITERATIONS) {
|
|
228
428
|
this.runCount++;
|
|
229
429
|
loopCount++;
|
|
230
|
-
if (
|
|
231
|
-
|
|
430
|
+
if (taskStep.result?.stepReasoning)
|
|
431
|
+
taskStep.result.stepReasoning.nextAction = undefined;
|
|
232
432
|
await this.runWorkIteration(taskStep);
|
|
233
433
|
const nextAction = getNextIterationAction();
|
|
234
434
|
this.logLine("STEP-LOOP", `nextAction = ${nextAction}`);
|
|
@@ -236,13 +436,23 @@ export class MainAgent {
|
|
|
236
436
|
return "complete";
|
|
237
437
|
if (nextAction === "request-feedback")
|
|
238
438
|
return "request-feedback";
|
|
439
|
+
if (nextAction === "redo-step")
|
|
440
|
+
continue;
|
|
239
441
|
}
|
|
240
442
|
return "continue";
|
|
241
443
|
}
|
|
242
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
|
+
*/
|
|
243
449
|
async runWorkIteration(taskStep) {
|
|
244
450
|
if (!this.context.analysis)
|
|
245
451
|
this.context.analysis = {};
|
|
452
|
+
if (taskStep.action?.startsWith("research-")) {
|
|
453
|
+
await this.executeResearchTaskStep(taskStep);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
246
456
|
if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
|
|
247
457
|
const tAnalysis = this.startTimer();
|
|
248
458
|
await analysisPlanGenStep.run(this.context);
|
|
@@ -288,6 +498,375 @@ export class MainAgent {
|
|
|
288
498
|
await integrateFeedbackStep.run(this.context);
|
|
289
499
|
this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
|
|
290
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
|
+
}
|
|
291
870
|
/* ───────────── step executor ───────────── */
|
|
292
871
|
/**
|
|
293
872
|
* Executes a single step using its corresponding module.
|
|
@@ -307,20 +886,386 @@ export class MainAgent {
|
|
|
307
886
|
}
|
|
308
887
|
try {
|
|
309
888
|
this.ui.update(`Running step: ${step.action}`);
|
|
310
|
-
await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
|
|
311
|
-
|
|
889
|
+
const output = await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
|
|
890
|
+
const errors = Array.isArray(output.data?.errors)
|
|
891
|
+
? output.data.errors.filter((e) => typeof e === "string" && e.trim().length > 0)
|
|
892
|
+
: [];
|
|
893
|
+
if (errors.length > 0) {
|
|
894
|
+
const detail = errors.slice(0, 2).join(" | ");
|
|
895
|
+
this.logLine("EXECUTE", step.action, stop(), `completed with errors: ${detail}`);
|
|
896
|
+
console.error(`[${step.action}] ${errors.join(" | ")}`);
|
|
897
|
+
}
|
|
898
|
+
return output;
|
|
312
899
|
}
|
|
313
900
|
catch (err) {
|
|
314
901
|
this.logLine("EXECUTE", step.action, stop(), "failed");
|
|
315
902
|
throw err;
|
|
316
903
|
}
|
|
317
904
|
}
|
|
905
|
+
/* ───────────── extracted from runSearch ───────────── */
|
|
906
|
+
resolveInitialRetrievalQueries() {
|
|
907
|
+
const rawUserQuery = this.context.initContext?.userQuery ?? this.query;
|
|
908
|
+
const retrievalQuery = this.context.analysis?.intent?.normalizedQuery?.trim() || rawUserQuery;
|
|
909
|
+
return { rawUserQuery, retrievalQuery };
|
|
910
|
+
}
|
|
911
|
+
async fetchInitialRetrievalResults(retrievalQuery) {
|
|
912
|
+
return semanticSearchFiles(retrievalQuery, RELATED_FILES_LIMIT, this.context.analysis?.intent ?? {});
|
|
913
|
+
}
|
|
914
|
+
mapSearchResultToTopFile(result) {
|
|
915
|
+
return {
|
|
916
|
+
id: result.id,
|
|
917
|
+
path: result.path,
|
|
918
|
+
summary: result.summary ?? undefined,
|
|
919
|
+
bm25Score: result.bm25Score,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
mapSearchResultToRelatedFile(result) {
|
|
923
|
+
return {
|
|
924
|
+
id: result.id,
|
|
925
|
+
path: result.path,
|
|
926
|
+
summary: result.summary ?? undefined,
|
|
927
|
+
bm25Score: result.bm25Score,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
buildInitialRetrievalPromptArgs(results, retrievalQuery) {
|
|
931
|
+
const topFiles = results
|
|
932
|
+
.slice(0, NUM_TOPFILES)
|
|
933
|
+
.map(result => this.mapSearchResultToTopFile(result));
|
|
934
|
+
const relatedFiles = results
|
|
935
|
+
.slice(NUM_TOPFILES)
|
|
936
|
+
.map(result => this.mapSearchResultToRelatedFile(result));
|
|
937
|
+
const queryExpansionTerms = results.find(result => Array.isArray(result.queryExpansionTerms))?.queryExpansionTerms;
|
|
938
|
+
return {
|
|
939
|
+
topFiles,
|
|
940
|
+
relatedFiles,
|
|
941
|
+
query: retrievalQuery,
|
|
942
|
+
queryExpansionTerms,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
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.
|
|
948
|
+
const existingInit = this.context.initContext ?? { userQuery: rawUserQuery };
|
|
949
|
+
const seededInit = seededContext.initContext;
|
|
950
|
+
const mergedRelatedFiles = Array.from(new Set([
|
|
951
|
+
...(existingInit.relatedFiles ?? []),
|
|
952
|
+
...(seededInit?.relatedFiles ?? []),
|
|
953
|
+
]));
|
|
954
|
+
const mergedScores = {
|
|
955
|
+
...(existingInit.relatedFileScores ?? {}),
|
|
956
|
+
...(seededInit?.relatedFileScores ?? {}),
|
|
957
|
+
};
|
|
958
|
+
const mergedQueryExpansionTerms = Array.from(new Set([
|
|
959
|
+
...(existingInit.queryExpansionTerms ?? []),
|
|
960
|
+
...(seededInit?.queryExpansionTerms ?? []),
|
|
961
|
+
]));
|
|
962
|
+
this.context.initContext = {
|
|
963
|
+
...existingInit,
|
|
964
|
+
...(seededInit ?? {}),
|
|
965
|
+
userQuery: rawUserQuery,
|
|
966
|
+
relatedFiles: mergedRelatedFiles,
|
|
967
|
+
relatedFileScores: mergedScores,
|
|
968
|
+
queryExpansionTerms: mergedQueryExpansionTerms,
|
|
969
|
+
folderCapsules: (seededInit?.folderCapsules?.length
|
|
970
|
+
? seededInit.folderCapsules
|
|
971
|
+
: existingInit.folderCapsules) ?? [],
|
|
972
|
+
};
|
|
973
|
+
return mergedRelatedFiles.length;
|
|
974
|
+
}
|
|
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.
|
|
978
|
+
const init = this.context.initContext;
|
|
979
|
+
if (!init?.relatedFiles?.length)
|
|
980
|
+
return { before: 0, after: 0 };
|
|
981
|
+
const before = init.relatedFiles.length;
|
|
982
|
+
const scored = scoreCandidateFiles(init.relatedFiles, init.relatedFileScores ?? {}, retrievalQuery);
|
|
983
|
+
if (scored.length === 0)
|
|
984
|
+
return { before, after: before };
|
|
985
|
+
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
986
|
+
const baseKeepCount = scope === "single-file" ? 6 : 10;
|
|
987
|
+
const keepCount = Math.min(Math.max(3, baseKeepCount), scored.length);
|
|
988
|
+
const selected = new Set(scored
|
|
989
|
+
.slice(0, keepCount)
|
|
990
|
+
.map(item => item.filePath));
|
|
991
|
+
for (const item of scored) {
|
|
992
|
+
if (item.reasons.includes("exact-filename") || item.reasons.includes("path-anchor")) {
|
|
993
|
+
selected.add(item.filePath);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
init.relatedFiles = scored
|
|
997
|
+
.filter(item => selected.has(item.filePath))
|
|
998
|
+
.map(item => item.filePath);
|
|
999
|
+
init.relatedFileScores = Object.fromEntries(Object.entries(init.relatedFileScores ?? {})
|
|
1000
|
+
.filter(([filePath]) => selected.has(filePath)));
|
|
1001
|
+
logInputOutput("deterministicPreGroundingPrefilter", "output", {
|
|
1002
|
+
retrievalQuery,
|
|
1003
|
+
scope,
|
|
1004
|
+
before,
|
|
1005
|
+
after: init.relatedFiles.length,
|
|
1006
|
+
keepCount,
|
|
1007
|
+
files: scored.map(item => ({
|
|
1008
|
+
file: item.filePath,
|
|
1009
|
+
score: Number(item.score.toFixed(2)),
|
|
1010
|
+
bm25Raw: item.bm25Raw,
|
|
1011
|
+
kept: selected.has(item.filePath),
|
|
1012
|
+
reasons: item.reasons,
|
|
1013
|
+
})),
|
|
1014
|
+
});
|
|
1015
|
+
return { before, after: init.relatedFiles.length };
|
|
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
|
+
}
|
|
1195
|
+
/* ───────────── extracted from runWorkLoop ───────────── */
|
|
1196
|
+
isWorkLoopReady() {
|
|
1197
|
+
const readinessDecision = this.context.analysis?.readiness?.decision;
|
|
1198
|
+
const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
|
|
1199
|
+
if (readinessDecision === "ready")
|
|
1200
|
+
return true;
|
|
1201
|
+
this.context.task.status = "deferred";
|
|
1202
|
+
this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
|
|
1203
|
+
this.persistTaskDataForRun();
|
|
1204
|
+
this.logLine("TASK", "Cannot start work loop — agent needs more evidence to safely proceed", undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
ensureTaskForWorkLoop() {
|
|
1208
|
+
if (!this.context.task) {
|
|
1209
|
+
throw new Error("runWorkLoop: missing task");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async resolveNextTaskAction() {
|
|
1213
|
+
await reasonNextTaskStep.run(this.context);
|
|
1214
|
+
const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
|
|
1215
|
+
if (nextAction === "request-feedback" || nextAction === "complete")
|
|
1216
|
+
return nextAction;
|
|
1217
|
+
return "continue";
|
|
1218
|
+
}
|
|
1219
|
+
persistTaskDataForRun() {
|
|
1220
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
1221
|
+
}
|
|
1222
|
+
persistTaskStatus(status) {
|
|
1223
|
+
this.context.task.status = status;
|
|
1224
|
+
this.persistTaskDataForRun();
|
|
1225
|
+
}
|
|
1226
|
+
startTaskStep(taskStep, stepCount) {
|
|
1227
|
+
this.context.task.currentStep = taskStep;
|
|
1228
|
+
taskStep.taskId = this.taskId;
|
|
1229
|
+
taskStep.stepIndex = stepCount;
|
|
1230
|
+
taskStep.status = "pending";
|
|
1231
|
+
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
1232
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1233
|
+
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
|
|
1234
|
+
taskStep.startTime = Date.now();
|
|
1235
|
+
persistTaskStepStart(taskStep, getDbForRepo());
|
|
1236
|
+
}
|
|
1237
|
+
finishTaskStep(taskStep, stepCount, stepAction) {
|
|
1238
|
+
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1239
|
+
taskStep.endTime = Date.now();
|
|
1240
|
+
if (stepAction === "complete") {
|
|
1241
|
+
taskStep.status = "completed";
|
|
1242
|
+
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
1243
|
+
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, displayPath, { highlight: false });
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
taskStep.status = "pending";
|
|
1247
|
+
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
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;
|
|
1258
|
+
}
|
|
318
1259
|
/* ───────────── execution gates ───────────── */
|
|
319
1260
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
|
|
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.
|
|
324
1269
|
*/
|
|
325
1270
|
canExecutePhase(phase) {
|
|
326
1271
|
const constraints = this.context.executionControl?.constraints;
|
|
@@ -340,10 +1285,9 @@ export class MainAgent {
|
|
|
340
1285
|
}
|
|
341
1286
|
/* ───────────── scope gates ───────────── */
|
|
342
1287
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* @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.
|
|
347
1291
|
*/
|
|
348
1292
|
canExecuteScope(phase) {
|
|
349
1293
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
@@ -357,6 +1301,24 @@ export class MainAgent {
|
|
|
357
1301
|
}
|
|
358
1302
|
return allowed;
|
|
359
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
|
+
}
|
|
360
1322
|
/* ----------------------------------- */
|
|
361
1323
|
/* ------------- helpers ------------- */
|
|
362
1324
|
/* ----------------------------------- */
|
|
@@ -403,6 +1365,66 @@ export class MainAgent {
|
|
|
403
1365
|
});
|
|
404
1366
|
}
|
|
405
1367
|
}
|
|
1368
|
+
function scoreCandidateFiles(filePaths, relatedFileScores, retrievalQuery) {
|
|
1369
|
+
const explicitRefs = extractFileReferences(retrievalQuery, { lowercase: true });
|
|
1370
|
+
const explicitBasenames = new Set(explicitRefs.map(ref => path.basename(ref)));
|
|
1371
|
+
const symbolAnchors = extractSymbolAnchors(retrievalQuery);
|
|
1372
|
+
const scored = filePaths.map(filePath => {
|
|
1373
|
+
const fileLower = filePath.toLowerCase();
|
|
1374
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
1375
|
+
let score = 0;
|
|
1376
|
+
const reasons = [];
|
|
1377
|
+
if (explicitBasenames.has(basename)) {
|
|
1378
|
+
score += 100;
|
|
1379
|
+
reasons.push("exact-filename");
|
|
1380
|
+
}
|
|
1381
|
+
if (explicitRefs.some(ref => fileLower.includes(ref))) {
|
|
1382
|
+
score += 60;
|
|
1383
|
+
reasons.push("path-anchor");
|
|
1384
|
+
}
|
|
1385
|
+
for (const anchor of symbolAnchors) {
|
|
1386
|
+
if (basename.includes(anchor)) {
|
|
1387
|
+
score += 40;
|
|
1388
|
+
reasons.push(`symbol:${anchor}:filename`);
|
|
1389
|
+
}
|
|
1390
|
+
else if (fileLower.includes(anchor)) {
|
|
1391
|
+
score += 20;
|
|
1392
|
+
reasons.push(`symbol:${anchor}:path`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const bm25Raw = relatedFileScores[filePath];
|
|
1396
|
+
if (typeof bm25Raw === "number" && Number.isFinite(bm25Raw)) {
|
|
1397
|
+
const prior = Math.max(0, Math.min(20, -bm25Raw));
|
|
1398
|
+
score += prior;
|
|
1399
|
+
reasons.push(`bm25-prior:${prior.toFixed(2)}`);
|
|
1400
|
+
}
|
|
1401
|
+
return { filePath, score, bm25Raw, reasons };
|
|
1402
|
+
});
|
|
1403
|
+
return scored.sort((a, b) => {
|
|
1404
|
+
if (b.score !== a.score)
|
|
1405
|
+
return b.score - a.score;
|
|
1406
|
+
const aBm25 = typeof a.bm25Raw === "number" ? a.bm25Raw : Number.POSITIVE_INFINITY;
|
|
1407
|
+
const bBm25 = typeof b.bm25Raw === "number" ? b.bm25Raw : Number.POSITIVE_INFINITY;
|
|
1408
|
+
return aBm25 - bBm25;
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function extractSymbolAnchors(query) {
|
|
1412
|
+
const matches = query.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) ?? [];
|
|
1413
|
+
const out = new Set();
|
|
1414
|
+
for (const token of matches) {
|
|
1415
|
+
const lowered = token.toLowerCase();
|
|
1416
|
+
const looksLikeSymbol = /[A-Z]/.test(token) ||
|
|
1417
|
+
token.includes("_") ||
|
|
1418
|
+
token.endsWith("Step") ||
|
|
1419
|
+
token.endsWith("Module");
|
|
1420
|
+
if (!looksLikeSymbol)
|
|
1421
|
+
continue;
|
|
1422
|
+
if (PREFILTER_STOP_WORDS.has(lowered))
|
|
1423
|
+
continue;
|
|
1424
|
+
out.add(lowered);
|
|
1425
|
+
}
|
|
1426
|
+
return Array.from(out);
|
|
1427
|
+
}
|
|
406
1428
|
// All helper functions (persistTaskData, bootTaskForRepo, persistTaskStep*) remain unchanged
|
|
407
1429
|
/* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
|
|
408
1430
|
/**
|