@neurcode-ai/cli 0.9.31 → 0.9.32

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 (51) hide show
  1. package/README.md +22 -0
  2. package/dist/commands/apply.d.ts.map +1 -1
  3. package/dist/commands/apply.js +45 -3
  4. package/dist/commands/apply.js.map +1 -1
  5. package/dist/commands/map.d.ts.map +1 -1
  6. package/dist/commands/map.js +78 -1
  7. package/dist/commands/map.js.map +1 -1
  8. package/dist/commands/plan-slo.d.ts +7 -0
  9. package/dist/commands/plan-slo.d.ts.map +1 -0
  10. package/dist/commands/plan-slo.js +205 -0
  11. package/dist/commands/plan-slo.js.map +1 -0
  12. package/dist/commands/plan.d.ts.map +1 -1
  13. package/dist/commands/plan.js +665 -29
  14. package/dist/commands/plan.js.map +1 -1
  15. package/dist/commands/repo.d.ts +3 -0
  16. package/dist/commands/repo.d.ts.map +1 -0
  17. package/dist/commands/repo.js +166 -0
  18. package/dist/commands/repo.js.map +1 -0
  19. package/dist/commands/ship.d.ts.map +1 -1
  20. package/dist/commands/ship.js +29 -0
  21. package/dist/commands/ship.js.map +1 -1
  22. package/dist/commands/verify.d.ts.map +1 -1
  23. package/dist/commands/verify.js +261 -9
  24. package/dist/commands/verify.js.map +1 -1
  25. package/dist/index.js +17 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/services/mapper/ProjectScanner.d.ts +76 -2
  28. package/dist/services/mapper/ProjectScanner.d.ts.map +1 -1
  29. package/dist/services/mapper/ProjectScanner.js +545 -40
  30. package/dist/services/mapper/ProjectScanner.js.map +1 -1
  31. package/dist/services/security/SecurityGuard.d.ts +21 -2
  32. package/dist/services/security/SecurityGuard.d.ts.map +1 -1
  33. package/dist/services/security/SecurityGuard.js +130 -27
  34. package/dist/services/security/SecurityGuard.js.map +1 -1
  35. package/dist/utils/governance.d.ts +2 -0
  36. package/dist/utils/governance.d.ts.map +1 -1
  37. package/dist/utils/governance.js +2 -0
  38. package/dist/utils/governance.js.map +1 -1
  39. package/dist/utils/plan-slo.d.ts +73 -0
  40. package/dist/utils/plan-slo.d.ts.map +1 -0
  41. package/dist/utils/plan-slo.js +271 -0
  42. package/dist/utils/plan-slo.js.map +1 -0
  43. package/dist/utils/project-root.d.ts +5 -4
  44. package/dist/utils/project-root.d.ts.map +1 -1
  45. package/dist/utils/project-root.js +82 -7
  46. package/dist/utils/project-root.js.map +1 -1
  47. package/dist/utils/repo-links.d.ts +17 -0
  48. package/dist/utils/repo-links.d.ts.map +1 -0
  49. package/dist/utils/repo-links.js +136 -0
  50. package/dist/utils/repo-links.js.map +1 -0
  51. package/package.json +3 -3
@@ -49,6 +49,7 @@ const plan_cache_1 = require("../utils/plan-cache");
49
49
  const neurcode_context_1 = require("../utils/neurcode-context");
50
50
  const brain_context_1 = require("../utils/brain-context");
51
51
  const project_root_1 = require("../utils/project-root");
52
+ const plan_slo_1 = require("../utils/plan-slo");
52
53
  // Import chalk with fallback for plain strings if not available
53
54
  let chalk;
54
55
  try {
@@ -84,6 +85,12 @@ function scanFiles(dir, baseDir, maxFiles = 200) {
84
85
  break;
85
86
  const fullPath = (0, path_1.join)(currentDir, entry);
86
87
  const relativePath = (0, path_1.relative)(baseDir, fullPath);
88
+ const normalizedRelativePath = toUnixPath(relativePath);
89
+ if (normalizedRelativePath === '' ||
90
+ normalizedRelativePath === '.' ||
91
+ normalizedRelativePath.startsWith('..')) {
92
+ continue;
93
+ }
87
94
  // Skip hidden files and directories
88
95
  if (entry.startsWith('.')) {
89
96
  // Allow .env, .gitignore, etc. but skip .git, .next, etc.
@@ -97,7 +104,11 @@ function scanFiles(dir, baseDir, maxFiles = 200) {
97
104
  if (ignoreDirs.has(entry))
98
105
  continue;
99
106
  try {
100
- const stat = (0, fs_1.statSync)(fullPath);
107
+ const stat = (0, fs_1.lstatSync)(fullPath);
108
+ if (stat.isSymbolicLink()) {
109
+ // Skip symlinked entries to avoid escaping repository boundaries.
110
+ continue;
111
+ }
101
112
  if (stat.isDirectory()) {
102
113
  scan(fullPath);
103
114
  }
@@ -190,35 +201,316 @@ function displayPlan(plan) {
190
201
  }
191
202
  console.log('');
192
203
  }
193
- /**
194
- * Ensure asset map exists, creating it silently if needed
195
- */
196
- async function ensureAssetMap(cwd) {
197
- let map = (0, map_1.loadAssetMap)(cwd);
198
- if (!map) {
199
- // Silently create the map if it doesn't exist
200
- try {
201
- const { ProjectScanner } = await Promise.resolve().then(() => __importStar(require('../services/mapper/ProjectScanner')));
202
- const scanner = new ProjectScanner(cwd);
203
- map = await scanner.scan();
204
- // Save it
205
- const { writeFileSync, mkdirSync } = await Promise.resolve().then(() => __importStar(require('fs')));
206
- const neurcodeDir = (0, path_1.join)(cwd, '.neurcode');
207
- if (!(0, fs_1.existsSync)(neurcodeDir)) {
208
- mkdirSync(neurcodeDir, { recursive: true });
209
- }
210
- const mapPath = (0, path_1.join)(neurcodeDir, 'asset-map.json');
211
- writeFileSync(mapPath, JSON.stringify(map, null, 2) + '\n', 'utf-8');
204
+ function parseRatio(raw) {
205
+ if (!raw)
206
+ return null;
207
+ const parsed = Number(raw);
208
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1)
209
+ return null;
210
+ return parsed;
211
+ }
212
+ function parsePercent(raw) {
213
+ if (!raw)
214
+ return null;
215
+ const parsed = Number(raw);
216
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100)
217
+ return null;
218
+ return Math.floor(parsed);
219
+ }
220
+ function getEscalationGuardPath(cwd) {
221
+ return (0, path_1.join)(cwd, '.neurcode', 'asset-map-escalation-guard.json');
222
+ }
223
+ function loadEscalationGuardState(cwd) {
224
+ const pathValue = getEscalationGuardPath(cwd);
225
+ if (!(0, fs_1.existsSync)(pathValue)) {
226
+ return {
227
+ version: 1,
228
+ updatedAt: new Date(0).toISOString(),
229
+ consecutiveBreaches: 0,
230
+ };
231
+ }
232
+ try {
233
+ const raw = JSON.parse((0, fs_1.readFileSync)(pathValue, 'utf-8'));
234
+ return {
235
+ version: 1,
236
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : new Date(0).toISOString(),
237
+ consecutiveBreaches: typeof raw.consecutiveBreaches === 'number' && Number.isFinite(raw.consecutiveBreaches) && raw.consecutiveBreaches > 0
238
+ ? Math.floor(raw.consecutiveBreaches)
239
+ : 0,
240
+ lastBreachAt: typeof raw.lastBreachAt === 'string' ? raw.lastBreachAt : undefined,
241
+ lastReason: typeof raw.lastReason === 'string' ? raw.lastReason : undefined,
242
+ cooldownUntil: typeof raw.cooldownUntil === 'string' ? raw.cooldownUntil : undefined,
243
+ };
244
+ }
245
+ catch {
246
+ return {
247
+ version: 1,
248
+ updatedAt: new Date(0).toISOString(),
249
+ consecutiveBreaches: 0,
250
+ };
251
+ }
252
+ }
253
+ function saveEscalationGuardState(cwd, state) {
254
+ const pathValue = getEscalationGuardPath(cwd);
255
+ const dir = (0, path_1.join)(cwd, '.neurcode');
256
+ if (!(0, fs_1.existsSync)(dir)) {
257
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
258
+ }
259
+ (0, fs_1.writeFileSync)(pathValue, JSON.stringify(state, null, 2) + '\n', 'utf-8');
260
+ }
261
+ function computeEscalationCanaryBucket(cwd) {
262
+ const seed = (process.env.NEURCODE_ASSET_MAP_ESCALATE_CANARY_SEED || 'neurcode-escalation-v1').trim();
263
+ const digest = (0, crypto_1.createHash)('sha1')
264
+ .update(`${(0, path_1.resolve)(cwd)}|${seed}`, 'utf-8')
265
+ .digest('hex');
266
+ const bucketRaw = parseInt(digest.slice(0, 8), 16);
267
+ if (!Number.isFinite(bucketRaw))
268
+ return 0;
269
+ return Math.abs(bucketRaw % 100);
270
+ }
271
+ function resolveEscalationPolicy(cwd) {
272
+ const enabledByEnv = parseBooleanFlag(process.env.NEURCODE_ASSET_MAP_ESCALATE_DEEPEN, true);
273
+ const canaryPercent = parsePercent(process.env.NEURCODE_ASSET_MAP_ESCALATE_CANARY_PERCENT) ?? 100;
274
+ const canaryBucket = computeEscalationCanaryBucket(cwd);
275
+ if (!enabledByEnv) {
276
+ return {
277
+ enabled: false,
278
+ reason: 'env_disabled',
279
+ canaryPercent,
280
+ canaryBucket,
281
+ };
282
+ }
283
+ const state = loadEscalationGuardState(cwd);
284
+ const nowMs = Date.now();
285
+ const cooldownMs = state.cooldownUntil ? Date.parse(state.cooldownUntil) : NaN;
286
+ if (Number.isFinite(cooldownMs) && cooldownMs > nowMs) {
287
+ return {
288
+ enabled: false,
289
+ reason: 'kill_switch_cooldown',
290
+ canaryPercent,
291
+ canaryBucket,
292
+ cooldownUntil: state.cooldownUntil,
293
+ };
294
+ }
295
+ if (canaryBucket >= canaryPercent) {
296
+ return {
297
+ enabled: false,
298
+ reason: 'canary_excluded',
299
+ canaryPercent,
300
+ canaryBucket,
301
+ };
302
+ }
303
+ return {
304
+ enabled: true,
305
+ reason: 'enabled',
306
+ canaryPercent,
307
+ canaryBucket,
308
+ };
309
+ }
310
+ function updateEscalationGuardForBuild(cwd, escalationEnabled, buildStartedAtMs) {
311
+ if (!escalationEnabled) {
312
+ return { killSwitchTripped: false };
313
+ }
314
+ const maxBuildMs = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_MAX_BUILD_MS) ?? 5000;
315
+ const maxRssKb = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_MAX_RSS_KB) ?? (512 * 1024);
316
+ const minBreaches = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_BREACHES) ?? 3;
317
+ const cooldownMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_COOLDOWN_MINUTES) ?? 60;
318
+ const elapsedMs = Math.max(0, Date.now() - buildStartedAtMs);
319
+ const rssKb = Math.max(0, Math.floor(process.memoryUsage().rss / 1024));
320
+ const breachReasons = [];
321
+ if (elapsedMs > maxBuildMs) {
322
+ breachReasons.push(`build_ms>${maxBuildMs}`);
323
+ }
324
+ if (rssKb > maxRssKb) {
325
+ breachReasons.push(`rss_kb>${maxRssKb}`);
326
+ }
327
+ const state = loadEscalationGuardState(cwd);
328
+ const nowIso = new Date().toISOString();
329
+ let killSwitchTripped = false;
330
+ let cooldownUntil;
331
+ if (breachReasons.length > 0) {
332
+ const nextBreaches = state.consecutiveBreaches + 1;
333
+ state.consecutiveBreaches = nextBreaches;
334
+ state.lastBreachAt = nowIso;
335
+ state.lastReason = breachReasons.join(',');
336
+ if (nextBreaches >= minBreaches) {
337
+ killSwitchTripped = true;
338
+ const cooldownMs = Date.now() + cooldownMinutes * 60 * 1000;
339
+ cooldownUntil = new Date(cooldownMs).toISOString();
340
+ state.cooldownUntil = cooldownUntil;
341
+ state.consecutiveBreaches = 0;
212
342
  }
213
- catch (error) {
214
- // If mapping fails, continue without it (graceful degradation)
215
- if (process.env.DEBUG) {
216
- console.warn(chalk.yellow(`⚠️ Could not generate asset map: ${error instanceof Error ? error.message : 'Unknown error'}`));
217
- }
218
- return null;
343
+ }
344
+ else {
345
+ state.consecutiveBreaches = 0;
346
+ state.lastReason = undefined;
347
+ state.lastBreachAt = undefined;
348
+ }
349
+ state.updatedAt = nowIso;
350
+ saveEscalationGuardState(cwd, state);
351
+ return {
352
+ killSwitchTripped,
353
+ cooldownUntil,
354
+ };
355
+ }
356
+ function computeAssetMapIntentFingerprint(intent) {
357
+ const tokens = (intent || '')
358
+ .toLowerCase()
359
+ .match(/[a-z0-9_]{3,}/g);
360
+ if (!tokens || tokens.length === 0)
361
+ return null;
362
+ const filtered = Array.from(new Set(tokens))
363
+ .filter((token) => token.length >= 3)
364
+ .slice(0, 24)
365
+ .sort();
366
+ if (filtered.length === 0)
367
+ return null;
368
+ return (0, crypto_1.createHash)('sha1').update(filtered.join('|'), 'utf-8').digest('hex');
369
+ }
370
+ function decideAssetMapRefresh(map, intentForAdaptiveDeepen) {
371
+ if (process.env.NEURCODE_ASSET_MAP_FORCE_REFRESH === '1') {
372
+ return { refresh: true, reason: 'forced_refresh' };
373
+ }
374
+ const nowMs = Date.now();
375
+ const scannedAtMs = Date.parse(map.scannedAt || '');
376
+ const ageMs = Number.isFinite(scannedAtMs) ? Math.max(0, nowMs - scannedAtMs) : Number.POSITIVE_INFINITY;
377
+ const maxAgeMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_AGE_MINUTES) ?? 360;
378
+ const minRefreshIntervalMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MIN_REFRESH_INTERVAL_MINUTES) ?? 20;
379
+ const minRefreshIntervalMs = minRefreshIntervalMinutes * 60 * 1000;
380
+ const staleByAge = ageMs > maxAgeMinutes * 60 * 1000;
381
+ if (staleByAge) {
382
+ return { refresh: true, reason: 'stale_age' };
383
+ }
384
+ const withinMinRefreshInterval = ageMs < minRefreshIntervalMs;
385
+ const stats = map.scanStats;
386
+ const indexedSourceFiles = Math.max(1, stats?.indexedSourceFiles || Object.keys(map.files || {}).length || 1);
387
+ const shallowIndexedSourceFiles = stats?.shallowIndexedSourceFiles || 0;
388
+ const shallowRatio = shallowIndexedSourceFiles / indexedSourceFiles;
389
+ const shallowRatioThreshold = parseRatio(process.env.NEURCODE_ASSET_MAP_SHALLOW_RATIO_REFRESH_THRESHOLD) ?? 0.3;
390
+ const shallowPressure = shallowIndexedSourceFiles > 0 && shallowRatio >= shallowRatioThreshold;
391
+ const shallowFailures = (stats?.shallowIndexFailures || 0) > 0;
392
+ const cappedCoverage = Boolean(stats?.cappedByMaxSourceFiles);
393
+ const refreshOnCapped = process.env.NEURCODE_ASSET_MAP_REFRESH_ON_CAPPED !== '0';
394
+ if (shallowFailures && !withinMinRefreshInterval) {
395
+ return { refresh: true, reason: 'shallow_index_failures' };
396
+ }
397
+ if (cappedCoverage && refreshOnCapped && !withinMinRefreshInterval) {
398
+ return { refresh: true, reason: 'capped_coverage' };
399
+ }
400
+ if (shallowPressure && !withinMinRefreshInterval) {
401
+ const currentIntentFingerprint = computeAssetMapIntentFingerprint(intentForAdaptiveDeepen);
402
+ const previousIntentFingerprint = map.scanContext?.adaptiveIntentFingerprint || null;
403
+ if (currentIntentFingerprint && previousIntentFingerprint && currentIntentFingerprint !== previousIntentFingerprint) {
404
+ return { refresh: true, reason: 'intent_shift_on_shallow_map' };
405
+ }
406
+ if ((stats?.adaptiveDeepenedFiles || 0) === 0) {
407
+ return { refresh: true, reason: 'shallow_pressure_without_deepening' };
408
+ }
409
+ }
410
+ return { refresh: false };
411
+ }
412
+ async function buildAssetMap(cwd, intentForAdaptiveDeepen) {
413
+ const escalationPolicy = resolveEscalationPolicy(cwd);
414
+ const startedAtMs = Date.now();
415
+ let escalationKillSwitchTripped = false;
416
+ let escalationKillSwitchCooldownUntil;
417
+ try {
418
+ const { ProjectScanner } = await Promise.resolve().then(() => __importStar(require('../services/mapper/ProjectScanner')));
419
+ const scanner = new ProjectScanner(cwd, {
420
+ maxSourceFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_FILES) ?? 800,
421
+ maxFileBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_BYTES) ?? (512 * 1024),
422
+ shallowScanBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_SHALLOW_SCAN_BYTES) ?? (256 * 1024),
423
+ shallowScanWindows: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_SHALLOW_SCAN_WINDOWS) ?? 5,
424
+ adaptiveDeepenIntent: intentForAdaptiveDeepen || '',
425
+ maxAdaptiveDeepenFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ADAPTIVE_DEEPEN_FILES) ?? 3,
426
+ maxAdaptiveDeepenTotalBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ADAPTIVE_DEEPEN_TOTAL_BYTES) ?? (2 * 1024 * 1024),
427
+ enableAdaptiveEscalation: escalationPolicy.enabled,
428
+ adaptiveEscalationShallowRatioThreshold: parseRatio(process.env.NEURCODE_ASSET_MAP_ESCALATE_SHALLOW_RATIO) ?? 0.35,
429
+ adaptiveEscalationMinCandidates: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MIN_CANDIDATES) ?? 3,
430
+ maxAdaptiveEscalationFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MAX_FILES) ?? 2,
431
+ maxAdaptiveEscalationTotalBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MAX_BYTES) ?? (1024 * 1024),
432
+ });
433
+ const map = await scanner.scan();
434
+ const killSwitchUpdate = updateEscalationGuardForBuild(cwd, escalationPolicy.enabled, startedAtMs);
435
+ escalationKillSwitchTripped = killSwitchUpdate.killSwitchTripped;
436
+ escalationKillSwitchCooldownUntil = killSwitchUpdate.cooldownUntil;
437
+ const { writeFileSync, mkdirSync } = await Promise.resolve().then(() => __importStar(require('fs')));
438
+ const neurcodeDir = (0, path_1.join)(cwd, '.neurcode');
439
+ if (!(0, fs_1.existsSync)(neurcodeDir)) {
440
+ mkdirSync(neurcodeDir, { recursive: true });
441
+ }
442
+ const mapPath = (0, path_1.join)(neurcodeDir, 'asset-map.json');
443
+ writeFileSync(mapPath, JSON.stringify(map, null, 2) + '\n', 'utf-8');
444
+ return {
445
+ map,
446
+ escalationPolicy,
447
+ escalationKillSwitchTripped,
448
+ escalationKillSwitchCooldownUntil,
449
+ };
450
+ }
451
+ catch (error) {
452
+ if (process.env.DEBUG) {
453
+ console.warn(chalk.yellow(`⚠️ Could not generate asset map: ${error instanceof Error ? error.message : 'Unknown error'}`));
219
454
  }
455
+ return {
456
+ map: null,
457
+ escalationPolicy,
458
+ escalationKillSwitchTripped: false,
459
+ escalationKillSwitchCooldownUntil: undefined,
460
+ };
461
+ }
462
+ }
463
+ async function ensureAssetMap(cwd, intentForAdaptiveDeepen) {
464
+ if (process.env.NEURCODE_DISABLE_ASSET_MAP === '1') {
465
+ return {
466
+ map: null,
467
+ generated: false,
468
+ refreshed: false,
469
+ };
470
+ }
471
+ const existingMap = (0, map_1.loadAssetMap)(cwd);
472
+ if (!existingMap) {
473
+ const generated = await buildAssetMap(cwd, intentForAdaptiveDeepen);
474
+ return {
475
+ map: generated.map,
476
+ generated: Boolean(generated.map),
477
+ refreshed: false,
478
+ refreshReason: generated.map ? 'missing' : undefined,
479
+ escalationPolicy: generated.escalationPolicy,
480
+ escalationKillSwitchTripped: generated.escalationKillSwitchTripped,
481
+ escalationKillSwitchCooldownUntil: generated.escalationKillSwitchCooldownUntil,
482
+ };
483
+ }
484
+ const refreshDecision = decideAssetMapRefresh(existingMap, intentForAdaptiveDeepen);
485
+ if (!refreshDecision.refresh) {
486
+ return {
487
+ map: existingMap,
488
+ generated: false,
489
+ refreshed: false,
490
+ };
491
+ }
492
+ const refreshed = await buildAssetMap(cwd, intentForAdaptiveDeepen);
493
+ if (!refreshed.map) {
494
+ return {
495
+ map: existingMap,
496
+ generated: false,
497
+ refreshed: false,
498
+ refreshReason: refreshDecision.reason,
499
+ refreshFailed: true,
500
+ escalationPolicy: refreshed.escalationPolicy,
501
+ escalationKillSwitchTripped: refreshed.escalationKillSwitchTripped,
502
+ escalationKillSwitchCooldownUntil: refreshed.escalationKillSwitchCooldownUntil,
503
+ };
220
504
  }
221
- return map;
505
+ return {
506
+ map: refreshed.map,
507
+ generated: false,
508
+ refreshed: true,
509
+ refreshReason: refreshDecision.reason,
510
+ escalationPolicy: refreshed.escalationPolicy,
511
+ escalationKillSwitchTripped: refreshed.escalationKillSwitchTripped,
512
+ escalationKillSwitchCooldownUntil: refreshed.escalationKillSwitchCooldownUntil,
513
+ };
222
514
  }
223
515
  function detectIntentMode(intent) {
224
516
  const normalized = (0, plan_cache_1.normalizeIntent)(intent);
@@ -282,6 +574,141 @@ function parsePositiveInt(raw) {
282
574
  return null;
283
575
  return Math.floor(parsed);
284
576
  }
577
+ function parseBooleanFlag(raw, fallback) {
578
+ if (!raw || !raw.trim())
579
+ return fallback;
580
+ const normalized = raw.trim().toLowerCase();
581
+ if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
582
+ return true;
583
+ }
584
+ if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
585
+ return false;
586
+ }
587
+ return fallback;
588
+ }
589
+ function parseConfidenceScoreThreshold(raw) {
590
+ if (!raw)
591
+ return null;
592
+ const parsed = Number(raw);
593
+ if (!Number.isFinite(parsed))
594
+ return null;
595
+ if (parsed < 0 || parsed > 100)
596
+ return null;
597
+ return Math.floor(parsed);
598
+ }
599
+ function buildPlanCoverageConfidence(input) {
600
+ const fileTreeCountSafe = Math.max(1, input.fileTreeCount);
601
+ const selectionCoverageRatio = input.filesUsedForGeneration / fileTreeCountSafe;
602
+ const readableSelectionRatio = input.selectedByScout > 0 ? input.readableSelected / input.selectedByScout : 1;
603
+ const mapStats = input.assetMap?.scanStats;
604
+ const metrics = {
605
+ fileTreeCount: input.fileTreeCount,
606
+ fileTreeCapped: input.fileTreeCapped,
607
+ selectedByScout: input.selectedByScout,
608
+ readableSelected: input.readableSelected,
609
+ filesUsedForGeneration: input.filesUsedForGeneration,
610
+ selectionCoverageRatio,
611
+ readableSelectionRatio,
612
+ usedFallbackSelection: input.usedFallbackSelection,
613
+ assetMapAvailable: Boolean(input.assetMap),
614
+ assetMapExports: input.assetMap?.globalExports.length || 0,
615
+ assetMapCapped: Boolean(mapStats?.cappedByMaxSourceFiles),
616
+ shallowIndexedSourceFiles: mapStats?.shallowIndexedSourceFiles || 0,
617
+ shallowIndexFailures: mapStats?.shallowIndexFailures || 0,
618
+ adaptiveDeepenedFiles: mapStats?.adaptiveDeepenedFiles || 0,
619
+ adaptiveDeepenSkippedBudget: mapStats?.adaptiveDeepenSkippedBudget || 0,
620
+ adaptiveEscalationTriggered: Boolean(mapStats?.adaptiveEscalationTriggered),
621
+ adaptiveEscalationReason: mapStats?.adaptiveEscalationReason || null,
622
+ adaptiveEscalationDeepenedFiles: mapStats?.adaptiveEscalationDeepenedFiles || 0,
623
+ adaptiveEscalationSkippedBudget: mapStats?.adaptiveEscalationSkippedBudget || 0,
624
+ };
625
+ let score = 100;
626
+ const reasons = [];
627
+ if (metrics.fileTreeCapped) {
628
+ score -= 12;
629
+ reasons.push('File tree context hit the cap and may miss relevant paths.');
630
+ }
631
+ if (metrics.usedFallbackSelection) {
632
+ score -= 14;
633
+ reasons.push('Semantic Scout fallback was used instead of ranked file selection.');
634
+ }
635
+ if (metrics.selectionCoverageRatio < 0.04) {
636
+ score -= 22;
637
+ reasons.push('Very low file coverage for this intent.');
638
+ }
639
+ else if (metrics.selectionCoverageRatio < 0.1) {
640
+ score -= 14;
641
+ reasons.push('Low file coverage for this intent.');
642
+ }
643
+ else if (metrics.selectionCoverageRatio < 0.2) {
644
+ score -= 8;
645
+ reasons.push('Moderate file coverage; consider expanding selected files.');
646
+ }
647
+ if (metrics.readableSelectionRatio < 0.7) {
648
+ score -= 10;
649
+ reasons.push('Many selected files were unreadable and dropped from planning.');
650
+ }
651
+ else if (metrics.readableSelectionRatio < 0.9) {
652
+ score -= 4;
653
+ reasons.push('Some selected files were unreadable and excluded.');
654
+ }
655
+ if (metrics.fileTreeCount >= 30 && metrics.filesUsedForGeneration < 5) {
656
+ score -= 8;
657
+ reasons.push('Plan uses a small file set relative to repository size.');
658
+ }
659
+ if (!metrics.assetMapAvailable) {
660
+ score -= 10;
661
+ reasons.push('Asset map unavailable; toolbox context was not injected.');
662
+ }
663
+ else {
664
+ if (metrics.assetMapCapped) {
665
+ score -= 8;
666
+ reasons.push('Asset map indexing was capped before full repository coverage.');
667
+ }
668
+ if (metrics.shallowIndexFailures > 0) {
669
+ score -= 10;
670
+ reasons.push('Some oversized files could not be indexed, reducing context completeness.');
671
+ }
672
+ if (metrics.shallowIndexedSourceFiles > 0 && metrics.adaptiveDeepenedFiles === 0) {
673
+ score -= 4;
674
+ reasons.push('Oversized files remained shallow-indexed with no adaptive deepening.');
675
+ }
676
+ if (metrics.adaptiveDeepenSkippedBudget > 0) {
677
+ score -= 4;
678
+ reasons.push('Adaptive deepening skipped candidates due to budget constraints.');
679
+ }
680
+ if (metrics.adaptiveEscalationTriggered && metrics.adaptiveEscalationDeepenedFiles === 0) {
681
+ score -= 3;
682
+ reasons.push('Escalation pass triggered but could not fully parse additional oversized files.');
683
+ }
684
+ if (metrics.adaptiveEscalationSkippedBudget > 0) {
685
+ score -= 2;
686
+ reasons.push('Escalation deepening skipped candidates due to strict byte/file limits.');
687
+ }
688
+ }
689
+ score = Math.max(0, Math.min(100, score));
690
+ const level = score >= 80 ? 'high' : score >= 60 ? 'medium' : 'low';
691
+ const status = score >= 70 ? 'sufficient' : score >= 50 ? 'warning' : 'insufficient';
692
+ if (reasons.length === 0) {
693
+ reasons.push('Context coverage is strong for this plan run.');
694
+ }
695
+ return {
696
+ score,
697
+ level,
698
+ status,
699
+ reasons,
700
+ metrics,
701
+ };
702
+ }
703
+ function displayPlanCoverageConfidence(coverage) {
704
+ const scoreColor = coverage.level === 'high' ? chalk.green : coverage.level === 'medium' ? chalk.yellow : chalk.red;
705
+ console.log(chalk.bold.white('Context Confidence:'), scoreColor(`${coverage.score}/100 (${coverage.level.toUpperCase()})`));
706
+ if (coverage.level !== 'high') {
707
+ for (const reason of coverage.reasons.slice(0, 3)) {
708
+ console.log(chalk.dim(` • ${reason}`));
709
+ }
710
+ }
711
+ }
285
712
  function parseSnapshotMode(raw) {
286
713
  if (!raw)
287
714
  return null;
@@ -453,6 +880,62 @@ function emitCachedPlanHit(input) {
453
880
  return persistedPlan;
454
881
  }
455
882
  async function planCommand(intent, options) {
883
+ const planStartedAtMs = Date.now();
884
+ let planRootForSlo = (0, path_1.resolve)(process.cwd());
885
+ try {
886
+ planRootForSlo = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
887
+ }
888
+ catch {
889
+ // Fallback to current working directory when root resolution fails.
890
+ }
891
+ let planIntentModeForSlo = 'implementation';
892
+ let planCachedForSlo = false;
893
+ let planSuccessForSlo = false;
894
+ let planEscalationPolicyForSlo = null;
895
+ let planEscalationKillSwitchTrippedForSlo = false;
896
+ let planEscalationKillSwitchCooldownUntilForSlo = null;
897
+ let planSloEventFlushed = false;
898
+ const flushPlanSloEvent = (exitCode) => {
899
+ if (planSloEventFlushed) {
900
+ return;
901
+ }
902
+ planSloEventFlushed = true;
903
+ const coverageMetrics = coverageConfidence?.metrics;
904
+ const rssKb = Math.max(0, Math.floor(process.memoryUsage().rss / 1024));
905
+ try {
906
+ (0, plan_slo_1.appendPlanSloEvent)(planRootForSlo, {
907
+ timestamp: new Date().toISOString(),
908
+ intentMode: planIntentModeForSlo,
909
+ cached: planCachedForSlo,
910
+ success: planSuccessForSlo && exitCode === 0,
911
+ exitCode,
912
+ elapsedMs: Math.max(0, Date.now() - planStartedAtMs),
913
+ rssKb,
914
+ coverageScore: coverageConfidence?.score ?? null,
915
+ coverageLevel: coverageConfidence?.level ?? null,
916
+ coverageStatus: coverageConfidence?.status ?? null,
917
+ adaptiveEscalationTriggered: coverageMetrics?.adaptiveEscalationTriggered === true,
918
+ adaptiveEscalationReason: coverageMetrics?.adaptiveEscalationReason || null,
919
+ adaptiveEscalationDeepenedFiles: coverageMetrics?.adaptiveEscalationDeepenedFiles || 0,
920
+ escalationPolicyEnabled: planEscalationPolicyForSlo?.enabled ?? null,
921
+ escalationPolicyReason: planEscalationPolicyForSlo?.reason ?? null,
922
+ escalationCanaryPercent: planEscalationPolicyForSlo?.canaryPercent ?? null,
923
+ escalationCanaryBucket: planEscalationPolicyForSlo?.canaryBucket ?? null,
924
+ escalationKillSwitchTripped: planEscalationKillSwitchTrippedForSlo,
925
+ escalationKillSwitchCooldownUntil: planEscalationKillSwitchCooldownUntilForSlo || planEscalationPolicyForSlo?.cooldownUntil || null,
926
+ fileTreeCount: coverageMetrics?.fileTreeCount ?? null,
927
+ filesUsedForGeneration: coverageMetrics?.filesUsedForGeneration ?? null,
928
+ });
929
+ }
930
+ catch {
931
+ // SLO logging is best-effort and should never block plan command.
932
+ }
933
+ };
934
+ const onPlanProcessExit = (code) => {
935
+ flushPlanSloEvent(code);
936
+ };
937
+ process.once('exit', onPlanProcessExit);
938
+ let coverageConfidence;
456
939
  try {
457
940
  if (!intent || !intent.trim()) {
458
941
  if (options.json) {
@@ -505,12 +988,38 @@ async function planCommand(intent, options) {
505
988
  // If the same user/org/project runs the same intent against the same repo snapshot,
506
989
  // return the cached plan immediately (no network, no file scanning).
507
990
  const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
991
+ const homeDir = (0, path_1.resolve)(process.env.HOME || process.env.USERPROFILE || '');
992
+ if (homeDir &&
993
+ cwd === homeDir &&
994
+ process.cwd() !== homeDir &&
995
+ process.env.NEURCODE_ALLOW_HOME_ROOT !== '1') {
996
+ const message = [
997
+ 'Resolved project root points to your home directory, which can mix unrelated repositories.',
998
+ 'Run `neurcode init` from this repository root to isolate Neurcode state.',
999
+ 'Set NEURCODE_ALLOW_HOME_ROOT=1 only if you intentionally want home-directory scope.',
1000
+ ].join(' ');
1001
+ if (options.json) {
1002
+ emitPlanJson({
1003
+ success: false,
1004
+ cached: false,
1005
+ mode: 'implementation',
1006
+ planId: null,
1007
+ sessionId: null,
1008
+ projectId: options.projectId || null,
1009
+ timestamp: new Date().toISOString(),
1010
+ message,
1011
+ });
1012
+ }
1013
+ console.error(chalk.red(`❌ ${message}`));
1014
+ process.exit(1);
1015
+ }
508
1016
  const orgId = (0, state_1.getOrgId)();
509
1017
  const stateProjectId = (0, state_1.getProjectId)();
510
1018
  const finalProjectIdEarly = options.projectId || stateProjectId || config.projectId;
511
1019
  const shouldUseCache = options.cache !== false && process.env.NEURCODE_PLAN_NO_CACHE !== '1';
512
1020
  const normalizedIntent = (0, plan_cache_1.normalizeIntent)(intent);
513
1021
  const intentMode = detectIntentMode(intent);
1022
+ planIntentModeForSlo = intentMode;
514
1023
  const isReadOnlyAnalysis = intentMode === 'analysis';
515
1024
  const nearIntentSimilarityFloor = isReadOnlyAnalysis ? 0.66 : undefined;
516
1025
  const policyVersionHash = (0, plan_cache_1.computePolicyVersionHash)(cwd);
@@ -557,8 +1066,12 @@ async function planCommand(intent, options) {
557
1066
  jsonMode: options.json === true,
558
1067
  intentMode,
559
1068
  });
1069
+ planCachedForSlo = true;
1070
+ planSuccessForSlo = persisted;
560
1071
  if (!persisted)
561
1072
  process.exit(2);
1073
+ flushPlanSloEvent(0);
1074
+ process.removeListener('exit', onPlanProcessExit);
562
1075
  return;
563
1076
  }
564
1077
  const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
@@ -585,8 +1098,12 @@ async function planCommand(intent, options) {
585
1098
  jsonMode: options.json === true,
586
1099
  intentMode,
587
1100
  });
1101
+ planCachedForSlo = true;
1102
+ planSuccessForSlo = persisted;
588
1103
  if (!persisted)
589
1104
  process.exit(2);
1105
+ flushPlanSloEvent(0);
1106
+ process.removeListener('exit', onPlanProcessExit);
590
1107
  return;
591
1108
  }
592
1109
  const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
@@ -719,12 +1236,16 @@ async function planCommand(intent, options) {
719
1236
  };
720
1237
  // Step B: Scan file tree (paths only, no content)
721
1238
  console.log(chalk.dim(`📂 Scanning file tree in ${cwd}...`));
722
- const fileTree = scanFiles(cwd, cwd, 200);
1239
+ const planFileTreeMaxFiles = Math.min(parsePositiveInt(process.env.NEURCODE_PLAN_FILE_TREE_MAX_FILES) ?? 200, 5000);
1240
+ const fileTree = scanFiles(cwd, cwd, planFileTreeMaxFiles);
723
1241
  if (fileTree.length === 0) {
724
1242
  console.warn(chalk.yellow('⚠️ No files found in current directory'));
725
1243
  process.exit(1);
726
1244
  }
727
1245
  console.log(chalk.dim(`Found ${fileTree.length} files in project`));
1246
+ if (fileTree.length >= planFileTreeMaxFiles) {
1247
+ console.log(chalk.dim(`📎 File tree context capped at ${planFileTreeMaxFiles} files. Set NEURCODE_PLAN_FILE_TREE_MAX_FILES to increase.`));
1248
+ }
728
1249
  // Load Neurcode static context (repo + local + org/project) early so it can:
729
1250
  // - influence plan generation
730
1251
  // - participate in the cache key (avoid stale cached plans after context edits)
@@ -775,8 +1296,12 @@ async function planCommand(intent, options) {
775
1296
  jsonMode: options.json === true,
776
1297
  intentMode,
777
1298
  });
1299
+ planCachedForSlo = true;
1300
+ planSuccessForSlo = persisted;
778
1301
  if (!persisted)
779
1302
  process.exit(2);
1303
+ flushPlanSloEvent(0);
1304
+ process.removeListener('exit', onPlanProcessExit);
780
1305
  return;
781
1306
  }
782
1307
  const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
@@ -803,8 +1328,12 @@ async function planCommand(intent, options) {
803
1328
  jsonMode: options.json === true,
804
1329
  intentMode,
805
1330
  });
1331
+ planCachedForSlo = true;
1332
+ planSuccessForSlo = persisted;
806
1333
  if (!persisted)
807
1334
  process.exit(2);
1335
+ flushPlanSloEvent(0);
1336
+ process.removeListener('exit', onPlanProcessExit);
808
1337
  return;
809
1338
  }
810
1339
  const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
@@ -850,9 +1379,75 @@ async function planCommand(intent, options) {
850
1379
  }
851
1380
  }
852
1381
  // Step 3: Load or create asset map for context injection (toolbox summary)
1382
+ let assetMapForCoverage = null;
853
1383
  try {
854
- const map = await ensureAssetMap(cwd);
1384
+ const assetMapResolution = await ensureAssetMap(cwd, enrichedIntent);
1385
+ const map = assetMapResolution.map;
1386
+ assetMapForCoverage = map;
1387
+ if (assetMapResolution.escalationPolicy) {
1388
+ planEscalationPolicyForSlo = assetMapResolution.escalationPolicy;
1389
+ }
1390
+ if (assetMapResolution.escalationKillSwitchTripped) {
1391
+ planEscalationKillSwitchTrippedForSlo = true;
1392
+ }
1393
+ if (assetMapResolution.escalationKillSwitchCooldownUntil) {
1394
+ planEscalationKillSwitchCooldownUntilForSlo = assetMapResolution.escalationKillSwitchCooldownUntil;
1395
+ }
1396
+ if (assetMapResolution.generated) {
1397
+ console.log(chalk.dim('♻️ Generated fresh asset map for this repository.'));
1398
+ }
1399
+ else if (assetMapResolution.refreshed) {
1400
+ console.log(chalk.dim(`♻️ Refreshed asset map (${assetMapResolution.refreshReason || 'policy_trigger'}).`));
1401
+ }
1402
+ else if (assetMapResolution.refreshFailed) {
1403
+ console.log(chalk.yellow(`⚠️ Asset map refresh failed (${assetMapResolution.refreshReason || 'unknown'}), using previous cached map.`));
1404
+ }
1405
+ if (assetMapResolution.escalationPolicy && (assetMapResolution.generated || assetMapResolution.refreshed)) {
1406
+ const escalationPolicy = assetMapResolution.escalationPolicy;
1407
+ if (escalationPolicy.enabled) {
1408
+ console.log(chalk.dim(`🧪 Escalation policy: enabled (canary=${escalationPolicy.canaryBucket}/${escalationPolicy.canaryPercent}).`));
1409
+ }
1410
+ else {
1411
+ let detail = `reason=${escalationPolicy.reason}`;
1412
+ if (escalationPolicy.reason === 'canary_excluded') {
1413
+ detail += `, canary=${escalationPolicy.canaryBucket}/${escalationPolicy.canaryPercent}`;
1414
+ }
1415
+ if (escalationPolicy.reason === 'kill_switch_cooldown' && escalationPolicy.cooldownUntil) {
1416
+ detail += `, cooldownUntil=${escalationPolicy.cooldownUntil}`;
1417
+ }
1418
+ console.log(chalk.yellow(`⚠️ Adaptive escalation disabled (${detail}).`));
1419
+ }
1420
+ }
1421
+ if (assetMapResolution.escalationKillSwitchTripped) {
1422
+ console.log(chalk.yellow(`⚠️ Escalation kill switch tripped; cooldown active until ${assetMapResolution.escalationKillSwitchCooldownUntil || 'later'}.`));
1423
+ }
855
1424
  if (map && map.globalExports.length > 0) {
1425
+ if (map.scanStats?.cappedByMaxSourceFiles) {
1426
+ console.log(chalk.yellow(`⚠️ Asset map coverage capped at ${map.scanStats.indexedSourceFiles} files (limit ${map.scanStats.maxSourceFiles}).`));
1427
+ console.log(chalk.dim(' Increase NEURCODE_ASSET_MAP_MAX_FILES for broader coverage.'));
1428
+ }
1429
+ if ((map.scanStats?.skippedBySize || 0) > 0) {
1430
+ console.log(chalk.dim(`📦 Asset map shallow-indexed ${map.scanStats?.skippedBySize} oversized source files (> ${map.scanStats?.maxFileBytes} bytes).`));
1431
+ }
1432
+ if ((map.scanStats?.shallowIndexFailures || 0) > 0) {
1433
+ console.log(chalk.yellow(`⚠️ Asset map could not shallow-index ${map.scanStats?.shallowIndexFailures} oversized file(s).`));
1434
+ }
1435
+ if ((map.scanStats?.adaptiveDeepenedFiles || 0) > 0) {
1436
+ console.log(chalk.dim(`🧠 Adaptive deepened ${map.scanStats?.adaptiveDeepenedFiles} oversized file(s) for this intent.`));
1437
+ }
1438
+ if ((map.scanStats?.adaptiveDeepenSkippedBudget || 0) > 0) {
1439
+ console.log(chalk.dim(`📎 Adaptive deepening skipped ${map.scanStats?.adaptiveDeepenSkippedBudget} candidate(s) due to budget.`));
1440
+ }
1441
+ if (map.scanStats?.adaptiveEscalationTriggered) {
1442
+ const escalationReason = map.scanStats?.adaptiveEscalationReason || 'policy_trigger';
1443
+ console.log(chalk.dim(`🎯 Adaptive escalation pass triggered (${escalationReason}).`));
1444
+ }
1445
+ if ((map.scanStats?.adaptiveEscalationDeepenedFiles || 0) > 0) {
1446
+ console.log(chalk.dim(`🚀 Escalation deepened ${map.scanStats?.adaptiveEscalationDeepenedFiles} additional oversized file(s).`));
1447
+ }
1448
+ if ((map.scanStats?.adaptiveEscalationSkippedBudget || 0) > 0) {
1449
+ console.log(chalk.dim(`📎 Escalation skipped ${map.scanStats?.adaptiveEscalationSkippedBudget} candidate(s) due to escalation budget limits.`));
1450
+ }
856
1451
  // Pass intent to generateToolboxSummary for relevance filtering
857
1452
  const toolboxSummary = (0, toolbox_service_1.generateToolboxSummary)(map, enrichedIntent);
858
1453
  if (toolboxSummary) {
@@ -998,12 +1593,14 @@ async function planCommand(intent, options) {
998
1593
  // Step C: Pass 1 - The Semantic Scout (select relevant files)
999
1594
  console.log(chalk.dim('🔍 Semantic Scout: Selecting relevant files...'));
1000
1595
  let selectedFiles = [];
1596
+ let usedFallbackSelection = false;
1001
1597
  try {
1002
1598
  selectedFiles = await client.selectFiles(enhancedIntent, fileTree, projectSummary);
1003
1599
  // Handle empty selection (fallback to top 10 files)
1004
1600
  if (selectedFiles.length === 0) {
1005
1601
  console.log(chalk.yellow('⚠️ No files selected by Semantic Scout, using fallback (top 10 files)'));
1006
1602
  selectedFiles = fileTree.slice(0, 10);
1603
+ usedFallbackSelection = true;
1007
1604
  }
1008
1605
  console.log(chalk.green(`✅ Semantic Scout selected ${selectedFiles.length} file(s) from ${fileTree.length} total`));
1009
1606
  if (process.env.DEBUG) {
@@ -1015,6 +1612,7 @@ async function planCommand(intent, options) {
1015
1612
  console.warn(chalk.yellow(`⚠️ File selection failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
1016
1613
  console.log(chalk.yellow(' Using fallback: top 10 files from tree'));
1017
1614
  selectedFiles = fileTree.slice(0, 10);
1615
+ usedFallbackSelection = true;
1018
1616
  }
1019
1617
  // Step D: Content Load - Verify selected files exist and are readable
1020
1618
  const validFiles = [];
@@ -1047,6 +1645,38 @@ async function planCommand(intent, options) {
1047
1645
  filesToUse = filesToUse.slice(0, 8);
1048
1646
  console.log(chalk.dim(`🔎 Analysis mode: narrowed to ${filesToUse.length} top file(s)`));
1049
1647
  }
1648
+ coverageConfidence = buildPlanCoverageConfidence({
1649
+ fileTreeCount: fileTree.length,
1650
+ fileTreeCapped: fileTree.length >= planFileTreeMaxFiles,
1651
+ selectedByScout: selectedFiles.length,
1652
+ readableSelected: validFiles.length,
1653
+ filesUsedForGeneration: filesToUse.length,
1654
+ usedFallbackSelection,
1655
+ assetMap: assetMapForCoverage,
1656
+ });
1657
+ displayPlanCoverageConfidence(coverageConfidence);
1658
+ const minConfidenceScore = parseConfidenceScoreThreshold(process.env.NEURCODE_PLAN_MIN_CONFIDENCE_SCORE);
1659
+ if (minConfidenceScore !== null && coverageConfidence.score < minConfidenceScore) {
1660
+ const message = [
1661
+ `Context confidence score ${coverageConfidence.score}/100 is below required threshold ${minConfidenceScore}.`,
1662
+ 'Increase file/asset-map coverage or relax NEURCODE_PLAN_MIN_CONFIDENCE_SCORE.',
1663
+ ].join(' ');
1664
+ if (options.json) {
1665
+ emitPlanJson({
1666
+ success: false,
1667
+ cached: false,
1668
+ mode: intentMode,
1669
+ planId: null,
1670
+ sessionId: null,
1671
+ projectId: finalProjectIdForGuard || null,
1672
+ timestamp: new Date().toISOString(),
1673
+ coverage: coverageConfidence,
1674
+ message,
1675
+ });
1676
+ }
1677
+ console.error(chalk.red(`❌ ${message}`));
1678
+ process.exit(1);
1679
+ }
1050
1680
  // Step E: Pass 2 - The Architect (generate plan with selected files)
1051
1681
  console.log(chalk.dim('🤖 Generating plan with selected files...\n'));
1052
1682
  const response = await client.generatePlan(enhancedIntent, filesToUse, finalProjectId, ticketMetadata, projectSummary);
@@ -1547,6 +2177,7 @@ async function planCommand(intent, options) {
1547
2177
  timestamp: response.timestamp,
1548
2178
  telemetry: response.telemetry,
1549
2179
  snapshot: snapshotSummary,
2180
+ coverage: coverageConfidence,
1550
2181
  message: missingPlanMessage,
1551
2182
  plan: response.plan,
1552
2183
  });
@@ -1595,10 +2226,14 @@ async function planCommand(intent, options) {
1595
2226
  timestamp: response.timestamp,
1596
2227
  telemetry: response.telemetry,
1597
2228
  snapshot: snapshotSummary,
2229
+ coverage: coverageConfidence,
1598
2230
  message: 'Plan generated and persisted',
1599
2231
  plan: response.plan,
1600
2232
  });
1601
2233
  }
2234
+ planSuccessForSlo = true;
2235
+ flushPlanSloEvent(0);
2236
+ process.removeListener('exit', onPlanProcessExit);
1602
2237
  }
1603
2238
  catch (error) {
1604
2239
  if (options.json) {
@@ -1610,6 +2245,7 @@ async function planCommand(intent, options) {
1610
2245
  sessionId: null,
1611
2246
  projectId: options.projectId || null,
1612
2247
  timestamp: new Date().toISOString(),
2248
+ coverage: coverageConfidence,
1613
2249
  message: error instanceof Error ? error.message : 'Unknown error',
1614
2250
  });
1615
2251
  }