@securecode-ai/mcp 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/api/client.d.ts +11 -1
- package/dist/api/client.js +93 -8
- package/dist/api/client.js.map +1 -1
- package/dist/api/types.d.ts +24 -0
- package/dist/approval/policy.js +5 -0
- package/dist/approval/policy.js.map +1 -1
- package/dist/attack/agentScanBatchProtocol.d.ts +98 -0
- package/dist/attack/agentScanBatchProtocol.js +125 -0
- package/dist/attack/agentScanBatchProtocol.js.map +1 -0
- package/dist/attack/agentScanBatchSelection.d.ts +38 -0
- package/dist/attack/agentScanBatchSelection.js +167 -0
- package/dist/attack/agentScanBatchSelection.js.map +1 -0
- package/dist/attack/agentScanCoordinator.d.ts +33 -0
- package/dist/attack/agentScanCoordinator.js +116 -0
- package/dist/attack/agentScanCoordinator.js.map +1 -0
- package/dist/attack/agentScanCreditPlan.d.ts +40 -0
- package/dist/attack/agentScanCreditPlan.js +50 -0
- package/dist/attack/agentScanCreditPlan.js.map +1 -0
- package/dist/attack/agentScanLoop.js +173 -111
- package/dist/attack/agentScanLoop.js.map +1 -1
- package/dist/attack/agentScanProtocol.d.ts +2 -2
- package/dist/attack/agentScanProtocol.js.map +1 -1
- package/dist/attack/investigationState.d.ts +1 -0
- package/dist/attack/investigationState.js +20 -7
- package/dist/attack/investigationState.js.map +1 -1
- package/dist/attack/scanScheduler.d.ts +5 -0
- package/dist/attack/scanScheduler.js +37 -2
- package/dist/attack/scanScheduler.js.map +1 -1
- package/dist/attack/scanState.d.ts +6 -0
- package/dist/attack/scanState.js +5 -0
- package/dist/attack/scanState.js.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +35 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-map/scanCache.d.ts +4 -1
- package/dist/project-map/scanCache.js +15 -0
- package/dist/project-map/scanCache.js.map +1 -1
- package/dist/tools/agentScan.js +10 -1
- package/dist/tools/agentScan.js.map +1 -1
- package/dist/tools/agentScanBatch.d.ts +18 -0
- package/dist/tools/agentScanBatch.js +178 -0
- package/dist/tools/agentScanBatch.js.map +1 -0
- package/icon.svg +6 -0
- package/package.json +4 -2
|
@@ -66,7 +66,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
66
66
|
const startValidation = (0, protocolValidator_1.validateStartResponse)(startRespRaw);
|
|
67
67
|
if (!startValidation.ok) {
|
|
68
68
|
return {
|
|
69
|
-
status: '
|
|
69
|
+
status: 'failed',
|
|
70
70
|
findings: [],
|
|
71
71
|
transcript: [],
|
|
72
72
|
stepsUsed: 0,
|
|
@@ -253,12 +253,14 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
253
253
|
let consecutiveBlockedReads = 0;
|
|
254
254
|
let meaningfulProgressSinceRecovery = true;
|
|
255
255
|
let aggressiveCompaction = false;
|
|
256
|
+
const attemptedRecoveryFingerprints = new Set();
|
|
257
|
+
const RECOVERY_FAILURE_LIMIT = 3;
|
|
256
258
|
while (true) {
|
|
257
259
|
// Wall clock check
|
|
258
260
|
if (Date.now() - startTime > wallClockMs) {
|
|
259
261
|
(0, scanState_1.terminateScan)(scanState, 'wall_clock', `Wall clock limit (${wallClockMs}ms) exceeded.`);
|
|
260
262
|
return {
|
|
261
|
-
status: '
|
|
263
|
+
status: 'incomplete',
|
|
262
264
|
findings: [],
|
|
263
265
|
transcript,
|
|
264
266
|
stepsUsed: stepsTaken,
|
|
@@ -293,23 +295,24 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
293
295
|
// 2 blocked reads: recovery mode — if unread ranges exist,
|
|
294
296
|
// require the next unread range; if no unread ranges remain,
|
|
295
297
|
// forbid read_file entirely and require an analysis tool.
|
|
296
|
-
// 3 blocked reads: MCP selects a deterministic recovery action
|
|
298
|
+
// 3+ blocked reads: MCP selects a deterministic recovery action via scheduler
|
|
297
299
|
let actionConstraint;
|
|
298
300
|
if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
:
|
|
302
|
-
|
|
301
|
+
const schedDecision = (0, scanScheduler_1.schedule)({
|
|
302
|
+
state: scanState,
|
|
303
|
+
evidence: evidenceLedger,
|
|
304
|
+
workItems: workItemQueue,
|
|
305
|
+
handlers: handlerInventory,
|
|
306
|
+
candidates: candidateStore,
|
|
307
|
+
investigation: investigationState,
|
|
308
|
+
target,
|
|
309
|
+
functionBoundaries,
|
|
310
|
+
});
|
|
311
|
+
if (schedDecision.action) {
|
|
303
312
|
actionConstraint = {
|
|
304
313
|
mode: 'recovery',
|
|
305
|
-
requiredAction:
|
|
306
|
-
|
|
307
|
-
path: target.filePath,
|
|
308
|
-
startLine: nextRange.start,
|
|
309
|
-
endLine: nextRange.end,
|
|
310
|
-
rationale: `Read next unread range: lines ${nextRange.start}-${nextRange.end}`,
|
|
311
|
-
},
|
|
312
|
-
reason: `You have been blocked by duplicate/overlapping reads. Read the next unread range: lines ${nextRange.start}-${nextRange.end}, or use an analysis tool (trace_flow, check_guard, check_policy).`,
|
|
314
|
+
requiredAction: schedDecision.action.type,
|
|
315
|
+
reason: `You have been blocked by duplicate/overlapping reads. Next required action: ${schedDecision.action.type}. ${schedDecision.reason}`,
|
|
313
316
|
};
|
|
314
317
|
}
|
|
315
318
|
else {
|
|
@@ -371,7 +374,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
371
374
|
});
|
|
372
375
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
373
376
|
return {
|
|
374
|
-
status: '
|
|
377
|
+
status: 'incomplete',
|
|
375
378
|
findings: [],
|
|
376
379
|
transcript,
|
|
377
380
|
stepsUsed: stepsTaken,
|
|
@@ -396,14 +399,14 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
396
399
|
if (apiCode === 'AGENT_RUN_NOT_FOUND' || /AGENT_RUN_NOT_FOUND|Invalid or expired agent run/i.test(errMsg)) {
|
|
397
400
|
console.warn(`[Agent Scan Loop] Agent run expired (API server restarted?). Stopping scan.`);
|
|
398
401
|
return {
|
|
399
|
-
status: '
|
|
402
|
+
status: 'failed',
|
|
400
403
|
findings: [],
|
|
401
404
|
transcript,
|
|
402
405
|
stepsUsed: stepsTaken,
|
|
403
406
|
stepsGranted,
|
|
404
407
|
extensionsGranted,
|
|
405
408
|
costSpentUsd,
|
|
406
|
-
terminationReason: '
|
|
409
|
+
terminationReason: 'api_restart',
|
|
407
410
|
error: 'API server restarted mid-scan — the run was lost. Please retry the scan.',
|
|
408
411
|
investigationNotes: [],
|
|
409
412
|
coverageGaps: [],
|
|
@@ -451,7 +454,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
451
454
|
});
|
|
452
455
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
453
456
|
return {
|
|
454
|
-
status: '
|
|
457
|
+
status: 'incomplete',
|
|
455
458
|
findings: [],
|
|
456
459
|
transcript,
|
|
457
460
|
stepsUsed: stepsTaken,
|
|
@@ -520,8 +523,8 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
520
523
|
// Null next = done (cost capped or steps exhausted)
|
|
521
524
|
if (!stepResp.next) {
|
|
522
525
|
const status = stepResp.costCapped
|
|
523
|
-
? '
|
|
524
|
-
: (stepResp.degraded ? '
|
|
526
|
+
? 'incomplete'
|
|
527
|
+
: (stepResp.degraded ? 'incomplete' : 'completed');
|
|
525
528
|
trace.logRunCompleted(status);
|
|
526
529
|
return {
|
|
527
530
|
status,
|
|
@@ -955,40 +958,28 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
955
958
|
consecutiveBlockedReads++;
|
|
956
959
|
scanState.recovery.consecutiveBlockedActions = consecutiveBlockedReads;
|
|
957
960
|
scanState.recovery.totalBlockedActions++;
|
|
961
|
+
scanState.recovery.consecutiveModelBlockedActions = consecutiveBlockedReads;
|
|
962
|
+
scanState.recovery.totalModelBlockedActions++;
|
|
958
963
|
scanState.recovery.lastBlockedAction = action.type;
|
|
959
964
|
scanState.recovery.meaningfulProgressSinceRecovery = false;
|
|
960
965
|
const recoveryLimit = agentScanProtocol_1.AGENT_SCAN_DEFAULTS.blockedReadRecoveryLimit;
|
|
961
966
|
if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
|
|
962
|
-
|
|
963
|
-
? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
|
|
964
|
-
: investigationState.getNextUnreadRange(target.filePath);
|
|
965
|
-
if (nextRange) {
|
|
966
|
-
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). Read the next unread range: lines ${nextRange.start}-${nextRange.end}, or use an analysis tool (trace_flow, check_guard, check_policy).`;
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
const recommendedTool = investigationState.getRecommendedRecoveryAction();
|
|
970
|
-
if (recommendedTool) {
|
|
971
|
-
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). No unread ranges remain. Your next action MUST be: ${recommendedTool}. If you have enough evidence, call finish.`;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
967
|
+
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). Use an analysis tool (trace_flow, check_guard, check_policy) or read a different file.`;
|
|
974
968
|
}
|
|
975
969
|
if (consecutiveBlockedReads >= recoveryLimit) {
|
|
976
|
-
const nextRange = target.fileContent
|
|
977
|
-
? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
|
|
978
|
-
: investigationState.getNextUnreadRange(target.filePath);
|
|
979
970
|
const hasBudget = budget.stepsRemaining > 0 && costSpentUsd < budget.costCapUsd;
|
|
980
971
|
const hasWallClock = Date.now() - startTime < wallClockMs;
|
|
981
|
-
if (
|
|
982
|
-
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery
|
|
972
|
+
if (hasBudget && hasWallClock) {
|
|
973
|
+
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery via scheduler.`);
|
|
983
974
|
}
|
|
984
975
|
else {
|
|
985
|
-
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available,
|
|
976
|
+
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available, terminating as incomplete.`);
|
|
986
977
|
transcript.push({ action, observation });
|
|
987
|
-
trace.logRunCompleted('
|
|
978
|
+
trace.logRunCompleted('incomplete');
|
|
988
979
|
const incompleteSteps = investigationState.getIncompleteSteps();
|
|
989
980
|
const autoGaps = incompleteSteps.map(step => ({
|
|
990
981
|
title: `Investigation step not completed: ${step}`,
|
|
991
|
-
detail: `The agent was
|
|
982
|
+
detail: `The agent was terminated after repeated blocked reads without completing this required investigation step: ${step}. The investigation was incomplete and vulnerabilities may have been missed.`,
|
|
992
983
|
file: target.filePath,
|
|
993
984
|
requiredEvidence: [`Complete the ${step} step before concluding no vulnerabilities exist`],
|
|
994
985
|
suggestedNextAction: step === 'config-inspection' ? 'read_config'
|
|
@@ -1024,7 +1015,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1024
1015
|
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1025
1016
|
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1026
1017
|
return {
|
|
1027
|
-
status: '
|
|
1018
|
+
status: 'incomplete',
|
|
1028
1019
|
findings: [],
|
|
1029
1020
|
transcript,
|
|
1030
1021
|
stepsUsed: stepsTaken,
|
|
@@ -1048,6 +1039,8 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1048
1039
|
if (meaningfulProgressSinceRecovery) {
|
|
1049
1040
|
consecutiveBlockedReads = 0;
|
|
1050
1041
|
scanState.recovery.consecutiveBlockedActions = 0;
|
|
1042
|
+
scanState.recovery.consecutiveModelBlockedActions = 0;
|
|
1043
|
+
scanState.recovery.consecutiveRecoveryFailures = 0;
|
|
1051
1044
|
scanState.recovery.meaningfulProgressSinceRecovery = true;
|
|
1052
1045
|
}
|
|
1053
1046
|
}
|
|
@@ -1247,42 +1240,156 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1247
1240
|
}
|
|
1248
1241
|
// Deterministic recovery: on the third consecutive blocked read,
|
|
1249
1242
|
// skip the API and directly execute a recovery action selected by
|
|
1250
|
-
// the
|
|
1251
|
-
// stuck re-reading files.
|
|
1243
|
+
// the scheduler. This prevents wasting API calls on an agent that
|
|
1244
|
+
// is stuck re-reading files. Track recovery fingerprints to avoid
|
|
1245
|
+
// repeating the same recovery action. Terminate after
|
|
1246
|
+
// RECOVERY_FAILURE_LIMIT failures without progress.
|
|
1252
1247
|
if (wasBlocked && consecutiveBlockedReads >= 3) {
|
|
1253
|
-
const
|
|
1248
|
+
const schedDecision = (0, scanScheduler_1.schedule)({
|
|
1249
|
+
state: scanState,
|
|
1250
|
+
evidence: evidenceLedger,
|
|
1251
|
+
workItems: workItemQueue,
|
|
1252
|
+
handlers: handlerInventory,
|
|
1253
|
+
candidates: candidateStore,
|
|
1254
|
+
investigation: investigationState,
|
|
1255
|
+
target,
|
|
1256
|
+
functionBoundaries,
|
|
1257
|
+
});
|
|
1258
|
+
const recoveryAction = schedDecision.action || null;
|
|
1254
1259
|
if (recoveryAction) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1260
|
+
const fp = (0, scanScheduler_1.actionFingerprint)(recoveryAction);
|
|
1261
|
+
const isDuplicate = attemptedRecoveryFingerprints.has(fp);
|
|
1262
|
+
scanState.recovery.totalRecoveryAttempts++;
|
|
1263
|
+
scanState.recovery.lastRecoveryAction = recoveryAction.type;
|
|
1264
|
+
if (isDuplicate) {
|
|
1265
|
+
scanState.recovery.consecutiveRecoveryFailures++;
|
|
1266
|
+
scanState.recovery.lastFailureReason = `Duplicate recovery action: ${fp}`;
|
|
1267
|
+
console.warn(`[Agent Scan Loop] Duplicate recovery action rejected: ${fp}. Failures: ${scanState.recovery.consecutiveRecoveryFailures}/${RECOVERY_FAILURE_LIMIT}`);
|
|
1268
|
+
if (scanState.recovery.consecutiveRecoveryFailures >= RECOVERY_FAILURE_LIMIT) {
|
|
1269
|
+
console.warn(`[Agent Scan Loop] ${RECOVERY_FAILURE_LIMIT} recovery failures without progress — terminating as incomplete.`);
|
|
1270
|
+
transcript.push({ action, observation });
|
|
1271
|
+
trace.logRunCompleted('incomplete');
|
|
1272
|
+
(0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', `${RECOVERY_FAILURE_LIMIT} recovery failures without progress.`);
|
|
1273
|
+
qualityTracker.recordForcedTermination('blocked_read_recovery');
|
|
1274
|
+
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1275
|
+
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1276
|
+
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1277
|
+
const incompleteSteps = investigationState.getIncompleteSteps();
|
|
1278
|
+
const autoGaps = incompleteSteps.map(step => ({
|
|
1279
|
+
title: `Investigation step not completed: ${step}`,
|
|
1280
|
+
detail: `The agent was terminated after ${RECOVERY_FAILURE_LIMIT} recovery failures without completing this required step: ${step}.`,
|
|
1281
|
+
file: target.filePath,
|
|
1282
|
+
requiredEvidence: [`Complete the ${step} step`],
|
|
1283
|
+
suggestedNextAction: 'continue investigation',
|
|
1284
|
+
priority: 'high',
|
|
1285
|
+
}));
|
|
1286
|
+
const unresolvedTasks = investigationState.getUnresolvedTasks();
|
|
1287
|
+
const taskGaps = unresolvedTasks.map(task => ({
|
|
1288
|
+
title: `Architecture risk unresolved: ${task.claim}`,
|
|
1289
|
+
detail: `This architecture-risk task was not resolved: ${task.claim}. Required evidence: ${task.requiredEvidence.join('; ')}.`,
|
|
1290
|
+
file: task.targetFiles[0] || target.filePath,
|
|
1291
|
+
requiredEvidence: task.requiredEvidence,
|
|
1292
|
+
suggestedNextAction: task.requiredTools[0] || 'continue investigation',
|
|
1293
|
+
priority: 'high',
|
|
1294
|
+
}));
|
|
1295
|
+
const uncoveredRanges = investigationState.getUncoveredRanges(target.filePath);
|
|
1296
|
+
const rangeGaps = uncoveredRanges.map(r => ({
|
|
1297
|
+
title: `Unread range: lines ${r.start}-${r.end} of ${target.filePath}`,
|
|
1298
|
+
detail: `This range was never read during the investigation.`,
|
|
1299
|
+
file: target.filePath,
|
|
1300
|
+
requiredEvidence: [`Read lines ${r.start}-${r.end}`],
|
|
1301
|
+
suggestedNextAction: 'read_file',
|
|
1302
|
+
priority: 'high',
|
|
1303
|
+
}));
|
|
1304
|
+
return {
|
|
1305
|
+
status: 'incomplete',
|
|
1306
|
+
findings: [],
|
|
1307
|
+
transcript,
|
|
1308
|
+
stepsUsed: stepsTaken,
|
|
1309
|
+
stepsGranted,
|
|
1310
|
+
extensionsGranted,
|
|
1311
|
+
costSpentUsd,
|
|
1312
|
+
terminationReason: 'blocked_read_recovery',
|
|
1313
|
+
summary: `Investigation incomplete — ${RECOVERY_FAILURE_LIMIT} recovery failures without progress. Model blocked ${scanState.recovery.totalModelBlockedActions} time(s), recovery attempted ${scanState.recovery.totalRecoveryAttempts} time(s).`,
|
|
1314
|
+
investigationNotes: [],
|
|
1315
|
+
coverageGaps: [...autoGaps, ...taskGaps, ...rangeGaps],
|
|
1316
|
+
};
|
|
1268
1317
|
}
|
|
1269
1318
|
}
|
|
1270
1319
|
else {
|
|
1271
|
-
|
|
1320
|
+
attemptedRecoveryFingerprints.add(fp);
|
|
1321
|
+
stepsTaken++;
|
|
1322
|
+
budget.stepsRemaining--;
|
|
1323
|
+
scanState.budget.stepsUsed = stepsTaken;
|
|
1324
|
+
scanState.budget.stepsRemaining = budget.stepsRemaining;
|
|
1325
|
+
trace.nextStep();
|
|
1326
|
+
trace.logActionSelected(recoveryAction.type, 'deterministic-recovery', false);
|
|
1327
|
+
if (options.onProgress) {
|
|
1328
|
+
options.onProgress(stepsTaken, stepsGranted, `Step ${stepsTaken}: deterministic recovery: ${describeAction(recoveryAction)}`);
|
|
1329
|
+
}
|
|
1330
|
+
let recoveryObservation;
|
|
1331
|
+
let recoveryMadeProgress = false;
|
|
1332
|
+
if (recoveryAction.type === 'read_file') {
|
|
1333
|
+
const readResult = await (0, agentScanExecutor_1.executeReadFileAction)(recoveryAction, ctx);
|
|
1334
|
+
recoveryObservation = readResult.observation;
|
|
1335
|
+
if (readResult.totalLines > 0 && !readResult.truncated) {
|
|
1336
|
+
investigationState.recordActualRead(recoveryAction.path, readResult.actualStart, readResult.actualEnd, readResult.totalLines, readResult.truncated);
|
|
1337
|
+
recoveryMadeProgress = true;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
recoveryObservation = await (0, agentScanExecutor_1.executeAction)(recoveryAction, ctx, startResp.runId, client, target);
|
|
1342
|
+
recoveryMadeProgress = true;
|
|
1343
|
+
}
|
|
1344
|
+
qualityTracker.recordToolUse(recoveryAction.type);
|
|
1345
|
+
investigationState.recordToolUse(recoveryAction.type);
|
|
1346
|
+
if (recoveryMadeProgress) {
|
|
1347
|
+
scanState.recovery.successfulRecoveryAttempts++;
|
|
1348
|
+
scanState.recovery.consecutiveRecoveryFailures = 0;
|
|
1349
|
+
consecutiveBlockedReads = 0;
|
|
1350
|
+
scanState.recovery.consecutiveBlockedActions = 0;
|
|
1351
|
+
scanState.recovery.consecutiveModelBlockedActions = 0;
|
|
1352
|
+
meaningfulProgressSinceRecovery = true;
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
scanState.recovery.consecutiveRecoveryFailures++;
|
|
1356
|
+
scanState.recovery.lastFailureReason = `Recovery action made no progress: ${recoveryAction.type}`;
|
|
1357
|
+
}
|
|
1358
|
+
trace.logToolCompleted(recoveryAction.type, recoveryObservation);
|
|
1359
|
+
transcript.push({ action: recoveryAction, observation: `[DETERMINISTIC RECOVERY] ${recoveryObservation}` });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
// Scheduler has no action — finish or let model try
|
|
1364
|
+
if (schedDecision.kind === 'finish-ready') {
|
|
1365
|
+
console.warn(`[Agent Scan Loop] Scheduler says finish-ready during recovery — terminating as incomplete.`);
|
|
1366
|
+
transcript.push({ action, observation });
|
|
1367
|
+
trace.logRunCompleted('incomplete');
|
|
1368
|
+
(0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', 'Scheduler has no executable work during recovery.');
|
|
1369
|
+
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1370
|
+
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1371
|
+
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1372
|
+
return {
|
|
1373
|
+
status: 'incomplete',
|
|
1374
|
+
findings: [],
|
|
1375
|
+
transcript,
|
|
1376
|
+
stepsUsed: stepsTaken,
|
|
1377
|
+
stepsGranted,
|
|
1378
|
+
extensionsGranted,
|
|
1379
|
+
costSpentUsd,
|
|
1380
|
+
terminationReason: 'blocked_read_recovery',
|
|
1381
|
+
summary: 'Investigation incomplete — scheduler determined no executable work remains during recovery.',
|
|
1382
|
+
investigationNotes: [],
|
|
1383
|
+
coverageGaps: [],
|
|
1384
|
+
};
|
|
1272
1385
|
}
|
|
1273
|
-
qualityTracker.recordToolUse(recoveryAction.type);
|
|
1274
|
-
investigationState.recordToolUse(recoveryAction.type);
|
|
1275
|
-
consecutiveBlockedReads = 0; // recovery resets the counter
|
|
1276
|
-
meaningfulProgressSinceRecovery = true; // recovery is meaningful progress
|
|
1277
|
-
trace.logToolCompleted(recoveryAction.type, recoveryObservation);
|
|
1278
|
-
transcript.push({ action: recoveryAction, observation: `[DETERMINISTIC RECOVERY] ${recoveryObservation}` });
|
|
1279
1386
|
}
|
|
1280
1387
|
}
|
|
1281
1388
|
}
|
|
1282
1389
|
}
|
|
1283
1390
|
catch (err) {
|
|
1284
1391
|
return {
|
|
1285
|
-
status: '
|
|
1392
|
+
status: 'failed',
|
|
1286
1393
|
findings: [],
|
|
1287
1394
|
transcript: [],
|
|
1288
1395
|
stepsUsed: stepsTaken,
|
|
@@ -1467,49 +1574,4 @@ function describeAction(action) {
|
|
|
1467
1574
|
case 'system_event': return `system_event(${action.eventType})`;
|
|
1468
1575
|
}
|
|
1469
1576
|
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Select a deterministic recovery action based on investigation state.
|
|
1472
|
-
*
|
|
1473
|
-
* Priority:
|
|
1474
|
-
* 1. Unread range exists on the target file → read_file(next unread range)
|
|
1475
|
-
* 2. route-discovery missing → get_endpoints
|
|
1476
|
-
* 3. policy-check missing → check_policy
|
|
1477
|
-
* 4. auth-symbol-search missing → search_code
|
|
1478
|
-
* 5. cross-file-flow missing → trace_flow_cross_file
|
|
1479
|
-
* 6. config-inspection missing → read_config
|
|
1480
|
-
* 7. tests-found missing → find_tests
|
|
1481
|
-
* 8. otherwise → null (force-finish will handle it)
|
|
1482
|
-
*/
|
|
1483
|
-
function selectDeterministicRecoveryAction(state, target, ctx) {
|
|
1484
|
-
// Priority 1: unread range on the target file
|
|
1485
|
-
const nextRange = state.getNextUnreadRange(target.filePath);
|
|
1486
|
-
if (nextRange) {
|
|
1487
|
-
return {
|
|
1488
|
-
type: 'read_file',
|
|
1489
|
-
path: target.filePath,
|
|
1490
|
-
startLine: nextRange.start,
|
|
1491
|
-
endLine: nextRange.end,
|
|
1492
|
-
rationale: 'Deterministic recovery: reading next unread range',
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
// Priority 2-7: incomplete investigation steps
|
|
1496
|
-
const incomplete = state.getIncompleteSteps();
|
|
1497
|
-
for (const step of incomplete) {
|
|
1498
|
-
switch (step) {
|
|
1499
|
-
case 'route-discovery':
|
|
1500
|
-
return { type: 'get_endpoints', rationale: 'Deterministic recovery: route discovery' };
|
|
1501
|
-
case 'policy-check':
|
|
1502
|
-
return { type: 'check_policy', filePath: target.filePath, rationale: 'Deterministic recovery: policy check' };
|
|
1503
|
-
case 'auth-symbol-search':
|
|
1504
|
-
return { type: 'search_code', pattern: 'auth|require|guard|permission|owner', rationale: 'Deterministic recovery: auth symbol search' };
|
|
1505
|
-
case 'cross-file-flow':
|
|
1506
|
-
return { type: 'trace_flow_cross_file', filePath: target.filePath, rationale: 'Deterministic recovery: cross-file flow' };
|
|
1507
|
-
case 'config-inspection':
|
|
1508
|
-
return { type: 'read_config', configKind: 'all', rationale: 'Deterministic recovery: config inspection' };
|
|
1509
|
-
case 'tests-found':
|
|
1510
|
-
return { type: 'find_tests', filePath: target.filePath, rationale: 'Deterministic recovery: find tests' };
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return null;
|
|
1514
|
-
}
|
|
1515
1577
|
//# sourceMappingURL=agentScanLoop.js.map
|