@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.
Files changed (46) 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 +173 -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/icon.svg +6 -0
  46. 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: 'spawn_failed',
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: 'capped',
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 (skip API)
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 nextRange = target.fileContent
300
- ? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
301
- : investigationState.getNextUnreadRange(target.filePath);
302
- if (nextRange) {
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
- 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).`,
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: 'capped',
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: 'spawn_failed',
402
+ status: 'failed',
400
403
  findings: [],
401
404
  transcript,
402
405
  stepsUsed: stepsTaken,
403
406
  stepsGranted,
404
407
  extensionsGranted,
405
408
  costSpentUsd,
406
- terminationReason: 'api_error',
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: 'capped',
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
- ? 'capped'
524
- : (stepResp.degraded ? 'degraded' : 'completed');
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
- 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
- }
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 (nextRange && hasBudget && hasWallClock) {
982
- console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery to unread range ${nextRange.start}-${nextRange.end}.`);
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, force-finishing.`);
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('completed');
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 force-finished after repeated blocked reads without completing this required investigation step: ${step}. The investigation was incomplete and vulnerabilities may have been missed.`,
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: 'completed',
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 MCP. This prevents wasting API calls on an agent that is
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 recoveryAction = selectDeterministicRecoveryAction(investigationState, target, ctx);
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
- 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);
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
- recoveryObservation = await (0, agentScanExecutor_1.executeAction)(recoveryAction, ctx, startResp.runId, client, target);
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: 'spawn_failed',
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