switchman-dev 0.1.3 → 0.1.5

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
@@ -7,18 +7,19 @@
7
7
  * switchman init - Initialize in current repo
8
8
  * switchman task add - Add a task to the queue
9
9
  * switchman task list - List all tasks
10
- * switchman task assign - Assign task to a worktree
10
+ * switchman task assign - Assign task to a workspace
11
11
  * switchman task done - Mark task complete
12
- * switchman worktree add - Register a worktree
13
- * switchman worktree list - List registered worktrees
14
- * switchman scan - Scan for conflicts across worktrees
12
+ * switchman worktree add - Register a workspace
13
+ * switchman worktree list - List registered workspaces
14
+ * switchman scan - Scan for conflicts across workspaces
15
15
  * switchman claim - Claim files for a task
16
- * switchman status - Show full system status
16
+ * switchman status - Show the repo dashboard
17
17
  */
18
18
 
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
 
@@ -29,20 +30,23 @@ import {
29
30
  createTask, startTaskLease, completeTask, failTask, getBoundaryValidationState, getTaskSpec, listTasks, getTask, getNextPendingTask,
30
31
  listDependencyInvalidations, listLeases, listScopeReservations, heartbeatLease, getStaleLeases, reapStaleLeases,
31
32
  registerWorktree, listWorktrees,
33
+ enqueueMergeItem, getMergeQueueItem, listMergeQueue, listMergeQueueEvents, removeMergeQueueItem, retryMergeQueueItem,
32
34
  claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts,
33
35
  verifyAuditTrail,
34
36
  } from '../core/db.js';
35
37
  import { scanAllWorktrees } from '../core/detector.js';
36
- import { upsertProjectMcpConfig } from '../core/mcp.js';
38
+ import { getWindsurfMcpConfigPath, upsertAllProjectMcpConfigs, upsertWindsurfMcpConfig } from '../core/mcp.js';
37
39
  import { gatewayAppendFile, gatewayMakeDirectory, gatewayMovePath, gatewayRemovePath, gatewayWriteFile, installGateHooks, monitorWorktreesOnce, runCommitGate, runWrappedCommand, writeEnforcementPolicy } from '../core/enforcement.js';
38
40
  import { runAiMergeGate } from '../core/merge-gate.js';
39
41
  import { clearMonitorState, getMonitorStatePath, isProcessRunning, readMonitorState, writeMonitorState } from '../core/monitor.js';
40
42
  import { buildPipelinePrSummary, createPipelineFollowupTasks, executePipeline, exportPipelinePrBundle, getPipelineStatus, publishPipelinePr, runPipeline, startPipeline } from '../core/pipeline.js';
41
43
  import { installGitHubActionsWorkflow, resolveGitHubOutputTargets, writeGitHubCiStatus } from '../core/ci.js';
42
44
  import { importCodeObjectsToStore, listCodeObjects, materializeCodeObjects, materializeSemanticIndex, updateCodeObjectSource } from '../core/semantic.js';
45
+ import { buildQueueStatusSummary, runMergeQueue } from '../core/queue.js';
46
+ import { DEFAULT_LEASE_POLICY, loadLeasePolicy, writeLeasePolicy } from '../core/policy.js';
43
47
 
44
48
  function installMcpConfig(targetDirs) {
45
- return targetDirs.map((targetDir) => upsertProjectMcpConfig(targetDir));
49
+ return targetDirs.flatMap((targetDir) => upsertAllProjectMcpConfigs(targetDir));
46
50
  }
47
51
 
48
52
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -80,6 +84,14 @@ function statusBadge(status) {
80
84
  observed: chalk.yellow,
81
85
  non_compliant: chalk.red,
82
86
  stale: chalk.red,
87
+ queued: chalk.yellow,
88
+ validating: chalk.blue,
89
+ rebasing: chalk.blue,
90
+ retrying: chalk.yellow,
91
+ blocked: chalk.red,
92
+ merging: chalk.blue,
93
+ merged: chalk.green,
94
+ canceled: chalk.gray,
83
95
  };
84
96
  return (colors[status] || chalk.white)(status.toUpperCase().padEnd(11));
85
97
  }
@@ -117,10 +129,239 @@ function printTable(rows, columns) {
117
129
  }
118
130
  }
119
131
 
132
+ function padRight(value, width) {
133
+ return String(value).padEnd(width);
134
+ }
135
+
136
+ function stripAnsi(text) {
137
+ return String(text).replace(/\x1B\[[0-9;]*m/g, '');
138
+ }
139
+
140
+ function colorForHealth(health) {
141
+ if (health === 'healthy') return chalk.green;
142
+ if (health === 'warn') return chalk.yellow;
143
+ return chalk.red;
144
+ }
145
+
146
+ function healthLabel(health) {
147
+ if (health === 'healthy') return 'HEALTHY';
148
+ if (health === 'warn') return 'ATTENTION';
149
+ return 'BLOCKED';
150
+ }
151
+
152
+ function renderPanel(title, lines, color = chalk.cyan) {
153
+ const content = lines.length > 0 ? lines : [chalk.dim('No items.')];
154
+ const width = Math.max(
155
+ stripAnsi(title).length + 2,
156
+ ...content.map((line) => stripAnsi(line).length),
157
+ );
158
+ const top = color(`+${'-'.repeat(width + 2)}+`);
159
+ const titleLine = color(`| ${padRight(title, width)} |`);
160
+ const body = content.map((line) => `| ${padRight(line, width)} |`);
161
+ const bottom = color(`+${'-'.repeat(width + 2)}+`);
162
+ return [top, titleLine, top, ...body, bottom];
163
+ }
164
+
165
+ function renderMetricRow(metrics) {
166
+ return metrics.map(({ label, value, color = chalk.white }) => `${chalk.dim(label)} ${color(String(value))}`).join(chalk.dim(' | '));
167
+ }
168
+
169
+ function renderMiniBar(items) {
170
+ if (!items.length) return chalk.dim('none');
171
+ return items.map(({ label, value, color = chalk.white }) => `${color('■')} ${label}:${value}`).join(chalk.dim(' '));
172
+ }
173
+
174
+ function renderChip(label, value, color = chalk.white) {
175
+ return color(`[${label}:${value}]`);
176
+ }
177
+
178
+ function renderSignalStrip(signals) {
179
+ return signals.join(chalk.dim(' '));
180
+ }
181
+
182
+ function formatClockTime(isoString) {
183
+ if (!isoString) return null;
184
+ const date = new Date(isoString);
185
+ if (Number.isNaN(date.getTime())) return null;
186
+ return date.toLocaleTimeString('en-GB', {
187
+ hour: '2-digit',
188
+ minute: '2-digit',
189
+ second: '2-digit',
190
+ hour12: false,
191
+ });
192
+ }
193
+
194
+ function buildWatchSignature(report) {
195
+ return JSON.stringify({
196
+ health: report.health,
197
+ summary: report.summary,
198
+ counts: report.counts,
199
+ active_work: report.active_work,
200
+ attention: report.attention,
201
+ queue_summary: report.queue?.summary || null,
202
+ next_up: report.next_up || null,
203
+ next_steps: report.next_steps,
204
+ suggested_commands: report.suggested_commands,
205
+ });
206
+ }
207
+
208
+ function formatRelativePolicy(policy) {
209
+ return `stale ${policy.stale_after_minutes}m • heartbeat ${policy.heartbeat_interval_seconds}s • auto-reap ${policy.reap_on_status_check ? 'on' : 'off'}`;
210
+ }
211
+
120
212
  function sleepSync(ms) {
121
213
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
122
214
  }
123
215
 
216
+ function boolBadge(ok) {
217
+ return ok ? chalk.green('OK ') : chalk.yellow('CHECK');
218
+ }
219
+
220
+ function printErrorWithNext(message, nextCommand = null) {
221
+ console.error(chalk.red(message));
222
+ if (nextCommand) {
223
+ console.error(`${chalk.yellow('next:')} ${nextCommand}`);
224
+ }
225
+ }
226
+
227
+ function collectSetupVerification(repoRoot, { homeDir = null } = {}) {
228
+ const dbPath = join(repoRoot, '.switchman', 'switchman.db');
229
+ const rootMcpPath = join(repoRoot, '.mcp.json');
230
+ const cursorMcpPath = join(repoRoot, '.cursor', 'mcp.json');
231
+ const claudeGuidePath = join(repoRoot, 'CLAUDE.md');
232
+ const checks = [];
233
+ const nextSteps = [];
234
+ let workspaces = [];
235
+ let db = null;
236
+
237
+ const dbExists = existsSync(dbPath);
238
+ checks.push({
239
+ key: 'database',
240
+ ok: dbExists,
241
+ label: 'Project database',
242
+ detail: dbExists ? '.switchman/switchman.db is ready' : 'Switchman database is missing',
243
+ });
244
+ if (!dbExists) {
245
+ nextSteps.push('Run `switchman init` or `switchman setup --agents 3` in this repo.');
246
+ }
247
+
248
+ if (dbExists) {
249
+ try {
250
+ db = getDb(repoRoot);
251
+ workspaces = listWorktrees(db);
252
+ } catch {
253
+ checks.push({
254
+ key: 'database_open',
255
+ ok: false,
256
+ label: 'Database access',
257
+ detail: 'Switchman could not open the project database',
258
+ });
259
+ nextSteps.push('Re-run `switchman init` if the project database looks corrupted.');
260
+ } finally {
261
+ try { db?.close(); } catch { /* no-op */ }
262
+ }
263
+ }
264
+
265
+ const agentWorkspaces = workspaces.filter((entry) => entry.name !== 'main');
266
+ const workspaceReady = agentWorkspaces.length > 0;
267
+ checks.push({
268
+ key: 'workspaces',
269
+ ok: workspaceReady,
270
+ label: 'Agent workspaces',
271
+ detail: workspaceReady
272
+ ? `${agentWorkspaces.length} agent workspace(s) registered`
273
+ : 'No agent workspaces are registered yet',
274
+ });
275
+ if (!workspaceReady) {
276
+ nextSteps.push('Run `switchman setup --agents 3` to create agent workspaces.');
277
+ }
278
+
279
+ const rootMcpExists = existsSync(rootMcpPath);
280
+ checks.push({
281
+ key: 'claude_mcp',
282
+ ok: rootMcpExists,
283
+ label: 'Claude Code MCP',
284
+ detail: rootMcpExists ? '.mcp.json is present in the repo root' : '.mcp.json is missing from the repo root',
285
+ });
286
+ if (!rootMcpExists) {
287
+ nextSteps.push('Re-run `switchman setup --agents 3` to restore the repo-local MCP config.');
288
+ }
289
+
290
+ const cursorMcpExists = existsSync(cursorMcpPath);
291
+ checks.push({
292
+ key: 'cursor_mcp',
293
+ ok: cursorMcpExists,
294
+ label: 'Cursor MCP',
295
+ detail: cursorMcpExists ? '.cursor/mcp.json is present in the repo root' : '.cursor/mcp.json is missing from the repo root',
296
+ });
297
+ if (!cursorMcpExists) {
298
+ nextSteps.push('Re-run `switchman setup --agents 3` if you want Cursor to attach automatically.');
299
+ }
300
+
301
+ const claudeGuideExists = existsSync(claudeGuidePath);
302
+ checks.push({
303
+ key: 'claude_md',
304
+ ok: claudeGuideExists,
305
+ label: 'Claude guide',
306
+ detail: claudeGuideExists ? 'CLAUDE.md is present' : 'CLAUDE.md is optional but recommended for Claude Code',
307
+ });
308
+ if (!claudeGuideExists) {
309
+ nextSteps.push('If you use Claude Code, add `CLAUDE.md` from the repo root setup guide.');
310
+ }
311
+
312
+ const windsurfConfigExists = existsSync(getWindsurfMcpConfigPath(homeDir || undefined));
313
+ checks.push({
314
+ key: 'windsurf_mcp',
315
+ ok: windsurfConfigExists,
316
+ label: 'Windsurf MCP',
317
+ detail: windsurfConfigExists
318
+ ? 'Windsurf shared MCP config is installed'
319
+ : 'Windsurf shared MCP config is optional and not installed',
320
+ });
321
+ if (!windsurfConfigExists) {
322
+ nextSteps.push('If you use Windsurf, run `switchman mcp install --windsurf` once.');
323
+ }
324
+
325
+ const ok = checks.every((item) => item.ok || ['claude_md', 'windsurf_mcp'].includes(item.key));
326
+ return {
327
+ ok,
328
+ repo_root: repoRoot,
329
+ checks,
330
+ workspaces: workspaces.map((entry) => ({
331
+ name: entry.name,
332
+ path: entry.path,
333
+ branch: entry.branch,
334
+ })),
335
+ suggested_commands: [
336
+ 'switchman status --watch',
337
+ 'switchman task add "Your first task" --priority 8',
338
+ 'switchman gate ci',
339
+ ...nextSteps.some((step) => step.includes('Windsurf')) ? ['switchman mcp install --windsurf'] : [],
340
+ ],
341
+ next_steps: [...new Set(nextSteps)].slice(0, 6),
342
+ };
343
+ }
344
+
345
+ function renderSetupVerification(report, { compact = false } = {}) {
346
+ console.log(chalk.bold(compact ? 'First-run check:' : 'Setup verification:'));
347
+ for (const check of report.checks) {
348
+ const badge = boolBadge(check.ok);
349
+ console.log(` ${badge} ${check.label} ${chalk.dim(`— ${check.detail}`)}`);
350
+ }
351
+ if (report.next_steps.length > 0) {
352
+ console.log('');
353
+ console.log(chalk.bold('Fix next:'));
354
+ for (const step of report.next_steps) {
355
+ console.log(` - ${step}`);
356
+ }
357
+ }
358
+ console.log('');
359
+ console.log(chalk.bold('Try next:'));
360
+ for (const command of report.suggested_commands.slice(0, 4)) {
361
+ console.log(` ${chalk.cyan(command)}`);
362
+ }
363
+ }
364
+
124
365
  function summarizeLeaseScope(db, lease) {
125
366
  const reservations = listScopeReservations(db, { leaseId: lease.id });
126
367
  const pathScopes = reservations
@@ -454,6 +695,303 @@ function buildDoctorReport({ db, repoRoot, tasks, activeLeases, staleLeases, sca
454
695
  };
455
696
  }
456
697
 
698
+ function buildUnifiedStatusReport({
699
+ repoRoot,
700
+ leasePolicy,
701
+ tasks,
702
+ claims,
703
+ doctorReport,
704
+ queueItems,
705
+ queueSummary,
706
+ recentQueueEvents,
707
+ }) {
708
+ const queueAttention = [
709
+ ...queueItems
710
+ .filter((item) => item.status === 'blocked')
711
+ .map((item) => ({
712
+ kind: 'queue_blocked',
713
+ title: `${item.id} is blocked from landing`,
714
+ detail: item.last_error_summary || `${item.source_type}:${item.source_ref}`,
715
+ next_step: item.next_action || `Run \`switchman queue retry ${item.id}\` after fixing the branch state.`,
716
+ command: item.next_action?.includes('queue retry') ? `switchman queue retry ${item.id}` : 'switchman queue status',
717
+ severity: 'block',
718
+ })),
719
+ ...queueItems
720
+ .filter((item) => item.status === 'retrying')
721
+ .map((item) => ({
722
+ kind: 'queue_retrying',
723
+ title: `${item.id} is waiting for another landing attempt`,
724
+ detail: item.last_error_summary || `${item.source_type}:${item.source_ref}`,
725
+ next_step: item.next_action || 'Run `switchman queue run` again to continue landing queued work.',
726
+ command: 'switchman queue run',
727
+ severity: 'warn',
728
+ })),
729
+ ];
730
+
731
+ const attention = [...doctorReport.attention, ...queueAttention];
732
+ const nextUp = tasks
733
+ .filter((task) => task.status === 'pending')
734
+ .sort((a, b) => Number(b.priority || 0) - Number(a.priority || 0))
735
+ .slice(0, 3)
736
+ .map((task) => ({
737
+ id: task.id,
738
+ title: task.title,
739
+ priority: task.priority,
740
+ }));
741
+ const failedTasks = tasks
742
+ .filter((task) => task.status === 'failed')
743
+ .slice(0, 5)
744
+ .map((task) => ({
745
+ id: task.id,
746
+ title: task.title,
747
+ failure: latestTaskFailure(task),
748
+ }));
749
+
750
+ const suggestedCommands = [
751
+ ...doctorReport.suggested_commands,
752
+ ...(queueItems.length > 0 ? ['switchman queue status'] : []),
753
+ ...(queueSummary.next ? ['switchman queue run'] : []),
754
+ ].filter(Boolean);
755
+
756
+ return {
757
+ generated_at: new Date().toISOString(),
758
+ repo_root: repoRoot,
759
+ health: attention.some((item) => item.severity === 'block')
760
+ ? 'block'
761
+ : attention.some((item) => item.severity === 'warn')
762
+ ? 'warn'
763
+ : doctorReport.health,
764
+ summary: attention.some((item) => item.severity === 'block')
765
+ ? 'Repo needs attention before more work or merge.'
766
+ : attention.some((item) => item.severity === 'warn')
767
+ ? 'Repo is running, but a few items need review.'
768
+ : 'Repo looks healthy. Agents are coordinated and merge checks are clear.',
769
+ lease_policy: leasePolicy,
770
+ counts: {
771
+ ...doctorReport.counts,
772
+ queue: queueSummary.counts,
773
+ active_claims: claims.length,
774
+ },
775
+ active_work: doctorReport.active_work,
776
+ attention,
777
+ next_up: nextUp,
778
+ failed_tasks: failedTasks,
779
+ queue: {
780
+ items: queueItems,
781
+ summary: queueSummary,
782
+ recent_events: recentQueueEvents,
783
+ },
784
+ merge_readiness: doctorReport.merge_readiness,
785
+ claims: claims.map((claim) => ({
786
+ worktree: claim.worktree,
787
+ task_id: claim.task_id,
788
+ file_path: claim.file_path,
789
+ })),
790
+ next_steps: [...new Set([
791
+ ...doctorReport.next_steps,
792
+ ...queueAttention.map((item) => item.next_step),
793
+ ])].slice(0, 6),
794
+ suggested_commands: [...new Set(suggestedCommands)].slice(0, 6),
795
+ };
796
+ }
797
+
798
+ async function collectStatusSnapshot(repoRoot) {
799
+ const db = getDb(repoRoot);
800
+ try {
801
+ const leasePolicy = loadLeasePolicy(repoRoot);
802
+
803
+ if (leasePolicy.reap_on_status_check) {
804
+ reapStaleLeases(db, leasePolicy.stale_after_minutes, {
805
+ requeueTask: leasePolicy.requeue_task_on_reap,
806
+ });
807
+ }
808
+
809
+ const tasks = listTasks(db);
810
+ const activeLeases = listLeases(db, 'active');
811
+ const staleLeases = getStaleLeases(db, leasePolicy.stale_after_minutes);
812
+ const claims = getActiveFileClaims(db);
813
+ const queueItems = listMergeQueue(db);
814
+ const queueSummary = buildQueueStatusSummary(queueItems);
815
+ const recentQueueEvents = queueItems
816
+ .slice(0, 5)
817
+ .flatMap((item) => listMergeQueueEvents(db, item.id, { limit: 3 }).map((event) => ({ ...event, queue_item_id: item.id })))
818
+ .sort((a, b) => b.id - a.id)
819
+ .slice(0, 8);
820
+ const scanReport = await scanAllWorktrees(db, repoRoot);
821
+ const aiGate = await runAiMergeGate(db, repoRoot);
822
+ const doctorReport = buildDoctorReport({
823
+ db,
824
+ repoRoot,
825
+ tasks,
826
+ activeLeases,
827
+ staleLeases,
828
+ scanReport,
829
+ aiGate,
830
+ });
831
+
832
+ return buildUnifiedStatusReport({
833
+ repoRoot,
834
+ leasePolicy,
835
+ tasks,
836
+ claims,
837
+ doctorReport,
838
+ queueItems,
839
+ queueSummary,
840
+ recentQueueEvents,
841
+ });
842
+ } finally {
843
+ db.close();
844
+ }
845
+ }
846
+
847
+ function renderUnifiedStatusReport(report) {
848
+ const healthColor = colorForHealth(report.health);
849
+ const badge = healthColor(healthLabel(report.health));
850
+ const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
851
+ const queueCounts = report.counts.queue;
852
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
853
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
854
+ const focusItem = blockedCount > 0
855
+ ? report.attention.find((item) => item.severity === 'block')
856
+ : warningCount > 0
857
+ ? report.attention.find((item) => item.severity !== 'block')
858
+ : report.next_up[0];
859
+ const focusLine = focusItem
860
+ ? ('title' in focusItem
861
+ ? `${focusItem.title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
862
+ : `${focusItem.title} ${chalk.dim(focusItem.id)}`)
863
+ : 'Nothing urgent. Safe to keep parallel work moving.';
864
+ const queueLoad = queueCounts.queued + queueCounts.retrying + queueCounts.merging + queueCounts.blocked;
865
+ const landingLabel = report.merge_readiness.ci_gate_ok ? 'ready' : 'hold';
866
+
867
+ console.log('');
868
+ console.log(healthColor('='.repeat(72)));
869
+ console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
870
+ console.log(`${chalk.dim(report.repo_root)}`);
871
+ console.log(`${chalk.dim(report.summary)}`);
872
+ console.log(healthColor('='.repeat(72)));
873
+ console.log(renderSignalStrip([
874
+ renderChip('health', healthLabel(report.health), healthColor),
875
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
876
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
877
+ renderChip('landing', landingLabel, mergeColor),
878
+ renderChip('queue', queueLoad, queueLoad > 0 ? chalk.blue : chalk.green),
879
+ ]));
880
+ console.log(renderMetricRow([
881
+ { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
882
+ { label: 'leases', value: `${report.counts.active_leases} active`, color: chalk.blue },
883
+ { label: 'claims', value: report.counts.active_claims, color: chalk.cyan },
884
+ { label: 'merge', value: report.merge_readiness.ci_gate_ok ? 'clear' : 'blocked', color: mergeColor },
885
+ ]));
886
+ console.log(renderMiniBar([
887
+ { label: 'queued', value: queueCounts.queued, color: chalk.yellow },
888
+ { label: 'retrying', value: queueCounts.retrying, color: chalk.yellow },
889
+ { label: 'blocked', value: queueCounts.blocked, color: chalk.red },
890
+ { label: 'merging', value: queueCounts.merging, color: chalk.blue },
891
+ { label: 'merged', value: queueCounts.merged, color: chalk.green },
892
+ ]));
893
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
894
+ console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
895
+
896
+ const runningLines = report.active_work.length > 0
897
+ ? report.active_work.slice(0, 5).map((item) => {
898
+ const boundary = item.boundary_validation
899
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
900
+ : '';
901
+ const stale = (item.dependency_invalidations?.length || 0) > 0
902
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
903
+ : '';
904
+ return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
905
+ })
906
+ : [chalk.dim('Nothing active right now.')];
907
+
908
+ const blockedItems = report.attention.filter((item) => item.severity === 'block');
909
+ const warningItems = report.attention.filter((item) => item.severity !== 'block');
910
+
911
+ const blockedLines = blockedItems.length > 0
912
+ ? blockedItems.slice(0, 4).flatMap((item) => {
913
+ const lines = [`${renderChip('BLOCKED', item.kind || 'item', chalk.red)} ${item.title}`];
914
+ if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
915
+ lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
916
+ if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
917
+ return lines;
918
+ })
919
+ : [chalk.green('Nothing blocked.')];
920
+
921
+ const warningLines = warningItems.length > 0
922
+ ? warningItems.slice(0, 4).flatMap((item) => {
923
+ const lines = [`${renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
924
+ if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
925
+ lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
926
+ if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
927
+ return lines;
928
+ })
929
+ : [chalk.green('Nothing warning-worthy right now.')];
930
+
931
+ const queueLines = report.queue.items.length > 0
932
+ ? [
933
+ ...(report.queue.summary.next
934
+ ? [`${chalk.dim('next:')} ${report.queue.summary.next.id} ${report.queue.summary.next.source_type}:${report.queue.summary.next.source_ref} ${chalk.dim(`retries:${report.queue.summary.next.retry_count}/${report.queue.summary.next.max_retries}`)}`]
935
+ : []),
936
+ ...report.queue.items
937
+ .filter((entry) => ['blocked', 'retrying', 'merging'].includes(entry.status))
938
+ .slice(0, 4)
939
+ .flatMap((item) => {
940
+ 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}`)}`];
941
+ if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
942
+ if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
943
+ return lines;
944
+ }),
945
+ ]
946
+ : [chalk.dim('No queued merges.')];
947
+
948
+ const nextActionLines = [
949
+ ...(report.next_up.length > 0
950
+ ? report.next_up.map((task) => `${renderChip('NEXT', `p${task.priority}`, chalk.green)} ${task.title} ${chalk.dim(task.id)}`)
951
+ : [chalk.dim('No pending tasks waiting right now.')]),
952
+ '',
953
+ ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
954
+ ];
955
+
956
+ const panelBlocks = [
957
+ renderPanel('Running now', runningLines, chalk.cyan),
958
+ renderPanel('Blocked', blockedLines, blockedItems.length > 0 ? chalk.red : chalk.green),
959
+ renderPanel('Warnings', warningLines, warningItems.length > 0 ? chalk.yellow : chalk.green),
960
+ renderPanel('Landing queue', queueLines, queueCounts.blocked > 0 ? chalk.red : chalk.blue),
961
+ renderPanel('Next action', nextActionLines, chalk.green),
962
+ ];
963
+
964
+ console.log('');
965
+ for (const block of panelBlocks) {
966
+ for (const line of block) console.log(line);
967
+ console.log('');
968
+ }
969
+
970
+ if (report.failed_tasks.length > 0) {
971
+ console.log(chalk.bold('Recent failed tasks:'));
972
+ for (const task of report.failed_tasks) {
973
+ const reason = humanizeReasonCode(task.failure?.reason_code);
974
+ const summary = task.failure?.summary || 'unknown failure';
975
+ console.log(` ${chalk.red(task.title)} ${chalk.dim(task.id)}`);
976
+ console.log(` ${chalk.red('why:')} ${summary} ${chalk.dim(`(${reason})`)}`);
977
+ }
978
+ console.log('');
979
+ }
980
+
981
+ if (report.queue.recent_events.length > 0) {
982
+ console.log(chalk.bold('Recent queue events:'));
983
+ for (const event of report.queue.recent_events.slice(0, 5)) {
984
+ console.log(` ${chalk.cyan(event.queue_item_id)} ${chalk.dim(event.event_type)} ${chalk.dim(event.status || '')} ${chalk.dim(event.created_at)}`.trim());
985
+ }
986
+ console.log('');
987
+ }
988
+
989
+ console.log(chalk.bold('Recommended next steps:'));
990
+ for (const step of report.next_steps) {
991
+ console.log(` - ${step}`);
992
+ }
993
+ }
994
+
457
995
  function acquireNextTaskLease(db, worktreeName, agent, attempts = 20) {
458
996
  for (let attempt = 1; attempt <= attempts; attempt++) {
459
997
  try {
@@ -530,6 +1068,23 @@ program
530
1068
  .description('Conflict-aware task coordinator for parallel AI coding agents')
531
1069
  .version('0.1.0');
532
1070
 
1071
+ program.showHelpAfterError('(run with --help for usage examples)');
1072
+ program.addHelpText('after', `
1073
+ Start here:
1074
+ switchman setup --agents 5
1075
+ switchman status --watch
1076
+ switchman gate ci
1077
+
1078
+ Most useful commands:
1079
+ switchman task add "Implement auth helper" --priority 9
1080
+ switchman lease next --json
1081
+ switchman queue run --watch
1082
+
1083
+ Docs:
1084
+ README.md
1085
+ docs/setup-cursor.md
1086
+ `);
1087
+
533
1088
  // ── init ──────────────────────────────────────────────────────────────────────
534
1089
 
535
1090
  program
@@ -570,9 +1125,14 @@ program
570
1125
 
571
1126
  program
572
1127
  .command('setup')
573
- .description('One-command setup: create agent worktrees and initialise switchman')
574
- .option('-a, --agents <n>', 'Number of agent worktrees to create (default: 3)', '3')
1128
+ .description('One-command setup: create agent workspaces and initialise Switchman')
1129
+ .option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
575
1130
  .option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
1131
+ .addHelpText('after', `
1132
+ Examples:
1133
+ switchman setup --agents 5
1134
+ switchman setup --agents 3 --prefix team
1135
+ `)
576
1136
  .action((opts) => {
577
1137
  const agentCount = parseInt(opts.agents);
578
1138
 
@@ -592,7 +1152,7 @@ program
592
1152
  stdio: ['pipe', 'pipe', 'pipe'],
593
1153
  });
594
1154
  } catch {
595
- spinner.fail('Your repo needs at least one commit before worktrees can be created.');
1155
+ spinner.fail('Your repo needs at least one commit before agent workspaces can be created.');
596
1156
  console.log(chalk.dim(' Run: git commit --allow-empty -m "init" then try again'));
597
1157
  process.exit(1);
598
1158
  }
@@ -600,7 +1160,7 @@ program
600
1160
  // Init the switchman database
601
1161
  const db = initDb(repoRoot);
602
1162
 
603
- // Create one worktree per agent
1163
+ // Create one workspace (git worktree) per agent
604
1164
  const created = [];
605
1165
  for (let i = 1; i <= agentCount; i++) {
606
1166
  const name = `agent${i}`;
@@ -650,21 +1210,97 @@ program
650
1210
  console.log(chalk.bold('Next steps:'));
651
1211
  console.log(` 1. Add your tasks:`);
652
1212
  console.log(` ${chalk.cyan('switchman task add "Your first task" --priority 8')}`);
653
- console.log(` 2. Open Claude Code in each folder above — the local .mcp.json will attach Switchman automatically`);
1213
+ console.log(` 2. Open Claude Code or Cursor in each folder above — the local MCP config will attach Switchman automatically`);
654
1214
  console.log(` 3. Check status at any time:`);
655
1215
  console.log(` ${chalk.cyan('switchman status')}`);
656
1216
  console.log('');
657
1217
 
1218
+ const verification = collectSetupVerification(repoRoot);
1219
+ renderSetupVerification(verification, { compact: true });
1220
+
658
1221
  } catch (err) {
659
1222
  spinner.fail(err.message);
660
1223
  process.exit(1);
661
1224
  }
662
1225
  });
663
1226
 
1227
+ program
1228
+ .command('verify-setup')
1229
+ .description('Check whether this repo is ready for a smooth first Switchman run')
1230
+ .option('--json', 'Output raw JSON')
1231
+ .option('--home <path>', 'Override the home directory for editor config checks')
1232
+ .addHelpText('after', `
1233
+ Examples:
1234
+ switchman verify-setup
1235
+ switchman verify-setup --json
1236
+
1237
+ Use this after setup or whenever editor/config wiring feels off.
1238
+ `)
1239
+ .action((opts) => {
1240
+ const repoRoot = getRepo();
1241
+ const report = collectSetupVerification(repoRoot, { homeDir: opts.home || null });
1242
+
1243
+ if (opts.json) {
1244
+ console.log(JSON.stringify(report, null, 2));
1245
+ if (!report.ok) process.exitCode = 1;
1246
+ return;
1247
+ }
1248
+
1249
+ renderSetupVerification(report);
1250
+ if (!report.ok) process.exitCode = 1;
1251
+ });
1252
+
1253
+
1254
+ // ── mcp ───────────────────────────────────────────────────────────────────────
1255
+
1256
+ const mcpCmd = program.command('mcp').description('Manage editor connections for Switchman');
1257
+
1258
+ mcpCmd
1259
+ .command('install')
1260
+ .description('Install editor-specific MCP config for Switchman')
1261
+ .option('--windsurf', 'Write Windsurf MCP config to ~/.codeium/mcp_config.json')
1262
+ .option('--home <path>', 'Override the home directory for config writes (useful for testing)')
1263
+ .option('--json', 'Output raw JSON')
1264
+ .addHelpText('after', `
1265
+ Examples:
1266
+ switchman mcp install --windsurf
1267
+ switchman mcp install --windsurf --json
1268
+ `)
1269
+ .action((opts) => {
1270
+ if (!opts.windsurf) {
1271
+ console.error(chalk.red('Choose an editor install target, for example `switchman mcp install --windsurf`.'));
1272
+ process.exitCode = 1;
1273
+ return;
1274
+ }
1275
+
1276
+ const result = upsertWindsurfMcpConfig(opts.home);
1277
+
1278
+ if (opts.json) {
1279
+ console.log(JSON.stringify({
1280
+ editor: 'windsurf',
1281
+ path: result.path,
1282
+ created: result.created,
1283
+ changed: result.changed,
1284
+ }, null, 2));
1285
+ return;
1286
+ }
1287
+
1288
+ console.log(`${chalk.green('✓')} Windsurf MCP config ${result.changed ? 'written' : 'already up to date'}`);
1289
+ console.log(` ${chalk.dim('path:')} ${chalk.cyan(result.path)}`);
1290
+ console.log(` ${chalk.dim('open:')} Windsurf -> Settings -> Cascade -> MCP Servers`);
1291
+ console.log(` ${chalk.dim('note:')} Windsurf reads the shared config from ${getWindsurfMcpConfigPath(opts.home)}`);
1292
+ });
1293
+
664
1294
 
665
1295
  // ── task ──────────────────────────────────────────────────────────────────────
666
1296
 
667
- const taskCmd = program.command('task').description('Manage the task queue');
1297
+ const taskCmd = program.command('task').description('Manage the task list');
1298
+ taskCmd.addHelpText('after', `
1299
+ Examples:
1300
+ switchman task add "Fix login bug" --priority 8
1301
+ switchman task list --status pending
1302
+ switchman task done task-123
1303
+ `);
668
1304
 
669
1305
  taskCmd
670
1306
  .command('add <title>')
@@ -720,7 +1356,7 @@ taskCmd
720
1356
 
721
1357
  taskCmd
722
1358
  .command('assign <taskId> <worktree>')
723
- .description('Assign a task to a worktree (compatibility shim for lease acquire)')
1359
+ .description('Assign a task to a workspace (compatibility shim for lease acquire)')
724
1360
  .option('--agent <name>', 'Agent name (e.g. claude-code)')
725
1361
  .action((taskId, worktree, opts) => {
726
1362
  const repoRoot = getRepo();
@@ -762,10 +1398,15 @@ taskCmd
762
1398
 
763
1399
  taskCmd
764
1400
  .command('next')
765
- .description('Get and assign the next pending task (compatibility shim for lease next)')
1401
+ .description('Get the next pending task quickly (use `lease next` for the full workflow)')
766
1402
  .option('--json', 'Output as JSON')
767
- .option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
1403
+ .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
768
1404
  .option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
1405
+ .addHelpText('after', `
1406
+ Examples:
1407
+ switchman task next
1408
+ switchman task next --json
1409
+ `)
769
1410
  .action((opts) => {
770
1411
  const repoRoot = getRepo();
771
1412
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -792,9 +1433,358 @@ taskCmd
792
1433
  }
793
1434
  });
794
1435
 
1436
+ // ── queue ─────────────────────────────────────────────────────────────────────
1437
+
1438
+ const queueCmd = program.command('queue').alias('land').description('Land finished work safely back onto main, one item at a time');
1439
+ queueCmd.addHelpText('after', `
1440
+ Examples:
1441
+ switchman queue add --worktree agent1
1442
+ switchman queue status
1443
+ switchman queue run --watch
1444
+ `);
1445
+
1446
+ queueCmd
1447
+ .command('add [branch]')
1448
+ .description('Add a branch, workspace, or pipeline to the landing queue')
1449
+ .option('--worktree <name>', 'Queue a registered workspace by name')
1450
+ .option('--pipeline <pipelineId>', 'Queue a pipeline by id')
1451
+ .option('--target <branch>', 'Target branch to merge into', 'main')
1452
+ .option('--max-retries <n>', 'Maximum automatic retries', '1')
1453
+ .option('--submitted-by <name>', 'Operator or automation name')
1454
+ .option('--json', 'Output raw JSON')
1455
+ .addHelpText('after', `
1456
+ Examples:
1457
+ switchman queue add feature/auth-hardening
1458
+ switchman queue add --worktree agent2
1459
+ switchman queue add --pipeline pipe-123
1460
+ `)
1461
+ .action((branch, opts) => {
1462
+ const repoRoot = getRepo();
1463
+ const db = getDb(repoRoot);
1464
+
1465
+ try {
1466
+ let payload;
1467
+ if (opts.worktree) {
1468
+ const worktree = listWorktrees(db).find((entry) => entry.name === opts.worktree);
1469
+ if (!worktree) {
1470
+ throw new Error(`Workspace ${opts.worktree} is not registered.`);
1471
+ }
1472
+ payload = {
1473
+ sourceType: 'worktree',
1474
+ sourceRef: worktree.branch,
1475
+ sourceWorktree: worktree.name,
1476
+ targetBranch: opts.target,
1477
+ maxRetries: opts.maxRetries,
1478
+ submittedBy: opts.submittedBy || null,
1479
+ };
1480
+ } else if (opts.pipeline) {
1481
+ payload = {
1482
+ sourceType: 'pipeline',
1483
+ sourceRef: opts.pipeline,
1484
+ sourcePipelineId: opts.pipeline,
1485
+ targetBranch: opts.target,
1486
+ maxRetries: opts.maxRetries,
1487
+ submittedBy: opts.submittedBy || null,
1488
+ };
1489
+ } else if (branch) {
1490
+ payload = {
1491
+ sourceType: 'branch',
1492
+ sourceRef: branch,
1493
+ targetBranch: opts.target,
1494
+ maxRetries: opts.maxRetries,
1495
+ submittedBy: opts.submittedBy || null,
1496
+ };
1497
+ } else {
1498
+ throw new Error('Choose one source to land: a branch name, `--worktree`, or `--pipeline`.');
1499
+ }
1500
+
1501
+ const result = enqueueMergeItem(db, payload);
1502
+ db.close();
1503
+
1504
+ if (opts.json) {
1505
+ console.log(JSON.stringify(result, null, 2));
1506
+ return;
1507
+ }
1508
+
1509
+ console.log(`${chalk.green('✓')} Queued ${chalk.cyan(result.id)} for ${chalk.bold(result.target_branch)}`);
1510
+ console.log(` ${chalk.dim('source:')} ${result.source_type} ${result.source_ref}`);
1511
+ if (result.source_worktree) console.log(` ${chalk.dim('worktree:')} ${result.source_worktree}`);
1512
+ } catch (err) {
1513
+ db.close();
1514
+ printErrorWithNext(err.message, 'switchman queue add --help');
1515
+ process.exitCode = 1;
1516
+ }
1517
+ });
1518
+
1519
+ queueCmd
1520
+ .command('list')
1521
+ .description('List merge queue items')
1522
+ .option('--status <status>', 'Filter by queue status')
1523
+ .option('--json', 'Output raw JSON')
1524
+ .action((opts) => {
1525
+ const repoRoot = getRepo();
1526
+ const db = getDb(repoRoot);
1527
+ const items = listMergeQueue(db, { status: opts.status || null });
1528
+ db.close();
1529
+
1530
+ if (opts.json) {
1531
+ console.log(JSON.stringify(items, null, 2));
1532
+ return;
1533
+ }
1534
+
1535
+ if (items.length === 0) {
1536
+ console.log(chalk.dim('Merge queue is empty.'));
1537
+ return;
1538
+ }
1539
+
1540
+ for (const item of items) {
1541
+ const retryInfo = chalk.dim(`retries:${item.retry_count}/${item.max_retries}`);
1542
+ const attemptInfo = item.last_attempt_at ? ` ${chalk.dim(`last-attempt:${item.last_attempt_at}`)}` : '';
1543
+ console.log(` ${statusBadge(item.status)} ${item.id} ${item.source_type}:${item.source_ref} ${chalk.dim(`→ ${item.target_branch}`)} ${retryInfo}${attemptInfo}`);
1544
+ if (item.last_error_summary) {
1545
+ console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
1546
+ }
1547
+ if (item.next_action) {
1548
+ console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
1549
+ }
1550
+ }
1551
+ });
1552
+
1553
+ queueCmd
1554
+ .command('status')
1555
+ .description('Show an operator-friendly merge queue summary')
1556
+ .option('--json', 'Output raw JSON')
1557
+ .addHelpText('after', `
1558
+ Plain English:
1559
+ Use this when finished branches are waiting to land and you want one safe queue view.
1560
+
1561
+ Examples:
1562
+ switchman queue status
1563
+ switchman queue status --json
1564
+
1565
+ What it helps you answer:
1566
+ - what lands next
1567
+ - what is blocked
1568
+ - what command should I run now
1569
+ `)
1570
+ .action((opts) => {
1571
+ const repoRoot = getRepo();
1572
+ const db = getDb(repoRoot);
1573
+ const items = listMergeQueue(db);
1574
+ const summary = buildQueueStatusSummary(items);
1575
+ const recentEvents = items.slice(0, 5).flatMap((item) =>
1576
+ listMergeQueueEvents(db, item.id, { limit: 3 }).map((event) => ({ ...event, queue_item_id: item.id })),
1577
+ ).sort((a, b) => b.id - a.id).slice(0, 8);
1578
+ db.close();
1579
+
1580
+ if (opts.json) {
1581
+ console.log(JSON.stringify({ items, summary, recent_events: recentEvents }, null, 2));
1582
+ return;
1583
+ }
1584
+
1585
+ const queueHealth = summary.counts.blocked > 0 ? 'block' : summary.counts.retrying > 0 ? 'warn' : 'healthy';
1586
+ const queueHealthColor = colorForHealth(queueHealth);
1587
+ const focus = summary.blocked[0] || summary.retrying[0] || summary.next || null;
1588
+ const focusLine = focus
1589
+ ? `${focus.id} ${focus.source_type}:${focus.source_ref}${focus.last_error_summary ? ` ${chalk.dim(`• ${focus.last_error_summary}`)}` : ''}`
1590
+ : 'Nothing waiting. Landing queue is clear.';
1591
+
1592
+ console.log('');
1593
+ console.log(queueHealthColor('='.repeat(72)));
1594
+ console.log(`${queueHealthColor(healthLabel(queueHealth))} ${chalk.bold('switchman queue status')} ${chalk.dim('• landing mission control')}`);
1595
+ console.log(queueHealthColor('='.repeat(72)));
1596
+ console.log(renderSignalStrip([
1597
+ renderChip('queued', summary.counts.queued, summary.counts.queued > 0 ? chalk.yellow : chalk.green),
1598
+ renderChip('retrying', summary.counts.retrying, summary.counts.retrying > 0 ? chalk.yellow : chalk.green),
1599
+ renderChip('blocked', summary.counts.blocked, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1600
+ renderChip('merging', summary.counts.merging, summary.counts.merging > 0 ? chalk.blue : chalk.green),
1601
+ renderChip('merged', summary.counts.merged, summary.counts.merged > 0 ? chalk.green : chalk.white),
1602
+ ]));
1603
+ console.log(renderMetricRow([
1604
+ { label: 'items', value: items.length, color: chalk.white },
1605
+ { label: 'validating', value: summary.counts.validating, color: chalk.blue },
1606
+ { label: 'rebasing', value: summary.counts.rebasing, color: chalk.blue },
1607
+ { label: 'target', value: summary.next?.target_branch || 'main', color: chalk.cyan },
1608
+ ]));
1609
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
1610
+
1611
+ const queueFocusLines = summary.next
1612
+ ? [
1613
+ `${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}`)}`,
1614
+ ` ${chalk.yellow('run:')} switchman queue run`,
1615
+ ]
1616
+ : [chalk.dim('No queued landing work right now.')];
1617
+
1618
+ const queueBlockedLines = summary.blocked.length > 0
1619
+ ? summary.blocked.slice(0, 4).flatMap((item) => {
1620
+ const lines = [`${renderChip('BLOCKED', item.id, chalk.red)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
1621
+ if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
1622
+ if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
1623
+ return lines;
1624
+ })
1625
+ : [chalk.green('Nothing blocked.')];
1626
+
1627
+ const queueWatchLines = items.filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status)).length > 0
1628
+ ? items
1629
+ .filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status))
1630
+ .slice(0, 4)
1631
+ .flatMap((item) => {
1632
+ const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref}`];
1633
+ if (item.last_error_summary) lines.push(` ${chalk.dim(item.last_error_summary)}`);
1634
+ return lines;
1635
+ })
1636
+ : [chalk.green('No in-flight queue items right now.')];
1637
+
1638
+ const queueCommandLines = [
1639
+ `${chalk.cyan('$')} switchman queue run`,
1640
+ `${chalk.cyan('$')} switchman queue status --json`,
1641
+ ...(summary.blocked[0] ? [`${chalk.cyan('$')} switchman queue retry ${summary.blocked[0].id}`] : []),
1642
+ ];
1643
+
1644
+ console.log('');
1645
+ for (const block of [
1646
+ renderPanel('Landing focus', queueFocusLines, chalk.green),
1647
+ renderPanel('Blocked', queueBlockedLines, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1648
+ renderPanel('In flight', queueWatchLines, queueWatchLines[0] === 'No in-flight queue items right now.' ? chalk.green : chalk.blue),
1649
+ renderPanel('Next commands', queueCommandLines, chalk.cyan),
1650
+ ]) {
1651
+ for (const line of block) console.log(line);
1652
+ console.log('');
1653
+ }
1654
+
1655
+ if (recentEvents.length > 0) {
1656
+ console.log(chalk.bold('Recent Queue Events:'));
1657
+ for (const event of recentEvents) {
1658
+ console.log(` ${chalk.cyan(event.queue_item_id)} ${chalk.dim(event.event_type)} ${chalk.dim(event.status || '')} ${chalk.dim(event.created_at)}`.trim());
1659
+ }
1660
+ }
1661
+ });
1662
+
1663
+ queueCmd
1664
+ .command('run')
1665
+ .description('Process landing-queue items one at a time')
1666
+ .option('--max-items <n>', 'Maximum queue items to process', '1')
1667
+ .option('--target <branch>', 'Default target branch', 'main')
1668
+ .option('--watch', 'Keep polling for new queue items')
1669
+ .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '1000')
1670
+ .option('--max-cycles <n>', 'Maximum watch cycles before exiting (mainly for tests)')
1671
+ .option('--json', 'Output raw JSON')
1672
+ .addHelpText('after', `
1673
+ Examples:
1674
+ switchman queue run
1675
+ switchman queue run --watch
1676
+ switchman queue run --watch --watch-interval-ms 1000
1677
+ `)
1678
+ .action(async (opts) => {
1679
+ const repoRoot = getRepo();
1680
+
1681
+ try {
1682
+ const watch = Boolean(opts.watch);
1683
+ const watchIntervalMs = Math.max(0, Number.parseInt(opts.watchIntervalMs, 10) || 1000);
1684
+ const maxCycles = opts.maxCycles ? Math.max(1, Number.parseInt(opts.maxCycles, 10) || 1) : null;
1685
+ const aggregate = {
1686
+ processed: [],
1687
+ cycles: 0,
1688
+ watch,
1689
+ };
1690
+
1691
+ while (true) {
1692
+ const db = getDb(repoRoot);
1693
+ const result = await runMergeQueue(db, repoRoot, {
1694
+ maxItems: Number.parseInt(opts.maxItems, 10) || 1,
1695
+ targetBranch: opts.target || 'main',
1696
+ });
1697
+ db.close();
1698
+
1699
+ aggregate.processed.push(...result.processed);
1700
+ aggregate.summary = result.summary;
1701
+ aggregate.cycles += 1;
1702
+
1703
+ if (!watch) break;
1704
+ if (maxCycles && aggregate.cycles >= maxCycles) break;
1705
+ if (result.processed.length === 0) {
1706
+ sleepSync(watchIntervalMs);
1707
+ }
1708
+ }
1709
+
1710
+ if (opts.json) {
1711
+ console.log(JSON.stringify(aggregate, null, 2));
1712
+ return;
1713
+ }
1714
+
1715
+ if (aggregate.processed.length === 0) {
1716
+ console.log(chalk.dim('No queued merge items.'));
1717
+ return;
1718
+ }
1719
+
1720
+ for (const entry of aggregate.processed) {
1721
+ const item = entry.item;
1722
+ if (entry.status === 'merged') {
1723
+ console.log(`${chalk.green('✓')} Merged ${chalk.cyan(item.id)} into ${chalk.bold(item.target_branch)}`);
1724
+ console.log(` ${chalk.dim('commit:')} ${item.merged_commit}`);
1725
+ } else {
1726
+ console.log(`${chalk.red('✗')} Blocked ${chalk.cyan(item.id)}`);
1727
+ console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
1728
+ if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
1729
+ }
1730
+ }
1731
+ } catch (err) {
1732
+ console.error(chalk.red(err.message));
1733
+ process.exitCode = 1;
1734
+ }
1735
+ });
1736
+
1737
+ queueCmd
1738
+ .command('retry <itemId>')
1739
+ .description('Retry a blocked merge queue item')
1740
+ .option('--json', 'Output raw JSON')
1741
+ .action((itemId, opts) => {
1742
+ const repoRoot = getRepo();
1743
+ const db = getDb(repoRoot);
1744
+ const item = retryMergeQueueItem(db, itemId);
1745
+ db.close();
1746
+
1747
+ if (!item) {
1748
+ printErrorWithNext(`Queue item ${itemId} is not retryable.`, 'switchman queue status');
1749
+ process.exitCode = 1;
1750
+ return;
1751
+ }
1752
+
1753
+ if (opts.json) {
1754
+ console.log(JSON.stringify(item, null, 2));
1755
+ return;
1756
+ }
1757
+
1758
+ console.log(`${chalk.green('✓')} Queue item ${chalk.cyan(item.id)} reset to retrying`);
1759
+ });
1760
+
1761
+ queueCmd
1762
+ .command('remove <itemId>')
1763
+ .description('Remove a merge queue item')
1764
+ .action((itemId) => {
1765
+ const repoRoot = getRepo();
1766
+ const db = getDb(repoRoot);
1767
+ const item = removeMergeQueueItem(db, itemId);
1768
+ db.close();
1769
+
1770
+ if (!item) {
1771
+ printErrorWithNext(`Queue item ${itemId} does not exist.`, 'switchman queue status');
1772
+ process.exitCode = 1;
1773
+ return;
1774
+ }
1775
+
1776
+ console.log(`${chalk.green('✓')} Removed ${chalk.cyan(item.id)} from the merge queue`);
1777
+ });
1778
+
795
1779
  // ── pipeline ──────────────────────────────────────────────────────────────────
796
1780
 
797
1781
  const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
1782
+ pipelineCmd.addHelpText('after', `
1783
+ Examples:
1784
+ switchman pipeline start "Harden auth API permissions"
1785
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
1786
+ switchman pipeline status pipe-123
1787
+ `);
798
1788
 
799
1789
  pipelineCmd
800
1790
  .command('start <title>')
@@ -832,6 +1822,14 @@ pipelineCmd
832
1822
  .command('status <pipelineId>')
833
1823
  .description('Show task status for a pipeline')
834
1824
  .option('--json', 'Output raw JSON')
1825
+ .addHelpText('after', `
1826
+ Plain English:
1827
+ Use this when one goal has been split into several tasks and you want to see what is running, stuck, or next.
1828
+
1829
+ Examples:
1830
+ switchman pipeline status pipe-123
1831
+ switchman pipeline status pipe-123 --json
1832
+ `)
835
1833
  .action((pipelineId, opts) => {
836
1834
  const repoRoot = getRepo();
837
1835
  const db = getDb(repoRoot);
@@ -845,20 +1843,74 @@ pipelineCmd
845
1843
  return;
846
1844
  }
847
1845
 
1846
+ const pipelineHealth = result.status === 'blocked'
1847
+ ? 'block'
1848
+ : result.counts.failed > 0
1849
+ ? 'warn'
1850
+ : result.counts.in_progress > 0
1851
+ ? 'warn'
1852
+ : 'healthy';
1853
+ const pipelineHealthColor = colorForHealth(pipelineHealth);
1854
+ const failedTask = result.tasks.find((task) => task.status === 'failed');
1855
+ const runningTask = result.tasks.find((task) => task.status === 'in_progress');
1856
+ const nextPendingTask = result.tasks.find((task) => task.status === 'pending');
1857
+ const focusTask = failedTask || runningTask || nextPendingTask || result.tasks[0] || null;
1858
+ const focusLine = focusTask
1859
+ ? `${focusTask.title} ${chalk.dim(focusTask.id)}`
1860
+ : 'No pipeline tasks found.';
1861
+
1862
+ console.log('');
1863
+ console.log(pipelineHealthColor('='.repeat(72)));
1864
+ console.log(`${pipelineHealthColor(healthLabel(pipelineHealth))} ${chalk.bold('switchman pipeline status')} ${chalk.dim('• pipeline mission control')}`);
848
1865
  console.log(`${chalk.bold(result.title)} ${chalk.dim(result.pipeline_id)}`);
849
- 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}`);
850
- for (const task of result.tasks) {
1866
+ console.log(pipelineHealthColor('='.repeat(72)));
1867
+ console.log(renderSignalStrip([
1868
+ renderChip('done', result.counts.done, result.counts.done > 0 ? chalk.green : chalk.white),
1869
+ renderChip('running', result.counts.in_progress, result.counts.in_progress > 0 ? chalk.blue : chalk.green),
1870
+ renderChip('pending', result.counts.pending, result.counts.pending > 0 ? chalk.yellow : chalk.green),
1871
+ renderChip('failed', result.counts.failed, result.counts.failed > 0 ? chalk.red : chalk.green),
1872
+ ]));
1873
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
1874
+
1875
+ const runningLines = result.tasks.filter((task) => task.status === 'in_progress').slice(0, 4).map((task) => {
851
1876
  const worktree = task.worktree || task.suggested_worktree || 'unassigned';
852
1877
  const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
853
1878
  const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
854
- console.log(` ${statusBadge(task.status)} ${task.id} ${task.title}${type} ${chalk.dim(worktree)}${blocked}`);
1879
+ return `${chalk.cyan(worktree)} -> ${task.title}${type} ${chalk.dim(task.id)}${blocked}`;
1880
+ });
1881
+
1882
+ const blockedLines = result.tasks.filter((task) => task.status === 'failed').slice(0, 4).flatMap((task) => {
1883
+ const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
1884
+ const lines = [`${renderChip('BLOCKED', task.id, chalk.red)} ${task.title}${type}`];
855
1885
  if (task.failure?.summary) {
856
1886
  const reasonLabel = humanizeReasonCode(task.failure.reason_code);
857
- console.log(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
858
- }
859
- if (task.next_action) {
860
- console.log(` ${chalk.yellow('next:')} ${task.next_action}`);
1887
+ lines.push(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
861
1888
  }
1889
+ if (task.next_action) lines.push(` ${chalk.yellow('next:')} ${task.next_action}`);
1890
+ return lines;
1891
+ });
1892
+
1893
+ const nextLines = result.tasks.filter((task) => task.status === 'pending').slice(0, 4).map((task) => {
1894
+ const worktree = task.suggested_worktree || task.worktree || 'unassigned';
1895
+ const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
1896
+ return `${renderChip('NEXT', task.id, chalk.green)} ${task.title} ${chalk.dim(worktree)}${blocked}`;
1897
+ });
1898
+
1899
+ const commandLines = [
1900
+ `${chalk.cyan('$')} switchman pipeline exec ${result.pipeline_id} "/path/to/agent-command"`,
1901
+ `${chalk.cyan('$')} switchman pipeline pr ${result.pipeline_id}`,
1902
+ ...(result.counts.failed > 0 ? [`${chalk.cyan('$')} switchman pipeline status ${result.pipeline_id}`] : []),
1903
+ ];
1904
+
1905
+ console.log('');
1906
+ for (const block of [
1907
+ renderPanel('Running now', runningLines.length > 0 ? runningLines : [chalk.dim('No tasks are actively running.')], runningLines.length > 0 ? chalk.cyan : chalk.green),
1908
+ renderPanel('Blocked', blockedLines.length > 0 ? blockedLines : [chalk.green('Nothing blocked.')], blockedLines.length > 0 ? chalk.red : chalk.green),
1909
+ renderPanel('Next up', nextLines.length > 0 ? nextLines : [chalk.dim('No pending tasks left.')], chalk.green),
1910
+ renderPanel('Next commands', commandLines, chalk.cyan),
1911
+ ]) {
1912
+ for (const line of block) console.log(line);
1913
+ console.log('');
862
1914
  }
863
1915
  } catch (err) {
864
1916
  db.close();
@@ -1048,6 +2100,14 @@ pipelineCmd
1048
2100
  .option('--retry-backoff-ms <ms>', 'Base backoff in milliseconds between retry attempts', '0')
1049
2101
  .option('--timeout-ms <ms>', 'Default command timeout in milliseconds when a task spec does not provide one', '0')
1050
2102
  .option('--json', 'Output raw JSON')
2103
+ .addHelpText('after', `
2104
+ Plain English:
2105
+ pipeline = one goal, broken into smaller safe tasks
2106
+
2107
+ Examples:
2108
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
2109
+ switchman pipeline exec pipe-123 "npm test"
2110
+ `)
1051
2111
  .action(async (pipelineId, agentCommand, opts) => {
1052
2112
  const repoRoot = getRepo();
1053
2113
  const db = getDb(repoRoot);
@@ -1081,20 +2141,34 @@ pipelineCmd
1081
2141
  console.log(chalk.dim(result.pr.markdown.split('\n')[0]));
1082
2142
  } catch (err) {
1083
2143
  db.close();
1084
- console.error(chalk.red(err.message));
2144
+ printErrorWithNext(err.message, `switchman pipeline status ${pipelineId}`);
1085
2145
  process.exitCode = 1;
1086
2146
  }
1087
2147
  });
1088
2148
 
1089
2149
  // ── lease ────────────────────────────────────────────────────────────────────
1090
2150
 
1091
- const leaseCmd = program.command('lease').description('Manage active work leases');
2151
+ const leaseCmd = program.command('lease').alias('session').description('Manage active work sessions and keep long-running tasks alive');
2152
+ leaseCmd.addHelpText('after', `
2153
+ Plain English:
2154
+ lease = a task currently checked out by an agent
2155
+
2156
+ Examples:
2157
+ switchman lease next --json
2158
+ switchman lease heartbeat lease-123
2159
+ switchman lease reap
2160
+ `);
1092
2161
 
1093
2162
  leaseCmd
1094
2163
  .command('acquire <taskId> <worktree>')
1095
- .description('Acquire a lease for a pending task')
2164
+ .description('Start a tracked work session for a specific pending task')
1096
2165
  .option('--agent <name>', 'Agent identifier for logging')
1097
2166
  .option('--json', 'Output as JSON')
2167
+ .addHelpText('after', `
2168
+ Examples:
2169
+ switchman lease acquire task-123 agent2
2170
+ switchman lease acquire task-123 agent2 --agent cursor
2171
+ `)
1098
2172
  .action((taskId, worktree, opts) => {
1099
2173
  const repoRoot = getRepo();
1100
2174
  const db = getDb(repoRoot);
@@ -1104,7 +2178,7 @@ leaseCmd
1104
2178
 
1105
2179
  if (!lease || !task) {
1106
2180
  if (opts.json) console.log(JSON.stringify({ lease: null, task: null }));
1107
- else console.log(chalk.red(`Could not acquire lease. The task may not exist or is not pending.`));
2181
+ else printErrorWithNext('Could not start a work session. The task may not exist or may already be in progress.', 'switchman task list --status pending');
1108
2182
  process.exitCode = 1;
1109
2183
  return;
1110
2184
  }
@@ -1124,10 +2198,16 @@ leaseCmd
1124
2198
 
1125
2199
  leaseCmd
1126
2200
  .command('next')
1127
- .description('Claim the next pending task and acquire its lease')
2201
+ .description('Start the next pending task and open a tracked work session for it')
1128
2202
  .option('--json', 'Output as JSON')
1129
- .option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
2203
+ .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
1130
2204
  .option('--agent <name>', 'Agent identifier for logging')
2205
+ .addHelpText('after', `
2206
+ Examples:
2207
+ switchman lease next
2208
+ switchman lease next --json
2209
+ switchman lease next --worktree agent2 --agent cursor
2210
+ `)
1131
2211
  .action((opts) => {
1132
2212
  const repoRoot = getRepo();
1133
2213
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -1135,7 +2215,7 @@ leaseCmd
1135
2215
 
1136
2216
  if (!task) {
1137
2217
  if (opts.json) console.log(JSON.stringify({ task: null, lease: null }));
1138
- else if (exhausted) console.log(chalk.dim('No pending tasks.'));
2218
+ else if (exhausted) console.log(chalk.dim('No pending tasks. Add one with `switchman task add "Your task"`.'));
1139
2219
  else console.log(chalk.yellow('Tasks were claimed by other agents during assignment. Run again to get the next one.'));
1140
2220
  return;
1141
2221
  }
@@ -1198,7 +2278,7 @@ leaseCmd
1198
2278
 
1199
2279
  if (!lease) {
1200
2280
  if (opts.json) console.log(JSON.stringify({ lease: null }));
1201
- else console.log(chalk.red(`No active lease found for ${leaseId}`));
2281
+ else printErrorWithNext(`No active work session found for ${leaseId}.`, 'switchman lease list --status active');
1202
2282
  process.exitCode = 1;
1203
2283
  return;
1204
2284
  }
@@ -1214,14 +2294,24 @@ leaseCmd
1214
2294
 
1215
2295
  leaseCmd
1216
2296
  .command('reap')
1217
- .description('Expire stale leases, release their claims, and return their tasks to pending')
1218
- .option('--stale-after-minutes <minutes>', 'Age threshold for staleness', String(DEFAULT_STALE_LEASE_MINUTES))
2297
+ .description('Clean up abandoned work sessions and release their file locks')
2298
+ .option('--stale-after-minutes <minutes>', 'Age threshold for staleness')
1219
2299
  .option('--json', 'Output as JSON')
2300
+ .addHelpText('after', `
2301
+ Examples:
2302
+ switchman lease reap
2303
+ switchman lease reap --stale-after-minutes 20
2304
+ `)
1220
2305
  .action((opts) => {
1221
2306
  const repoRoot = getRepo();
1222
2307
  const db = getDb(repoRoot);
1223
- const staleAfterMinutes = Number.parseInt(opts.staleAfterMinutes, 10);
1224
- const expired = reapStaleLeases(db, staleAfterMinutes);
2308
+ const leasePolicy = loadLeasePolicy(repoRoot);
2309
+ const staleAfterMinutes = opts.staleAfterMinutes
2310
+ ? Number.parseInt(opts.staleAfterMinutes, 10)
2311
+ : leasePolicy.stale_after_minutes;
2312
+ const expired = reapStaleLeases(db, staleAfterMinutes, {
2313
+ requeueTask: leasePolicy.requeue_task_on_reap,
2314
+ });
1225
2315
  db.close();
1226
2316
 
1227
2317
  if (opts.json) {
@@ -1240,13 +2330,76 @@ leaseCmd
1240
2330
  }
1241
2331
  });
1242
2332
 
2333
+ const leasePolicyCmd = leaseCmd.command('policy').description('Inspect or update the stale-lease policy for this repo');
2334
+
2335
+ leasePolicyCmd
2336
+ .command('set')
2337
+ .description('Persist a stale-lease policy for this repo')
2338
+ .option('--heartbeat-interval-seconds <seconds>', 'Recommended heartbeat interval')
2339
+ .option('--stale-after-minutes <minutes>', 'Age threshold for staleness')
2340
+ .option('--reap-on-status-check <boolean>', 'Automatically reap stale leases during `switchman status`')
2341
+ .option('--requeue-task-on-reap <boolean>', 'Return stale tasks to pending instead of failing them')
2342
+ .option('--json', 'Output as JSON')
2343
+ .action((opts) => {
2344
+ const repoRoot = getRepo();
2345
+ const current = loadLeasePolicy(repoRoot);
2346
+ const next = {
2347
+ ...current,
2348
+ ...(opts.heartbeatIntervalSeconds ? { heartbeat_interval_seconds: Number.parseInt(opts.heartbeatIntervalSeconds, 10) } : {}),
2349
+ ...(opts.staleAfterMinutes ? { stale_after_minutes: Number.parseInt(opts.staleAfterMinutes, 10) } : {}),
2350
+ ...(opts.reapOnStatusCheck ? { reap_on_status_check: opts.reapOnStatusCheck === 'true' } : {}),
2351
+ ...(opts.requeueTaskOnReap ? { requeue_task_on_reap: opts.requeueTaskOnReap === 'true' } : {}),
2352
+ };
2353
+ const path = writeLeasePolicy(repoRoot, next);
2354
+ const saved = loadLeasePolicy(repoRoot);
2355
+
2356
+ if (opts.json) {
2357
+ console.log(JSON.stringify({ path, policy: saved }, null, 2));
2358
+ return;
2359
+ }
2360
+
2361
+ console.log(`${chalk.green('✓')} Lease policy updated`);
2362
+ console.log(` ${chalk.dim(path)}`);
2363
+ console.log(` ${chalk.dim('heartbeat_interval_seconds:')} ${saved.heartbeat_interval_seconds}`);
2364
+ console.log(` ${chalk.dim('stale_after_minutes:')} ${saved.stale_after_minutes}`);
2365
+ console.log(` ${chalk.dim('reap_on_status_check:')} ${saved.reap_on_status_check}`);
2366
+ console.log(` ${chalk.dim('requeue_task_on_reap:')} ${saved.requeue_task_on_reap}`);
2367
+ });
2368
+
2369
+ leasePolicyCmd
2370
+ .description('Show the active stale-lease policy for this repo')
2371
+ .option('--json', 'Output as JSON')
2372
+ .action((opts) => {
2373
+ const repoRoot = getRepo();
2374
+ const policy = loadLeasePolicy(repoRoot);
2375
+ if (opts.json) {
2376
+ console.log(JSON.stringify({ policy }, null, 2));
2377
+ return;
2378
+ }
2379
+
2380
+ console.log(chalk.bold('Lease policy'));
2381
+ console.log(` ${chalk.dim('heartbeat_interval_seconds:')} ${policy.heartbeat_interval_seconds}`);
2382
+ console.log(` ${chalk.dim('stale_after_minutes:')} ${policy.stale_after_minutes}`);
2383
+ console.log(` ${chalk.dim('reap_on_status_check:')} ${policy.reap_on_status_check}`);
2384
+ console.log(` ${chalk.dim('requeue_task_on_reap:')} ${policy.requeue_task_on_reap}`);
2385
+ });
2386
+
1243
2387
  // ── worktree ───────────────────────────────────────────────────────────────────
1244
2388
 
1245
- const wtCmd = program.command('worktree').description('Manage worktrees');
2389
+ const wtCmd = program.command('worktree').alias('workspace').description('Manage registered workspaces (Git worktrees)');
2390
+ wtCmd.addHelpText('after', `
2391
+ Plain English:
2392
+ worktree = the Git feature behind each agent workspace
2393
+
2394
+ Examples:
2395
+ switchman worktree list
2396
+ switchman workspace list
2397
+ switchman worktree sync
2398
+ `);
1246
2399
 
1247
2400
  wtCmd
1248
2401
  .command('add <name> <path> <branch>')
1249
- .description('Register a worktree with switchman')
2402
+ .description('Register a workspace with Switchman')
1250
2403
  .option('--agent <name>', 'Agent assigned to this worktree')
1251
2404
  .action((name, path, branch, opts) => {
1252
2405
  const repoRoot = getRepo();
@@ -1258,7 +2411,7 @@ wtCmd
1258
2411
 
1259
2412
  wtCmd
1260
2413
  .command('list')
1261
- .description('List all registered worktrees')
2414
+ .description('List all registered workspaces')
1262
2415
  .action(() => {
1263
2416
  const repoRoot = getRepo();
1264
2417
  const db = getDb(repoRoot);
@@ -1267,7 +2420,7 @@ wtCmd
1267
2420
  db.close();
1268
2421
 
1269
2422
  if (!worktrees.length && !gitWorktrees.length) {
1270
- console.log(chalk.dim('No worktrees found.'));
2423
+ console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
1271
2424
  return;
1272
2425
  }
1273
2426
 
@@ -1287,7 +2440,7 @@ wtCmd
1287
2440
 
1288
2441
  wtCmd
1289
2442
  .command('sync')
1290
- .description('Sync git worktrees into the switchman database')
2443
+ .description('Sync Git workspaces into the Switchman database')
1291
2444
  .action(() => {
1292
2445
  const repoRoot = getRepo();
1293
2446
  const db = getDb(repoRoot);
@@ -1304,12 +2457,20 @@ wtCmd
1304
2457
 
1305
2458
  program
1306
2459
  .command('claim <taskId> <worktree> [files...]')
1307
- .description('Claim files for a task (warns if conflicts exist)')
2460
+ .description('Lock files for a task before editing')
1308
2461
  .option('--agent <name>', 'Agent name')
1309
2462
  .option('--force', 'Claim even if conflicts exist')
2463
+ .addHelpText('after', `
2464
+ Examples:
2465
+ switchman claim task-123 agent2 src/auth.js src/server.js
2466
+ switchman claim task-123 agent2 src/auth.js --agent cursor
2467
+
2468
+ Use this before editing files in a shared repo.
2469
+ `)
1310
2470
  .action((taskId, worktree, files, opts) => {
1311
2471
  if (!files.length) {
1312
- console.log(chalk.yellow('No files specified. Use: switchman claim <taskId> <worktree> file1 file2 ...'));
2472
+ console.log(chalk.yellow('No files specified.'));
2473
+ console.log(`${chalk.yellow('next:')} switchman claim <taskId> <workspace> file1 file2`);
1313
2474
  return;
1314
2475
  }
1315
2476
  const repoRoot = getRepo();
@@ -1323,7 +2484,8 @@ program
1323
2484
  for (const c of conflicts) {
1324
2485
  console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
1325
2486
  }
1326
- console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
2487
+ console.log(chalk.dim('\nUse --force to claim anyway, or pick different files first.'));
2488
+ console.log(`${chalk.yellow('next:')} switchman status`);
1327
2489
  process.exitCode = 1;
1328
2490
  return;
1329
2491
  }
@@ -1332,7 +2494,7 @@ program
1332
2494
  console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
1333
2495
  files.forEach(f => console.log(` ${chalk.dim(f)}`));
1334
2496
  } catch (err) {
1335
- console.error(chalk.red(err.message));
2497
+ printErrorWithNext(err.message, 'switchman task list --status in_progress');
1336
2498
  process.exitCode = 1;
1337
2499
  } finally {
1338
2500
  db.close();
@@ -1501,13 +2663,19 @@ program
1501
2663
 
1502
2664
  program
1503
2665
  .command('scan')
1504
- .description('Scan all worktrees for conflicts')
2666
+ .description('Scan all workspaces for conflicts')
1505
2667
  .option('--json', 'Output raw JSON')
1506
2668
  .option('--quiet', 'Only show conflicts')
2669
+ .addHelpText('after', `
2670
+ Examples:
2671
+ switchman scan
2672
+ switchman scan --quiet
2673
+ switchman scan --json
2674
+ `)
1507
2675
  .action(async (opts) => {
1508
2676
  const repoRoot = getRepo();
1509
2677
  const db = getDb(repoRoot);
1510
- const spinner = ora('Scanning worktrees for conflicts...').start();
2678
+ const spinner = ora('Scanning workspaces for conflicts...').start();
1511
2679
 
1512
2680
  try {
1513
2681
  const report = await scanAllWorktrees(db, repoRoot);
@@ -1600,7 +2768,7 @@ program
1600
2768
 
1601
2769
  // All clear
1602
2770
  if (report.conflicts.length === 0 && report.fileConflicts.length === 0 && (report.ownershipConflicts?.length || 0) === 0 && (report.semanticConflicts?.length || 0) === 0 && report.unclaimedChanges.length === 0) {
1603
- console.log(chalk.green(`✓ No conflicts detected across ${report.worktrees.length} worktree(s)`));
2771
+ console.log(chalk.green(`✓ No conflicts detected across ${report.worktrees.length} workspace(s)`));
1604
2772
  }
1605
2773
 
1606
2774
  } catch (err) {
@@ -1614,200 +2782,72 @@ program
1614
2782
 
1615
2783
  program
1616
2784
  .command('status')
1617
- .description('Show full system status: tasks, worktrees, claims, and conflicts')
1618
- .action(async () => {
2785
+ .description('Show one dashboard view of what is running, blocked, and ready next')
2786
+ .option('--json', 'Output raw JSON')
2787
+ .option('--watch', 'Keep refreshing status in the terminal')
2788
+ .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
2789
+ .option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
2790
+ .addHelpText('after', `
2791
+ Examples:
2792
+ switchman status
2793
+ switchman status --watch
2794
+ switchman status --json
2795
+
2796
+ Use this first when the repo feels stuck.
2797
+ `)
2798
+ .action(async (opts) => {
1619
2799
  const repoRoot = getRepo();
1620
- const db = getDb(repoRoot);
1621
-
1622
- console.log('');
1623
- console.log(chalk.bold.cyan('━━━ switchman status ━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1624
- console.log(chalk.dim(`Repo: ${repoRoot}`));
1625
- console.log('');
2800
+ const watch = Boolean(opts.watch);
2801
+ const watchIntervalMs = Math.max(100, Number.parseInt(opts.watchIntervalMs, 10) || 2000);
2802
+ const maxCycles = Math.max(0, Number.parseInt(opts.maxCycles, 10) || 0);
2803
+ let cycles = 0;
2804
+ let lastSignature = null;
1626
2805
 
1627
- // Tasks
1628
- const tasks = listTasks(db);
1629
- const pending = tasks.filter(t => t.status === 'pending');
1630
- const inProgress = tasks.filter(t => t.status === 'in_progress');
1631
- const done = tasks.filter(t => t.status === 'done');
1632
- const failed = tasks.filter(t => t.status === 'failed');
1633
- const activeLeases = listLeases(db, 'active');
1634
- const staleLeases = getStaleLeases(db);
1635
-
1636
- console.log(chalk.bold('Tasks:'));
1637
- console.log(` ${chalk.yellow('Pending')} ${pending.length}`);
1638
- console.log(` ${chalk.blue('In Progress')} ${inProgress.length}`);
1639
- console.log(` ${chalk.green('Done')} ${done.length}`);
1640
- console.log(` ${chalk.red('Failed')} ${failed.length}`);
1641
-
1642
- if (activeLeases.length > 0) {
1643
- console.log('');
1644
- console.log(chalk.bold('Active Leases:'));
1645
- for (const lease of activeLeases) {
1646
- const agent = lease.agent ? ` ${chalk.dim(`agent:${lease.agent}`)}` : '';
1647
- const scope = summarizeLeaseScope(db, lease);
1648
- const boundaryValidation = getBoundaryValidationState(db, lease.id);
1649
- const dependencyInvalidations = listDependencyInvalidations(db, { affectedTaskId: lease.task_id });
1650
- const boundary = boundaryValidation ? ` ${chalk.dim(`validation:${boundaryValidation.status}`)}` : '';
1651
- const staleMarker = dependencyInvalidations.length > 0 ? ` ${chalk.dim(`stale:${dependencyInvalidations.length}`)}` : '';
1652
- console.log(` ${chalk.cyan(lease.worktree)} → ${lease.task_title} ${chalk.dim(lease.id)} ${chalk.dim(`task:${lease.task_id}`)}${agent}${scope ? ` ${chalk.dim(scope)}` : ''}${boundary}${staleMarker}`);
2806
+ while (true) {
2807
+ if (watch && process.stdout.isTTY && !opts.json) {
2808
+ console.clear();
1653
2809
  }
1654
- } else if (inProgress.length > 0) {
1655
- console.log('');
1656
- console.log(chalk.bold('In-Progress Tasks Without Lease:'));
1657
- for (const t of inProgress) {
1658
- console.log(` ${chalk.cyan(t.worktree || 'unassigned')} → ${t.title}`);
1659
- }
1660
- }
1661
2810
 
1662
- if (staleLeases.length > 0) {
1663
- console.log('');
1664
- console.log(chalk.bold('Stale Leases:'));
1665
- for (const lease of staleLeases) {
1666
- console.log(` ${chalk.red(lease.worktree)} → ${lease.task_title} ${chalk.dim(lease.id)} ${chalk.dim(lease.heartbeat_at)}`);
1667
- }
1668
- }
1669
-
1670
- if (pending.length > 0) {
1671
- console.log('');
1672
- console.log(chalk.bold('Next Up:'));
1673
- const next = pending.slice(0, 3);
1674
- for (const t of next) {
1675
- console.log(` [p${t.priority}] ${t.title} ${chalk.dim(t.id)}`);
1676
- }
1677
- }
1678
-
1679
- if (failed.length > 0) {
1680
- console.log('');
1681
- console.log(chalk.bold('Failed Tasks:'));
1682
- for (const task of failed.slice(0, 5)) {
1683
- const failureLine = String(task.description || '')
1684
- .split('\n')
1685
- .map((line) => line.trim())
1686
- .filter(Boolean)
1687
- .reverse()
1688
- .find((line) => line.startsWith('FAILED: '));
1689
- const failureText = failureLine ? failureLine.slice('FAILED: '.length) : 'unknown failure';
1690
- const reasonMatch = failureText.match(/^([a-z0-9_]+):\s*(.+)$/i);
1691
- const reasonCode = reasonMatch ? reasonMatch[1] : null;
1692
- const summary = reasonMatch ? reasonMatch[2] : failureText;
1693
- const nextStep = nextStepForReason(reasonCode);
1694
- console.log(` ${chalk.red(task.title)} ${chalk.dim(task.id)}`);
1695
- console.log(` ${chalk.red('why:')} ${summary} ${chalk.dim(`(${humanizeReasonCode(reasonCode)})`)}`);
1696
- if (nextStep) {
1697
- console.log(` ${chalk.yellow('next:')} ${nextStep}`);
1698
- }
1699
- }
1700
- }
1701
-
1702
- // File Claims
1703
- const claims = getActiveFileClaims(db);
1704
- if (claims.length > 0) {
1705
- console.log('');
1706
- console.log(chalk.bold(`Active File Claims (${claims.length}):`));
1707
- const byWorktree = {};
1708
- for (const c of claims) {
1709
- if (!byWorktree[c.worktree]) byWorktree[c.worktree] = [];
1710
- byWorktree[c.worktree].push(c.file_path);
1711
- }
1712
- for (const [wt, files] of Object.entries(byWorktree)) {
1713
- console.log(` ${chalk.cyan(wt)}: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5} more` : ''}`);
1714
- }
1715
- }
2811
+ const report = await collectStatusSnapshot(repoRoot);
2812
+ cycles += 1;
1716
2813
 
1717
- // Quick conflict scan
1718
- console.log('');
1719
- const spinner = ora('Running conflict scan...').start();
1720
- try {
1721
- const report = await scanAllWorktrees(db, repoRoot);
1722
- spinner.stop();
1723
-
1724
- const totalConflicts = report.conflicts.length + report.fileConflicts.length + (report.ownershipConflicts?.length || 0) + (report.semanticConflicts?.length || 0) + report.unclaimedChanges.length;
1725
- if (totalConflicts === 0) {
1726
- console.log(chalk.green(`✓ No conflicts across ${report.worktrees.length} worktree(s)`));
2814
+ if (opts.json) {
2815
+ console.log(JSON.stringify(watch ? { ...report, watch: true, cycles } : report, null, 2));
1727
2816
  } else {
1728
- console.log(chalk.red(`⚠ ${totalConflicts} conflict(s) detected — run 'switchman scan' for details`));
1729
- }
1730
-
1731
- console.log('');
1732
- console.log(chalk.bold('Compliance:'));
1733
- console.log(` ${chalk.green('Managed')} ${report.complianceSummary.managed}`);
1734
- console.log(` ${chalk.yellow('Observed')} ${report.complianceSummary.observed}`);
1735
- console.log(` ${chalk.red('Non-Compliant')} ${report.complianceSummary.non_compliant}`);
1736
- console.log(` ${chalk.red('Stale')} ${report.complianceSummary.stale}`);
1737
-
1738
- if (report.unclaimedChanges.length > 0) {
1739
- console.log('');
1740
- console.log(chalk.bold('Unclaimed Changed Paths:'));
1741
- for (const entry of report.unclaimedChanges) {
1742
- const reasonCode = entry.reasons?.[0]?.reason_code || null;
1743
- const nextStep = nextStepForReason(reasonCode);
1744
- console.log(` ${chalk.cyan(entry.worktree)}: ${entry.files.slice(0, 5).join(', ')}${entry.files.length > 5 ? ` +${entry.files.length - 5} more` : ''}`);
1745
- console.log(` ${chalk.dim(humanizeReasonCode(reasonCode))}${nextStep ? ` — ${nextStep}` : ''}`);
1746
- }
1747
- }
1748
-
1749
- if ((report.ownershipConflicts?.length || 0) > 0) {
1750
- console.log('');
1751
- console.log(chalk.bold('Ownership Boundary Overlaps:'));
1752
- for (const conflict of report.ownershipConflicts.slice(0, 5)) {
1753
- if (conflict.type === 'subsystem_overlap') {
1754
- console.log(` ${chalk.cyan(conflict.worktreeA)}: ${chalk.dim(`subsystem:${conflict.subsystemTag}`)} ${chalk.dim('vs')} ${chalk.cyan(conflict.worktreeB)}`);
1755
- } else {
1756
- console.log(` ${chalk.cyan(conflict.worktreeA)}: ${chalk.dim(conflict.scopeA)} ${chalk.dim('vs')} ${chalk.cyan(conflict.worktreeB)} ${chalk.dim(conflict.scopeB)}`);
1757
- }
1758
- }
1759
- }
1760
-
1761
- if ((report.semanticConflicts?.length || 0) > 0) {
1762
- console.log('');
1763
- console.log(chalk.bold('Semantic Overlaps:'));
1764
- for (const conflict of report.semanticConflicts.slice(0, 5)) {
1765
- console.log(` ${chalk.cyan(conflict.worktreeA)}: ${chalk.dim(conflict.object_name)} ${chalk.dim('vs')} ${chalk.cyan(conflict.worktreeB)} ${chalk.dim(`${conflict.fileA} ↔ ${conflict.fileB}`)}`);
1766
- }
1767
- }
1768
-
1769
- const staleInvalidations = listDependencyInvalidations(db, { status: 'stale' });
1770
- if (staleInvalidations.length > 0) {
1771
- console.log('');
1772
- console.log(chalk.bold('Stale For Revalidation:'));
1773
- for (const invalidation of staleInvalidations.slice(0, 5)) {
1774
- const staleArea = invalidation.reason_type === 'subsystem_overlap'
1775
- ? `subsystem:${invalidation.subsystem_tag}`
1776
- : `${invalidation.source_scope_pattern} ↔ ${invalidation.affected_scope_pattern}`;
1777
- console.log(` ${chalk.cyan(invalidation.affected_worktree || 'unknown')}: ${chalk.dim(staleArea)} ${chalk.dim('because')} ${chalk.cyan(invalidation.source_worktree || 'unknown')} changed it`);
1778
- }
1779
- }
1780
-
1781
- if (report.commitGateFailures.length > 0) {
1782
- console.log('');
1783
- console.log(chalk.bold('Recent Commit Gate Failures:'));
1784
- for (const failure of report.commitGateFailures.slice(0, 5)) {
1785
- console.log(` ${chalk.red(failure.worktree || 'unknown')} ${chalk.dim(humanizeReasonCode(failure.reason_code || 'rejected'))} ${chalk.dim(failure.created_at)}`);
2817
+ renderUnifiedStatusReport(report);
2818
+ if (watch) {
2819
+ const signature = buildWatchSignature(report);
2820
+ const watchState = lastSignature === null
2821
+ ? chalk.cyan('baseline snapshot')
2822
+ : signature === lastSignature
2823
+ ? chalk.dim('no repo changes since last refresh')
2824
+ : chalk.green('change detected');
2825
+ const updatedAt = formatClockTime(report.generated_at);
2826
+ lastSignature = signature;
2827
+ console.log('');
2828
+ console.log(chalk.dim(`Live watch • updated ${updatedAt || 'just now'} • ${watchState}${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''} • refresh ${watchIntervalMs}ms`));
1786
2829
  }
1787
2830
  }
1788
2831
 
1789
- if (report.deniedWrites.length > 0) {
1790
- console.log('');
1791
- console.log(chalk.bold('Recent Denied Events:'));
1792
- for (const event of report.deniedWrites.slice(0, 5)) {
1793
- console.log(` ${chalk.red(event.event_type)} ${chalk.cyan(event.worktree || 'repo')} ${chalk.dim(humanizeReasonCode(event.reason_code || event.status))}`);
1794
- }
1795
- }
1796
- } catch {
1797
- spinner.stop();
1798
- console.log(chalk.dim('Could not run conflict scan'));
2832
+ if (!watch) break;
2833
+ if (maxCycles > 0 && cycles >= maxCycles) break;
2834
+ if (opts.json) break;
2835
+ sleepSync(watchIntervalMs);
1799
2836
  }
1800
-
1801
- db.close();
1802
- console.log('');
1803
- console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1804
- console.log('');
1805
2837
  });
1806
2838
 
1807
2839
  program
1808
2840
  .command('doctor')
1809
2841
  .description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
1810
2842
  .option('--json', 'Output raw JSON')
2843
+ .addHelpText('after', `
2844
+ Plain English:
2845
+ Use this when the repo feels risky, noisy, or stuck and you want the health summary plus exact next moves.
2846
+
2847
+ Examples:
2848
+ switchman doctor
2849
+ switchman doctor --json
2850
+ `)
1811
2851
  .action(async (opts) => {
1812
2852
  const repoRoot = getRepo();
1813
2853
  const db = getDb(repoRoot);
@@ -1832,66 +2872,83 @@ program
1832
2872
  return;
1833
2873
  }
1834
2874
 
1835
- const badge = report.health === 'healthy'
1836
- ? chalk.green('HEALTHY')
1837
- : report.health === 'warn'
1838
- ? chalk.yellow('ATTENTION')
1839
- : chalk.red('BLOCKED');
1840
- console.log(`${badge} ${report.summary}`);
1841
- console.log(chalk.dim(repoRoot));
1842
- console.log('');
1843
-
1844
- console.log(chalk.bold('At a glance:'));
1845
- console.log(` ${chalk.dim('tasks')} ${report.counts.pending} pending, ${report.counts.in_progress} in progress, ${report.counts.done} done, ${report.counts.failed} failed`);
1846
- console.log(` ${chalk.dim('leases')} ${report.counts.active_leases} active, ${report.counts.stale_leases} stale`);
1847
- 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}`);
2875
+ const doctorColor = colorForHealth(report.health);
2876
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
2877
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
2878
+ const focusItem = report.attention[0] || report.active_work[0] || null;
2879
+ const focusLine = focusItem
2880
+ ? `${focusItem.title || focusItem.task_title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
2881
+ : 'Nothing urgent. Repo health looks steady.';
1848
2882
 
1849
- if (report.active_work.length > 0) {
1850
- console.log('');
1851
- console.log(chalk.bold('Running now:'));
1852
- for (const item of report.active_work.slice(0, 5)) {
2883
+ console.log('');
2884
+ console.log(doctorColor('='.repeat(72)));
2885
+ console.log(`${doctorColor(healthLabel(report.health))} ${chalk.bold('switchman doctor')} ${chalk.dim('• repo health mission control')}`);
2886
+ console.log(chalk.dim(repoRoot));
2887
+ console.log(chalk.dim(report.summary));
2888
+ console.log(doctorColor('='.repeat(72)));
2889
+ console.log(renderSignalStrip([
2890
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
2891
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
2892
+ renderChip('leases', report.counts.active_leases, report.counts.active_leases > 0 ? chalk.blue : chalk.green),
2893
+ renderChip('stale', report.counts.stale_leases, report.counts.stale_leases > 0 ? chalk.red : chalk.green),
2894
+ renderChip('merge', report.merge_readiness.ci_gate_ok ? 'clear' : 'hold', report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red),
2895
+ ]));
2896
+ console.log(renderMetricRow([
2897
+ { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
2898
+ { 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 },
2899
+ ]));
2900
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
2901
+
2902
+ const runningLines = report.active_work.length > 0
2903
+ ? report.active_work.slice(0, 5).map((item) => {
1853
2904
  const leaseId = activeLeases.find((lease) => lease.task_id === item.task_id && lease.worktree === item.worktree)?.id || null;
1854
2905
  const boundary = item.boundary_validation
1855
- ? ` ${chalk.dim(`validation:${item.boundary_validation.status}`)}`
2906
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
1856
2907
  : '';
1857
2908
  const stale = (item.dependency_invalidations?.length || 0) > 0
1858
- ? ` ${chalk.dim(`stale:${item.dependency_invalidations.length}`)}`
2909
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
1859
2910
  : '';
1860
- 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}`);
1861
- }
1862
- }
2911
+ 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}`;
2912
+ })
2913
+ : [chalk.dim('Nothing active right now.')];
2914
+
2915
+ const attentionLines = report.attention.length > 0
2916
+ ? report.attention.slice(0, 6).flatMap((item) => {
2917
+ const lines = [`${item.severity === 'block' ? renderChip('BLOCKED', item.kind || 'item', chalk.red) : renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
2918
+ if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
2919
+ lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
2920
+ if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
2921
+ return lines;
2922
+ })
2923
+ : [chalk.green('Nothing urgent.')];
2924
+
2925
+ const nextStepLines = [
2926
+ ...report.next_steps.slice(0, 4).map((step) => `- ${step}`),
2927
+ '',
2928
+ ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
2929
+ ];
1863
2930
 
1864
2931
  console.log('');
1865
2932
  console.log(chalk.bold('Attention now:'));
1866
- if (report.attention.length === 0) {
1867
- console.log(` ${chalk.green('Nothing urgent.')}`);
1868
- } else {
1869
- for (const item of report.attention.slice(0, 6)) {
1870
- const itemBadge = item.severity === 'block' ? chalk.red('block') : chalk.yellow('warn ');
1871
- console.log(` ${itemBadge} ${item.title}`);
1872
- if (item.detail) console.log(` ${chalk.dim(item.detail)}`);
1873
- console.log(` ${chalk.yellow('next:')} ${item.next_step}`);
1874
- if (item.command) console.log(` ${chalk.cyan('run:')} ${item.command}`);
1875
- }
1876
- }
1877
-
1878
- console.log('');
1879
- console.log(chalk.bold('Recommended next steps:'));
1880
- for (const step of report.next_steps) {
1881
- console.log(` - ${step}`);
1882
- }
1883
- if (report.suggested_commands.length > 0) {
2933
+ for (const block of [
2934
+ renderPanel('Running now', runningLines, chalk.cyan),
2935
+ renderPanel('Attention now', attentionLines, report.attention.some((item) => item.severity === 'block') ? chalk.red : report.attention.length > 0 ? chalk.yellow : chalk.green),
2936
+ renderPanel('Recommended next steps', nextStepLines, chalk.green),
2937
+ ]) {
2938
+ for (const line of block) console.log(line);
1884
2939
  console.log('');
1885
- console.log(chalk.bold('Suggested commands:'));
1886
- for (const command of report.suggested_commands) {
1887
- console.log(` ${chalk.cyan(command)}`);
1888
- }
1889
2940
  }
1890
2941
  });
1891
2942
 
1892
2943
  // ── gate ─────────────────────────────────────────────────────────────────────
1893
2944
 
1894
- const gateCmd = program.command('gate').description('Enforcement and commit-gate helpers');
2945
+ const gateCmd = program.command('gate').description('Safety checks for edits, merges, and CI');
2946
+ gateCmd.addHelpText('after', `
2947
+ Examples:
2948
+ switchman gate ci
2949
+ switchman gate ai
2950
+ switchman gate install-ci
2951
+ `);
1895
2952
 
1896
2953
  const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
1897
2954
 
@@ -2103,7 +3160,7 @@ gateCmd
2103
3160
 
2104
3161
  gateCmd
2105
3162
  .command('ai')
2106
- .description('Run the AI-style merge gate to assess semantic integration risk across worktrees')
3163
+ .description('Run the AI-style merge check to assess risky overlap across workspaces')
2107
3164
  .option('--json', 'Output raw JSON')
2108
3165
  .action(async (opts) => {
2109
3166
  const repoRoot = getRepo();
@@ -2270,7 +3327,7 @@ objectCmd
2270
3327
 
2271
3328
  // ── monitor ──────────────────────────────────────────────────────────────────
2272
3329
 
2273
- const monitorCmd = program.command('monitor').description('Observe worktrees for runtime file mutations');
3330
+ const monitorCmd = program.command('monitor').description('Observe workspaces for runtime file changes');
2274
3331
 
2275
3332
  monitorCmd
2276
3333
  .command('once')
@@ -2304,7 +3361,7 @@ monitorCmd
2304
3361
 
2305
3362
  monitorCmd
2306
3363
  .command('watch')
2307
- .description('Poll worktrees continuously and log observed file changes')
3364
+ .description('Poll workspaces continuously and log observed file changes')
2308
3365
  .option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
2309
3366
  .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
2310
3367
  .option('--daemonized', 'Internal flag used by monitor start', false)
@@ -2317,7 +3374,7 @@ monitorCmd
2317
3374
  process.exit(1);
2318
3375
  }
2319
3376
 
2320
- console.log(chalk.cyan(`Watching worktrees every ${intervalMs}ms. Press Ctrl+C to stop.`));
3377
+ console.log(chalk.cyan(`Watching workspaces every ${intervalMs}ms. Press Ctrl+C to stop.`));
2321
3378
 
2322
3379
  let stopped = false;
2323
3380
  const stop = () => {