@covibes/zeroshot 2.1.0 → 3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,78 @@
1
+ # [3.0.0](https://github.com/covibes/zeroshot/compare/v2.1.0...v3.0.0) (2025-12-29)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **isolation:** replace busy-wait with async/await for parallel copy ([c8afbf0](https://github.com/covibes/zeroshot/commit/c8afbf00927ce939af633406c47a928507c339c4)), closes [#21](https://github.com/covibes/zeroshot/issues/21)
7
+ * **security:** escape shell arguments in Docker commands ([43476ad](https://github.com/covibes/zeroshot/commit/43476adfb3c67634d478b4dd53d52a6afb42b297))
8
+ * shell injection prevention and test reliability improvements ([45254f7](https://github.com/covibes/zeroshot/commit/45254f7f75b027ba43f6e16fa3668960d4b77f97))
9
+ * **status-footer:** use decimal display for interpolated metrics ([#26](https://github.com/covibes/zeroshot/issues/26)) ([73ce673](https://github.com/covibes/zeroshot/commit/73ce67376078f97faefe6724e32ff34619f33374))
10
+
11
+
12
+ ### Features
13
+
14
+ * **cli:** change default model ceiling to opus ([#28](https://github.com/covibes/zeroshot/issues/28)) ([1810be3](https://github.com/covibes/zeroshot/commit/1810be3a6a2cbfbb4d3aefa711c32f9ff9718f5a))
15
+ * **cli:** change default model ceiling to opus + fix worktree flag cascade ([#29](https://github.com/covibes/zeroshot/issues/29)) ([eaa30b0](https://github.com/covibes/zeroshot/commit/eaa30b06baf381c4fb7306d08fcd2d4e980de002))
16
+ * **cli:** consolidate StatusFooter for logs -f mode + add blinking agent indicator ([fe2722d](https://github.com/covibes/zeroshot/commit/fe2722d157e04048b56368e2c0ffcd7052604f36))
17
+ * real-time metrics via interpolation + maxModel cost ceiling ([#24](https://github.com/covibes/zeroshot/issues/24)) ([f1db466](https://github.com/covibes/zeroshot/commit/f1db46691eca592de67e399aca18f6db3e94d628)), closes [#21](https://github.com/covibes/zeroshot/issues/21)
18
+ * **settings:** replace defaultModel with maxModel cost ceiling ([#25](https://github.com/covibes/zeroshot/issues/25)) ([9877dad](https://github.com/covibes/zeroshot/commit/9877dadad890f78b3af1404b0341da392f6f4bb7)), closes [#23](https://github.com/covibes/zeroshot/issues/23)
19
+ * **validation:** add Phase 5 template variable validation ([#27](https://github.com/covibes/zeroshot/issues/27)) ([5e5e7c6](https://github.com/covibes/zeroshot/commit/5e5e7c6ab2a11ba23a3600d101a9c9c7de02569e))
20
+
21
+
22
+ ### Performance Improvements
23
+
24
+ * **isolation:** optimize startup with 4 key improvements ([f28f89c](https://github.com/covibes/zeroshot/commit/f28f89c36ac98c341484124bbaffee745818dffa)), closes [#20](https://github.com/covibes/zeroshot/issues/20) [#21](https://github.com/covibes/zeroshot/issues/21) [#22](https://github.com/covibes/zeroshot/issues/22) [#23](https://github.com/covibes/zeroshot/issues/23) [#20](https://github.com/covibes/zeroshot/issues/20) [#21](https://github.com/covibes/zeroshot/issues/21) [#22](https://github.com/covibes/zeroshot/issues/22) [#23](https://github.com/covibes/zeroshot/issues/23)
25
+
26
+
27
+ ### BREAKING CHANGES
28
+
29
+ * None
30
+ * **settings:** defaultModel setting renamed to maxModel
31
+ * defaultModel setting renamed to maxModel
32
+
33
+ * feat(status-footer): implement real-time metrics via interpolation
34
+
35
+ Replace blocking 1s metrics polling with background sampling + interpolation:
36
+ - Sample actual metrics every 500ms (non-blocking background)
37
+ - Display updates every 100ms (10 fps - appears continuous)
38
+ - Values smoothly drift toward targets via lerp (15% per tick)
39
+ - CPU and RAM interpolate; Network is cumulative (no interpolation)
40
+
41
+ Result: Real-time seeming monitoring while reducing actual polling.
42
+
43
+ šŸ¤– Generated with [Claude Code](https://claude.com/claude-code)
44
+
45
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
46
+
47
+ * feat(debug-workflow): harden investigator/fixer/tester for senior-dev quality
48
+
49
+ Implements 7 hardening changes to ensure debug-workflow produces
50
+ trustworthy output without manual code review:
51
+
52
+ **Investigator:**
53
+ - Structured rootCauses schema requiring proof each is fundamental
54
+ - Mandatory similarPatternLocations field from codebase-wide scan
55
+ - Prompt requires documenting WHY each cause is root (not symptom)
56
+
57
+ **Fixer:**
58
+ - Mandatory root cause mapping (each cause → specific fix)
59
+ - Mandatory test addition with escape hatch for valid justifications
60
+ - Must fix ALL similar pattern locations, not just original failure
61
+
62
+ **Tester:**
63
+ - Structured verification schema with commandResult, rootCauseVerification,
64
+ similarLocationVerification, testVerification, regressionCheck
65
+ - Comprehensive checklist: A (command), B (root causes), C (similar locs),
66
+ D (test quality), E (regression via smart tiering)
67
+ - Explicit forbidden rationalizations and approval criteria
68
+
69
+ Result: Workflow now blocks incomplete work, band-aid fixes, missing tests,
70
+ and ignored similar bugs. Output can be trusted.
71
+
72
+ šŸ¤– Generated with [Claude Code](https://claude.com/claude-code)
73
+
74
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
75
+
1
76
  # [2.1.0](https://github.com/covibes/zeroshot/compare/v2.0.0...v2.1.0) (2025-12-29)
2
77
 
3
78
 
package/README.md CHANGED
@@ -70,10 +70,11 @@ gh auth login
70
70
  zeroshot run 123 # Run on GitHub issue
71
71
  zeroshot run "Add dark mode" # Run from description
72
72
 
73
- # Automation levels (cascading: --ship → --pr → --isolation)
74
- zeroshot run 123 --isolation # Docker isolation, no PR
75
- zeroshot run 123 --pr # Isolation + PR (human reviews)
76
- zeroshot run 123 --ship # Isolation + PR + auto-merge (full automation)
73
+ # Automation levels (cascading: --ship → --pr → --worktree)
74
+ zeroshot run 123 --docker # Docker isolation (full container)
75
+ zeroshot run 123 --worktree # Git worktree isolation (lightweight)
76
+ zeroshot run 123 --pr # Worktree + PR (human reviews)
77
+ zeroshot run 123 --ship # Worktree + PR + auto-merge (full automation)
77
78
 
78
79
  # Background mode
79
80
  zeroshot run 123 -d # Detached/daemon
@@ -331,13 +332,23 @@ zeroshot resume cluster-bold-panther
331
332
 
332
333
  ---
333
334
 
334
- ## Docker Isolation
335
+ ## Isolation Modes
336
+
337
+ ### Git Worktree (Default for --pr/--ship)
338
+
339
+ ```bash
340
+ zeroshot 123 --worktree
341
+ ```
342
+
343
+ Lightweight isolation using git worktree. Creates a separate working directory with its own branch. Fast (<1s setup), no Docker required. Auto-enabled with `--pr` and `--ship`.
344
+
345
+ ### Docker Container
335
346
 
336
347
  ```bash
337
- zeroshot 123 --isolation
348
+ zeroshot 123 --docker
338
349
  ```
339
350
 
340
- Runs in a fresh container. Your workspace stays untouched. Good for risky experiments.
351
+ Full isolation in a fresh container. Your workspace stays untouched. Good for risky experiments or parallel agents.
341
352
 
342
353
  ---
343
354
 
@@ -356,7 +367,7 @@ Runs in a fresh container. Your workspace stays untouched. Good for risky experi
356
367
  | `claude: command not found` | `npm i -g @anthropic-ai/claude-code && claude auth login` |
357
368
  | `gh: command not found` | [Install GitHub CLI](https://cli.github.com/) |
358
369
  | CLI frozen for minutes | Normal - agents use JSON schema output, can't stream partial results |
359
- | `--isolation` fails | Docker must be running: `docker ps` to verify |
370
+ | `--docker` fails | Docker must be running: `docker ps` to verify |
360
371
  | Cluster stuck | `zeroshot resume <id>` to continue with guidance |
361
372
  | Agent keeps failing | Check `zeroshot logs <id>` for actual error |
362
373
  | `zeroshot: command not found` | `npm install -g @covibes/zeroshot` |
package/cli/index.js CHANGED
@@ -386,7 +386,7 @@ Examples:
386
386
  ${chalk.cyan('zeroshot run 123')} Run cluster and attach to first agent
387
387
  ${chalk.cyan('zeroshot run 123 -d')} Run cluster in background (detached)
388
388
  ${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster on plain text task
389
- ${chalk.cyan('zeroshot run 123 --isolation')} Run in Docker container (safe for e2e tests)
389
+ ${chalk.cyan('zeroshot run 123 --docker')} Run in Docker container (safe for e2e tests)
390
390
  ${chalk.cyan('zeroshot task run "Fix the bug"')} Run single-agent background task
391
391
  ${chalk.cyan('zeroshot list')} List all tasks and clusters
392
392
  ${chalk.cyan('zeroshot task list')} List tasks only
@@ -400,17 +400,18 @@ Examples:
400
400
  ${chalk.cyan('zeroshot kill <id>')} Kill a running task or cluster
401
401
  ${chalk.cyan('zeroshot purge')} Kill all processes and delete all data (with confirmation)
402
402
  ${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
403
- ${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (default model, config, etc.)
404
- ${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., defaultModel haiku)
403
+ ${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (maxModel, config, etc.)
404
+ ${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., maxModel haiku)
405
405
  ${chalk.cyan('zeroshot config list')} List available cluster configs
406
406
  ${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
407
407
  ${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
408
408
 
409
- Automation levels (cascading: --ship → --pr → --isolation):
409
+ Automation levels (cascading: --ship → --pr → --worktree):
410
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)
411
+ ${chalk.yellow('zeroshot run 123 --docker')} → Docker isolation, no PR
412
+ ${chalk.yellow('zeroshot run 123 --worktree')} → Git worktree isolation, no PR
413
+ ${chalk.yellow('zeroshot run 123 --pr')} → Worktree + PR (human reviews)
414
+ ${chalk.yellow('zeroshot run 123 --ship')} → Worktree + PR + auto-merge (full automation)
414
415
  ${chalk.yellow('zeroshot task run')} → Single-agent background task (simpler, faster)
415
416
 
416
417
  Shell completion:
@@ -423,18 +424,18 @@ program
423
424
  .command('run <input>')
424
425
  .description('Start a multi-agent cluster (auto-detects GitHub issue or plain text)')
425
426
  .option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
426
- .option('-m, --model <model>', 'Model for all agents: opus, sonnet, haiku (default: from config)')
427
- .option('--isolation', 'Run cluster inside Docker container (for e2e testing)')
427
+ .option('--docker', 'Run cluster inside Docker container (full isolation)')
428
+ .option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
428
429
  .option(
429
- '--isolation-image <image>',
430
- 'Docker image for isolation (default: zeroshot-cluster-base)'
430
+ '--docker-image <image>',
431
+ 'Docker image for --docker mode (default: zeroshot-cluster-base)'
431
432
  )
432
433
  .option(
433
434
  '--strict-schema',
434
435
  'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
435
436
  )
436
- .option('--pr', 'Create PR for human review (auto-enables --isolation)')
437
- .option('--ship', 'Full automation: isolation + PR + auto-merge')
437
+ .option('--pr', 'Create PR for human review (uses worktree isolation by default, use --docker for Docker)')
438
+ .option('--ship', 'Full automation: worktree isolation + PR + auto-merge (use --docker for Docker)')
438
439
  .option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
439
440
  .option('-d, --detach', 'Run in background (default: attach to first agent)')
440
441
  .addHelpText(
@@ -449,15 +450,23 @@ Input formats:
449
450
  )
450
451
  .action(async (inputArg, options) => {
451
452
  try {
452
- // Cascading flag implications: --ship → --pr → --isolation
453
- // --ship = full automation (isolation + PR + auto-merge)
453
+ // Cascading flag implications: --ship → --pr → worktree (unless --docker)
454
+ // --ship = full automation (worktree isolation + PR + auto-merge)
454
455
  if (options.ship) {
455
456
  options.pr = true;
456
- options.isolation = true;
457
+ // Use worktree by default, Docker only if explicitly requested
458
+ if (!options.docker) {
459
+ options.worktree = true;
460
+ }
461
+ }
462
+ // --pr = PR for human review (worktree by default, Docker if requested)
463
+ if (options.pr && !options.docker && !options.worktree) {
464
+ options.worktree = true;
457
465
  }
458
- // --pr = PR for human review (auto-enables isolation)
459
- if (options.pr) {
460
- options.isolation = true;
466
+
467
+ // Mutual exclusivity: --docker explicitly disables worktree
468
+ if (options.docker) {
469
+ options.worktree = false;
461
470
  }
462
471
 
463
472
  // Auto-detect input type
@@ -485,7 +494,8 @@ Input formats:
485
494
  // This gives users clear, actionable error messages upfront
486
495
  const preflightOptions = {
487
496
  requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
488
- requireDocker: options.isolation, // Docker required for isolation mode
497
+ requireDocker: options.docker, // Docker required for --docker mode
498
+ requireGit: options.worktree, // Git required for worktree isolation
489
499
  quiet: process.env.CREW_DAEMON === '1', // Suppress success in daemon mode
490
500
  };
491
501
  requirePreflight(preflightOptions);
@@ -503,8 +513,8 @@ Input formats:
503
513
  const clusterId = generateName('cluster');
504
514
 
505
515
  // Output cluster ID and help
506
- if (options.isolation) {
507
- console.log(`Started ${clusterId} (isolated)`);
516
+ if (options.docker) {
517
+ console.log(`Started ${clusterId} (docker)`);
508
518
  } else {
509
519
  console.log(`Started ${clusterId}`);
510
520
  }
@@ -533,10 +543,10 @@ Input formats:
533
543
  ...process.env,
534
544
  CREW_DAEMON: '1',
535
545
  CREW_CLUSTER_ID: clusterId,
536
- CREW_MODEL: options.model || '',
537
- CREW_ISOLATION: options.isolation ? '1' : '',
538
- CREW_ISOLATION_IMAGE: options.isolationImage || '',
546
+ CREW_DOCKER: options.docker ? '1' : '',
547
+ CREW_DOCKER_IMAGE: options.dockerImage || '',
539
548
  CREW_PR: options.pr ? '1' : '',
549
+ CREW_WORKTREE: options.worktree ? '1' : '',
540
550
  CREW_WORKERS: options.workers?.toString() || '',
541
551
  CREW_CWD: targetCwd, // Explicit CWD for orchestrator
542
552
  },
@@ -587,8 +597,10 @@ Input formats:
587
597
 
588
598
  // In foreground mode, show startup info
589
599
  if (!process.env.CREW_DAEMON) {
590
- if (options.isolation) {
591
- console.log(`Starting ${clusterId} (isolated)`);
600
+ if (options.docker) {
601
+ console.log(`Starting ${clusterId} (docker)`);
602
+ } else if (options.worktree) {
603
+ console.log(`Starting ${clusterId} (worktree)`);
592
604
  } else {
593
605
  console.log(`Starting ${clusterId}`);
594
606
  }
@@ -596,17 +608,6 @@ Input formats:
596
608
  console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
597
609
  }
598
610
 
599
- // Override model (CLI > settings > config)
600
- const modelOverride = process.env.CREW_MODEL || options.model || settings.defaultModel;
601
- if (modelOverride) {
602
- for (const agent of config.agents) {
603
- // Only override if agent doesn't already specify a model
604
- if (!agent.model || modelOverride) {
605
- agent.model = modelOverride;
606
- }
607
- }
608
- }
609
-
610
611
  // Apply strictSchema setting to all agents (CLI > env > settings)
611
612
  const strictSchema =
612
613
  options.strictSchema || process.env.CREW_STRICT_SCHEMA === '1' || settings.strictSchema;
@@ -623,8 +624,9 @@ Input formats:
623
624
  const startOptions = {
624
625
  cwd: targetCwd, // Target working directory for agents
625
626
  isolation:
626
- options.isolation || process.env.CREW_ISOLATION === '1' || settings.defaultIsolation,
627
- isolationImage: options.isolationImage || process.env.CREW_ISOLATION_IMAGE || undefined,
627
+ options.docker || process.env.CREW_DOCKER === '1' || settings.defaultDocker,
628
+ isolationImage: options.dockerImage || process.env.CREW_DOCKER_IMAGE || undefined,
629
+ worktree: options.worktree || process.env.CREW_WORKTREE === '1',
628
630
  autoPr: options.pr || process.env.CREW_PR === '1',
629
631
  autoMerge: process.env.CREW_MERGE === '1',
630
632
  autoPush: process.env.CREW_PUSH === '1',
@@ -822,10 +824,6 @@ taskCmd
822
824
  .command('run <prompt>')
823
825
  .description('Run a single-agent background task')
824
826
  .option('-C, --cwd <path>', 'Working directory for task')
825
- .option(
826
- '-m, --model <model>',
827
- 'Model to use: opus, sonnet, haiku (default: sonnet or ANTHROPIC_MODEL env)'
828
- )
829
827
  .option('-r, --resume <sessionId>', 'Resume a specific Claude session')
830
828
  .option('-c, --continue', 'Continue the most recent session')
831
829
  .option(
@@ -1269,11 +1267,23 @@ program
1269
1267
  clusterStates.set(c.id, c.state);
1270
1268
  }
1271
1269
 
1272
- // Track agent states from AGENT_LIFECYCLE messages (cross-process compatible)
1273
- const agentStates = new Map(); // agent -> { state, timestamp }
1274
-
1275
- // Track if status line is currently displayed (to clear before printing logs)
1276
- let statusLineShown = false;
1270
+ // === STATUS FOOTER: Live agent monitoring (same as foreground mode) ===
1271
+ // Shows CPU, memory, network metrics for all agents at bottom of terminal
1272
+ let statusFooter = null;
1273
+ if ((options.follow || options.watch) && process.stdout.isTTY) {
1274
+ statusFooter = new StatusFooter({
1275
+ refreshInterval: 1000,
1276
+ enabled: true,
1277
+ });
1278
+ // Set first cluster as the active one (for display purposes)
1279
+ if (allClusters.length > 0) {
1280
+ statusFooter.setCluster(allClusters[0].id);
1281
+ statusFooter.setClusterState(clusterStates.get(allClusters[0].id) || 'running');
1282
+ }
1283
+ // Set module-level reference so safePrint/safeWrite route through footer
1284
+ activeStatusFooter = statusFooter;
1285
+ statusFooter.start();
1286
+ }
1277
1287
 
1278
1288
  // Buffered message handler - collects messages and sorts by timestamp
1279
1289
  const flushMessages = () => {
@@ -1287,19 +1297,46 @@ program
1287
1297
  if (msg.topic === 'AGENT_OUTPUT' && msg.sender) {
1288
1298
  sendersWithOutput.add(msg.sender);
1289
1299
  }
1290
- // Track agent state from AGENT_LIFECYCLE messages
1291
- if (msg.topic === 'AGENT_LIFECYCLE' && msg.sender && msg.content?.data?.state) {
1292
- agentStates.set(msg.sender, {
1293
- state: msg.content.data.state,
1294
- model: msg.sender_model, // sender_model is always set by agent-wrapper._publish
1295
- timestamp: msg.timestamp || Date.now(),
1296
- });
1297
- }
1298
1300
 
1299
- // Clear status line before printing message
1300
- if (statusLineShown) {
1301
- process.stdout.write('\r' + ' '.repeat(120) + '\r');
1302
- statusLineShown = false;
1301
+ // Update StatusFooter from polled AGENT_LIFECYCLE messages (cross-process)
1302
+ if (msg.topic === 'AGENT_LIFECYCLE' && statusFooter) {
1303
+ const data = msg.content?.data || {};
1304
+ const event = data.event;
1305
+ const agentId = data.agent || msg.sender;
1306
+
1307
+ if (event === 'STARTED') {
1308
+ statusFooter.updateAgent({
1309
+ id: agentId,
1310
+ state: 'idle',
1311
+ pid: null,
1312
+ iteration: data.iteration || 0,
1313
+ });
1314
+ } else if (event === 'TASK_STARTED') {
1315
+ statusFooter.updateAgent({
1316
+ id: agentId,
1317
+ state: 'executing',
1318
+ pid: statusFooter.agents.get(agentId)?.pid || null,
1319
+ iteration: data.iteration || 0,
1320
+ });
1321
+ } else if (event === 'PROCESS_SPAWNED') {
1322
+ // Got the PID - update the agent with it for CPU/memory metrics
1323
+ const current = statusFooter.agents.get(agentId) || { state: 'executing', iteration: 0 };
1324
+ statusFooter.updateAgent({
1325
+ id: agentId,
1326
+ state: current.state,
1327
+ pid: data.pid,
1328
+ iteration: current.iteration,
1329
+ });
1330
+ } else if (event === 'TASK_COMPLETED' || event === 'TASK_FAILED') {
1331
+ statusFooter.updateAgent({
1332
+ id: agentId,
1333
+ state: 'idle',
1334
+ pid: null,
1335
+ iteration: data.iteration || 0,
1336
+ });
1337
+ } else if (event === 'STOPPED') {
1338
+ statusFooter.removeAgent(agentId);
1339
+ }
1303
1340
  }
1304
1341
 
1305
1342
  const isActive = clusterStates.get(msg.cluster_id) === 'running';
@@ -1322,51 +1359,6 @@ program
1322
1359
  // Flush buffer every 250ms
1323
1360
  const flushInterval = setInterval(flushMessages, 250);
1324
1361
 
1325
- // Blinking status indicator (follow/watch mode) - uses AGENT_LIFECYCLE state
1326
- let blinkState = false;
1327
- let statusInterval = null;
1328
- if (options.follow || options.watch) {
1329
- statusInterval = setInterval(() => {
1330
- blinkState = !blinkState;
1331
-
1332
- // Get active agents from tracked states
1333
- const activeList = [];
1334
- for (const [agentId, info] of agentStates.entries()) {
1335
- // Agent is active if not idle and not stopped
1336
- if (info.state !== 'idle' && info.state !== 'stopped') {
1337
- activeList.push({
1338
- id: agentId,
1339
- state: info.state,
1340
- model: info.model,
1341
- });
1342
- }
1343
- }
1344
-
1345
- // Build status line - only show when agents are actively working
1346
- if (activeList.length > 0) {
1347
- const indicator = blinkState ? chalk.yellow('ā—') : chalk.dim('ā—‹');
1348
- const agents = activeList
1349
- .map((a) => {
1350
- // Show state only for non-standard states (error, etc.)
1351
- const showState = a.state === 'error';
1352
- const stateLabel = showState ? chalk.red(` (${a.state})`) : '';
1353
- // Always show model
1354
- const modelLabel = a.model ? chalk.dim(` [${a.model}]`) : '';
1355
- return getColorForSender(a.id)(a.id) + modelLabel + stateLabel;
1356
- })
1357
- .join(', ');
1358
- process.stdout.write(`\r${indicator} Active: ${agents}` + ' '.repeat(20));
1359
- statusLineShown = true;
1360
- } else {
1361
- // Clear status line when no agents actively working
1362
- if (statusLineShown) {
1363
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
1364
- statusLineShown = false;
1365
- }
1366
- }
1367
- }, 500);
1368
- }
1369
-
1370
1362
  for (const clusterInfo of allClusters) {
1371
1363
  const cluster = quietOrchestrator.getCluster(clusterInfo.id);
1372
1364
  if (cluster) {
@@ -1399,13 +1391,13 @@ program
1399
1391
 
1400
1392
  keepProcessAlive(() => {
1401
1393
  clearInterval(flushInterval);
1402
- if (statusInterval) clearInterval(statusInterval);
1403
1394
  flushMessages();
1404
1395
  stopPollers.forEach((stop) => stop());
1405
1396
  stopWatching();
1406
- // Clear status line on exit
1407
- if (statusLineShown) {
1408
- process.stdout.write('\r' + ' '.repeat(120) + '\r');
1397
+ // Stop status footer and restore terminal
1398
+ if (statusFooter) {
1399
+ statusFooter.stop();
1400
+ activeStatusFooter = null;
1409
1401
  }
1410
1402
  // Restore terminal title
1411
1403
  restoreTerminalTitle();
@@ -2833,6 +2825,48 @@ settingsCmd.action(() => {
2833
2825
  console.log('');
2834
2826
  });
2835
2827
 
2828
+ // Update command
2829
+ program
2830
+ .command('update')
2831
+ .description('Update zeroshot to the latest version')
2832
+ .option('--check', 'Check for updates without installing')
2833
+ .action(async (options) => {
2834
+ const {
2835
+ getCurrentVersion,
2836
+ fetchLatestVersion,
2837
+ isNewerVersion,
2838
+ runUpdate,
2839
+ } = require('./lib/update-checker');
2840
+
2841
+ const currentVersion = getCurrentVersion();
2842
+ console.log(chalk.dim(`Current version: ${currentVersion}`));
2843
+ console.log(chalk.dim('Checking for updates...'));
2844
+
2845
+ const latestVersion = await fetchLatestVersion();
2846
+
2847
+ if (!latestVersion) {
2848
+ console.error(chalk.red('Failed to check for updates. Check your internet connection.'));
2849
+ process.exit(1);
2850
+ }
2851
+
2852
+ console.log(chalk.dim(`Latest version: ${latestVersion}`));
2853
+
2854
+ if (!isNewerVersion(currentVersion, latestVersion)) {
2855
+ console.log(chalk.green('\nāœ“ You are already on the latest version!'));
2856
+ return;
2857
+ }
2858
+
2859
+ console.log(chalk.yellow(`\nšŸ“¦ Update available: ${currentVersion} → ${latestVersion}`));
2860
+
2861
+ if (options.check) {
2862
+ console.log(chalk.dim('\nRun `zeroshot update` to install the update.'));
2863
+ return;
2864
+ }
2865
+
2866
+ const success = await runUpdate();
2867
+ process.exit(success ? 0 : 1);
2868
+ });
2869
+
2836
2870
  // Config visualization commands
2837
2871
  const configCmd = program.command('config').description('Manage and visualize cluster configs');
2838
2872
 
@@ -4319,7 +4353,8 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4319
4353
  // Main async entry point
4320
4354
  async function main() {
4321
4355
  // First-run setup wizard (blocks on first use only)
4322
- const isQuiet = process.argv.includes('-q') || process.argv.includes('--quiet');
4356
+ // CRITICAL: Auto-enable quiet mode in test environment to prevent stdin hangs
4357
+ const isQuiet = process.argv.includes('-q') || process.argv.includes('--quiet') || process.env.NODE_ENV === 'test';
4323
4358
  await checkFirstRun({ quiet: isQuiet });
4324
4359
 
4325
4360
  // Check for updates (non-blocking if offline)
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Interactive setup on first use:
5
5
  * - Welcome banner
6
- * - Default model selection (sonnet/opus/haiku)
6
+ * - Max model ceiling selection (sonnet/opus/haiku)
7
7
  * - Auto-update preference
8
8
  * - Marks setup as complete
9
9
  */
@@ -45,13 +45,13 @@ function createReadline() {
45
45
  */
46
46
  function promptModel(rl) {
47
47
  return new Promise((resolve) => {
48
- console.log('Which Claude model should agents use by default?\n');
49
- console.log(' 1) sonnet - Fast & capable (recommended)');
50
- console.log(' 2) opus - Most capable, slower');
51
- console.log(' 3) haiku - Fastest, for simple tasks\n');
48
+ console.log('What is the maximum model agents can use? (cost ceiling)\n');
49
+ console.log(' 1) sonnet - Agents can use sonnet or haiku (recommended)');
50
+ console.log(' 2) opus - Agents can use opus, sonnet, or haiku');
51
+ console.log(' 3) haiku - Agents can only use haiku (lowest cost)\n');
52
52
 
53
- rl.question('Enter 1, 2, or 3 [1]: ', (answer) => {
54
- const choice = answer.trim() || '1';
53
+ rl.question('Enter 1, 2, or 3 [2]: ', (answer) => {
54
+ const choice = answer.trim() || '2';
55
55
  switch (choice) {
56
56
  case '2':
57
57
  resolve('opus');
@@ -95,8 +95,8 @@ function printComplete(settings) {
95
95
  ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
96
96
 
97
97
  Your settings:
98
- • Default model: ${settings.defaultModel}
99
- • Auto-updates: ${settings.autoCheckUpdates ? 'enabled' : 'disabled'}
98
+ • Max model: ${settings.maxModel} (agents can use this model or lower)
99
+ • Auto-updates: ${settings.autoCheckUpdates ? 'enabled' : 'disabled'}
100
100
 
101
101
  Change anytime with: zeroshot settings set <key> <value>
102
102
 
@@ -144,9 +144,9 @@ async function checkFirstRun(options = {}) {
144
144
  const rl = createReadline();
145
145
 
146
146
  try {
147
- // Model selection
147
+ // Model ceiling selection
148
148
  const model = await promptModel(rl);
149
- settings.defaultModel = model;
149
+ settings.maxModel = model;
150
150
 
151
151
  // Auto-update preference
152
152
  const autoUpdate = await promptAutoUpdate(rl);
@@ -225,10 +225,11 @@ async function checkForUpdates(options = {}) {
225
225
 
226
226
  module.exports = {
227
227
  checkForUpdates,
228
- // Exported for testing
228
+ // Exported for testing and CLI update command
229
229
  getCurrentVersion,
230
230
  isNewerVersion,
231
231
  fetchLatestVersion,
232
+ runUpdate,
232
233
  shouldCheckForUpdates,
233
234
  CHECK_INTERVAL_MS,
234
235
  };