@the-open-engine/zeroshot 6.31.3 → 6.32.1

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 (47) hide show
  1. package/README.md +66 -98
  2. package/cli/index.js +251 -252
  3. package/cli/lib/setup-provider-readiness.js +86 -0
  4. package/cli/lib/setup-scanner-worker.js +120 -0
  5. package/cli/lib/setup-scanner.js +185 -0
  6. package/cli/lib/setup-wizard-input.js +146 -0
  7. package/cli/lib/setup-wizard-model.js +205 -0
  8. package/cli/lib/setup-wizard-plan-view.js +157 -0
  9. package/cli/lib/setup-wizard-scan-view.js +144 -0
  10. package/cli/lib/setup-wizard-terminal.js +237 -0
  11. package/cli/lib/setup-wizard-view.js +180 -0
  12. package/cli/lib/setup-wizard.js +281 -0
  13. package/cli/message-formatters-normal.js +14 -18
  14. package/cli/message-formatters-watch.js +53 -141
  15. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/codex.js +8 -2
  17. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  18. package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
  19. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  20. package/lib/agent-cli-provider/provider-registry.js +13 -3
  21. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  22. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
  24. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  25. package/lib/agent-cli-provider/types.d.ts +2 -0
  26. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/types.js.map +1 -1
  28. package/lib/completion.js +102 -153
  29. package/lib/settings.js +10 -2
  30. package/lib/setup-apply.js +62 -55
  31. package/lib/setup-plan.js +32 -52
  32. package/lib/start-cluster.js +65 -25
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +3 -3
  35. package/scripts/postinstall.js +54 -0
  36. package/src/agent/agent-lifecycle.js +11 -1
  37. package/src/agent/agent-task-executor.js +25 -7
  38. package/src/agent/structured-output-error.js +42 -0
  39. package/src/agent-cli-provider/adapters/codex.ts +8 -12
  40. package/src/agent-cli-provider/provider-registry.ts +15 -3
  41. package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
  42. package/src/agent-cli-provider/types.ts +2 -0
  43. package/src/preflight.js +27 -1
  44. package/src/status-footer.js +19 -12
  45. package/task-lib/commands/list.js +90 -78
  46. package/task-lib/commands/status.js +97 -40
  47. package/task-lib/effective-status.js +52 -0
package/cli/index.js CHANGED
@@ -22,7 +22,7 @@ const { URL } = require('url');
22
22
  const chalk = require('chalk');
23
23
  const Orchestrator = require('../src/orchestrator');
24
24
  const { setupCompletion } = require('../lib/completion');
25
- const { resolveRunMode, describeRunMode } = require('../lib/run-mode');
25
+ const { resolveRunMode, runModeFromPlan, describeRunMode } = require('../lib/run-mode');
26
26
  const { formatWatchMode } = require('./message-formatters-watch');
27
27
  const {
28
28
  formatAgentLifecycle,
@@ -35,11 +35,7 @@ const {
35
35
  formatClusterFailed,
36
36
  formatGenericMessage,
37
37
  } = require('./message-formatters-normal');
38
- const {
39
- getColorForSender,
40
- buildMessagePrefix,
41
- buildClusterPrefix,
42
- } = require('./message-formatter-utils');
38
+ const { getColorForSender, buildMessagePrefix } = require('./message-formatter-utils');
43
39
  const {
44
40
  loadSettings,
45
41
  mutateSettings,
@@ -74,6 +70,7 @@ const {
74
70
  startClusterFromFile,
75
71
  startClusterFromIssue,
76
72
  startClusterFromText,
73
+ resolveEffectiveRunPlan,
77
74
  } = require('../lib/start-cluster');
78
75
  const { requirePreflight } = require('../src/preflight');
79
76
  const {
@@ -92,6 +89,7 @@ const {
92
89
  waitForResumeOwnership,
93
90
  } = require('../lib/detached-startup');
94
91
  const { isProcessRunning: isClusterProcessAlive } = require('../lib/process-liveness');
92
+ const { runSetupWizard } = require('./lib/setup-wizard');
95
93
  const {
96
94
  checkForUpdates,
97
95
  isAutomaticUpdateEligible,
@@ -117,30 +115,41 @@ const PROVIDER_CHOICES = VALID_PROVIDERS.join(', ');
117
115
  /** @type {import('../src/status-footer').StatusFooter | null} */
118
116
  let activeStatusFooter = null;
119
117
 
120
- /**
121
- * Safe print - routes through statusFooter when active to prevent garbling
122
- * @param {...any} args - Arguments to print (like console.log)
123
- */
124
- function safePrint(...args) {
125
- const text = args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' ');
118
+ function normalizeLineText(text) {
119
+ let line = String(text);
120
+ while (line.endsWith('\n')) {
121
+ line = line.slice(0, -1);
122
+ if (line.endsWith('\r')) line = line.slice(0, -1);
123
+ }
124
+ return line;
125
+ }
126
126
 
127
+ function printLine(text = '') {
128
+ const line = normalizeLineText(text);
127
129
  if (activeStatusFooter) {
128
- activeStatusFooter.print(text + '\n');
129
- } else {
130
- console.log(...args);
130
+ activeStatusFooter.print(line);
131
+ return;
131
132
  }
133
+ process.stdout.write(`${line}\n`);
132
134
  }
133
135
 
134
- /**
135
- * Safe write - routes through statusFooter when active
136
- * @param {string} text - Text to write
137
- */
138
- function safeWrite(text) {
136
+ function write(text) {
137
+ const chunk = String(text);
139
138
  if (activeStatusFooter) {
140
- activeStatusFooter.print(text);
141
- } else {
142
- process.stdout.write(text);
139
+ activeStatusFooter.write(chunk);
140
+ return;
143
141
  }
142
+ process.stdout.write(chunk);
143
+ }
144
+
145
+ const liveOutputWriter = Object.freeze({ printLine, write });
146
+
147
+ function safePrint(...args) {
148
+ printLine(args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' '));
149
+ }
150
+
151
+ function safeWrite(text) {
152
+ write(text);
144
153
  }
145
154
 
146
155
  /**
@@ -198,56 +207,43 @@ process.on('unhandledRejection', (reason) => {
198
207
  // Package root directory (for resolving default config paths)
199
208
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
200
209
 
201
- function normalizeRunOptions(options) {
202
- if (options.ship) {
203
- options.pr = true;
204
- if (!options.docker) {
205
- options.worktree = true;
206
- }
207
- }
208
- if (options.pr && !options.docker && !options.worktree) {
209
- options.worktree = true;
210
- }
211
- if (options.docker) {
212
- options.worktree = false;
213
- }
214
- // autoMerge is NOT stored here — it is derived from the run plan (delivery ===
215
- // 'ship') at every consumer. Writing it here unconditionally would clobber an
216
- // explicit autoMerge intent (e.g. a future `--auto-merge` flag) back to false.
217
- }
218
-
219
- async function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
220
- // Detect which issue provider tool is needed
210
+ async function runClusterPreflight({
211
+ input,
212
+ options,
213
+ plan,
214
+ providerOverride,
215
+ settings,
216
+ forceProvider,
217
+ deps = {},
218
+ }) {
221
219
  let issueProvider = null;
222
220
  let targetHost = null;
223
221
 
224
222
  if (input.issue) {
225
223
  const { detectProvider: detectIssueProvider } = require('../src/issue-providers');
226
224
  const ProviderClass = detectIssueProvider(input.issue, settings, forceProvider);
227
- if (ProviderClass) {
228
- issueProvider = ProviderClass.id;
229
- }
230
-
231
- // Extract hostname from URL input for auth checks
232
- // This ensures we check auth for the target host, not the current git repo
225
+ if (ProviderClass) issueProvider = ProviderClass.id;
233
226
  if (/^https?:\/\//.test(input.issue)) {
234
227
  try {
235
- const url = new URL(input.issue);
236
- targetHost = url.hostname;
228
+ targetHost = new URL(input.issue).hostname;
237
229
  } catch {
238
- // Invalid URL - let provider handle the error
230
+ // Provider parsing reports malformed issue URLs.
239
231
  }
240
232
  }
241
233
  }
242
234
 
243
- await requirePreflight({
244
- requireGh: issueProvider === 'github', // gh CLI required for GitHub
245
- requireDocker: options.docker,
246
- requireGit: options.worktree,
235
+ const effectivePlan = plan || resolveEffectiveRunPlan(options, settings);
236
+ const preflight = deps.requirePreflight || requirePreflight;
237
+ await preflight({
238
+ requireGh: issueProvider === 'github',
239
+ requireDocker: effectivePlan.isolation === 'docker',
240
+ requireGit: effectivePlan.isolation === 'worktree',
241
+ autoPr: effectivePlan.delivery !== 'none',
247
242
  quiet: process.env.ZEROSHOT_DAEMON === '1',
248
243
  provider: providerOverride,
249
- issueProvider, // Pass detected issue provider for tool checking
250
- targetHost, // Pass target host for multi-instance auth checks (e.g., GitLab self-hosted)
244
+ settings,
245
+ issueProvider,
246
+ targetHost,
251
247
  });
252
248
  }
253
249
 
@@ -255,8 +251,8 @@ function shouldRunDetached(options) {
255
251
  return options.detach && !process.env.ZEROSHOT_DAEMON;
256
252
  }
257
253
 
258
- function printDetachedClusterStart(options, clusterId, logPath) {
259
- const runMode = resolveRunMode(options);
254
+ function printDetachedClusterStart(plan, clusterId, logPath) {
255
+ const runMode = runModeFromPlan(plan);
260
256
  console.log(runMode ? `Started ${clusterId} (${runMode})` : `Started ${clusterId}`);
261
257
  if (logPath) {
262
258
  console.log(`Setup log: ${logPath}`);
@@ -328,7 +324,18 @@ function spawnDetachedChild(env, cwd, logFd) {
328
324
  return daemon;
329
325
  }
330
326
 
331
- async function spawnDetachedCluster(options, clusterId, stdinText) {
327
+ function applyRunPlanToOptions(options, plan) {
328
+ return {
329
+ ...options,
330
+ docker: plan.isolation === 'docker',
331
+ worktree: plan.isolation === 'worktree',
332
+ pr: plan.delivery === 'pr',
333
+ ship: plan.delivery === 'ship',
334
+ noIsolation: plan.isolation === 'none',
335
+ };
336
+ }
337
+
338
+ async function spawnDetachedCluster(options, plan, clusterId, stdinText) {
332
339
  const logFd = createDaemonLogFile(clusterId);
333
340
  const targetCwd = detectGitRepoRoot();
334
341
  const logPath = path.join(os.homedir(), '.zeroshot', `${clusterId}-daemon.log`);
@@ -346,7 +353,7 @@ async function spawnDetachedCluster(options, clusterId, stdinText) {
346
353
  cwd: targetCwd,
347
354
  });
348
355
  fs.closeSync(logFd);
349
- printDetachedClusterStart(options, clusterId, logPath);
356
+ printDetachedClusterStart(plan, clusterId, logPath);
350
357
  }
351
358
 
352
359
  // Resume's daemon env is deliberately NOT buildDaemonEnv: run-only keys like
@@ -407,11 +414,11 @@ function trackActiveCluster(clusterId, orchestrator) {
407
414
  orchestratorInstance = orchestrator;
408
415
  }
409
416
 
410
- function printForegroundStartInfo(options, clusterId, configName) {
417
+ function printForegroundStartInfo(plan, clusterId, configName) {
411
418
  if (process.env.ZEROSHOT_DAEMON) {
412
419
  return;
413
420
  }
414
- const runMode = resolveRunMode(options);
421
+ const runMode = runModeFromPlan(plan);
415
422
  console.log(runMode ? `Starting ${clusterId} (${runMode})` : `Starting ${clusterId}`);
416
423
  console.log(chalk.dim(`Config: ${configName}`));
417
424
  console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
@@ -656,11 +663,11 @@ function waitForClusterCompletion(orchestrator, clusterId, cleanup) {
656
663
  });
657
664
  }
658
665
 
659
- async function streamClusterInForeground(cluster, orchestrator, clusterId, options) {
666
+ async function streamClusterInForeground(cluster, orchestrator, clusterId, plan) {
660
667
  const sendersWithOutput = new Set();
661
668
  const processedMessageIds = new Set();
662
669
 
663
- const statusFooter = createStatusFooter(clusterId, cluster.messageBus, resolveRunMode(options));
670
+ const statusFooter = createStatusFooter(clusterId, cluster.messageBus, runModeFromPlan(plan));
664
671
  const handleLifecycleMessage = createLifecycleHandler(statusFooter);
665
672
  const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic(
666
673
  'AGENT_LIFECYCLE',
@@ -789,17 +796,6 @@ function printClusterTable(enrichedClusters) {
789
796
  }
790
797
  }
791
798
 
792
- async function tryGetTasksData(getTasksData, options) {
793
- if (typeof getTasksData !== 'function') {
794
- return [];
795
- }
796
- try {
797
- return await getTasksData(options);
798
- } catch {
799
- return [];
800
- }
801
- }
802
-
803
799
  function printListJson(enrichedClusters, tasks) {
804
800
  console.log(
805
801
  JSON.stringify(
@@ -813,6 +809,10 @@ function printListJson(enrichedClusters, tasks) {
813
809
  );
814
810
  }
815
811
 
812
+ function printJsonError(error) {
813
+ console.log(JSON.stringify({ error: error.message }, null, 2));
814
+ }
815
+
816
816
  function reportMissingId(id, options) {
817
817
  if (options.json) {
818
818
  console.log(JSON.stringify({ error: 'ID not found', id }, null, 2));
@@ -932,22 +932,11 @@ function printClusterStatusHuman(status, tokensByRole, clusterId) {
932
932
  printClusterAgents(status);
933
933
  }
934
934
 
935
- async function tryGetTaskStatusData(getStatusData, id) {
936
- if (typeof getStatusData !== 'function') {
937
- return null;
938
- }
939
- try {
940
- return await getStatusData(id);
941
- } catch {
942
- return null;
943
- }
944
- }
945
-
946
935
  async function showTaskStatus(id, options) {
947
936
  const { showStatus, getStatusData } = await import('../task-lib/commands/status.js');
948
937
  if (options.json) {
949
- const taskData = await tryGetTaskStatusData(getStatusData, id);
950
- console.log(JSON.stringify({ type: 'task', id, ...taskData }, null, 2));
938
+ const taskData = await getStatusData(id);
939
+ console.log(JSON.stringify({ type: 'task', ...taskData }, null, 2));
951
940
  return;
952
941
  }
953
942
  await showStatus(id);
@@ -2577,9 +2566,6 @@ function buildTaskLogMessage({ taskId, timestamp, jsonContent, cluster, agent, i
2577
2566
  };
2578
2567
  }
2579
2568
 
2580
- // Setup shell completion
2581
- setupCompletion();
2582
-
2583
2569
  // Banner disabled
2584
2570
  function showBanner() {
2585
2571
  // Banner removed for cleaner output
@@ -2600,48 +2586,18 @@ if (shouldShowBanner) {
2600
2586
 
2601
2587
  program
2602
2588
  .name('zeroshot')
2603
- .description('Multi-agent orchestration and task management for Claude, Codex, and Gemini')
2589
+ .description('Independent executor–verifier orchestration for software changes.')
2604
2590
  .version(require('../package.json').version)
2605
- .option('-q, --quiet', 'Skip automatic update checks')
2591
+ .option('-q, --quiet', 'Suppress prompts (first-run wizard, update checks)')
2592
+ .helpCommand(false)
2606
2593
  .addHelpText(
2607
2594
  'after',
2608
2595
  `
2609
2596
  Examples:
2610
- ${chalk.cyan('zeroshot run 123 --ship')} Full automation: isolated + auto-merge PR
2611
- ${chalk.cyan('zeroshot run 123')} Run cluster from GitHub issue
2612
- ${chalk.cyan('zeroshot run feature.md')} Run cluster from markdown file
2613
- ${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster from plain text
2614
- ${chalk.cyan('zeroshot run 123 -d')} Run in background (detached)
2615
- ${chalk.cyan('zeroshot run 123 --docker')} Run in Docker container (safe for e2e tests)
2616
- ${chalk.cyan('zeroshot task run "Fix the bug"')} Run single-agent background task
2617
- ${chalk.cyan('zeroshot list')} List all tasks and clusters
2618
- ${chalk.cyan('zeroshot task list')} List tasks only
2619
- ${chalk.cyan('zeroshot attach <id>')} Attach to running task (Ctrl+B d to detach)
2620
- ${chalk.cyan('zeroshot logs -f')} Stream logs in real-time (like tail -f)
2621
- ${chalk.cyan('zeroshot logs -w')} Watch cluster lifecycle and event summaries
2622
- ${chalk.cyan('zeroshot logs <id> -f')} Stream logs for specific cluster/task
2623
- ${chalk.cyan('zeroshot status <id>')} Detailed status of task or cluster
2624
- ${chalk.cyan('zeroshot finish <id>')} Convert cluster to completion task (creates and merges PR)
2625
- ${chalk.cyan('zeroshot kill <id>')} Kill a running task or cluster
2626
- ${chalk.cyan('zeroshot purge')} Kill all processes and delete all data (with confirmation)
2627
- ${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
2628
- ${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (maxModel, config, etc.)
2629
- ${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., maxModel haiku)
2630
- ${chalk.cyan('zeroshot providers')} Show provider status and defaults
2631
- ${chalk.cyan('zeroshot config list')} List available cluster configs
2632
- ${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
2633
- ${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
2634
-
2635
- Automation levels (cascading: --ship → --pr → --worktree):
2636
- ${chalk.yellow('zeroshot run 123')} → Local run, no isolation
2637
- ${chalk.yellow('zeroshot run 123 --docker')} → Docker isolation, no PR
2638
- ${chalk.yellow('zeroshot run 123 --worktree')} → Git worktree isolation, no PR
2639
- ${chalk.yellow('zeroshot run 123 --pr')} → Worktree + PR (human reviews)
2640
- ${chalk.yellow('zeroshot run 123 --ship')} → Worktree + PR + auto-merge (full automation)
2641
- ${chalk.yellow('zeroshot task run')} → Single-agent background task (simpler, faster)
2642
-
2643
- Shell completion:
2644
- ${chalk.dim('zeroshot --completion >> ~/.bashrc && source ~/.bashrc')}
2597
+ ${chalk.cyan('zeroshot')} Run guided setup or show help
2598
+ ${chalk.cyan('zeroshot run "Add tests"')} Start an explicit software-change run
2599
+ ${chalk.cyan('zeroshot list')} List tasks and clusters
2600
+ ${chalk.cyan('zeroshot logs <id> -f')} Follow a run
2645
2601
  `
2646
2602
  );
2647
2603
 
@@ -2649,20 +2605,34 @@ Shell completion:
2649
2605
 
2650
2606
  program
2651
2607
  .command('run <input>')
2608
+ .helpGroup('Start:')
2652
2609
  .description(
2653
2610
  'Start a multi-agent cluster (GitHub issue, markdown file, plain text, or "-" for stdin)'
2654
2611
  )
2612
+ .optionsGroup('Input:')
2655
2613
  .option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
2614
+ .option('-G, --github', 'Force GitHub as issue source')
2615
+ .option('-L, --gitlab', 'Force GitLab as issue source')
2616
+ .option('-J, --jira', 'Force Jira as issue source')
2617
+ .option('-D, --devops', 'Force Azure DevOps as issue source')
2618
+ .option('-N, --linear', 'Force Linear as issue source')
2619
+ .optionsGroup('Isolation:')
2656
2620
  .option('--docker', 'Run cluster inside Docker container (full isolation)')
2657
2621
  .option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
2622
+ .addOption(
2623
+ new Option('--no-isolation', 'Run in the current checkout without isolation').default(undefined)
2624
+ )
2658
2625
  .option(
2659
2626
  '--docker-image <image>',
2660
2627
  'Docker image for --docker mode (default: zeroshot-cluster-base)'
2661
2628
  )
2629
+ .option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
2630
+ .option('--no-mounts', 'Disable all Docker credential mounts')
2662
2631
  .option(
2663
- '--strict-schema',
2664
- 'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
2632
+ '--container-home <path>',
2633
+ 'Container home directory for $HOME expansion (default: /root)'
2665
2634
  )
2635
+ .optionsGroup('Delivery:')
2666
2636
  .option(
2667
2637
  '--pr',
2668
2638
  'Create PR for human review (uses worktree isolation by default, use --docker for Docker). Never auto-merges itself; a repo-side branch-protection auto-merge rule or merge queue may still merge the PR independently of zeroshot.'
@@ -2677,26 +2647,21 @@ program
2677
2647
  '--close-issue <mode>',
2678
2648
  'When to close issue after merge: auto|always|never (default: from .zeroshot/settings.json or never)'
2679
2649
  )
2680
- .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
2650
+ .optionsGroup('Provider:')
2681
2651
  .option('--provider <provider>', `Override all agents to use a provider (${PROVIDER_CHOICES})`)
2682
2652
  .option('--model <model>', 'Override all agent models (provider-specific model id)')
2653
+ .optionsGroup('Runtime:')
2654
+ .option(
2655
+ '--strict-schema',
2656
+ 'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
2657
+ )
2658
+ .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
2683
2659
  .option(
2684
2660
  '--sim <mode>',
2685
2661
  'Token-free simulation gate for templates (off|fast|deep). Default: fast',
2686
2662
  'fast'
2687
2663
  )
2688
- .option('-G, --github', 'Force GitHub as issue source')
2689
- .option('-L, --gitlab', 'Force GitLab as issue source')
2690
- .option('-J, --jira', 'Force Jira as issue source')
2691
- .option('-D, --devops', 'Force Azure DevOps as issue source')
2692
- .option('-N, --linear', 'Force Linear as issue source')
2693
2664
  .option('-d, --detach', 'Run in background (default: attach to first agent)')
2694
- .option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
2695
- .option('--no-mounts', 'Disable all Docker credential mounts')
2696
- .option(
2697
- '--container-home <path>',
2698
- 'Container home directory for $HOME expansion (default: /root)'
2699
- )
2700
2665
  .addHelpText(
2701
2666
  'after',
2702
2667
  `
@@ -2735,10 +2700,6 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2735
2700
  )
2736
2701
  .action(async (inputArg, options) => {
2737
2702
  try {
2738
- // Normalize options (--ship → --pr → --worktree flags)
2739
- normalizeRunOptions(options);
2740
-
2741
- // Determine force provider from CLI flags
2742
2703
  let forceProvider = null;
2743
2704
  if (options.github) forceProvider = 'github';
2744
2705
  else if (options.gitlab) forceProvider = 'gitlab';
@@ -2746,7 +2707,6 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2746
2707
  else if (options.devops) forceProvider = 'azure-devops';
2747
2708
  else if (options.linear) forceProvider = 'linear';
2748
2709
 
2749
- // Stdin input ('-'): read task body from stdin to avoid shell-quoting breakage
2750
2710
  let stdinText;
2751
2711
  if (isStdinInput(inputArg)) {
2752
2712
  if (process.env.ZEROSHOT_DAEMON === '1') {
@@ -2769,24 +2729,29 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2769
2729
  }
2770
2730
  }
2771
2731
 
2772
- // Auto-detect input type
2773
2732
  const settings = loadSettings();
2774
2733
  const input =
2775
2734
  stdinText !== undefined
2776
2735
  ? buildTextInput(stdinText)
2777
2736
  : detectRunInput(inputArg, settings, forceProvider);
2778
2737
  const providerOverride = resolveProviderOverride(options);
2738
+ const effectiveRunPlan = resolveEffectiveRunPlan(options, settings);
2739
+ const effectiveOptions = applyRunPlanToOptions(options, effectiveRunPlan);
2740
+
2741
+ await runClusterPreflight({
2742
+ input,
2743
+ options: effectiveOptions,
2744
+ providerOverride,
2745
+ settings,
2746
+ forceProvider,
2747
+ plan: effectiveRunPlan,
2748
+ });
2779
2749
 
2780
- // Preflight checks
2781
- await runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
2782
-
2783
- // Secondary preflight: token-free template simulation/validation
2784
- const simMode = String(options.sim || 'fast').toLowerCase();
2750
+ const simMode = String(effectiveOptions.sim || 'fast').toLowerCase();
2785
2751
  if (simMode !== 'off') {
2786
2752
  const { validateTemplates } = require('../src/template-validation');
2787
2753
  const templatesDir = path.join(PACKAGE_ROOT, 'cluster-templates');
2788
- const deep = simMode === 'deep';
2789
- const report = await validateTemplates({ templatesDir, deep });
2754
+ const report = await validateTemplates({ templatesDir, deep: simMode === 'deep' });
2790
2755
  if (!report.valid) {
2791
2756
  console.error('\n' + '='.repeat(60));
2792
2757
  console.error(`TEMPLATE VALIDATION FAILED (sim=${simMode})`);
@@ -2795,9 +2760,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2795
2760
  if (result.valid) continue;
2796
2761
  const rel = path.relative(process.cwd(), filePath);
2797
2762
  console.error(`\n❌ ${rel}`);
2798
- for (const err of result.errors) {
2799
- console.error(` ERROR: ${err}`);
2800
- }
2763
+ for (const err of result.errors) console.error(` ERROR: ${err}`);
2801
2764
  }
2802
2765
  console.error('\nFix template errors before running to avoid token burn.\n');
2803
2766
  process.exit(1);
@@ -2805,67 +2768,42 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2805
2768
  }
2806
2769
 
2807
2770
  const { generateName } = require('../src/name-generator');
2808
-
2809
- if (shouldRunDetached(options)) {
2771
+ if (shouldRunDetached(effectiveOptions)) {
2810
2772
  const clusterId = generateName('cluster');
2811
- await spawnDetachedCluster(options, clusterId, stdinText);
2773
+ await spawnDetachedCluster(effectiveOptions, effectiveRunPlan, clusterId, stdinText);
2812
2774
  return;
2813
2775
  }
2814
2776
 
2815
2777
  const clusterId = resolveClusterId(generateName);
2816
-
2817
- // === LOAD CONFIG ===
2818
- // Priority: CLI --config > settings.defaultConfig
2819
- const configName = resolveConfigName(options, settings);
2778
+ const configName = resolveConfigName(effectiveOptions, settings);
2820
2779
  const configPath = resolveConfigPath(configName);
2821
2780
  const orchestrator = await getOrchestrator();
2822
2781
  const config = loadClusterConfig(orchestrator, configPath, settings, providerOverride);
2823
2782
  trackActiveCluster(clusterId, orchestrator);
2824
- printForegroundStartInfo(options, clusterId, configName);
2783
+ printForegroundStartInfo(effectiveRunPlan, clusterId, configName);
2825
2784
 
2826
- const strictSchema = resolveStrictSchema(options, settings);
2785
+ const strictSchema = resolveStrictSchema(effectiveOptions, settings);
2827
2786
  applyStrictSchema(config, strictSchema);
2828
-
2829
- const modelOverride = resolveModelOverride(options);
2787
+ const modelOverride = resolveModelOverride(effectiveOptions);
2830
2788
  applyModelOverrideToConfig(config, modelOverride, providerOverride, settings);
2831
2789
 
2832
- let cluster = null;
2790
+ const startArgs = {
2791
+ orchestrator,
2792
+ config,
2793
+ settings,
2794
+ providerOverride,
2795
+ modelOverride,
2796
+ forceProvider,
2797
+ clusterId,
2798
+ options: effectiveOptions,
2799
+ };
2800
+ let cluster;
2833
2801
  if (input.text) {
2834
- cluster = await startClusterFromText({
2835
- orchestrator,
2836
- text: input.text,
2837
- config,
2838
- settings,
2839
- providerOverride,
2840
- modelOverride,
2841
- forceProvider,
2842
- clusterId,
2843
- options,
2844
- });
2802
+ cluster = await startClusterFromText({ ...startArgs, text: input.text });
2845
2803
  } else if (input.issue) {
2846
- cluster = await startClusterFromIssue({
2847
- orchestrator,
2848
- issue: input.issue,
2849
- config,
2850
- settings,
2851
- providerOverride,
2852
- modelOverride,
2853
- forceProvider,
2854
- clusterId,
2855
- options,
2856
- });
2804
+ cluster = await startClusterFromIssue({ ...startArgs, issue: input.issue });
2857
2805
  } else if (input.file) {
2858
- cluster = await startClusterFromFile({
2859
- orchestrator,
2860
- file: input.file,
2861
- config,
2862
- settings,
2863
- providerOverride,
2864
- modelOverride,
2865
- forceProvider,
2866
- clusterId,
2867
- options,
2868
- });
2806
+ cluster = await startClusterFromFile({ ...startArgs, file: input.file });
2869
2807
  } else {
2870
2808
  throw new Error(
2871
2809
  `Invalid run input for cluster ${clusterId}: expected text, issue, or file`
@@ -2873,15 +2811,12 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2873
2811
  }
2874
2812
 
2875
2813
  if (!process.env.ZEROSHOT_DAEMON) {
2876
- await streamClusterInForeground(cluster, orchestrator, clusterId, options);
2814
+ await streamClusterInForeground(cluster, orchestrator, clusterId, effectiveRunPlan);
2877
2815
  orchestrator.close();
2878
2816
  }
2879
-
2880
2817
  setupDaemonCleanup(orchestrator, clusterId);
2881
2818
  } catch (error) {
2882
2819
  if (error.code === 'DUPLICATE_CLUSTER') {
2883
- // Benign guard rejection, not a crash: nothing was allocated, so there is
2884
- // nothing to roll back. No stack trace, no "Error:" framing.
2885
2820
  if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
2886
2821
  try {
2887
2822
  await removeDetachedSetupCluster({
@@ -2919,10 +2854,14 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2919
2854
 
2920
2855
  // === TASK COMMANDS ===
2921
2856
  // Task run - single-agent background task
2922
- const taskCmd = program.command('task').description('Single-agent task management');
2857
+ const taskCmd = program
2858
+ .command('task')
2859
+ .helpGroup('Automation:')
2860
+ .description('Single-agent task management');
2923
2861
 
2924
2862
  const cmdproofCmd = program
2925
2863
  .command('cmdproof')
2864
+ .helpGroup('Configure:')
2926
2865
  .description('Run configured cmdproof command proofs');
2927
2866
 
2928
2867
  for (const mode of ['prove', 'verify', 'check']) {
@@ -3041,6 +2980,7 @@ taskCmd
3041
2980
  // List command - unified (shows both tasks and clusters)
3042
2981
  program
3043
2982
  .command('list')
2983
+ .helpGroup('Observe:')
3044
2984
  .alias('ls')
3045
2985
  .description('List all tasks and clusters')
3046
2986
  .option('-s, --status <status>', 'Filter tasks by status (running, completed, failed)')
@@ -3055,7 +2995,7 @@ program
3055
2995
  const { listTasks, getTasksData } = await import('../task-lib/commands/list.js');
3056
2996
 
3057
2997
  if (options.json) {
3058
- const tasks = await tryGetTasksData(getTasksData, options);
2998
+ const tasks = await getTasksData(options);
3059
2999
  printListJson(enrichedClusters, tasks);
3060
3000
  return;
3061
3001
  }
@@ -3065,7 +3005,11 @@ program
3065
3005
  console.log(chalk.bold('\n=== Tasks ==='));
3066
3006
  await listTasks(options);
3067
3007
  } catch (error) {
3068
- console.error('Error listing:', error.message);
3008
+ if (options.json) {
3009
+ printJsonError(error);
3010
+ } else {
3011
+ console.error('Error listing:', error.message);
3012
+ }
3069
3013
  process.exit(1);
3070
3014
  }
3071
3015
  });
@@ -3073,6 +3017,7 @@ program
3073
3017
  // Status command - smart (works for both tasks and clusters)
3074
3018
  program
3075
3019
  .command('status <id>')
3020
+ .helpGroup('Observe:')
3076
3021
  .description('Get detailed status of a task or cluster')
3077
3022
  .option('--json', 'Output as JSON')
3078
3023
  .action(async (id, options) => {
@@ -3100,7 +3045,7 @@ program
3100
3045
  await showTaskStatus(id, options);
3101
3046
  } catch (error) {
3102
3047
  if (options.json) {
3103
- console.log(JSON.stringify({ error: error.message }, null, 2));
3048
+ printJsonError(error);
3104
3049
  } else {
3105
3050
  console.error('Error getting status:', error.message);
3106
3051
  }
@@ -3110,6 +3055,7 @@ program
3110
3055
 
3111
3056
  program
3112
3057
  .command('inspect <id>')
3058
+ .helpGroup('Observe:')
3113
3059
  .description('Inspect live process activity for a task or cluster')
3114
3060
  .option('--json', 'Output as JSON')
3115
3061
  .option('--sample-ms <ms>', 'Sampling period for process activity checks', '1000')
@@ -3129,6 +3075,7 @@ program
3129
3075
  // Logs command - smart (works for both tasks and clusters)
3130
3076
  program
3131
3077
  .command('logs [id]')
3078
+ .helpGroup('Observe:')
3132
3079
  .description('View logs (omit ID for all clusters)')
3133
3080
  .option('-f, --follow', 'Follow logs in real-time (stream output like tail -f)')
3134
3081
  .option('-n, --limit <number>', 'Number of recent messages to show (default: 50)', '50')
@@ -3161,6 +3108,7 @@ program
3161
3108
  // Stop command (cluster-only)
3162
3109
  program
3163
3110
  .command('stop <cluster-id>')
3111
+ .helpGroup('Control:')
3164
3112
  .description('Stop a cluster gracefully')
3165
3113
  .action(async (clusterId) => {
3166
3114
  try {
@@ -3176,6 +3124,7 @@ program
3176
3124
  // Kill command - smart (works for both tasks and clusters)
3177
3125
  program
3178
3126
  .command('kill <id>')
3127
+ .helpGroup('Control:')
3179
3128
  .description('Kill a task or cluster')
3180
3129
  .action(async (id) => {
3181
3130
  try {
@@ -3205,6 +3154,7 @@ program
3205
3154
  // Attach command - tmux-style attach to running task or cluster agent
3206
3155
  program
3207
3156
  .command('attach [id]')
3157
+ .helpGroup('Observe:')
3208
3158
  .description('Attach to a running task or cluster agent (Ctrl+C to detach, task keeps running)')
3209
3159
  .option('-a, --agent <name>', 'Attach to specific agent in cluster (required for clusters)')
3210
3160
  .addHelpText(
@@ -3257,6 +3207,7 @@ Key bindings:
3257
3207
  // Kill-all command - kills all running tasks and clusters
3258
3208
  program
3259
3209
  .command('kill-all')
3210
+ .helpGroup('Maintenance:')
3260
3211
  .description('Kill all running tasks and clusters')
3261
3212
  .option('-y, --yes', 'Skip confirmation')
3262
3213
  .action(async (options) => {
@@ -3329,6 +3280,7 @@ program
3329
3280
  // Export command (cluster-only)
3330
3281
  program
3331
3282
  .command('export <cluster-id>')
3283
+ .helpGroup('Maintenance:')
3332
3284
  .description('Export cluster conversation')
3333
3285
  .option('-f, --format <format>', 'Export format: json, markdown, html', 'html')
3334
3286
  .option('-o, --output <file>', 'Output file (auto-generated for html)')
@@ -3443,6 +3395,7 @@ program
3443
3395
  // Resume task or cluster
3444
3396
  program
3445
3397
  .command('resume <id> [prompt]')
3398
+ .helpGroup('Control:')
3446
3399
  .description('Resume a failed task or cluster')
3447
3400
  .option('-d, --detach', 'Resume in background (daemon mode)')
3448
3401
  .action(async (id, prompt, options) => {
@@ -3686,6 +3639,7 @@ program
3686
3639
  // Finish cluster - convert to single-agent completion task
3687
3640
  program
3688
3641
  .command('finish <id>')
3642
+ .helpGroup('Control:')
3689
3643
  .description('Take existing cluster and create completion-focused task (creates PR and merges)')
3690
3644
  .option('-y, --yes', 'Skip confirmation if cluster is running')
3691
3645
  .action(async (id, options) => {
@@ -3724,6 +3678,7 @@ program
3724
3678
  // Clean tasks
3725
3679
  program
3726
3680
  .command('clean')
3681
+ .helpGroup('Maintenance:')
3727
3682
  .description('Remove old task records and logs')
3728
3683
  .option('-a, --all', 'Remove all tasks')
3729
3684
  .option('-c, --completed', 'Remove completed tasks')
@@ -3791,6 +3746,7 @@ async function detectAndReportCorruptedClusters(dryRun) {
3791
3746
 
3792
3747
  program
3793
3748
  .command('gc')
3749
+ .helpGroup('Maintenance:')
3794
3750
  .description('Clean up orphaned worktree directories and stale database files')
3795
3751
  .option('--dry-run', 'Show what would be removed without deleting')
3796
3752
  .action(async (options) => {
@@ -3803,10 +3759,11 @@ program
3803
3759
  }
3804
3760
  });
3805
3761
 
3806
- // Purge all runs (clusters + tasks) - NUCLEAR option
3762
+ // Purge all runs (clusters + tasks).
3807
3763
  program
3808
3764
  .command('purge')
3809
- .description('NUCLEAR: Kill all running processes and delete all data')
3765
+ .helpGroup('Maintenance:')
3766
+ .description('Kill all running tasks and clusters, then delete all Zeroshot run data')
3810
3767
  .option('-y, --yes', 'Skip confirmation')
3811
3768
  .action(async (options) => {
3812
3769
  try {
@@ -3842,6 +3799,7 @@ program
3842
3799
  // Schedule a task
3843
3800
  program
3844
3801
  .command('schedule <prompt>')
3802
+ .helpGroup('Automation:')
3845
3803
  .description('Create a recurring scheduled task')
3846
3804
  .option('-e, --every <interval>', 'Interval (e.g., "1h", "30m", "1d")')
3847
3805
  .option('--cron <expression>', 'Cron expression')
@@ -3859,6 +3817,7 @@ program
3859
3817
  // List schedules
3860
3818
  program
3861
3819
  .command('schedules')
3820
+ .helpGroup('Automation:')
3862
3821
  .description('List all scheduled tasks')
3863
3822
  .action(async () => {
3864
3823
  try {
@@ -3873,6 +3832,7 @@ program
3873
3832
  // Unschedule a task
3874
3833
  program
3875
3834
  .command('unschedule <scheduleId>')
3835
+ .helpGroup('Automation:')
3876
3836
  .description('Remove a scheduled task')
3877
3837
  .action(async (scheduleId) => {
3878
3838
  try {
@@ -3887,6 +3847,7 @@ program
3887
3847
  // Scheduler daemon management
3888
3848
  program
3889
3849
  .command('scheduler <action>')
3850
+ .helpGroup('Automation:')
3890
3851
  .description('Manage scheduler daemon (start, stop, status, logs)')
3891
3852
  .action(async (action) => {
3892
3853
  try {
@@ -3900,7 +3861,7 @@ program
3900
3861
 
3901
3862
  // Get log path (machine-readable)
3902
3863
  program
3903
- .command('get-log-path <taskId>')
3864
+ .command('get-log-path <taskId>', { hidden: true })
3904
3865
  .description('Output log file path for a task (machine-readable)')
3905
3866
  .action(async (taskId) => {
3906
3867
  try {
@@ -3914,7 +3875,7 @@ program
3914
3875
 
3915
3876
  // Resolve the durable task ownership receipt for an in-flight detached launch.
3916
3877
  program
3917
- .command('get-task-id-by-spawn-token <token>')
3878
+ .command('get-task-id-by-spawn-token <token>', { hidden: true })
3918
3879
  .description('Output task ID for an internal spawn ownership token (machine-readable)')
3919
3880
  .action(async (token) => {
3920
3881
  try {
@@ -3934,10 +3895,13 @@ function failTuiUnavailable() {
3934
3895
  process.exit(1);
3935
3896
  }
3936
3897
 
3937
- program.command('watch').description('TUI unavailable in this release').action(failTuiUnavailable);
3898
+ program
3899
+ .command('watch', { hidden: true })
3900
+ .description('TUI unavailable in this release')
3901
+ .action(failTuiUnavailable);
3938
3902
 
3939
3903
  program
3940
- .command('tui')
3904
+ .command('tui', { hidden: true })
3941
3905
  .description('TUI unavailable in this release')
3942
3906
  .allowExcessArguments(true)
3943
3907
  .allowUnknownOption(true)
@@ -3945,7 +3909,7 @@ program
3945
3909
 
3946
3910
  function registerTuiEntrypoint(commandName, providerName) {
3947
3911
  program
3948
- .command(commandName)
3912
+ .command(commandName, { hidden: true })
3949
3913
  .description(`TUI unavailable in this release (provider: ${providerName})`)
3950
3914
  .allowExcessArguments(true)
3951
3915
  .allowUnknownOption(true)
@@ -3957,12 +3921,11 @@ for (const providerName of VALID_PROVIDERS) {
3957
3921
  }
3958
3922
 
3959
3923
  // Settings management
3960
- const settingsCmd = program.command('settings').description('Manage zeroshot settings');
3961
- const INTERNAL_SETTINGS_KEYS = new Set(['lastUpdateCheckClaim', '_targets']);
3962
- // Fail closed while hosted commands are unpublished. Keeping these names out of the
3963
- // default `run` rewrite makes them unknown commands rather than local task input.
3964
- // Issue #920 deliberately has no CLI lifecycle or runtime execution surface.
3965
- const UNREGISTERED_HOSTED_COMMAND_NAMES = new Set(['target', 'capsule']);
3924
+ const settingsCmd = program
3925
+ .command('settings')
3926
+ .helpGroup('Configure:')
3927
+ .description('Manage zeroshot settings');
3928
+ const INTERNAL_SETTINGS_KEYS = new Set(['lastUpdateCheckClaim', 'setupVersion', '_targets']);
3966
3929
 
3967
3930
  function visibleSettingKeys() {
3968
3931
  return Object.keys(DEFAULT_SETTINGS).filter((key) => !INTERNAL_SETTINGS_KEYS.has(key));
@@ -4496,7 +4459,10 @@ settingsCmd.action(() => {
4496
4459
 
4497
4460
  // Hosted target commands intentionally are not registered on the stable CLI.
4498
4461
  // Providers management
4499
- const providersCmd = program.command('providers').description('Manage AI providers');
4462
+ const providersCmd = program
4463
+ .command('providers')
4464
+ .helpGroup('Configure:')
4465
+ .description('Manage AI providers');
4500
4466
  providersCmd.action(async () => {
4501
4467
  await providersCommand();
4502
4468
  });
@@ -4516,7 +4482,14 @@ providersCmd
4516
4482
  });
4517
4483
 
4518
4484
  // Setup wizard (read-only facts + setup contract; apply/undo/TTY wizard land separately)
4519
- const setupCmd = program.command('setup').description('Setup and configuration wizard');
4485
+ const setupCmd = program
4486
+ .command('setup')
4487
+ .helpGroup('Start:')
4488
+ .description('Setup and configuration wizard');
4489
+ setupCmd.action(async () => {
4490
+ const result = await runSetupWizard();
4491
+ process.exitCode = result.exitCode;
4492
+ });
4520
4493
  setupCmd
4521
4494
  .command('plan')
4522
4495
  .description('Show read-only setup facts and proposed contract (no writes)')
@@ -4580,6 +4553,7 @@ setupCmd
4580
4553
  // Update command
4581
4554
  program
4582
4555
  .command('update')
4556
+ .helpGroup('Maintenance:')
4583
4557
  .description('Update zeroshot to the latest version')
4584
4558
  .option('--check', 'Check for updates without installing')
4585
4559
  .action(async (options) => {
@@ -4620,7 +4594,10 @@ program
4620
4594
  });
4621
4595
 
4622
4596
  // Config visualization commands
4623
- const configCmd = program.command('config').description('Manage and visualize cluster configs');
4597
+ const configCmd = program
4598
+ .command('config')
4599
+ .helpGroup('Configure:')
4600
+ .description('Manage and visualize cluster configs');
4624
4601
 
4625
4602
  configCmd
4626
4603
  .command('list')
@@ -4731,7 +4708,10 @@ configCmd
4731
4708
  });
4732
4709
 
4733
4710
  // Agent library commands
4734
- const agentsCmd = program.command('agents').description('View available agent definitions');
4711
+ const agentsCmd = program
4712
+ .command('agents')
4713
+ .helpGroup('Configure:')
4714
+ .description('View available agent definitions');
4735
4715
 
4736
4716
  agentsCmd
4737
4717
  .command('list')
@@ -6006,14 +5986,15 @@ function formatAgentOutput(msg, prefix) {
6006
5986
  }
6007
5987
 
6008
5988
  const NORMAL_MESSAGE_HANDLERS = {
6009
- AGENT_LIFECYCLE: ({ msg, prefix }) => formatAgentLifecycle(msg, prefix),
6010
- AGENT_ERROR: ({ msg, prefix, timestamp }) => formatAgentErrorNormal(msg, prefix, timestamp),
5989
+ AGENT_LIFECYCLE: ({ msg, prefix }) => formatAgentLifecycle(msg, prefix, safePrint),
5990
+ AGENT_ERROR: ({ msg, prefix, timestamp }) =>
5991
+ formatAgentErrorNormal(msg, prefix, timestamp, safePrint),
6011
5992
  ISSUE_OPENED: ({ msg, prefix, timestamp }) =>
6012
- formatIssueOpenedNormal(msg, prefix, timestamp, shownNewTaskForCluster),
5993
+ formatIssueOpenedNormal(msg, prefix, timestamp, shownNewTaskForCluster, safePrint),
6013
5994
  IMPLEMENTATION_READY: ({ msg, prefix, timestamp }) =>
6014
- formatImplementationReadyNormal(msg, prefix, timestamp),
5995
+ formatImplementationReadyNormal(msg, prefix, timestamp, safePrint),
6015
5996
  VALIDATION_RESULT: ({ msg, prefix, timestamp }) =>
6016
- formatValidationResultNormal(msg, prefix, timestamp),
5997
+ formatValidationResultNormal(msg, prefix, timestamp, safePrint),
6017
5998
  PR_CREATED: ({ msg, prefix, timestamp }) => formatPrCreated(msg, prefix, timestamp, safePrint),
6018
5999
  CLUSTER_COMPLETE: ({ msg, prefix, timestamp }) =>
6019
6000
  formatClusterComplete(msg, prefix, timestamp, safePrint),
@@ -6030,8 +6011,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
6030
6011
 
6031
6012
  // Watch mode: delegate to watch mode formatter
6032
6013
  if (watchMode) {
6033
- const clusterPrefix = buildClusterPrefix(msg, isActive);
6034
- formatWatchMode(msg, clusterPrefix);
6014
+ formatWatchMode(msg, isActive, liveOutputWriter);
6035
6015
  return;
6036
6016
  }
6037
6017
 
@@ -6055,14 +6035,36 @@ function isStartupUpdateEligible(argv, options = {}) {
6055
6035
  });
6056
6036
  }
6057
6037
 
6058
- function applyDefaultCommand(args) {
6059
- const firstArg = args[0];
6060
- if (!firstArg || firstArg.startsWith('-')) return;
6061
-
6062
- const commandNames = program.commands.map((command) => command.name());
6063
- if (commandNames.includes(firstArg) || UNREGISTERED_HOSTED_COMMAND_NAMES.has(firstArg)) return;
6038
+ function shouldRunInitialSetup({ args, stdin, stdout, settingsExist }) {
6039
+ return (
6040
+ args.length === 0 &&
6041
+ !args.includes('--quiet') &&
6042
+ !settingsExist &&
6043
+ stdin.isTTY === true &&
6044
+ stdout.isTTY === true
6045
+ );
6046
+ }
6064
6047
 
6065
- process.argv.splice(2, 0, 'run');
6048
+ async function handleNoArgumentInvocation({
6049
+ args,
6050
+ stdin = process.stdin,
6051
+ stdout = process.stdout,
6052
+ settingsExist,
6053
+ runWizard = runSetupWizard,
6054
+ outputHelp = () => program.outputHelp(),
6055
+ setExitCode = (code) => {
6056
+ process.exitCode = code;
6057
+ },
6058
+ }) {
6059
+ if (args.length !== 0) return false;
6060
+ const hasSettings = settingsExist ?? settingsFileExists();
6061
+ if (shouldRunInitialSetup({ args, stdin, stdout, settingsExist: hasSettings })) {
6062
+ const result = await runWizard({ stdin, stdout });
6063
+ setExitCode(result.exitCode);
6064
+ } else {
6065
+ outputHelp();
6066
+ }
6067
+ return true;
6066
6068
  }
6067
6069
 
6068
6070
  // Main entry point
@@ -6092,14 +6094,8 @@ async function main() {
6092
6094
 
6093
6095
  const args = startupArgs;
6094
6096
 
6095
- if (args.length === 0) {
6096
- program.outputHelp();
6097
- return;
6098
- }
6099
-
6100
- // Preserve the default local run shorthand without treating hosted-only command
6101
- // names as task input in the stable parser.
6102
- applyDefaultCommand(args);
6097
+ if (await handleNoArgumentInvocation({ args })) return;
6098
+ setupCompletion(program);
6103
6099
 
6104
6100
  program.parse();
6105
6101
  }
@@ -6114,11 +6110,14 @@ if (require.main === module) {
6114
6110
 
6115
6111
  module.exports = {
6116
6112
  assertRequestedWebSearchCliAvailable,
6113
+ runClusterPreflight,
6117
6114
  applyModelOverrideToConfig,
6118
6115
  inspectAgentAttachment,
6119
6116
  printAttachableAgentList,
6120
6117
  renderRecentMessagesToTerminal,
6121
6118
  isStartupUpdateEligible,
6119
+ handleNoArgumentInvocation,
6120
+ shouldRunInitialSetup,
6122
6121
  resolveRunMode,
6123
6122
  killRunningClusters,
6124
6123
  };