@covibes/zeroshot 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +178 -186
  2. package/README.md +199 -248
  3. package/cli/commands/providers.js +150 -0
  4. package/cli/index.js +214 -58
  5. package/cli/lib/first-run.js +40 -3
  6. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  7. package/cluster-templates/base-templates/full-workflow.json +44 -145
  8. package/cluster-templates/base-templates/single-worker.json +23 -15
  9. package/cluster-templates/base-templates/worker-validator.json +47 -34
  10. package/cluster-templates/conductor-bootstrap.json +7 -5
  11. package/lib/docker-config.js +6 -1
  12. package/lib/provider-detection.js +59 -0
  13. package/lib/provider-names.js +56 -0
  14. package/lib/settings.js +191 -6
  15. package/lib/stream-json-parser.js +4 -238
  16. package/package.json +21 -5
  17. package/scripts/validate-templates.js +100 -0
  18. package/src/agent/agent-config.js +37 -13
  19. package/src/agent/agent-context-builder.js +64 -2
  20. package/src/agent/agent-hook-executor.js +82 -9
  21. package/src/agent/agent-lifecycle.js +53 -14
  22. package/src/agent/agent-task-executor.js +196 -194
  23. package/src/agent/output-extraction.js +200 -0
  24. package/src/agent/output-reformatter.js +175 -0
  25. package/src/agent/schema-utils.js +111 -0
  26. package/src/agent-wrapper.js +102 -30
  27. package/src/agents/git-pusher-agent.json +2 -2
  28. package/src/claude-task-runner.js +80 -30
  29. package/src/config-router.js +13 -13
  30. package/src/config-validator.js +231 -10
  31. package/src/github.js +36 -0
  32. package/src/isolation-manager.js +243 -154
  33. package/src/ledger.js +28 -6
  34. package/src/orchestrator.js +391 -96
  35. package/src/preflight.js +85 -82
  36. package/src/providers/anthropic/cli-builder.js +45 -0
  37. package/src/providers/anthropic/index.js +134 -0
  38. package/src/providers/anthropic/models.js +23 -0
  39. package/src/providers/anthropic/output-parser.js +159 -0
  40. package/src/providers/base-provider.js +181 -0
  41. package/src/providers/capabilities.js +51 -0
  42. package/src/providers/google/cli-builder.js +55 -0
  43. package/src/providers/google/index.js +116 -0
  44. package/src/providers/google/models.js +24 -0
  45. package/src/providers/google/output-parser.js +92 -0
  46. package/src/providers/index.js +75 -0
  47. package/src/providers/openai/cli-builder.js +122 -0
  48. package/src/providers/openai/index.js +135 -0
  49. package/src/providers/openai/models.js +21 -0
  50. package/src/providers/openai/output-parser.js +129 -0
  51. package/src/sub-cluster-wrapper.js +18 -3
  52. package/src/task-runner.js +8 -6
  53. package/src/tui/layout.js +20 -3
  54. package/task-lib/attachable-watcher.js +80 -78
  55. package/task-lib/claude-recovery.js +119 -0
  56. package/task-lib/commands/list.js +1 -1
  57. package/task-lib/commands/resume.js +3 -2
  58. package/task-lib/commands/run.js +12 -3
  59. package/task-lib/runner.js +59 -38
  60. package/task-lib/scheduler.js +2 -2
  61. package/task-lib/store.js +43 -30
  62. package/task-lib/watcher.js +81 -62
package/cli/index.js CHANGED
@@ -21,7 +21,6 @@ const os = require('os');
21
21
  const chalk = require('chalk');
22
22
  const Orchestrator = require('../src/orchestrator');
23
23
  const { setupCompletion } = require('../lib/completion');
24
- const { parseChunk } = require('../lib/stream-json-parser');
25
24
  const { formatWatchMode } = require('./message-formatters-watch');
26
25
  const {
27
26
  formatAgentLifecycle,
@@ -46,8 +45,11 @@ const {
46
45
  coerceValue,
47
46
  DEFAULT_SETTINGS,
48
47
  } = require('../lib/settings');
48
+ const { normalizeProviderName } = require('../lib/provider-names');
49
+ const { getProvider, parseProviderChunk } = require('../src/providers');
49
50
  const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
50
51
  const { requirePreflight } = require('../src/preflight');
52
+ const { providersCommand, setDefaultCommand, setupCommand } = require('./commands/providers');
51
53
  // Setup wizard removed - use: zeroshot settings set <key> <value>
52
54
  const { checkForUpdates } = require('./lib/update-checker');
53
55
  const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
@@ -73,9 +75,7 @@ let activeStatusFooter = null;
73
75
  * @param {...any} args - Arguments to print (like console.log)
74
76
  */
75
77
  function safePrint(...args) {
76
- const text = args.map(arg =>
77
- typeof arg === 'string' ? arg : String(arg)
78
- ).join(' ');
78
+ const text = args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' ');
79
79
 
80
80
  if (activeStatusFooter) {
81
81
  activeStatusFooter.print(text + '\n');
@@ -191,14 +191,23 @@ function parseMountSpecs(specs) {
191
191
  // Lazy-loaded orchestrator (quiet by default) - created on first use
192
192
  /** @type {import('../src/orchestrator') | null} */
193
193
  let _orchestrator = null;
194
+ /** @type {Promise<import('../src/orchestrator')> | null} */
195
+ let _orchestratorPromise = null;
194
196
  /**
195
- * @returns {import('../src/orchestrator')}
197
+ * @returns {Promise<import('../src/orchestrator')>}
196
198
  */
197
199
  function getOrchestrator() {
198
- if (!_orchestrator) {
199
- _orchestrator = new Orchestrator({ quiet: true });
200
+ if (_orchestrator) {
201
+ return Promise.resolve(_orchestrator);
200
202
  }
201
- return _orchestrator;
203
+ // Use a promise to prevent multiple concurrent initializations
204
+ if (!_orchestratorPromise) {
205
+ _orchestratorPromise = Orchestrator.create({ quiet: true }).then((orch) => {
206
+ _orchestrator = orch;
207
+ return orch;
208
+ });
209
+ }
210
+ return _orchestratorPromise;
202
211
  }
203
212
 
204
213
  /**
@@ -394,7 +403,7 @@ if (shouldShowBanner) {
394
403
 
395
404
  program
396
405
  .name('zeroshot')
397
- .description('Multi-agent orchestration and task management for Claude')
406
+ .description('Multi-agent orchestration and task management for Claude, Codex, and Gemini')
398
407
  .version(require('../package.json').version)
399
408
  .option('-q, --quiet', 'Suppress prompts (first-run wizard, update checks)')
400
409
  .addHelpText(
@@ -402,9 +411,10 @@ program
402
411
  `
403
412
  Examples:
404
413
  ${chalk.cyan('zeroshot run 123 --ship')} Full automation: isolated + auto-merge PR
405
- ${chalk.cyan('zeroshot run 123')} Run cluster and attach to first agent
406
- ${chalk.cyan('zeroshot run 123 -d')} Run cluster in background (detached)
407
- ${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster on plain text task
414
+ ${chalk.cyan('zeroshot run 123')} Run cluster from GitHub issue
415
+ ${chalk.cyan('zeroshot run feature.md')} Run cluster from markdown file
416
+ ${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster from plain text
417
+ ${chalk.cyan('zeroshot run 123 -d')} Run in background (detached)
408
418
  ${chalk.cyan('zeroshot run 123 --docker')} Run in Docker container (safe for e2e tests)
409
419
  ${chalk.cyan('zeroshot task run "Fix the bug"')} Run single-agent background task
410
420
  ${chalk.cyan('zeroshot list')} List all tasks and clusters
@@ -421,6 +431,7 @@ Examples:
421
431
  ${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
422
432
  ${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (maxModel, config, etc.)
423
433
  ${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., maxModel haiku)
434
+ ${chalk.cyan('zeroshot providers')} Show provider status and defaults
424
435
  ${chalk.cyan('zeroshot config list')} List available cluster configs
425
436
  ${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
426
437
  ${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
@@ -441,7 +452,7 @@ Shell completion:
441
452
  // Run command - CLUSTER with auto-detection
442
453
  program
443
454
  .command('run <input>')
444
- .description('Start a multi-agent cluster (auto-detects GitHub issue or plain text)')
455
+ .description('Start a multi-agent cluster (GitHub issue, markdown file, or plain text)')
445
456
  .option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
446
457
  .option('--docker', 'Run cluster inside Docker container (full isolation)')
447
458
  .option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
@@ -453,13 +464,24 @@ program
453
464
  '--strict-schema',
454
465
  'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
455
466
  )
456
- .option('--pr', 'Create PR for human review (uses worktree isolation by default, use --docker for Docker)')
457
- .option('--ship', 'Full automation: worktree isolation + PR + auto-merge (use --docker for Docker)')
467
+ .option(
468
+ '--pr',
469
+ 'Create PR for human review (uses worktree isolation by default, use --docker for Docker)'
470
+ )
471
+ .option(
472
+ '--ship',
473
+ 'Full automation: worktree isolation + PR + auto-merge (use --docker for Docker)'
474
+ )
458
475
  .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
476
+ .option('--provider <provider>', 'Override all agents to use a provider (claude, codex, gemini)')
477
+ .option('--model <model>', 'Override all agent models (provider-specific model id)')
459
478
  .option('-d, --detach', 'Run in background (default: attach to first agent)')
460
479
  .option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
461
480
  .option('--no-mounts', 'Disable all Docker credential mounts')
462
- .option('--container-home <path>', 'Container home directory for $HOME expansion (default: /root)')
481
+ .option(
482
+ '--container-home <path>',
483
+ 'Container home directory for $HOME expansion (default: /root)'
484
+ )
463
485
  .addHelpText(
464
486
  'after',
465
487
  `
@@ -506,6 +528,10 @@ Input formats:
506
528
  else if (inputArg.match(/^[\w-]+\/[\w-]+#\d+$/)) {
507
529
  input.issue = inputArg;
508
530
  }
531
+ // Check if it's a markdown file (.md or .markdown)
532
+ else if (/\.(md|markdown)$/i.test(inputArg)) {
533
+ input.file = inputArg;
534
+ }
509
535
  // Otherwise, treat as plain text
510
536
  else {
511
537
  input.text = inputArg;
@@ -514,11 +540,16 @@ Input formats:
514
540
  // === PREFLIGHT CHECKS ===
515
541
  // Validate all dependencies BEFORE starting anything
516
542
  // This gives users clear, actionable error messages upfront
543
+ const settings = loadSettings();
544
+ const providerOverride = normalizeProviderName(
545
+ options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
546
+ );
517
547
  const preflightOptions = {
518
548
  requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
519
549
  requireDocker: options.docker, // Docker required for --docker mode
520
550
  requireGit: options.worktree, // Git required for worktree isolation
521
551
  quiet: process.env.ZEROSHOT_DAEMON === '1', // Suppress success in daemon mode
552
+ provider: providerOverride,
522
553
  };
523
554
  requirePreflight(preflightOptions);
524
555
 
@@ -570,6 +601,8 @@ Input formats:
570
601
  ZEROSHOT_PR: options.pr ? '1' : '',
571
602
  ZEROSHOT_WORKTREE: options.worktree ? '1' : '',
572
603
  ZEROSHOT_WORKERS: options.workers?.toString() || '',
604
+ ZEROSHOT_MODEL: options.model || '',
605
+ ZEROSHOT_PROVIDER: options.provider || '',
573
606
  ZEROSHOT_CWD: targetCwd, // Explicit CWD for orchestrator
574
607
  },
575
608
  });
@@ -580,8 +613,6 @@ Input formats:
580
613
  }
581
614
 
582
615
  // === FOREGROUND MODE (default) or DAEMON CHILD ===
583
- // Load user settings
584
- const settings = loadSettings();
585
616
 
586
617
  // Use cluster ID from env (daemon mode) or generate new one (foreground mode)
587
618
  // IMPORTANT: Set env var so orchestrator picks it up
@@ -610,9 +641,24 @@ Input formats:
610
641
  }
611
642
 
612
643
  // Create orchestrator with clusterId override for foreground mode
613
- const orchestrator = getOrchestrator();
644
+ const orchestrator = await getOrchestrator();
614
645
  config = orchestrator.loadConfig(configPath);
615
646
 
647
+ if (!config.defaultProvider) {
648
+ config.defaultProvider = settings.defaultProvider || 'claude';
649
+ }
650
+ config.defaultProvider = normalizeProviderName(config.defaultProvider) || 'claude';
651
+
652
+ if (providerOverride) {
653
+ const provider = getProvider(providerOverride);
654
+ const providerSettings = settings.providerSettings?.[providerOverride] || {};
655
+ config.forceProvider = providerOverride;
656
+ config.defaultProvider = providerOverride;
657
+ config.forceLevel = providerSettings.defaultLevel || provider.getDefaultLevel();
658
+ config.defaultLevel = config.forceLevel;
659
+ console.log(chalk.dim(`Provider override: ${providerOverride} (all agents)`));
660
+ }
661
+
616
662
  // Track for global error handler cleanup
617
663
  activeClusterId = clusterId;
618
664
  orchestratorInstance = orchestrator;
@@ -639,19 +685,59 @@ Input formats:
639
685
  }
640
686
  }
641
687
 
688
+ // Apply model override to all agents (CLI > env)
689
+ const modelOverride = options.model || process.env.ZEROSHOT_MODEL;
690
+ if (modelOverride) {
691
+ const providerName = normalizeProviderName(
692
+ providerOverride || config.defaultProvider || settings.defaultProvider || 'claude'
693
+ );
694
+ const provider = getProvider(providerName);
695
+ const catalog = provider.getModelCatalog();
696
+
697
+ if (catalog && !catalog[modelOverride]) {
698
+ console.warn(
699
+ chalk.yellow(
700
+ `Warning: model override "${modelOverride}" is not in the ${providerName} catalog`
701
+ )
702
+ );
703
+ }
704
+
705
+ if (providerName === 'claude' && ['opus', 'sonnet', 'haiku'].includes(modelOverride)) {
706
+ const { validateModelAgainstMax } = require('../lib/settings');
707
+ try {
708
+ validateModelAgainstMax(modelOverride, settings.maxModel);
709
+ } catch (err) {
710
+ console.error(chalk.red(`Error: ${err.message}`));
711
+ process.exit(1);
712
+ }
713
+ }
714
+
715
+ // Override all agent models
716
+ for (const agent of config.agents) {
717
+ agent.model = modelOverride;
718
+ if (agent.modelRules) {
719
+ delete agent.modelRules;
720
+ }
721
+ }
722
+ console.log(chalk.dim(`Model override: ${modelOverride} (all agents)`));
723
+ }
724
+
642
725
  // Build start options (CLI flags > env vars > settings)
643
726
  // In foreground mode, use CLI options directly; in daemon mode, use env vars
644
727
  // CRITICAL: cwd must be passed to orchestrator for agent CWD propagation
645
728
  const targetCwd = process.env.ZEROSHOT_CWD || detectGitRepoRoot();
646
729
  const startOptions = {
730
+ clusterId,
647
731
  cwd: targetCwd, // Target working directory for agents
648
- isolation:
649
- options.docker || process.env.ZEROSHOT_DOCKER === '1' || settings.defaultDocker,
732
+ isolation: options.docker || process.env.ZEROSHOT_DOCKER === '1' || settings.defaultDocker,
650
733
  isolationImage: options.dockerImage || process.env.ZEROSHOT_DOCKER_IMAGE || undefined,
651
734
  worktree: options.worktree || process.env.ZEROSHOT_WORKTREE === '1',
652
735
  autoPr: options.pr || process.env.ZEROSHOT_PR === '1',
653
736
  autoMerge: process.env.ZEROSHOT_MERGE === '1',
654
737
  autoPush: process.env.ZEROSHOT_PUSH === '1',
738
+ // Model override (for dynamically added agents)
739
+ modelOverride: modelOverride || undefined,
740
+ providerOverride: providerOverride || undefined,
655
741
  // Docker mount options
656
742
  noMounts: options.noMounts || false,
657
743
  mounts: options.mount ? parseMountSpecs(options.mount) : undefined,
@@ -850,8 +936,12 @@ taskCmd
850
936
  .command('run <prompt>')
851
937
  .description('Run a single-agent background task')
852
938
  .option('-C, --cwd <path>', 'Working directory for task')
853
- .option('-r, --resume <sessionId>', 'Resume a specific Claude session')
854
- .option('-c, --continue', 'Continue the most recent session')
939
+ .option('--provider <provider>', 'Provider to use (claude, codex, gemini)')
940
+ .option('--model <model>', 'Model id override for the provider')
941
+ .option('--model-level <level>', 'Model level override (level1, level2, level3)')
942
+ .option('--reasoning-effort <effort>', 'Reasoning effort (low, medium, high, xhigh)')
943
+ .option('-r, --resume <sessionId>', 'Resume a specific Claude session (claude only)')
944
+ .option('-c, --continue', 'Continue the most recent Claude session (claude only)')
855
945
  .option(
856
946
  '-o, --output-format <format>',
857
947
  'Output format: stream-json (default), text, json',
@@ -862,11 +952,16 @@ taskCmd
862
952
  .action(async (prompt, options) => {
863
953
  try {
864
954
  // === PREFLIGHT CHECKS ===
865
- // Claude CLI must be installed and authenticated for task execution
955
+ // Provider CLI must be installed for task execution
956
+ const settings = loadSettings();
957
+ const providerOverride = normalizeProviderName(
958
+ options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
959
+ );
866
960
  requirePreflight({
867
961
  requireGh: false, // gh not needed for plain tasks
868
962
  requireDocker: false, // Docker not needed for plain tasks
869
963
  quiet: false,
964
+ provider: providerOverride,
870
965
  });
871
966
 
872
967
  // Dynamically import task command (ESM module)
@@ -925,8 +1020,8 @@ program
925
1020
  .action(async (options) => {
926
1021
  try {
927
1022
  // Get clusters
928
- const clusters = getOrchestrator().listClusters();
929
- const orchestrator = getOrchestrator();
1023
+ const clusters = (await getOrchestrator()).listClusters();
1024
+ const orchestrator = await getOrchestrator();
930
1025
 
931
1026
  // Enrich clusters with token data
932
1027
  const enrichedClusters = clusters.map((cluster) => {
@@ -992,7 +1087,8 @@ program
992
1087
  for (const cluster of enrichedClusters) {
993
1088
  const created = new Date(cluster.createdAt).toLocaleString();
994
1089
  const tokenDisplay = cluster.totalTokens > 0 ? cluster.totalTokens.toLocaleString() : '-';
995
- const costDisplay = cluster.totalCostUsd > 0 ? '$' + cluster.totalCostUsd.toFixed(3) : '-';
1090
+ const costDisplay =
1091
+ cluster.totalCostUsd > 0 ? '$' + cluster.totalCostUsd.toFixed(3) : '-';
996
1092
 
997
1093
  // Highlight zombie clusters in red
998
1094
  const stateDisplay =
@@ -1042,12 +1138,12 @@ program
1042
1138
 
1043
1139
  if (type === 'cluster') {
1044
1140
  // Show cluster status
1045
- const status = getOrchestrator().getStatus(id);
1141
+ const status = (await getOrchestrator()).getStatus(id);
1046
1142
 
1047
1143
  // Get token usage
1048
1144
  let tokensByRole = null;
1049
1145
  try {
1050
- const cluster = getOrchestrator().getCluster(id);
1146
+ const cluster = (await getOrchestrator()).getCluster(id);
1051
1147
  if (cluster?.messageBus) {
1052
1148
  tokensByRole = cluster.messageBus.getTokensByRole(id);
1053
1149
  }
@@ -1186,7 +1282,7 @@ program
1186
1282
 
1187
1283
  // === CLUSTER LOGS ===
1188
1284
  const limit = parseInt(options.limit);
1189
- const quietOrchestrator = new Orchestrator({ quiet: true });
1285
+ const quietOrchestrator = await Orchestrator.create({ quiet: true });
1190
1286
 
1191
1287
  // No ID: show/follow ALL clusters
1192
1288
  if (!id) {
@@ -1597,7 +1693,7 @@ program
1597
1693
  .action(async (clusterId) => {
1598
1694
  try {
1599
1695
  console.log(`Stopping cluster ${clusterId}...`);
1600
- await getOrchestrator().stop(clusterId);
1696
+ await (await getOrchestrator()).stop(clusterId);
1601
1697
  console.log('Cluster stopped successfully');
1602
1698
  } catch (error) {
1603
1699
  console.error('Error stopping cluster:', error.message);
@@ -1621,7 +1717,7 @@ program
1621
1717
 
1622
1718
  if (type === 'cluster') {
1623
1719
  console.log(`Killing cluster ${id}...`);
1624
- await getOrchestrator().kill(id);
1720
+ await (await getOrchestrator()).kill(id);
1625
1721
  console.log('Cluster killed successfully');
1626
1722
  } else {
1627
1723
  // Kill task
@@ -1749,15 +1845,13 @@ Key bindings:
1749
1845
 
1750
1846
  // Create orchestrator instance to query agent states
1751
1847
  // This loads the cluster from disk including its ledger and agents
1752
- const orchestrator = new Orchestrator({ quiet: true });
1848
+ const orchestrator = await Orchestrator.create({ quiet: true });
1753
1849
 
1754
1850
  try {
1755
1851
  const status = orchestrator.getStatus(id);
1756
1852
  // Agent is "active" if in any working state
1757
1853
  // Note: currentTaskId may be null briefly between TASK_STARTED and TASK_ID_ASSIGNED
1758
- const activeAgents = status.agents.filter(
1759
- (a) => ACTIVE_STATES.has(a.state)
1760
- );
1854
+ const activeAgents = status.agents.filter((a) => ACTIVE_STATES.has(a.state));
1761
1855
 
1762
1856
  if (activeAgents.length === 0) {
1763
1857
  console.error(chalk.yellow(`No agents currently executing tasks in cluster ${id}`));
@@ -1808,10 +1902,16 @@ Key bindings:
1808
1902
  if (!agent.currentTaskId) {
1809
1903
  if (ACTIVE_STATES.has(agent.state)) {
1810
1904
  // Agent is working but task ID not yet assigned
1811
- console.error(chalk.yellow(`Agent '${options.agent}' is working (state: ${agent.state}, task ID not yet assigned)`));
1905
+ console.error(
1906
+ chalk.yellow(
1907
+ `Agent '${options.agent}' is working (state: ${agent.state}, task ID not yet assigned)`
1908
+ )
1909
+ );
1812
1910
  console.log(chalk.dim('Try again in a moment...'));
1813
1911
  } else {
1814
- console.error(chalk.yellow(`Agent '${options.agent}' is not currently running a task`));
1912
+ console.error(
1913
+ chalk.yellow(`Agent '${options.agent}' is not currently running a task`)
1914
+ );
1815
1915
  console.log(chalk.dim(`State: ${agent.state}`));
1816
1916
  }
1817
1917
  return;
@@ -1917,7 +2017,7 @@ program
1917
2017
  .action(async (options) => {
1918
2018
  try {
1919
2019
  // Get counts first
1920
- const orchestrator = getOrchestrator();
2020
+ const orchestrator = await getOrchestrator();
1921
2021
  const clusters = orchestrator.listClusters();
1922
2022
  const runningClusters = clusters.filter(
1923
2023
  (c) => c.state === 'running' || c.state === 'initializing'
@@ -2148,17 +2248,24 @@ program
2148
2248
  // Check if cluster exists
2149
2249
  const cluster = orchestrator.getCluster(id);
2150
2250
 
2151
- // === PREFLIGHT CHECKS ===
2152
- // Claude CLI must be installed and authenticated
2153
- // Check if cluster uses isolation (needs Docker)
2154
- const requiresDocker = cluster?.isolation?.enabled || false;
2155
- requirePreflight({
2156
- requireGh: false, // Resume doesn't fetch new issues
2157
- requireDocker: requiresDocker,
2158
- quiet: false,
2159
- });
2251
+ const settings = loadSettings();
2160
2252
 
2161
2253
  if (cluster) {
2254
+ // === PREFLIGHT CHECKS ===
2255
+ // Provider CLI must be installed; Docker needed if isolation was used
2256
+ const requiresDocker = cluster?.isolation?.enabled || false;
2257
+ const providerName =
2258
+ cluster.config?.forceProvider ||
2259
+ cluster.config?.defaultProvider ||
2260
+ settings.defaultProvider;
2261
+
2262
+ requirePreflight({
2263
+ requireGh: false, // Resume doesn't fetch new issues
2264
+ requireDocker: requiresDocker,
2265
+ quiet: false,
2266
+ provider: providerName,
2267
+ });
2268
+
2162
2269
  // Resume cluster
2163
2270
  console.log(chalk.cyan(`Resuming cluster ${id}...`));
2164
2271
  const result = await orchestrator.resume(id, prompt);
@@ -2273,6 +2380,24 @@ program
2273
2380
 
2274
2381
  console.log(chalk.dim(`\nCluster ${id} completed.`));
2275
2382
  } else {
2383
+ let providerName = settings.defaultProvider;
2384
+ try {
2385
+ const { getTask } = await import('../task-lib/store.js');
2386
+ const task = getTask(id);
2387
+ if (task?.provider) {
2388
+ providerName = task.provider;
2389
+ }
2390
+ } catch {
2391
+ // If task store is unavailable, fall back to default provider
2392
+ }
2393
+
2394
+ requirePreflight({
2395
+ requireGh: false,
2396
+ requireDocker: false,
2397
+ quiet: false,
2398
+ provider: providerName,
2399
+ });
2400
+
2276
2401
  // Try resuming as task
2277
2402
  const { resumeTask } = await import('../task-lib/commands/resume.js');
2278
2403
  await resumeTask(id, prompt);
@@ -2525,7 +2650,7 @@ program
2525
2650
  .option('-y, --yes', 'Skip confirmation')
2526
2651
  .action(async (options) => {
2527
2652
  try {
2528
- const orchestrator = getOrchestrator();
2653
+ const orchestrator = await getOrchestrator();
2529
2654
 
2530
2655
  // Get counts first
2531
2656
  const clusters = orchestrator.listClusters();
@@ -2751,7 +2876,7 @@ program
2751
2876
  try {
2752
2877
  const TUI = require('../src/tui');
2753
2878
  const tui = new TUI({
2754
- orchestrator: getOrchestrator(),
2879
+ orchestrator: await getOrchestrator(),
2755
2880
  refreshRate: parseInt(options.refreshRate, 10),
2756
2881
  });
2757
2882
  await tui.start();
@@ -2866,7 +2991,11 @@ function formatSettingsList(settings, showUsage = false) {
2866
2991
  console.log(chalk.dim(' zeroshot settings set dockerMounts \'["gh","git","ssh","aws"]\''));
2867
2992
  console.log(chalk.dim(' zeroshot settings set dockerEnvPassthrough \'["AWS_*","TF_VAR_*"]\''));
2868
2993
  console.log('');
2869
- console.log(chalk.dim('Available mount presets: gh, git, ssh, aws, azure, kube, terraform, gcloud'));
2994
+ console.log(
2995
+ chalk.dim(
2996
+ 'Available mount presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini'
2997
+ )
2998
+ );
2870
2999
  console.log('');
2871
3000
  }
2872
3001
  }
@@ -2960,6 +3089,26 @@ settingsCmd.action(() => {
2960
3089
  formatSettingsList(settings, true);
2961
3090
  });
2962
3091
 
3092
+ // Providers management
3093
+ const providersCmd = program.command('providers').description('Manage AI providers');
3094
+ providersCmd.action(async () => {
3095
+ await providersCommand();
3096
+ });
3097
+
3098
+ providersCmd
3099
+ .command('set-default <provider>')
3100
+ .description('Set default provider (claude, codex, gemini)')
3101
+ .action(async (provider) => {
3102
+ await setDefaultCommand([provider]);
3103
+ });
3104
+
3105
+ providersCmd
3106
+ .command('setup <provider>')
3107
+ .description('Configure provider model levels and overrides')
3108
+ .action(async (provider) => {
3109
+ await setupCommand([provider]);
3110
+ });
3111
+
2963
3112
  // Update command
2964
3113
  program
2965
3114
  .command('update')
@@ -3882,7 +4031,10 @@ function renderMessagesToTerminal(clusterId, messages) {
3882
4031
  const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
3883
4032
  if (!content || !content.trim()) continue;
3884
4033
 
3885
- const events = parseChunk(content);
4034
+ const provider = normalizeProviderName(
4035
+ msg.content?.data?.provider || msg.sender_provider || 'claude'
4036
+ );
4037
+ const events = parseProviderChunk(provider, content);
3886
4038
  for (const event of events) {
3887
4039
  switch (event.type) {
3888
4040
  case 'text':
@@ -4181,7 +4333,7 @@ const FILTERED_PATTERNS = [
4181
4333
  /^--- Following log/,
4182
4334
  /--- Following logs/,
4183
4335
  /Ctrl\+C to stop/,
4184
- /^=== Claude Task:/,
4336
+ /^=== (Claude|Codex|Gemini) Task:/,
4185
4337
  /^Started:/,
4186
4338
  /^Finished:/,
4187
4339
  /^Exit code:/,
@@ -4276,7 +4428,10 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4276
4428
  if (!content || !content.trim()) return;
4277
4429
 
4278
4430
  // Parse streaming JSON events using the parser
4279
- const events = parseChunk(content);
4431
+ const provider = normalizeProviderName(
4432
+ msg.content?.data?.provider || msg.sender_provider || 'claude'
4433
+ );
4434
+ const events = parseProviderChunk(provider, content);
4280
4435
 
4281
4436
  for (const event of events) {
4282
4437
  switch (event.type) {
@@ -4431,9 +4586,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4431
4586
 
4432
4587
  // IMPLEMENTATION_READY: milestone marker
4433
4588
  if (msg.topic === 'IMPLEMENTATION_READY') {
4434
- safePrint(
4435
- `${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
4436
- );
4589
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`);
4437
4590
  if (msg.content?.data?.commit) {
4438
4591
  safePrint(
4439
4592
  `${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
@@ -4487,7 +4640,10 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4487
4640
 
4488
4641
  // Main async entry point
4489
4642
  async function main() {
4490
- const isQuiet = process.argv.includes('-q') || process.argv.includes('--quiet') || process.env.NODE_ENV === 'test';
4643
+ const isQuiet =
4644
+ process.argv.includes('-q') ||
4645
+ process.argv.includes('--quiet') ||
4646
+ process.env.NODE_ENV === 'test';
4491
4647
 
4492
4648
  // Check for updates (non-blocking if offline)
4493
4649
  await checkForUpdates({ quiet: isQuiet });
@@ -10,6 +10,7 @@
10
10
 
11
11
  const readline = require('readline');
12
12
  const { loadSettings, saveSettings } = require('../../lib/settings');
13
+ const { detectProviders } = require('../../src/providers');
13
14
 
14
15
  /**
15
16
  * Print welcome banner
@@ -19,7 +20,7 @@ function printWelcome() {
19
20
  ╔═══════════════════════════════════════════════════════════════╗
20
21
  ║ ║
21
22
  ║ Welcome to Zeroshot! ║
22
- ║ Multi-agent orchestration for Claude
23
+ ║ Multi-agent orchestration engine
23
24
  ║ ║
24
25
  ║ Let's configure a few settings to get started. ║
25
26
  ║ ║
@@ -38,6 +39,37 @@ function createReadline() {
38
39
  });
39
40
  }
40
41
 
42
+ /**
43
+ * Prompt for provider selection
44
+ * @param {readline.Interface} rl
45
+ * @param {object} detected
46
+ * @returns {Promise<string>}
47
+ */
48
+ function promptProvider(rl, detected) {
49
+ console.log('\nWhich AI provider would you like to use by default?\n');
50
+
51
+ const available = Object.entries(detected).filter(([_, status]) => status.available);
52
+
53
+ if (available.length === 0) {
54
+ console.log('No AI CLI tools detected. Please install one of:');
55
+ console.log(' - Claude Code: npm install -g @anthropic-ai/claude-code');
56
+ console.log(' - Codex CLI: npm install -g @openai/codex');
57
+ console.log(' - Gemini CLI: npm install -g @google/gemini-cli');
58
+ process.exit(1);
59
+ }
60
+
61
+ available.forEach(([name], i) => {
62
+ console.log(` ${i + 1}) ${name} (installed)`);
63
+ });
64
+
65
+ return new Promise((resolve) => {
66
+ rl.question('\nChoice [1]: ', (answer) => {
67
+ const idx = parseInt(answer) - 1 || 0;
68
+ resolve(available[idx]?.[0] || available[0][0]);
69
+ });
70
+ });
71
+ }
72
+
41
73
  /**
42
74
  * Prompt for model selection
43
75
  * @param {readline.Interface} rl
@@ -45,7 +77,7 @@ function createReadline() {
45
77
  */
46
78
  function promptModel(rl) {
47
79
  return new Promise((resolve) => {
48
- console.log('What is the maximum model agents can use? (cost ceiling)\n');
80
+ console.log('What is the maximum Claude model agents can use? (cost ceiling)\n');
49
81
  console.log(' 1) sonnet - Agents can use sonnet or haiku (recommended)');
50
82
  console.log(' 2) opus - Agents can use opus, sonnet, or haiku');
51
83
  console.log(' 3) haiku - Agents can only use haiku (lowest cost)\n');
@@ -95,7 +127,8 @@ function printComplete(settings) {
95
127
  ╚═══════════════════════════════════════════════════════════════╝
96
128
 
97
129
  Your settings:
98
- Max model: ${settings.maxModel} (agents can use this model or lower)
130
+ Provider: ${settings.defaultProvider}
131
+ • Max model: ${settings.maxModel} (Claude ceiling)
99
132
  • Auto-updates: ${settings.autoCheckUpdates ? 'enabled' : 'disabled'}
100
133
 
101
134
  Change anytime with: zeroshot settings set <key> <value>
@@ -144,6 +177,10 @@ async function checkFirstRun(options = {}) {
144
177
  const rl = createReadline();
145
178
 
146
179
  try {
180
+ const detected = await detectProviders();
181
+ const provider = await promptProvider(rl, detected);
182
+ settings.defaultProvider = provider;
183
+
147
184
  // Model ceiling selection
148
185
  const model = await promptModel(rl);
149
186
  settings.maxModel = model;