@securecode-ai/mcp 0.5.8 → 0.6.1

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.
Files changed (45) hide show
  1. package/README.md +2 -1
  2. package/dist/api/client.d.ts +11 -1
  3. package/dist/api/client.js +93 -8
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/api/types.d.ts +24 -0
  6. package/dist/approval/policy.js +5 -0
  7. package/dist/approval/policy.js.map +1 -1
  8. package/dist/attack/agentScanBatchProtocol.d.ts +98 -0
  9. package/dist/attack/agentScanBatchProtocol.js +125 -0
  10. package/dist/attack/agentScanBatchProtocol.js.map +1 -0
  11. package/dist/attack/agentScanBatchSelection.d.ts +38 -0
  12. package/dist/attack/agentScanBatchSelection.js +167 -0
  13. package/dist/attack/agentScanBatchSelection.js.map +1 -0
  14. package/dist/attack/agentScanCoordinator.d.ts +33 -0
  15. package/dist/attack/agentScanCoordinator.js +116 -0
  16. package/dist/attack/agentScanCoordinator.js.map +1 -0
  17. package/dist/attack/agentScanCreditPlan.d.ts +40 -0
  18. package/dist/attack/agentScanCreditPlan.js +50 -0
  19. package/dist/attack/agentScanCreditPlan.js.map +1 -0
  20. package/dist/attack/agentScanLoop.js +205 -111
  21. package/dist/attack/agentScanLoop.js.map +1 -1
  22. package/dist/attack/agentScanProtocol.d.ts +2 -2
  23. package/dist/attack/agentScanProtocol.js.map +1 -1
  24. package/dist/attack/investigationState.d.ts +1 -0
  25. package/dist/attack/investigationState.js +20 -7
  26. package/dist/attack/investigationState.js.map +1 -1
  27. package/dist/attack/scanScheduler.d.ts +5 -0
  28. package/dist/attack/scanScheduler.js +37 -2
  29. package/dist/attack/scanScheduler.js.map +1 -1
  30. package/dist/attack/scanState.d.ts +6 -0
  31. package/dist/attack/scanState.js +5 -0
  32. package/dist/attack/scanState.js.map +1 -1
  33. package/dist/mcp/server.js +2 -0
  34. package/dist/mcp/server.js.map +1 -1
  35. package/dist/mcp/tools.js +35 -0
  36. package/dist/mcp/tools.js.map +1 -1
  37. package/dist/project-map/scanCache.d.ts +4 -1
  38. package/dist/project-map/scanCache.js +15 -0
  39. package/dist/project-map/scanCache.js.map +1 -1
  40. package/dist/tools/agentScan.js +10 -1
  41. package/dist/tools/agentScan.js.map +1 -1
  42. package/dist/tools/agentScanBatch.d.ts +18 -0
  43. package/dist/tools/agentScanBatch.js +178 -0
  44. package/dist/tools/agentScanBatch.js.map +1 -0
  45. package/package.json +1 -1
@@ -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: 'spawn_failed',
69
+ status: 'failed',
70
70
  findings: [],
71
71
  transcript: [],
72
72
  stepsUsed: 0,
@@ -253,12 +253,16 @@ async function runAgentScan(ctx, target, options = {}) {
253
253
  let consecutiveBlockedReads = 0;
254
254
  let meaningfulProgressSinceRecovery = true;
255
255
  let aggressiveCompaction = false;
256
+ let lastFinishAction = null;
257
+ let lastFinishRejectionReasons = [];
258
+ const attemptedRecoveryFingerprints = new Set();
259
+ const RECOVERY_FAILURE_LIMIT = 3;
256
260
  while (true) {
257
261
  // Wall clock check
258
262
  if (Date.now() - startTime > wallClockMs) {
259
263
  (0, scanState_1.terminateScan)(scanState, 'wall_clock', `Wall clock limit (${wallClockMs}ms) exceeded.`);
260
264
  return {
261
- status: 'capped',
265
+ status: 'incomplete',
262
266
  findings: [],
263
267
  transcript,
264
268
  stepsUsed: stepsTaken,
@@ -293,23 +297,24 @@ async function runAgentScan(ctx, target, options = {}) {
293
297
  // 2 blocked reads: recovery mode — if unread ranges exist,
294
298
  // require the next unread range; if no unread ranges remain,
295
299
  // forbid read_file entirely and require an analysis tool.
296
- // 3 blocked reads: MCP selects a deterministic recovery action (skip API)
300
+ // 3+ blocked reads: MCP selects a deterministic recovery action via scheduler
297
301
  let actionConstraint;
298
302
  if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
299
- const nextRange = target.fileContent
300
- ? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
301
- : investigationState.getNextUnreadRange(target.filePath);
302
- if (nextRange) {
303
+ const schedDecision = (0, scanScheduler_1.schedule)({
304
+ state: scanState,
305
+ evidence: evidenceLedger,
306
+ workItems: workItemQueue,
307
+ handlers: handlerInventory,
308
+ candidates: candidateStore,
309
+ investigation: investigationState,
310
+ target,
311
+ functionBoundaries,
312
+ });
313
+ if (schedDecision.action) {
303
314
  actionConstraint = {
304
315
  mode: 'recovery',
305
- requiredAction: {
306
- type: 'read_file',
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).`,
316
+ requiredAction: schedDecision.action.type,
317
+ reason: `You have been blocked by duplicate/overlapping reads. Next required action: ${schedDecision.action.type}. ${schedDecision.reason}`,
313
318
  };
314
319
  }
315
320
  else {
@@ -371,7 +376,7 @@ async function runAgentScan(ctx, target, options = {}) {
371
376
  });
372
377
  if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
373
378
  return {
374
- status: 'capped',
379
+ status: 'incomplete',
375
380
  findings: [],
376
381
  transcript,
377
382
  stepsUsed: stepsTaken,
@@ -394,16 +399,43 @@ async function runAgentScan(ctx, target, options = {}) {
394
399
  // Detect API server restart — the run was lost. Don't waste
395
400
  // 3 retry steps on a dead run; return immediately.
396
401
  if (apiCode === 'AGENT_RUN_NOT_FOUND' || /AGENT_RUN_NOT_FOUND|Invalid or expired agent run/i.test(errMsg)) {
402
+ if (lastFinishAction) {
403
+ console.warn('[Agent Scan Loop] Agent run already terminated after a delivered finish — surfacing the finish findings.');
404
+ const finish = lastFinishAction;
405
+ const coverageGaps = [...(finish.coverageGaps ?? [])];
406
+ if (lastFinishRejectionReasons.length > 0) {
407
+ coverageGaps.push({
408
+ title: 'Finish accepted by the server despite local gate concerns',
409
+ detail: `Local finish gate still flagged: ${lastFinishRejectionReasons.join('; ')}`,
410
+ requiredEvidence: [],
411
+ suggestedNextAction: 'Re-run a scan to cover the flagged gaps.',
412
+ priority: 'medium',
413
+ });
414
+ }
415
+ return {
416
+ status: 'completed',
417
+ findings: sanitizeFindings(finish.findings),
418
+ investigationNotes: finish.investigationNotes ?? [],
419
+ coverageGaps,
420
+ transcript,
421
+ stepsUsed: stepsTaken,
422
+ stepsGranted,
423
+ extensionsGranted,
424
+ costSpentUsd,
425
+ terminationReason: 'agent_finish',
426
+ summary: finish.summary,
427
+ };
428
+ }
397
429
  console.warn(`[Agent Scan Loop] Agent run expired (API server restarted?). Stopping scan.`);
398
430
  return {
399
- status: 'spawn_failed',
431
+ status: 'failed',
400
432
  findings: [],
401
433
  transcript,
402
434
  stepsUsed: stepsTaken,
403
435
  stepsGranted,
404
436
  extensionsGranted,
405
437
  costSpentUsd,
406
- terminationReason: 'api_error',
438
+ terminationReason: 'api_restart',
407
439
  error: 'API server restarted mid-scan — the run was lost. Please retry the scan.',
408
440
  investigationNotes: [],
409
441
  coverageGaps: [],
@@ -451,7 +483,7 @@ async function runAgentScan(ctx, target, options = {}) {
451
483
  });
452
484
  if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
453
485
  return {
454
- status: 'capped',
486
+ status: 'incomplete',
455
487
  findings: [],
456
488
  transcript,
457
489
  stepsUsed: stepsTaken,
@@ -520,8 +552,8 @@ async function runAgentScan(ctx, target, options = {}) {
520
552
  // Null next = done (cost capped or steps exhausted)
521
553
  if (!stepResp.next) {
522
554
  const status = stepResp.costCapped
523
- ? 'capped'
524
- : (stepResp.degraded ? 'degraded' : 'completed');
555
+ ? 'incomplete'
556
+ : (stepResp.degraded ? 'incomplete' : 'completed');
525
557
  trace.logRunCompleted(status);
526
558
  return {
527
559
  status,
@@ -555,6 +587,8 @@ async function runAgentScan(ctx, target, options = {}) {
555
587
  if (action.type === 'finish') {
556
588
  scanState.finishAttempts++;
557
589
  qualityTracker.recordFinishAttempt();
590
+ lastFinishAction = action;
591
+ lastFinishRejectionReasons = [];
558
592
  // Register candidates from findings — each finding becomes a
559
593
  // tracked candidate. If the candidate is new (discovered), the
560
594
  // finish gate will reject and the agent must gather evidence.
@@ -700,6 +734,7 @@ async function runAgentScan(ctx, target, options = {}) {
700
734
  // Finish rejected — continue investigation
701
735
  trace.logToolBlocked('finish', `Finish rejected: ${gateResult.reasons.map(r => r.description).join('; ')}`);
702
736
  qualityTracker.recordFinishRejection();
737
+ lastFinishRejectionReasons = gateResult.reasons.map(r => r.description);
703
738
  // Add a system event so the model sees the rejection
704
739
  const rejectionMessage = `FINISH REJECTED — your investigation is incomplete:\n${gateResult.reasons.map(r => ` - ${r.description}`).join('\n')}\n\nYou must complete the remaining investigation steps before calling finish.${gateResult.recoveryAction ? `\n\nYour next action MUST be: ${gateResult.recoveryAction.type}` : ''}`;
705
740
  transcript.push({
@@ -955,40 +990,28 @@ async function runAgentScan(ctx, target, options = {}) {
955
990
  consecutiveBlockedReads++;
956
991
  scanState.recovery.consecutiveBlockedActions = consecutiveBlockedReads;
957
992
  scanState.recovery.totalBlockedActions++;
993
+ scanState.recovery.consecutiveModelBlockedActions = consecutiveBlockedReads;
994
+ scanState.recovery.totalModelBlockedActions++;
958
995
  scanState.recovery.lastBlockedAction = action.type;
959
996
  scanState.recovery.meaningfulProgressSinceRecovery = false;
960
997
  const recoveryLimit = agentScanProtocol_1.AGENT_SCAN_DEFAULTS.blockedReadRecoveryLimit;
961
998
  if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
962
- const nextRange = target.fileContent
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
- }
999
+ 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
1000
  }
975
1001
  if (consecutiveBlockedReads >= recoveryLimit) {
976
- const nextRange = target.fileContent
977
- ? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
978
- : investigationState.getNextUnreadRange(target.filePath);
979
1002
  const hasBudget = budget.stepsRemaining > 0 && costSpentUsd < budget.costCapUsd;
980
1003
  const hasWallClock = Date.now() - startTime < wallClockMs;
981
- if (nextRange && hasBudget && hasWallClock) {
982
- console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery to unread range ${nextRange.start}-${nextRange.end}.`);
1004
+ if (hasBudget && hasWallClock) {
1005
+ console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery via scheduler.`);
983
1006
  }
984
1007
  else {
985
- console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available, force-finishing.`);
1008
+ console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available, terminating as incomplete.`);
986
1009
  transcript.push({ action, observation });
987
- trace.logRunCompleted('completed');
1010
+ trace.logRunCompleted('incomplete');
988
1011
  const incompleteSteps = investigationState.getIncompleteSteps();
989
1012
  const autoGaps = incompleteSteps.map(step => ({
990
1013
  title: `Investigation step not completed: ${step}`,
991
- detail: `The agent was force-finished after repeated blocked reads without completing this required investigation step: ${step}. The investigation was incomplete and vulnerabilities may have been missed.`,
1014
+ 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
1015
  file: target.filePath,
993
1016
  requiredEvidence: [`Complete the ${step} step before concluding no vulnerabilities exist`],
994
1017
  suggestedNextAction: step === 'config-inspection' ? 'read_config'
@@ -1024,7 +1047,7 @@ async function runAgentScan(ctx, target, options = {}) {
1024
1047
  qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
1025
1048
  qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
1026
1049
  return {
1027
- status: 'completed',
1050
+ status: 'incomplete',
1028
1051
  findings: [],
1029
1052
  transcript,
1030
1053
  stepsUsed: stepsTaken,
@@ -1048,6 +1071,8 @@ async function runAgentScan(ctx, target, options = {}) {
1048
1071
  if (meaningfulProgressSinceRecovery) {
1049
1072
  consecutiveBlockedReads = 0;
1050
1073
  scanState.recovery.consecutiveBlockedActions = 0;
1074
+ scanState.recovery.consecutiveModelBlockedActions = 0;
1075
+ scanState.recovery.consecutiveRecoveryFailures = 0;
1051
1076
  scanState.recovery.meaningfulProgressSinceRecovery = true;
1052
1077
  }
1053
1078
  }
@@ -1247,42 +1272,156 @@ async function runAgentScan(ctx, target, options = {}) {
1247
1272
  }
1248
1273
  // Deterministic recovery: on the third consecutive blocked read,
1249
1274
  // skip the API and directly execute a recovery action selected by
1250
- // the MCP. This prevents wasting API calls on an agent that is
1251
- // stuck re-reading files.
1275
+ // the scheduler. This prevents wasting API calls on an agent that
1276
+ // is stuck re-reading files. Track recovery fingerprints to avoid
1277
+ // repeating the same recovery action. Terminate after
1278
+ // RECOVERY_FAILURE_LIMIT failures without progress.
1252
1279
  if (wasBlocked && consecutiveBlockedReads >= 3) {
1253
- const recoveryAction = selectDeterministicRecoveryAction(investigationState, target, ctx);
1280
+ const schedDecision = (0, scanScheduler_1.schedule)({
1281
+ state: scanState,
1282
+ evidence: evidenceLedger,
1283
+ workItems: workItemQueue,
1284
+ handlers: handlerInventory,
1285
+ candidates: candidateStore,
1286
+ investigation: investigationState,
1287
+ target,
1288
+ functionBoundaries,
1289
+ });
1290
+ const recoveryAction = schedDecision.action || null;
1254
1291
  if (recoveryAction) {
1255
- stepsTaken++;
1256
- budget.stepsRemaining--;
1257
- trace.nextStep();
1258
- trace.logActionSelected(recoveryAction.type, 'deterministic-recovery', false);
1259
- if (options.onProgress) {
1260
- options.onProgress(stepsTaken, stepsGranted, `Step ${stepsTaken}: deterministic recovery: ${describeAction(recoveryAction)}`);
1261
- }
1262
- let recoveryObservation;
1263
- if (recoveryAction.type === 'read_file') {
1264
- const readResult = await (0, agentScanExecutor_1.executeReadFileAction)(recoveryAction, ctx);
1265
- recoveryObservation = readResult.observation;
1266
- if (readResult.totalLines > 0) {
1267
- investigationState.recordActualRead(recoveryAction.path, readResult.actualStart, readResult.actualEnd, readResult.totalLines, readResult.truncated);
1292
+ const fp = (0, scanScheduler_1.actionFingerprint)(recoveryAction);
1293
+ const isDuplicate = attemptedRecoveryFingerprints.has(fp);
1294
+ scanState.recovery.totalRecoveryAttempts++;
1295
+ scanState.recovery.lastRecoveryAction = recoveryAction.type;
1296
+ if (isDuplicate) {
1297
+ scanState.recovery.consecutiveRecoveryFailures++;
1298
+ scanState.recovery.lastFailureReason = `Duplicate recovery action: ${fp}`;
1299
+ console.warn(`[Agent Scan Loop] Duplicate recovery action rejected: ${fp}. Failures: ${scanState.recovery.consecutiveRecoveryFailures}/${RECOVERY_FAILURE_LIMIT}`);
1300
+ if (scanState.recovery.consecutiveRecoveryFailures >= RECOVERY_FAILURE_LIMIT) {
1301
+ console.warn(`[Agent Scan Loop] ${RECOVERY_FAILURE_LIMIT} recovery failures without progress — terminating as incomplete.`);
1302
+ transcript.push({ action, observation });
1303
+ trace.logRunCompleted('incomplete');
1304
+ (0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', `${RECOVERY_FAILURE_LIMIT} recovery failures without progress.`);
1305
+ qualityTracker.recordForcedTermination('blocked_read_recovery');
1306
+ const covSummary = investigationState.getCoverageSummary(target.filePath);
1307
+ qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
1308
+ qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
1309
+ const incompleteSteps = investigationState.getIncompleteSteps();
1310
+ const autoGaps = incompleteSteps.map(step => ({
1311
+ title: `Investigation step not completed: ${step}`,
1312
+ detail: `The agent was terminated after ${RECOVERY_FAILURE_LIMIT} recovery failures without completing this required step: ${step}.`,
1313
+ file: target.filePath,
1314
+ requiredEvidence: [`Complete the ${step} step`],
1315
+ suggestedNextAction: 'continue investigation',
1316
+ priority: 'high',
1317
+ }));
1318
+ const unresolvedTasks = investigationState.getUnresolvedTasks();
1319
+ const taskGaps = unresolvedTasks.map(task => ({
1320
+ title: `Architecture risk unresolved: ${task.claim}`,
1321
+ detail: `This architecture-risk task was not resolved: ${task.claim}. Required evidence: ${task.requiredEvidence.join('; ')}.`,
1322
+ file: task.targetFiles[0] || target.filePath,
1323
+ requiredEvidence: task.requiredEvidence,
1324
+ suggestedNextAction: task.requiredTools[0] || 'continue investigation',
1325
+ priority: 'high',
1326
+ }));
1327
+ const uncoveredRanges = investigationState.getUncoveredRanges(target.filePath);
1328
+ const rangeGaps = uncoveredRanges.map(r => ({
1329
+ title: `Unread range: lines ${r.start}-${r.end} of ${target.filePath}`,
1330
+ detail: `This range was never read during the investigation.`,
1331
+ file: target.filePath,
1332
+ requiredEvidence: [`Read lines ${r.start}-${r.end}`],
1333
+ suggestedNextAction: 'read_file',
1334
+ priority: 'high',
1335
+ }));
1336
+ return {
1337
+ status: 'incomplete',
1338
+ findings: [],
1339
+ transcript,
1340
+ stepsUsed: stepsTaken,
1341
+ stepsGranted,
1342
+ extensionsGranted,
1343
+ costSpentUsd,
1344
+ terminationReason: 'blocked_read_recovery',
1345
+ summary: `Investigation incomplete — ${RECOVERY_FAILURE_LIMIT} recovery failures without progress. Model blocked ${scanState.recovery.totalModelBlockedActions} time(s), recovery attempted ${scanState.recovery.totalRecoveryAttempts} time(s).`,
1346
+ investigationNotes: [],
1347
+ coverageGaps: [...autoGaps, ...taskGaps, ...rangeGaps],
1348
+ };
1268
1349
  }
1269
1350
  }
1270
1351
  else {
1271
- recoveryObservation = await (0, agentScanExecutor_1.executeAction)(recoveryAction, ctx, startResp.runId, client, target);
1352
+ attemptedRecoveryFingerprints.add(fp);
1353
+ stepsTaken++;
1354
+ budget.stepsRemaining--;
1355
+ scanState.budget.stepsUsed = stepsTaken;
1356
+ scanState.budget.stepsRemaining = budget.stepsRemaining;
1357
+ trace.nextStep();
1358
+ trace.logActionSelected(recoveryAction.type, 'deterministic-recovery', false);
1359
+ if (options.onProgress) {
1360
+ options.onProgress(stepsTaken, stepsGranted, `Step ${stepsTaken}: deterministic recovery: ${describeAction(recoveryAction)}`);
1361
+ }
1362
+ let recoveryObservation;
1363
+ let recoveryMadeProgress = false;
1364
+ if (recoveryAction.type === 'read_file') {
1365
+ const readResult = await (0, agentScanExecutor_1.executeReadFileAction)(recoveryAction, ctx);
1366
+ recoveryObservation = readResult.observation;
1367
+ if (readResult.totalLines > 0 && !readResult.truncated) {
1368
+ investigationState.recordActualRead(recoveryAction.path, readResult.actualStart, readResult.actualEnd, readResult.totalLines, readResult.truncated);
1369
+ recoveryMadeProgress = true;
1370
+ }
1371
+ }
1372
+ else {
1373
+ recoveryObservation = await (0, agentScanExecutor_1.executeAction)(recoveryAction, ctx, startResp.runId, client, target);
1374
+ recoveryMadeProgress = true;
1375
+ }
1376
+ qualityTracker.recordToolUse(recoveryAction.type);
1377
+ investigationState.recordToolUse(recoveryAction.type);
1378
+ if (recoveryMadeProgress) {
1379
+ scanState.recovery.successfulRecoveryAttempts++;
1380
+ scanState.recovery.consecutiveRecoveryFailures = 0;
1381
+ consecutiveBlockedReads = 0;
1382
+ scanState.recovery.consecutiveBlockedActions = 0;
1383
+ scanState.recovery.consecutiveModelBlockedActions = 0;
1384
+ meaningfulProgressSinceRecovery = true;
1385
+ }
1386
+ else {
1387
+ scanState.recovery.consecutiveRecoveryFailures++;
1388
+ scanState.recovery.lastFailureReason = `Recovery action made no progress: ${recoveryAction.type}`;
1389
+ }
1390
+ trace.logToolCompleted(recoveryAction.type, recoveryObservation);
1391
+ transcript.push({ action: recoveryAction, observation: `[DETERMINISTIC RECOVERY] ${recoveryObservation}` });
1392
+ }
1393
+ }
1394
+ else {
1395
+ // Scheduler has no action — finish or let model try
1396
+ if (schedDecision.kind === 'finish-ready') {
1397
+ console.warn(`[Agent Scan Loop] Scheduler says finish-ready during recovery — terminating as incomplete.`);
1398
+ transcript.push({ action, observation });
1399
+ trace.logRunCompleted('incomplete');
1400
+ (0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', 'Scheduler has no executable work during recovery.');
1401
+ const covSummary = investigationState.getCoverageSummary(target.filePath);
1402
+ qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
1403
+ qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
1404
+ return {
1405
+ status: 'incomplete',
1406
+ findings: [],
1407
+ transcript,
1408
+ stepsUsed: stepsTaken,
1409
+ stepsGranted,
1410
+ extensionsGranted,
1411
+ costSpentUsd,
1412
+ terminationReason: 'blocked_read_recovery',
1413
+ summary: 'Investigation incomplete — scheduler determined no executable work remains during recovery.',
1414
+ investigationNotes: [],
1415
+ coverageGaps: [],
1416
+ };
1272
1417
  }
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
1418
  }
1280
1419
  }
1281
1420
  }
1282
1421
  }
1283
1422
  catch (err) {
1284
1423
  return {
1285
- status: 'spawn_failed',
1424
+ status: 'failed',
1286
1425
  findings: [],
1287
1426
  transcript: [],
1288
1427
  stepsUsed: stepsTaken,
@@ -1467,49 +1606,4 @@ function describeAction(action) {
1467
1606
  case 'system_event': return `system_event(${action.eventType})`;
1468
1607
  }
1469
1608
  }
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
1609
  //# sourceMappingURL=agentScanLoop.js.map