@covibes/zeroshot 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js CHANGED
@@ -47,6 +47,8 @@ const {
47
47
  DEFAULT_SETTINGS,
48
48
  } = require('../lib/settings');
49
49
  const { requirePreflight } = require('../src/preflight');
50
+ const { checkFirstRun } = require('./lib/first-run');
51
+ const { checkForUpdates } = require('./lib/update-checker');
50
52
  const { StatusFooter } = require('../src/status-footer');
51
53
 
52
54
  const program = new Command();
@@ -60,6 +62,39 @@ let activeClusterId = null;
60
62
  /** @type {import('../src/orchestrator') | null} */
61
63
  let orchestratorInstance = null;
62
64
 
65
+ // Track active status footer for safe output routing
66
+ // When set, all output routes through statusFooter.print() to prevent garbling
67
+ /** @type {import('../src/status-footer').StatusFooter | null} */
68
+ let activeStatusFooter = null;
69
+
70
+ /**
71
+ * Safe print - routes through statusFooter when active to prevent garbling
72
+ * @param {...any} args - Arguments to print (like console.log)
73
+ */
74
+ function safePrint(...args) {
75
+ const text = args.map(arg =>
76
+ typeof arg === 'string' ? arg : String(arg)
77
+ ).join(' ');
78
+
79
+ if (activeStatusFooter) {
80
+ activeStatusFooter.print(text + '\n');
81
+ } else {
82
+ console.log(...args);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Safe write - routes through statusFooter when active
88
+ * @param {string} text - Text to write
89
+ */
90
+ function safeWrite(text) {
91
+ if (activeStatusFooter) {
92
+ activeStatusFooter.print(text);
93
+ } else {
94
+ process.stdout.write(text);
95
+ }
96
+ }
97
+
63
98
  /**
64
99
  * Handle fatal errors: log, cleanup cluster state, exit
65
100
  * @param {string} type - 'uncaughtException' or 'unhandledRejection'
@@ -342,11 +377,12 @@ program
342
377
  .name('zeroshot')
343
378
  .description('Multi-agent orchestration and task management for Claude')
344
379
  .version(require('../package.json').version)
380
+ .option('-q, --quiet', 'Suppress prompts (first-run wizard, update checks)')
345
381
  .addHelpText(
346
382
  'after',
347
383
  `
348
384
  Examples:
349
- ${chalk.cyan('zeroshot auto 123')} Full automation: isolated + auto-merge PR
385
+ ${chalk.cyan('zeroshot run 123 --ship')} Full automation: isolated + auto-merge PR
350
386
  ${chalk.cyan('zeroshot run 123')} Run cluster and attach to first agent
351
387
  ${chalk.cyan('zeroshot run 123 -d')} Run cluster in background (detached)
352
388
  ${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster on plain text task
@@ -362,19 +398,20 @@ Examples:
362
398
  ${chalk.cyan('zeroshot status <id>')} Detailed status of task or cluster
363
399
  ${chalk.cyan('zeroshot finish <id>')} Convert cluster to completion task (creates and merges PR)
364
400
  ${chalk.cyan('zeroshot kill <id>')} Kill a running task or cluster
365
- ${chalk.cyan('zeroshot clear')} Kill all processes and delete all data (with confirmation)
366
- ${chalk.cyan('zeroshot clear -y')} Clear everything without confirmation
401
+ ${chalk.cyan('zeroshot purge')} Kill all processes and delete all data (with confirmation)
402
+ ${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
367
403
  ${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (default model, config, etc.)
368
404
  ${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., defaultModel haiku)
369
405
  ${chalk.cyan('zeroshot config list')} List available cluster configs
370
406
  ${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
371
407
  ${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
372
408
 
373
- Cluster vs Task:
374
- ${chalk.yellow('zeroshot auto')} Full automation (isolated + auto-merge PR)
375
- ${chalk.yellow('zeroshot run')} Multi-agent cluster (auto-attaches, Ctrl+B d to detach)
376
- ${chalk.yellow('zeroshot run -d')} Multi-agent cluster (background/detached)
377
- ${chalk.yellow('zeroshot task run')} Single-agent background task (simpler, faster)
409
+ Automation levels (cascading: --ship → --pr → --isolation):
410
+ ${chalk.yellow('zeroshot run 123')} Local run, no isolation
411
+ ${chalk.yellow('zeroshot run 123 --isolation')} Docker isolation, no PR
412
+ ${chalk.yellow('zeroshot run 123 --pr')} Isolation + PR (human reviews)
413
+ ${chalk.yellow('zeroshot run 123 --ship')} Isolation + PR + auto-merge (full automation)
414
+ ${chalk.yellow('zeroshot task run')} → Single-agent background task (simpler, faster)
378
415
 
379
416
  Shell completion:
380
417
  ${chalk.dim('zeroshot --completion >> ~/.bashrc && source ~/.bashrc')}
@@ -396,8 +433,8 @@ program
396
433
  '--strict-schema',
397
434
  'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
398
435
  )
399
- .option('--pr', 'Create PR and merge on successful completion (requires --isolation)')
400
- .option('--full', 'Shorthand for --isolation --pr (full automation)')
436
+ .option('--pr', 'Create PR for human review (auto-enables --isolation)')
437
+ .option('--ship', 'Full automation: isolation + PR + auto-merge')
401
438
  .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
402
439
  .option('-d, --detach', 'Run in background (default: attach to first agent)')
403
440
  .addHelpText(
@@ -412,10 +449,15 @@ Input formats:
412
449
  )
413
450
  .action(async (inputArg, options) => {
414
451
  try {
415
- // Expand --full to --isolation + --pr
416
- if (options.full) {
417
- options.isolation = true;
452
+ // Cascading flag implications: --ship --pr --isolation
453
+ // --ship = full automation (isolation + PR + auto-merge)
454
+ if (options.ship) {
418
455
  options.pr = true;
456
+ options.isolation = true;
457
+ }
458
+ // --pr = PR for human review (auto-enables isolation)
459
+ if (options.pr) {
460
+ options.isolation = true;
419
461
  }
420
462
 
421
463
  // Auto-detect input type
@@ -443,18 +485,12 @@ Input formats:
443
485
  // This gives users clear, actionable error messages upfront
444
486
  const preflightOptions = {
445
487
  requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
446
- requireDocker: options.isolation || options.full, // Docker required for isolation mode
488
+ requireDocker: options.isolation, // Docker required for isolation mode
447
489
  quiet: process.env.CREW_DAEMON === '1', // Suppress success in daemon mode
448
490
  };
449
491
  requirePreflight(preflightOptions);
450
492
 
451
493
  // === CLUSTER MODE ===
452
- // Validate --pr requires --isolation
453
- if (options.pr && !options.isolation) {
454
- console.error(chalk.red('Error: --pr requires --isolation flag for safety'));
455
- console.error(chalk.dim(' Usage: zeroshot run 123 --isolation --pr'));
456
- process.exit(1);
457
- }
458
494
 
459
495
  const { generateName } = require('../src/name-generator');
460
496
 
@@ -614,6 +650,8 @@ Input formats:
614
650
  statusFooter.setCluster(clusterId);
615
651
  statusFooter.setClusterState('running');
616
652
  statusFooter.setMessageBus(cluster.messageBus);
653
+ // Set module-level reference so safePrint/safeWrite route through footer
654
+ activeStatusFooter = statusFooter;
617
655
 
618
656
  // Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
619
657
  const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
@@ -698,14 +736,16 @@ Input formats:
698
736
  if (status.state !== 'running') {
699
737
  clearInterval(checkInterval);
700
738
  clearInterval(flushInterval);
701
- // Stop status footer
702
- statusFooter.stop();
703
739
  lifecycleUnsubscribe();
704
- // Final flush
740
+ // Final flush BEFORE stopping status footer
741
+ // (statusFooter.stop() sends ANSI codes that can clear terminal area)
705
742
  for (const sender of sendersWithOutput) {
706
743
  const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
707
744
  flushLineBuffer(prefix, sender);
708
745
  }
746
+ // Stop status footer AFTER output is done
747
+ statusFooter.stop();
748
+ activeStatusFooter = null;
709
749
  unsubscribe();
710
750
  resolve();
711
751
  }
@@ -714,6 +754,7 @@ Input formats:
714
754
  clearInterval(checkInterval);
715
755
  clearInterval(flushInterval);
716
756
  statusFooter.stop();
757
+ activeStatusFooter = null;
717
758
  lifecycleUnsubscribe();
718
759
  unsubscribe();
719
760
  resolve();
@@ -726,6 +767,7 @@ Input formats:
726
767
  process.on('SIGINT', async () => {
727
768
  // Stop status footer first to restore terminal
728
769
  statusFooter.stop();
770
+ activeStatusFooter = null;
729
771
  lifecycleUnsubscribe();
730
772
 
731
773
  console.log(chalk.dim('\n\n--- Interrupted ---'));
@@ -750,74 +792,28 @@ Input formats:
750
792
  }
751
793
 
752
794
  // Daemon mode: cluster runs in background, stay alive via orchestrator's setInterval
795
+ // Add cleanup handlers for daemon mode to ensure container cleanup on process exit
796
+ // CRITICAL: Without this, containers become orphaned when daemon process dies
797
+ if (process.env.CREW_DAEMON) {
798
+ const cleanup = async (signal) => {
799
+ console.log(`\n[DAEMON] Received ${signal}, cleaning up cluster ${clusterId}...`);
800
+ try {
801
+ await orchestrator.stop(clusterId);
802
+ console.log(`[DAEMON] Cluster ${clusterId} stopped.`);
803
+ } catch (e) {
804
+ console.error(`[DAEMON] Cleanup error: ${e.message}`);
805
+ }
806
+ process.exit(0);
807
+ };
808
+ process.on('SIGTERM', () => cleanup('SIGTERM'));
809
+ process.on('SIGINT', () => cleanup('SIGINT'));
810
+ }
753
811
  } catch (error) {
754
812
  console.error('Error:', error.message);
755
813
  process.exit(1);
756
814
  }
757
815
  });
758
816
 
759
- // Auto command - full automation (isolation + PR)
760
- program
761
- .command('auto <input>')
762
- .description('Full automation: isolated + auto-merge PR (shorthand for run --isolation --pr)')
763
- .option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
764
- .option('-m, --model <model>', 'Model for all agents: opus, sonnet, haiku (default: from config)')
765
- .option(
766
- '--isolation-image <image>',
767
- 'Docker image for isolation (default: zeroshot-cluster-base)'
768
- )
769
- .option(
770
- '--strict-schema',
771
- 'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
772
- )
773
- .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
774
- .option('-d, --detach', 'Run in background (default: attach to first agent)')
775
- .addHelpText(
776
- 'after',
777
- `
778
- Input formats:
779
- 123 GitHub issue number (uses current repo)
780
- org/repo#123 GitHub issue with explicit repo
781
- https://github.com/.../issues/1 Full GitHub issue URL
782
- "Implement feature X" Plain text task description
783
-
784
- Examples:
785
- ${chalk.cyan('zeroshot auto 123')} Auto-resolve issue (isolated + PR)
786
- ${chalk.cyan('zeroshot auto 123 -d')} Same, but detached/background
787
- `
788
- )
789
- .action((inputArg, options) => {
790
- // Auto command is shorthand for: zeroshot run <input> --isolation --pr [options]
791
- // Re-invoke CLI with the correct flags to avoid Commander.js internal API issues
792
- const { spawn } = require('child_process');
793
-
794
- const args = ['run', inputArg, '--isolation', '--pr'];
795
-
796
- // Forward other options
797
- if (options.config) args.push('--config', options.config);
798
- if (options.model) args.push('--model', options.model);
799
- if (options.isolationImage) args.push('--isolation-image', options.isolationImage);
800
- if (options.strictSchema) args.push('--strict-schema');
801
- if (options.workers) args.push('--workers', String(options.workers));
802
- if (options.detach) args.push('--detach');
803
-
804
- // Spawn zeroshot run with inherited stdio
805
- const proc = spawn(process.execPath, [process.argv[1], ...args], {
806
- stdio: 'inherit',
807
- cwd: process.cwd(),
808
- env: process.env,
809
- });
810
-
811
- proc.on('close', (code) => {
812
- process.exit(code || 0);
813
- });
814
-
815
- proc.on('error', (err) => {
816
- console.error(chalk.red(`Error: ${err.message}`));
817
- process.exit(1);
818
- });
819
- });
820
-
821
817
  // === TASK COMMANDS ===
822
818
  // Task run - single-agent background task
823
819
  const taskCmd = program.command('task').description('Single-agent task management');
@@ -901,48 +897,78 @@ program
901
897
  .description('List all tasks and clusters')
902
898
  .option('-s, --status <status>', 'Filter tasks by status (running, completed, failed)')
903
899
  .option('-n, --limit <n>', 'Limit number of results', parseInt)
900
+ .option('--json', 'Output as JSON')
904
901
  .action(async (options) => {
905
902
  try {
906
903
  // Get clusters
907
904
  const clusters = getOrchestrator().listClusters();
905
+ const orchestrator = getOrchestrator();
906
+
907
+ // Enrich clusters with token data
908
+ const enrichedClusters = clusters.map((cluster) => {
909
+ let totalTokens = 0;
910
+ let totalCostUsd = 0;
911
+ try {
912
+ const clusterObj = orchestrator.getCluster(cluster.id);
913
+ if (clusterObj?.messageBus) {
914
+ const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
915
+ if (tokensByRole?._total?.count > 0) {
916
+ const total = tokensByRole._total;
917
+ totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
918
+ totalCostUsd = total.totalCostUsd || 0;
919
+ }
920
+ }
921
+ } catch {
922
+ /* Token tracking not available */
923
+ }
924
+ return {
925
+ ...cluster,
926
+ totalTokens,
927
+ totalCostUsd,
928
+ };
929
+ });
908
930
 
909
931
  // Get tasks (dynamic import)
910
- const { listTasks } = await import('../task-lib/commands/list.js');
932
+ const { listTasks, getTasksData } = await import('../task-lib/commands/list.js');
911
933
 
912
- // Capture task output (listTasks prints directly, we need to capture)
913
- // For now, let's list them separately
934
+ // JSON output mode
935
+ if (options.json) {
936
+ // Get tasks data if available
937
+ let tasks = [];
938
+ try {
939
+ if (typeof getTasksData === 'function') {
940
+ tasks = await getTasksData(options);
941
+ }
942
+ } catch {
943
+ /* Tasks not available */
944
+ }
914
945
 
946
+ console.log(
947
+ JSON.stringify(
948
+ {
949
+ clusters: enrichedClusters,
950
+ tasks,
951
+ },
952
+ null,
953
+ 2
954
+ )
955
+ );
956
+ return;
957
+ }
958
+
959
+ // Human-readable output (default)
915
960
  // Print clusters
916
- if (clusters.length > 0) {
961
+ if (enrichedClusters.length > 0) {
917
962
  console.log(chalk.bold('\n=== Clusters ==='));
918
963
  console.log(
919
964
  `${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Agents'.padEnd(8)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
920
965
  );
921
966
  console.log('-'.repeat(100));
922
967
 
923
- const orchestrator = getOrchestrator();
924
- for (const cluster of clusters) {
968
+ for (const cluster of enrichedClusters) {
925
969
  const created = new Date(cluster.createdAt).toLocaleString();
926
-
927
- // Get token usage
928
- let tokenDisplay = '-';
929
- let costDisplay = '-';
930
- try {
931
- const clusterObj = orchestrator.getCluster(cluster.id);
932
- if (clusterObj?.messageBus) {
933
- const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
934
- if (tokensByRole?._total?.count > 0) {
935
- const total = tokensByRole._total;
936
- const totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
937
- tokenDisplay = totalTokens.toLocaleString();
938
- if (total.totalCostUsd > 0) {
939
- costDisplay = '$' + total.totalCostUsd.toFixed(3);
940
- }
941
- }
942
- }
943
- } catch {
944
- /* Token tracking not available */
945
- }
970
+ const tokenDisplay = cluster.totalTokens > 0 ? cluster.totalTokens.toLocaleString() : '-';
971
+ const costDisplay = cluster.totalCostUsd > 0 ? '$' + cluster.totalCostUsd.toFixed(3) : '-';
946
972
 
947
973
  // Highlight zombie clusters in red
948
974
  const stateDisplay =
@@ -974,14 +1000,19 @@ program
974
1000
  program
975
1001
  .command('status <id>')
976
1002
  .description('Get detailed status of a task or cluster')
977
- .action(async (id) => {
1003
+ .option('--json', 'Output as JSON')
1004
+ .action(async (id, options) => {
978
1005
  try {
979
1006
  const { detectIdType } = require('../lib/id-detector');
980
1007
  const type = detectIdType(id);
981
1008
 
982
1009
  if (!type) {
983
- console.error(`ID not found: ${id}`);
984
- console.error('Not found in tasks or clusters');
1010
+ if (options.json) {
1011
+ console.log(JSON.stringify({ error: 'ID not found', id }, null, 2));
1012
+ } else {
1013
+ console.error(`ID not found: ${id}`);
1014
+ console.error('Not found in tasks or clusters');
1015
+ }
985
1016
  process.exit(1);
986
1017
  }
987
1018
 
@@ -989,6 +1020,35 @@ program
989
1020
  // Show cluster status
990
1021
  const status = getOrchestrator().getStatus(id);
991
1022
 
1023
+ // Get token usage
1024
+ let tokensByRole = null;
1025
+ try {
1026
+ const cluster = getOrchestrator().getCluster(id);
1027
+ if (cluster?.messageBus) {
1028
+ tokensByRole = cluster.messageBus.getTokensByRole(id);
1029
+ }
1030
+ } catch {
1031
+ /* Token tracking not available */
1032
+ }
1033
+
1034
+ // JSON output mode
1035
+ if (options.json) {
1036
+ console.log(
1037
+ JSON.stringify(
1038
+ {
1039
+ type: 'cluster',
1040
+ ...status,
1041
+ createdAtISO: new Date(status.createdAt).toISOString(),
1042
+ tokensByRole,
1043
+ },
1044
+ null,
1045
+ 2
1046
+ )
1047
+ );
1048
+ return;
1049
+ }
1050
+
1051
+ // Human-readable output
992
1052
  console.log(`\nCluster: ${status.id}`);
993
1053
  if (status.isZombie) {
994
1054
  console.log(
@@ -1011,20 +1071,14 @@ program
1011
1071
  console.log(`Messages: ${status.messageCount}`);
1012
1072
 
1013
1073
  // Show token usage if available
1014
- try {
1015
- const cluster = getOrchestrator().getCluster(id);
1016
- if (cluster?.messageBus) {
1017
- const tokensByRole = cluster.messageBus.getTokensByRole(id);
1018
- const tokenLines = formatTokenUsage(tokensByRole);
1019
- if (tokenLines) {
1020
- console.log('');
1021
- for (const line of tokenLines) {
1022
- console.log(line);
1023
- }
1074
+ if (tokensByRole) {
1075
+ const tokenLines = formatTokenUsage(tokensByRole);
1076
+ if (tokenLines) {
1077
+ console.log('');
1078
+ for (const line of tokenLines) {
1079
+ console.log(line);
1024
1080
  }
1025
1081
  }
1026
- } catch {
1027
- /* Token tracking not available */
1028
1082
  }
1029
1083
 
1030
1084
  console.log(`\nAgents:`);
@@ -1049,11 +1103,30 @@ program
1049
1103
  console.log('');
1050
1104
  } else {
1051
1105
  // Show task status
1052
- const { showStatus } = await import('../task-lib/commands/status.js');
1106
+ const { showStatus, getStatusData } = await import('../task-lib/commands/status.js');
1107
+
1108
+ if (options.json) {
1109
+ // Try to get JSON data if available
1110
+ let taskData = null;
1111
+ try {
1112
+ if (typeof getStatusData === 'function') {
1113
+ taskData = await getStatusData(id);
1114
+ }
1115
+ } catch {
1116
+ /* Not available */
1117
+ }
1118
+ console.log(JSON.stringify({ type: 'task', id, ...taskData }, null, 2));
1119
+ return;
1120
+ }
1121
+
1053
1122
  await showStatus(id);
1054
1123
  }
1055
1124
  } catch (error) {
1056
- console.error('Error getting status:', error.message);
1125
+ if (options.json) {
1126
+ console.log(JSON.stringify({ error: error.message }, null, 2));
1127
+ } else {
1128
+ console.error('Error getting status:', error.message);
1129
+ }
1057
1130
  process.exit(1);
1058
1131
  }
1059
1132
  });
@@ -2405,10 +2478,10 @@ program
2405
2478
  }
2406
2479
  });
2407
2480
 
2408
- // Clear all runs (clusters + tasks)
2481
+ // Purge all runs (clusters + tasks) - NUCLEAR option
2409
2482
  program
2410
- .command('clear')
2411
- .description('Kill all running processes and delete all data')
2483
+ .command('purge')
2484
+ .description('NUCLEAR: Kill all running processes and delete all data')
2412
2485
  .option('-y, --yes', 'Skip confirmation')
2413
2486
  .action(async (options) => {
2414
2487
  try {
@@ -2549,7 +2622,7 @@ program
2549
2622
  await cleanTasks({ all: true });
2550
2623
  }
2551
2624
 
2552
- console.log(chalk.bold.green('\nAll runs killed and cleared.'));
2625
+ console.log(chalk.bold.green('\nAll runs purged.'));
2553
2626
  } catch (error) {
2554
2627
  console.error('Error clearing runs:', error.message);
2555
2628
  process.exit(1);
@@ -2950,6 +3023,210 @@ configCmd
2950
3023
  }
2951
3024
  });
2952
3025
 
3026
+ // Agent library commands
3027
+ const agentsCmd = program.command('agents').description('View available agent definitions');
3028
+
3029
+ agentsCmd
3030
+ .command('list')
3031
+ .alias('ls')
3032
+ .description('List available agent definitions')
3033
+ .option('--verbose', 'Show full agent details')
3034
+ .option('--json', 'Output as JSON')
3035
+ .action((options) => {
3036
+ try {
3037
+ const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
3038
+
3039
+ // Check if agents directory exists
3040
+ if (!fs.existsSync(agentsDir)) {
3041
+ if (options.json) {
3042
+ console.log(JSON.stringify({ agents: [], error: null }, null, 2));
3043
+ } else {
3044
+ console.log(chalk.dim('No agents directory found.'));
3045
+ }
3046
+ return;
3047
+ }
3048
+
3049
+ const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
3050
+
3051
+ if (files.length === 0) {
3052
+ if (options.json) {
3053
+ console.log(JSON.stringify({ agents: [], error: null }, null, 2));
3054
+ } else {
3055
+ console.log(chalk.dim('No agent definitions found in src/agents/'));
3056
+ }
3057
+ return;
3058
+ }
3059
+
3060
+ // Parse all agent files
3061
+ const agents = [];
3062
+ for (const file of files) {
3063
+ try {
3064
+ const agentPath = path.join(agentsDir, file);
3065
+ const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
3066
+ agents.push({
3067
+ file: file.replace('.json', ''),
3068
+ id: agent.id || file.replace('.json', ''),
3069
+ role: agent.role || 'unspecified',
3070
+ model: agent.model || 'default',
3071
+ triggers: agent.triggers?.length || 0,
3072
+ prompt: agent.prompt || null,
3073
+ output: agent.output || null,
3074
+ });
3075
+ } catch (err) {
3076
+ // Skip invalid JSON files
3077
+ console.error(chalk.yellow(`Warning: Could not parse ${file}: ${err.message}`));
3078
+ }
3079
+ }
3080
+
3081
+ // JSON output
3082
+ if (options.json) {
3083
+ console.log(JSON.stringify({ agents, error: null }, null, 2));
3084
+ return;
3085
+ }
3086
+
3087
+ // Human-readable output
3088
+ console.log(chalk.bold('\nAvailable agent definitions:\n'));
3089
+
3090
+ for (const agent of agents) {
3091
+ console.log(
3092
+ ` ${chalk.cyan(agent.id.padEnd(25))} ${chalk.dim('role:')} ${agent.role.padEnd(20)} ${chalk.dim('model:')} ${agent.model}`
3093
+ );
3094
+
3095
+ if (options.verbose) {
3096
+ console.log(chalk.dim(` Triggers: ${agent.triggers}`));
3097
+ if (agent.output) {
3098
+ console.log(chalk.dim(` Output topic: ${agent.output.topic || 'none'}`));
3099
+ }
3100
+ if (agent.prompt) {
3101
+ const promptPreview = agent.prompt.substring(0, 100).replace(/\n/g, ' ');
3102
+ console.log(chalk.dim(` Prompt: ${promptPreview}...`));
3103
+ }
3104
+ console.log('');
3105
+ }
3106
+ }
3107
+
3108
+ if (!options.verbose) {
3109
+ console.log('');
3110
+ console.log(chalk.dim(' Use --verbose for full details'));
3111
+ }
3112
+ console.log('');
3113
+ } catch (error) {
3114
+ if (options.json) {
3115
+ console.log(JSON.stringify({ agents: [], error: error.message }, null, 2));
3116
+ } else {
3117
+ console.error(chalk.red(`Error listing agents: ${error.message}`));
3118
+ }
3119
+ process.exit(1);
3120
+ }
3121
+ });
3122
+
3123
+ agentsCmd
3124
+ .command('show <name>')
3125
+ .description('Show detailed agent definition')
3126
+ .option('--json', 'Output as JSON')
3127
+ .action((name, options) => {
3128
+ try {
3129
+ const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
3130
+
3131
+ // Support both with and without .json extension
3132
+ const agentName = name.endsWith('.json') ? name : `${name}.json`;
3133
+ const agentPath = path.join(agentsDir, agentName);
3134
+
3135
+ if (!fs.existsSync(agentPath)) {
3136
+ // Try with -agent.json suffix
3137
+ const altPath = path.join(agentsDir, `${name}-agent.json`);
3138
+ if (fs.existsSync(altPath)) {
3139
+ const agent = JSON.parse(fs.readFileSync(altPath, 'utf8'));
3140
+ outputAgent(agent, options);
3141
+ return;
3142
+ }
3143
+
3144
+ if (options.json) {
3145
+ console.log(JSON.stringify({ error: `Agent not found: ${name}` }, null, 2));
3146
+ } else {
3147
+ console.error(chalk.red(`Agent not found: ${name}`));
3148
+ console.log(chalk.dim('\nAvailable agents:'));
3149
+ const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
3150
+ files.forEach((f) => console.log(chalk.dim(` - ${f.replace('.json', '')}`)));
3151
+ }
3152
+ process.exit(1);
3153
+ }
3154
+
3155
+ const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
3156
+ outputAgent(agent, options);
3157
+ } catch (error) {
3158
+ if (options.json) {
3159
+ console.log(JSON.stringify({ error: error.message }, null, 2));
3160
+ } else {
3161
+ console.error(chalk.red(`Error: ${error.message}`));
3162
+ }
3163
+ process.exit(1);
3164
+ }
3165
+ });
3166
+
3167
+ function outputAgent(agent, options) {
3168
+ if (options.json) {
3169
+ console.log(JSON.stringify(agent, null, 2));
3170
+ return;
3171
+ }
3172
+
3173
+ // Human-readable output
3174
+ console.log('');
3175
+ console.log(chalk.bold.cyan('═'.repeat(80)));
3176
+ console.log(chalk.bold.cyan(` Agent: ${agent.id}`));
3177
+ console.log(chalk.bold.cyan('═'.repeat(80)));
3178
+ console.log('');
3179
+
3180
+ // Basic info
3181
+ console.log(chalk.bold('Configuration:'));
3182
+ console.log(` ${chalk.dim('ID:')} ${agent.id}`);
3183
+ console.log(` ${chalk.dim('Role:')} ${agent.role || 'unspecified'}`);
3184
+ console.log(` ${chalk.dim('Model:')} ${agent.model || 'default'}`);
3185
+ console.log('');
3186
+
3187
+ // Triggers
3188
+ if (agent.triggers && agent.triggers.length > 0) {
3189
+ console.log(chalk.bold('Triggers:'));
3190
+ for (const trigger of agent.triggers) {
3191
+ console.log(` ${chalk.yellow('•')} Topic: ${chalk.cyan(trigger.topic)}`);
3192
+ if (trigger.action) {
3193
+ console.log(` Action: ${trigger.action}`);
3194
+ }
3195
+ if (trigger.logic?.script) {
3196
+ const scriptPreview = trigger.logic.script.substring(0, 80).replace(/\n/g, ' ');
3197
+ console.log(chalk.dim(` Logic: ${scriptPreview}...`));
3198
+ }
3199
+ }
3200
+ console.log('');
3201
+ }
3202
+
3203
+ // Output
3204
+ if (agent.output) {
3205
+ console.log(chalk.bold('Output:'));
3206
+ console.log(` ${chalk.dim('Topic:')} ${agent.output.topic || 'none'}`);
3207
+ if (agent.output.publishAfter) {
3208
+ console.log(` ${chalk.dim('Publish after:')} ${agent.output.publishAfter}`);
3209
+ }
3210
+ console.log('');
3211
+ }
3212
+
3213
+ // Prompt
3214
+ if (agent.prompt) {
3215
+ console.log(chalk.bold('Prompt:'));
3216
+ console.log(chalk.dim('─'.repeat(76)));
3217
+ // Show first 500 chars of prompt
3218
+ const promptLines = agent.prompt.substring(0, 500).split('\n');
3219
+ for (const line of promptLines) {
3220
+ console.log(` ${line}`);
3221
+ }
3222
+ if (agent.prompt.length > 500) {
3223
+ console.log(chalk.dim(` ... (${agent.prompt.length - 500} more characters)`));
3224
+ }
3225
+ console.log(chalk.dim('─'.repeat(76)));
3226
+ console.log('');
3227
+ }
3228
+ }
3229
+
2953
3230
  // Helper function to keep the process alive for follow mode
2954
3231
  function keepProcessAlive(cleanupFn) {
2955
3232
  // Prevent Node.js from exiting by keeping the event loop active
@@ -3570,32 +3847,40 @@ function accumulateText(prefix, sender, text) {
3570
3847
  buf.textBuffer = buf.textBuffer.slice(newlineIdx + 1);
3571
3848
 
3572
3849
  // Word wrap and print the complete line
3850
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3573
3851
  const wrappedLines = wordWrap(completeLine, contentWidth);
3852
+ let outputBuffer = '';
3853
+
3574
3854
  for (let i = 0; i < wrappedLines.length; i++) {
3575
3855
  const wrappedLine = wrappedLines[i];
3576
3856
 
3577
3857
  // Print prefix (real or continuation)
3578
3858
  if (buf.needsPrefix) {
3579
- process.stdout.write(`${prefix} `);
3859
+ outputBuffer += `${prefix} `;
3580
3860
  buf.needsPrefix = false;
3581
3861
  } else if (i > 0) {
3582
- process.stdout.write(`${continuationPrefix}`);
3862
+ outputBuffer += `${continuationPrefix}`;
3583
3863
  }
3584
3864
 
3585
3865
  if (wrappedLine.trim()) {
3586
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3866
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3587
3867
  }
3588
3868
 
3589
3869
  // Newline after each wrapped segment
3590
3870
  if (i < wrappedLines.length - 1) {
3591
- process.stdout.write('\n');
3871
+ outputBuffer += '\n';
3592
3872
  }
3593
3873
  }
3594
3874
 
3595
3875
  // Complete the line
3596
- process.stdout.write('\n');
3876
+ outputBuffer += '\n';
3597
3877
  buf.needsPrefix = true;
3598
3878
  buf.pendingNewline = false;
3879
+
3880
+ // Single atomic write prevents interleaving
3881
+ if (outputBuffer) {
3882
+ safeWrite(outputBuffer);
3883
+ }
3599
3884
  }
3600
3885
 
3601
3886
  // Mark that we have pending text (no newline yet)
@@ -3620,35 +3905,45 @@ function accumulateThinking(prefix, sender, text) {
3620
3905
  const newlineIdx = remaining.indexOf('\n');
3621
3906
  const rawLine = newlineIdx === -1 ? remaining : remaining.slice(0, newlineIdx);
3622
3907
 
3908
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3623
3909
  const wrappedLines = wordWrap(rawLine, contentWidth);
3910
+ let outputBuffer = '';
3624
3911
 
3625
3912
  for (let i = 0; i < wrappedLines.length; i++) {
3626
3913
  const wrappedLine = wrappedLines[i];
3627
3914
 
3628
3915
  if (buf.thinkingNeedsPrefix) {
3629
- process.stdout.write(`${prefix} ${chalk.dim.italic('💭 ')}`);
3916
+ outputBuffer += `${prefix} ${chalk.dim.italic('💭 ')}`;
3630
3917
  buf.thinkingNeedsPrefix = false;
3631
3918
  } else if (i > 0) {
3632
- process.stdout.write(`${continuationPrefix}`);
3919
+ outputBuffer += `${continuationPrefix}`;
3633
3920
  }
3634
3921
 
3635
3922
  if (wrappedLine.trim()) {
3636
- process.stdout.write(chalk.dim.italic(wrappedLine));
3923
+ outputBuffer += chalk.dim.italic(wrappedLine);
3637
3924
  }
3638
3925
 
3639
3926
  if (i < wrappedLines.length - 1) {
3640
- process.stdout.write('\n');
3927
+ outputBuffer += '\n';
3641
3928
  }
3642
3929
  }
3643
3930
 
3644
3931
  if (newlineIdx === -1) {
3645
3932
  buf.thinkingPendingNewline = true;
3933
+ // Single atomic write
3934
+ if (outputBuffer) {
3935
+ safeWrite(outputBuffer);
3936
+ }
3646
3937
  break;
3647
3938
  } else {
3648
- process.stdout.write('\n');
3939
+ outputBuffer += '\n';
3649
3940
  buf.thinkingNeedsPrefix = true;
3650
3941
  buf.thinkingPendingNewline = false;
3651
3942
  remaining = remaining.slice(newlineIdx + 1);
3943
+ // Single atomic write
3944
+ if (outputBuffer) {
3945
+ safeWrite(outputBuffer);
3946
+ }
3652
3947
  }
3653
3948
  }
3654
3949
  }
@@ -3658,7 +3953,10 @@ function flushLineBuffer(prefix, sender) {
3658
3953
  const buf = lineBuffers.get(sender);
3659
3954
  if (!buf) return;
3660
3955
 
3661
- // CRITICAL: Flush any remaining text in textBuffer (text without trailing newline)
3956
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3957
+ let outputBuffer = '';
3958
+
3959
+ // Flush any remaining text in textBuffer (text without trailing newline)
3662
3960
  if (buf.textBuffer && buf.textBuffer.length > 0) {
3663
3961
  // Calculate widths for word wrapping (same as accumulateText)
3664
3962
  const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 1;
@@ -3671,18 +3969,18 @@ function flushLineBuffer(prefix, sender) {
3671
3969
  const wrappedLine = wrappedLines[i];
3672
3970
 
3673
3971
  if (buf.needsPrefix) {
3674
- process.stdout.write(`${prefix} `);
3972
+ outputBuffer += `${prefix} `;
3675
3973
  buf.needsPrefix = false;
3676
3974
  } else if (i > 0) {
3677
- process.stdout.write(`${continuationPrefix}`);
3975
+ outputBuffer += `${continuationPrefix}`;
3678
3976
  }
3679
3977
 
3680
3978
  if (wrappedLine.trim()) {
3681
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3979
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3682
3980
  }
3683
3981
 
3684
3982
  if (i < wrappedLines.length - 1) {
3685
- process.stdout.write('\n');
3983
+ outputBuffer += '\n';
3686
3984
  }
3687
3985
  }
3688
3986
 
@@ -3692,15 +3990,20 @@ function flushLineBuffer(prefix, sender) {
3692
3990
  }
3693
3991
 
3694
3992
  if (buf.pendingNewline) {
3695
- process.stdout.write('\n');
3993
+ outputBuffer += '\n';
3696
3994
  buf.needsPrefix = true;
3697
3995
  buf.pendingNewline = false;
3698
3996
  }
3699
3997
  if (buf.thinkingPendingNewline) {
3700
- process.stdout.write('\n');
3998
+ outputBuffer += '\n';
3701
3999
  buf.thinkingNeedsPrefix = true;
3702
4000
  buf.thinkingPendingNewline = false;
3703
4001
  }
4002
+
4003
+ // Single atomic write prevents interleaving
4004
+ if (outputBuffer) {
4005
+ safeWrite(outputBuffer);
4006
+ }
3704
4007
  }
3705
4008
 
3706
4009
  // Lines to filter out (noise, metadata, errors)
@@ -3783,17 +4086,17 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3783
4086
  }
3784
4087
 
3785
4088
  if (msg.topic === 'PR_CREATED') {
3786
- formatPrCreated(msg, prefix, timestamp);
4089
+ formatPrCreated(msg, prefix, timestamp, safePrint);
3787
4090
  return;
3788
4091
  }
3789
4092
 
3790
4093
  if (msg.topic === 'CLUSTER_COMPLETE') {
3791
- formatClusterComplete(msg, prefix, timestamp);
4094
+ formatClusterComplete(msg, prefix, timestamp, safePrint);
3792
4095
  return;
3793
4096
  }
3794
4097
 
3795
4098
  if (msg.topic === 'CLUSTER_FAILED') {
3796
- formatClusterFailed(msg, prefix, timestamp);
4099
+ formatClusterFailed(msg, prefix, timestamp, safePrint);
3797
4100
  return;
3798
4101
  }
3799
4102
 
@@ -3819,7 +4122,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3819
4122
  if (event.text) {
3820
4123
  accumulateThinking(prefix, msg.sender, event.text);
3821
4124
  } else if (event.type === 'thinking_start') {
3822
- console.log(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
4125
+ safePrint(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
3823
4126
  }
3824
4127
  break;
3825
4128
 
@@ -3833,7 +4136,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3833
4136
  flushLineBuffer(prefix, msg.sender);
3834
4137
  const icon = getToolIcon(event.toolName);
3835
4138
  const toolDesc = formatToolCall(event.toolName, event.input);
3836
- console.log(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
4139
+ safePrint(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
3837
4140
  // Store tool call info for matching with result
3838
4141
  currentToolCall.set(msg.sender, {
3839
4142
  toolName: event.toolName,
@@ -3855,7 +4158,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3855
4158
  toolCall?.toolName,
3856
4159
  toolCall?.input
3857
4160
  );
3858
- console.log(`${prefix} ${status} ${resultDesc}`);
4161
+ safePrint(`${prefix} ${status} ${resultDesc}`);
3859
4162
  // Clear stored tool call after result
3860
4163
  currentToolCall.delete(msg.sender);
3861
4164
  break;
@@ -3865,7 +4168,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3865
4168
  flushLineBuffer(prefix, msg.sender);
3866
4169
  // Final result - only show errors (success text already streamed)
3867
4170
  if (!event.success) {
3868
- console.log(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
4171
+ safePrint(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
3869
4172
  }
3870
4173
  break;
3871
4174
 
@@ -3906,7 +4209,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3906
4209
  // Skip duplicate content
3907
4210
  if (isDuplicate(trimmed)) continue;
3908
4211
 
3909
- console.log(`${prefix} ${line}`);
4212
+ safePrint(`${prefix} ${line}`);
3910
4213
  }
3911
4214
  }
3912
4215
  return;
@@ -3914,22 +4217,22 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3914
4217
 
3915
4218
  // AGENT_ERROR: Show errors with visual prominence
3916
4219
  if (msg.topic === 'AGENT_ERROR') {
3917
- console.log(''); // Blank line before error
3918
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
3919
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
4220
+ safePrint(''); // Blank line before error
4221
+ safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
4222
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
3920
4223
  if (msg.content?.text) {
3921
- console.log(`${prefix} ${chalk.red(msg.content.text)}`);
4224
+ safePrint(`${prefix} ${chalk.red(msg.content.text)}`);
3922
4225
  }
3923
4226
  if (msg.content?.data?.stack) {
3924
4227
  // Show first 5 lines of stack trace
3925
4228
  const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
3926
4229
  for (const line of stackLines) {
3927
4230
  if (line.trim()) {
3928
- console.log(`${prefix} ${chalk.dim(line)}`);
4231
+ safePrint(`${prefix} ${chalk.dim(line)}`);
3929
4232
  }
3930
4233
  }
3931
4234
  }
3932
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
4235
+ safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
3933
4236
  return;
3934
4237
  }
3935
4238
 
@@ -3941,29 +4244,29 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3941
4244
  }
3942
4245
  shownNewTaskForCluster.add(msg.cluster_id);
3943
4246
 
3944
- console.log(''); // Blank line before new task
3945
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
3946
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
4247
+ safePrint(''); // Blank line before new task
4248
+ safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
4249
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
3947
4250
  if (msg.content?.text) {
3948
4251
  // Show task description (first 3 lines max)
3949
4252
  const lines = msg.content.text.split('\n').slice(0, 3);
3950
4253
  for (const line of lines) {
3951
4254
  if (line.trim() && line.trim() !== '# Manual Input') {
3952
- console.log(`${prefix} ${chalk.white(line)}`);
4255
+ safePrint(`${prefix} ${chalk.white(line)}`);
3953
4256
  }
3954
4257
  }
3955
4258
  }
3956
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
4259
+ safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
3957
4260
  return;
3958
4261
  }
3959
4262
 
3960
4263
  // IMPLEMENTATION_READY: milestone marker
3961
4264
  if (msg.topic === 'IMPLEMENTATION_READY') {
3962
- console.log(
4265
+ safePrint(
3963
4266
  `${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
3964
4267
  );
3965
4268
  if (msg.content?.data?.commit) {
3966
- console.log(
4269
+ safePrint(
3967
4270
  `${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
3968
4271
  );
3969
4272
  }
@@ -3976,33 +4279,33 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3976
4279
  const approved = data.approved === true || data.approved === 'true';
3977
4280
  const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
3978
4281
 
3979
- console.log(`${prefix} ${chalk.gray(timestamp)} ${status}`);
4282
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${status}`);
3980
4283
 
3981
4284
  // Show summary if present and not a template variable
3982
4285
  if (msg.content?.text && !msg.content.text.includes('{{')) {
3983
- console.log(`${prefix} ${msg.content.text.substring(0, 100)}`);
4286
+ safePrint(`${prefix} ${msg.content.text.substring(0, 100)}`);
3984
4287
  }
3985
4288
 
3986
4289
  // Show full JSON data structure
3987
- console.log(
4290
+ safePrint(
3988
4291
  `${prefix} ${chalk.dim(JSON.stringify(data, null, 2).split('\n').join(`\n${prefix} `))}`
3989
4292
  );
3990
4293
 
3991
4294
  // Show errors/issues if any
3992
4295
  if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
3993
- console.log(`${prefix} ${chalk.red('Errors:')}`);
4296
+ safePrint(`${prefix} ${chalk.red('Errors:')}`);
3994
4297
  data.errors.forEach((err) => {
3995
4298
  if (err && typeof err === 'string') {
3996
- console.log(`${prefix} - ${err}`);
4299
+ safePrint(`${prefix} - ${err}`);
3997
4300
  }
3998
4301
  });
3999
4302
  }
4000
4303
 
4001
4304
  if (data.issues && Array.isArray(data.issues) && data.issues.length > 0) {
4002
- console.log(`${prefix} ${chalk.yellow('Issues:')}`);
4305
+ safePrint(`${prefix} ${chalk.yellow('Issues:')}`);
4003
4306
  data.issues.forEach((issue) => {
4004
4307
  if (issue && typeof issue === 'string') {
4005
- console.log(`${prefix} - ${issue}`);
4308
+ safePrint(`${prefix} - ${issue}`);
4006
4309
  }
4007
4310
  });
4008
4311
  }
@@ -4010,26 +4313,42 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4010
4313
  }
4011
4314
 
4012
4315
  // Fallback: generic message display for unknown topics
4013
- formatGenericMessage(msg, prefix, timestamp);
4316
+ formatGenericMessage(msg, prefix, timestamp, safePrint);
4014
4317
  }
4015
4318
 
4016
- // Default command handling: if first arg doesn't match a known command, treat it as 'run'
4017
- // This allows `zeroshot "task"` to work the same as `zeroshot run "task"`
4018
- const args = process.argv.slice(2);
4019
- if (args.length > 0) {
4020
- const firstArg = args[0];
4021
-
4022
- // Skip if it's a flag/option (starts with -)
4023
- // Skip if it's --help or --version (these are handled by commander)
4024
- if (!firstArg.startsWith('-')) {
4025
- // Get all registered command names
4026
- const commandNames = program.commands.map((cmd) => cmd.name());
4027
-
4028
- // If first arg is not a known command, prepend 'run'
4029
- if (!commandNames.includes(firstArg)) {
4030
- process.argv.splice(2, 0, 'run');
4319
+ // Main async entry point
4320
+ async function main() {
4321
+ // First-run setup wizard (blocks on first use only)
4322
+ const isQuiet = process.argv.includes('-q') || process.argv.includes('--quiet');
4323
+ await checkFirstRun({ quiet: isQuiet });
4324
+
4325
+ // Check for updates (non-blocking if offline)
4326
+ await checkForUpdates({ quiet: isQuiet });
4327
+
4328
+ // Default command handling: if first arg doesn't match a known command, treat it as 'run'
4329
+ // This allows `zeroshot "task"` to work the same as `zeroshot run "task"`
4330
+ const args = process.argv.slice(2);
4331
+ if (args.length > 0) {
4332
+ const firstArg = args[0];
4333
+
4334
+ // Skip if it's a flag/option (starts with -)
4335
+ // Skip if it's --help or --version (these are handled by commander)
4336
+ if (!firstArg.startsWith('-')) {
4337
+ // Get all registered command names
4338
+ const commandNames = program.commands.map((cmd) => cmd.name());
4339
+
4340
+ // If first arg is not a known command, prepend 'run'
4341
+ if (!commandNames.includes(firstArg)) {
4342
+ process.argv.splice(2, 0, 'run');
4343
+ }
4031
4344
  }
4032
4345
  }
4346
+
4347
+ program.parse();
4033
4348
  }
4034
4349
 
4035
- program.parse();
4350
+ // Run main
4351
+ main().catch((err) => {
4352
+ console.error('Fatal error:', err.message);
4353
+ process.exit(1);
4354
+ });