@vibecheckai/cli 3.7.0 → 3.9.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 (118) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
  26. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  27. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  28. package/bin/runners/lib/artifact-envelope.js +540 -0
  29. package/bin/runners/lib/auth-shared.js +977 -0
  30. package/bin/runners/lib/checkpoint.js +941 -0
  31. package/bin/runners/lib/cleanup/engine.js +571 -0
  32. package/bin/runners/lib/cleanup/index.js +53 -0
  33. package/bin/runners/lib/cleanup/output.js +375 -0
  34. package/bin/runners/lib/cleanup/rules.js +1060 -0
  35. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  36. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  37. package/bin/runners/lib/doctor/fix-script.js +336 -0
  38. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  39. package/bin/runners/lib/doctor/modules/index.js +62 -3
  40. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  41. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  42. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  43. package/bin/runners/lib/engine/ast-cache.js +210 -210
  44. package/bin/runners/lib/engine/auth-extractor.js +211 -211
  45. package/bin/runners/lib/engine/billing-extractor.js +112 -112
  46. package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
  47. package/bin/runners/lib/engine/env-extractor.js +207 -207
  48. package/bin/runners/lib/engine/express-extractor.js +208 -208
  49. package/bin/runners/lib/engine/extractors.js +849 -849
  50. package/bin/runners/lib/engine/index.js +207 -207
  51. package/bin/runners/lib/engine/repo-index.js +514 -514
  52. package/bin/runners/lib/engine/types.js +124 -124
  53. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  54. package/bin/runners/lib/entitlements-v2.js +2 -2
  55. package/bin/runners/lib/missions/briefing.js +427 -0
  56. package/bin/runners/lib/missions/checkpoint.js +753 -0
  57. package/bin/runners/lib/missions/hardening.js +851 -0
  58. package/bin/runners/lib/missions/plan.js +421 -32
  59. package/bin/runners/lib/missions/safety-gates.js +645 -0
  60. package/bin/runners/lib/missions/schema.js +478 -0
  61. package/bin/runners/lib/packs/bundle.js +675 -0
  62. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  63. package/bin/runners/lib/packs/pack-factory.js +837 -0
  64. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  65. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  66. package/bin/runners/lib/safelist/index.js +96 -0
  67. package/bin/runners/lib/safelist/integration.js +334 -0
  68. package/bin/runners/lib/safelist/matcher.js +696 -0
  69. package/bin/runners/lib/safelist/schema.js +948 -0
  70. package/bin/runners/lib/safelist/store.js +438 -0
  71. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  72. package/bin/runners/lib/ship-gate.js +832 -0
  73. package/bin/runners/lib/ship-manifest.js +1153 -0
  74. package/bin/runners/lib/ship-output.js +1 -1
  75. package/bin/runners/lib/unified-cli-output.js +710 -383
  76. package/bin/runners/lib/upsell.js +3 -3
  77. package/bin/runners/lib/why-tree.js +650 -0
  78. package/bin/runners/runAllowlist.js +33 -4
  79. package/bin/runners/runApprove.js +240 -1122
  80. package/bin/runners/runAudit.js +692 -0
  81. package/bin/runners/runAuth.js +325 -29
  82. package/bin/runners/runCheckpoint.js +442 -494
  83. package/bin/runners/runCleanup.js +343 -0
  84. package/bin/runners/runDoctor.js +269 -19
  85. package/bin/runners/runFix.js +411 -32
  86. package/bin/runners/runForge.js +411 -0
  87. package/bin/runners/runIntent.js +906 -0
  88. package/bin/runners/runKickoff.js +878 -0
  89. package/bin/runners/runLaunch.js +2000 -0
  90. package/bin/runners/runLink.js +785 -0
  91. package/bin/runners/runMcp.js +1741 -837
  92. package/bin/runners/runPacks.js +2089 -0
  93. package/bin/runners/runPolish.js +41 -0
  94. package/bin/runners/runReality.js +178 -1
  95. package/bin/runners/runSafelist.js +1190 -0
  96. package/bin/runners/runScan.js +21 -9
  97. package/bin/runners/runShield.js +1282 -0
  98. package/bin/runners/runShip.js +395 -16
  99. package/bin/vibecheck.js +34 -6
  100. package/mcp-server/README.md +117 -158
  101. package/mcp-server/handlers/index.ts +2 -2
  102. package/mcp-server/handlers/tool-handler.ts +50 -11
  103. package/mcp-server/index.js +16 -0
  104. package/mcp-server/intent-firewall-interceptor.js +529 -0
  105. package/mcp-server/lib/executor.ts +5 -5
  106. package/mcp-server/lib/index.ts +14 -4
  107. package/mcp-server/lib/sandbox.test.ts +4 -4
  108. package/mcp-server/lib/sandbox.ts +2 -2
  109. package/mcp-server/manifest.json +473 -0
  110. package/mcp-server/package.json +1 -1
  111. package/mcp-server/registry/tool-registry.js +315 -523
  112. package/mcp-server/registry/tools.json +442 -428
  113. package/mcp-server/registry.test.ts +18 -12
  114. package/mcp-server/tier-auth.js +68 -11
  115. package/mcp-server/tools-v3.js +70 -16
  116. package/mcp-server/tsconfig.json +1 -0
  117. package/package.json +2 -1
  118. package/bin/runners/runProof.zip +0 -0
@@ -1,8 +1,24 @@
1
1
  // bin/runners/lib/missions/plan.js
2
2
  // ═══════════════════════════════════════════════════════════════════════════════
3
- // MISSION PLANNING - Hardened with confidence scoring and better deduplication
3
+ // MISSION PLANNING V2 - Enhanced with dependency-aware grouping, blast radius
4
+ // analysis, and risk-based batching. "Missions, not chaos."
4
5
  // ═══════════════════════════════════════════════════════════════════════════════
5
6
 
7
+ const {
8
+ createMission,
9
+ calculateBlastRadius,
10
+ calculateRiskLevel,
11
+ RISK_LEVEL,
12
+ BLAST_RADIUS,
13
+ } = require('./schema');
14
+ const { templateForMissionType } = require('./templates');
15
+ const {
16
+ ValidationError,
17
+ validateFinding,
18
+ validateOptions,
19
+ getAuditTrail,
20
+ } = require('./hardening');
21
+
6
22
  /**
7
23
  * Score a finding for priority ordering
8
24
  * Enhanced with confidence-based scoring
@@ -131,54 +147,239 @@ const MISSION_PRIORITY = {
131
147
  };
132
148
 
133
149
  /**
134
- * Create a mission from a finding
150
+ * Extract files from findings
151
+ * @param {Array} findings - Findings array
152
+ * @returns {string[]} Unique file paths
153
+ */
154
+ function extractFilesFromFindings(findings) {
155
+ const files = new Set();
156
+ for (const f of findings) {
157
+ if (f.file) files.add(f.file);
158
+ for (const ev of (f.evidence || [])) {
159
+ if (ev.file) files.add(ev.file);
160
+ if (ev.path) files.add(ev.path);
161
+ }
162
+ }
163
+ return Array.from(files);
164
+ }
165
+
166
+ /**
167
+ * Build a simple import graph from truthpack
168
+ * @param {object} truthpack - Truthpack object
169
+ * @returns {Map} Map of file -> imported files
170
+ */
171
+ function buildImportGraph(truthpack) {
172
+ const graph = new Map();
173
+
174
+ // Extract imports from truthpack if available
175
+ const imports = truthpack?.imports || truthpack?.dependencies?.imports || [];
176
+ for (const imp of imports) {
177
+ if (imp.from && imp.to) {
178
+ if (!graph.has(imp.from)) graph.set(imp.from, new Set());
179
+ graph.get(imp.from).add(imp.to);
180
+ }
181
+ }
182
+
183
+ return graph;
184
+ }
185
+
186
+ /**
187
+ * Find connected components in file graph
188
+ * Files that import each other should be grouped together
189
+ * @param {string[]} files - List of files
190
+ * @param {Map} importGraph - Import graph
191
+ * @returns {string[][]} Array of file clusters
192
+ */
193
+ function findConnectedFileClusters(files, importGraph) {
194
+ const fileSet = new Set(files);
195
+ const visited = new Set();
196
+ const clusters = [];
197
+
198
+ function dfs(file, cluster) {
199
+ if (visited.has(file)) return;
200
+ visited.add(file);
201
+ cluster.push(file);
202
+
203
+ // Check files this one imports
204
+ const imports = importGraph.get(file) || new Set();
205
+ for (const imported of imports) {
206
+ if (fileSet.has(imported)) {
207
+ dfs(imported, cluster);
208
+ }
209
+ }
210
+
211
+ // Check files that import this one
212
+ for (const [from, toSet] of importGraph) {
213
+ if (toSet.has(file) && fileSet.has(from)) {
214
+ dfs(from, cluster);
215
+ }
216
+ }
217
+ }
218
+
219
+ for (const file of files) {
220
+ if (!visited.has(file)) {
221
+ const cluster = [];
222
+ dfs(file, cluster);
223
+ if (cluster.length > 0) {
224
+ clusters.push(cluster);
225
+ }
226
+ }
227
+ }
228
+
229
+ return clusters;
230
+ }
231
+
232
+ /**
233
+ * Calculate cluster risk score
234
+ * @param {Array} findings - Findings in cluster
235
+ * @returns {number} Risk score 0-100
236
+ */
237
+ function calculateClusterRisk(findings) {
238
+ let score = 0;
239
+
240
+ for (const f of findings) {
241
+ // Severity contribution
242
+ if (f.severity === 'BLOCK') score += 30;
243
+ else if (f.severity === 'WARN') score += 15;
244
+ else score += 5;
245
+
246
+ // Confidence inverse (lower confidence = higher risk)
247
+ const confidence = f.confidence || 0.5;
248
+ score += Math.round((1 - confidence) * 10);
249
+ }
250
+
251
+ // Normalize by finding count, cap at 100
252
+ return Math.min(100, score);
253
+ }
254
+
255
+ /**
256
+ * Create a mission from a finding using the new schema
135
257
  * Enhanced with confidence and better metadata
136
258
  */
137
- function missionFromFinding(f, relatedFindings = []) {
259
+ function missionFromFinding(f, relatedFindings = [], options = {}) {
138
260
  const type = CATEGORY_TO_MISSION_TYPE[f.category] || "GENERIC_FIX";
139
261
  const allFindingIds = [f.id, ...relatedFindings.map(r => r.id)];
262
+ const allFindings = [f, ...relatedFindings];
140
263
 
141
264
  // Calculate mission confidence based on findings
142
265
  const confidences = [f.confidence || 0.5, ...relatedFindings.map(r => r.confidence || 0.5)];
143
266
  const avgConfidence = confidences.reduce((a, b) => a + b, 0) / confidences.length;
144
267
 
145
- return {
146
- id: `M_${f.id}`,
268
+ // Extract all files from findings
269
+ const allowedFiles = extractFilesFromFindings(allFindings);
270
+
271
+ // Get template for this mission type
272
+ const template = templateForMissionType(type);
273
+
274
+ // Use the new schema to create a proper mission object
275
+ return createMission({
147
276
  type,
148
277
  title: f.title,
149
278
  severity: f.severity,
150
279
  category: f.category,
151
- confidence: avgConfidence,
152
- successCriteria: [
153
- `Finding ${f.id} no longer appears in ship results`,
154
- ...(relatedFindings.length > 0 ?
155
- [`${relatedFindings.length} related finding(s) also resolved`] : []
156
- )
157
- ],
158
280
  targetFindingIds: allFindingIds,
159
- findingCount: allFindingIds.length,
160
- // Include evidence for the LLM context
281
+ template,
282
+ allowedFiles,
283
+ readOnlyContext: options.readOnlyContext || [],
284
+ confidence: avgConfidence,
161
285
  evidence: f.evidence || [],
162
286
  file: f.file || null,
163
- };
287
+ });
164
288
  }
165
289
 
166
290
  /**
167
291
  * Group related findings that can be fixed together
168
292
  * E.g., multiple Dead UI issues in the same file
293
+ * Now with dependency-aware clustering
169
294
  */
170
- function groupRelatedFindings(findings) {
295
+ function groupRelatedFindings(findings, options = {}) {
296
+ const { importGraph = new Map(), maxClusterRisk = 80 } = options;
171
297
  const groups = new Map();
172
298
 
299
+ // First pass: group by category
300
+ const byCategory = new Map();
173
301
  for (const f of findings) {
174
- // Group key: category + file (if available)
175
- const file = f.file || f.evidence?.[0]?.file || 'unknown';
176
- const groupKey = `${f.category}:${file}`;
302
+ const cat = f.category || 'Unknown';
303
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
304
+ byCategory.get(cat).push(f);
305
+ }
306
+
307
+ // Second pass: within each category, cluster by file dependencies
308
+ for (const [category, catFindings] of byCategory) {
309
+ const files = extractFilesFromFindings(catFindings);
310
+ const clusters = findConnectedFileClusters(files, importGraph);
311
+
312
+ // Map files to their cluster index
313
+ const fileToCluster = new Map();
314
+ clusters.forEach((cluster, idx) => {
315
+ for (const file of cluster) {
316
+ fileToCluster.set(file, idx);
317
+ }
318
+ });
177
319
 
178
- if (!groups.has(groupKey)) {
179
- groups.set(groupKey, []);
320
+ // Group findings by their file's cluster
321
+ const clusterGroups = new Map();
322
+ for (const f of catFindings) {
323
+ const file = f.file || f.evidence?.[0]?.file || f.evidence?.[0]?.path || 'unknown';
324
+ const clusterIdx = fileToCluster.get(file) ?? -1;
325
+ const groupKey = `${category}:cluster_${clusterIdx}:${file}`;
326
+
327
+ if (!clusterGroups.has(groupKey)) {
328
+ clusterGroups.set(groupKey, []);
329
+ }
330
+ clusterGroups.get(groupKey).push(f);
331
+ }
332
+
333
+ // Third pass: split groups that exceed risk threshold
334
+ for (const [groupKey, groupFindings] of clusterGroups) {
335
+ const risk = calculateClusterRisk(groupFindings);
336
+
337
+ if (risk > maxClusterRisk && groupFindings.length > 1) {
338
+ // Split into individual findings for high-risk groups
339
+ for (let i = 0; i < groupFindings.length; i++) {
340
+ groups.set(`${groupKey}:split_${i}`, [groupFindings[i]]);
341
+ }
342
+ } else {
343
+ groups.set(groupKey, groupFindings);
344
+ }
345
+ }
346
+ }
347
+
348
+ return groups;
349
+ }
350
+
351
+ /**
352
+ * Advanced grouping with blast radius analysis
353
+ * Groups findings while respecting blast radius limits
354
+ */
355
+ function groupWithBlastRadiusLimit(findings, options = {}) {
356
+ const { maxBlastRadius = 5, importGraph = new Map() } = options;
357
+ const groups = new Map();
358
+
359
+ // Start with basic grouping
360
+ const basicGroups = groupRelatedFindings(findings, { importGraph });
361
+
362
+ // Check each group's blast radius
363
+ for (const [key, groupFindings] of basicGroups) {
364
+ const files = extractFilesFromFindings(groupFindings);
365
+
366
+ if (files.length > maxBlastRadius) {
367
+ // Split into smaller groups by file
368
+ const byFile = new Map();
369
+ for (const f of groupFindings) {
370
+ const file = f.file || f.evidence?.[0]?.file || 'unknown';
371
+ if (!byFile.has(file)) byFile.set(file, []);
372
+ byFile.get(file).push(f);
373
+ }
374
+
375
+ // Create separate groups for each file
376
+ let idx = 0;
377
+ for (const [file, fileFindings] of byFile) {
378
+ groups.set(`${key}:file_${idx++}`, fileFindings);
379
+ }
380
+ } else {
381
+ groups.set(key, groupFindings);
180
382
  }
181
- groups.get(groupKey).push(f);
182
383
  }
183
384
 
184
385
  return groups;
@@ -189,11 +390,105 @@ function groupRelatedFindings(findings) {
189
390
  *
190
391
  * @param {Array} findings - List of findings from ship/scan
191
392
  * @param {Object} options - Planning options
393
+ * @param {number} [options.maxMissions=12] - Maximum missions to plan
394
+ * @param {boolean} [options.blocksOnlyFirst=true] - Prioritize BLOCK findings
395
+ * @param {boolean} [options.groupRelated=true] - Group related findings
396
+ * @param {object} [options.truthpack] - Truthpack for dependency analysis
397
+ * @param {number} [options.maxBlastRadius=5] - Maximum files per mission
398
+ * @param {number} [options.maxClusterRisk=80] - Maximum cluster risk score
399
+ * @param {number} [options.minConfidence=0] - Minimum confidence threshold
192
400
  * @returns {Array} Planned missions
193
401
  */
194
- function planMissions(findings, { maxMissions = 12, blocksOnlyFirst = true, groupRelated = true } = {}) {
402
+ function planMissions(findings, options = {}) {
403
+ const audit = getAuditTrail();
404
+
405
+ // ═══════════════════════════════════════════════════════════════════════════════
406
+ // INPUT VALIDATION
407
+ // ═══════════════════════════════════════════════════════════════════════════════
408
+
409
+ // Handle null/undefined findings
410
+ if (!findings) {
411
+ audit.warn('plan_missions_no_findings', { findings });
412
+ return [];
413
+ }
414
+
415
+ // Handle non-array findings
416
+ if (!Array.isArray(findings)) {
417
+ audit.error('plan_missions_invalid_findings', { type: typeof findings });
418
+ throw new ValidationError('findings must be an array', 'findings', findings);
419
+ }
420
+
421
+ // Handle empty findings
422
+ if (findings.length === 0) {
423
+ audit.info('plan_missions_empty_findings');
424
+ return [];
425
+ }
426
+
427
+ // Validate and sanitize options
428
+ const optionsSchema = {
429
+ maxMissions: { type: 'number', default: 12, min: 1, max: 100 },
430
+ blocksOnlyFirst: { type: 'boolean', default: true },
431
+ groupRelated: { type: 'boolean', default: true },
432
+ maxBlastRadius: { type: 'number', default: 5, min: 1, max: 50 },
433
+ maxClusterRisk: { type: 'number', default: 80, min: 0, max: 100 },
434
+ minConfidence: { type: 'number', default: 0, min: 0, max: 1 },
435
+ };
436
+
437
+ const validatedOptions = validateOptions(options, optionsSchema);
438
+ if (!validatedOptions.valid) {
439
+ audit.warn('plan_missions_invalid_options', { errors: validatedOptions.errors });
440
+ }
441
+
442
+ const {
443
+ maxMissions,
444
+ blocksOnlyFirst,
445
+ groupRelated,
446
+ maxBlastRadius,
447
+ maxClusterRisk,
448
+ minConfidence,
449
+ } = validatedOptions.sanitized;
450
+
451
+ const truthpack = options.truthpack || null;
452
+
453
+ audit.info('plan_missions_start', {
454
+ findingCount: findings.length,
455
+ maxMissions,
456
+ blocksOnlyFirst,
457
+ groupRelated,
458
+ });
459
+
460
+ // Build import graph from truthpack if available
461
+ const importGraph = truthpack ? buildImportGraph(truthpack) : new Map();
462
+
463
+ // ═══════════════════════════════════════════════════════════════════════════════
464
+ // FINDING VALIDATION & FILTERING
465
+ // ═══════════════════════════════════════════════════════════════════════════════
466
+
467
+ // Validate and filter findings
468
+ const validFindings = [];
469
+ let invalidCount = 0;
470
+
471
+ for (const f of findings) {
472
+ const validation = validateFinding(f);
473
+ if (validation.valid) {
474
+ validFindings.push(f);
475
+ } else {
476
+ invalidCount++;
477
+ audit.debug('plan_missions_invalid_finding', { id: f?.id, errors: validation.errors });
478
+ }
479
+ }
480
+
481
+ if (invalidCount > 0) {
482
+ audit.warn('plan_missions_invalid_findings_skipped', { invalidCount, validCount: validFindings.length });
483
+ }
484
+
485
+ if (validFindings.length === 0) {
486
+ audit.info('plan_missions_no_valid_findings');
487
+ return [];
488
+ }
489
+
195
490
  // Step 1: Sort by score (severity + confidence + evidence)
196
- const sorted = [...findings].sort((a, b) => scoreFinding(b) - scoreFinding(a));
491
+ const sorted = [...validFindings].sort((a, b) => scoreFinding(b) - scoreFinding(a));
197
492
 
198
493
  // Step 2: Filter to BLOCKs only if we have them (cost control)
199
494
  const hasBlocks = sorted.some(f => f.severity === "BLOCK");
@@ -217,43 +512,137 @@ function planMissions(findings, { maxMissions = 12, blocksOnlyFirst = true, grou
217
512
  if (f.severity === "WARN" && seenFingerprints.has(nearDupeKey)) continue;
218
513
  seenFingerprints.add(nearDupeKey);
219
514
 
515
+ // Filter by minimum confidence if specified
516
+ const confidence = f.confidence || 0.5;
517
+ if (confidence < minConfidence) continue;
518
+
220
519
  deduplicated.push(f);
221
520
  }
222
521
 
223
- // Step 4: Group related findings (optional - reduces noise)
522
+ // Step 4: Group related findings with enhanced heuristics
224
523
  let missions = [];
225
524
 
226
525
  if (groupRelated) {
227
- const groups = groupRelatedFindings(deduplicated);
526
+ // Use blast radius-aware grouping
527
+ const groups = groupWithBlastRadiusLimit(deduplicated, {
528
+ maxBlastRadius,
529
+ importGraph,
530
+ maxClusterRisk,
531
+ });
228
532
 
229
533
  for (const [groupKey, groupFindings] of groups) {
230
534
  // Take the highest severity finding as primary
231
535
  const primary = groupFindings[0]; // Already sorted by score
232
536
  const related = groupFindings.slice(1, 5); // Limit related findings
233
537
 
234
- missions.push(missionFromFinding(primary, related));
538
+ missions.push(missionFromFinding(primary, related, { importGraph }));
235
539
  }
236
540
  } else {
237
541
  missions = deduplicated.map(f => missionFromFinding(f));
238
542
  }
239
543
 
240
- // Step 5: Sort by priority and limit
544
+ // Step 5: Sort by priority and risk
241
545
  missions.sort((a, b) => {
242
546
  const prioA = MISSION_PRIORITY[a.type] || 50;
243
547
  const prioB = MISSION_PRIORITY[b.type] || 50;
244
548
  if (prioA !== prioB) return prioA - prioB;
245
549
 
246
- // Secondary sort by confidence (higher first)
247
- return (b.confidence || 0.5) - (a.confidence || 0.5);
550
+ // Secondary sort by risk level (lower risk first for safety)
551
+ const riskOrder = { low: 0, medium: 1, high: 2, critical: 3 };
552
+ const riskA = riskOrder[a.safety?.riskLevel] ?? 1;
553
+ const riskB = riskOrder[b.safety?.riskLevel] ?? 1;
554
+ if (riskA !== riskB) return riskA - riskB;
555
+
556
+ // Tertiary sort by confidence (higher first)
557
+ return (b.safety?.confidence || 0.5) - (a.safety?.confidence || 0.5);
248
558
  });
249
559
 
250
- return missions.slice(0, maxMissions);
560
+ const result = missions.slice(0, maxMissions);
561
+
562
+ // Log planning results
563
+ audit.info('plan_missions_complete', {
564
+ inputFindings: findings.length,
565
+ validFindings: validFindings.length,
566
+ deduplicated: deduplicated.length,
567
+ missions: result.length,
568
+ missionTypes: result.map(m => m.type),
569
+ });
570
+
571
+ return result;
572
+ }
573
+
574
+ /**
575
+ * Plan a single mission for a specific finding
576
+ * @param {object} finding - The finding to create a mission for
577
+ * @param {object} options - Planning options
578
+ * @returns {object} Mission object
579
+ */
580
+ function planSingleMission(finding, options = {}) {
581
+ return missionFromFinding(finding, [], options);
582
+ }
583
+
584
+ /**
585
+ * Get mission statistics
586
+ * @param {Array} missions - Array of missions
587
+ * @returns {object} Statistics object
588
+ */
589
+ function getMissionStats(missions) {
590
+ const stats = {
591
+ total: missions.length,
592
+ byType: {},
593
+ byRisk: { low: 0, medium: 0, high: 0, critical: 0 },
594
+ byBlastRadius: { low: 0, medium: 0, high: 0 },
595
+ totalFindings: 0,
596
+ avgConfidence: 0,
597
+ };
598
+
599
+ let totalConfidence = 0;
600
+
601
+ for (const m of missions) {
602
+ // By type
603
+ stats.byType[m.type] = (stats.byType[m.type] || 0) + 1;
604
+
605
+ // By risk
606
+ const risk = m.safety?.riskLevel || 'medium';
607
+ stats.byRisk[risk] = (stats.byRisk[risk] || 0) + 1;
608
+
609
+ // By blast radius
610
+ const blast = m.scope?.blastRadius || 'medium';
611
+ stats.byBlastRadius[blast] = (stats.byBlastRadius[blast] || 0) + 1;
612
+
613
+ // Finding count
614
+ stats.totalFindings += m.objective?.findingCount || 1;
615
+
616
+ // Confidence
617
+ totalConfidence += m.safety?.confidence || 0.5;
618
+ }
619
+
620
+ stats.avgConfidence = missions.length > 0
621
+ ? Math.round((totalConfidence / missions.length) * 100) / 100
622
+ : 0;
623
+
624
+ return stats;
251
625
  }
252
626
 
253
627
  module.exports = {
628
+ // Main planning functions
254
629
  planMissions,
630
+ planSingleMission,
631
+ getMissionStats,
632
+
633
+ // Grouping functions
634
+ groupRelatedFindings,
635
+ groupWithBlastRadiusLimit,
636
+
637
+ // Utility functions
255
638
  scoreFinding,
256
639
  generateFingerprint,
640
+ extractFilesFromFindings,
641
+ buildImportGraph,
642
+ findConnectedFileClusters,
643
+ calculateClusterRisk,
644
+
645
+ // Constants
257
646
  CATEGORY_TO_MISSION_TYPE,
258
- MISSION_PRIORITY
647
+ MISSION_PRIORITY,
259
648
  };