helixmind 0.6.0 → 0.6.2

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 (35) hide show
  1. package/dist/cli/agent/turn-directives.d.ts +12 -0
  2. package/dist/cli/agent/turn-directives.d.ts.map +1 -0
  3. package/dist/cli/agent/turn-directives.js +89 -0
  4. package/dist/cli/agent/turn-directives.js.map +1 -0
  5. package/dist/cli/auth/feature-gate.js +4 -4
  6. package/dist/cli/auth/feature-gate.js.map +1 -1
  7. package/dist/cli/auth/guard.js +2 -2
  8. package/dist/cli/auth/guard.js.map +1 -1
  9. package/dist/cli/brain/generator.d.ts.map +1 -1
  10. package/dist/cli/brain/generator.js +44 -1
  11. package/dist/cli/brain/generator.js.map +1 -1
  12. package/dist/cli/commands/chat.d.ts.map +1 -1
  13. package/dist/cli/commands/chat.js +312 -143
  14. package/dist/cli/commands/chat.js.map +1 -1
  15. package/dist/cli/jarvis/daemon.js +1 -1
  16. package/dist/cli/jarvis/daemon.js.map +1 -1
  17. package/dist/cli/jarvis/identity.d.ts +1 -1
  18. package/dist/cli/jarvis/identity.js +6 -6
  19. package/dist/cli/jarvis/onboarding.js +1 -1
  20. package/dist/cli/jarvis/onboarding.js.map +1 -1
  21. package/dist/cli/jarvis/orchestrator.d.ts +13 -0
  22. package/dist/cli/jarvis/orchestrator.d.ts.map +1 -1
  23. package/dist/cli/jarvis/orchestrator.js +102 -26
  24. package/dist/cli/jarvis/orchestrator.js.map +1 -1
  25. package/dist/cli/ui/activity.d.ts.map +1 -1
  26. package/dist/cli/ui/activity.js +12 -1
  27. package/dist/cli/ui/activity.js.map +1 -1
  28. package/dist/cli/ui/command-suggest.d.ts.map +1 -1
  29. package/dist/cli/ui/command-suggest.js +2 -0
  30. package/dist/cli/ui/command-suggest.js.map +1 -1
  31. package/dist/cli/validation/policy.d.ts +13 -0
  32. package/dist/cli/validation/policy.d.ts.map +1 -0
  33. package/dist/cli/validation/policy.js +34 -0
  34. package/dist/cli/validation/policy.js.map +1 -0
  35. package/package.json +1 -1
@@ -21,6 +21,7 @@ import { showHelixMenu } from './helix-menu.js';
21
21
  import { initializeTools } from '../agent/tools/registry.js';
22
22
  import { runAgentLoop, AgentController, AgentAbortError } from '../agent/loop.js';
23
23
  import { PermissionManager } from '../agent/permissions.js';
24
+ import { parseTurnDirectives } from '../agent/turn-directives.js';
24
25
  import { UndoStack } from '../agent/undo.js';
25
26
  import { renderStatusBar, getGitInfo } from '../ui/statusbar.js';
26
27
  import { CheckpointStore } from '../checkpoints/store.js';
@@ -60,6 +61,7 @@ import { classifyTask } from '../validation/classifier.js';
60
61
  import { generateCriteria } from '../validation/criteria.js';
61
62
  import { validationLoop } from '../validation/autofix.js';
62
63
  import { createValidationProvider } from '../validation/model.js';
64
+ import { resolveValidationDecision } from '../validation/policy.js';
63
65
  import { renderValidationSummary, renderValidationStart, renderClassification } from '../validation/reporter.js';
64
66
  import { storeValidationResult, getValidationStats, renderValidationStats } from '../validation/stats.js';
65
67
  import { isFeatureAvailable, getJarvisLimitsForPlan, getBrainLimitsForPlan, isLoggedIn } from '../auth/feature-gate.js';
@@ -80,6 +82,8 @@ const HELP_CATEGORIES = [
80
82
  { cmd: '/clear', label: '/clear', description: 'Clear conversation history' },
81
83
  { cmd: '/model', label: '/model', description: 'Switch LLM model' },
82
84
  { cmd: '/keys', label: '/keys', description: 'Manage API keys' },
85
+ { cmd: '/fast [prompt]', label: '/fast', description: 'Low-latency turn (skip validation, no auto-swarm)' },
86
+ { cmd: '/swarm [prompt]', label: '/swarm', description: 'Force swarm decomposition for one turn' },
83
87
  { cmd: '/yolo', label: '/yolo', description: 'Toggle YOLO mode' },
84
88
  { cmd: '/skip-permissions', label: '/skip-permissions', description: 'Toggle skip-permissions' },
85
89
  { cmd: '/plan', label: '/plan', description: 'Toggle plan mode (read-only → plan → execute)' },
@@ -225,10 +229,12 @@ const HELP_TEXT = `
225
229
  ${chalk.hex('#00d4ff').bold(' Chat & Interaction')}
226
230
  ${theme.primary('/help'.padEnd(22))} ${theme.dim('Show this help')}
227
231
  ${theme.primary('/clear'.padEnd(22))} ${theme.dim('Clear conversation history')}
228
- ${theme.primary('/model [name]'.padEnd(22))} ${theme.dim('Switch model (interactive or direct: /model gpt-4o)')}
229
- ${theme.primary('/keys'.padEnd(22))} ${theme.dim('Add/remove/update API keys')}
230
- ${theme.primary('/yolo [on|off]'.padEnd(22))} ${theme.dim('Toggle YOLO mode auto-approve ALL operations')}
231
- ${theme.primary('/skip-permissions'.padEnd(22))} ${theme.dim('Toggle skip-permissions (auto-approve safe ops)')}
232
+ ${theme.primary('/model [name]'.padEnd(22))} ${theme.dim('Switch model (interactive or direct: /model gpt-4o)')}
233
+ ${theme.primary('/keys'.padEnd(22))} ${theme.dim('Add/remove/update API keys')}
234
+ ${theme.primary('/fast [prompt]'.padEnd(22))} ${theme.dim('One fast turn: skip validation and auto-swarm')}
235
+ ${theme.primary('/swarm [prompt]'.padEnd(22))} ${theme.dim('Force multi-agent swarm for one request')}
236
+ ${theme.primary('/yolo [on|off]'.padEnd(22))} ${theme.dim('Toggle YOLO mode — auto-approve ALL operations')}
237
+ ${theme.primary('/skip-permissions'.padEnd(22))} ${theme.dim('Toggle skip-permissions (auto-approve safe ops)')}
232
238
 
233
239
  ${chalk.hex('#00ff88').bold(' Spiral Memory')}
234
240
  ${theme.primary('/spiral'.padEnd(22))} ${theme.dim('Show spiral status (nodes per level)')}
@@ -411,11 +417,13 @@ export async function chatCommand(options) {
411
417
  let jarvisTelegramBot = null;
412
418
  let jarvisLearning;
413
419
  let jarvisScope;
420
+ let jarvisRuntimeReady = false;
421
+ let pendingJarvisQueueBinder = null;
414
422
  let jarvisDaemonSession = null;
415
423
  let jarvisPaused = false;
416
424
  const resolveJarvisRoot = (scope) => scope === 'project' ? process.cwd() : join(homedir(), '.spiral-context');
417
- // Browser controller (always instantiated launch() is the expensive part)
418
- let browserController = new BrowserController();
425
+ // Browser controller stays lazy so plain chat startup avoids browser setup work.
426
+ let browserController;
419
427
  let visionProcessor;
420
428
  // Terminal + Screen (replaces BottomChrome with framed input area)
421
429
  const terminal = new Terminal();
@@ -428,6 +436,16 @@ export async function chatCommand(options) {
428
436
  const chrome = screen;
429
437
  // Activity indicator (renders on chrome row 0 during agent work)
430
438
  const activity = new ActivityIndicator(chrome);
439
+ const chromeRowCache = new Map();
440
+ function invalidateChromeRows() {
441
+ chromeRowCache.clear();
442
+ }
443
+ function setChromeRow(row, content, force = false) {
444
+ if (!force && chromeRowCache.get(row) === content)
445
+ return;
446
+ chromeRowCache.set(row, content);
447
+ chrome.setRow(row, content);
448
+ }
431
449
  // Agent controller for pause/resume
432
450
  const agentController = new AgentController();
433
451
  let agentRunning = false;
@@ -557,28 +575,53 @@ export async function chatCommand(options) {
557
575
  }
558
576
  // Jarvis scope follows brain scope — init after brainScope detection
559
577
  jarvisScope = brainScope;
560
- jarvisQueue = new JarvisQueue(resolveJarvisRoot(jarvisScope));
561
- jarvisIdentity = new JarvisIdentityManager(resolveJarvisRoot(jarvisScope));
562
- jarvisProposals = new ProposalJournal(resolveJarvisRoot(jarvisScope));
563
- jarvisScheduler = new JarvisScheduler(resolveJarvisRoot(jarvisScope));
564
- jarvisTriggers = new TriggerManager(resolveJarvisRoot(jarvisScope));
565
- jarvisWorldModel = new WorldModelManager(resolveJarvisRoot(jarvisScope));
566
- jarvisAutonomy = new AutonomyManager(jarvisIdentity.getIdentity().autonomyLevel);
567
- jarvisNotifications = new NotificationManager(resolveJarvisRoot(jarvisScope));
568
- jarvisSkills = new SkillManager(resolveJarvisRoot(jarvisScope));
569
- jarvisSkills.syncRegistry();
570
- jarvisLearning = new LearningJournal(resolveJarvisRoot(jarvisScope));
571
- jarvisSentiment = new SentimentAnalyzer(resolveJarvisRoot(jarvisScope), {
572
- onShift: (from, to, frustrationLevel) => {
573
- jarvisIdentity.recordEvent({ type: 'sentiment_shift', from, to, frustrationLevel });
574
- import('../brain/generator.js').then(mod => {
575
- if (mod.isBrainServerRunning?.()) {
576
- mod.pushNeuronFired?.('yellow', '#ff6b6b', 'sentiment');
577
- }
578
- }).catch(() => { });
579
- },
580
- });
578
+ function initializeJarvisRuntime(scope = jarvisScope) {
579
+ const root = resolveJarvisRoot(scope);
580
+ jarvisScope = scope;
581
+ jarvisQueue = new JarvisQueue(root);
582
+ jarvisIdentity = new JarvisIdentityManager(root);
583
+ jarvisProposals = new ProposalJournal(root);
584
+ jarvisScheduler = new JarvisScheduler(root);
585
+ jarvisTriggers = new TriggerManager(root);
586
+ jarvisWorldModel = new WorldModelManager(root);
587
+ jarvisAutonomy = new AutonomyManager(jarvisIdentity.getIdentity().autonomyLevel);
588
+ jarvisNotifications = new NotificationManager(root);
589
+ jarvisSkills = new SkillManager(root);
590
+ jarvisSkills.syncRegistry();
591
+ jarvisLearning = new LearningJournal(root);
592
+ jarvisSentiment = new SentimentAnalyzer(root, {
593
+ onShift: (from, to, frustrationLevel) => {
594
+ jarvisIdentity.recordEvent({ type: 'sentiment_shift', from, to, frustrationLevel });
595
+ import('../brain/generator.js').then(mod => {
596
+ if (mod.isBrainServerRunning?.()) {
597
+ mod.pushNeuronFired?.('yellow', '#ff6b6b', 'sentiment');
598
+ }
599
+ }).catch(() => { });
600
+ },
601
+ });
602
+ jarvisRuntimeReady = true;
603
+ pendingJarvisQueueBinder?.();
604
+ }
605
+ function ensureJarvisRuntime(scope = jarvisScope) {
606
+ if (jarvisRuntimeReady && jarvisScope === scope)
607
+ return;
608
+ initializeJarvisRuntime(scope);
609
+ }
610
+ function warmJarvisRuntime(scope = jarvisScope) {
611
+ if (jarvisRuntimeReady && jarvisScope === scope)
612
+ return;
613
+ setTimeout(() => {
614
+ try {
615
+ ensureJarvisRuntime(scope);
616
+ }
617
+ catch {
618
+ // Best effort: Jarvis can still initialize on first real use.
619
+ }
620
+ }, 0);
621
+ }
581
622
  let spiralEngine = null;
623
+ let spiralInitPromise = null;
624
+ let spiralInitScope = null;
582
625
  async function initSpiralEngine(scope) {
583
626
  try {
584
627
  const { SpiralEngine } = await import('../../spiral/engine.js');
@@ -593,8 +636,58 @@ export async function chatCommand(options) {
593
636
  return null;
594
637
  }
595
638
  }
596
- if (config.spiral.enabled) {
597
- spiralEngine = await initSpiralEngine(brainScope);
639
+ async function ensureSpiralEngine(scope = brainScope) {
640
+ if (!config.spiral.enabled)
641
+ return null;
642
+ if (spiralEngine && spiralInitScope === scope)
643
+ return spiralEngine;
644
+ if (spiralInitPromise && spiralInitScope === scope)
645
+ return spiralInitPromise;
646
+ spiralInitScope = scope;
647
+ spiralInitPromise = initSpiralEngine(scope).then(async (engine) => {
648
+ spiralEngine = engine;
649
+ spiralInitPromise = null;
650
+ if (engine && brainUrl) {
651
+ try {
652
+ const { startLiveBrain } = await import('../brain/generator.js');
653
+ await startLiveBrain(engine, project.name || 'HelixMind', scope);
654
+ }
655
+ catch {
656
+ // Dashboard sync is best effort.
657
+ }
658
+ }
659
+ return engine;
660
+ }).catch((err) => {
661
+ spiralInitPromise = null;
662
+ spiralInitScope = null;
663
+ throw err;
664
+ });
665
+ return spiralInitPromise;
666
+ }
667
+ async function getSpiralEngine(scope = brainScope) {
668
+ if (!config.spiral.enabled)
669
+ return null;
670
+ if (spiralEngine && spiralInitScope === scope)
671
+ return spiralEngine;
672
+ const engine = await ensureSpiralEngine(scope);
673
+ spiralEngine = engine;
674
+ return engine;
675
+ }
676
+ function warmSpiralEngine(scope = brainScope) {
677
+ if (!config.spiral.enabled)
678
+ return;
679
+ void ensureSpiralEngine(scope).catch(() => { });
680
+ }
681
+ function resetSpiralEngine(scope) {
682
+ if (spiralEngine) {
683
+ try {
684
+ spiralEngine.close();
685
+ }
686
+ catch { /* best effort */ }
687
+ }
688
+ spiralEngine = null;
689
+ spiralInitPromise = null;
690
+ spiralInitScope = scope ?? null;
598
691
  }
599
692
  // Register current brain in manager (so limits track it)
600
693
  if (brainManager) {
@@ -617,6 +710,7 @@ export async function chatCommand(options) {
617
710
  }
618
711
  // Build SkillContext for skill activation
619
712
  function buildSkillContext() {
713
+ ensureJarvisRuntime();
620
714
  return {
621
715
  registerTool: (name, def, handler) => {
622
716
  // Skills register tools into the skill manager's registry
@@ -626,10 +720,11 @@ export async function chatCommand(options) {
626
720
  unregisterTool: (_name) => { },
627
721
  addTask: (title, desc, priority) => jarvisQueue.addTask(title, desc, { priority }),
628
722
  querySpiral: async (query) => {
629
- if (!spiralEngine)
723
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
724
+ if (!engine)
630
725
  return '';
631
726
  try {
632
- const results = await spiralEngine.query(query, { maxResults: 10 });
727
+ const results = await engine.query(query, { maxResults: 10 });
633
728
  return results.level_1.concat(results.level_2, results.level_3)
634
729
  .map((n) => `[${n.type}] ${n.content}`)
635
730
  .join('\n')
@@ -640,10 +735,11 @@ export async function chatCommand(options) {
640
735
  }
641
736
  },
642
737
  storeInSpiral: async (content, type, tags) => {
643
- if (!spiralEngine)
738
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
739
+ if (!engine)
644
740
  return;
645
741
  try {
646
- await spiralEngine.add(content, { type, tags });
742
+ await engine.add(content, { type, tags });
647
743
  }
648
744
  catch { }
649
745
  },
@@ -657,6 +753,7 @@ export async function chatCommand(options) {
657
753
  }
658
754
  // Start Telegram bot if configured (called when daemon starts)
659
755
  function startTelegramBot() {
756
+ ensureJarvisRuntime();
660
757
  if (jarvisTelegramBot?.isRunning)
661
758
  return;
662
759
  const teleConfig = jarvisNotifications.getConfig().targets.find(t => t.channel === 'telegram');
@@ -777,6 +874,7 @@ export async function chatCommand(options) {
777
874
  }
778
875
  // Build ThinkingCallbacks for Jarvis AGI thinking loop
779
876
  function buildThinkingCallbacks(bgSession) {
877
+ ensureJarvisRuntime();
780
878
  return {
781
879
  sendMessage: async (prompt) => {
782
880
  bgSession.controller.reset();
@@ -789,10 +887,11 @@ export async function chatCommand(options) {
789
887
  isAborted: () => bgSession.controller.isAborted,
790
888
  isPaused: () => jarvisPaused,
791
889
  querySpiral: async (query, maxTokens) => {
792
- if (!spiralEngine)
890
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
891
+ if (!engine)
793
892
  return '';
794
893
  try {
795
- const results = await spiralEngine.query(query, { maxResults: maxTokens ?? 50 });
894
+ const results = await engine.query(query, { maxResults: maxTokens ?? 50 });
796
895
  return results.level_1.concat(results.level_2, results.level_3)
797
896
  .map((n) => `[${n.type}] ${n.content}`)
798
897
  .join('\n')
@@ -803,10 +902,11 @@ export async function chatCommand(options) {
803
902
  }
804
903
  },
805
904
  storeInSpiral: async (content, type, tags) => {
806
- if (!spiralEngine)
905
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
906
+ if (!engine)
807
907
  return;
808
908
  try {
809
- await spiralEngine.add(content, { type: type, tags });
909
+ await engine.add(content, { type: type, tags });
810
910
  }
811
911
  catch { }
812
912
  },
@@ -1210,7 +1310,8 @@ export async function chatCommand(options) {
1210
1310
  bgSession.controller.reset();
1211
1311
  const rth = { text: '' };
1212
1312
  bgSession.buffer.onSummary = (t) => { rth.text = t; };
1213
- await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
1313
+ const activeSpiral = await getSpiralEngine(brainScope);
1314
+ await sendAgentMessage(prompt, bgSession.history, provider, project, activeSpiral, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
1214
1315
  bgSession.buffer.onSummary = undefined;
1215
1316
  return rth.text;
1216
1317
  },
@@ -1306,6 +1407,7 @@ export async function chatCommand(options) {
1306
1407
  return bugJournal.delete(id);
1307
1408
  },
1308
1409
  startJarvis: () => {
1410
+ ensureJarvisRuntime();
1309
1411
  if (jarvisDaemonSession && jarvisDaemonSession.status === 'running')
1310
1412
  return jarvisDaemonSession.id;
1311
1413
  // Enforce cross-process Jarvis instance limit
@@ -1362,9 +1464,12 @@ export async function chatCommand(options) {
1362
1464
  pushSessionUpdate(serializeSession(bgSession));
1363
1465
  },
1364
1466
  updateStatus: () => updateStatusBar(),
1365
- storeInSpiral: spiralEngine ? async (content, type, tags) => {
1467
+ storeInSpiral: config.spiral.enabled ? async (content, type, tags) => {
1468
+ const activeSpiral = await getSpiralEngine(brainScope);
1469
+ if (!activeSpiral)
1470
+ return;
1366
1471
  try {
1367
- await spiralEngine.add(content, { type: type, tags });
1472
+ await activeSpiral.add(content, { type: type, tags });
1368
1473
  }
1369
1474
  catch { }
1370
1475
  } : undefined,
@@ -1398,6 +1503,7 @@ export async function chatCommand(options) {
1398
1503
  return true;
1399
1504
  },
1400
1505
  pauseJarvis: () => {
1506
+ ensureJarvisRuntime();
1401
1507
  if (!jarvisDaemonSession || jarvisPaused)
1402
1508
  return false;
1403
1509
  jarvisPaused = true;
@@ -1405,6 +1511,7 @@ export async function chatCommand(options) {
1405
1511
  return true;
1406
1512
  },
1407
1513
  resumeJarvis: () => {
1514
+ ensureJarvisRuntime();
1408
1515
  if (!jarvisDaemonSession || !jarvisPaused)
1409
1516
  return false;
1410
1517
  jarvisPaused = false;
@@ -1412,24 +1519,36 @@ export async function chatCommand(options) {
1412
1519
  return true;
1413
1520
  },
1414
1521
  addJarvisTask: (title, description, opts) => {
1522
+ ensureJarvisRuntime();
1415
1523
  const task = jarvisQueue.addTask(title, description, opts);
1416
1524
  return serializeJarvisTask(task);
1417
1525
  },
1418
- listJarvisTasks: () => jarvisQueue.getAllTasks().map(serializeJarvisTask),
1526
+ listJarvisTasks: () => {
1527
+ ensureJarvisRuntime();
1528
+ return jarvisQueue.getAllTasks().map(serializeJarvisTask);
1529
+ },
1419
1530
  deleteJarvisTask: (taskId) => {
1531
+ ensureJarvisRuntime();
1420
1532
  return jarvisQueue.removeTask(taskId);
1421
1533
  },
1422
- getJarvisStatus: () => ({
1423
- ...jarvisQueue.getStatus(),
1424
- scope: jarvisScope === 'project' ? 'local' : 'global',
1425
- jarvisName: jarvisIdentity.getIdentity().name,
1426
- }),
1427
- clearJarvisCompleted: () => jarvisQueue.clearCompleted(),
1534
+ getJarvisStatus: () => {
1535
+ ensureJarvisRuntime();
1536
+ return {
1537
+ ...jarvisQueue.getStatus(),
1538
+ scope: jarvisScope === 'project' ? 'local' : 'global',
1539
+ jarvisName: jarvisIdentity.getIdentity().name,
1540
+ };
1541
+ },
1542
+ clearJarvisCompleted: () => {
1543
+ ensureJarvisRuntime();
1544
+ return jarvisQueue.clearCompleted();
1545
+ },
1428
1546
  // Jarvis AGI handlers (stubs — wired when AGI modules are initialized)
1429
1547
  listProposals: () => [],
1430
1548
  approveProposal: () => false,
1431
1549
  denyProposal: () => false,
1432
1550
  setAutonomyLevel: (level) => {
1551
+ ensureJarvisRuntime();
1433
1552
  if (level < 0 || level > 5)
1434
1553
  return false;
1435
1554
  jarvisAutonomy.setLevel(level);
@@ -1776,19 +1895,24 @@ export async function chatCommand(options) {
1776
1895
  pushBugUpdated(bugInfo);
1777
1896
  }
1778
1897
  });
1779
- // Wire Jarvis queue change events to brain server
1780
- jarvisQueue.setOnChange((event, task) => {
1781
- const info = serializeJarvisTask(task);
1782
- if (event === 'task_created') {
1783
- pushJarvisTaskCreated(info);
1784
- }
1785
- else if (event === 'task_removed') {
1786
- pushJarvisTaskRemoved(task.id);
1787
- }
1788
- else {
1789
- pushJarvisTaskUpdated(info);
1790
- }
1791
- });
1898
+ // Wire Jarvis queue change events to brain server when Jarvis runtime exists.
1899
+ pendingJarvisQueueBinder = () => {
1900
+ if (!jarvisRuntimeReady)
1901
+ return;
1902
+ jarvisQueue.setOnChange((event, task) => {
1903
+ const info = serializeJarvisTask(task);
1904
+ if (event === 'task_created') {
1905
+ pushJarvisTaskCreated(info);
1906
+ }
1907
+ else if (event === 'task_removed') {
1908
+ pushJarvisTaskRemoved(task.id);
1909
+ }
1910
+ else {
1911
+ pushJarvisTaskUpdated(info);
1912
+ }
1913
+ });
1914
+ };
1915
+ pendingJarvisQueueBinder();
1792
1916
  // Wire browser screenshots to brain server
1793
1917
  pushScreenshotToBrainFn = (info) => {
1794
1918
  pushBrowserScreenshot({
@@ -1888,6 +2012,34 @@ export async function chatCommand(options) {
1888
2012
  hints.push(chalk.dim('esc to interrupt'));
1889
2013
  return hints.join(chalk.dim(' \u00B7 '));
1890
2014
  }
2015
+ function renderExecutionIntent(directives, validationDecision, swarmHeuristic, usingSwarm) {
2016
+ const parts = [];
2017
+ if (usingSwarm) {
2018
+ const reason = swarmHeuristic.reasons.slice(0, 2).join(', ');
2019
+ parts.push(chalk.hex('#ffd700')('swarm'));
2020
+ if (reason)
2021
+ parts.push(chalk.dim(reason));
2022
+ }
2023
+ else {
2024
+ parts.push(chalk.hex('#00d4ff')('single-agent'));
2025
+ if (directives.skipSwarm) {
2026
+ parts.push(chalk.dim('swarm skipped'));
2027
+ }
2028
+ else if (swarmHeuristic.shouldOrchestrate) {
2029
+ parts.push(chalk.dim('swarm available but not used'));
2030
+ }
2031
+ }
2032
+ if (validationDecision.enabled) {
2033
+ parts.push(chalk.green('validation on'));
2034
+ }
2035
+ else {
2036
+ parts.push(chalk.yellow(`validation off (${validationDecision.reason})`));
2037
+ }
2038
+ if (directives.fastMode) {
2039
+ parts.push(chalk.hex('#ff6600')('fast lane'));
2040
+ }
2041
+ renderInfo(chalk.dim('Intent: ') + parts.join(chalk.dim(' · ')));
2042
+ }
1891
2043
  /**
1892
2044
  * Show the full prompt area using sticky bottom chrome (4 rows):
1893
2045
  * ┌──────────────────────────────────────┐ ← chrome row 0 (N-3): top border / activity indicator
@@ -1912,9 +2064,9 @@ export async function chatCommand(options) {
1912
2064
  }
1913
2065
  // Now set chrome content and draw (screen must be active for these to render)
1914
2066
  screen.drawFrameBottom();
1915
- chrome.setRow(1, hintLine);
1916
- chrome.setRow(2, ' ' + statusLine1);
1917
- chrome.setRow(3, ' ' + statusLine2);
2067
+ setChromeRow(1, hintLine, true);
2068
+ setChromeRow(2, ' ' + statusLine1, true);
2069
+ setChromeRow(3, ' ' + statusLine2, true);
1918
2070
  isAtPrompt = true;
1919
2071
  // Suppress stdout hook redraws while InputManager manages the cursor
1920
2072
  screen.inputActive = true;
@@ -2008,6 +2160,7 @@ export async function chatCommand(options) {
2008
2160
  // Update screen and activity on terminal resize
2009
2161
  process.stdout.on('resize', () => {
2010
2162
  closeSuggestionPanel(); // Close panel on resize (reopens on next keypress)
2163
+ invalidateChromeRows();
2011
2164
  screen.handleResize();
2012
2165
  activity.handleResize();
2013
2166
  if (isAtPrompt) {
@@ -2039,9 +2192,11 @@ export async function chatCommand(options) {
2039
2192
  isAtPrompt = active;
2040
2193
  // Deactivate/activate bottom chrome so permission select menu renders cleanly
2041
2194
  if (active) {
2195
+ invalidateChromeRows();
2042
2196
  chrome.deactivate();
2043
2197
  }
2044
2198
  else {
2199
+ invalidateChromeRows();
2045
2200
  chrome.activate();
2046
2201
  }
2047
2202
  });
@@ -2065,7 +2220,7 @@ export async function chatCommand(options) {
2065
2220
  if (isAtPrompt) {
2066
2221
  const hintLine = buildHintLine();
2067
2222
  activity.setRestoreContent(hintLine);
2068
- chrome.setRow(1, hintLine);
2223
+ invalidateChromeRows();
2069
2224
  updateStatusBar();
2070
2225
  }
2071
2226
  });
@@ -2234,12 +2389,7 @@ export async function chatCommand(options) {
2234
2389
  if (newScope === brainScope)
2235
2390
  return;
2236
2391
  try {
2237
- if (spiralEngine) {
2238
- try {
2239
- spiralEngine.close();
2240
- }
2241
- catch { /* best effort */ }
2242
- }
2392
+ resetSpiralEngine();
2243
2393
  brainScope = newScope;
2244
2394
  if (newScope === 'project') {
2245
2395
  const { mkdirSync, existsSync } = await import('node:fs');
@@ -2258,7 +2408,7 @@ export async function chatCommand(options) {
2258
2408
  }
2259
2409
  }
2260
2410
  }
2261
- spiralEngine = await initSpiralEngine(newScope);
2411
+ spiralEngine = await ensureSpiralEngine(newScope);
2262
2412
  const { exportBrainData } = await import('../brain/exporter.js');
2263
2413
  const { startLiveBrain } = await import('../brain/generator.js');
2264
2414
  await startLiveBrain(spiralEngine, project.name || 'HelixMind', newScope);
@@ -2583,6 +2733,7 @@ export async function chatCommand(options) {
2583
2733
  function getJarvisContextForPrompt() {
2584
2734
  if (!jarvisDaemonSession || jarvisDaemonSession.status !== 'running')
2585
2735
  return null;
2736
+ ensureJarvisRuntime();
2586
2737
  const sentimentGuidance = jarvisSentiment.getResponseGuidance();
2587
2738
  const identityPrompt = jarvisIdentity.getIdentityPrompt(jarvisSkills.getSkillsPrompt() ?? undefined, sentimentGuidance || undefined);
2588
2739
  const runtimeContext = buildRuntimeContext({
@@ -2623,9 +2774,9 @@ export async function chatCommand(options) {
2623
2774
  line1 = `${tabBar} ${statusLine1}`;
2624
2775
  }
2625
2776
  // Refresh hint line on row 1 (prevents stale border/status from previous layout)
2626
- chrome.setRow(1, buildHintLine());
2627
- chrome.setRow(2, ' ' + line1);
2628
- chrome.setRow(3, ' ' + (statusLine2 || ''));
2777
+ setChromeRow(1, buildHintLine());
2778
+ setChromeRow(2, ' ' + line1);
2779
+ setChromeRow(3, ' ' + (statusLine2 || ''));
2629
2780
  }
2630
2781
  /** Push session findings to brain visualization */
2631
2782
  function pushFindingsToBrain(session) {
@@ -2688,6 +2839,9 @@ export async function chatCommand(options) {
2688
2839
  // Write the collected startup banner into the scroll region.
2689
2840
  // Content flows bottom-up within the region, sitting just above the input frame.
2690
2841
  screen.writeOutput(startupBannerParts.join(''));
2842
+ // Warm optional heavy subsystems after the prompt is already visible.
2843
+ warmJarvisRuntime(jarvisScope);
2844
+ warmSpiralEngine(brainScope);
2691
2845
  // Footer timer — redraws statusbar on chrome rows 2+3 during agent work.
2692
2846
  // Skipped when:
2693
2847
  // - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
@@ -2701,6 +2855,7 @@ export async function chatCommand(options) {
2701
2855
  footerTimer.unref();
2702
2856
  /** Process a complete input (single line or assembled paste block) */
2703
2857
  async function processInput(input, displayText) {
2858
+ const rawInput = input;
2704
2859
  // Clear input frame FIRST — before any renderInfo/process.stdout.write.
2705
2860
  // This ensures the cursor moves out of the input area so all output
2706
2861
  // (slash command results, "daemon started", etc.) goes to the scroll region.
@@ -2711,12 +2866,14 @@ export async function chatCommand(options) {
2711
2866
  promptHistory.add(input, process.cwd()).catch(() => { });
2712
2867
  // Handle /feed directly here (needs access to inlineProgressActive flag)
2713
2868
  if (input.startsWith('/feed')) {
2714
- if (spiralEngine) {
2869
+ const activeSpiral = spiralEngine ?? await ensureSpiralEngine(brainScope);
2870
+ if (activeSpiral) {
2871
+ spiralEngine = activeSpiral;
2715
2872
  const feedPath = input.split(/\s+/)[1];
2716
2873
  const rootDir = process.cwd();
2717
2874
  renderInfo('\u{1F300} Feeding project...\n');
2718
2875
  try {
2719
- const result = await runFeedPipeline(rootDir, spiralEngine, {
2876
+ const result = await runFeedPipeline(rootDir, activeSpiral, {
2720
2877
  targetPath: feedPath,
2721
2878
  onProgress: wrappedFeedProgress,
2722
2879
  });
@@ -2778,8 +2935,27 @@ export async function chatCommand(options) {
2778
2935
  showPrompt();
2779
2936
  return;
2780
2937
  }
2938
+ const turnDirectives = parseTurnDirectives(rawInput);
2939
+ input = turnDirectives.input;
2940
+ const renderedInput = displayText ?? turnDirectives.displayInput;
2941
+ if (!input) {
2942
+ if (turnDirectives.fastMode) {
2943
+ renderInfo(chalk.dim('Usage: /fast <prompt>'));
2944
+ }
2945
+ else if (turnDirectives.forceSwarm) {
2946
+ renderInfo(chalk.dim('Usage: /swarm <prompt>'));
2947
+ }
2948
+ else if (turnDirectives.strippedFlags.length > 0) {
2949
+ renderInfo(chalk.dim('Execution flags were provided without a prompt.'));
2950
+ }
2951
+ showPrompt();
2952
+ return;
2953
+ }
2781
2954
  // Handle slash commands
2782
- if (input.startsWith('/')) {
2955
+ if (input.startsWith('/') && !turnDirectives.forceSwarm) {
2956
+ if (/^\/(?:jarvis|del)\b/i.test(input)) {
2957
+ ensureJarvisRuntime();
2958
+ }
2783
2959
  const handled = await handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, { input: sessionTokensInput, output: sessionTokensOutput }, sessionToolCalls, (newProvider) => {
2784
2960
  provider = newProvider;
2785
2961
  config = store.getAll();
@@ -2788,12 +2964,7 @@ export async function chatCommand(options) {
2788
2964
  typeAheadBuffer.length = 0;
2789
2965
  }, async (newScope) => {
2790
2966
  // Switch brain scope
2791
- if (spiralEngine) {
2792
- try {
2793
- spiralEngine.close();
2794
- }
2795
- catch { /* best effort */ }
2796
- }
2967
+ resetSpiralEngine();
2797
2968
  brainScope = newScope;
2798
2969
  // Create .helixmind/ dir if switching to project and it doesn't exist
2799
2970
  if (newScope === 'project') {
@@ -2817,8 +2988,8 @@ export async function chatCommand(options) {
2817
2988
  }
2818
2989
  }
2819
2990
  }
2820
- spiralEngine = await initSpiralEngine(newScope);
2821
- }, brainScope, async (action, goal) => {
2991
+ spiralEngine = await ensureSpiralEngine(newScope);
2992
+ }, brainScope, (scope) => getSpiralEngine(scope ?? brainScope), async (action, goal) => {
2822
2993
  if (action === 'stop') {
2823
2994
  // Stop all background sessions + autonomous mode
2824
2995
  const running = sessionMgr.running;
@@ -3075,23 +3246,7 @@ export async function chatCommand(options) {
3075
3246
  sentiment: jarvisSentiment,
3076
3247
  getScope: () => jarvisScope,
3077
3248
  setScope: (scope) => {
3078
- jarvisScope = scope;
3079
- const newRoot = resolveJarvisRoot(scope);
3080
- jarvisQueue = new JarvisQueue(newRoot);
3081
- jarvisIdentity = new JarvisIdentityManager(newRoot);
3082
- jarvisProposals = new ProposalJournal(newRoot);
3083
- jarvisScheduler = new JarvisScheduler(newRoot);
3084
- jarvisTriggers = new TriggerManager(newRoot);
3085
- jarvisWorldModel = new WorldModelManager(newRoot);
3086
- jarvisNotifications = new NotificationManager(newRoot);
3087
- jarvisSkills = new SkillManager(newRoot);
3088
- jarvisSkills.syncRegistry();
3089
- jarvisSentiment = new SentimentAnalyzer(newRoot, {
3090
- onShift: (from, to, frustrationLevel) => {
3091
- jarvisIdentity.recordEvent({ type: 'sentiment_shift', from, to, frustrationLevel });
3092
- },
3093
- });
3094
- jarvisAutonomy = new AutonomyManager(jarvisIdentity.getIdentity().autonomyLevel);
3249
+ initializeJarvisRuntime(scope);
3095
3250
  },
3096
3251
  getSession: () => jarvisDaemonSession,
3097
3252
  setSession: (s) => { jarvisDaemonSession = s; },
@@ -3106,6 +3261,7 @@ export async function chatCommand(options) {
3106
3261
  startTelegramBot,
3107
3262
  learning: jarvisLearning,
3108
3263
  startDaemon: () => {
3264
+ ensureJarvisRuntime();
3109
3265
  const jName = jarvisIdentity.getIdentity().name;
3110
3266
  const bgSession = sessionMgr.create(jName, '\u{1F916}', agentHistory);
3111
3267
  bgSession.start();
@@ -3116,13 +3272,13 @@ export async function chatCommand(options) {
3116
3272
  const d = chalk.dim;
3117
3273
  const j = chalk.hex('#ff00ff');
3118
3274
  const g = chalk.hex('#FFB800');
3119
- const displayName = jName.toUpperCase() + ' AGI';
3275
+ const displayName = jName.toUpperCase();
3120
3276
  const namePad = Math.max(0, 34 - displayName.length);
3121
3277
  const banner = '\n' +
3122
3278
  d('\u256D' + '\u2500'.repeat(45) + '\u256E') + '\n' +
3123
3279
  d('\u2502 ') + g('\u{1F31F}') + ' ' + j(displayName) + d(' '.repeat(namePad) + '\u2502') + '\n' +
3124
3280
  d('\u2502' + ' '.repeat(45) + '\u2502') + '\n' +
3125
- d('\u2502 ') + 'Autonomous agent \u2014 thinking, learning,' + d(' \u2502') + '\n' +
3281
+ d('\u2502 ') + 'Autonomous assistant \u2014 thinking,' + d(' \u2502') + '\n' +
3126
3282
  d('\u2502 ') + 'proposing, executing tasks' + d(' '.repeat(17) + '\u2502') + '\n' +
3127
3283
  d('\u2502' + ' '.repeat(45) + '\u2502') + '\n' +
3128
3284
  d('\u2502 ') + d('/jarvis stop or ESC to stop') + d(' '.repeat(13) + '\u2502') + '\n' +
@@ -3149,7 +3305,8 @@ export async function chatCommand(options) {
3149
3305
  bgSession.controller.reset();
3150
3306
  const rth = { text: '' };
3151
3307
  bgSession.buffer.onSummary = (t) => { rth.text = t; };
3152
- await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, daemonPermissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
3308
+ const activeSpiral = await getSpiralEngine(brainScope);
3309
+ await sendAgentMessage(prompt, bgSession.history, provider, project, activeSpiral, config, daemonPermissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
3153
3310
  bgSession.buffer.onSummary = undefined;
3154
3311
  return rth.text;
3155
3312
  },
@@ -3184,9 +3341,12 @@ export async function chatCommand(options) {
3184
3341
  }
3185
3342
  },
3186
3343
  updateStatus: () => updateStatusBar(),
3187
- storeInSpiral: spiralEngine ? async (content, type, tags) => {
3344
+ storeInSpiral: config.spiral.enabled ? async (content, type, tags) => {
3345
+ const activeSpiral = await getSpiralEngine(brainScope);
3346
+ if (!activeSpiral)
3347
+ return;
3188
3348
  try {
3189
- await spiralEngine.add(content, { type: type, tags });
3349
+ await activeSpiral.add(content, { type: type, tags });
3190
3350
  }
3191
3351
  catch { }
3192
3352
  } : undefined,
@@ -3245,10 +3405,11 @@ export async function chatCommand(options) {
3245
3405
  }
3246
3406
  // Frame was already cleared at processInput entry — render user message
3247
3407
  // Use display text (badge placeholders) if available, full text goes to model
3248
- renderUserMessage(displayText || input);
3408
+ renderUserMessage(renderedInput);
3249
3409
  // Track user message in session buffer
3250
3410
  sessionBuffer.addUserMessage(input);
3251
3411
  // Sentiment detection on every user message
3412
+ ensureJarvisRuntime();
3252
3413
  const sentimentReading = jarvisSentiment.detectSentiment(input);
3253
3414
  jarvisSentiment.recordReading(sentimentReading);
3254
3415
  // Auto-detect bug reports from user messages
@@ -3401,10 +3562,16 @@ export async function chatCommand(options) {
3401
3562
  }
3402
3563
  else {
3403
3564
  // ═══ NORMAL MODE ═══
3404
- // --- Swarm auto-detection: check if multi-task request ---
3405
- const forceSwarm = input.startsWith('/swarm ');
3406
- const swarmInput = forceSwarm ? input.slice(7).trim() : input;
3407
- if (!activeSwarm && (forceSwarm || swarmOrchestrator.shouldOrchestrate(swarmInput))) {
3565
+ if (config.spiral.enabled && !turnDirectives.fastMode) {
3566
+ spiralEngine = await ensureSpiralEngine(brainScope);
3567
+ }
3568
+ const effectiveValidation = resolveValidationDecision(input, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, turnDirectives);
3569
+ const swarmHeuristic = swarmOrchestrator.analyze(input);
3570
+ const shouldUseSwarm = !activeSwarm &&
3571
+ !turnDirectives.skipSwarm &&
3572
+ (turnDirectives.forceSwarm || swarmHeuristic.shouldOrchestrate);
3573
+ renderExecutionIntent(turnDirectives, effectiveValidation, swarmHeuristic, shouldUseSwarm);
3574
+ if (shouldUseSwarm) {
3408
3575
  const { pushSwarmCreated, pushSwarmUpdated, pushSwarmCompleted, pushWorkerStarted, pushWorkerCompleted, pushOutputLine: _pushOutputLine, pushSessionCreated: _pushSessionCreated } = await import('../brain/generator.js');
3409
3576
  const { serializeSession: _serializeSession } = await import('../brain/control-protocol.js');
3410
3577
  const swarm = new SwarmController(swarmOrchestrator, swarmExecutor, sessionMgr, {
@@ -3434,7 +3601,7 @@ export async function chatCommand(options) {
3434
3601
  };
3435
3602
  _pushSessionCreated(_serializeSession(session));
3436
3603
  try {
3437
- await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, undefined, undefined, undefined, null, undefined, undefined, runtime);
3604
+ await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, effectiveValidation, bugJournal, undefined, undefined, undefined, null, undefined, undefined, runtime);
3438
3605
  }
3439
3606
  finally {
3440
3607
  await runtime.release();
@@ -3459,8 +3626,10 @@ export async function chatCommand(options) {
3459
3626
  pushWorkerCompleted,
3460
3627
  });
3461
3628
  activeSwarm = swarm;
3462
- renderInfo(chalk.hex('#ffd700')('\u{1F41D} Swarm detected — decomposing into parallel tasks...'));
3463
- const summary = await swarm.run(swarmInput);
3629
+ const swarmReason = swarmHeuristic.reasons.slice(0, 2).join(', ');
3630
+ renderInfo(chalk.hex('#ffd700')('\u{1F41D} Swarm engaged') +
3631
+ (swarmReason ? chalk.dim(` — ${swarmReason}`) : ''));
3632
+ const summary = await swarm.run(input);
3464
3633
  activeSwarm = null;
3465
3634
  if (summary) {
3466
3635
  // Swarm completed — render summary
@@ -3469,7 +3638,7 @@ export async function chatCommand(options) {
3469
3638
  }
3470
3639
  else {
3471
3640
  // Swarm decided single task — fall through to normal agent
3472
- await sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = false; }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3641
+ await sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = false; }, effectiveValidation, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3473
3642
  currentStepLabel = label;
3474
3643
  const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3475
3644
  currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
@@ -3488,7 +3657,7 @@ export async function chatCommand(options) {
3488
3657
  // Muting is handled by activity.setMuteCallbacks (mute during LLM stream,
3489
3658
  // unmute during tool execution so user can answer permission prompts).
3490
3659
  isAtPrompt = false;
3491
- }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3660
+ }, effectiveValidation, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3492
3661
  currentStepLabel = label;
3493
3662
  const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3494
3663
  currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
@@ -3503,22 +3672,7 @@ export async function chatCommand(options) {
3503
3672
  while (typeAheadBuffer.length > 0 && !agentController.isAborted) {
3504
3673
  const buffered = typeAheadBuffer.shift();
3505
3674
  if (buffered.trim()) {
3506
- // Process the buffered input as if user just typed it
3507
- sessionBuffer.addUserMessage(buffered.trim());
3508
- checkpointStore.createForChat(buffered.trim(), agentHistory.length);
3509
- roundToolCalls = 0;
3510
- currentStepLabel = '';
3511
- currentStepFile = '';
3512
- agentRunning = true;
3513
- agentController.reset();
3514
- updateStatusBar();
3515
- await sendAgentMessage(buffered.trim(), agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { showPrompt(); }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3516
- currentStepLabel = label;
3517
- const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3518
- currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
3519
- });
3520
- agentRunning = false;
3521
- messages.push({ role: 'user', content: buffered.trim() });
3675
+ await processInput(buffered.trim());
3522
3676
  }
3523
3677
  }
3524
3678
  showPrompt();
@@ -4008,9 +4162,15 @@ function showFullAutonomousWarning() {
4008
4162
  process.stdout.write(d('\u2502 ') + d('ESC = stop agent if needed.') + d(' \u2502') + '\n');
4009
4163
  process.stdout.write(d('\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F') + '\n\n');
4010
4164
  }
4011
- async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx) {
4165
+ async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, resolveSpiralEngine, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx) {
4012
4166
  const parts = input.split(/\s+/);
4013
4167
  let cmd = parts[0].toLowerCase();
4168
+ const ensureSpiralForScope = async (scope = currentBrainScope || 'global') => {
4169
+ if (!resolveSpiralEngine)
4170
+ return spiralEngine;
4171
+ spiralEngine = await resolveSpiralEngine(scope);
4172
+ return spiralEngine;
4173
+ };
4014
4174
  // ─── Chrome-aware selectMenu ───────────────────────────
4015
4175
  // Deactivates Screen during interactive menus to prevent
4016
4176
  // stdout hook interference (menu stacking on Windows Terminal).
@@ -4112,7 +4272,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4112
4272
  rl.resume();
4113
4273
  if (helpIdx >= 0 && helpCmds[helpIdx]) {
4114
4274
  // Execute the selected command
4115
- return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx);
4275
+ return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, resolveSpiralEngine, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx);
4116
4276
  }
4117
4277
  break;
4118
4278
  }
@@ -4211,6 +4371,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4211
4371
  return 'drain'; // Sub-readline may leave phantom line events
4212
4372
  }
4213
4373
  case '/spiral':
4374
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4214
4375
  if (spiralEngine) {
4215
4376
  try {
4216
4377
  const status = spiralEngine.status();
@@ -4221,7 +4382,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4221
4382
  }
4222
4383
  }
4223
4384
  else {
4224
- renderInfo('Spiral engine disabled.');
4385
+ renderInfo(config.spiral.enabled ? 'Spiral engine not available.' : 'Spiral engine disabled.');
4225
4386
  }
4226
4387
  break;
4227
4388
  case '/helix':
@@ -4242,6 +4403,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4242
4403
  renderInfo(chalk.yellow(' Cannot use local brain here — using global'));
4243
4404
  }
4244
4405
  }
4406
+ spiralEngine = await ensureSpiralForScope('project');
4245
4407
  // Auto-start brain visualization
4246
4408
  if (spiralEngine) {
4247
4409
  try {
@@ -4282,6 +4444,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4282
4444
  }
4283
4445
  catch { /* optional */ }
4284
4446
  }
4447
+ spiralEngine = await ensureSpiralForScope('global');
4285
4448
  // Auto-start brain visualization
4286
4449
  if (spiralEngine) {
4287
4450
  try {
@@ -4365,6 +4528,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4365
4528
  }
4366
4529
  // Open 3D visualization (for /brain or /brain view)
4367
4530
  if (!brainArg || brainArg === 'view') {
4531
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4368
4532
  if (spiralEngine) {
4369
4533
  try {
4370
4534
  const { exportBrainData } = await import('../brain/exporter.js');
@@ -4401,6 +4565,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4401
4565
  // Handled directly in chatCommand() for access to inlineProgressActive flag
4402
4566
  break;
4403
4567
  case '/context':
4568
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4404
4569
  if (spiralEngine) {
4405
4570
  const status = spiralEngine.status();
4406
4571
  renderInfo(`Context: ${status.total_nodes} spiral nodes, ${status.total_edges} edges`);
@@ -4423,6 +4588,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4423
4588
  break;
4424
4589
  }
4425
4590
  case '/compact':
4591
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4426
4592
  if (spiralEngine) {
4427
4593
  const result = spiralEngine.evolve();
4428
4594
  renderInfo(`Evolution: ${result.promoted} promoted, ${result.demoted} demoted, ${result.summarized} summarized`);
@@ -4739,6 +4905,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4739
4905
  }
4740
4906
  case '/export': {
4741
4907
  const outputDir = parts[1] || process.cwd();
4908
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4742
4909
  if (spiralEngine) {
4743
4910
  try {
4744
4911
  const { exportToZip } = await import('../brain/archive.js');
@@ -4856,7 +5023,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4856
5023
  break;
4857
5024
  case '/del': {
4858
5025
  // /del task <id> — shorthand for /jarvis delete <id>
4859
- if (!(await gateCheck('jarvis', 'Jarvis AGI')))
5026
+ if (!(await gateCheck('jarvis', 'Jarvis Assistant')))
4860
5027
  break;
4861
5028
  if (!jarvisCtx) {
4862
5029
  renderInfo('Jarvis not available.');
@@ -4883,7 +5050,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4883
5050
  break;
4884
5051
  }
4885
5052
  case '/jarvis': {
4886
- if (!(await gateCheck('jarvis', 'Jarvis AGI')))
5053
+ if (!(await gateCheck('jarvis', 'Jarvis Assistant')))
4887
5054
  break;
4888
5055
  if (!jarvisCtx) {
4889
5056
  renderInfo('Jarvis not available.');
@@ -4914,7 +5081,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4914
5081
  if (!identity.customized) {
4915
5082
  // Deactivate screen so onboarding questions render directly
4916
5083
  // (rl.question output goes to devNull; onboarding uses process.stdout)
4917
- chrome?.deactivate();
5084
+ chrome?.deactivate({ suspend: false });
4918
5085
  rl.pause();
4919
5086
  const onboardResult = await runOnboarding(jarvisCtx.identity, rl, jarvisCtx.getScope());
4920
5087
  rl.resume();
@@ -5577,7 +5744,12 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
5577
5744
  const port = getBrainPort();
5578
5745
  process.stdout.write(` ${theme.success('\u{1F310} Already connected')} on port ${port}\n`);
5579
5746
  }
5580
- else if (spiralEngine) {
5747
+ else {
5748
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'project');
5749
+ if (!spiralEngine) {
5750
+ renderError(config.spiral.enabled ? 'No spiral engine available.' : 'Spiral engine is disabled.');
5751
+ break;
5752
+ }
5581
5753
  const { exportBrainData } = await import('../brain/exporter.js');
5582
5754
  exportBrainData(spiralEngine, 'HelixMind Project', currentBrainScope || 'project');
5583
5755
  const url = await startLiveBrain(spiralEngine, 'HelixMind Project', currentBrainScope || 'project');
@@ -5586,9 +5758,6 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
5586
5758
  process.stdout.write(` ${theme.success('\u{1F310} Brain server started:')} ${url}\n`);
5587
5759
  process.stdout.write(` ${theme.dim('Web dashboard can now connect.')}\n`);
5588
5760
  }
5589
- else {
5590
- renderError('No spiral engine available. Run /helix first.');
5591
- }
5592
5761
  }
5593
5762
  catch (err) {
5594
5763
  renderError(`Failed to start brain server: ${err}`);