scai 0.1.174 → 0.1.176
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 +114 -77
- package/dist/agents/fileCheckStep.js +13 -5
- package/dist/agents/guardPolicy.js +43 -0
- package/dist/fileRules/ignoredPaths.js +17 -0
- package/dist/pipeline/modules/fileSearchModule.js +8 -6
- package/dist/utils/resolveTargetsToFiles.js +5 -0
- package/package.json +1 -1
package/dist/agents/MainAgent.js
CHANGED
|
@@ -28,6 +28,8 @@ import { structuralPreloadStep } from "./structuralPreloadStep.js";
|
|
|
28
28
|
import { extractFileReferences } from "../utils/extractFileReferences.js";
|
|
29
29
|
import { PREFILTER_STOP_WORDS } from "../fileRules/stopWords.js";
|
|
30
30
|
import { MAX_WELL_KNOWN_REPO_FILES, WELL_KNOWN_REPO_FILE_BASENAMES } from "../fileRules/wellKnownRepoFiles.js";
|
|
31
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
32
|
+
import { canExecutePhase, canExecuteRoute, canExecuteScope } from "./guardPolicy.js";
|
|
31
33
|
import chalk from "chalk";
|
|
32
34
|
import path from "path";
|
|
33
35
|
import fs from "fs";
|
|
@@ -172,12 +174,12 @@ export class MainAgent {
|
|
|
172
174
|
break;
|
|
173
175
|
}
|
|
174
176
|
// ---------------- INFORMATION ACQUISITION ----------------
|
|
175
|
-
const canRouteSearchExpansion = this.
|
|
177
|
+
const canRouteSearchExpansion = canExecuteRoute(this.context, "search-expand");
|
|
176
178
|
if (!canRouteSearchExpansion) {
|
|
177
179
|
this.logLine("PLAN", "infoPlanGen", undefined, "skipped (routing disallows search expansion)", { highlight: false });
|
|
178
180
|
}
|
|
179
|
-
else if (this.
|
|
180
|
-
this.
|
|
181
|
+
else if (canExecutePhase(this.context, "planning") &&
|
|
182
|
+
canExecuteScope(this.context, "planning")) {
|
|
181
183
|
const t = this.startTimer();
|
|
182
184
|
await infoPlanGenStep.run(this.context);
|
|
183
185
|
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
@@ -208,7 +210,7 @@ export class MainAgent {
|
|
|
208
210
|
*/
|
|
209
211
|
async runResearch() {
|
|
210
212
|
var _a, _b;
|
|
211
|
-
if (!this.
|
|
213
|
+
if (!canExecuteRoute(this.context, "research")) {
|
|
212
214
|
this.logLine("RESEARCH", "taskStepSeed", undefined, "skipped (route disallows research)");
|
|
213
215
|
return;
|
|
214
216
|
}
|
|
@@ -298,7 +300,7 @@ export class MainAgent {
|
|
|
298
300
|
var _a, _b;
|
|
299
301
|
if (!this.context.task)
|
|
300
302
|
return;
|
|
301
|
-
if (!this.
|
|
303
|
+
if (!canExecutePhase(this.context, "planning") || !canExecuteScope(this.context, "planning"))
|
|
302
304
|
return;
|
|
303
305
|
(_a = this.context).analysis || (_a.analysis = {});
|
|
304
306
|
(_b = this.context.task).taskSteps || (_b.taskSteps = []);
|
|
@@ -347,6 +349,7 @@ export class MainAgent {
|
|
|
347
349
|
.filter(filePath => !!filePath && !filePath.startsWith("__research__/") && fs.existsSync(filePath))
|
|
348
350
|
.sort((a, b) => rankPath(a) - rankPath(b))
|
|
349
351
|
.slice(0, 16);
|
|
352
|
+
this.ensureWorkingFilesLoaded(plannedPaths, "Planned for execution");
|
|
350
353
|
let seededCount = 0;
|
|
351
354
|
const seeded = [];
|
|
352
355
|
for (const filePath of plannedPaths) {
|
|
@@ -409,6 +412,15 @@ export class MainAgent {
|
|
|
409
412
|
const routing = this.context.analysis.routingDecision;
|
|
410
413
|
if (!routing)
|
|
411
414
|
return;
|
|
415
|
+
const intentCategory = (this.context.analysis.intent?.intentCategory ?? "").toLowerCase();
|
|
416
|
+
const isEditLikeIntent = intentCategory === "codingtask" ||
|
|
417
|
+
intentCategory === "refactortask" ||
|
|
418
|
+
intentCategory === "request" ||
|
|
419
|
+
intentCategory === "docsandcomments";
|
|
420
|
+
const canWrite = this.context.executionControl?.constraints?.allowFileWrites ?? false;
|
|
421
|
+
if (!isEditLikeIntent || !canWrite) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
412
424
|
const selectedFiles = this.context.analysis.focus?.selectedFiles ?? [];
|
|
413
425
|
if (selectedFiles.length === 0)
|
|
414
426
|
return;
|
|
@@ -468,6 +480,9 @@ export class MainAgent {
|
|
|
468
480
|
// Step-level iterations
|
|
469
481
|
// ---------------------------
|
|
470
482
|
const stepAction = await this.runStepIterations(taskStep);
|
|
483
|
+
if (stepAction === "expand-scope") {
|
|
484
|
+
this.applyScopeExpansionRouteHint();
|
|
485
|
+
}
|
|
471
486
|
this.finishTaskStep(taskStep, stepCount, stepAction);
|
|
472
487
|
}
|
|
473
488
|
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
@@ -507,6 +522,8 @@ export class MainAgent {
|
|
|
507
522
|
return "request-feedback";
|
|
508
523
|
if (nextAction === "redo-step")
|
|
509
524
|
continue;
|
|
525
|
+
if (nextAction === "expand-scope")
|
|
526
|
+
return "expand-scope";
|
|
510
527
|
}
|
|
511
528
|
return "continue";
|
|
512
529
|
}
|
|
@@ -522,7 +539,7 @@ export class MainAgent {
|
|
|
522
539
|
await this.executeResearchTaskStep(taskStep);
|
|
523
540
|
return;
|
|
524
541
|
}
|
|
525
|
-
if (this.
|
|
542
|
+
if (canExecutePhase(this.context, "analysis") && canExecuteScope(this.context, "analysis")) {
|
|
526
543
|
const tAnalysis = this.startTimer();
|
|
527
544
|
await analysisPlanGenStep.run(this.context);
|
|
528
545
|
this.logLine("PLAN", "analysisPlanGen", tAnalysis(), undefined, { highlight: false });
|
|
@@ -535,7 +552,9 @@ export class MainAgent {
|
|
|
535
552
|
if (this.context.analysis)
|
|
536
553
|
this.context.analysis.planSuggestion = undefined;
|
|
537
554
|
}
|
|
538
|
-
if (
|
|
555
|
+
if (canExecutePhase(this.context, "transform") &&
|
|
556
|
+
canExecuteScope(this.context, "transform") &&
|
|
557
|
+
canExecuteRoute(this.context, "transform")) {
|
|
539
558
|
const tTransform = this.startTimer();
|
|
540
559
|
await transformPlanGenStep.run(this.context);
|
|
541
560
|
this.logLine("PLAN", "transformPlanGen", tTransform(), undefined, { highlight: false });
|
|
@@ -548,7 +567,7 @@ export class MainAgent {
|
|
|
548
567
|
}
|
|
549
568
|
if (this.context.analysis)
|
|
550
569
|
this.context.analysis.planSuggestion = undefined;
|
|
551
|
-
if (this.
|
|
570
|
+
if (canExecutePhase(this.context, "write") && canExecuteScope(this.context, "write")) {
|
|
552
571
|
const tWrite = this.startTimer();
|
|
553
572
|
await writeFileStep.run({ query: this.query, context: this.context });
|
|
554
573
|
this.logLine("WRITE", "writeFileStep", tWrite());
|
|
@@ -1155,6 +1174,8 @@ export class MainAgent {
|
|
|
1155
1174
|
const existsOrResearch = (filePath) => {
|
|
1156
1175
|
if (filePath.startsWith("__research__/"))
|
|
1157
1176
|
return true;
|
|
1177
|
+
if (isPathIgnoredByFolderGlobs(filePath))
|
|
1178
|
+
return false;
|
|
1158
1179
|
return fs.existsSync(filePath);
|
|
1159
1180
|
};
|
|
1160
1181
|
let removedRelated = 0;
|
|
@@ -1208,7 +1229,7 @@ export class MainAgent {
|
|
|
1208
1229
|
*/
|
|
1209
1230
|
getTaskStepBudget() {
|
|
1210
1231
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1211
|
-
if (this.
|
|
1232
|
+
if (canExecuteRoute(this.context, "research"))
|
|
1212
1233
|
return 10;
|
|
1213
1234
|
if (scope === "multi-file")
|
|
1214
1235
|
return 7;
|
|
@@ -1221,7 +1242,7 @@ export class MainAgent {
|
|
|
1221
1242
|
* Example: require at least two analyzed files plus one understanding signal.
|
|
1222
1243
|
*/
|
|
1223
1244
|
isResearchGateSatisfied() {
|
|
1224
|
-
if (!this.
|
|
1245
|
+
if (!canExecuteRoute(this.context, "research"))
|
|
1225
1246
|
return true;
|
|
1226
1247
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1227
1248
|
const researchPlanCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" && s.action.startsWith("research-")).length ?? 0;
|
|
@@ -1292,28 +1313,45 @@ export class MainAgent {
|
|
|
1292
1313
|
this.context.task.status = status;
|
|
1293
1314
|
this.persistTaskDataForRun();
|
|
1294
1315
|
}
|
|
1316
|
+
applyScopeExpansionRouteHint() {
|
|
1317
|
+
var _a;
|
|
1318
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
1319
|
+
const route = this.context.analysis.routingDecision;
|
|
1320
|
+
if (!route)
|
|
1321
|
+
return;
|
|
1322
|
+
route.decision = "needs-info";
|
|
1323
|
+
route.allowSearch = true;
|
|
1324
|
+
route.scopeLocked = false;
|
|
1325
|
+
route.rationale = `${route.rationale}; stepReasoning=expand-scope`;
|
|
1326
|
+
this.logLine("TASK", "Route updated from step reasoning", undefined, "expand-scope requested; re-enabled search expansion");
|
|
1327
|
+
}
|
|
1295
1328
|
startTaskStep(taskStep, stepCount) {
|
|
1329
|
+
this.ensureWorkingFilesLoaded([taskStep.filePath], "Current task step");
|
|
1330
|
+
const db = getDbForRepo();
|
|
1331
|
+
this.ensureTaskIdentityForPersistence(db);
|
|
1296
1332
|
this.context.task.currentStep = taskStep;
|
|
1297
1333
|
taskStep.taskId = this.taskId;
|
|
1298
1334
|
taskStep.stepIndex = stepCount;
|
|
1299
1335
|
taskStep.status = "pending";
|
|
1300
|
-
persistTaskStepInsert(taskStep,
|
|
1336
|
+
persistTaskStepInsert(taskStep, db);
|
|
1301
1337
|
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1302
1338
|
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
|
|
1303
1339
|
taskStep.startTime = Date.now();
|
|
1304
|
-
persistTaskStepStart(taskStep,
|
|
1340
|
+
persistTaskStepStart(taskStep, db);
|
|
1305
1341
|
}
|
|
1306
1342
|
finishTaskStep(taskStep, stepCount, stepAction) {
|
|
1343
|
+
const db = getDbForRepo();
|
|
1344
|
+
this.ensureTaskIdentityForPersistence(db);
|
|
1307
1345
|
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1308
1346
|
taskStep.endTime = Date.now();
|
|
1309
1347
|
if (stepAction === "complete") {
|
|
1310
1348
|
taskStep.status = "completed";
|
|
1311
|
-
persistTaskStepCompletion(taskStep,
|
|
1349
|
+
persistTaskStepCompletion(taskStep, db);
|
|
1312
1350
|
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, displayPath, { highlight: false });
|
|
1313
1351
|
return;
|
|
1314
1352
|
}
|
|
1315
1353
|
taskStep.status = "pending";
|
|
1316
|
-
persistTaskStepCompletion(taskStep,
|
|
1354
|
+
persistTaskStepCompletion(taskStep, db);
|
|
1317
1355
|
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, displayPath);
|
|
1318
1356
|
}
|
|
1319
1357
|
/**
|
|
@@ -1325,69 +1363,6 @@ export class MainAgent {
|
|
|
1325
1363
|
? filePath.replace("__research__/", "research/")
|
|
1326
1364
|
: filePath;
|
|
1327
1365
|
}
|
|
1328
|
-
/* ───────────── execution gates ───────────── */
|
|
1329
|
-
/**
|
|
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.
|
|
1338
|
-
*/
|
|
1339
|
-
canExecutePhase(phase) {
|
|
1340
|
-
const constraints = this.context.executionControl?.constraints;
|
|
1341
|
-
const docsOnly = constraints?.docsOnly ?? false;
|
|
1342
|
-
let allowed = false;
|
|
1343
|
-
switch (phase) {
|
|
1344
|
-
case "analysis":
|
|
1345
|
-
case "planning":
|
|
1346
|
-
allowed = !docsOnly;
|
|
1347
|
-
break;
|
|
1348
|
-
case "transform":
|
|
1349
|
-
case "write":
|
|
1350
|
-
allowed = constraints?.allowFileWrites ?? false;
|
|
1351
|
-
break;
|
|
1352
|
-
}
|
|
1353
|
-
return allowed;
|
|
1354
|
-
}
|
|
1355
|
-
/* ───────────── scope gates ───────────── */
|
|
1356
|
-
/**
|
|
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.
|
|
1360
|
-
*/
|
|
1361
|
-
canExecuteScope(phase) {
|
|
1362
|
-
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1363
|
-
let allowed = false;
|
|
1364
|
-
switch (scope) {
|
|
1365
|
-
case "none":
|
|
1366
|
-
allowed = phase === "analysis" || phase === "planning";
|
|
1367
|
-
break;
|
|
1368
|
-
default:
|
|
1369
|
-
allowed = true;
|
|
1370
|
-
}
|
|
1371
|
-
return allowed;
|
|
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
|
-
}
|
|
1391
1366
|
/* ----------------------------------- */
|
|
1392
1367
|
/* ------------- helpers ------------- */
|
|
1393
1368
|
/* ----------------------------------- */
|
|
@@ -1433,6 +1408,68 @@ export class MainAgent {
|
|
|
1433
1408
|
console.log(`[USER OUTPUT] ${message}`);
|
|
1434
1409
|
});
|
|
1435
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Ensures workingFiles has file capsules with code for the given paths.
|
|
1413
|
+
* Example: planned verify-relevant files are hydrated before transform starts.
|
|
1414
|
+
*/
|
|
1415
|
+
ensureWorkingFilesLoaded(paths, reason) {
|
|
1416
|
+
var _a;
|
|
1417
|
+
(_a = this.context).workingFiles || (_a.workingFiles = []);
|
|
1418
|
+
const indexByPath = new Map(this.context.workingFiles.map(file => [file.path, file]));
|
|
1419
|
+
for (const filePath of paths) {
|
|
1420
|
+
if (!filePath || filePath.startsWith("__research__/"))
|
|
1421
|
+
continue;
|
|
1422
|
+
if (!fs.existsSync(filePath))
|
|
1423
|
+
continue;
|
|
1424
|
+
const existing = indexByPath.get(filePath);
|
|
1425
|
+
if (existing && typeof existing.code === "string" && existing.code.length > 0) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
let code;
|
|
1429
|
+
try {
|
|
1430
|
+
code = fs.readFileSync(filePath, "utf-8");
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
code = undefined;
|
|
1434
|
+
}
|
|
1435
|
+
if (existing) {
|
|
1436
|
+
existing.code = code;
|
|
1437
|
+
existing.selectionReason || (existing.selectionReason = reason);
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
const capsule = { path: filePath, code, selectionReason: reason };
|
|
1441
|
+
this.context.workingFiles.push(capsule);
|
|
1442
|
+
indexByPath.set(filePath, capsule);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Ensures the current task id exists in the active DB before step persistence.
|
|
1448
|
+
* Example: if repo/db context switched, re-create task row and rebind step taskIds.
|
|
1449
|
+
*/
|
|
1450
|
+
ensureTaskIdentityForPersistence(db) {
|
|
1451
|
+
var _a;
|
|
1452
|
+
const activeTaskId = this.taskId ?? this.context.task?.id;
|
|
1453
|
+
if (!activeTaskId) {
|
|
1454
|
+
this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
1455
|
+
this.context.task.id = this.taskId;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const row = db.prepare("SELECT id FROM tasks WHERE id = ?").get(activeTaskId);
|
|
1459
|
+
if (row?.id) {
|
|
1460
|
+
this.taskId = activeTaskId;
|
|
1461
|
+
this.context.task.id = activeTaskId;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const reboundTaskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
1465
|
+
this.taskId = reboundTaskId;
|
|
1466
|
+
this.context.task.id = reboundTaskId;
|
|
1467
|
+
(_a = this.context.task).taskSteps || (_a.taskSteps = []);
|
|
1468
|
+
for (const step of this.context.task.taskSteps) {
|
|
1469
|
+
step.taskId = reboundTaskId;
|
|
1470
|
+
}
|
|
1471
|
+
this.logLine("TASK", "taskId rebound", undefined, `previous=${activeTaskId}, rebound=${reboundTaskId}`);
|
|
1472
|
+
}
|
|
1436
1473
|
}
|
|
1437
1474
|
function scoreCandidateFiles(filePaths, relatedFileScores, retrievalQuery) {
|
|
1438
1475
|
const explicitRefs = extractFileReferences(retrievalQuery, { lowercase: true });
|
|
@@ -3,6 +3,7 @@ import { generate } from "../lib/generate.js";
|
|
|
3
3
|
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
4
4
|
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
6
7
|
export async function fileCheckStep(context) {
|
|
7
8
|
var _a;
|
|
8
9
|
context.analysis ?? (context.analysis = {});
|
|
@@ -10,9 +11,7 @@ export async function fileCheckStep(context) {
|
|
|
10
11
|
const intent = context.analysis.intent;
|
|
11
12
|
const planSuggestion = context.analysis.planSuggestion?.text ?? "";
|
|
12
13
|
// Step 1: gather known files from initContext only
|
|
13
|
-
const knownFiles = new Set([
|
|
14
|
-
...(context.initContext?.relatedFiles ?? []),
|
|
15
|
-
]);
|
|
14
|
+
const knownFiles = new Set((context.initContext?.relatedFiles ?? []).filter(filePath => !isPathIgnoredByFolderGlobs(filePath)));
|
|
16
15
|
// Step 2: extract file names from normalizedQuery or planSuggestion
|
|
17
16
|
const extractedFiles = extractFilesFromAnalysis(context.analysis);
|
|
18
17
|
// Step 3: populate focus with safe defaults
|
|
@@ -138,16 +137,25 @@ Task:
|
|
|
138
137
|
// Merge parsed output safely
|
|
139
138
|
if (Array.isArray(parsed.selectedFiles)) {
|
|
140
139
|
const existing = new Set(context.analysis.focus.selectedFiles);
|
|
140
|
+
const safeSelected = parsed.selectedFiles
|
|
141
|
+
.filter((f) => typeof f === "string" && knownFiles.has(f) && !isPathIgnoredByFolderGlobs(f));
|
|
141
142
|
context.analysis.focus.selectedFiles = [
|
|
142
143
|
...context.analysis.focus.selectedFiles,
|
|
143
|
-
...
|
|
144
|
+
...safeSelected.filter((f) => !existing.has(f))
|
|
144
145
|
];
|
|
145
146
|
}
|
|
146
147
|
if (Array.isArray(parsed.candidateFiles)) {
|
|
147
148
|
const existing = new Set(context.analysis.focus.candidateFiles);
|
|
149
|
+
const safeCandidates = parsed.candidateFiles
|
|
150
|
+
.filter((f) => typeof f === "string")
|
|
151
|
+
.filter((f) => {
|
|
152
|
+
if (knownFiles.has(f))
|
|
153
|
+
return !isPathIgnoredByFolderGlobs(f);
|
|
154
|
+
return /^[\w\-./]+\.(js|ts|jsx|tsx|py|java|go|rs|cpp|c|cs|md)$/i.test(f);
|
|
155
|
+
});
|
|
148
156
|
context.analysis.focus.candidateFiles = [
|
|
149
157
|
...context.analysis.focus.candidateFiles,
|
|
150
|
-
...
|
|
158
|
+
...safeCandidates.filter((f) => !existing.has(f))
|
|
151
159
|
];
|
|
152
160
|
}
|
|
153
161
|
if (typeof parsed.rationale === "string") {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate 1: capability constraints (execution control).
|
|
3
|
+
*/
|
|
4
|
+
export function canExecutePhase(context, phase) {
|
|
5
|
+
const constraints = context.executionControl?.constraints;
|
|
6
|
+
const docsOnly = constraints?.docsOnly ?? false;
|
|
7
|
+
switch (phase) {
|
|
8
|
+
case "analysis":
|
|
9
|
+
case "planning":
|
|
10
|
+
return !docsOnly;
|
|
11
|
+
case "transform":
|
|
12
|
+
case "write":
|
|
13
|
+
return constraints?.allowFileWrites ?? false;
|
|
14
|
+
default:
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gate 2: scope constraints (none/single/multi/repo-wide).
|
|
20
|
+
*/
|
|
21
|
+
export function canExecuteScope(context, phase) {
|
|
22
|
+
const scope = context.analysis?.scopeType ?? "repo-wide";
|
|
23
|
+
if (scope === "none") {
|
|
24
|
+
return phase === "analysis" || phase === "planning";
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gate 3: route constraints (dynamic routing decisions).
|
|
30
|
+
*/
|
|
31
|
+
export function canExecuteRoute(context, action) {
|
|
32
|
+
const routing = context.analysis?.routingDecision;
|
|
33
|
+
switch (action) {
|
|
34
|
+
case "search-expand":
|
|
35
|
+
return routing?.allowSearch ?? true;
|
|
36
|
+
case "transform":
|
|
37
|
+
return routing?.allowTransform ?? true;
|
|
38
|
+
case "research":
|
|
39
|
+
return routing?.allowResearch ?? false;
|
|
40
|
+
default:
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -43,3 +43,20 @@ export const IGNORED_FOLDER_GLOBS = [
|
|
|
43
43
|
'**/debug.log',
|
|
44
44
|
'**/Dockerfile',
|
|
45
45
|
];
|
|
46
|
+
export const IGNORED_DIR_NAMES = Array.from(new Set(IGNORED_FOLDER_GLOBS
|
|
47
|
+
.map((pattern) => {
|
|
48
|
+
const match = pattern.match(/^\*\*\/([^*\/]+)\/\*\*$/);
|
|
49
|
+
return match?.[1];
|
|
50
|
+
})
|
|
51
|
+
.filter((name) => !!name)));
|
|
52
|
+
/**
|
|
53
|
+
* Coarse path guard used by runtime search/selection fallback paths.
|
|
54
|
+
* Matches the same ignore-glob intent used during indexing.
|
|
55
|
+
*/
|
|
56
|
+
export function isPathIgnoredByFolderGlobs(filePath) {
|
|
57
|
+
const normalizedPath = String(filePath ?? "").replace(/\\/g, "/");
|
|
58
|
+
return IGNORED_FOLDER_GLOBS.some((pattern) => {
|
|
59
|
+
const cleanPattern = pattern.replace(/^\*\*\/?/, "").replace(/\/\*\*$/, "");
|
|
60
|
+
return cleanPattern.length > 0 && normalizedPath.includes(cleanPattern);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// File: src/modules/fileSearchModule.ts
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { plannerSearchFiles } from "../../db/fileIndex.js";
|
|
6
6
|
import { Config } from "../../config.js";
|
|
7
7
|
import { getDbForRepo } from "../../db/client.js";
|
|
8
8
|
import { IGNORED_EXTENSIONS } from "../../fileRules/ignoredExtensions.js";
|
|
9
|
+
import { IGNORED_DIR_NAMES, isPathIgnoredByFolderGlobs } from "../../fileRules/ignoredPaths.js";
|
|
9
10
|
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
10
11
|
async function fetchSummariesForPaths(paths) {
|
|
11
12
|
if (paths.length === 0)
|
|
@@ -63,14 +64,15 @@ export const fileSearchModule = {
|
|
|
63
64
|
// -------------------------------------------------
|
|
64
65
|
if (results.length === 0) {
|
|
65
66
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const stdout =
|
|
67
|
+
const excludeExtArgs = IGNORED_EXTENSIONS.map(ext => `--exclude=*${ext}`);
|
|
68
|
+
const excludeDirArgs = IGNORED_DIR_NAMES.map(dir => `--exclude-dir=${dir}`);
|
|
69
|
+
const grepArgs = ["-ril", ...excludeDirArgs, ...excludeExtArgs, subQuery, repoRoot];
|
|
70
|
+
const stdout = execFileSync("grep", grepArgs, { encoding: "utf8" });
|
|
70
71
|
results = stdout
|
|
71
72
|
.split("\n")
|
|
72
73
|
.filter(Boolean)
|
|
73
|
-
.map(f => ({ path: f }))
|
|
74
|
+
.map(f => ({ path: f }))
|
|
75
|
+
.filter(record => !isPathIgnoredByFolderGlobs(record.path));
|
|
74
76
|
}
|
|
75
77
|
catch (err) {
|
|
76
78
|
if (err?.status !== 1) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
3
4
|
export async function resolveTargetsToFiles(targets, exts) {
|
|
4
5
|
const files = [];
|
|
5
6
|
for (const target of targets) {
|
|
7
|
+
if (isPathIgnoredByFolderGlobs(target))
|
|
8
|
+
continue;
|
|
6
9
|
const stat = await fs.stat(target);
|
|
7
10
|
if (stat.isDirectory()) {
|
|
8
11
|
files.push(...await collectFilesRecursive(target, exts));
|
|
@@ -18,6 +21,8 @@ async function collectFilesRecursive(dir, exts) {
|
|
|
18
21
|
const files = [];
|
|
19
22
|
for (const entry of entries) {
|
|
20
23
|
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
if (isPathIgnoredByFolderGlobs(fullPath))
|
|
25
|
+
continue;
|
|
21
26
|
if (entry.isDirectory()) {
|
|
22
27
|
files.push(...await collectFilesRecursive(fullPath, exts));
|
|
23
28
|
}
|