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.
- package/README.md +34 -3
- package/dist/cli/agent/loop.d.ts.map +1 -1
- package/dist/cli/agent/loop.js +8 -4
- package/dist/cli/agent/loop.js.map +1 -1
- package/dist/cli/agent/permissions.d.ts +8 -2
- package/dist/cli/agent/permissions.d.ts.map +1 -1
- package/dist/cli/agent/permissions.js +19 -8
- package/dist/cli/agent/permissions.js.map +1 -1
- package/dist/cli/agent/tools/bug-list.js +8 -1
- package/dist/cli/agent/tools/bug-list.js.map +1 -1
- package/dist/cli/agent/tools/git-commit.js +12 -13
- package/dist/cli/agent/tools/git-commit.js.map +1 -1
- package/dist/cli/agent/tools/git-diff.js +3 -4
- package/dist/cli/agent/tools/git-diff.js.map +1 -1
- package/dist/cli/agent/tools/git-exec.d.ts +36 -0
- package/dist/cli/agent/tools/git-exec.d.ts.map +1 -0
- package/dist/cli/agent/tools/git-exec.js +56 -0
- package/dist/cli/agent/tools/git-exec.js.map +1 -0
- package/dist/cli/agent/tools/git-log.js +4 -5
- package/dist/cli/agent/tools/git-log.js.map +1 -1
- package/dist/cli/agent/tools/git-status.js +6 -6
- package/dist/cli/agent/tools/git-status.js.map +1 -1
- package/dist/cli/agent/tools/registry.d.ts +9 -0
- package/dist/cli/agent/tools/registry.d.ts.map +1 -1
- package/dist/cli/agent/tools/registry.js.map +1 -1
- package/dist/cli/agent/tools/run-command.js +22 -0
- package/dist/cli/agent/tools/run-command.js.map +1 -1
- package/dist/cli/agent/tools/web-research.js +7 -3
- package/dist/cli/agent/tools/web-research.js.map +1 -1
- package/dist/cli/bugs/journal.d.ts +11 -0
- package/dist/cli/bugs/journal.d.ts.map +1 -1
- package/dist/cli/bugs/journal.js +67 -2
- package/dist/cli/bugs/journal.js.map +1 -1
- package/dist/cli/bugs/types.d.ts +2 -0
- package/dist/cli/bugs/types.d.ts.map +1 -1
- package/dist/cli/commands/chat.d.ts.map +1 -1
- package/dist/cli/commands/chat.js +317 -18
- package/dist/cli/commands/chat.js.map +1 -1
- package/dist/cli/core/stdin-envelope.d.ts +41 -0
- package/dist/cli/core/stdin-envelope.d.ts.map +1 -0
- package/dist/cli/core/stdin-envelope.js +107 -0
- package/dist/cli/core/stdin-envelope.js.map +1 -0
- package/dist/cli/jarvis/notifications.js +1 -1
- package/dist/cli/jarvis/telemetry.js +1 -1
- package/dist/cli/jarvis/types.d.ts +2 -2
- package/dist/cli/jarvis/types.d.ts.map +1 -1
- package/dist/cli/jarvis/types.js.map +1 -1
- package/dist/cli/license/checker.js +1 -1
- package/dist/cli/ui/select-menu.d.ts +12 -0
- package/dist/cli/ui/select-menu.d.ts.map +1 -1
- package/dist/cli/ui/select-menu.js +47 -27
- package/dist/cli/ui/select-menu.js.map +1 -1
- package/dist/cli/validation/static-checks.d.ts.map +1 -1
- package/dist/cli/validation/static-checks.js +62 -3
- package/dist/cli/validation/static-checks.js.map +1 -1
- package/dist/cli/validation/stats.d.ts.map +1 -1
- package/dist/cli/validation/stats.js +5 -3
- package/dist/cli/validation/stats.js.map +1 -1
- package/dist/spiral/cloud/search-provider.d.ts.map +1 -1
- package/dist/spiral/cloud/search-provider.js +29 -1
- package/dist/spiral/cloud/search-provider.js.map +1 -1
- package/dist/spiral/engine.d.ts +13 -0
- package/dist/spiral/engine.d.ts.map +1 -1
- package/dist/spiral/engine.js +35 -0
- package/dist/spiral/engine.js.map +1 -1
- package/dist/spiral/injection.d.ts +13 -0
- package/dist/spiral/injection.d.ts.map +1 -1
- package/dist/spiral/injection.js +66 -1
- package/dist/spiral/injection.js.map +1 -1
- package/dist/spiral/provenance.d.ts +37 -0
- package/dist/spiral/provenance.d.ts.map +1 -0
- package/dist/spiral/provenance.js +167 -0
- package/dist/spiral/provenance.js.map +1 -0
- 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
|
-
|
|
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
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
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
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
3812
|
+
const PREPENDED = Symbol.for('helixmind.stdin.prepended');
|
|
3585
3813
|
for (const listener of savedPlanListeners) {
|
|
3586
|
-
|
|
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
|
-
|
|
3778
|
-
|
|
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://
|
|
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}"`
|