@neurcode-ai/cli 0.9.8 → 0.9.10

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.
@@ -45,6 +45,7 @@ const ROILogger_1 = require("../utils/ROILogger");
45
45
  const toolbox_service_1 = require("../services/toolbox-service");
46
46
  const plan_cache_1 = require("../utils/plan-cache");
47
47
  const neurcode_context_1 = require("../utils/neurcode-context");
48
+ const brain_context_1 = require("../utils/brain-context");
48
49
  const project_root_1 = require("../utils/project-root");
49
50
  // Import chalk with fallback for plain strings if not available
50
51
  let chalk;
@@ -217,6 +218,99 @@ async function ensureAssetMap(cwd) {
217
218
  }
218
219
  return map;
219
220
  }
221
+ function detectIntentMode(intent) {
222
+ const normalized = (0, plan_cache_1.normalizeIntent)(intent);
223
+ if (!normalized)
224
+ return 'implementation';
225
+ const implementationSignals = [
226
+ /\b(add|create|implement|build|fix|refactor|update|change|write|generate|migrate|remove|delete|ship)\b/,
227
+ ];
228
+ for (const pattern of implementationSignals) {
229
+ if (pattern.test(normalized)) {
230
+ return 'implementation';
231
+ }
232
+ }
233
+ const analysisSignals = [
234
+ /\b(read|review|inspect|analyze|audit|check|find|search|locate|compare|list|tell me|where|whether|is there)\b/,
235
+ /\?$/,
236
+ ];
237
+ for (const pattern of analysisSignals) {
238
+ if (pattern.test(normalized)) {
239
+ return 'analysis';
240
+ }
241
+ }
242
+ return 'implementation';
243
+ }
244
+ function applyReadOnlyDirective(intentText) {
245
+ return [
246
+ intentText,
247
+ '',
248
+ 'NEURCODE_EXECUTION_MODE: READ_ONLY_ANALYSIS',
249
+ '- The user asked for analysis only, not code implementation.',
250
+ '- Do not propose code writes or file creation.',
251
+ '- Prefer a compact investigation plan with target files and concrete checks.',
252
+ '- Treat suggested files as inspection targets.',
253
+ ].join('\n');
254
+ }
255
+ function renderCacheMissReason(reason, bestIntentSimilarity) {
256
+ const reasonText = {
257
+ no_scope_entries: 'no cached plans exist yet for this org/project scope',
258
+ repo_identity_changed: 'repo identity changed for this scope',
259
+ repo_snapshot_changed: 'repo snapshot changed (HEAD tree differs)',
260
+ policy_changed: 'policy fingerprint changed',
261
+ neurcode_version_changed: 'neurcode version changed',
262
+ prompt_changed: 'prompt/context changed',
263
+ };
264
+ if (reason === 'prompt_changed' && bestIntentSimilarity > 0) {
265
+ return `${reasonText[reason]} (closest intent similarity ${bestIntentSimilarity.toFixed(2)})`;
266
+ }
267
+ return reasonText[reason];
268
+ }
269
+ function emitCachedPlanHit(input) {
270
+ if (input.touchKey) {
271
+ try {
272
+ (0, plan_cache_1.readCachedPlan)(input.cwd, input.touchKey);
273
+ }
274
+ catch {
275
+ // Ignore cache touch failures.
276
+ }
277
+ }
278
+ const createdAtLabel = new Date(input.createdAt).toLocaleString();
279
+ if (input.mode === 'near' && typeof input.similarity === 'number') {
280
+ console.log(chalk.dim(`⚡ Using near-cached plan (similarity ${input.similarity.toFixed(2)}, created: ${createdAtLabel})\n`));
281
+ }
282
+ else {
283
+ console.log(chalk.dim(`⚡ Using cached plan (created: ${createdAtLabel})\n`));
284
+ }
285
+ displayPlan(input.response.plan);
286
+ console.log(chalk.dim(`\nGenerated at: ${new Date(input.response.timestamp).toLocaleString()} (cached)`));
287
+ if (input.response.planId && input.response.planId !== 'unknown') {
288
+ console.log(chalk.bold.cyan(`\n📌 Plan ID: ${input.response.planId} (Cached)`));
289
+ console.log(chalk.dim(' Run \'neurcode prompt\' to generate a Cursor/AI prompt.'));
290
+ }
291
+ try {
292
+ if (input.response.planId && input.response.planId !== 'unknown') {
293
+ (0, state_1.setActivePlanId)(input.response.planId);
294
+ (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
295
+ }
296
+ if (input.response.sessionId) {
297
+ (0, state_1.setSessionId)(input.response.sessionId);
298
+ }
299
+ (0, brain_context_1.recordBrainProgressEvent)(input.cwd, {
300
+ orgId: input.orgId,
301
+ projectId: input.projectId,
302
+ }, {
303
+ type: 'plan',
304
+ planId: input.response.planId || undefined,
305
+ note: input.mode === 'near'
306
+ ? `cache_hit=near;similarity=${typeof input.similarity === 'number' ? input.similarity.toFixed(2) : 'n/a'}`
307
+ : 'cache_hit=exact',
308
+ });
309
+ }
310
+ catch {
311
+ // ignore state write errors
312
+ }
313
+ }
220
314
  async function planCommand(intent, options) {
221
315
  try {
222
316
  if (!intent || !intent.trim()) {
@@ -243,6 +337,17 @@ async function planCommand(intent, options) {
243
337
  const stateProjectId = (0, state_1.getProjectId)();
244
338
  const finalProjectIdEarly = options.projectId || stateProjectId || config.projectId;
245
339
  const shouldUseCache = options.cache !== false && process.env.NEURCODE_PLAN_NO_CACHE !== '1';
340
+ const normalizedIntent = (0, plan_cache_1.normalizeIntent)(intent);
341
+ const intentMode = detectIntentMode(intent);
342
+ const isReadOnlyAnalysis = intentMode === 'analysis';
343
+ const policyVersionHash = (0, plan_cache_1.computePolicyVersionHash)(cwd);
344
+ const neurcodeVersion = (0, plan_cache_1.getNeurcodeVersion)();
345
+ // Create a local, gitignored context file scaffold (similar to CLAUDE.md workflows),
346
+ // so teams/users have a predictable place to add project-specific guidance.
347
+ // This runs only on interactive terminals to avoid polluting CI checkouts.
348
+ if (process.stdout.isTTY && !process.env.CI) {
349
+ (0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
350
+ }
246
351
  const staticContextEarly = (0, neurcode_context_1.loadStaticNeurcodeContext)(cwd, orgId && finalProjectIdEarly ? { orgId, projectId: finalProjectIdEarly } : undefined);
247
352
  const ticketRef = options.issue
248
353
  ? `github_issue:${options.issue}`
@@ -251,48 +356,66 @@ async function planCommand(intent, options) {
251
356
  : options.ticket
252
357
  ? `ticket:${options.ticket}`
253
358
  : undefined;
359
+ const promptHashEarly = (0, plan_cache_1.computePromptHash)({
360
+ intent: normalizedIntent,
361
+ ticketRef,
362
+ contextHash: staticContextEarly.hash,
363
+ });
254
364
  const gitFingerprint = (0, plan_cache_1.getGitRepoFingerprint)(cwd);
255
365
  if (shouldUseCache && orgId && finalProjectIdEarly && gitFingerprint) {
256
366
  const key = (0, plan_cache_1.computePlanCacheKey)({
257
- schemaVersion: 1,
258
- apiUrl: (config.apiUrl || '').replace(/\/$/, ''),
367
+ schemaVersion: 2,
259
368
  orgId,
260
369
  projectId: finalProjectIdEarly,
261
- intent: (0, plan_cache_1.normalizeIntent)(intent),
262
- ticketRef,
263
- contextHash: staticContextEarly.hash,
264
370
  repo: gitFingerprint,
371
+ promptHash: promptHashEarly,
372
+ policyVersionHash,
373
+ neurcodeVersion,
265
374
  });
266
375
  const cached = (0, plan_cache_1.readCachedPlan)(cwd, key);
267
376
  if (cached) {
268
- console.log(chalk.dim(`⚡ Using cached plan (created: ${new Date(cached.createdAt).toLocaleString()})\n`));
269
- displayPlan(cached.response.plan);
270
- console.log(chalk.dim(`\nGenerated at: ${new Date(cached.response.timestamp).toLocaleString()} (cached)`));
271
- if (cached.response.planId && cached.response.planId !== 'unknown') {
272
- console.log(chalk.bold.cyan(`\n📌 Plan ID: ${cached.response.planId} (Cached)`));
273
- console.log(chalk.dim(' Run \'neurcode prompt\' to generate a Cursor/AI prompt.'));
274
- }
275
- // Maintain the same state side-effects as a fresh plan.
276
- try {
277
- if (cached.response.planId && cached.response.planId !== 'unknown') {
278
- (0, state_1.setActivePlanId)(cached.response.planId);
279
- (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
280
- }
281
- if (cached.response.sessionId) {
282
- (0, state_1.setSessionId)(cached.response.sessionId);
283
- }
284
- }
285
- catch {
286
- // ignore state write errors
287
- }
377
+ emitCachedPlanHit({
378
+ cwd,
379
+ response: cached.response,
380
+ createdAt: cached.createdAt,
381
+ mode: 'exact',
382
+ orgId,
383
+ projectId: finalProjectIdEarly,
384
+ });
288
385
  return;
289
386
  }
290
- }
291
- // Create a local, gitignored context file scaffold (similar to CLAUDE.md workflows),
292
- // so teams/users have a predictable place to add project-specific guidance.
293
- // This runs only on interactive terminals to avoid polluting CI checkouts.
294
- if (process.stdout.isTTY && !process.env.CI) {
295
- (0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
387
+ const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
388
+ orgId,
389
+ projectId: finalProjectIdEarly,
390
+ repo: gitFingerprint,
391
+ intent: normalizedIntent,
392
+ policyVersionHash,
393
+ neurcodeVersion,
394
+ ticketRef,
395
+ contextHash: staticContextEarly.hash,
396
+ });
397
+ if (near) {
398
+ emitCachedPlanHit({
399
+ cwd,
400
+ response: near.entry.response,
401
+ createdAt: near.entry.createdAt,
402
+ mode: 'near',
403
+ similarity: near.intentSimilarity,
404
+ orgId,
405
+ projectId: finalProjectIdEarly,
406
+ touchKey: near.entry.key,
407
+ });
408
+ return;
409
+ }
410
+ const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
411
+ orgId,
412
+ projectId: finalProjectIdEarly,
413
+ repo: gitFingerprint,
414
+ intent: normalizedIntent,
415
+ policyVersionHash,
416
+ neurcodeVersion,
417
+ });
418
+ console.log(chalk.dim(`🧠 Cache miss: ${renderCacheMissReason(miss.reason, miss.bestIntentSimilarity)}`));
296
419
  }
297
420
  // Initialize Security Guard, Ticket Service, and Project Knowledge Service
298
421
  const { SecurityGuard } = await Promise.resolve().then(() => __importStar(require('../services/security/SecurityGuard')));
@@ -408,6 +531,10 @@ async function planCommand(intent, options) {
408
531
  await initCommand();
409
532
  return;
410
533
  }
534
+ const brainScope = {
535
+ orgId: orgId || null,
536
+ projectId: finalProjectIdForGuard || null,
537
+ };
411
538
  // Step B: Scan file tree (paths only, no content)
412
539
  console.log(chalk.dim(`📂 Scanning file tree in ${cwd}...`));
413
540
  const fileTree = scanFiles(cwd, cwd, 200);
@@ -420,43 +547,84 @@ async function planCommand(intent, options) {
420
547
  // - influence plan generation
421
548
  // - participate in the cache key (avoid stale cached plans after context edits)
422
549
  const staticContext = (0, neurcode_context_1.loadStaticNeurcodeContext)(cwd, orgId && finalProjectIdForGuard ? { orgId, projectId: finalProjectIdForGuard } : undefined);
550
+ // Incremental Brain refresh: keep context in sync with human progress even when watch is not running.
551
+ if (orgId && finalProjectIdForGuard) {
552
+ try {
553
+ const refresh = (0, brain_context_1.refreshBrainContextFromWorkspace)(cwd, brainScope, {
554
+ workingTreeHash: gitFingerprint?.kind === 'git' ? gitFingerprint.workingTreeHash : undefined,
555
+ maxFiles: 80,
556
+ recordEvent: false,
557
+ });
558
+ if (refresh.refreshed && (refresh.indexed > 0 || refresh.removed > 0)) {
559
+ console.log(chalk.dim(`🧠 Brain refresh: indexed ${refresh.indexed}, removed ${refresh.removed}, considered ${refresh.considered}`));
560
+ }
561
+ }
562
+ catch {
563
+ // Brain refresh should never block plan generation.
564
+ }
565
+ }
423
566
  // If we couldn't use the git-based fast path (non-git projects), try a filesystem-based cache hit
424
567
  // after we have the file tree fingerprint.
425
568
  if (shouldUseCache && orgId && finalProjectIdForGuard && !gitFingerprint) {
426
- const fsFingerprint = (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree);
569
+ const fsFingerprint = (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree, cwd);
570
+ const promptHash = (0, plan_cache_1.computePromptHash)({
571
+ intent: normalizedIntent,
572
+ ticketRef,
573
+ contextHash: staticContext.hash,
574
+ });
427
575
  const key = (0, plan_cache_1.computePlanCacheKey)({
428
- schemaVersion: 1,
429
- apiUrl: (config.apiUrl || '').replace(/\/$/, ''),
576
+ schemaVersion: 2,
430
577
  orgId,
431
578
  projectId: finalProjectIdForGuard,
432
- intent: (0, plan_cache_1.normalizeIntent)(intent),
433
- ticketRef,
434
- contextHash: staticContext.hash,
435
579
  repo: fsFingerprint,
580
+ promptHash,
581
+ policyVersionHash,
582
+ neurcodeVersion,
436
583
  });
437
584
  const cached = (0, plan_cache_1.readCachedPlan)(cwd, key);
438
585
  if (cached) {
439
- console.log(chalk.dim(`⚡ Using cached plan (created: ${new Date(cached.createdAt).toLocaleString()})\n`));
440
- displayPlan(cached.response.plan);
441
- console.log(chalk.dim(`\nGenerated at: ${new Date(cached.response.timestamp).toLocaleString()} (cached)`));
442
- if (cached.response.planId && cached.response.planId !== 'unknown') {
443
- console.log(chalk.bold.cyan(`\n📌 Plan ID: ${cached.response.planId} (Cached)`));
444
- console.log(chalk.dim(' Run \'neurcode prompt\' to generate a Cursor/AI prompt.'));
445
- }
446
- try {
447
- if (cached.response.planId && cached.response.planId !== 'unknown') {
448
- (0, state_1.setActivePlanId)(cached.response.planId);
449
- (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
450
- }
451
- if (cached.response.sessionId) {
452
- (0, state_1.setSessionId)(cached.response.sessionId);
453
- }
454
- }
455
- catch {
456
- // ignore
457
- }
586
+ emitCachedPlanHit({
587
+ cwd,
588
+ response: cached.response,
589
+ createdAt: cached.createdAt,
590
+ mode: 'exact',
591
+ orgId,
592
+ projectId: finalProjectIdForGuard,
593
+ });
594
+ return;
595
+ }
596
+ const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
597
+ orgId,
598
+ projectId: finalProjectIdForGuard,
599
+ repo: fsFingerprint,
600
+ intent: normalizedIntent,
601
+ policyVersionHash,
602
+ neurcodeVersion,
603
+ ticketRef,
604
+ contextHash: staticContext.hash,
605
+ });
606
+ if (near) {
607
+ emitCachedPlanHit({
608
+ cwd,
609
+ response: near.entry.response,
610
+ createdAt: near.entry.createdAt,
611
+ mode: 'near',
612
+ similarity: near.intentSimilarity,
613
+ orgId,
614
+ projectId: finalProjectIdForGuard,
615
+ touchKey: near.entry.key,
616
+ });
458
617
  return;
459
618
  }
619
+ const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
620
+ orgId,
621
+ projectId: finalProjectIdForGuard,
622
+ repo: fsFingerprint,
623
+ intent: normalizedIntent,
624
+ policyVersionHash,
625
+ neurcodeVersion,
626
+ });
627
+ console.log(chalk.dim(`🧠 Cache miss: ${renderCacheMissReason(miss.reason, miss.bestIntentSimilarity)}`));
460
628
  }
461
629
  // Step 2: Build enhanced intent with static context + org/project memory (before any LLM call)
462
630
  let enhancedIntent = enrichedIntent;
@@ -469,6 +637,27 @@ async function planCommand(intent, options) {
469
637
  enhancedIntent = `${enhancedIntent}\n\n${memoryTail}`;
470
638
  }
471
639
  }
640
+ if (isReadOnlyAnalysis) {
641
+ console.log(chalk.dim('🔎 Read-only analysis mode enabled (no-code intent detected)'));
642
+ enhancedIntent = applyReadOnlyDirective(enhancedIntent);
643
+ }
644
+ // Retrieval augmentation: include a repo-grounded live context pack (file summaries + recent progress events).
645
+ if (orgId && finalProjectIdForGuard) {
646
+ try {
647
+ const pack = (0, brain_context_1.buildBrainContextPack)(cwd, brainScope, normalizedIntent, {
648
+ maxFiles: 8,
649
+ maxEvents: 6,
650
+ maxBytes: 10 * 1024,
651
+ });
652
+ if (pack.text) {
653
+ enhancedIntent = `${enhancedIntent}\n\n${pack.text}`;
654
+ console.log(chalk.dim(`🧠 Brain context: ${pack.selectedFiles} relevant file summary(s), ${pack.recentEvents} recent event(s) from ${pack.totalIndexedFiles} indexed file(s)`));
655
+ }
656
+ }
657
+ catch {
658
+ // Brain context pack should never block plan generation.
659
+ }
660
+ }
472
661
  // Step 3: Load or create asset map for context injection (toolbox summary)
473
662
  try {
474
663
  const map = await ensureAssetMap(cwd);
@@ -490,7 +679,14 @@ async function planCommand(intent, options) {
490
679
  }
491
680
  // Retrieval augmentation: include summaries from similar cached plans (org+project scoped)
492
681
  if (shouldUseCache && orgId && finalProjectIdForGuard) {
493
- const similar = (0, plan_cache_1.findSimilarCachedPlans)(cwd, { orgId, projectId: finalProjectIdForGuard }, (0, plan_cache_1.normalizeIntent)(intent), 3);
682
+ const repoIdentityForSimilarity = gitFingerprint
683
+ ? gitFingerprint.repoIdentity
684
+ : (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree, cwd).repoIdentity;
685
+ const similar = (0, plan_cache_1.findSimilarCachedPlans)(cwd, {
686
+ orgId,
687
+ projectId: finalProjectIdForGuard,
688
+ repoIdentity: repoIdentityForSimilarity,
689
+ }, normalizedIntent, 3);
494
690
  if (similar.length > 0) {
495
691
  const memoryLines = [];
496
692
  memoryLines.push('PROJECT MEMORY (recent Neurcode plans in this repo; use only if relevant):');
@@ -557,7 +753,7 @@ async function planCommand(intent, options) {
557
753
  }
558
754
  // Check for active sessions before creating a new one
559
755
  const finalProjectId = options.projectId || projectId || config.projectId;
560
- if (finalProjectId && process.stdout.isTTY && !process.env.CI) {
756
+ if (finalProjectId && process.stdout.isTTY && !process.env.CI && !isReadOnlyAnalysis) {
561
757
  try {
562
758
  const sessions = await client.getSessions(finalProjectId, 10);
563
759
  const activeSessions = sessions.filter(s => s.status === 'active');
@@ -652,41 +848,64 @@ async function planCommand(intent, options) {
652
848
  }
653
849
  }
654
850
  // Ensure we have at least some files
655
- const filesToUse = validFiles.length > 0 ? validFiles : fileTree.slice(0, 10);
851
+ let filesToUse = validFiles.length > 0 ? validFiles : fileTree.slice(0, 10);
656
852
  if (validFiles.length < selectedFiles.length) {
657
853
  console.log(chalk.yellow(`⚠️ ${selectedFiles.length - validFiles.length} selected file(s) could not be read, using ${filesToUse.length} valid file(s)`));
658
854
  }
855
+ if (isReadOnlyAnalysis && filesToUse.length > 8) {
856
+ filesToUse = filesToUse.slice(0, 8);
857
+ console.log(chalk.dim(`🔎 Analysis mode: narrowed to ${filesToUse.length} top file(s)`));
858
+ }
659
859
  // Step E: Pass 2 - The Architect (generate plan with selected files)
660
860
  console.log(chalk.dim('🤖 Generating plan with selected files...\n'));
661
861
  const response = await client.generatePlan(enhancedIntent, filesToUse, finalProjectId, ticketMetadata, projectSummary);
862
+ if (orgId && finalProjectIdForGuard) {
863
+ try {
864
+ const refreshed = (0, brain_context_1.refreshBrainContextForFiles)(cwd, brainScope, filesToUse);
865
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, brainScope, {
866
+ type: 'plan',
867
+ planId: response.planId || undefined,
868
+ note: `selected=${filesToUse.length};indexed=${refreshed.indexed};removed=${refreshed.removed};complexity=${response.plan.estimatedComplexity || 'unknown'}`,
869
+ });
870
+ }
871
+ catch {
872
+ // Brain progression tracking should never block plan generation.
873
+ }
874
+ }
662
875
  // Persist in local cache for instant repeat plans.
663
876
  if (shouldUseCache && orgId && finalProjectIdForGuard) {
664
877
  // Recompute repo fingerprint right before writing to cache so Neurcode-managed
665
878
  // housekeeping (e.g. `.neurcode/config.json`, `.gitignore` updates) doesn't
666
879
  // cause immediate cache misses on the next identical run.
667
880
  const finalGitFingerprint = (0, plan_cache_1.getGitRepoFingerprint)(cwd);
668
- const repo = finalGitFingerprint || (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree);
881
+ const repo = finalGitFingerprint || (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree, cwd);
882
+ const promptHash = (0, plan_cache_1.computePromptHash)({
883
+ intent: normalizedIntent,
884
+ ticketRef,
885
+ contextHash: staticContext.hash,
886
+ });
669
887
  const key = (0, plan_cache_1.computePlanCacheKey)({
670
- schemaVersion: 1,
671
- apiUrl: (config.apiUrl || '').replace(/\/$/, ''),
888
+ schemaVersion: 2,
672
889
  orgId,
673
890
  projectId: finalProjectIdForGuard,
674
- intent: (0, plan_cache_1.normalizeIntent)(intent),
675
- ticketRef,
676
- contextHash: staticContext.hash,
677
891
  repo,
892
+ promptHash,
893
+ policyVersionHash,
894
+ neurcodeVersion,
678
895
  });
679
896
  (0, plan_cache_1.writeCachedPlan)(cwd, {
680
897
  key,
681
898
  input: {
682
- schemaVersion: 1,
683
- apiUrl: (config.apiUrl || '').replace(/\/$/, ''),
899
+ schemaVersion: 2,
684
900
  orgId,
685
901
  projectId: finalProjectIdForGuard,
686
- intent: (0, plan_cache_1.normalizeIntent)(intent),
902
+ repo,
903
+ promptHash,
904
+ policyVersionHash,
905
+ neurcodeVersion,
906
+ intent: normalizedIntent,
687
907
  ticketRef,
688
908
  contextHash: staticContext.hash,
689
- repo,
690
909
  },
691
910
  response,
692
911
  });
@@ -698,7 +917,10 @@ async function planCommand(intent, options) {
698
917
  // Pre-Flight Snapshot: Capture current state of files that will be MODIFIED
699
918
  // This ensures we have a baseline to revert to if AI destroys files
700
919
  const modifyFiles = response.plan.files.filter(f => f.action === 'MODIFY');
701
- if (modifyFiles.length > 0) {
920
+ if (isReadOnlyAnalysis) {
921
+ console.log(chalk.dim('\n🔎 Analysis mode: skipping pre-flight file snapshots'));
922
+ }
923
+ else if (modifyFiles.length > 0) {
702
924
  console.log(chalk.dim(`\n📸 Capturing pre-flight snapshots for ${modifyFiles.length} file(s)...`));
703
925
  let snapshotsSaved = 0;
704
926
  let snapshotsFailed = 0;
@@ -746,11 +968,16 @@ async function planCommand(intent, options) {
746
968
  // Step 3: Post-Generation Hallucination Check (DEEP SCAN)
747
969
  // Scan ALL plan content for phantom packages - not just summaries, but full proposed code
748
970
  let hasHallucinations = false;
971
+ let skippedHallucinationScan = false;
749
972
  const allHallucinations = [];
750
973
  // Check tier for hallucination scanning (PRO feature)
751
974
  const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
752
975
  const tier = await getUserTier();
753
- if (tier === 'FREE') {
976
+ if (isReadOnlyAnalysis) {
977
+ skippedHallucinationScan = true;
978
+ console.log(chalk.dim('🔎 Analysis mode: skipping hallucination package scan'));
979
+ }
980
+ else if (tier === 'FREE') {
754
981
  console.log(chalk.yellow('\n🛡️ Hallucination Shield is a PRO feature.'));
755
982
  console.log(chalk.dim(' Upgrade at: https://www.neurcode.com/dashboard/purchase-plan\n'));
756
983
  }
@@ -863,9 +1090,18 @@ async function planCommand(intent, options) {
863
1090
  console.log('');
864
1091
  }
865
1092
  else {
866
- console.log(chalk.green('✅ No hallucinations detected'));
1093
+ if (skippedHallucinationScan) {
1094
+ console.log(chalk.dim('🔎 Hallucination scan skipped (read-only analysis mode)'));
1095
+ }
1096
+ else {
1097
+ console.log(chalk.green('✅ No hallucinations detected'));
1098
+ }
867
1099
  }
868
1100
  // Display the plan (AFTER hallucination warnings)
1101
+ if (isReadOnlyAnalysis) {
1102
+ const writableTargets = response.plan.files.filter((file) => file.action !== 'BLOCK').length;
1103
+ console.log(chalk.dim(`🔎 Read-only query guidance: treat listed files as inspection targets (${writableTargets} target(s)).`));
1104
+ }
869
1105
  displayPlan(response.plan);
870
1106
  console.log(chalk.dim(`\nGenerated at: ${new Date(response.timestamp).toLocaleString()}`));
871
1107
  // Display plan ID if available