helixmind 0.6.1 → 0.6.3

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 (39) hide show
  1. package/dist/cli/agent/loop.d.ts.map +1 -1
  2. package/dist/cli/agent/loop.js +40 -3
  3. package/dist/cli/agent/loop.js.map +1 -1
  4. package/dist/cli/agent/tools/registry.d.ts +2 -0
  5. package/dist/cli/agent/tools/registry.d.ts.map +1 -1
  6. package/dist/cli/agent/tools/registry.js.map +1 -1
  7. package/dist/cli/agent/turn-directives.d.ts +12 -0
  8. package/dist/cli/agent/turn-directives.d.ts.map +1 -0
  9. package/dist/cli/agent/turn-directives.js +89 -0
  10. package/dist/cli/agent/turn-directives.js.map +1 -0
  11. package/dist/cli/brain/generator.d.ts.map +1 -1
  12. package/dist/cli/brain/generator.js +44 -1
  13. package/dist/cli/brain/generator.js.map +1 -1
  14. package/dist/cli/commands/chat.d.ts.map +1 -1
  15. package/dist/cli/commands/chat.js +392 -144
  16. package/dist/cli/commands/chat.js.map +1 -1
  17. package/dist/cli/jarvis/daemon.d.ts +4 -0
  18. package/dist/cli/jarvis/daemon.d.ts.map +1 -1
  19. package/dist/cli/jarvis/daemon.js +22 -0
  20. package/dist/cli/jarvis/daemon.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/jarvis/skill-builder.d.ts +26 -0
  26. package/dist/cli/jarvis/skill-builder.d.ts.map +1 -0
  27. package/dist/cli/jarvis/skill-builder.js +127 -0
  28. package/dist/cli/jarvis/skill-builder.js.map +1 -0
  29. package/dist/cli/ui/activity.d.ts.map +1 -1
  30. package/dist/cli/ui/activity.js +12 -1
  31. package/dist/cli/ui/activity.js.map +1 -1
  32. package/dist/cli/ui/command-suggest.d.ts.map +1 -1
  33. package/dist/cli/ui/command-suggest.js +2 -0
  34. package/dist/cli/ui/command-suggest.js.map +1 -1
  35. package/dist/cli/validation/policy.d.ts +13 -0
  36. package/dist/cli/validation/policy.d.ts.map +1 -0
  37. package/dist/cli/validation/policy.js +34 -0
  38. package/dist/cli/validation/policy.js.map +1 -0
  39. 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';
@@ -48,11 +49,13 @@ import { WorldModelManager } from '../jarvis/world-model.js';
48
49
  import { AutonomyManager } from '../jarvis/autonomy.js';
49
50
  import { NotificationManager } from '../jarvis/notifications.js';
50
51
  import { SkillManager } from '../jarvis/skills.js';
52
+ import { SkillScorer } from '../jarvis/skill-scoring.js';
51
53
  import { JarvisTelegramBot, parseTelegramCommand } from '../jarvis/telegram-bot.js';
52
54
  import { LearningJournal } from '../jarvis/learning.js';
53
55
  import { SentimentAnalyzer } from '../jarvis/sentiment.js';
54
56
  import { acquireJarvisSlot, releaseJarvisSlot } from '../jarvis/instance-lock.js';
55
57
  import { buildRuntimeContext } from '../jarvis/runtime-context.js';
58
+ import { buildSkillBuildSpecFromProposal, buildSkillBuildTask, detectSkillBuildFromFailure } from '../jarvis/skill-builder.js';
56
59
  import { BrainInstanceManager } from '../brain/instance-manager.js';
57
60
  import { BrowserController } from '../browser/controller.js';
58
61
  import { VisionProcessor } from '../browser/vision.js';
@@ -60,6 +63,7 @@ import { classifyTask } from '../validation/classifier.js';
60
63
  import { generateCriteria } from '../validation/criteria.js';
61
64
  import { validationLoop } from '../validation/autofix.js';
62
65
  import { createValidationProvider } from '../validation/model.js';
66
+ import { resolveValidationDecision } from '../validation/policy.js';
63
67
  import { renderValidationSummary, renderValidationStart, renderClassification } from '../validation/reporter.js';
64
68
  import { storeValidationResult, getValidationStats, renderValidationStats } from '../validation/stats.js';
65
69
  import { isFeatureAvailable, getJarvisLimitsForPlan, getBrainLimitsForPlan, isLoggedIn } from '../auth/feature-gate.js';
@@ -80,6 +84,8 @@ const HELP_CATEGORIES = [
80
84
  { cmd: '/clear', label: '/clear', description: 'Clear conversation history' },
81
85
  { cmd: '/model', label: '/model', description: 'Switch LLM model' },
82
86
  { cmd: '/keys', label: '/keys', description: 'Manage API keys' },
87
+ { cmd: '/fast [prompt]', label: '/fast', description: 'Low-latency turn (skip validation, no auto-swarm)' },
88
+ { cmd: '/swarm [prompt]', label: '/swarm', description: 'Force swarm decomposition for one turn' },
83
89
  { cmd: '/yolo', label: '/yolo', description: 'Toggle YOLO mode' },
84
90
  { cmd: '/skip-permissions', label: '/skip-permissions', description: 'Toggle skip-permissions' },
85
91
  { cmd: '/plan', label: '/plan', description: 'Toggle plan mode (read-only → plan → execute)' },
@@ -225,10 +231,12 @@ const HELP_TEXT = `
225
231
  ${chalk.hex('#00d4ff').bold(' Chat & Interaction')}
226
232
  ${theme.primary('/help'.padEnd(22))} ${theme.dim('Show this help')}
227
233
  ${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)')}
234
+ ${theme.primary('/model [name]'.padEnd(22))} ${theme.dim('Switch model (interactive or direct: /model gpt-4o)')}
235
+ ${theme.primary('/keys'.padEnd(22))} ${theme.dim('Add/remove/update API keys')}
236
+ ${theme.primary('/fast [prompt]'.padEnd(22))} ${theme.dim('One fast turn: skip validation and auto-swarm')}
237
+ ${theme.primary('/swarm [prompt]'.padEnd(22))} ${theme.dim('Force multi-agent swarm for one request')}
238
+ ${theme.primary('/yolo [on|off]'.padEnd(22))} ${theme.dim('Toggle YOLO mode — auto-approve ALL operations')}
239
+ ${theme.primary('/skip-permissions'.padEnd(22))} ${theme.dim('Toggle skip-permissions (auto-approve safe ops)')}
232
240
 
233
241
  ${chalk.hex('#00ff88').bold(' Spiral Memory')}
234
242
  ${theme.primary('/spiral'.padEnd(22))} ${theme.dim('Show spiral status (nodes per level)')}
@@ -407,15 +415,18 @@ export async function chatCommand(options) {
407
415
  let jarvisAutonomy;
408
416
  let jarvisNotifications;
409
417
  let jarvisSkills;
418
+ let jarvisSkillScorer;
410
419
  let jarvisSentiment;
411
420
  let jarvisTelegramBot = null;
412
421
  let jarvisLearning;
413
422
  let jarvisScope;
423
+ let jarvisRuntimeReady = false;
424
+ let pendingJarvisQueueBinder = null;
414
425
  let jarvisDaemonSession = null;
415
426
  let jarvisPaused = false;
416
427
  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();
428
+ // Browser controller stays lazy so plain chat startup avoids browser setup work.
429
+ let browserController;
419
430
  let visionProcessor;
420
431
  // Terminal + Screen (replaces BottomChrome with framed input area)
421
432
  const terminal = new Terminal();
@@ -428,6 +439,16 @@ export async function chatCommand(options) {
428
439
  const chrome = screen;
429
440
  // Activity indicator (renders on chrome row 0 during agent work)
430
441
  const activity = new ActivityIndicator(chrome);
442
+ const chromeRowCache = new Map();
443
+ function invalidateChromeRows() {
444
+ chromeRowCache.clear();
445
+ }
446
+ function setChromeRow(row, content, force = false) {
447
+ if (!force && chromeRowCache.get(row) === content)
448
+ return;
449
+ chromeRowCache.set(row, content);
450
+ chrome.setRow(row, content);
451
+ }
431
452
  // Agent controller for pause/resume
432
453
  const agentController = new AgentController();
433
454
  let agentRunning = false;
@@ -557,28 +578,54 @@ export async function chatCommand(options) {
557
578
  }
558
579
  // Jarvis scope follows brain scope — init after brainScope detection
559
580
  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
- });
581
+ function initializeJarvisRuntime(scope = jarvisScope) {
582
+ const root = resolveJarvisRoot(scope);
583
+ jarvisScope = scope;
584
+ jarvisQueue = new JarvisQueue(root);
585
+ jarvisIdentity = new JarvisIdentityManager(root);
586
+ jarvisProposals = new ProposalJournal(root);
587
+ jarvisScheduler = new JarvisScheduler(root);
588
+ jarvisTriggers = new TriggerManager(root);
589
+ jarvisWorldModel = new WorldModelManager(root);
590
+ jarvisAutonomy = new AutonomyManager(jarvisIdentity.getIdentity().autonomyLevel);
591
+ jarvisNotifications = new NotificationManager(root);
592
+ jarvisSkills = new SkillManager(root);
593
+ jarvisSkillScorer = new SkillScorer(root);
594
+ jarvisSkills.syncRegistry();
595
+ jarvisLearning = new LearningJournal(root);
596
+ jarvisSentiment = new SentimentAnalyzer(root, {
597
+ onShift: (from, to, frustrationLevel) => {
598
+ jarvisIdentity.recordEvent({ type: 'sentiment_shift', from, to, frustrationLevel });
599
+ import('../brain/generator.js').then(mod => {
600
+ if (mod.isBrainServerRunning?.()) {
601
+ mod.pushNeuronFired?.('yellow', '#ff6b6b', 'sentiment');
602
+ }
603
+ }).catch(() => { });
604
+ },
605
+ });
606
+ jarvisRuntimeReady = true;
607
+ pendingJarvisQueueBinder?.();
608
+ }
609
+ function ensureJarvisRuntime(scope = jarvisScope) {
610
+ if (jarvisRuntimeReady && jarvisScope === scope)
611
+ return;
612
+ initializeJarvisRuntime(scope);
613
+ }
614
+ function warmJarvisRuntime(scope = jarvisScope) {
615
+ if (jarvisRuntimeReady && jarvisScope === scope)
616
+ return;
617
+ setTimeout(() => {
618
+ try {
619
+ ensureJarvisRuntime(scope);
620
+ }
621
+ catch {
622
+ // Best effort: Jarvis can still initialize on first real use.
623
+ }
624
+ }, 0);
625
+ }
581
626
  let spiralEngine = null;
627
+ let spiralInitPromise = null;
628
+ let spiralInitScope = null;
582
629
  async function initSpiralEngine(scope) {
583
630
  try {
584
631
  const { SpiralEngine } = await import('../../spiral/engine.js');
@@ -593,8 +640,58 @@ export async function chatCommand(options) {
593
640
  return null;
594
641
  }
595
642
  }
596
- if (config.spiral.enabled) {
597
- spiralEngine = await initSpiralEngine(brainScope);
643
+ async function ensureSpiralEngine(scope = brainScope) {
644
+ if (!config.spiral.enabled)
645
+ return null;
646
+ if (spiralEngine && spiralInitScope === scope)
647
+ return spiralEngine;
648
+ if (spiralInitPromise && spiralInitScope === scope)
649
+ return spiralInitPromise;
650
+ spiralInitScope = scope;
651
+ spiralInitPromise = initSpiralEngine(scope).then(async (engine) => {
652
+ spiralEngine = engine;
653
+ spiralInitPromise = null;
654
+ if (engine && brainUrl) {
655
+ try {
656
+ const { startLiveBrain } = await import('../brain/generator.js');
657
+ await startLiveBrain(engine, project.name || 'HelixMind', scope);
658
+ }
659
+ catch {
660
+ // Dashboard sync is best effort.
661
+ }
662
+ }
663
+ return engine;
664
+ }).catch((err) => {
665
+ spiralInitPromise = null;
666
+ spiralInitScope = null;
667
+ throw err;
668
+ });
669
+ return spiralInitPromise;
670
+ }
671
+ async function getSpiralEngine(scope = brainScope) {
672
+ if (!config.spiral.enabled)
673
+ return null;
674
+ if (spiralEngine && spiralInitScope === scope)
675
+ return spiralEngine;
676
+ const engine = await ensureSpiralEngine(scope);
677
+ spiralEngine = engine;
678
+ return engine;
679
+ }
680
+ function warmSpiralEngine(scope = brainScope) {
681
+ if (!config.spiral.enabled)
682
+ return;
683
+ void ensureSpiralEngine(scope).catch(() => { });
684
+ }
685
+ function resetSpiralEngine(scope) {
686
+ if (spiralEngine) {
687
+ try {
688
+ spiralEngine.close();
689
+ }
690
+ catch { /* best effort */ }
691
+ }
692
+ spiralEngine = null;
693
+ spiralInitPromise = null;
694
+ spiralInitScope = scope ?? null;
598
695
  }
599
696
  // Register current brain in manager (so limits track it)
600
697
  if (brainManager) {
@@ -617,6 +714,7 @@ export async function chatCommand(options) {
617
714
  }
618
715
  // Build SkillContext for skill activation
619
716
  function buildSkillContext() {
717
+ ensureJarvisRuntime();
620
718
  return {
621
719
  registerTool: (name, def, handler) => {
622
720
  // Skills register tools into the skill manager's registry
@@ -626,10 +724,11 @@ export async function chatCommand(options) {
626
724
  unregisterTool: (_name) => { },
627
725
  addTask: (title, desc, priority) => jarvisQueue.addTask(title, desc, { priority }),
628
726
  querySpiral: async (query) => {
629
- if (!spiralEngine)
727
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
728
+ if (!engine)
630
729
  return '';
631
730
  try {
632
- const results = await spiralEngine.query(query, { maxResults: 10 });
731
+ const results = await engine.query(query, { maxResults: 10 });
633
732
  return results.level_1.concat(results.level_2, results.level_3)
634
733
  .map((n) => `[${n.type}] ${n.content}`)
635
734
  .join('\n')
@@ -640,10 +739,11 @@ export async function chatCommand(options) {
640
739
  }
641
740
  },
642
741
  storeInSpiral: async (content, type, tags) => {
643
- if (!spiralEngine)
742
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
743
+ if (!engine)
644
744
  return;
645
745
  try {
646
- await spiralEngine.add(content, { type, tags });
746
+ await engine.add(content, { type, tags });
647
747
  }
648
748
  catch { }
649
749
  },
@@ -655,8 +755,64 @@ export async function chatCommand(options) {
655
755
  log: (msg) => { renderInfo(chalk.dim(`[skill] ${msg}`)); },
656
756
  };
657
757
  }
758
+ function findOpenSkillBuildTask(skillName) {
759
+ ensureJarvisRuntime();
760
+ return jarvisQueue.getAllTasks().find((task) => task.status !== 'completed' &&
761
+ task.status !== 'failed' &&
762
+ task.tags?.includes('skill_build') &&
763
+ task.tags?.includes(`skill:${skillName}`));
764
+ }
765
+ function enqueueProposalTask(proposal) {
766
+ ensureJarvisRuntime();
767
+ if (proposal.category === 'skill_creation' || proposal.category === 'skill_update') {
768
+ const spec = buildSkillBuildSpecFromProposal(proposal);
769
+ const existing = findOpenSkillBuildTask(spec.skillName);
770
+ if (existing)
771
+ return existing;
772
+ const buildTask = buildSkillBuildTask(spec);
773
+ return jarvisQueue.addTask(buildTask.title, buildTask.description, {
774
+ priority: buildTask.priority,
775
+ tags: buildTask.tags,
776
+ });
777
+ }
778
+ return jarvisQueue.addTask(proposal.title, proposal.description, {
779
+ priority: proposal.impact === 'high' ? 'high' : 'medium',
780
+ });
781
+ }
782
+ function queueSkillBuildFromFailure(task, failure) {
783
+ ensureJarvisRuntime();
784
+ const spec = detectSkillBuildFromFailure({
785
+ taskTitle: task.title,
786
+ taskDescription: task.description,
787
+ failure,
788
+ existingScores: jarvisSkillScorer.scoreSkills(task.description || task.title, 'skill_gap', jarvisSkills.listSkills()),
789
+ existingSkillNames: jarvisSkills.listSkills().map((skill) => skill.manifest.name),
790
+ });
791
+ if (!spec)
792
+ return null;
793
+ const existing = findOpenSkillBuildTask(spec.skillName);
794
+ if (existing)
795
+ return existing;
796
+ const buildTask = buildSkillBuildTask(spec);
797
+ return jarvisQueue.addTask(buildTask.title, buildTask.description, {
798
+ priority: buildTask.priority,
799
+ tags: buildTask.tags.concat('self_improvement'),
800
+ });
801
+ }
802
+ async function activateBuiltSkill(task, skillName) {
803
+ ensureJarvisRuntime();
804
+ jarvisSkills.syncRegistry();
805
+ const result = await jarvisSkills.activateSkill(skillName, buildSkillContext());
806
+ if (result.success) {
807
+ renderInfo(chalk.green(`Activated skill "${skillName}" after task #${task.id}.`));
808
+ }
809
+ else {
810
+ renderInfo(chalk.yellow(`Built skill "${skillName}" but activation failed: ${result.error || 'unknown error'}`));
811
+ }
812
+ }
658
813
  // Start Telegram bot if configured (called when daemon starts)
659
814
  function startTelegramBot() {
815
+ ensureJarvisRuntime();
660
816
  if (jarvisTelegramBot?.isRunning)
661
817
  return;
662
818
  const teleConfig = jarvisNotifications.getConfig().targets.find(t => t.channel === 'telegram');
@@ -699,9 +855,7 @@ export async function chatCommand(options) {
699
855
  const proposal = jarvisProposals.approve(id);
700
856
  if (proposal) {
701
857
  jarvisIdentity.recordEvent({ type: 'proposal_approved', proposalId: id });
702
- const task = jarvisQueue.addTask(proposal.title, proposal.description, {
703
- priority: proposal.impact === 'high' ? 'high' : 'medium',
704
- });
858
+ const task = enqueueProposalTask(proposal);
705
859
  proposal.convertedTaskId = task.id;
706
860
  reply(`\u{1F7E2} Proposal #${id} approved \u2192 Task #${task.id} queued`);
707
861
  }
@@ -760,7 +914,7 @@ export async function chatCommand(options) {
760
914
  const proposal = jarvisProposals.approve(id);
761
915
  if (proposal) {
762
916
  jarvisIdentity.recordEvent({ type: 'proposal_approved', proposalId: id });
763
- const task = jarvisQueue.addTask(proposal.title, proposal.description, { priority: 'medium' });
917
+ const task = enqueueProposalTask(proposal);
764
918
  proposal.convertedTaskId = task.id;
765
919
  jarvisTelegramBot?.send(`\u2705 Approved #${id} \u2192 Task #${task.id}`, { chatId });
766
920
  }
@@ -777,6 +931,7 @@ export async function chatCommand(options) {
777
931
  }
778
932
  // Build ThinkingCallbacks for Jarvis AGI thinking loop
779
933
  function buildThinkingCallbacks(bgSession) {
934
+ ensureJarvisRuntime();
780
935
  return {
781
936
  sendMessage: async (prompt) => {
782
937
  bgSession.controller.reset();
@@ -789,10 +944,11 @@ export async function chatCommand(options) {
789
944
  isAborted: () => bgSession.controller.isAborted,
790
945
  isPaused: () => jarvisPaused,
791
946
  querySpiral: async (query, maxTokens) => {
792
- if (!spiralEngine)
947
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
948
+ if (!engine)
793
949
  return '';
794
950
  try {
795
- const results = await spiralEngine.query(query, { maxResults: maxTokens ?? 50 });
951
+ const results = await engine.query(query, { maxResults: maxTokens ?? 50 });
796
952
  return results.level_1.concat(results.level_2, results.level_3)
797
953
  .map((n) => `[${n.type}] ${n.content}`)
798
954
  .join('\n')
@@ -803,15 +959,29 @@ export async function chatCommand(options) {
803
959
  }
804
960
  },
805
961
  storeInSpiral: async (content, type, tags) => {
806
- if (!spiralEngine)
962
+ const engine = spiralEngine ?? await ensureSpiralEngine(brainScope);
963
+ if (!engine)
807
964
  return;
808
965
  try {
809
- await spiralEngine.add(content, { type: type, tags });
966
+ await engine.add(content, { type: type, tags });
810
967
  }
811
968
  catch { }
812
969
  },
813
970
  createProposal: (title, desc, rationale, opts) => {
814
- return jarvisProposals.create(title, desc, rationale, opts);
971
+ const proposal = jarvisProposals.create(title, desc, rationale, opts);
972
+ if (proposal.category === 'skill_creation' &&
973
+ proposal.source.startsWith('thinking') &&
974
+ jarvisAutonomy.getLevel() >= 4) {
975
+ const approved = jarvisProposals.approve(proposal.id);
976
+ if (approved) {
977
+ jarvisIdentity.recordEvent({ type: 'proposal_approved', proposalId: approved.id });
978
+ const task = enqueueProposalTask(approved);
979
+ approved.convertedTaskId = task.id;
980
+ renderInfo(chalk.dim(`Auto-approved skill proposal #${approved.id} -> Task #${task.id}`));
981
+ return approved;
982
+ }
983
+ }
984
+ return proposal;
815
985
  },
816
986
  wouldLikelyBeDenied: (cat, files) => {
817
987
  return jarvisProposals.wouldLikelyBeDenied(cat, files);
@@ -852,6 +1022,10 @@ export async function chatCommand(options) {
852
1022
  topCategories,
853
1023
  };
854
1024
  },
1025
+ getSkillScores: (taskDesc) => {
1026
+ return jarvisSkillScorer.scoreSkills(taskDesc, 'skill_gap', jarvisSkills.listSkills())
1027
+ .map((score) => ({ skillName: score.skillName, totalScore: score.totalScore }));
1028
+ },
855
1029
  };
856
1030
  }
857
1031
  // Create session start checkpoint
@@ -1210,7 +1384,8 @@ export async function chatCommand(options) {
1210
1384
  bgSession.controller.reset();
1211
1385
  const rth = { text: '' };
1212
1386
  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 });
1387
+ const activeSpiral = await getSpiralEngine(brainScope);
1388
+ 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 }, undefined, undefined, undefined, undefined, null, undefined, jarvisLearning, undefined, jarvisSkills);
1214
1389
  bgSession.buffer.onSummary = undefined;
1215
1390
  return rth.text;
1216
1391
  },
@@ -1306,6 +1481,7 @@ export async function chatCommand(options) {
1306
1481
  return bugJournal.delete(id);
1307
1482
  },
1308
1483
  startJarvis: () => {
1484
+ ensureJarvisRuntime();
1309
1485
  if (jarvisDaemonSession && jarvisDaemonSession.status === 'running')
1310
1486
  return jarvisDaemonSession.id;
1311
1487
  // Enforce cross-process Jarvis instance limit
@@ -1362,9 +1538,12 @@ export async function chatCommand(options) {
1362
1538
  pushSessionUpdate(serializeSession(bgSession));
1363
1539
  },
1364
1540
  updateStatus: () => updateStatusBar(),
1365
- storeInSpiral: spiralEngine ? async (content, type, tags) => {
1541
+ storeInSpiral: config.spiral.enabled ? async (content, type, tags) => {
1542
+ const activeSpiral = await getSpiralEngine(brainScope);
1543
+ if (!activeSpiral)
1544
+ return;
1366
1545
  try {
1367
- await spiralEngine.add(content, { type: type, tags });
1546
+ await activeSpiral.add(content, { type: type, tags });
1368
1547
  }
1369
1548
  catch { }
1370
1549
  } : undefined,
@@ -1375,6 +1554,8 @@ export async function chatCommand(options) {
1375
1554
  jarvisLearning.recordLearning(error, solution, category, context, ['jarvis_daemon']);
1376
1555
  },
1377
1556
  thinkingCallbacks: buildThinkingCallbacks(bgSession),
1557
+ queueSkillBuildTask: queueSkillBuildFromFailure,
1558
+ onSkillBuildComplete: activateBuiltSkill,
1378
1559
  });
1379
1560
  }
1380
1561
  catch (err) {
@@ -1398,6 +1579,7 @@ export async function chatCommand(options) {
1398
1579
  return true;
1399
1580
  },
1400
1581
  pauseJarvis: () => {
1582
+ ensureJarvisRuntime();
1401
1583
  if (!jarvisDaemonSession || jarvisPaused)
1402
1584
  return false;
1403
1585
  jarvisPaused = true;
@@ -1405,6 +1587,7 @@ export async function chatCommand(options) {
1405
1587
  return true;
1406
1588
  },
1407
1589
  resumeJarvis: () => {
1590
+ ensureJarvisRuntime();
1408
1591
  if (!jarvisDaemonSession || !jarvisPaused)
1409
1592
  return false;
1410
1593
  jarvisPaused = false;
@@ -1412,24 +1595,36 @@ export async function chatCommand(options) {
1412
1595
  return true;
1413
1596
  },
1414
1597
  addJarvisTask: (title, description, opts) => {
1598
+ ensureJarvisRuntime();
1415
1599
  const task = jarvisQueue.addTask(title, description, opts);
1416
1600
  return serializeJarvisTask(task);
1417
1601
  },
1418
- listJarvisTasks: () => jarvisQueue.getAllTasks().map(serializeJarvisTask),
1602
+ listJarvisTasks: () => {
1603
+ ensureJarvisRuntime();
1604
+ return jarvisQueue.getAllTasks().map(serializeJarvisTask);
1605
+ },
1419
1606
  deleteJarvisTask: (taskId) => {
1607
+ ensureJarvisRuntime();
1420
1608
  return jarvisQueue.removeTask(taskId);
1421
1609
  },
1422
- getJarvisStatus: () => ({
1423
- ...jarvisQueue.getStatus(),
1424
- scope: jarvisScope === 'project' ? 'local' : 'global',
1425
- jarvisName: jarvisIdentity.getIdentity().name,
1426
- }),
1427
- clearJarvisCompleted: () => jarvisQueue.clearCompleted(),
1610
+ getJarvisStatus: () => {
1611
+ ensureJarvisRuntime();
1612
+ return {
1613
+ ...jarvisQueue.getStatus(),
1614
+ scope: jarvisScope === 'project' ? 'local' : 'global',
1615
+ jarvisName: jarvisIdentity.getIdentity().name,
1616
+ };
1617
+ },
1618
+ clearJarvisCompleted: () => {
1619
+ ensureJarvisRuntime();
1620
+ return jarvisQueue.clearCompleted();
1621
+ },
1428
1622
  // Jarvis AGI handlers (stubs — wired when AGI modules are initialized)
1429
1623
  listProposals: () => [],
1430
1624
  approveProposal: () => false,
1431
1625
  denyProposal: () => false,
1432
1626
  setAutonomyLevel: (level) => {
1627
+ ensureJarvisRuntime();
1433
1628
  if (level < 0 || level > 5)
1434
1629
  return false;
1435
1630
  jarvisAutonomy.setLevel(level);
@@ -1776,19 +1971,24 @@ export async function chatCommand(options) {
1776
1971
  pushBugUpdated(bugInfo);
1777
1972
  }
1778
1973
  });
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
- });
1974
+ // Wire Jarvis queue change events to brain server when Jarvis runtime exists.
1975
+ pendingJarvisQueueBinder = () => {
1976
+ if (!jarvisRuntimeReady)
1977
+ return;
1978
+ jarvisQueue.setOnChange((event, task) => {
1979
+ const info = serializeJarvisTask(task);
1980
+ if (event === 'task_created') {
1981
+ pushJarvisTaskCreated(info);
1982
+ }
1983
+ else if (event === 'task_removed') {
1984
+ pushJarvisTaskRemoved(task.id);
1985
+ }
1986
+ else {
1987
+ pushJarvisTaskUpdated(info);
1988
+ }
1989
+ });
1990
+ };
1991
+ pendingJarvisQueueBinder();
1792
1992
  // Wire browser screenshots to brain server
1793
1993
  pushScreenshotToBrainFn = (info) => {
1794
1994
  pushBrowserScreenshot({
@@ -1888,6 +2088,34 @@ export async function chatCommand(options) {
1888
2088
  hints.push(chalk.dim('esc to interrupt'));
1889
2089
  return hints.join(chalk.dim(' \u00B7 '));
1890
2090
  }
2091
+ function renderExecutionIntent(directives, validationDecision, swarmHeuristic, usingSwarm) {
2092
+ const parts = [];
2093
+ if (usingSwarm) {
2094
+ const reason = swarmHeuristic.reasons.slice(0, 2).join(', ');
2095
+ parts.push(chalk.hex('#ffd700')('swarm'));
2096
+ if (reason)
2097
+ parts.push(chalk.dim(reason));
2098
+ }
2099
+ else {
2100
+ parts.push(chalk.hex('#00d4ff')('single-agent'));
2101
+ if (directives.skipSwarm) {
2102
+ parts.push(chalk.dim('swarm skipped'));
2103
+ }
2104
+ else if (swarmHeuristic.shouldOrchestrate) {
2105
+ parts.push(chalk.dim('swarm available but not used'));
2106
+ }
2107
+ }
2108
+ if (validationDecision.enabled) {
2109
+ parts.push(chalk.green('validation on'));
2110
+ }
2111
+ else {
2112
+ parts.push(chalk.yellow(`validation off (${validationDecision.reason})`));
2113
+ }
2114
+ if (directives.fastMode) {
2115
+ parts.push(chalk.hex('#ff6600')('fast lane'));
2116
+ }
2117
+ renderInfo(chalk.dim('Intent: ') + parts.join(chalk.dim(' · ')));
2118
+ }
1891
2119
  /**
1892
2120
  * Show the full prompt area using sticky bottom chrome (4 rows):
1893
2121
  * ┌──────────────────────────────────────┐ ← chrome row 0 (N-3): top border / activity indicator
@@ -1912,9 +2140,9 @@ export async function chatCommand(options) {
1912
2140
  }
1913
2141
  // Now set chrome content and draw (screen must be active for these to render)
1914
2142
  screen.drawFrameBottom();
1915
- chrome.setRow(1, hintLine);
1916
- chrome.setRow(2, ' ' + statusLine1);
1917
- chrome.setRow(3, ' ' + statusLine2);
2143
+ setChromeRow(1, hintLine, true);
2144
+ setChromeRow(2, ' ' + statusLine1, true);
2145
+ setChromeRow(3, ' ' + statusLine2, true);
1918
2146
  isAtPrompt = true;
1919
2147
  // Suppress stdout hook redraws while InputManager manages the cursor
1920
2148
  screen.inputActive = true;
@@ -2008,6 +2236,7 @@ export async function chatCommand(options) {
2008
2236
  // Update screen and activity on terminal resize
2009
2237
  process.stdout.on('resize', () => {
2010
2238
  closeSuggestionPanel(); // Close panel on resize (reopens on next keypress)
2239
+ invalidateChromeRows();
2011
2240
  screen.handleResize();
2012
2241
  activity.handleResize();
2013
2242
  if (isAtPrompt) {
@@ -2039,9 +2268,11 @@ export async function chatCommand(options) {
2039
2268
  isAtPrompt = active;
2040
2269
  // Deactivate/activate bottom chrome so permission select menu renders cleanly
2041
2270
  if (active) {
2271
+ invalidateChromeRows();
2042
2272
  chrome.deactivate();
2043
2273
  }
2044
2274
  else {
2275
+ invalidateChromeRows();
2045
2276
  chrome.activate();
2046
2277
  }
2047
2278
  });
@@ -2065,7 +2296,7 @@ export async function chatCommand(options) {
2065
2296
  if (isAtPrompt) {
2066
2297
  const hintLine = buildHintLine();
2067
2298
  activity.setRestoreContent(hintLine);
2068
- chrome.setRow(1, hintLine);
2299
+ invalidateChromeRows();
2069
2300
  updateStatusBar();
2070
2301
  }
2071
2302
  });
@@ -2234,12 +2465,7 @@ export async function chatCommand(options) {
2234
2465
  if (newScope === brainScope)
2235
2466
  return;
2236
2467
  try {
2237
- if (spiralEngine) {
2238
- try {
2239
- spiralEngine.close();
2240
- }
2241
- catch { /* best effort */ }
2242
- }
2468
+ resetSpiralEngine();
2243
2469
  brainScope = newScope;
2244
2470
  if (newScope === 'project') {
2245
2471
  const { mkdirSync, existsSync } = await import('node:fs');
@@ -2258,7 +2484,7 @@ export async function chatCommand(options) {
2258
2484
  }
2259
2485
  }
2260
2486
  }
2261
- spiralEngine = await initSpiralEngine(newScope);
2487
+ spiralEngine = await ensureSpiralEngine(newScope);
2262
2488
  const { exportBrainData } = await import('../brain/exporter.js');
2263
2489
  const { startLiveBrain } = await import('../brain/generator.js');
2264
2490
  await startLiveBrain(spiralEngine, project.name || 'HelixMind', newScope);
@@ -2583,6 +2809,7 @@ export async function chatCommand(options) {
2583
2809
  function getJarvisContextForPrompt() {
2584
2810
  if (!jarvisDaemonSession || jarvisDaemonSession.status !== 'running')
2585
2811
  return null;
2812
+ ensureJarvisRuntime();
2586
2813
  const sentimentGuidance = jarvisSentiment.getResponseGuidance();
2587
2814
  const identityPrompt = jarvisIdentity.getIdentityPrompt(jarvisSkills.getSkillsPrompt() ?? undefined, sentimentGuidance || undefined);
2588
2815
  const runtimeContext = buildRuntimeContext({
@@ -2623,9 +2850,9 @@ export async function chatCommand(options) {
2623
2850
  line1 = `${tabBar} ${statusLine1}`;
2624
2851
  }
2625
2852
  // 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 || ''));
2853
+ setChromeRow(1, buildHintLine());
2854
+ setChromeRow(2, ' ' + line1);
2855
+ setChromeRow(3, ' ' + (statusLine2 || ''));
2629
2856
  }
2630
2857
  /** Push session findings to brain visualization */
2631
2858
  function pushFindingsToBrain(session) {
@@ -2688,6 +2915,9 @@ export async function chatCommand(options) {
2688
2915
  // Write the collected startup banner into the scroll region.
2689
2916
  // Content flows bottom-up within the region, sitting just above the input frame.
2690
2917
  screen.writeOutput(startupBannerParts.join(''));
2918
+ // Warm optional heavy subsystems after the prompt is already visible.
2919
+ warmJarvisRuntime(jarvisScope);
2920
+ warmSpiralEngine(brainScope);
2691
2921
  // Footer timer — redraws statusbar on chrome rows 2+3 during agent work.
2692
2922
  // Skipped when:
2693
2923
  // - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
@@ -2701,6 +2931,7 @@ export async function chatCommand(options) {
2701
2931
  footerTimer.unref();
2702
2932
  /** Process a complete input (single line or assembled paste block) */
2703
2933
  async function processInput(input, displayText) {
2934
+ const rawInput = input;
2704
2935
  // Clear input frame FIRST — before any renderInfo/process.stdout.write.
2705
2936
  // This ensures the cursor moves out of the input area so all output
2706
2937
  // (slash command results, "daemon started", etc.) goes to the scroll region.
@@ -2711,12 +2942,14 @@ export async function chatCommand(options) {
2711
2942
  promptHistory.add(input, process.cwd()).catch(() => { });
2712
2943
  // Handle /feed directly here (needs access to inlineProgressActive flag)
2713
2944
  if (input.startsWith('/feed')) {
2714
- if (spiralEngine) {
2945
+ const activeSpiral = spiralEngine ?? await ensureSpiralEngine(brainScope);
2946
+ if (activeSpiral) {
2947
+ spiralEngine = activeSpiral;
2715
2948
  const feedPath = input.split(/\s+/)[1];
2716
2949
  const rootDir = process.cwd();
2717
2950
  renderInfo('\u{1F300} Feeding project...\n');
2718
2951
  try {
2719
- const result = await runFeedPipeline(rootDir, spiralEngine, {
2952
+ const result = await runFeedPipeline(rootDir, activeSpiral, {
2720
2953
  targetPath: feedPath,
2721
2954
  onProgress: wrappedFeedProgress,
2722
2955
  });
@@ -2778,8 +3011,27 @@ export async function chatCommand(options) {
2778
3011
  showPrompt();
2779
3012
  return;
2780
3013
  }
3014
+ const turnDirectives = parseTurnDirectives(rawInput);
3015
+ input = turnDirectives.input;
3016
+ const renderedInput = displayText ?? turnDirectives.displayInput;
3017
+ if (!input) {
3018
+ if (turnDirectives.fastMode) {
3019
+ renderInfo(chalk.dim('Usage: /fast <prompt>'));
3020
+ }
3021
+ else if (turnDirectives.forceSwarm) {
3022
+ renderInfo(chalk.dim('Usage: /swarm <prompt>'));
3023
+ }
3024
+ else if (turnDirectives.strippedFlags.length > 0) {
3025
+ renderInfo(chalk.dim('Execution flags were provided without a prompt.'));
3026
+ }
3027
+ showPrompt();
3028
+ return;
3029
+ }
2781
3030
  // Handle slash commands
2782
- if (input.startsWith('/')) {
3031
+ if (input.startsWith('/') && !turnDirectives.forceSwarm) {
3032
+ if (/^\/(?:jarvis|del)\b/i.test(input)) {
3033
+ ensureJarvisRuntime();
3034
+ }
2783
3035
  const handled = await handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, { input: sessionTokensInput, output: sessionTokensOutput }, sessionToolCalls, (newProvider) => {
2784
3036
  provider = newProvider;
2785
3037
  config = store.getAll();
@@ -2788,12 +3040,7 @@ export async function chatCommand(options) {
2788
3040
  typeAheadBuffer.length = 0;
2789
3041
  }, async (newScope) => {
2790
3042
  // Switch brain scope
2791
- if (spiralEngine) {
2792
- try {
2793
- spiralEngine.close();
2794
- }
2795
- catch { /* best effort */ }
2796
- }
3043
+ resetSpiralEngine();
2797
3044
  brainScope = newScope;
2798
3045
  // Create .helixmind/ dir if switching to project and it doesn't exist
2799
3046
  if (newScope === 'project') {
@@ -2817,8 +3064,8 @@ export async function chatCommand(options) {
2817
3064
  }
2818
3065
  }
2819
3066
  }
2820
- spiralEngine = await initSpiralEngine(newScope);
2821
- }, brainScope, async (action, goal) => {
3067
+ spiralEngine = await ensureSpiralEngine(newScope);
3068
+ }, brainScope, (scope) => getSpiralEngine(scope ?? brainScope), async (action, goal) => {
2822
3069
  if (action === 'stop') {
2823
3070
  // Stop all background sessions + autonomous mode
2824
3071
  const running = sessionMgr.running;
@@ -3075,23 +3322,7 @@ export async function chatCommand(options) {
3075
3322
  sentiment: jarvisSentiment,
3076
3323
  getScope: () => jarvisScope,
3077
3324
  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);
3325
+ initializeJarvisRuntime(scope);
3095
3326
  },
3096
3327
  getSession: () => jarvisDaemonSession,
3097
3328
  setSession: (s) => { jarvisDaemonSession = s; },
@@ -3106,6 +3337,7 @@ export async function chatCommand(options) {
3106
3337
  startTelegramBot,
3107
3338
  learning: jarvisLearning,
3108
3339
  startDaemon: () => {
3340
+ ensureJarvisRuntime();
3109
3341
  const jName = jarvisIdentity.getIdentity().name;
3110
3342
  const bgSession = sessionMgr.create(jName, '\u{1F916}', agentHistory);
3111
3343
  bgSession.start();
@@ -3149,7 +3381,8 @@ export async function chatCommand(options) {
3149
3381
  bgSession.controller.reset();
3150
3382
  const rth = { text: '' };
3151
3383
  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 });
3384
+ const activeSpiral = await getSpiralEngine(brainScope);
3385
+ 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 }, undefined, undefined, undefined, undefined, null, undefined, jarvisLearning, undefined, jarvisSkills);
3153
3386
  bgSession.buffer.onSummary = undefined;
3154
3387
  return rth.text;
3155
3388
  },
@@ -3184,9 +3417,12 @@ export async function chatCommand(options) {
3184
3417
  }
3185
3418
  },
3186
3419
  updateStatus: () => updateStatusBar(),
3187
- storeInSpiral: spiralEngine ? async (content, type, tags) => {
3420
+ storeInSpiral: config.spiral.enabled ? async (content, type, tags) => {
3421
+ const activeSpiral = await getSpiralEngine(brainScope);
3422
+ if (!activeSpiral)
3423
+ return;
3188
3424
  try {
3189
- await spiralEngine.add(content, { type: type, tags });
3425
+ await activeSpiral.add(content, { type: type, tags });
3190
3426
  }
3191
3427
  catch { }
3192
3428
  } : undefined,
@@ -3197,6 +3433,8 @@ export async function chatCommand(options) {
3197
3433
  jarvisLearning.recordLearning(error, solution, category, context, ['jarvis_daemon']);
3198
3434
  },
3199
3435
  thinkingCallbacks: buildThinkingCallbacks(bgSession),
3436
+ queueSkillBuildTask: queueSkillBuildFromFailure,
3437
+ onSkillBuildComplete: activateBuiltSkill,
3200
3438
  });
3201
3439
  }
3202
3440
  catch (err) {
@@ -3245,10 +3483,11 @@ export async function chatCommand(options) {
3245
3483
  }
3246
3484
  // Frame was already cleared at processInput entry — render user message
3247
3485
  // Use display text (badge placeholders) if available, full text goes to model
3248
- renderUserMessage(displayText || input);
3486
+ renderUserMessage(renderedInput);
3249
3487
  // Track user message in session buffer
3250
3488
  sessionBuffer.addUserMessage(input);
3251
3489
  // Sentiment detection on every user message
3490
+ ensureJarvisRuntime();
3252
3491
  const sentimentReading = jarvisSentiment.detectSentiment(input);
3253
3492
  jarvisSentiment.recordReading(sentimentReading);
3254
3493
  // Auto-detect bug reports from user messages
@@ -3401,10 +3640,16 @@ export async function chatCommand(options) {
3401
3640
  }
3402
3641
  else {
3403
3642
  // ═══ 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))) {
3643
+ if (config.spiral.enabled && !turnDirectives.fastMode) {
3644
+ spiralEngine = await ensureSpiralEngine(brainScope);
3645
+ }
3646
+ const effectiveValidation = resolveValidationDecision(input, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, turnDirectives);
3647
+ const swarmHeuristic = swarmOrchestrator.analyze(input);
3648
+ const shouldUseSwarm = !activeSwarm &&
3649
+ !turnDirectives.skipSwarm &&
3650
+ (turnDirectives.forceSwarm || swarmHeuristic.shouldOrchestrate);
3651
+ renderExecutionIntent(turnDirectives, effectiveValidation, swarmHeuristic, shouldUseSwarm);
3652
+ if (shouldUseSwarm) {
3408
3653
  const { pushSwarmCreated, pushSwarmUpdated, pushSwarmCompleted, pushWorkerStarted, pushWorkerCompleted, pushOutputLine: _pushOutputLine, pushSessionCreated: _pushSessionCreated } = await import('../brain/generator.js');
3409
3654
  const { serializeSession: _serializeSession } = await import('../brain/control-protocol.js');
3410
3655
  const swarm = new SwarmController(swarmOrchestrator, swarmExecutor, sessionMgr, {
@@ -3434,7 +3679,7 @@ export async function chatCommand(options) {
3434
3679
  };
3435
3680
  _pushSessionCreated(_serializeSession(session));
3436
3681
  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);
3682
+ 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
3683
  }
3439
3684
  finally {
3440
3685
  await runtime.release();
@@ -3459,8 +3704,10 @@ export async function chatCommand(options) {
3459
3704
  pushWorkerCompleted,
3460
3705
  });
3461
3706
  activeSwarm = swarm;
3462
- renderInfo(chalk.hex('#ffd700')('\u{1F41D} Swarm detected — decomposing into parallel tasks...'));
3463
- const summary = await swarm.run(swarmInput);
3707
+ const swarmReason = swarmHeuristic.reasons.slice(0, 2).join(', ');
3708
+ renderInfo(chalk.hex('#ffd700')('\u{1F41D} Swarm engaged') +
3709
+ (swarmReason ? chalk.dim(` — ${swarmReason}`) : ''));
3710
+ const summary = await swarm.run(input);
3464
3711
  activeSwarm = null;
3465
3712
  if (summary) {
3466
3713
  // Swarm completed — render summary
@@ -3469,7 +3716,7 @@ export async function chatCommand(options) {
3469
3716
  }
3470
3717
  else {
3471
3718
  // 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) => {
3719
+ 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
3720
  currentStepLabel = label;
3474
3721
  const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3475
3722
  currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
@@ -3488,7 +3735,7 @@ export async function chatCommand(options) {
3488
3735
  // Muting is handled by activity.setMuteCallbacks (mute during LLM stream,
3489
3736
  // unmute during tool execution so user can answer permission prompts).
3490
3737
  isAtPrompt = false;
3491
- }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3738
+ }, effectiveValidation, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn, getJarvisContextForPrompt(), (label, tool) => {
3492
3739
  currentStepLabel = label;
3493
3740
  const fileTools = new Set(['read_file', 'write_file', 'edit_file']);
3494
3741
  currentStepFile = fileTools.has(tool) ? label.replace(/^(reading|writing|editing)\s+/, '') : '';
@@ -3503,22 +3750,7 @@ export async function chatCommand(options) {
3503
3750
  while (typeAheadBuffer.length > 0 && !agentController.isAborted) {
3504
3751
  const buffered = typeAheadBuffer.shift();
3505
3752
  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() });
3753
+ await processInput(buffered.trim());
3522
3754
  }
3523
3755
  }
3524
3756
  showPrompt();
@@ -3712,7 +3944,7 @@ async function runBackgroundSession(session, prompt, provider, project, spiralEn
3712
3944
  durationMs: session.elapsed,
3713
3945
  };
3714
3946
  }
3715
- 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) {
3947
+ 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) {
3716
3948
  // User message was rendered by renderUserMessage() in the caller before entering here.
3717
3949
  const executionRoot = runtime?.executionRoot ?? process.cwd();
3718
3950
  // Intent Detection: Check if user wants to feed the codebase
@@ -3811,6 +4043,7 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
3811
4043
  visionProcessor,
3812
4044
  onBrowserScreenshot: onBrowserScreenshot ?? undefined,
3813
4045
  learningJournal,
4046
+ skillManager,
3814
4047
  worktree: runtime?.worktree,
3815
4048
  },
3816
4049
  checkpointStore,
@@ -4008,9 +4241,15 @@ function showFullAutonomousWarning() {
4008
4241
  process.stdout.write(d('\u2502 ') + d('ESC = stop agent if needed.') + d(' \u2502') + '\n');
4009
4242
  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
4243
  }
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) {
4244
+ 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
4245
  const parts = input.split(/\s+/);
4013
4246
  let cmd = parts[0].toLowerCase();
4247
+ const ensureSpiralForScope = async (scope = currentBrainScope || 'global') => {
4248
+ if (!resolveSpiralEngine)
4249
+ return spiralEngine;
4250
+ spiralEngine = await resolveSpiralEngine(scope);
4251
+ return spiralEngine;
4252
+ };
4014
4253
  // ─── Chrome-aware selectMenu ───────────────────────────
4015
4254
  // Deactivates Screen during interactive menus to prevent
4016
4255
  // stdout hook interference (menu stacking on Windows Terminal).
@@ -4112,7 +4351,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4112
4351
  rl.resume();
4113
4352
  if (helpIdx >= 0 && helpCmds[helpIdx]) {
4114
4353
  // 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);
4354
+ 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
4355
  }
4117
4356
  break;
4118
4357
  }
@@ -4211,6 +4450,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4211
4450
  return 'drain'; // Sub-readline may leave phantom line events
4212
4451
  }
4213
4452
  case '/spiral':
4453
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4214
4454
  if (spiralEngine) {
4215
4455
  try {
4216
4456
  const status = spiralEngine.status();
@@ -4221,7 +4461,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4221
4461
  }
4222
4462
  }
4223
4463
  else {
4224
- renderInfo('Spiral engine disabled.');
4464
+ renderInfo(config.spiral.enabled ? 'Spiral engine not available.' : 'Spiral engine disabled.');
4225
4465
  }
4226
4466
  break;
4227
4467
  case '/helix':
@@ -4242,6 +4482,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4242
4482
  renderInfo(chalk.yellow(' Cannot use local brain here — using global'));
4243
4483
  }
4244
4484
  }
4485
+ spiralEngine = await ensureSpiralForScope('project');
4245
4486
  // Auto-start brain visualization
4246
4487
  if (spiralEngine) {
4247
4488
  try {
@@ -4282,6 +4523,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4282
4523
  }
4283
4524
  catch { /* optional */ }
4284
4525
  }
4526
+ spiralEngine = await ensureSpiralForScope('global');
4285
4527
  // Auto-start brain visualization
4286
4528
  if (spiralEngine) {
4287
4529
  try {
@@ -4365,6 +4607,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4365
4607
  }
4366
4608
  // Open 3D visualization (for /brain or /brain view)
4367
4609
  if (!brainArg || brainArg === 'view') {
4610
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4368
4611
  if (spiralEngine) {
4369
4612
  try {
4370
4613
  const { exportBrainData } = await import('../brain/exporter.js');
@@ -4401,6 +4644,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4401
4644
  // Handled directly in chatCommand() for access to inlineProgressActive flag
4402
4645
  break;
4403
4646
  case '/context':
4647
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4404
4648
  if (spiralEngine) {
4405
4649
  const status = spiralEngine.status();
4406
4650
  renderInfo(`Context: ${status.total_nodes} spiral nodes, ${status.total_edges} edges`);
@@ -4423,6 +4667,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4423
4667
  break;
4424
4668
  }
4425
4669
  case '/compact':
4670
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4426
4671
  if (spiralEngine) {
4427
4672
  const result = spiralEngine.evolve();
4428
4673
  renderInfo(`Evolution: ${result.promoted} promoted, ${result.demoted} demoted, ${result.summarized} summarized`);
@@ -4739,6 +4984,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4739
4984
  }
4740
4985
  case '/export': {
4741
4986
  const outputDir = parts[1] || process.cwd();
4987
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'global');
4742
4988
  if (spiralEngine) {
4743
4989
  try {
4744
4990
  const { exportToZip } = await import('../brain/archive.js');
@@ -5577,7 +5823,12 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
5577
5823
  const port = getBrainPort();
5578
5824
  process.stdout.write(` ${theme.success('\u{1F310} Already connected')} on port ${port}\n`);
5579
5825
  }
5580
- else if (spiralEngine) {
5826
+ else {
5827
+ spiralEngine = await ensureSpiralForScope(currentBrainScope || 'project');
5828
+ if (!spiralEngine) {
5829
+ renderError(config.spiral.enabled ? 'No spiral engine available.' : 'Spiral engine is disabled.');
5830
+ break;
5831
+ }
5581
5832
  const { exportBrainData } = await import('../brain/exporter.js');
5582
5833
  exportBrainData(spiralEngine, 'HelixMind Project', currentBrainScope || 'project');
5583
5834
  const url = await startLiveBrain(spiralEngine, 'HelixMind Project', currentBrainScope || 'project');
@@ -5586,9 +5837,6 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
5586
5837
  process.stdout.write(` ${theme.success('\u{1F310} Brain server started:')} ${url}\n`);
5587
5838
  process.stdout.write(` ${theme.dim('Web dashboard can now connect.')}\n`);
5588
5839
  }
5589
- else {
5590
- renderError('No spiral engine available. Run /helix first.');
5591
- }
5592
5840
  }
5593
5841
  catch (err) {
5594
5842
  renderError(`Failed to start brain server: ${err}`);