switchman-dev 0.1.4 → 0.1.6

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/src/cli/index.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { program } from 'commander';
20
20
  import chalk from 'chalk';
21
21
  import ora from 'ora';
22
+ import { existsSync } from 'fs';
22
23
  import { join } from 'path';
23
24
  import { execSync, spawn } from 'child_process';
24
25
 
@@ -43,6 +44,16 @@ import { installGitHubActionsWorkflow, resolveGitHubOutputTargets, writeGitHubCi
43
44
  import { importCodeObjectsToStore, listCodeObjects, materializeCodeObjects, materializeSemanticIndex, updateCodeObjectSource } from '../core/semantic.js';
44
45
  import { buildQueueStatusSummary, runMergeQueue } from '../core/queue.js';
45
46
  import { DEFAULT_LEASE_POLICY, loadLeasePolicy, writeLeasePolicy } from '../core/policy.js';
47
+ import {
48
+ captureTelemetryEvent,
49
+ disableTelemetry,
50
+ enableTelemetry,
51
+ getTelemetryConfigPath,
52
+ getTelemetryRuntimeConfig,
53
+ loadTelemetryConfig,
54
+ maybePromptForTelemetry,
55
+ sendTelemetryEvent,
56
+ } from '../core/telemetry.js';
46
57
 
47
58
  function installMcpConfig(targetDirs) {
48
59
  return targetDirs.flatMap((targetDir) => upsertAllProjectMcpConfigs(targetDir));
@@ -170,6 +181,40 @@ function renderMiniBar(items) {
170
181
  return items.map(({ label, value, color = chalk.white }) => `${color('■')} ${label}:${value}`).join(chalk.dim(' '));
171
182
  }
172
183
 
184
+ function renderChip(label, value, color = chalk.white) {
185
+ return color(`[${label}:${value}]`);
186
+ }
187
+
188
+ function renderSignalStrip(signals) {
189
+ return signals.join(chalk.dim(' '));
190
+ }
191
+
192
+ function formatClockTime(isoString) {
193
+ if (!isoString) return null;
194
+ const date = new Date(isoString);
195
+ if (Number.isNaN(date.getTime())) return null;
196
+ return date.toLocaleTimeString('en-GB', {
197
+ hour: '2-digit',
198
+ minute: '2-digit',
199
+ second: '2-digit',
200
+ hour12: false,
201
+ });
202
+ }
203
+
204
+ function buildWatchSignature(report) {
205
+ return JSON.stringify({
206
+ health: report.health,
207
+ summary: report.summary,
208
+ counts: report.counts,
209
+ active_work: report.active_work,
210
+ attention: report.attention,
211
+ queue_summary: report.queue?.summary || null,
212
+ next_up: report.next_up || null,
213
+ next_steps: report.next_steps,
214
+ suggested_commands: report.suggested_commands,
215
+ });
216
+ }
217
+
173
218
  function formatRelativePolicy(policy) {
174
219
  return `stale ${policy.stale_after_minutes}m • heartbeat ${policy.heartbeat_interval_seconds}s • auto-reap ${policy.reap_on_status_check ? 'on' : 'off'}`;
175
220
  }
@@ -178,6 +223,169 @@ function sleepSync(ms) {
178
223
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
179
224
  }
180
225
 
226
+ function boolBadge(ok) {
227
+ return ok ? chalk.green('OK ') : chalk.yellow('CHECK');
228
+ }
229
+
230
+ function printErrorWithNext(message, nextCommand = null) {
231
+ console.error(chalk.red(message));
232
+ if (nextCommand) {
233
+ console.error(`${chalk.yellow('next:')} ${nextCommand}`);
234
+ }
235
+ }
236
+
237
+ async function maybeCaptureTelemetry(event, properties = {}, { homeDir = null } = {}) {
238
+ try {
239
+ await maybePromptForTelemetry({ homeDir: homeDir || undefined });
240
+ await captureTelemetryEvent(event, {
241
+ app_version: program.version(),
242
+ os: process.platform,
243
+ node_version: process.version,
244
+ ...properties,
245
+ }, { homeDir: homeDir || undefined });
246
+ } catch {
247
+ // Telemetry must never block CLI usage.
248
+ }
249
+ }
250
+
251
+ function collectSetupVerification(repoRoot, { homeDir = null } = {}) {
252
+ const dbPath = join(repoRoot, '.switchman', 'switchman.db');
253
+ const rootMcpPath = join(repoRoot, '.mcp.json');
254
+ const cursorMcpPath = join(repoRoot, '.cursor', 'mcp.json');
255
+ const claudeGuidePath = join(repoRoot, 'CLAUDE.md');
256
+ const checks = [];
257
+ const nextSteps = [];
258
+ let workspaces = [];
259
+ let db = null;
260
+
261
+ const dbExists = existsSync(dbPath);
262
+ checks.push({
263
+ key: 'database',
264
+ ok: dbExists,
265
+ label: 'Project database',
266
+ detail: dbExists ? '.switchman/switchman.db is ready' : 'Switchman database is missing',
267
+ });
268
+ if (!dbExists) {
269
+ nextSteps.push('Run `switchman init` or `switchman setup --agents 3` in this repo.');
270
+ }
271
+
272
+ if (dbExists) {
273
+ try {
274
+ db = getDb(repoRoot);
275
+ workspaces = listWorktrees(db);
276
+ } catch {
277
+ checks.push({
278
+ key: 'database_open',
279
+ ok: false,
280
+ label: 'Database access',
281
+ detail: 'Switchman could not open the project database',
282
+ });
283
+ nextSteps.push('Re-run `switchman init` if the project database looks corrupted.');
284
+ } finally {
285
+ try { db?.close(); } catch { /* no-op */ }
286
+ }
287
+ }
288
+
289
+ const agentWorkspaces = workspaces.filter((entry) => entry.name !== 'main');
290
+ const workspaceReady = agentWorkspaces.length > 0;
291
+ checks.push({
292
+ key: 'workspaces',
293
+ ok: workspaceReady,
294
+ label: 'Agent workspaces',
295
+ detail: workspaceReady
296
+ ? `${agentWorkspaces.length} agent workspace(s) registered`
297
+ : 'No agent workspaces are registered yet',
298
+ });
299
+ if (!workspaceReady) {
300
+ nextSteps.push('Run `switchman setup --agents 3` to create agent workspaces.');
301
+ }
302
+
303
+ const rootMcpExists = existsSync(rootMcpPath);
304
+ checks.push({
305
+ key: 'claude_mcp',
306
+ ok: rootMcpExists,
307
+ label: 'Claude Code MCP',
308
+ detail: rootMcpExists ? '.mcp.json is present in the repo root' : '.mcp.json is missing from the repo root',
309
+ });
310
+ if (!rootMcpExists) {
311
+ nextSteps.push('Re-run `switchman setup --agents 3` to restore the repo-local MCP config.');
312
+ }
313
+
314
+ const cursorMcpExists = existsSync(cursorMcpPath);
315
+ checks.push({
316
+ key: 'cursor_mcp',
317
+ ok: cursorMcpExists,
318
+ label: 'Cursor MCP',
319
+ detail: cursorMcpExists ? '.cursor/mcp.json is present in the repo root' : '.cursor/mcp.json is missing from the repo root',
320
+ });
321
+ if (!cursorMcpExists) {
322
+ nextSteps.push('Re-run `switchman setup --agents 3` if you want Cursor to attach automatically.');
323
+ }
324
+
325
+ const claudeGuideExists = existsSync(claudeGuidePath);
326
+ checks.push({
327
+ key: 'claude_md',
328
+ ok: claudeGuideExists,
329
+ label: 'Claude guide',
330
+ detail: claudeGuideExists ? 'CLAUDE.md is present' : 'CLAUDE.md is optional but recommended for Claude Code',
331
+ });
332
+ if (!claudeGuideExists) {
333
+ nextSteps.push('If you use Claude Code, add `CLAUDE.md` from the repo root setup guide.');
334
+ }
335
+
336
+ const windsurfConfigExists = existsSync(getWindsurfMcpConfigPath(homeDir || undefined));
337
+ checks.push({
338
+ key: 'windsurf_mcp',
339
+ ok: windsurfConfigExists,
340
+ label: 'Windsurf MCP',
341
+ detail: windsurfConfigExists
342
+ ? 'Windsurf shared MCP config is installed'
343
+ : 'Windsurf shared MCP config is optional and not installed',
344
+ });
345
+ if (!windsurfConfigExists) {
346
+ nextSteps.push('If you use Windsurf, run `switchman mcp install --windsurf` once.');
347
+ }
348
+
349
+ const ok = checks.every((item) => item.ok || ['claude_md', 'windsurf_mcp'].includes(item.key));
350
+ return {
351
+ ok,
352
+ repo_root: repoRoot,
353
+ checks,
354
+ workspaces: workspaces.map((entry) => ({
355
+ name: entry.name,
356
+ path: entry.path,
357
+ branch: entry.branch,
358
+ })),
359
+ suggested_commands: [
360
+ 'switchman status --watch',
361
+ 'switchman task add "Your first task" --priority 8',
362
+ 'switchman gate ci',
363
+ ...nextSteps.some((step) => step.includes('Windsurf')) ? ['switchman mcp install --windsurf'] : [],
364
+ ],
365
+ next_steps: [...new Set(nextSteps)].slice(0, 6),
366
+ };
367
+ }
368
+
369
+ function renderSetupVerification(report, { compact = false } = {}) {
370
+ console.log(chalk.bold(compact ? 'First-run check:' : 'Setup verification:'));
371
+ for (const check of report.checks) {
372
+ const badge = boolBadge(check.ok);
373
+ console.log(` ${badge} ${check.label} ${chalk.dim(`— ${check.detail}`)}`);
374
+ }
375
+ if (report.next_steps.length > 0) {
376
+ console.log('');
377
+ console.log(chalk.bold('Fix next:'));
378
+ for (const step of report.next_steps) {
379
+ console.log(` - ${step}`);
380
+ }
381
+ }
382
+ console.log('');
383
+ console.log(chalk.bold('Try next:'));
384
+ for (const command of report.suggested_commands.slice(0, 4)) {
385
+ console.log(` ${chalk.cyan(command)}`);
386
+ }
387
+ }
388
+
181
389
  function summarizeLeaseScope(db, lease) {
182
390
  const reservations = listScopeReservations(db, { leaseId: lease.id });
183
391
  const pathScopes = reservations
@@ -665,13 +873,34 @@ function renderUnifiedStatusReport(report) {
665
873
  const badge = healthColor(healthLabel(report.health));
666
874
  const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
667
875
  const queueCounts = report.counts.queue;
876
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
877
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
878
+ const focusItem = blockedCount > 0
879
+ ? report.attention.find((item) => item.severity === 'block')
880
+ : warningCount > 0
881
+ ? report.attention.find((item) => item.severity !== 'block')
882
+ : report.next_up[0];
883
+ const focusLine = focusItem
884
+ ? ('title' in focusItem
885
+ ? `${focusItem.title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
886
+ : `${focusItem.title} ${chalk.dim(focusItem.id)}`)
887
+ : 'Nothing urgent. Safe to keep parallel work moving.';
888
+ const queueLoad = queueCounts.queued + queueCounts.retrying + queueCounts.merging + queueCounts.blocked;
889
+ const landingLabel = report.merge_readiness.ci_gate_ok ? 'ready' : 'hold';
668
890
 
669
891
  console.log('');
670
- console.log(healthColor('='.repeat(64)));
671
- console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• user-centred repo overview')}`);
892
+ console.log(healthColor('='.repeat(72)));
893
+ console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
672
894
  console.log(`${chalk.dim(report.repo_root)}`);
673
895
  console.log(`${chalk.dim(report.summary)}`);
674
- console.log(healthColor('='.repeat(64)));
896
+ console.log(healthColor('='.repeat(72)));
897
+ console.log(renderSignalStrip([
898
+ renderChip('health', healthLabel(report.health), healthColor),
899
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
900
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
901
+ renderChip('landing', landingLabel, mergeColor),
902
+ renderChip('queue', queueLoad, queueLoad > 0 ? chalk.blue : chalk.green),
903
+ ]));
675
904
  console.log(renderMetricRow([
676
905
  { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
677
906
  { label: 'leases', value: `${report.counts.active_leases} active`, color: chalk.blue },
@@ -685,13 +914,18 @@ function renderUnifiedStatusReport(report) {
685
914
  { label: 'merging', value: queueCounts.merging, color: chalk.blue },
686
915
  { label: 'merged', value: queueCounts.merged, color: chalk.green },
687
916
  ]));
917
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
688
918
  console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
689
919
 
690
920
  const runningLines = report.active_work.length > 0
691
921
  ? report.active_work.slice(0, 5).map((item) => {
692
- const boundary = item.boundary_validation ? ` validation:${item.boundary_validation.status}` : '';
693
- const stale = (item.dependency_invalidations?.length || 0) > 0 ? ` stale:${item.dependency_invalidations.length}` : '';
694
- return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${chalk.dim(boundary)}${chalk.dim(stale)}`;
922
+ const boundary = item.boundary_validation
923
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
924
+ : '';
925
+ const stale = (item.dependency_invalidations?.length || 0) > 0
926
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
927
+ : '';
928
+ return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
695
929
  })
696
930
  : [chalk.dim('Nothing active right now.')];
697
931
 
@@ -700,7 +934,7 @@ function renderUnifiedStatusReport(report) {
700
934
 
701
935
  const blockedLines = blockedItems.length > 0
702
936
  ? blockedItems.slice(0, 4).flatMap((item) => {
703
- const lines = [`${chalk.red('BLOCK')} ${item.title}`];
937
+ const lines = [`${renderChip('BLOCKED', item.kind || 'item', chalk.red)} ${item.title}`];
704
938
  if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
705
939
  lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
706
940
  if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
@@ -710,8 +944,7 @@ function renderUnifiedStatusReport(report) {
710
944
 
711
945
  const warningLines = warningItems.length > 0
712
946
  ? warningItems.slice(0, 4).flatMap((item) => {
713
- const tone = chalk.yellow('WARN ');
714
- const lines = [`${tone} ${item.title}`];
947
+ const lines = [`${renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
715
948
  if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
716
949
  lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
717
950
  if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
@@ -728,7 +961,7 @@ function renderUnifiedStatusReport(report) {
728
961
  .filter((entry) => ['blocked', 'retrying', 'merging'].includes(entry.status))
729
962
  .slice(0, 4)
730
963
  .flatMap((item) => {
731
- const lines = [`${statusBadge(item.status)} ${item.id} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
964
+ const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'blocked' ? chalk.red : item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
732
965
  if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
733
966
  if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
734
967
  return lines;
@@ -738,7 +971,7 @@ function renderUnifiedStatusReport(report) {
738
971
 
739
972
  const nextActionLines = [
740
973
  ...(report.next_up.length > 0
741
- ? report.next_up.map((task) => `[p${task.priority}] ${task.title} ${chalk.dim(task.id)}`)
974
+ ? report.next_up.map((task) => `${renderChip('NEXT', `p${task.priority}`, chalk.green)} ${task.title} ${chalk.dim(task.id)}`)
742
975
  : [chalk.dim('No pending tasks waiting right now.')]),
743
976
  '',
744
977
  ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
@@ -859,6 +1092,23 @@ program
859
1092
  .description('Conflict-aware task coordinator for parallel AI coding agents')
860
1093
  .version('0.1.0');
861
1094
 
1095
+ program.showHelpAfterError('(run with --help for usage examples)');
1096
+ program.addHelpText('after', `
1097
+ Start here:
1098
+ switchman setup --agents 5
1099
+ switchman status --watch
1100
+ switchman gate ci
1101
+
1102
+ Most useful commands:
1103
+ switchman task add "Implement auth helper" --priority 9
1104
+ switchman lease next --json
1105
+ switchman queue run --watch
1106
+
1107
+ Docs:
1108
+ README.md
1109
+ docs/setup-cursor.md
1110
+ `);
1111
+
862
1112
  // ── init ──────────────────────────────────────────────────────────────────────
863
1113
 
864
1114
  program
@@ -902,7 +1152,12 @@ program
902
1152
  .description('One-command setup: create agent workspaces and initialise Switchman')
903
1153
  .option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
904
1154
  .option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
905
- .action((opts) => {
1155
+ .addHelpText('after', `
1156
+ Examples:
1157
+ switchman setup --agents 5
1158
+ switchman setup --agents 3 --prefix team
1159
+ `)
1160
+ .action(async (opts) => {
906
1161
  const agentCount = parseInt(opts.agents);
907
1162
 
908
1163
  if (isNaN(agentCount) || agentCount < 1 || agentCount > 10) {
@@ -984,16 +1239,148 @@ program
984
1239
  console.log(` ${chalk.cyan('switchman status')}`);
985
1240
  console.log('');
986
1241
 
1242
+ const verification = collectSetupVerification(repoRoot);
1243
+ renderSetupVerification(verification, { compact: true });
1244
+ await maybeCaptureTelemetry('setup_completed', {
1245
+ agent_count: agentCount,
1246
+ verification_ok: verification.ok,
1247
+ });
1248
+
987
1249
  } catch (err) {
988
1250
  spinner.fail(err.message);
989
1251
  process.exit(1);
990
1252
  }
991
1253
  });
992
1254
 
1255
+ program
1256
+ .command('verify-setup')
1257
+ .description('Check whether this repo is ready for a smooth first Switchman run')
1258
+ .option('--json', 'Output raw JSON')
1259
+ .option('--home <path>', 'Override the home directory for editor config checks')
1260
+ .addHelpText('after', `
1261
+ Examples:
1262
+ switchman verify-setup
1263
+ switchman verify-setup --json
1264
+
1265
+ Use this after setup or whenever editor/config wiring feels off.
1266
+ `)
1267
+ .action(async (opts) => {
1268
+ const repoRoot = getRepo();
1269
+ const report = collectSetupVerification(repoRoot, { homeDir: opts.home || null });
1270
+
1271
+ if (opts.json) {
1272
+ console.log(JSON.stringify(report, null, 2));
1273
+ if (!report.ok) process.exitCode = 1;
1274
+ return;
1275
+ }
1276
+
1277
+ renderSetupVerification(report);
1278
+ await maybeCaptureTelemetry(report.ok ? 'verify_setup_passed' : 'verify_setup_failed', {
1279
+ check_count: report.checks.length,
1280
+ next_step_count: report.next_steps.length,
1281
+ }, { homeDir: opts.home || null });
1282
+ if (!report.ok) process.exitCode = 1;
1283
+ });
1284
+
993
1285
 
994
1286
  // ── mcp ───────────────────────────────────────────────────────────────────────
995
1287
 
996
1288
  const mcpCmd = program.command('mcp').description('Manage editor connections for Switchman');
1289
+ const telemetryCmd = program.command('telemetry').description('Control anonymous opt-in telemetry for Switchman');
1290
+
1291
+ telemetryCmd
1292
+ .command('status')
1293
+ .description('Show whether telemetry is enabled and where events would be sent')
1294
+ .option('--home <path>', 'Override the home directory for telemetry config')
1295
+ .option('--json', 'Output raw JSON')
1296
+ .action((opts) => {
1297
+ const config = loadTelemetryConfig(opts.home || undefined);
1298
+ const runtime = getTelemetryRuntimeConfig();
1299
+ const payload = {
1300
+ enabled: config.telemetry_enabled === true,
1301
+ configured: Boolean(runtime.apiKey) && !runtime.disabled,
1302
+ install_id: config.telemetry_install_id,
1303
+ destination: runtime.apiKey && !runtime.disabled ? runtime.host : null,
1304
+ config_path: getTelemetryConfigPath(opts.home || undefined),
1305
+ };
1306
+
1307
+ if (opts.json) {
1308
+ console.log(JSON.stringify(payload, null, 2));
1309
+ return;
1310
+ }
1311
+
1312
+ console.log(`Telemetry: ${payload.enabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
1313
+ console.log(`Configured destination: ${payload.configured ? chalk.cyan(payload.destination) : chalk.dim('not configured')}`);
1314
+ console.log(`Config file: ${chalk.dim(payload.config_path)}`);
1315
+ if (payload.install_id) {
1316
+ console.log(`Install ID: ${chalk.dim(payload.install_id)}`);
1317
+ }
1318
+ });
1319
+
1320
+ telemetryCmd
1321
+ .command('enable')
1322
+ .description('Enable anonymous telemetry for setup and operator workflows')
1323
+ .option('--home <path>', 'Override the home directory for telemetry config')
1324
+ .action((opts) => {
1325
+ const runtime = getTelemetryRuntimeConfig();
1326
+ if (!runtime.apiKey || runtime.disabled) {
1327
+ printErrorWithNext('Telemetry destination is not configured. Set SWITCHMAN_TELEMETRY_API_KEY first.', 'switchman telemetry status');
1328
+ process.exitCode = 1;
1329
+ return;
1330
+ }
1331
+ const result = enableTelemetry(opts.home || undefined);
1332
+ console.log(`${chalk.green('✓')} Telemetry enabled`);
1333
+ console.log(` ${chalk.dim(result.path)}`);
1334
+ });
1335
+
1336
+ telemetryCmd
1337
+ .command('disable')
1338
+ .description('Disable anonymous telemetry')
1339
+ .option('--home <path>', 'Override the home directory for telemetry config')
1340
+ .action((opts) => {
1341
+ const result = disableTelemetry(opts.home || undefined);
1342
+ console.log(`${chalk.green('✓')} Telemetry disabled`);
1343
+ console.log(` ${chalk.dim(result.path)}`);
1344
+ });
1345
+
1346
+ telemetryCmd
1347
+ .command('test')
1348
+ .description('Send one test telemetry event and report whether delivery succeeded')
1349
+ .option('--home <path>', 'Override the home directory for telemetry config')
1350
+ .option('--json', 'Output raw JSON')
1351
+ .action(async (opts) => {
1352
+ const result = await sendTelemetryEvent('telemetry_test', {
1353
+ app_version: program.version(),
1354
+ os: process.platform,
1355
+ node_version: process.version,
1356
+ source: 'switchman-cli-test',
1357
+ }, { homeDir: opts.home || undefined });
1358
+
1359
+ if (opts.json) {
1360
+ console.log(JSON.stringify(result, null, 2));
1361
+ if (!result.ok) process.exitCode = 1;
1362
+ return;
1363
+ }
1364
+
1365
+ if (result.ok) {
1366
+ console.log(`${chalk.green('✓')} Telemetry test event delivered`);
1367
+ console.log(` ${chalk.dim('destination:')} ${chalk.cyan(result.destination)}`);
1368
+ if (result.status) {
1369
+ console.log(` ${chalk.dim('status:')} ${result.status}`);
1370
+ }
1371
+ return;
1372
+ }
1373
+
1374
+ printErrorWithNext(`Telemetry test failed (${result.reason || 'unknown_error'}).`, 'switchman telemetry status');
1375
+ console.log(` ${chalk.dim('destination:')} ${result.destination || 'unknown'}`);
1376
+ if (result.status) {
1377
+ console.log(` ${chalk.dim('status:')} ${result.status}`);
1378
+ }
1379
+ if (result.error) {
1380
+ console.log(` ${chalk.dim('error:')} ${result.error}`);
1381
+ }
1382
+ process.exitCode = 1;
1383
+ });
997
1384
 
998
1385
  mcpCmd
999
1386
  .command('install')
@@ -1001,6 +1388,11 @@ mcpCmd
1001
1388
  .option('--windsurf', 'Write Windsurf MCP config to ~/.codeium/mcp_config.json')
1002
1389
  .option('--home <path>', 'Override the home directory for config writes (useful for testing)')
1003
1390
  .option('--json', 'Output raw JSON')
1391
+ .addHelpText('after', `
1392
+ Examples:
1393
+ switchman mcp install --windsurf
1394
+ switchman mcp install --windsurf --json
1395
+ `)
1004
1396
  .action((opts) => {
1005
1397
  if (!opts.windsurf) {
1006
1398
  console.error(chalk.red('Choose an editor install target, for example `switchman mcp install --windsurf`.'));
@@ -1030,6 +1422,12 @@ mcpCmd
1030
1422
  // ── task ──────────────────────────────────────────────────────────────────────
1031
1423
 
1032
1424
  const taskCmd = program.command('task').description('Manage the task list');
1425
+ taskCmd.addHelpText('after', `
1426
+ Examples:
1427
+ switchman task add "Fix login bug" --priority 8
1428
+ switchman task list --status pending
1429
+ switchman task done task-123
1430
+ `);
1033
1431
 
1034
1432
  taskCmd
1035
1433
  .command('add <title>')
@@ -1127,10 +1525,15 @@ taskCmd
1127
1525
 
1128
1526
  taskCmd
1129
1527
  .command('next')
1130
- .description('Get and assign the next pending task (compatibility shim for lease next)')
1528
+ .description('Get the next pending task quickly (use `lease next` for the full workflow)')
1131
1529
  .option('--json', 'Output as JSON')
1132
1530
  .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
1133
1531
  .option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
1532
+ .addHelpText('after', `
1533
+ Examples:
1534
+ switchman task next
1535
+ switchman task next --json
1536
+ `)
1134
1537
  .action((opts) => {
1135
1538
  const repoRoot = getRepo();
1136
1539
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -1159,7 +1562,13 @@ taskCmd
1159
1562
 
1160
1563
  // ── queue ─────────────────────────────────────────────────────────────────────
1161
1564
 
1162
- const queueCmd = program.command('queue').description('Land finished work safely back onto main, one item at a time');
1565
+ const queueCmd = program.command('queue').alias('land').description('Land finished work safely back onto main, one item at a time');
1566
+ queueCmd.addHelpText('after', `
1567
+ Examples:
1568
+ switchman queue add --worktree agent1
1569
+ switchman queue status
1570
+ switchman queue run --watch
1571
+ `);
1163
1572
 
1164
1573
  queueCmd
1165
1574
  .command('add [branch]')
@@ -1170,6 +1579,12 @@ queueCmd
1170
1579
  .option('--max-retries <n>', 'Maximum automatic retries', '1')
1171
1580
  .option('--submitted-by <name>', 'Operator or automation name')
1172
1581
  .option('--json', 'Output raw JSON')
1582
+ .addHelpText('after', `
1583
+ Examples:
1584
+ switchman queue add feature/auth-hardening
1585
+ switchman queue add --worktree agent2
1586
+ switchman queue add --pipeline pipe-123
1587
+ `)
1173
1588
  .action((branch, opts) => {
1174
1589
  const repoRoot = getRepo();
1175
1590
  const db = getDb(repoRoot);
@@ -1179,7 +1594,7 @@ queueCmd
1179
1594
  if (opts.worktree) {
1180
1595
  const worktree = listWorktrees(db).find((entry) => entry.name === opts.worktree);
1181
1596
  if (!worktree) {
1182
- throw new Error(`Worktree ${opts.worktree} is not registered.`);
1597
+ throw new Error(`Workspace ${opts.worktree} is not registered.`);
1183
1598
  }
1184
1599
  payload = {
1185
1600
  sourceType: 'worktree',
@@ -1207,7 +1622,7 @@ queueCmd
1207
1622
  submittedBy: opts.submittedBy || null,
1208
1623
  };
1209
1624
  } else {
1210
- throw new Error('Provide a branch, --worktree, or --pipeline.');
1625
+ throw new Error('Choose one source to land: a branch name, `--worktree`, or `--pipeline`.');
1211
1626
  }
1212
1627
 
1213
1628
  const result = enqueueMergeItem(db, payload);
@@ -1223,7 +1638,7 @@ queueCmd
1223
1638
  if (result.source_worktree) console.log(` ${chalk.dim('worktree:')} ${result.source_worktree}`);
1224
1639
  } catch (err) {
1225
1640
  db.close();
1226
- console.error(chalk.red(err.message));
1641
+ printErrorWithNext(err.message, 'switchman queue add --help');
1227
1642
  process.exitCode = 1;
1228
1643
  }
1229
1644
  });
@@ -1266,6 +1681,19 @@ queueCmd
1266
1681
  .command('status')
1267
1682
  .description('Show an operator-friendly merge queue summary')
1268
1683
  .option('--json', 'Output raw JSON')
1684
+ .addHelpText('after', `
1685
+ Plain English:
1686
+ Use this when finished branches are waiting to land and you want one safe queue view.
1687
+
1688
+ Examples:
1689
+ switchman queue status
1690
+ switchman queue status --json
1691
+
1692
+ What it helps you answer:
1693
+ - what lands next
1694
+ - what is blocked
1695
+ - what command should I run now
1696
+ `)
1269
1697
  .action((opts) => {
1270
1698
  const repoRoot = getRepo();
1271
1699
  const db = getDb(repoRoot);
@@ -1281,18 +1709,77 @@ queueCmd
1281
1709
  return;
1282
1710
  }
1283
1711
 
1284
- console.log(`Queue: ${items.length} item(s)`);
1285
- console.log(` ${chalk.dim('queued')} ${summary.counts.queued} ${chalk.dim('validating')} ${summary.counts.validating} ${chalk.dim('rebasing')} ${summary.counts.rebasing} ${chalk.dim('merging')} ${summary.counts.merging} ${chalk.dim('retrying')} ${summary.counts.retrying} ${chalk.dim('blocked')} ${summary.counts.blocked} ${chalk.dim('merged')} ${summary.counts.merged}`);
1286
- if (summary.next) {
1287
- console.log(` ${chalk.dim('next:')} ${summary.next.id} ${summary.next.source_type}:${summary.next.source_ref} ${chalk.dim(`retries:${summary.next.retry_count}/${summary.next.max_retries}`)}`);
1288
- }
1289
- for (const item of summary.blocked) {
1290
- console.log(` ${statusBadge(item.status)} ${item.id} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`);
1291
- if (item.last_error_summary) console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
1292
- if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
1712
+ const queueHealth = summary.counts.blocked > 0 ? 'block' : summary.counts.retrying > 0 ? 'warn' : 'healthy';
1713
+ const queueHealthColor = colorForHealth(queueHealth);
1714
+ const focus = summary.blocked[0] || summary.retrying[0] || summary.next || null;
1715
+ const focusLine = focus
1716
+ ? `${focus.id} ${focus.source_type}:${focus.source_ref}${focus.last_error_summary ? ` ${chalk.dim(`• ${focus.last_error_summary}`)}` : ''}`
1717
+ : 'Nothing waiting. Landing queue is clear.';
1718
+
1719
+ console.log('');
1720
+ console.log(queueHealthColor('='.repeat(72)));
1721
+ console.log(`${queueHealthColor(healthLabel(queueHealth))} ${chalk.bold('switchman queue status')} ${chalk.dim('• landing mission control')}`);
1722
+ console.log(queueHealthColor('='.repeat(72)));
1723
+ console.log(renderSignalStrip([
1724
+ renderChip('queued', summary.counts.queued, summary.counts.queued > 0 ? chalk.yellow : chalk.green),
1725
+ renderChip('retrying', summary.counts.retrying, summary.counts.retrying > 0 ? chalk.yellow : chalk.green),
1726
+ renderChip('blocked', summary.counts.blocked, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1727
+ renderChip('merging', summary.counts.merging, summary.counts.merging > 0 ? chalk.blue : chalk.green),
1728
+ renderChip('merged', summary.counts.merged, summary.counts.merged > 0 ? chalk.green : chalk.white),
1729
+ ]));
1730
+ console.log(renderMetricRow([
1731
+ { label: 'items', value: items.length, color: chalk.white },
1732
+ { label: 'validating', value: summary.counts.validating, color: chalk.blue },
1733
+ { label: 'rebasing', value: summary.counts.rebasing, color: chalk.blue },
1734
+ { label: 'target', value: summary.next?.target_branch || 'main', color: chalk.cyan },
1735
+ ]));
1736
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
1737
+
1738
+ const queueFocusLines = summary.next
1739
+ ? [
1740
+ `${renderChip('NEXT', summary.next.id, chalk.green)} ${summary.next.source_type}:${summary.next.source_ref} ${chalk.dim(`retries:${summary.next.retry_count}/${summary.next.max_retries}`)}`,
1741
+ ` ${chalk.yellow('run:')} switchman queue run`,
1742
+ ]
1743
+ : [chalk.dim('No queued landing work right now.')];
1744
+
1745
+ const queueBlockedLines = summary.blocked.length > 0
1746
+ ? summary.blocked.slice(0, 4).flatMap((item) => {
1747
+ const lines = [`${renderChip('BLOCKED', item.id, chalk.red)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
1748
+ if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
1749
+ if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
1750
+ return lines;
1751
+ })
1752
+ : [chalk.green('Nothing blocked.')];
1753
+
1754
+ const queueWatchLines = items.filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status)).length > 0
1755
+ ? items
1756
+ .filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status))
1757
+ .slice(0, 4)
1758
+ .flatMap((item) => {
1759
+ const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref}`];
1760
+ if (item.last_error_summary) lines.push(` ${chalk.dim(item.last_error_summary)}`);
1761
+ return lines;
1762
+ })
1763
+ : [chalk.green('No in-flight queue items right now.')];
1764
+
1765
+ const queueCommandLines = [
1766
+ `${chalk.cyan('$')} switchman queue run`,
1767
+ `${chalk.cyan('$')} switchman queue status --json`,
1768
+ ...(summary.blocked[0] ? [`${chalk.cyan('$')} switchman queue retry ${summary.blocked[0].id}`] : []),
1769
+ ];
1770
+
1771
+ console.log('');
1772
+ for (const block of [
1773
+ renderPanel('Landing focus', queueFocusLines, chalk.green),
1774
+ renderPanel('Blocked', queueBlockedLines, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1775
+ renderPanel('In flight', queueWatchLines, queueWatchLines[0] === 'No in-flight queue items right now.' ? chalk.green : chalk.blue),
1776
+ renderPanel('Next commands', queueCommandLines, chalk.cyan),
1777
+ ]) {
1778
+ for (const line of block) console.log(line);
1779
+ console.log('');
1293
1780
  }
1781
+
1294
1782
  if (recentEvents.length > 0) {
1295
- console.log('');
1296
1783
  console.log(chalk.bold('Recent Queue Events:'));
1297
1784
  for (const event of recentEvents) {
1298
1785
  console.log(` ${chalk.cyan(event.queue_item_id)} ${chalk.dim(event.event_type)} ${chalk.dim(event.status || '')} ${chalk.dim(event.created_at)}`.trim());
@@ -1302,13 +1789,19 @@ queueCmd
1302
1789
 
1303
1790
  queueCmd
1304
1791
  .command('run')
1305
- .description('Process queued merge items serially')
1792
+ .description('Process landing-queue items one at a time')
1306
1793
  .option('--max-items <n>', 'Maximum queue items to process', '1')
1307
1794
  .option('--target <branch>', 'Default target branch', 'main')
1308
1795
  .option('--watch', 'Keep polling for new queue items')
1309
1796
  .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '1000')
1310
1797
  .option('--max-cycles <n>', 'Maximum watch cycles before exiting (mainly for tests)')
1311
1798
  .option('--json', 'Output raw JSON')
1799
+ .addHelpText('after', `
1800
+ Examples:
1801
+ switchman queue run
1802
+ switchman queue run --watch
1803
+ switchman queue run --watch --watch-interval-ms 1000
1804
+ `)
1312
1805
  .action(async (opts) => {
1313
1806
  const repoRoot = getRepo();
1314
1807
 
@@ -1348,6 +1841,13 @@ queueCmd
1348
1841
 
1349
1842
  if (aggregate.processed.length === 0) {
1350
1843
  console.log(chalk.dim('No queued merge items.'));
1844
+ await maybeCaptureTelemetry('queue_used', {
1845
+ watch,
1846
+ cycles: aggregate.cycles,
1847
+ processed_count: 0,
1848
+ merged_count: 0,
1849
+ blocked_count: 0,
1850
+ });
1351
1851
  return;
1352
1852
  }
1353
1853
 
@@ -1362,6 +1862,14 @@ queueCmd
1362
1862
  if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
1363
1863
  }
1364
1864
  }
1865
+
1866
+ await maybeCaptureTelemetry('queue_used', {
1867
+ watch,
1868
+ cycles: aggregate.cycles,
1869
+ processed_count: aggregate.processed.length,
1870
+ merged_count: aggregate.processed.filter((entry) => entry.status === 'merged').length,
1871
+ blocked_count: aggregate.processed.filter((entry) => entry.status !== 'merged').length,
1872
+ });
1365
1873
  } catch (err) {
1366
1874
  console.error(chalk.red(err.message));
1367
1875
  process.exitCode = 1;
@@ -1379,7 +1887,7 @@ queueCmd
1379
1887
  db.close();
1380
1888
 
1381
1889
  if (!item) {
1382
- console.error(chalk.red(`Queue item ${itemId} is not retryable.`));
1890
+ printErrorWithNext(`Queue item ${itemId} is not retryable.`, 'switchman queue status');
1383
1891
  process.exitCode = 1;
1384
1892
  return;
1385
1893
  }
@@ -1402,7 +1910,7 @@ queueCmd
1402
1910
  db.close();
1403
1911
 
1404
1912
  if (!item) {
1405
- console.error(chalk.red(`Queue item ${itemId} does not exist.`));
1913
+ printErrorWithNext(`Queue item ${itemId} does not exist.`, 'switchman queue status');
1406
1914
  process.exitCode = 1;
1407
1915
  return;
1408
1916
  }
@@ -1413,6 +1921,12 @@ queueCmd
1413
1921
  // ── pipeline ──────────────────────────────────────────────────────────────────
1414
1922
 
1415
1923
  const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
1924
+ pipelineCmd.addHelpText('after', `
1925
+ Examples:
1926
+ switchman pipeline start "Harden auth API permissions"
1927
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
1928
+ switchman pipeline status pipe-123
1929
+ `);
1416
1930
 
1417
1931
  pipelineCmd
1418
1932
  .command('start <title>')
@@ -1450,6 +1964,14 @@ pipelineCmd
1450
1964
  .command('status <pipelineId>')
1451
1965
  .description('Show task status for a pipeline')
1452
1966
  .option('--json', 'Output raw JSON')
1967
+ .addHelpText('after', `
1968
+ Plain English:
1969
+ Use this when one goal has been split into several tasks and you want to see what is running, stuck, or next.
1970
+
1971
+ Examples:
1972
+ switchman pipeline status pipe-123
1973
+ switchman pipeline status pipe-123 --json
1974
+ `)
1453
1975
  .action((pipelineId, opts) => {
1454
1976
  const repoRoot = getRepo();
1455
1977
  const db = getDb(repoRoot);
@@ -1463,20 +1985,74 @@ pipelineCmd
1463
1985
  return;
1464
1986
  }
1465
1987
 
1988
+ const pipelineHealth = result.status === 'blocked'
1989
+ ? 'block'
1990
+ : result.counts.failed > 0
1991
+ ? 'warn'
1992
+ : result.counts.in_progress > 0
1993
+ ? 'warn'
1994
+ : 'healthy';
1995
+ const pipelineHealthColor = colorForHealth(pipelineHealth);
1996
+ const failedTask = result.tasks.find((task) => task.status === 'failed');
1997
+ const runningTask = result.tasks.find((task) => task.status === 'in_progress');
1998
+ const nextPendingTask = result.tasks.find((task) => task.status === 'pending');
1999
+ const focusTask = failedTask || runningTask || nextPendingTask || result.tasks[0] || null;
2000
+ const focusLine = focusTask
2001
+ ? `${focusTask.title} ${chalk.dim(focusTask.id)}`
2002
+ : 'No pipeline tasks found.';
2003
+
2004
+ console.log('');
2005
+ console.log(pipelineHealthColor('='.repeat(72)));
2006
+ console.log(`${pipelineHealthColor(healthLabel(pipelineHealth))} ${chalk.bold('switchman pipeline status')} ${chalk.dim('• pipeline mission control')}`);
1466
2007
  console.log(`${chalk.bold(result.title)} ${chalk.dim(result.pipeline_id)}`);
1467
- console.log(` ${chalk.dim('done')} ${result.counts.done} ${chalk.dim('in_progress')} ${result.counts.in_progress} ${chalk.dim('pending')} ${result.counts.pending} ${chalk.dim('failed')} ${result.counts.failed}`);
1468
- for (const task of result.tasks) {
2008
+ console.log(pipelineHealthColor('='.repeat(72)));
2009
+ console.log(renderSignalStrip([
2010
+ renderChip('done', result.counts.done, result.counts.done > 0 ? chalk.green : chalk.white),
2011
+ renderChip('running', result.counts.in_progress, result.counts.in_progress > 0 ? chalk.blue : chalk.green),
2012
+ renderChip('pending', result.counts.pending, result.counts.pending > 0 ? chalk.yellow : chalk.green),
2013
+ renderChip('failed', result.counts.failed, result.counts.failed > 0 ? chalk.red : chalk.green),
2014
+ ]));
2015
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
2016
+
2017
+ const runningLines = result.tasks.filter((task) => task.status === 'in_progress').slice(0, 4).map((task) => {
1469
2018
  const worktree = task.worktree || task.suggested_worktree || 'unassigned';
1470
2019
  const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
1471
2020
  const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
1472
- console.log(` ${statusBadge(task.status)} ${task.id} ${task.title}${type} ${chalk.dim(worktree)}${blocked}`);
2021
+ return `${chalk.cyan(worktree)} -> ${task.title}${type} ${chalk.dim(task.id)}${blocked}`;
2022
+ });
2023
+
2024
+ const blockedLines = result.tasks.filter((task) => task.status === 'failed').slice(0, 4).flatMap((task) => {
2025
+ const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
2026
+ const lines = [`${renderChip('BLOCKED', task.id, chalk.red)} ${task.title}${type}`];
1473
2027
  if (task.failure?.summary) {
1474
2028
  const reasonLabel = humanizeReasonCode(task.failure.reason_code);
1475
- console.log(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
1476
- }
1477
- if (task.next_action) {
1478
- console.log(` ${chalk.yellow('next:')} ${task.next_action}`);
2029
+ lines.push(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
1479
2030
  }
2031
+ if (task.next_action) lines.push(` ${chalk.yellow('next:')} ${task.next_action}`);
2032
+ return lines;
2033
+ });
2034
+
2035
+ const nextLines = result.tasks.filter((task) => task.status === 'pending').slice(0, 4).map((task) => {
2036
+ const worktree = task.suggested_worktree || task.worktree || 'unassigned';
2037
+ const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
2038
+ return `${renderChip('NEXT', task.id, chalk.green)} ${task.title} ${chalk.dim(worktree)}${blocked}`;
2039
+ });
2040
+
2041
+ const commandLines = [
2042
+ `${chalk.cyan('$')} switchman pipeline exec ${result.pipeline_id} "/path/to/agent-command"`,
2043
+ `${chalk.cyan('$')} switchman pipeline pr ${result.pipeline_id}`,
2044
+ ...(result.counts.failed > 0 ? [`${chalk.cyan('$')} switchman pipeline status ${result.pipeline_id}`] : []),
2045
+ ];
2046
+
2047
+ console.log('');
2048
+ for (const block of [
2049
+ renderPanel('Running now', runningLines.length > 0 ? runningLines : [chalk.dim('No tasks are actively running.')], runningLines.length > 0 ? chalk.cyan : chalk.green),
2050
+ renderPanel('Blocked', blockedLines.length > 0 ? blockedLines : [chalk.green('Nothing blocked.')], blockedLines.length > 0 ? chalk.red : chalk.green),
2051
+ renderPanel('Next up', nextLines.length > 0 ? nextLines : [chalk.dim('No pending tasks left.')], chalk.green),
2052
+ renderPanel('Next commands', commandLines, chalk.cyan),
2053
+ ]) {
2054
+ for (const line of block) console.log(line);
2055
+ console.log('');
1480
2056
  }
1481
2057
  } catch (err) {
1482
2058
  db.close();
@@ -1666,6 +2242,14 @@ pipelineCmd
1666
2242
  .option('--retry-backoff-ms <ms>', 'Base backoff in milliseconds between retry attempts', '0')
1667
2243
  .option('--timeout-ms <ms>', 'Default command timeout in milliseconds when a task spec does not provide one', '0')
1668
2244
  .option('--json', 'Output raw JSON')
2245
+ .addHelpText('after', `
2246
+ Plain English:
2247
+ pipeline = one goal, broken into smaller safe tasks
2248
+
2249
+ Examples:
2250
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
2251
+ switchman pipeline exec pipe-123 "npm test"
2252
+ `)
1669
2253
  .action(async (pipelineId, agentCommand, opts) => {
1670
2254
  const repoRoot = getRepo();
1671
2255
  const db = getDb(repoRoot);
@@ -1699,20 +2283,34 @@ pipelineCmd
1699
2283
  console.log(chalk.dim(result.pr.markdown.split('\n')[0]));
1700
2284
  } catch (err) {
1701
2285
  db.close();
1702
- console.error(chalk.red(err.message));
2286
+ printErrorWithNext(err.message, `switchman pipeline status ${pipelineId}`);
1703
2287
  process.exitCode = 1;
1704
2288
  }
1705
2289
  });
1706
2290
 
1707
2291
  // ── lease ────────────────────────────────────────────────────────────────────
1708
2292
 
1709
- const leaseCmd = program.command('lease').description('Manage active work leases');
2293
+ const leaseCmd = program.command('lease').alias('session').description('Manage active work sessions and keep long-running tasks alive');
2294
+ leaseCmd.addHelpText('after', `
2295
+ Plain English:
2296
+ lease = a task currently checked out by an agent
2297
+
2298
+ Examples:
2299
+ switchman lease next --json
2300
+ switchman lease heartbeat lease-123
2301
+ switchman lease reap
2302
+ `);
1710
2303
 
1711
2304
  leaseCmd
1712
2305
  .command('acquire <taskId> <worktree>')
1713
- .description('Acquire a lease for a pending task')
2306
+ .description('Start a tracked work session for a specific pending task')
1714
2307
  .option('--agent <name>', 'Agent identifier for logging')
1715
2308
  .option('--json', 'Output as JSON')
2309
+ .addHelpText('after', `
2310
+ Examples:
2311
+ switchman lease acquire task-123 agent2
2312
+ switchman lease acquire task-123 agent2 --agent cursor
2313
+ `)
1716
2314
  .action((taskId, worktree, opts) => {
1717
2315
  const repoRoot = getRepo();
1718
2316
  const db = getDb(repoRoot);
@@ -1722,7 +2320,7 @@ leaseCmd
1722
2320
 
1723
2321
  if (!lease || !task) {
1724
2322
  if (opts.json) console.log(JSON.stringify({ lease: null, task: null }));
1725
- else console.log(chalk.red(`Could not acquire lease. The task may not exist or is not pending.`));
2323
+ else printErrorWithNext('Could not start a work session. The task may not exist or may already be in progress.', 'switchman task list --status pending');
1726
2324
  process.exitCode = 1;
1727
2325
  return;
1728
2326
  }
@@ -1742,10 +2340,16 @@ leaseCmd
1742
2340
 
1743
2341
  leaseCmd
1744
2342
  .command('next')
1745
- .description('Claim the next pending task and acquire its lease')
2343
+ .description('Start the next pending task and open a tracked work session for it')
1746
2344
  .option('--json', 'Output as JSON')
1747
- .option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
2345
+ .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
1748
2346
  .option('--agent <name>', 'Agent identifier for logging')
2347
+ .addHelpText('after', `
2348
+ Examples:
2349
+ switchman lease next
2350
+ switchman lease next --json
2351
+ switchman lease next --worktree agent2 --agent cursor
2352
+ `)
1749
2353
  .action((opts) => {
1750
2354
  const repoRoot = getRepo();
1751
2355
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -1753,7 +2357,7 @@ leaseCmd
1753
2357
 
1754
2358
  if (!task) {
1755
2359
  if (opts.json) console.log(JSON.stringify({ task: null, lease: null }));
1756
- else if (exhausted) console.log(chalk.dim('No pending tasks.'));
2360
+ else if (exhausted) console.log(chalk.dim('No pending tasks. Add one with `switchman task add "Your task"`.'));
1757
2361
  else console.log(chalk.yellow('Tasks were claimed by other agents during assignment. Run again to get the next one.'));
1758
2362
  return;
1759
2363
  }
@@ -1816,7 +2420,7 @@ leaseCmd
1816
2420
 
1817
2421
  if (!lease) {
1818
2422
  if (opts.json) console.log(JSON.stringify({ lease: null }));
1819
- else console.log(chalk.red(`No active lease found for ${leaseId}`));
2423
+ else printErrorWithNext(`No active work session found for ${leaseId}.`, 'switchman lease list --status active');
1820
2424
  process.exitCode = 1;
1821
2425
  return;
1822
2426
  }
@@ -1832,9 +2436,14 @@ leaseCmd
1832
2436
 
1833
2437
  leaseCmd
1834
2438
  .command('reap')
1835
- .description('Expire stale leases, release their claims, and return their tasks to pending')
2439
+ .description('Clean up abandoned work sessions and release their file locks')
1836
2440
  .option('--stale-after-minutes <minutes>', 'Age threshold for staleness')
1837
2441
  .option('--json', 'Output as JSON')
2442
+ .addHelpText('after', `
2443
+ Examples:
2444
+ switchman lease reap
2445
+ switchman lease reap --stale-after-minutes 20
2446
+ `)
1838
2447
  .action((opts) => {
1839
2448
  const repoRoot = getRepo();
1840
2449
  const db = getDb(repoRoot);
@@ -1919,11 +2528,20 @@ leasePolicyCmd
1919
2528
 
1920
2529
  // ── worktree ───────────────────────────────────────────────────────────────────
1921
2530
 
1922
- const wtCmd = program.command('worktree').description('Manage worktrees');
2531
+ const wtCmd = program.command('worktree').alias('workspace').description('Manage registered workspaces (Git worktrees)');
2532
+ wtCmd.addHelpText('after', `
2533
+ Plain English:
2534
+ worktree = the Git feature behind each agent workspace
2535
+
2536
+ Examples:
2537
+ switchman worktree list
2538
+ switchman workspace list
2539
+ switchman worktree sync
2540
+ `);
1923
2541
 
1924
2542
  wtCmd
1925
2543
  .command('add <name> <path> <branch>')
1926
- .description('Register a worktree with switchman')
2544
+ .description('Register a workspace with Switchman')
1927
2545
  .option('--agent <name>', 'Agent assigned to this worktree')
1928
2546
  .action((name, path, branch, opts) => {
1929
2547
  const repoRoot = getRepo();
@@ -1935,7 +2553,7 @@ wtCmd
1935
2553
 
1936
2554
  wtCmd
1937
2555
  .command('list')
1938
- .description('List all registered worktrees')
2556
+ .description('List all registered workspaces')
1939
2557
  .action(() => {
1940
2558
  const repoRoot = getRepo();
1941
2559
  const db = getDb(repoRoot);
@@ -1944,7 +2562,7 @@ wtCmd
1944
2562
  db.close();
1945
2563
 
1946
2564
  if (!worktrees.length && !gitWorktrees.length) {
1947
- console.log(chalk.dim('No worktrees found.'));
2565
+ console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
1948
2566
  return;
1949
2567
  }
1950
2568
 
@@ -1964,7 +2582,7 @@ wtCmd
1964
2582
 
1965
2583
  wtCmd
1966
2584
  .command('sync')
1967
- .description('Sync git worktrees into the switchman database')
2585
+ .description('Sync Git workspaces into the Switchman database')
1968
2586
  .action(() => {
1969
2587
  const repoRoot = getRepo();
1970
2588
  const db = getDb(repoRoot);
@@ -1981,12 +2599,20 @@ wtCmd
1981
2599
 
1982
2600
  program
1983
2601
  .command('claim <taskId> <worktree> [files...]')
1984
- .description('Claim files for a task (warns if conflicts exist)')
2602
+ .description('Lock files for a task before editing')
1985
2603
  .option('--agent <name>', 'Agent name')
1986
2604
  .option('--force', 'Claim even if conflicts exist')
2605
+ .addHelpText('after', `
2606
+ Examples:
2607
+ switchman claim task-123 agent2 src/auth.js src/server.js
2608
+ switchman claim task-123 agent2 src/auth.js --agent cursor
2609
+
2610
+ Use this before editing files in a shared repo.
2611
+ `)
1987
2612
  .action((taskId, worktree, files, opts) => {
1988
2613
  if (!files.length) {
1989
- console.log(chalk.yellow('No files specified. Use: switchman claim <taskId> <worktree> file1 file2 ...'));
2614
+ console.log(chalk.yellow('No files specified.'));
2615
+ console.log(`${chalk.yellow('next:')} switchman claim <taskId> <workspace> file1 file2`);
1990
2616
  return;
1991
2617
  }
1992
2618
  const repoRoot = getRepo();
@@ -2000,7 +2626,8 @@ program
2000
2626
  for (const c of conflicts) {
2001
2627
  console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
2002
2628
  }
2003
- console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
2629
+ console.log(chalk.dim('\nUse --force to claim anyway, or pick different files first.'));
2630
+ console.log(`${chalk.yellow('next:')} switchman status`);
2004
2631
  process.exitCode = 1;
2005
2632
  return;
2006
2633
  }
@@ -2009,7 +2636,7 @@ program
2009
2636
  console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
2010
2637
  files.forEach(f => console.log(` ${chalk.dim(f)}`));
2011
2638
  } catch (err) {
2012
- console.error(chalk.red(err.message));
2639
+ printErrorWithNext(err.message, 'switchman task list --status in_progress');
2013
2640
  process.exitCode = 1;
2014
2641
  } finally {
2015
2642
  db.close();
@@ -2181,6 +2808,12 @@ program
2181
2808
  .description('Scan all workspaces for conflicts')
2182
2809
  .option('--json', 'Output raw JSON')
2183
2810
  .option('--quiet', 'Only show conflicts')
2811
+ .addHelpText('after', `
2812
+ Examples:
2813
+ switchman scan
2814
+ switchman scan --quiet
2815
+ switchman scan --json
2816
+ `)
2184
2817
  .action(async (opts) => {
2185
2818
  const repoRoot = getRepo();
2186
2819
  const db = getDb(repoRoot);
@@ -2296,12 +2929,21 @@ program
2296
2929
  .option('--watch', 'Keep refreshing status in the terminal')
2297
2930
  .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
2298
2931
  .option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
2932
+ .addHelpText('after', `
2933
+ Examples:
2934
+ switchman status
2935
+ switchman status --watch
2936
+ switchman status --json
2937
+
2938
+ Use this first when the repo feels stuck.
2939
+ `)
2299
2940
  .action(async (opts) => {
2300
2941
  const repoRoot = getRepo();
2301
2942
  const watch = Boolean(opts.watch);
2302
2943
  const watchIntervalMs = Math.max(100, Number.parseInt(opts.watchIntervalMs, 10) || 2000);
2303
2944
  const maxCycles = Math.max(0, Number.parseInt(opts.maxCycles, 10) || 0);
2304
2945
  let cycles = 0;
2946
+ let lastSignature = null;
2305
2947
 
2306
2948
  while (true) {
2307
2949
  if (watch && process.stdout.isTTY && !opts.json) {
@@ -2316,8 +2958,16 @@ program
2316
2958
  } else {
2317
2959
  renderUnifiedStatusReport(report);
2318
2960
  if (watch) {
2961
+ const signature = buildWatchSignature(report);
2962
+ const watchState = lastSignature === null
2963
+ ? chalk.cyan('baseline snapshot')
2964
+ : signature === lastSignature
2965
+ ? chalk.dim('no repo changes since last refresh')
2966
+ : chalk.green('change detected');
2967
+ const updatedAt = formatClockTime(report.generated_at);
2968
+ lastSignature = signature;
2319
2969
  console.log('');
2320
- console.log(chalk.dim(`Watching every ${watchIntervalMs}ms${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''}`));
2970
+ console.log(chalk.dim(`Live watch • updated ${updatedAt || 'just now'}${watchState}${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''} • refresh ${watchIntervalMs}ms`));
2321
2971
  }
2322
2972
  }
2323
2973
 
@@ -2326,12 +2976,27 @@ program
2326
2976
  if (opts.json) break;
2327
2977
  sleepSync(watchIntervalMs);
2328
2978
  }
2979
+
2980
+ if (watch) {
2981
+ await maybeCaptureTelemetry('status_watch_used', {
2982
+ cycles,
2983
+ interval_ms: watchIntervalMs,
2984
+ });
2985
+ }
2329
2986
  });
2330
2987
 
2331
2988
  program
2332
2989
  .command('doctor')
2333
2990
  .description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
2334
2991
  .option('--json', 'Output raw JSON')
2992
+ .addHelpText('after', `
2993
+ Plain English:
2994
+ Use this when the repo feels risky, noisy, or stuck and you want the health summary plus exact next moves.
2995
+
2996
+ Examples:
2997
+ switchman doctor
2998
+ switchman doctor --json
2999
+ `)
2335
3000
  .action(async (opts) => {
2336
3001
  const repoRoot = getRepo();
2337
3002
  const db = getDb(repoRoot);
@@ -2356,66 +3021,83 @@ program
2356
3021
  return;
2357
3022
  }
2358
3023
 
2359
- const badge = report.health === 'healthy'
2360
- ? chalk.green('HEALTHY')
2361
- : report.health === 'warn'
2362
- ? chalk.yellow('ATTENTION')
2363
- : chalk.red('BLOCKED');
2364
- console.log(`${badge} ${report.summary}`);
2365
- console.log(chalk.dim(repoRoot));
2366
- console.log('');
2367
-
2368
- console.log(chalk.bold('At a glance:'));
2369
- console.log(` ${chalk.dim('tasks')} ${report.counts.pending} pending, ${report.counts.in_progress} in progress, ${report.counts.done} done, ${report.counts.failed} failed`);
2370
- console.log(` ${chalk.dim('leases')} ${report.counts.active_leases} active, ${report.counts.stale_leases} stale`);
2371
- console.log(` ${chalk.dim('merge')} CI ${report.merge_readiness.ci_gate_ok ? chalk.green('clear') : chalk.red('blocked')} AI ${report.merge_readiness.ai_gate_status}`);
3024
+ const doctorColor = colorForHealth(report.health);
3025
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
3026
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
3027
+ const focusItem = report.attention[0] || report.active_work[0] || null;
3028
+ const focusLine = focusItem
3029
+ ? `${focusItem.title || focusItem.task_title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
3030
+ : 'Nothing urgent. Repo health looks steady.';
2372
3031
 
2373
- if (report.active_work.length > 0) {
2374
- console.log('');
2375
- console.log(chalk.bold('Running now:'));
2376
- for (const item of report.active_work.slice(0, 5)) {
3032
+ console.log('');
3033
+ console.log(doctorColor('='.repeat(72)));
3034
+ console.log(`${doctorColor(healthLabel(report.health))} ${chalk.bold('switchman doctor')} ${chalk.dim('• repo health mission control')}`);
3035
+ console.log(chalk.dim(repoRoot));
3036
+ console.log(chalk.dim(report.summary));
3037
+ console.log(doctorColor('='.repeat(72)));
3038
+ console.log(renderSignalStrip([
3039
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
3040
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
3041
+ renderChip('leases', report.counts.active_leases, report.counts.active_leases > 0 ? chalk.blue : chalk.green),
3042
+ renderChip('stale', report.counts.stale_leases, report.counts.stale_leases > 0 ? chalk.red : chalk.green),
3043
+ renderChip('merge', report.merge_readiness.ci_gate_ok ? 'clear' : 'hold', report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red),
3044
+ ]));
3045
+ console.log(renderMetricRow([
3046
+ { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
3047
+ { label: 'AI gate', value: report.merge_readiness.ai_gate_status, color: report.merge_readiness.ai_gate_status === 'blocked' ? chalk.red : report.merge_readiness.ai_gate_status === 'warn' ? chalk.yellow : chalk.green },
3048
+ ]));
3049
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
3050
+
3051
+ const runningLines = report.active_work.length > 0
3052
+ ? report.active_work.slice(0, 5).map((item) => {
2377
3053
  const leaseId = activeLeases.find((lease) => lease.task_id === item.task_id && lease.worktree === item.worktree)?.id || null;
2378
3054
  const boundary = item.boundary_validation
2379
- ? ` ${chalk.dim(`validation:${item.boundary_validation.status}`)}`
3055
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
2380
3056
  : '';
2381
3057
  const stale = (item.dependency_invalidations?.length || 0) > 0
2382
- ? ` ${chalk.dim(`stale:${item.dependency_invalidations.length}`)}`
3058
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
2383
3059
  : '';
2384
- console.log(` ${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${leaseId ? ` ${chalk.dim(`lease:${leaseId}`)}` : ''}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`);
2385
- }
2386
- }
3060
+ return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${leaseId ? ` ${chalk.dim(`lease:${leaseId}`)}` : ''}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
3061
+ })
3062
+ : [chalk.dim('Nothing active right now.')];
3063
+
3064
+ const attentionLines = report.attention.length > 0
3065
+ ? report.attention.slice(0, 6).flatMap((item) => {
3066
+ const lines = [`${item.severity === 'block' ? renderChip('BLOCKED', item.kind || 'item', chalk.red) : renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
3067
+ if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
3068
+ lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
3069
+ if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
3070
+ return lines;
3071
+ })
3072
+ : [chalk.green('Nothing urgent.')];
3073
+
3074
+ const nextStepLines = [
3075
+ ...report.next_steps.slice(0, 4).map((step) => `- ${step}`),
3076
+ '',
3077
+ ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
3078
+ ];
2387
3079
 
2388
3080
  console.log('');
2389
3081
  console.log(chalk.bold('Attention now:'));
2390
- if (report.attention.length === 0) {
2391
- console.log(` ${chalk.green('Nothing urgent.')}`);
2392
- } else {
2393
- for (const item of report.attention.slice(0, 6)) {
2394
- const itemBadge = item.severity === 'block' ? chalk.red('block') : chalk.yellow('warn ');
2395
- console.log(` ${itemBadge} ${item.title}`);
2396
- if (item.detail) console.log(` ${chalk.dim(item.detail)}`);
2397
- console.log(` ${chalk.yellow('next:')} ${item.next_step}`);
2398
- if (item.command) console.log(` ${chalk.cyan('run:')} ${item.command}`);
2399
- }
2400
- }
2401
-
2402
- console.log('');
2403
- console.log(chalk.bold('Recommended next steps:'));
2404
- for (const step of report.next_steps) {
2405
- console.log(` - ${step}`);
2406
- }
2407
- if (report.suggested_commands.length > 0) {
3082
+ for (const block of [
3083
+ renderPanel('Running now', runningLines, chalk.cyan),
3084
+ renderPanel('Attention now', attentionLines, report.attention.some((item) => item.severity === 'block') ? chalk.red : report.attention.length > 0 ? chalk.yellow : chalk.green),
3085
+ renderPanel('Recommended next steps', nextStepLines, chalk.green),
3086
+ ]) {
3087
+ for (const line of block) console.log(line);
2408
3088
  console.log('');
2409
- console.log(chalk.bold('Suggested commands:'));
2410
- for (const command of report.suggested_commands) {
2411
- console.log(` ${chalk.cyan(command)}`);
2412
- }
2413
3089
  }
2414
3090
  });
2415
3091
 
2416
3092
  // ── gate ─────────────────────────────────────────────────────────────────────
2417
3093
 
2418
3094
  const gateCmd = program.command('gate').description('Safety checks for edits, merges, and CI');
3095
+ gateCmd.addHelpText('after', `
3096
+ Examples:
3097
+ switchman gate ci
3098
+ switchman gate ai
3099
+ switchman gate install-ci
3100
+ `);
2419
3101
 
2420
3102
  const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
2421
3103
 
@@ -2612,6 +3294,15 @@ gateCmd
2612
3294
  }
2613
3295
  }
2614
3296
 
3297
+ await maybeCaptureTelemetry(ok ? 'gate_ci_passed' : 'gate_ci_failed', {
3298
+ worktree_count: report.worktrees.length,
3299
+ unclaimed_change_count: result.unclaimed_changes.length,
3300
+ file_conflict_count: result.file_conflicts.length,
3301
+ ownership_conflict_count: result.ownership_conflicts.length,
3302
+ semantic_conflict_count: result.semantic_conflicts.length,
3303
+ branch_conflict_count: result.branch_conflicts.length,
3304
+ });
3305
+
2615
3306
  if (!ok) process.exitCode = 1;
2616
3307
  });
2617
3308