helixmind 0.7.1 → 0.8.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 (74) hide show
  1. package/README.md +34 -3
  2. package/dist/cli/agent/loop.d.ts.map +1 -1
  3. package/dist/cli/agent/loop.js +8 -4
  4. package/dist/cli/agent/loop.js.map +1 -1
  5. package/dist/cli/agent/permissions.d.ts +8 -2
  6. package/dist/cli/agent/permissions.d.ts.map +1 -1
  7. package/dist/cli/agent/permissions.js +19 -8
  8. package/dist/cli/agent/permissions.js.map +1 -1
  9. package/dist/cli/agent/tools/bug-list.js +8 -1
  10. package/dist/cli/agent/tools/bug-list.js.map +1 -1
  11. package/dist/cli/agent/tools/git-commit.js +12 -13
  12. package/dist/cli/agent/tools/git-commit.js.map +1 -1
  13. package/dist/cli/agent/tools/git-diff.js +3 -4
  14. package/dist/cli/agent/tools/git-diff.js.map +1 -1
  15. package/dist/cli/agent/tools/git-exec.d.ts +36 -0
  16. package/dist/cli/agent/tools/git-exec.d.ts.map +1 -0
  17. package/dist/cli/agent/tools/git-exec.js +56 -0
  18. package/dist/cli/agent/tools/git-exec.js.map +1 -0
  19. package/dist/cli/agent/tools/git-log.js +4 -5
  20. package/dist/cli/agent/tools/git-log.js.map +1 -1
  21. package/dist/cli/agent/tools/git-status.js +6 -6
  22. package/dist/cli/agent/tools/git-status.js.map +1 -1
  23. package/dist/cli/agent/tools/registry.d.ts +9 -0
  24. package/dist/cli/agent/tools/registry.d.ts.map +1 -1
  25. package/dist/cli/agent/tools/registry.js.map +1 -1
  26. package/dist/cli/agent/tools/run-command.js +22 -0
  27. package/dist/cli/agent/tools/run-command.js.map +1 -1
  28. package/dist/cli/agent/tools/web-research.js +7 -3
  29. package/dist/cli/agent/tools/web-research.js.map +1 -1
  30. package/dist/cli/bugs/journal.d.ts +11 -0
  31. package/dist/cli/bugs/journal.d.ts.map +1 -1
  32. package/dist/cli/bugs/journal.js +67 -2
  33. package/dist/cli/bugs/journal.js.map +1 -1
  34. package/dist/cli/bugs/types.d.ts +2 -0
  35. package/dist/cli/bugs/types.d.ts.map +1 -1
  36. package/dist/cli/commands/chat.d.ts.map +1 -1
  37. package/dist/cli/commands/chat.js +317 -18
  38. package/dist/cli/commands/chat.js.map +1 -1
  39. package/dist/cli/core/stdin-envelope.d.ts +41 -0
  40. package/dist/cli/core/stdin-envelope.d.ts.map +1 -0
  41. package/dist/cli/core/stdin-envelope.js +107 -0
  42. package/dist/cli/core/stdin-envelope.js.map +1 -0
  43. package/dist/cli/jarvis/notifications.js +1 -1
  44. package/dist/cli/jarvis/telemetry.js +1 -1
  45. package/dist/cli/jarvis/types.d.ts +2 -2
  46. package/dist/cli/jarvis/types.d.ts.map +1 -1
  47. package/dist/cli/jarvis/types.js.map +1 -1
  48. package/dist/cli/license/checker.js +1 -1
  49. package/dist/cli/ui/select-menu.d.ts +12 -0
  50. package/dist/cli/ui/select-menu.d.ts.map +1 -1
  51. package/dist/cli/ui/select-menu.js +47 -27
  52. package/dist/cli/ui/select-menu.js.map +1 -1
  53. package/dist/cli/validation/static-checks.d.ts.map +1 -1
  54. package/dist/cli/validation/static-checks.js +62 -3
  55. package/dist/cli/validation/static-checks.js.map +1 -1
  56. package/dist/cli/validation/stats.d.ts.map +1 -1
  57. package/dist/cli/validation/stats.js +5 -3
  58. package/dist/cli/validation/stats.js.map +1 -1
  59. package/dist/spiral/cloud/search-provider.d.ts.map +1 -1
  60. package/dist/spiral/cloud/search-provider.js +29 -1
  61. package/dist/spiral/cloud/search-provider.js.map +1 -1
  62. package/dist/spiral/engine.d.ts +13 -0
  63. package/dist/spiral/engine.d.ts.map +1 -1
  64. package/dist/spiral/engine.js +35 -0
  65. package/dist/spiral/engine.js.map +1 -1
  66. package/dist/spiral/injection.d.ts +13 -0
  67. package/dist/spiral/injection.d.ts.map +1 -1
  68. package/dist/spiral/injection.js +66 -1
  69. package/dist/spiral/injection.js.map +1 -1
  70. package/dist/spiral/provenance.d.ts +37 -0
  71. package/dist/spiral/provenance.d.ts.map +1 -0
  72. package/dist/spiral/provenance.js +167 -0
  73. package/dist/spiral/provenance.js.map +1 -0
  74. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { Writable } from 'node:stream';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, basename } from 'node:path';
5
5
  import { writeFileSync } from 'node:fs';
6
+ import { markPrepended } from '../core/stdin-envelope.js';
6
7
  import { ConfigStore } from '../config/store.js';
7
8
  import { createProvider, registerFreeModel } from '../providers/registry.js';
8
9
  import { registerModelContextLength } from '../providers/model-limits.js';
@@ -103,6 +104,7 @@ const HELP_CATEGORIES = [
103
104
  { cmd: '/context', label: '/context', description: 'Show context size & embeddings' },
104
105
  { cmd: '/compact', label: '/compact', description: 'Trigger spiral evolution' },
105
106
  { cmd: '/tokens', label: '/tokens', description: 'Show token usage & memory' },
107
+ { cmd: '/recap', label: '/recap', description: 'Toggle per-turn recap summary (on|off)' },
106
108
  ],
107
109
  },
108
110
  {
@@ -245,6 +247,7 @@ ${chalk.hex('#00ff88').bold(' Spiral Memory')}
245
247
  ${theme.primary('/context'.padEnd(22))} ${theme.dim('Show current context size & embeddings')}
246
248
  ${theme.primary('/compact'.padEnd(22))} ${theme.dim('Trigger spiral evolution (promote/demote nodes)')}
247
249
  ${theme.primary('/tokens'.padEnd(22))} ${theme.dim('Show token usage, checkpoints, memory')}
250
+ ${theme.primary('/recap [on|off]'.padEnd(22))} ${theme.dim('Toggle short summary line after each agent turn')}
248
251
 
249
252
  ${chalk.hex('#4169e1').bold(' Visualization & Brain')}
250
253
  ${theme.primary('/brain'.padEnd(22))} ${theme.dim('Show brain scope + open 3D visualization')}
@@ -508,6 +511,174 @@ export async function chatCommand(options) {
508
511
  let roundToolCalls = 0;
509
512
  let currentStepLabel = '';
510
513
  let currentStepFile = '';
514
+ // Recap tracking — per-round snapshot so we can summarise what just happened.
515
+ let roundStartTokensIn = 0;
516
+ let roundStartTokensOut = 0;
517
+ let roundStartTime = 0;
518
+ const roundToolCounts = new Map();
519
+ const roundFilesTouched = new Set();
520
+ let recapEnabled = true; // Can be toggled via /recap
521
+ // audit-016/v0.8.0: brain growth telemetry for the session-exit display.
522
+ // Captured lazily on first spiral access; the diff is rendered before
523
+ // graceful exit so users SEE memory compounding ("+12 patterns learned,
524
+ // 3 stale retired"). The first session shows just the absolute count.
525
+ let brainStartNodes = null;
526
+ let brainStartConfidence = null;
527
+ function captureBrainStart() {
528
+ if (brainStartNodes !== null)
529
+ return;
530
+ try {
531
+ brainStartNodes = spiralEngine?.status?.().total_nodes ?? null;
532
+ }
533
+ catch {
534
+ brainStartNodes = null;
535
+ }
536
+ try {
537
+ brainStartConfidence = jarvisIdentity?.getIdentity?.()?.traits?.confidence ?? null;
538
+ }
539
+ catch {
540
+ brainStartConfidence = null;
541
+ }
542
+ }
543
+ function renderBrainGrowth() {
544
+ if (!spiralEngine)
545
+ return;
546
+ let nodesNow;
547
+ try {
548
+ nodesNow = spiralEngine.status().total_nodes;
549
+ }
550
+ catch {
551
+ return;
552
+ }
553
+ const stale = (() => {
554
+ try {
555
+ return spiralEngine.getStaleSeenLifetime?.() ?? 0;
556
+ }
557
+ catch {
558
+ return 0;
559
+ }
560
+ })();
561
+ let confNow = null;
562
+ try {
563
+ confNow = jarvisIdentity?.getIdentity?.()?.traits?.confidence ?? null;
564
+ }
565
+ catch { /* ignore */ }
566
+ const parts = [];
567
+ if (brainStartNodes !== null) {
568
+ const delta = nodesNow - brainStartNodes;
569
+ if (delta > 0)
570
+ parts.push(chalk.green(`+${delta} pattern${delta !== 1 ? 's' : ''} learned`));
571
+ else if (delta < 0)
572
+ parts.push(chalk.dim(`${delta} compacted`));
573
+ }
574
+ if (stale > 0)
575
+ parts.push(chalk.yellow(`${stale} stale retired`));
576
+ if (confNow !== null && brainStartConfidence !== null && Math.abs(confNow - brainStartConfidence) > 0.001) {
577
+ const delta = confNow - brainStartConfidence;
578
+ const arrow = delta > 0 ? chalk.green('↑') : chalk.red('↓');
579
+ parts.push(`confidence ${brainStartConfidence.toFixed(2)}${arrow}${confNow.toFixed(2)}`);
580
+ }
581
+ if (parts.length === 0) {
582
+ parts.push(chalk.dim(`brain has ${nodesNow} pattern${nodesNow !== 1 ? 's' : ''}`));
583
+ }
584
+ process.stdout.write(chalk.dim(' 🧠 ') + parts.join(chalk.dim(' · ')) + '\n');
585
+ }
586
+ function recordRoundTool(toolName, input) {
587
+ roundToolCounts.set(toolName, (roundToolCounts.get(toolName) ?? 0) + 1);
588
+ const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
589
+ if (fileTools.has(toolName)) {
590
+ const p = typeof input?.path === 'string' ? input.path : null;
591
+ if (p)
592
+ roundFilesTouched.add(p);
593
+ }
594
+ }
595
+ function buildRecapLine() {
596
+ if (!recapEnabled)
597
+ return null;
598
+ const totalTools = [...roundToolCounts.values()].reduce((a, b) => a + b, 0);
599
+ const tokensIn = sessionTokensInput - roundStartTokensIn;
600
+ const tokensOut = sessionTokensOutput - roundStartTokensOut;
601
+ const elapsedMs = Date.now() - roundStartTime;
602
+ if (totalTools === 0 && tokensIn === 0 && tokensOut === 0)
603
+ return null;
604
+ const parts = [];
605
+ // Tool breakdown — compact verbs. Group reads (read_file/list_directory/
606
+ // search_files/find_files), writes (write_file/edit_file), shell, other.
607
+ const reads = (roundToolCounts.get('read_file') ?? 0)
608
+ + (roundToolCounts.get('list_directory') ?? 0)
609
+ + (roundToolCounts.get('search_files') ?? 0)
610
+ + (roundToolCounts.get('find_files') ?? 0);
611
+ const writes = (roundToolCounts.get('write_file') ?? 0)
612
+ + (roundToolCounts.get('edit_file') ?? 0);
613
+ const runs = roundToolCounts.get('run_command') ?? 0;
614
+ const gitOps = [...roundToolCounts.entries()]
615
+ .filter(([k]) => k.startsWith('git_'))
616
+ .reduce((sum, [, n]) => sum + n, 0);
617
+ const spiralOps = [...roundToolCounts.entries()]
618
+ .filter(([k]) => k.startsWith('spiral_'))
619
+ .reduce((sum, [, n]) => sum + n, 0);
620
+ const browserOps = [...roundToolCounts.entries()]
621
+ .filter(([k]) => k.startsWith('browser_'))
622
+ .reduce((sum, [, n]) => sum + n, 0);
623
+ if (reads > 0)
624
+ parts.push(`${reads} read${reads === 1 ? '' : 's'}`);
625
+ if (writes > 0) {
626
+ const fileNames = [...roundFilesTouched]
627
+ .map(p => p.split(/[\/\\]/).pop() ?? p)
628
+ .slice(0, 2);
629
+ if (writes === 1 && fileNames.length === 1) {
630
+ parts.push(`edited ${fileNames[0]}`);
631
+ }
632
+ else if (fileNames.length > 0) {
633
+ const suffix = writes > fileNames.length ? `+${writes - fileNames.length}` : '';
634
+ parts.push(`edited ${fileNames.join(', ')}${suffix}`);
635
+ }
636
+ else {
637
+ parts.push(`${writes} edit${writes === 1 ? '' : 's'}`);
638
+ }
639
+ }
640
+ if (runs > 0)
641
+ parts.push(`${runs} shell`);
642
+ if (gitOps > 0)
643
+ parts.push(`${gitOps} git`);
644
+ if (spiralOps > 0)
645
+ parts.push(`${spiralOps} spiral`);
646
+ if (browserOps > 0)
647
+ parts.push(`${browserOps} browser`);
648
+ // Other tools not caught above
649
+ const known = new Set([
650
+ 'read_file', 'list_directory', 'search_files', 'find_files',
651
+ 'write_file', 'edit_file', 'run_command',
652
+ ]);
653
+ const otherTotal = [...roundToolCounts.entries()]
654
+ .filter(([k]) => !known.has(k) && !k.startsWith('git_') && !k.startsWith('spiral_') && !k.startsWith('browser_'))
655
+ .reduce((sum, [, n]) => sum + n, 0);
656
+ if (otherTotal > 0)
657
+ parts.push(`${otherTotal} other`);
658
+ // Tokens
659
+ const totalTokens = tokensIn + tokensOut;
660
+ if (totalTokens >= 1000) {
661
+ parts.push(`${(totalTokens / 1000).toFixed(1)}k tokens`);
662
+ }
663
+ else if (totalTokens > 0) {
664
+ parts.push(`${totalTokens} tokens`);
665
+ }
666
+ // Time
667
+ if (elapsedMs >= 1000) {
668
+ const secs = Math.round(elapsedMs / 1000);
669
+ if (secs >= 60) {
670
+ const m = Math.floor(secs / 60);
671
+ const s = secs % 60;
672
+ parts.push(`${m}m${s > 0 ? ` ${s}s` : ''}`);
673
+ }
674
+ else {
675
+ parts.push(`${secs}s`);
676
+ }
677
+ }
678
+ if (parts.length === 0)
679
+ return null;
680
+ return chalk.dim(` \u258E Recap: ${parts.join(' \u00B7 ')}`);
681
+ }
511
682
  // Timer tracking
512
683
  const sessionStartTime = Date.now();
513
684
  let currentSection = null;
@@ -633,6 +804,11 @@ export async function chatCommand(options) {
633
804
  const dataDir = resolveSpiralDir(scope, process.cwd());
634
805
  const spiralConfig = loadSpiralConfig(dataDir);
635
806
  const engine = new SpiralEngine(spiralConfig);
807
+ // audit-014/v0.8.0 "Compounds": pin engine to project root so
808
+ // store() can capture file provenance and the injection layer can
809
+ // detect stale recall. Without this, the [STALE] marking is silent
810
+ // and the compounds feature is effectively disabled.
811
+ engine.setProjectRoot(projectRoot);
636
812
  await engine.initialize();
637
813
  return engine;
638
814
  }
@@ -652,6 +828,8 @@ export async function chatCommand(options) {
652
828
  spiralInitPromise = initSpiralEngine(scope).then(async (engine) => {
653
829
  spiralEngine = engine;
654
830
  spiralInitPromise = null;
831
+ // audit-016: capture session-start metrics for the growth display.
832
+ captureBrainStart();
655
833
  if (engine && brainUrl) {
656
834
  try {
657
835
  const { startLiveBrain } = await import('../brain/generator.js');
@@ -2410,6 +2588,13 @@ export async function chatCommand(options) {
2410
2588
  ctrlCCount++;
2411
2589
  if (ctrlCCount >= 2) {
2412
2590
  process.stdout.write('\n');
2591
+ // audit-016: brain growth display before saving + exit. Single
2592
+ // line, only when a spiral exists, never blocks. Compounding made
2593
+ // visible \u2014 the User-sp\u00fcrbare Pro-tier value moment.
2594
+ try {
2595
+ renderBrainGrowth();
2596
+ }
2597
+ catch { /* never block exit */ }
2413
2598
  renderInfo('Force exit \u2014 saving state...');
2414
2599
  if (spiralEngine) {
2415
2600
  try {
@@ -2576,7 +2761,12 @@ export async function chatCommand(options) {
2576
2761
  {
2577
2762
  let lastRawEscTime = 0;
2578
2763
  const RAW_ESC_THRESHOLD = 800; // ms
2579
- process.stdin.prependListener('data', async (chunk) => {
2764
+ // audit-005: tag the raw ESC detector so any modal-stdin envelope (rewind
2765
+ // browser, plan browser, …) restores it via prependListener instead of
2766
+ // .on(), preserving its first-in-line position. Without the tag, after
2767
+ // the first modal exit readline's data parser could land before the ESC
2768
+ // detector and break double-ESC + bracketed-paste detection.
2769
+ const escDetector = markPrepended(async (chunk) => {
2580
2770
  if (fullScreenBrowserOpen)
2581
2771
  return;
2582
2772
  const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
@@ -2685,6 +2875,7 @@ export async function chatCommand(options) {
2685
2875
  // Everything else (text, arrow keys, etc.) — don't reset timer
2686
2876
  // because arrow key \x1b[A could arrive just after a real ESC press
2687
2877
  });
2878
+ process.stdin.prependListener('data', escDetector);
2688
2879
  async function openRewindBrowser() {
2689
2880
  // Don't re-enter if already open
2690
2881
  if (fullScreenBrowserOpen)
@@ -2715,13 +2906,20 @@ export async function chatCommand(options) {
2715
2906
  // Fullscreen overlay: suspend=false removes stdout hook entirely so the
2716
2907
  // Rewind browser can write directly to the terminal without buffering.
2717
2908
  chrome.deactivate({ suspend: false });
2718
- // CRITICAL: Remove ALL stdin data+keypress listeners to prevent readline's
2719
- // internal handler from buffering keypresses during Rewind navigation.
2720
- // The browser adds its own raw 'data' listener for arrow keys / ESC.
2909
+ // audit-011: snapshot+restore data listeners only. Do NOT touch
2910
+ // 'keypress' listeners Node's readline attaches an internal data
2911
+ // parser when the first keypress listener is added, and removeAll +
2912
+ // re-add can cause it to register a second parser, which makes every
2913
+ // typed character afterwards fire twice ("double-write" symptom).
2914
+ // Keypress listeners are gated by fullScreenBrowserOpen anyway, so
2915
+ // they're safe to leave attached during the modal.
2916
+ //
2917
+ // Restore data listeners with prepend-symmetry: any listener tagged
2918
+ // via markPrepended() (the raw ESC detector at chat.ts:2915) is
2919
+ // re-attached as prepended so readline's data parser cannot land in
2920
+ // front of it after rl.resume().
2721
2921
  const savedDataListeners = process.stdin.rawListeners('data').slice();
2722
- const savedKeypressListeners = process.stdin.rawListeners('keypress').slice();
2723
2922
  process.stdin.removeAllListeners('data');
2724
- process.stdin.removeAllListeners('keypress');
2725
2923
  let didRevertWithMessage = false;
2726
2924
  try {
2727
2925
  try {
@@ -2751,11 +2949,16 @@ export async function chatCommand(options) {
2751
2949
  }
2752
2950
  finally {
2753
2951
  // Guaranteed listener restore, even if the browser or revert threw.
2952
+ // Tagged listeners (raw ESC detector) re-attach as prepended.
2953
+ const PREPENDED = Symbol.for('helixmind.stdin.prepended');
2754
2954
  for (const listener of savedDataListeners) {
2755
- process.stdin.on('data', listener);
2756
- }
2757
- for (const listener of savedKeypressListeners) {
2758
- process.stdin.on('keypress', listener);
2955
+ const isPrepended = Boolean(listener[PREPENDED]);
2956
+ if (isPrepended) {
2957
+ process.stdin.prependListener('data', listener);
2958
+ }
2959
+ else {
2960
+ process.stdin.on('data', listener);
2961
+ }
2759
2962
  }
2760
2963
  if (!didRevertWithMessage) {
2761
2964
  inputMgr.setLine('');
@@ -2956,6 +3159,24 @@ export async function chatCommand(options) {
2956
3159
  }
2957
3160
  // Persist to JSONL history (async, non-blocking)
2958
3161
  promptHistory.add(input, process.cwd()).catch(() => { });
3162
+ // Handle /recap here (toggles local recapEnabled state in the closure).
3163
+ const trimmedForRecap = input.trim();
3164
+ if (trimmedForRecap === '/recap' || trimmedForRecap.startsWith('/recap ')) {
3165
+ const arg = trimmedForRecap.split(/\s+/)[1]?.toLowerCase();
3166
+ if (arg === 'on') {
3167
+ recapEnabled = true;
3168
+ renderInfo('Recap enabled \u2014 summary line after every agent turn.');
3169
+ }
3170
+ else if (arg === 'off') {
3171
+ recapEnabled = false;
3172
+ renderInfo('Recap disabled.');
3173
+ }
3174
+ else {
3175
+ renderInfo(`Recap is ${recapEnabled ? 'ON' : 'OFF'}. Use /recap on | off to toggle.`);
3176
+ }
3177
+ showPrompt();
3178
+ return;
3179
+ }
2959
3180
  // Handle /feed directly here (needs access to inlineProgressActive flag)
2960
3181
  if (input.startsWith('/feed')) {
2961
3182
  const activeSpiral = spiralEngine ?? await ensureSpiralEngine(brainScope);
@@ -3547,6 +3768,12 @@ export async function chatCommand(options) {
3547
3768
  roundToolCalls = 0;
3548
3769
  currentStepLabel = '';
3549
3770
  currentStepFile = '';
3771
+ // Recap: snapshot round-start state.
3772
+ roundStartTokensIn = sessionTokensInput;
3773
+ roundStartTokensOut = sessionTokensOutput;
3774
+ roundStartTime = Date.now();
3775
+ roundToolCounts.clear();
3776
+ roundFilesTouched.clear();
3550
3777
  suggestionPanel.close(); // Close suggestions when agent starts
3551
3778
  agentRunning = true;
3552
3779
  chrome.promptActive = false; // Re-enable stdout hook redraws for agent output
@@ -3577,13 +3804,20 @@ export async function chatCommand(options) {
3577
3804
  fullScreenBrowserOpen = true;
3578
3805
  rl.pause();
3579
3806
  chrome.deactivate({ suspend: false });
3580
- // Isolate stdin: remove all data listeners to prevent readline buffering
3807
+ // audit-011: same envelope as openRewindBrowser. Snapshot only data
3808
+ // listeners; restore with prepend-symmetry for tagged ones.
3581
3809
  const savedPlanListeners = process.stdin.rawListeners('data').slice();
3582
3810
  process.stdin.removeAllListeners('data');
3583
3811
  const { choice } = await runPlanBrowser(plan);
3584
- // Restore stdin listeners
3812
+ const PREPENDED = Symbol.for('helixmind.stdin.prepended');
3585
3813
  for (const listener of savedPlanListeners) {
3586
- process.stdin.on('data', listener);
3814
+ const isPrepended = Boolean(listener[PREPENDED]);
3815
+ if (isPrepended) {
3816
+ process.stdin.prependListener('data', listener);
3817
+ }
3818
+ else {
3819
+ process.stdin.on('data', listener);
3820
+ }
3587
3821
  }
3588
3822
  inputMgr.setLine('');
3589
3823
  chrome.activate();
@@ -3763,9 +3997,13 @@ export async function chatCommand(options) {
3763
3997
  await sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => {
3764
3998
  sessionTokensInput += inp;
3765
3999
  sessionTokensOutput += out;
3766
- }, () => {
4000
+ }, (toolName) => {
3767
4001
  sessionToolCalls++;
3768
4002
  roundToolCalls++;
4003
+ // Recap: record tool invocation by name.
4004
+ if (typeof toolName === 'string') {
4005
+ recordRoundTool(toolName, undefined);
4006
+ }
3769
4007
  }, () => {
3770
4008
  // Activity started — readline stays active for type-ahead buffering.
3771
4009
  // Muting is handled by activity.setMuteCallbacks (mute during LLM stream,
@@ -3774,11 +4012,46 @@ export async function chatCommand(options) {
3774
4012
  }, effectiveValidation, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3775
4013
  currentStepLabel = label;
3776
4014
  const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3777
- currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
3778
- }, jarvisLearning);
4015
+ if (fileTools.has(tool)) {
4016
+ const file = label.replace(/^(reading|writing|editing)\s+/, '');
4017
+ currentStepFile = file;
4018
+ // Recap: track files touched so the summary can name them.
4019
+ if (file && (tool === 'write_file' || tool === 'edit_file')) {
4020
+ roundFilesTouched.add(file);
4021
+ }
4022
+ }
4023
+ else {
4024
+ currentStepFile = '';
4025
+ }
4026
+ }, jarvisLearning, undefined, // runtime
4027
+ undefined, // skillManager
4028
+ // audit-006: feed validation outcome into identity calibration.
4029
+ (status, taskCategory) => {
4030
+ if (!jarvisIdentity)
4031
+ return;
4032
+ if (status === 'passed') {
4033
+ jarvisIdentity.recordEvent({
4034
+ type: 'task_completed',
4035
+ taskId: `validation:${taskCategory}`,
4036
+ summary: 'validation passed',
4037
+ });
4038
+ }
4039
+ else if (status === 'errors' || status === 'max_loops') {
4040
+ jarvisIdentity.recordEvent({
4041
+ type: 'task_failed',
4042
+ taskId: `validation:${taskCategory}`,
4043
+ error: `validation ${status}`,
4044
+ });
4045
+ }
4046
+ });
3779
4047
  }
3780
4048
  }
3781
4049
  agentRunning = false;
4050
+ // Recap: emit a short summary of what just happened.
4051
+ const recapLine = buildRecapLine();
4052
+ if (recapLine) {
4053
+ process.stdout.write(recapLine + '\n');
4054
+ }
3782
4055
  // Keep simple message history for state persistence.
3783
4056
  // FIX: CHATFLOW-001 — also persist the assistant's reply, otherwise
3784
4057
  // saveState() writes a user-only transcript and checkpoint browser shows
@@ -3973,6 +4246,13 @@ export async function chatCommand(options) {
3973
4246
  await spiralEngine.saveState(messages);
3974
4247
  }
3975
4248
  catch { /* best effort */ }
4249
+ // audit-016: render the growth line BEFORE close so the spiral is
4250
+ // still queryable. The graceful path (rl close → here) is the
4251
+ // common exit; the force-exit path renders earlier (Ctrl+C×2).
4252
+ try {
4253
+ renderBrainGrowth();
4254
+ }
4255
+ catch { /* never block exit */ }
3976
4256
  spiralEngine.close();
3977
4257
  }
3978
4258
  // Close browser if open
@@ -4009,7 +4289,13 @@ async function runBackgroundSession(session, prompt, provider, project, spiralEn
4009
4289
  durationMs: session.elapsed,
4010
4290
  };
4011
4291
  }
4012
- async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot, jarvisContext, onStepInfo, learningJournal, runtime, skillManager) {
4292
+ async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot, jarvisContext, onStepInfo, learningJournal, runtime, skillManager,
4293
+ // audit-006: optional callback fired once per validation run. Used by the
4294
+ // foreground chat path to feed the result into JarvisIdentityManager so
4295
+ // confidence calibration actually updates after successful agent turns.
4296
+ // Background-session callers leave this undefined so they do not pollute
4297
+ // the user's confidence score with their own outcomes.
4298
+ onValidationComplete) {
4013
4299
  // User message was rendered by renderUserMessage() in the caller before entering here.
4014
4300
  const executionRoot = runtime?.executionRoot ?? process.cwd();
4015
4301
  // Intent Detection: Check if user wants to feed the codebase
@@ -4197,6 +4483,19 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
4197
4483
  process.stdout.write(renderValidationSummary(valResult, validationOpts.verbose) + '\n');
4198
4484
  // Store stats in spiral
4199
4485
  await storeValidationResult(valResult, classification.category, spiralEngine || undefined);
4486
+ // audit-006: bridge validation outcome into the identity
4487
+ // calibration layer. Previously the confidence score was
4488
+ // initialized at 0.5 and only nudged by proposal approvals and
4489
+ // the autonomous-task scheduler; the regular agent turn that
4490
+ // ran validation never updated it. This is why the user-visible
4491
+ // confidence stayed stuck at 0.52 across many sessions despite
4492
+ // successful fixes. We treat 'passed' as task_completed and
4493
+ // 'errors'/'max_loops' as task_failed; 'warnings' is neutral
4494
+ // (do not penalize).
4495
+ try {
4496
+ onValidationComplete?.(valResult.status, classification.category);
4497
+ }
4498
+ catch { /* never block on a calibration callback */ }
4200
4499
  }
4201
4500
  }
4202
4501
  }
@@ -5683,7 +5982,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
5683
5982
  cancelLabel: 'Cancel',
5684
5983
  });
5685
5984
  if (modeIdx === docsItemIdx) {
5686
- const docsUrl = 'https://helixmind.dev/docs/security-monitor';
5985
+ const docsUrl = 'https://helix-mind.ai/docs/security-monitor';
5687
5986
  const { exec } = await import('node:child_process');
5688
5987
  const { platform } = await import('node:os');
5689
5988
  const openCmd = platform() === 'win32' ? `start "" "${docsUrl}"`