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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/README.md +11 -7
- package/examples/README.md +18 -0
- package/examples/demo.sh +124 -0
- package/examples/setup.sh +13 -6
- package/examples/taskapi/.cursor/mcp.json +8 -0
- package/examples/taskapi/.mcp.json +8 -0
- package/examples/taskapi/src/middleware/auth.js +4 -0
- package/examples/taskapi/src/middleware/validate.js +4 -0
- package/examples/taskapi/src/routes/tasks.js +4 -0
- package/examples/taskapi/src/server.js +4 -0
- package/examples/walkthrough.sh +16 -9
- package/package.json +1 -1
- package/src/cli/index.js +791 -100
- package/src/core/telemetry.js +210 -0
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(
|
|
671
|
-
console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('•
|
|
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(
|
|
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
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
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
|
|
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 = [`${
|
|
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) => `
|
|
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
|
-
.
|
|
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
|
|
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(`
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
|
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('
|
|
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>', '
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
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
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
-
? ` ${
|
|
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
|
-
? ` ${
|
|
3058
|
+
? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
|
|
2383
3059
|
: '';
|
|
2384
|
-
|
|
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
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
|