switchman-dev 0.1.7 → 0.1.8

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
@@ -16,7 +16,7 @@
16
16
  * switchman status - Show the repo dashboard
17
17
  */
18
18
 
19
- import { program } from 'commander';
19
+ import { Help, program } from 'commander';
20
20
  import chalk from 'chalk';
21
21
  import ora from 'ora';
22
22
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
@@ -24,7 +24,7 @@ import { tmpdir } from 'os';
24
24
  import { dirname, join, posix } from 'path';
25
25
  import { execSync, spawn } from 'child_process';
26
26
 
27
- import { cleanupCrashedLandingTempWorktrees, findRepoRoot, listGitWorktrees, createGitWorktree } from '../core/git.js';
27
+ import { cleanupCrashedLandingTempWorktrees, createGitWorktree, findRepoRoot, getWorktreeBranch, getWorktreeChangedFiles, gitAssessBranchFreshness, gitBranchExists, listGitWorktrees } from '../core/git.js';
28
28
  import { matchesPathPatterns } from '../core/ignore.js';
29
29
  import {
30
30
  initDb, openDb,
@@ -37,18 +37,20 @@ import {
37
37
  createPolicyOverride, listPolicyOverrides, revokePolicyOverride,
38
38
  finishOperationJournalEntry, listOperationJournal, listTempResources, updateTempResource,
39
39
  claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts, retryTask,
40
- listAuditEvents, verifyAuditTrail,
40
+ upsertTaskSpec,
41
+ listAuditEvents, pruneDatabaseMaintenance, verifyAuditTrail,
41
42
  } from '../core/db.js';
42
43
  import { scanAllWorktrees } from '../core/detector.js';
43
- import { getWindsurfMcpConfigPath, upsertAllProjectMcpConfigs, upsertWindsurfMcpConfig } from '../core/mcp.js';
44
- import { gatewayAppendFile, gatewayMakeDirectory, gatewayMovePath, gatewayRemovePath, gatewayWriteFile, installGateHooks, monitorWorktreesOnce, runCommitGate, runWrappedCommand, writeEnforcementPolicy } from '../core/enforcement.js';
44
+ import { ensureProjectLocalMcpGitExcludes, getWindsurfMcpConfigPath, upsertAllProjectMcpConfigs, upsertWindsurfMcpConfig } from '../core/mcp.js';
45
+ import { evaluateRepoCompliance, gatewayAppendFile, gatewayMakeDirectory, gatewayMovePath, gatewayRemovePath, gatewayWriteFile, installGateHooks, monitorWorktreesOnce, runCommitGate, runWrappedCommand, writeEnforcementPolicy } from '../core/enforcement.js';
45
46
  import { runAiMergeGate } from '../core/merge-gate.js';
46
47
  import { clearMonitorState, getMonitorStatePath, isProcessRunning, readMonitorState, writeMonitorState } from '../core/monitor.js';
47
48
  import { buildPipelinePrSummary, cleanupPipelineLandingRecovery, commentPipelinePr, createPipelineFollowupTasks, evaluatePipelinePolicyGate, executePipeline, exportPipelinePrBundle, getPipelineLandingBranchStatus, getPipelineLandingExplainReport, getPipelineStatus, inferPipelineIdFromBranch, materializePipelineLandingBranch, preparePipelineLandingRecovery, preparePipelineLandingTarget, publishPipelinePr, repairPipelineState, resumePipelineLandingRecovery, runPipeline, startPipeline, summarizePipelinePolicyState, syncPipelinePr } from '../core/pipeline.js';
48
49
  import { installGitHubActionsWorkflow, resolveGitHubOutputTargets, writeGitHubCiStatus, writeGitHubPipelineLandingStatus } from '../core/ci.js';
49
50
  import { importCodeObjectsToStore, listCodeObjects, materializeCodeObjects, materializeSemanticIndex, updateCodeObjectSource } from '../core/semantic.js';
50
- import { buildQueueStatusSummary, resolveQueueSource, runMergeQueue } from '../core/queue.js';
51
+ import { buildQueueStatusSummary, evaluateQueueRepoGate, resolveQueueSource, runMergeQueue } from '../core/queue.js';
51
52
  import { DEFAULT_CHANGE_POLICY, DEFAULT_LEASE_POLICY, getChangePolicyPath, loadChangePolicy, loadLeasePolicy, writeChangePolicy, writeLeasePolicy } from '../core/policy.js';
53
+ import { planPipelineTasks } from '../core/planner.js';
52
54
  import {
53
55
  captureTelemetryEvent,
54
56
  disableTelemetry,
@@ -59,6 +61,9 @@ import {
59
61
  maybePromptForTelemetry,
60
62
  sendTelemetryEvent,
61
63
  } from '../core/telemetry.js';
64
+ import { checkLicence, clearCredentials, FREE_AGENT_LIMIT, getRetentionDaysForCurrentPlan, loginWithGitHub, PRO_PAGE_URL, readCredentials } from '../core/licence.js';
65
+ import { homedir } from 'os';
66
+ import { cleanupOldSyncEvents, pullActiveTeamMembers, pullTeamState, pushSyncEvent } from '../core/sync.js';
62
67
 
63
68
  const originalProcessEmit = process.emit.bind(process);
64
69
  process.emit = function patchedProcessEmit(event, ...args) {
@@ -95,6 +100,177 @@ function getDb(repoRoot) {
95
100
  }
96
101
  }
97
102
 
103
+ function getOptionalDb(repoRoot) {
104
+ try {
105
+ return openDb(repoRoot);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ function slugifyValue(value) {
112
+ return String(value || '')
113
+ .toLowerCase()
114
+ .replace(/[^a-z0-9]+/g, '-')
115
+ .replace(/^-+|-+$/g, '')
116
+ .slice(0, 40) || 'plan';
117
+ }
118
+
119
+ function capitalizeSentence(value) {
120
+ const text = String(value || '').trim();
121
+ if (!text) return text;
122
+ return text.charAt(0).toUpperCase() + text.slice(1);
123
+ }
124
+
125
+ function formatHumanList(values = []) {
126
+ if (values.length === 0) return '';
127
+ if (values.length === 1) return values[0];
128
+ if (values.length === 2) return `${values[0]} and ${values[1]}`;
129
+ return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`;
130
+ }
131
+
132
+ function readPlanningFile(repoRoot, fileName, maxChars = 1200) {
133
+ const filePath = join(repoRoot, fileName);
134
+ if (!existsSync(filePath)) return null;
135
+ try {
136
+ const text = readFileSync(filePath, 'utf8').trim();
137
+ if (!text) return null;
138
+ return {
139
+ file: fileName,
140
+ text: text.slice(0, maxChars),
141
+ };
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ function extractMarkdownSignal(text) {
148
+ const lines = String(text || '')
149
+ .split('\n')
150
+ .map((line) => line.trim())
151
+ .filter(Boolean);
152
+ for (const line of lines) {
153
+ const normalized = line.replace(/^#+\s*/, '').replace(/^[-*]\s+/, '').trim();
154
+ if (!normalized) continue;
155
+ if (/^switchman\b/i.test(normalized)) continue;
156
+ return normalized;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function deriveGoalFromBranch(branchName) {
162
+ const raw = String(branchName || '').replace(/^refs\/heads\//, '').trim();
163
+ if (!raw || ['main', 'master', 'trunk', 'develop', 'development'].includes(raw)) return null;
164
+ const tail = raw.split('/').pop() || raw;
165
+ const tokens = tail
166
+ .replace(/^\d+[-_]?/, '')
167
+ .split(/[-_]/)
168
+ .filter(Boolean)
169
+ .filter((token) => !['feature', 'feat', 'fix', 'bugfix', 'chore', 'task', 'issue', 'story', 'work'].includes(token.toLowerCase()));
170
+ if (tokens.length === 0) return null;
171
+ return capitalizeSentence(tokens.join(' '));
172
+ }
173
+
174
+ function getRecentCommitSubjects(repoRoot, limit = 6) {
175
+ try {
176
+ return execSync(`git log --pretty=%s -n ${limit}`, {
177
+ cwd: repoRoot,
178
+ encoding: 'utf8',
179
+ stdio: ['pipe', 'pipe', 'pipe'],
180
+ }).trim().split('\n').map((line) => line.trim()).filter(Boolean);
181
+ } catch {
182
+ return [];
183
+ }
184
+ }
185
+
186
+ function summarizeRecentCommitContext(branchGoal, subjects) {
187
+ if (!subjects.length) return null;
188
+ const topicWords = String(branchGoal || '')
189
+ .toLowerCase()
190
+ .split(/\s+/)
191
+ .filter((word) => word.length >= 4);
192
+ const relatedCount = topicWords.length > 0
193
+ ? subjects.filter((subject) => {
194
+ const lower = subject.toLowerCase();
195
+ return topicWords.some((word) => lower.includes(word));
196
+ }).length
197
+ : 0;
198
+ const effectiveCount = relatedCount > 0 ? relatedCount : Math.min(subjects.length, 3);
199
+ const topicLabel = relatedCount > 0 && topicWords.length > 0 ? `${topicWords[0]}-related ` : '';
200
+ return `${effectiveCount} recent ${topicLabel}commit${effectiveCount === 1 ? '' : 's'}`;
201
+ }
202
+
203
+ function collectPlanContext(repoRoot, explicitGoal = null) {
204
+ const planningFiles = ['CLAUDE.md', 'ROADMAP.md', 'tasks.md', 'TASKS.md', 'TODO.md', 'README.md']
205
+ .map((fileName) => readPlanningFile(repoRoot, fileName))
206
+ .filter(Boolean);
207
+ const planningByName = new Map(planningFiles.map((entry) => [entry.file, entry]));
208
+ const branch = getWorktreeBranch(process.cwd()) || null;
209
+ const branchGoal = deriveGoalFromBranch(branch);
210
+ const recentCommitSubjects = getRecentCommitSubjects(repoRoot, 6);
211
+ const recentCommitSummary = summarizeRecentCommitContext(branchGoal, recentCommitSubjects);
212
+ const preferredPlanningFile = planningByName.get('CLAUDE.md')
213
+ || planningByName.get('tasks.md')
214
+ || planningByName.get('TASKS.md')
215
+ || planningByName.get('ROADMAP.md')
216
+ || planningByName.get('TODO.md')
217
+ || planningByName.get('README.md')
218
+ || null;
219
+ const planningSignal = preferredPlanningFile ? extractMarkdownSignal(preferredPlanningFile.text) : null;
220
+ const title = capitalizeSentence(explicitGoal || branchGoal || planningSignal || 'Plan the next coordinated change');
221
+ const descriptionParts = [];
222
+ if (preferredPlanningFile?.text) descriptionParts.push(preferredPlanningFile.text);
223
+ if (recentCommitSubjects.length > 0) descriptionParts.push(`Recent git history summary: ${recentCommitSubjects.slice(0, 3).join('; ')}.`);
224
+ const description = descriptionParts.join('\n\n').trim() || null;
225
+
226
+ const found = [];
227
+ const used = [];
228
+ if (explicitGoal) {
229
+ used.push('explicit goal');
230
+ }
231
+ if (branch) {
232
+ found.push(`branch ${branch}`);
233
+ if (branchGoal) used.push('branch name');
234
+ }
235
+ if (preferredPlanningFile?.file) {
236
+ found.push(preferredPlanningFile.file);
237
+ used.push(preferredPlanningFile.file);
238
+ }
239
+ if (recentCommitSummary) {
240
+ found.push(recentCommitSummary);
241
+ used.push('recent git history');
242
+ }
243
+
244
+ return {
245
+ branch,
246
+ title,
247
+ description,
248
+ found,
249
+ used: [...new Set(used)],
250
+ };
251
+ }
252
+
253
+ function resolvePlanningWorktrees(repoRoot, db = null) {
254
+ if (db) {
255
+ const registered = listWorktrees(db)
256
+ .filter((worktree) => worktree.name !== 'main' && worktree.status !== 'missing')
257
+ .map((worktree) => ({ name: worktree.name, path: worktree.path, branch: worktree.branch }));
258
+ if (registered.length > 0) return registered;
259
+ }
260
+ return listGitWorktrees(repoRoot)
261
+ .filter((worktree) => !worktree.isMain)
262
+ .map((worktree) => ({ name: worktree.name, path: worktree.path, branch: worktree.branch || null }));
263
+ }
264
+
265
+ function planTaskPriority(taskSpec = null) {
266
+ const taskType = taskSpec?.task_type || 'implementation';
267
+ if (taskType === 'implementation') return 8;
268
+ if (taskType === 'tests') return 7;
269
+ if (taskType === 'docs') return 6;
270
+ if (taskType === 'governance') return 6;
271
+ return 5;
272
+ }
273
+
98
274
  function resolvePrNumberFromEnv(env = process.env) {
99
275
  if (env.SWITCHMAN_PR_NUMBER) return String(env.SWITCHMAN_PR_NUMBER);
100
276
  if (env.GITHUB_PR_NUMBER) return String(env.GITHUB_PR_NUMBER);
@@ -1783,12 +1959,24 @@ function buildDoctorReport({ db, repoRoot, tasks, activeLeases, staleLeases, sca
1783
1959
  };
1784
1960
  });
1785
1961
 
1786
- const blockedWorktrees = scanReport.unclaimedChanges.map((entry) => ({
1787
- worktree: entry.worktree,
1788
- files: entry.files,
1789
- reason_code: entry.reasons?.[0]?.reason_code || null,
1790
- next_step: nextStepForReason(entry.reasons?.[0]?.reason_code) || 'inspect the changed files and bring them back under Switchman claims',
1791
- }));
1962
+ const worktreeByName = new Map((scanReport.worktrees || []).map((worktree) => [worktree.name, worktree]));
1963
+ const blockedWorktrees = scanReport.unclaimedChanges.map((entry) => {
1964
+ const worktreeInfo = worktreeByName.get(entry.worktree) || null;
1965
+ const reasonCode = entry.reasons?.[0]?.reason_code || null;
1966
+ const isDirtyWorktree = reasonCode === 'no_active_lease';
1967
+ return {
1968
+ worktree: entry.worktree,
1969
+ path: worktreeInfo?.path || null,
1970
+ files: entry.files,
1971
+ reason_code: reasonCode,
1972
+ next_step: isDirtyWorktree
1973
+ ? 'commit or discard the changed files in that worktree, then rescan before continuing'
1974
+ : (nextStepForReason(reasonCode) || 'inspect the changed files and bring them back under Switchman claims'),
1975
+ command: worktreeInfo?.path
1976
+ ? `cd ${JSON.stringify(worktreeInfo.path)} && git status`
1977
+ : 'switchman scan',
1978
+ };
1979
+ });
1792
1980
 
1793
1981
  const fileConflicts = scanReport.fileConflicts.map((conflict) => ({
1794
1982
  file: conflict.file,
@@ -1838,9 +2026,9 @@ function buildDoctorReport({ db, repoRoot, tasks, activeLeases, staleLeases, sca
1838
2026
  ...blockedWorktrees.map((entry) => ({
1839
2027
  kind: 'unmanaged_changes',
1840
2028
  title: `${entry.worktree} has unmanaged changed files`,
1841
- detail: `${entry.files.slice(0, 3).join(', ')}${entry.files.length > 3 ? ` +${entry.files.length - 3} more` : ''}`,
2029
+ detail: `${entry.files.slice(0, 5).join(', ')}${entry.files.length > 5 ? ` +${entry.files.length - 5} more` : ''}${entry.path ? ` • ${entry.path}` : ''}`,
1842
2030
  next_step: entry.next_step,
1843
- command: 'switchman scan',
2031
+ command: entry.command,
1844
2032
  severity: 'block',
1845
2033
  })),
1846
2034
  ...fileConflicts.map((conflict) => ({
@@ -1997,6 +2185,7 @@ function buildUnifiedStatusReport({
1997
2185
  queueItems,
1998
2186
  queueSummary,
1999
2187
  recentQueueEvents,
2188
+ retentionDays = 7,
2000
2189
  }) {
2001
2190
  const queueAttention = [
2002
2191
  ...queueItems
@@ -2101,6 +2290,7 @@ function buildUnifiedStatusReport({
2101
2290
  ...queueAttention.map((item) => item.next_step),
2102
2291
  ])].slice(0, 6),
2103
2292
  suggested_commands: [...new Set(attention.length > 0 ? suggestedCommands : defaultSuggestedCommands)].slice(0, 6),
2293
+ retention_days: retentionDays,
2104
2294
  };
2105
2295
  }
2106
2296
 
@@ -2108,6 +2298,9 @@ async function collectStatusSnapshot(repoRoot) {
2108
2298
  const db = getDb(repoRoot);
2109
2299
  try {
2110
2300
  const leasePolicy = loadLeasePolicy(repoRoot);
2301
+ const retentionDays = await getRetentionDaysForCurrentPlan();
2302
+ pruneDatabaseMaintenance(db, { retentionDays });
2303
+ cleanupOldSyncEvents({ retentionDays }).catch(() => {});
2111
2304
 
2112
2305
  if (leasePolicy.reap_on_status_check) {
2113
2306
  reapStaleLeases(db, leasePolicy.stale_after_minutes, {
@@ -2147,13 +2340,40 @@ async function collectStatusSnapshot(repoRoot) {
2147
2340
  queueItems,
2148
2341
  queueSummary,
2149
2342
  recentQueueEvents,
2343
+ retentionDays,
2150
2344
  });
2151
2345
  } finally {
2152
2346
  db.close();
2153
2347
  }
2154
2348
  }
2155
2349
 
2156
- function renderUnifiedStatusReport(report) {
2350
+ function summarizeTeamCoordinationState(events = [], myUserId = null) {
2351
+ const visibleEvents = events.filter((event) => event.user_id !== myUserId);
2352
+ if (visibleEvents.length === 0) {
2353
+ return {
2354
+ members: 0,
2355
+ queue_events: 0,
2356
+ lease_events: 0,
2357
+ claim_events: 0,
2358
+ latest_queue_event: null,
2359
+ };
2360
+ }
2361
+
2362
+ const activeMembers = new Set(visibleEvents.map((event) => event.user_id || `${event.payload?.email || 'unknown'}:${event.worktree || 'unknown'}`));
2363
+ const queueEvents = visibleEvents.filter((event) => ['queue_added', 'queue_merged', 'queue_blocked'].includes(event.event_type));
2364
+ const leaseEvents = visibleEvents.filter((event) => ['lease_acquired', 'task_done', 'task_failed', 'task_retried'].includes(event.event_type));
2365
+ const claimEvents = visibleEvents.filter((event) => ['claim_added', 'claim_released'].includes(event.event_type));
2366
+
2367
+ return {
2368
+ members: activeMembers.size,
2369
+ queue_events: queueEvents.length,
2370
+ lease_events: leaseEvents.length,
2371
+ claim_events: claimEvents.length,
2372
+ latest_queue_event: queueEvents[0] || null,
2373
+ };
2374
+ }
2375
+
2376
+ function renderUnifiedStatusReport(report, { teamActivity = [], teamSummary = null } = {}) {
2157
2377
  const healthColor = colorForHealth(report.health);
2158
2378
  const badge = healthColor(healthLabel(report.health));
2159
2379
  const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
@@ -2212,10 +2432,43 @@ function renderUnifiedStatusReport(report) {
2212
2432
  console.log(`${chalk.bold('Run next:')} ${chalk.cyan(primaryCommand)}`);
2213
2433
  console.log(`${chalk.dim('why:')} ${nextStepLine}`);
2214
2434
  console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
2435
+ console.log(chalk.dim(`history retention: ${report.retention_days || 7} days`));
2215
2436
  if (report.merge_readiness.policy_state?.active) {
2216
2437
  console.log(chalk.dim(`change policy: ${report.merge_readiness.policy_state.domains.join(', ')} • ${report.merge_readiness.policy_state.enforcement} • missing ${report.merge_readiness.policy_state.missing_task_types.join(', ') || 'none'}`));
2217
2438
  }
2218
2439
 
2440
+ // ── Team activity (Pro cloud sync) ──────────────────────────────────────────
2441
+ if (teamSummary && teamSummary.members > 0) {
2442
+ console.log('');
2443
+ console.log(chalk.bold('Shared cloud state:'));
2444
+ console.log(` ${chalk.dim('members:')} ${teamSummary.members} ${chalk.dim('leases:')} ${teamSummary.lease_events} ${chalk.dim('claims:')} ${teamSummary.claim_events} ${chalk.dim('queue:')} ${teamSummary.queue_events}`);
2445
+ if (teamSummary.latest_queue_event) {
2446
+ console.log(` ${chalk.dim('latest queue event:')} ${chalk.cyan(teamSummary.latest_queue_event.event_type)} ${chalk.dim(teamSummary.latest_queue_event.payload?.source_ref || teamSummary.latest_queue_event.payload?.item_id || '')}`.trim());
2447
+ }
2448
+ }
2449
+ if (teamActivity.length > 0) {
2450
+ console.log('');
2451
+ console.log(chalk.bold('Team activity:'));
2452
+ for (const member of teamActivity) {
2453
+ const email = member.payload?.email ?? chalk.dim(member.user_id?.slice(0, 8) ?? 'unknown');
2454
+ const worktree = chalk.cyan(member.worktree ?? 'unknown');
2455
+ const eventLabel = {
2456
+ task_added: 'added a task',
2457
+ task_done: 'completed a task',
2458
+ task_failed: 'failed a task',
2459
+ task_retried: 'retried a task',
2460
+ lease_acquired: `working on: ${chalk.dim(member.payload?.title ?? '')}`,
2461
+ claim_added: `claimed ${chalk.dim(member.payload?.file_count ?? 0)} file(s)`,
2462
+ claim_released: 'released file claims',
2463
+ queue_added: `queued ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
2464
+ queue_merged: `landed ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
2465
+ queue_blocked: `blocked ${chalk.dim(member.payload?.source_ref ?? member.payload?.item_id ?? 'work')}`,
2466
+ status_ping: 'active',
2467
+ }[member.event_type] ?? member.event_type;
2468
+ console.log(` ${chalk.dim('○')} ${email} · ${worktree} · ${eventLabel}`);
2469
+ }
2470
+ }
2471
+
2219
2472
  const runningLines = report.active_work.length > 0
2220
2473
  ? report.active_work.slice(0, 5).map((item) => {
2221
2474
  const boundary = item.boundary_validation
@@ -2230,6 +2483,27 @@ function renderUnifiedStatusReport(report) {
2230
2483
 
2231
2484
  const blockedItems = report.attention.filter((item) => item.severity === 'block');
2232
2485
  const warningItems = report.attention.filter((item) => item.severity !== 'block');
2486
+ const isQuietEmptyState = report.active_work.length === 0
2487
+ && blockedItems.length === 0
2488
+ && warningItems.length === 0
2489
+ && report.queue.items.length === 0
2490
+ && report.next_up.length === 0
2491
+ && report.failed_tasks.length === 0;
2492
+
2493
+ if (isQuietEmptyState) {
2494
+ console.log('');
2495
+ console.log(healthColor('='.repeat(72)));
2496
+ console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
2497
+ console.log(`${chalk.dim(report.repo_root)}`);
2498
+ console.log(`${chalk.dim(report.summary)}`);
2499
+ console.log(healthColor('='.repeat(72)));
2500
+ console.log('');
2501
+ console.log(chalk.green('Nothing is running yet.'));
2502
+ console.log(`Add work with: ${chalk.cyan('switchman task add "Your first task" --priority 8')}`);
2503
+ console.log(`Or prove the flow in 30 seconds with: ${chalk.cyan('switchman demo')}`);
2504
+ console.log('');
2505
+ return;
2506
+ }
2233
2507
 
2234
2508
  const blockedLines = blockedItems.length > 0
2235
2509
  ? blockedItems.slice(0, 4).flatMap((item) => {
@@ -2405,10 +2679,12 @@ function completeTaskWithRetries(repoRoot, taskId, attempts = 20) {
2405
2679
  let db = null;
2406
2680
  try {
2407
2681
  db = openDb(repoRoot);
2408
- completeTask(db, taskId);
2409
- releaseFileClaims(db, taskId);
2682
+ const result = completeTask(db, taskId);
2683
+ if (result?.status === 'completed') {
2684
+ releaseFileClaims(db, taskId);
2685
+ }
2410
2686
  db.close();
2411
- return;
2687
+ return result;
2412
2688
  } catch (err) {
2413
2689
  lastError = err;
2414
2690
  try { db?.close(); } catch { /* no-op */ }
@@ -2421,6 +2697,210 @@ function completeTaskWithRetries(repoRoot, taskId, attempts = 20) {
2421
2697
  throw lastError;
2422
2698
  }
2423
2699
 
2700
+ function startBackgroundMonitor(repoRoot, { intervalMs = 2000, quarantine = false } = {}) {
2701
+ const existingState = readMonitorState(repoRoot);
2702
+ if (existingState && isProcessRunning(existingState.pid)) {
2703
+ return {
2704
+ already_running: true,
2705
+ state: existingState,
2706
+ state_path: getMonitorStatePath(repoRoot),
2707
+ };
2708
+ }
2709
+
2710
+ const logPath = join(repoRoot, '.switchman', 'monitor.log');
2711
+ const child = spawn(process.execPath, [
2712
+ process.argv[1],
2713
+ 'monitor',
2714
+ 'watch',
2715
+ '--interval-ms',
2716
+ String(intervalMs),
2717
+ ...(quarantine ? ['--quarantine'] : []),
2718
+ '--daemonized',
2719
+ ], {
2720
+ cwd: repoRoot,
2721
+ detached: true,
2722
+ stdio: 'ignore',
2723
+ });
2724
+ child.unref();
2725
+
2726
+ const statePath = writeMonitorState(repoRoot, {
2727
+ pid: child.pid,
2728
+ interval_ms: intervalMs,
2729
+ quarantine: Boolean(quarantine),
2730
+ log_path: logPath,
2731
+ started_at: new Date().toISOString(),
2732
+ });
2733
+
2734
+ return {
2735
+ already_running: false,
2736
+ state: readMonitorState(repoRoot),
2737
+ state_path: statePath,
2738
+ };
2739
+ }
2740
+
2741
+ function renderMonitorEvent(event) {
2742
+ const ownerText = event.owner_worktree
2743
+ ? `${event.owner_worktree}${event.owner_task_id ? ` (${event.owner_task_id})` : ''}`
2744
+ : null;
2745
+ const claimCommand = event.task_id
2746
+ ? `switchman claim ${event.task_id} ${event.worktree} ${event.file_path}`
2747
+ : null;
2748
+
2749
+ if (event.status === 'denied') {
2750
+ console.log(`${chalk.yellow('⚠')} ${chalk.cyan(event.worktree)} modified ${chalk.yellow(event.file_path)} without governed ownership`);
2751
+ if (ownerText) {
2752
+ console.log(` ${chalk.dim('Owned by:')} ${chalk.cyan(ownerText)}${event.owner_task_title ? ` ${chalk.dim(`— ${event.owner_task_title}`)}` : ''}`);
2753
+ }
2754
+ if (claimCommand) {
2755
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan(claimCommand)}`);
2756
+ }
2757
+ console.log(` ${chalk.dim('Or:')} ${chalk.cyan('switchman status')} ${chalk.dim('to inspect current claims and blockers')}`);
2758
+ if (event.enforcement_action) {
2759
+ console.log(` ${chalk.dim('Action:')} ${event.enforcement_action}`);
2760
+ }
2761
+ return;
2762
+ }
2763
+
2764
+ const ownerSuffix = ownerText ? ` ${chalk.dim(`(${ownerText})`)}` : '';
2765
+ console.log(`${chalk.green('✓')} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${ownerSuffix}`);
2766
+ }
2767
+
2768
+ function resolveMonitoredWorktrees(db, repoRoot) {
2769
+ const registeredByPath = new Map(
2770
+ listWorktrees(db)
2771
+ .filter((worktree) => worktree.path)
2772
+ .map((worktree) => [worktree.path, worktree])
2773
+ );
2774
+
2775
+ return listGitWorktrees(repoRoot).map((worktree) => {
2776
+ const registered = registeredByPath.get(worktree.path);
2777
+ if (!registered) return worktree;
2778
+ return {
2779
+ ...worktree,
2780
+ name: registered.name,
2781
+ path: registered.path || worktree.path,
2782
+ branch: registered.branch || worktree.branch,
2783
+ };
2784
+ });
2785
+ }
2786
+
2787
+ function discoverMergeCandidates(db, repoRoot, { targetBranch = 'main' } = {}) {
2788
+ const worktrees = listWorktrees(db).filter((worktree) => worktree.name !== 'main');
2789
+ const activeLeases = new Set(listLeases(db, 'active').map((lease) => lease.worktree));
2790
+ const tasks = listTasks(db);
2791
+ const queueItems = listMergeQueue(db).filter((item) => item.status !== 'merged');
2792
+ const alreadyQueued = new Set(queueItems.map((item) => item.source_worktree).filter(Boolean));
2793
+
2794
+ const eligible = [];
2795
+ const blocked = [];
2796
+ const skipped = [];
2797
+
2798
+ for (const worktree of worktrees) {
2799
+ const doneTasks = tasks.filter((task) => task.worktree === worktree.name && task.status === 'done');
2800
+ if (doneTasks.length === 0) {
2801
+ skipped.push({
2802
+ worktree: worktree.name,
2803
+ branch: worktree.branch,
2804
+ reason: 'no_completed_tasks',
2805
+ summary: 'no completed tasks are assigned to this worktree yet',
2806
+ command: `switchman task list --status done`,
2807
+ });
2808
+ continue;
2809
+ }
2810
+
2811
+ if (!worktree.branch || worktree.branch === targetBranch) {
2812
+ skipped.push({
2813
+ worktree: worktree.name,
2814
+ branch: worktree.branch || null,
2815
+ reason: 'no_merge_branch',
2816
+ summary: `worktree is on ${targetBranch} already`,
2817
+ command: `switchman worktree list`,
2818
+ });
2819
+ continue;
2820
+ }
2821
+
2822
+ if (!gitBranchExists(repoRoot, worktree.branch)) {
2823
+ blocked.push({
2824
+ worktree: worktree.name,
2825
+ branch: worktree.branch,
2826
+ reason: 'missing_branch',
2827
+ summary: `branch ${worktree.branch} is not available in git`,
2828
+ command: `switchman worktree sync`,
2829
+ });
2830
+ continue;
2831
+ }
2832
+
2833
+ if (activeLeases.has(worktree.name)) {
2834
+ blocked.push({
2835
+ worktree: worktree.name,
2836
+ branch: worktree.branch,
2837
+ reason: 'active_lease',
2838
+ summary: 'an active lease is still running in this worktree',
2839
+ command: `switchman status`,
2840
+ });
2841
+ continue;
2842
+ }
2843
+
2844
+ if (alreadyQueued.has(worktree.name)) {
2845
+ skipped.push({
2846
+ worktree: worktree.name,
2847
+ branch: worktree.branch,
2848
+ reason: 'already_queued',
2849
+ summary: 'worktree is already in the landing queue',
2850
+ command: `switchman queue status`,
2851
+ });
2852
+ continue;
2853
+ }
2854
+
2855
+ const dirtyFiles = getWorktreeChangedFiles(worktree.path, repoRoot);
2856
+ if (dirtyFiles.length > 0) {
2857
+ blocked.push({
2858
+ worktree: worktree.name,
2859
+ branch: worktree.branch,
2860
+ path: worktree.path,
2861
+ files: dirtyFiles,
2862
+ reason: 'dirty_worktree',
2863
+ summary: `worktree has uncommitted changes: ${dirtyFiles.slice(0, 5).join(', ')}${dirtyFiles.length > 5 ? ` +${dirtyFiles.length - 5} more` : ''}`,
2864
+ command: `cd ${JSON.stringify(worktree.path)} && git status`,
2865
+ });
2866
+ continue;
2867
+ }
2868
+
2869
+ const freshness = gitAssessBranchFreshness(repoRoot, targetBranch, worktree.branch);
2870
+ eligible.push({
2871
+ worktree: worktree.name,
2872
+ branch: worktree.branch,
2873
+ path: worktree.path,
2874
+ done_task_count: doneTasks.length,
2875
+ done_task_titles: doneTasks.slice(0, 3).map((task) => task.title),
2876
+ freshness,
2877
+ });
2878
+ }
2879
+
2880
+ return { eligible, blocked, skipped, queue_items: queueItems };
2881
+ }
2882
+
2883
+ function printMergeDiscovery(discovery) {
2884
+ console.log('');
2885
+ console.log(chalk.bold(`Checking ${discovery.eligible.length + discovery.blocked.length + discovery.skipped.length} worktree(s)...`));
2886
+
2887
+ for (const entry of discovery.eligible) {
2888
+ const freshness = entry.freshness?.state && entry.freshness.state !== 'unknown'
2889
+ ? ` ${chalk.dim(`(${entry.freshness.state})`)}`
2890
+ : '';
2891
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch)}${freshness}`);
2892
+ }
2893
+
2894
+ for (const entry of discovery.blocked) {
2895
+ console.log(` ${chalk.yellow('!')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch || 'no branch')} ${chalk.dim(`— ${entry.summary}`)}`);
2896
+ }
2897
+
2898
+ for (const entry of discovery.skipped) {
2899
+ if (entry.reason === 'no_completed_tasks') continue;
2900
+ console.log(` ${chalk.dim('·')} ${chalk.cyan(entry.worktree)} ${chalk.dim(entry.branch || 'no branch')} ${chalk.dim(`— ${entry.summary}`)}`);
2901
+ }
2902
+ }
2903
+
2424
2904
  // ─── Program ──────────────────────────────────────────────────────────────────
2425
2905
 
2426
2906
  program
@@ -2429,23 +2909,87 @@ program
2429
2909
  .version('0.1.0');
2430
2910
 
2431
2911
  program.showHelpAfterError('(run with --help for usage examples)');
2912
+ const ROOT_HELP_COMMANDS = new Set([
2913
+ 'advanced',
2914
+ 'demo',
2915
+ 'setup',
2916
+ 'verify-setup',
2917
+ 'login',
2918
+ 'upgrade',
2919
+ 'plan',
2920
+ 'task',
2921
+ 'status',
2922
+ 'merge',
2923
+ 'repair',
2924
+ 'help',
2925
+ ]);
2926
+ program.configureHelp({
2927
+ visibleCommands(cmd) {
2928
+ const commands = Help.prototype.visibleCommands.call(this, cmd);
2929
+ if (cmd.parent) return commands;
2930
+ return commands.filter((command) => ROOT_HELP_COMMANDS.has(command.name()) && !command._switchmanAdvanced);
2931
+ },
2932
+ });
2432
2933
  program.addHelpText('after', `
2433
2934
  Start here:
2434
2935
  switchman demo
2435
- switchman setup --agents 5
2936
+ switchman setup --agents 3
2937
+ switchman task add "Your task" --priority 8
2436
2938
  switchman status --watch
2437
- switchman gate ci
2939
+ switchman gate ci && switchman queue run
2438
2940
 
2439
- Most useful commands:
2440
- switchman task add "Implement auth helper" --priority 9
2441
- switchman lease next --json
2442
- switchman queue run --watch
2941
+ For you (the operator):
2942
+ switchman demo
2943
+ switchman setup
2944
+ switchman task add
2945
+ switchman status
2946
+ switchman merge
2947
+ switchman repair
2948
+ switchman upgrade
2949
+ switchman login
2950
+ switchman plan "Add authentication" (Pro)
2951
+
2952
+ For your agents (via CLAUDE.md or MCP):
2953
+ switchman lease next
2954
+ switchman claim
2955
+ switchman task done
2956
+ switchman write
2957
+ switchman wrap
2443
2958
 
2444
2959
  Docs:
2445
2960
  README.md
2446
- docs/setup-cursor.md
2961
+ docs/setup-claude-code.md
2962
+
2963
+ Power tools:
2964
+ switchman advanced --help
2447
2965
  `);
2448
2966
 
2967
+ const advancedCmd = program
2968
+ .command('advanced')
2969
+ .description('Show advanced, experimental, and power-user command groups')
2970
+ .addHelpText('after', `
2971
+ Advanced operator commands:
2972
+ switchman pipeline <...>
2973
+ switchman audit <...>
2974
+ switchman policy <...>
2975
+ switchman monitor <...>
2976
+ switchman repair
2977
+
2978
+ Experimental commands:
2979
+ switchman semantic <...>
2980
+ switchman object <...>
2981
+
2982
+ Compatibility aliases:
2983
+ switchman doctor
2984
+
2985
+ Tip:
2986
+ The main help keeps the day-one workflow small on purpose.
2987
+ `)
2988
+ .action(() => {
2989
+ advancedCmd.outputHelp();
2990
+ });
2991
+ advancedCmd._switchmanAdvanced = false;
2992
+
2449
2993
  program
2450
2994
  .command('demo')
2451
2995
  .description('Create a throwaway repo that proves overlapping claims are blocked and safe landing works')
@@ -2508,12 +3052,16 @@ program
2508
3052
  }
2509
3053
 
2510
3054
  const mcpConfigWrites = installMcpConfig([...new Set([repoRoot, ...gitWorktrees.map((wt) => wt.path)])]);
3055
+ const mcpExclude = ensureProjectLocalMcpGitExcludes(repoRoot);
2511
3056
 
2512
3057
  db.close();
2513
3058
  spinner.succeed(`Initialized in ${chalk.cyan(repoRoot)}`);
2514
3059
  console.log(chalk.dim(` Found and registered ${gitWorktrees.length} git worktree(s)`));
2515
3060
  console.log(chalk.dim(` Database: .switchman/switchman.db`));
2516
3061
  console.log(chalk.dim(` MCP config: ${mcpConfigWrites.filter((result) => result.changed).length} file(s) written`));
3062
+ if (mcpExclude.managed) {
3063
+ console.log(chalk.dim(` MCP excludes: ${mcpExclude.changed ? 'updated' : 'already set'} in .git/info/exclude`));
3064
+ }
2517
3065
  console.log('');
2518
3066
  console.log(`Next steps:`);
2519
3067
  console.log(` ${chalk.cyan('switchman task add "Fix the login bug"')} — add a task`);
@@ -2533,6 +3081,8 @@ program
2533
3081
  .description('One-command setup: create agent workspaces and initialise Switchman')
2534
3082
  .option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
2535
3083
  .option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
3084
+ .option('--no-monitor', 'Do not start the background rogue-edit monitor')
3085
+ .option('--monitor-interval-ms <ms>', 'Polling interval for the background monitor', '2000')
2536
3086
  .addHelpText('after', `
2537
3087
  Examples:
2538
3088
  switchman setup --agents 5
@@ -2546,6 +3096,24 @@ Examples:
2546
3096
  process.exit(1);
2547
3097
  }
2548
3098
 
3099
+ if (agentCount > FREE_AGENT_LIMIT) {
3100
+ const licence = await checkLicence();
3101
+ if (!licence.valid) {
3102
+ console.log('');
3103
+ console.log(chalk.yellow(` ⚠ Free tier supports up to ${FREE_AGENT_LIMIT} agents.`));
3104
+ console.log('');
3105
+ console.log(` You requested ${chalk.cyan(agentCount)} agents — that requires ${chalk.bold('Switchman Pro')}.`);
3106
+ console.log('');
3107
+ console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
3108
+ console.log(` ${chalk.dim('Or run: ')} ${chalk.cyan('switchman upgrade')}`);
3109
+ console.log('');
3110
+ process.exit(1);
3111
+ }
3112
+ if (licence.offline) {
3113
+ console.log(chalk.dim(` Pro licence verified (offline cache · ${Math.ceil((7 * 24 * 60 * 60 * 1000 - (Date.now() - (licence.cached_at ?? 0))) / (24 * 60 * 60 * 1000))}d remaining)`));
3114
+ }
3115
+ }
3116
+
2549
3117
  const repoRoot = getRepo();
2550
3118
  const spinner = ora('Setting up Switchman...').start();
2551
3119
 
@@ -2591,6 +3159,12 @@ Examples:
2591
3159
  }
2592
3160
 
2593
3161
  const mcpConfigWrites = installMcpConfig([...new Set([repoRoot, ...created.map((wt) => wt.path)])]);
3162
+ const mcpExclude = ensureProjectLocalMcpGitExcludes(repoRoot);
3163
+
3164
+ const monitorIntervalMs = Math.max(100, Number.parseInt(opts.monitorIntervalMs, 10) || 2000);
3165
+ const monitorState = opts.monitor
3166
+ ? startBackgroundMonitor(repoRoot, { intervalMs: monitorIntervalMs, quarantine: false })
3167
+ : null;
2594
3168
 
2595
3169
  db.close();
2596
3170
 
@@ -2610,6 +3184,20 @@ Examples:
2610
3184
  const status = result.created ? 'created' : result.changed ? 'updated' : 'unchanged';
2611
3185
  console.log(` ${chalk.green('✓')} ${chalk.cyan(result.path)} ${chalk.dim(`(${status})`)}`);
2612
3186
  }
3187
+ if (mcpExclude.managed) {
3188
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(mcpExclude.path)} ${chalk.dim(`(${mcpExclude.changed ? 'updated' : 'unchanged'})`)}`);
3189
+ }
3190
+
3191
+ if (opts.monitor) {
3192
+ console.log('');
3193
+ console.log(chalk.bold('Monitor:'));
3194
+ if (monitorState?.already_running) {
3195
+ console.log(` ${chalk.green('✓')} Background rogue-edit monitor already running ${chalk.dim(`(pid ${monitorState.state.pid})`)}`);
3196
+ } else {
3197
+ console.log(` ${chalk.green('✓')} Started background rogue-edit monitor ${chalk.dim(`(pid ${monitorState?.state?.pid ?? 'unknown'})`)}`);
3198
+ }
3199
+ console.log(` ${chalk.dim('interval:')} ${monitorIntervalMs}ms`);
3200
+ }
2613
3201
 
2614
3202
  console.log('');
2615
3203
  console.log(chalk.bold('Next steps:'));
@@ -2618,6 +3206,13 @@ Examples:
2618
3206
  console.log(` 2. Open Claude Code or Cursor in the workspaces above — the local MCP config will attach Switchman automatically`);
2619
3207
  console.log(` 3. Keep the repo dashboard open while work starts:`);
2620
3208
  console.log(` ${chalk.cyan('switchman status --watch')}`);
3209
+ console.log(` 4. Run the final check and land finished work:`);
3210
+ console.log(` ${chalk.cyan('switchman gate ci')}`);
3211
+ console.log(` ${chalk.cyan('switchman queue run')}`);
3212
+ if (opts.monitor) {
3213
+ console.log(` 5. Watch for rogue edits or direct writes in real time:`);
3214
+ console.log(` ${chalk.cyan('switchman monitor status')}`);
3215
+ }
2621
3216
  console.log('');
2622
3217
 
2623
3218
  const verification = collectSetupVerification(repoRoot);
@@ -2800,6 +3395,132 @@ Examples:
2800
3395
  });
2801
3396
 
2802
3397
 
3398
+ // ── plan ──────────────────────────────────────────────────────────────────────
3399
+
3400
+ program
3401
+ .command('plan [goal]')
3402
+ .description('Pro: suggest a parallel task plan from an explicit goal')
3403
+ .option('--apply', 'Create the suggested tasks in Switchman')
3404
+ .option('--max-tasks <n>', 'Maximum number of suggested tasks', '6')
3405
+ .option('--json', 'Output raw JSON')
3406
+ .addHelpText('after', `
3407
+ Examples:
3408
+ switchman plan "Add authentication"
3409
+ switchman plan "Add authentication" --apply
3410
+ `)
3411
+ .action(async (goal, opts) => {
3412
+ const repoRoot = getRepo();
3413
+ const db = getOptionalDb(repoRoot);
3414
+
3415
+ try {
3416
+ const licence = await checkLicence();
3417
+ if (!licence.valid) {
3418
+ console.log('');
3419
+ console.log(chalk.yellow(' ⚠ AI planning requires Switchman Pro.'));
3420
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
3421
+ console.log(` ${chalk.dim('Or visit:')} ${chalk.cyan(PRO_PAGE_URL)}`);
3422
+ console.log('');
3423
+ process.exitCode = 1;
3424
+ return;
3425
+ }
3426
+
3427
+ if (!goal || !goal.trim()) {
3428
+ console.log('');
3429
+ console.log(chalk.yellow(' ⚠ AI planning currently requires an explicit goal.'));
3430
+ console.log(` ${chalk.dim('Try:')} ${chalk.cyan('switchman plan "Add authentication"')}`);
3431
+ console.log(` ${chalk.dim('Then:')} ${chalk.cyan('switchman plan "Add authentication" --apply')}`);
3432
+ console.log('');
3433
+ process.exitCode = 1;
3434
+ return;
3435
+ }
3436
+
3437
+ const context = collectPlanContext(repoRoot, goal || null);
3438
+ const planningWorktrees = resolvePlanningWorktrees(repoRoot, db);
3439
+ const pipelineId = `plan-${slugifyValue(context.title)}-${Date.now().toString(36)}`;
3440
+ const plannedTasks = planPipelineTasks({
3441
+ pipelineId,
3442
+ title: context.title,
3443
+ description: context.description,
3444
+ worktrees: planningWorktrees,
3445
+ maxTasks: Math.max(1, parseInt(opts.maxTasks, 10) || 6),
3446
+ repoRoot,
3447
+ });
3448
+
3449
+ if (opts.json) {
3450
+ const payload = {
3451
+ title: context.title,
3452
+ context: {
3453
+ found: context.found,
3454
+ used: context.used,
3455
+ branch: context.branch,
3456
+ },
3457
+ planned_tasks: plannedTasks.map((task) => ({
3458
+ id: task.id,
3459
+ title: task.title,
3460
+ suggested_worktree: task.suggested_worktree || null,
3461
+ task_type: task.task_spec?.task_type || null,
3462
+ dependencies: task.dependencies || [],
3463
+ })),
3464
+ apply_ready: Boolean(db),
3465
+ };
3466
+ console.log(JSON.stringify(payload, null, 2));
3467
+ return;
3468
+ }
3469
+
3470
+ console.log(chalk.bold('Reading repo context...'));
3471
+ if (context.found.length > 0) {
3472
+ console.log(`${chalk.dim('Found:')} ${context.found.join(', ')}`);
3473
+ } else {
3474
+ console.log(chalk.dim('Found: local repo context only'));
3475
+ }
3476
+ console.log('');
3477
+ console.log(`${chalk.bold('Suggested plan based on:')} ${context.used.length > 0 ? formatHumanList(context.used) : 'available repo context'}`);
3478
+ console.log('');
3479
+
3480
+ plannedTasks.forEach((task, index) => {
3481
+ const worktreeLabel = task.suggested_worktree ? chalk.cyan(task.suggested_worktree) : chalk.dim('unassigned');
3482
+ console.log(` ${chalk.green('✓')} ${chalk.bold(`${index + 1}.`)} ${task.title} ${chalk.dim('→')} ${worktreeLabel}`);
3483
+ });
3484
+
3485
+ if (!opts.apply) {
3486
+ console.log('');
3487
+ if (!db) {
3488
+ console.log(chalk.dim('Preview only — run `switchman setup --agents 3` first if you want Switchman to create and track these tasks.'));
3489
+ } else {
3490
+ console.log(chalk.dim('Preview only — rerun with `switchman plan --apply` to create these tasks.'));
3491
+ }
3492
+ return;
3493
+ }
3494
+
3495
+ if (!db) {
3496
+ console.log('');
3497
+ console.log(`${chalk.red('✗')} Switchman is not set up in this repo yet.`);
3498
+ console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman setup --agents 3')}`);
3499
+ process.exitCode = 1;
3500
+ return;
3501
+ }
3502
+
3503
+ console.log('');
3504
+ for (const task of plannedTasks) {
3505
+ createTask(db, {
3506
+ id: task.id,
3507
+ title: task.title,
3508
+ description: `Planned from: ${context.title}`,
3509
+ priority: planTaskPriority(task.task_spec),
3510
+ });
3511
+ upsertTaskSpec(db, task.id, task.task_spec);
3512
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(task.id)} ${chalk.dim(task.title)}`);
3513
+ }
3514
+
3515
+ console.log('');
3516
+ console.log(`${chalk.green('✓')} Planned ${plannedTasks.length} task(s) from repo context.`);
3517
+ console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman status --watch')}`);
3518
+ } finally {
3519
+ db?.close();
3520
+ }
3521
+ });
3522
+
3523
+
2803
3524
  // ── task ──────────────────────────────────────────────────────────────────────
2804
3525
 
2805
3526
  const taskCmd = program.command('task').description('Manage the task list');
@@ -2828,6 +3549,7 @@ taskCmd
2828
3549
  db.close();
2829
3550
  const scopeWarning = analyzeTaskScope(title, opts.description || '');
2830
3551
  console.log(`${chalk.green('✓')} Task created: ${chalk.cyan(taskId)}`);
3552
+ pushSyncEvent('task_added', { task_id: taskId, title, priority: parseInt(opts.priority) }).catch(() => {});
2831
3553
  console.log(` ${chalk.dim(title)}`);
2832
3554
  if (scopeWarning) {
2833
3555
  console.log(chalk.yellow(` warning: ${scopeWarning.summary}`));
@@ -2901,6 +3623,7 @@ taskCmd
2901
3623
  }
2902
3624
 
2903
3625
  console.log(`${chalk.green('✓')} Reset ${chalk.cyan(task.id)} to pending`);
3626
+ pushSyncEvent('task_retried', { task_id: task.id, title: task.title, reason: opts.reason || 'manual retry' }).catch(() => {});
2904
3627
  console.log(` ${chalk.dim('title:')} ${task.title}`);
2905
3628
  console.log(`${chalk.yellow('next:')} switchman task assign ${task.id} <workspace>`);
2906
3629
  });
@@ -2932,6 +3655,12 @@ taskCmd
2932
3655
  }
2933
3656
 
2934
3657
  console.log(`${chalk.green('✓')} Reset ${result.retried.length} stale task(s) to pending`);
3658
+ pushSyncEvent('task_retried', {
3659
+ pipeline_id: result.pipeline_id || null,
3660
+ task_count: result.retried.length,
3661
+ task_ids: result.retried.map((task) => task.id),
3662
+ reason: opts.reason,
3663
+ }).catch(() => {});
2935
3664
  if (result.pipeline_id) {
2936
3665
  console.log(` ${chalk.dim('pipeline:')} ${result.pipeline_id}`);
2937
3666
  }
@@ -2945,8 +3674,25 @@ taskCmd
2945
3674
  .action((taskId) => {
2946
3675
  const repoRoot = getRepo();
2947
3676
  try {
2948
- completeTaskWithRetries(repoRoot, taskId);
3677
+ const result = completeTaskWithRetries(repoRoot, taskId);
3678
+ if (result?.status === 'already_done') {
3679
+ console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} was already marked done — no new changes were recorded`);
3680
+ return;
3681
+ }
3682
+ if (result?.status === 'failed') {
3683
+ console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is currently failed — retry it before marking it done again`);
3684
+ return;
3685
+ }
3686
+ if (result?.status === 'not_in_progress') {
3687
+ console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} is not currently in progress — start a lease before marking it done`);
3688
+ return;
3689
+ }
3690
+ if (result?.status === 'no_active_lease') {
3691
+ console.log(`${chalk.yellow('!')} Task ${chalk.cyan(taskId)} has no active lease — reacquire the task before marking it done`);
3692
+ return;
3693
+ }
2949
3694
  console.log(`${chalk.green('✓')} Task ${chalk.cyan(taskId)} marked done — file claims released`);
3695
+ pushSyncEvent('task_done', { task_id: taskId }).catch(() => {});
2950
3696
  } catch (err) {
2951
3697
  console.error(chalk.red(err.message));
2952
3698
  process.exitCode = 1;
@@ -2963,6 +3709,7 @@ taskCmd
2963
3709
  releaseFileClaims(db, taskId);
2964
3710
  db.close();
2965
3711
  console.log(`${chalk.red('✗')} Task ${chalk.cyan(taskId)} marked failed`);
3712
+ pushSyncEvent('task_failed', { task_id: taskId, reason: reason || null }).catch(() => {});
2966
3713
  });
2967
3714
 
2968
3715
  taskCmd
@@ -3090,6 +3837,13 @@ Pipeline landing rule:
3090
3837
 
3091
3838
  const result = enqueueMergeItem(db, payload);
3092
3839
  db.close();
3840
+ pushSyncEvent('queue_added', {
3841
+ item_id: result.id,
3842
+ source_type: result.source_type,
3843
+ source_ref: result.source_ref,
3844
+ source_worktree: result.source_worktree || null,
3845
+ target_branch: result.target_branch,
3846
+ }, { worktree: result.source_worktree || null }).catch(() => {});
3093
3847
 
3094
3848
  if (opts.json) {
3095
3849
  console.log(JSON.stringify(result, null, 2));
@@ -3177,6 +3931,15 @@ What it helps you answer:
3177
3931
  return;
3178
3932
  }
3179
3933
 
3934
+ if (items.length === 0) {
3935
+ console.log('');
3936
+ console.log(chalk.bold('switchman queue status'));
3937
+ console.log('');
3938
+ console.log('Queue is empty.');
3939
+ console.log(`Add finished work with: ${chalk.cyan('switchman queue add --worktree agent1')}`);
3940
+ return;
3941
+ }
3942
+
3180
3943
  const queueHealth = summary.counts.blocked > 0
3181
3944
  ? 'block'
3182
3945
  : summary.counts.retrying > 0 || summary.counts.held > 0 || summary.counts.wave_blocked > 0 || summary.counts.escalated > 0
@@ -3389,9 +4152,26 @@ Examples:
3389
4152
  for (const entry of aggregate.processed) {
3390
4153
  const item = entry.item;
3391
4154
  if (entry.status === 'merged') {
4155
+ pushSyncEvent('queue_merged', {
4156
+ item_id: item.id,
4157
+ source_type: item.source_type,
4158
+ source_ref: item.source_ref,
4159
+ source_worktree: item.source_worktree || null,
4160
+ target_branch: item.target_branch,
4161
+ merged_commit: item.merged_commit || null,
4162
+ }, { worktree: item.source_worktree || null }).catch(() => {});
3392
4163
  console.log(`${chalk.green('✓')} Merged ${chalk.cyan(item.id)} into ${chalk.bold(item.target_branch)}`);
3393
4164
  console.log(` ${chalk.dim('commit:')} ${item.merged_commit}`);
3394
4165
  } else {
4166
+ pushSyncEvent('queue_blocked', {
4167
+ item_id: item.id,
4168
+ source_type: item.source_type,
4169
+ source_ref: item.source_ref,
4170
+ source_worktree: item.source_worktree || null,
4171
+ target_branch: item.target_branch,
4172
+ error_code: item.last_error_code || null,
4173
+ error_summary: item.last_error_summary || null,
4174
+ }, { worktree: item.source_worktree || null }).catch(() => {});
3395
4175
  console.log(`${chalk.red('✗')} Blocked ${chalk.cyan(item.id)}`);
3396
4176
  console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
3397
4177
  if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
@@ -3415,8 +4195,144 @@ Examples:
3415
4195
  }
3416
4196
  });
3417
4197
 
3418
- queueCmd
3419
- .command('retry <itemId>')
4198
+ program
4199
+ .command('merge')
4200
+ .description('Queue finished worktrees and land safe work through one guided front door')
4201
+ .option('--target <branch>', 'Target branch to merge into', 'main')
4202
+ .option('--dry-run', 'Preview mergeable work without queueing or landing anything')
4203
+ .option('--json', 'Output raw JSON')
4204
+ .addHelpText('after', `
4205
+ Examples:
4206
+ switchman merge
4207
+ switchman merge --dry-run
4208
+ switchman merge --target release
4209
+ `)
4210
+ .action(async (opts) => {
4211
+ const repoRoot = getRepo();
4212
+ const db = getDb(repoRoot);
4213
+
4214
+ try {
4215
+ const discovery = discoverMergeCandidates(db, repoRoot, { targetBranch: opts.target || 'main' });
4216
+ const queued = [];
4217
+
4218
+ for (const entry of discovery.eligible) {
4219
+ queued.push(enqueueMergeItem(db, {
4220
+ sourceType: 'worktree',
4221
+ sourceRef: entry.branch,
4222
+ sourceWorktree: entry.worktree,
4223
+ targetBranch: opts.target || 'main',
4224
+ submittedBy: 'switchman merge',
4225
+ }));
4226
+ }
4227
+
4228
+ const queueItems = listMergeQueue(db);
4229
+ const summary = buildQueueStatusSummary(queueItems, { db, repoRoot });
4230
+ const mergeOrder = summary.recommended_sequence
4231
+ .filter((item) => ['land_now', 'prepare_next'].includes(item.lane))
4232
+ .map((item) => item.source_ref);
4233
+
4234
+ if (opts.json) {
4235
+ if (opts.dryRun) {
4236
+ console.log(JSON.stringify({ discovery, queued, summary, merge_order: mergeOrder, dry_run: true }, null, 2));
4237
+ db.close();
4238
+ return;
4239
+ }
4240
+
4241
+ const gate = await evaluateQueueRepoGate(db, repoRoot);
4242
+ const runnableCount = listMergeQueue(db).filter((item) => ['queued', 'retrying'].includes(item.status)).length;
4243
+ const result = gate.ok
4244
+ ? await runMergeQueue(db, repoRoot, {
4245
+ targetBranch: opts.target || 'main',
4246
+ maxItems: Math.max(1, runnableCount),
4247
+ mergeBudget: Math.max(1, runnableCount),
4248
+ followPlan: false,
4249
+ })
4250
+ : null;
4251
+ console.log(JSON.stringify({ discovery, queued, summary, merge_order: mergeOrder, gate, result }, null, 2));
4252
+ db.close();
4253
+ return;
4254
+ }
4255
+
4256
+ printMergeDiscovery(discovery);
4257
+
4258
+ if (discovery.blocked.length > 0) {
4259
+ console.log('');
4260
+ console.log(chalk.bold('Needs attention before landing:'));
4261
+ for (const entry of discovery.blocked) {
4262
+ console.log(` ${chalk.yellow(entry.worktree)}: ${entry.summary}`);
4263
+ console.log(` ${chalk.dim('run:')} ${chalk.cyan(entry.command)}`);
4264
+ }
4265
+ }
4266
+
4267
+ if (mergeOrder.length > 0) {
4268
+ console.log('');
4269
+ console.log(`${chalk.bold('Merge order:')} ${mergeOrder.join(' → ')}`);
4270
+ }
4271
+
4272
+ if (opts.dryRun) {
4273
+ console.log('');
4274
+ console.log(chalk.dim('Dry run only — nothing was landed.'));
4275
+ db.close();
4276
+ return;
4277
+ }
4278
+
4279
+ if (discovery.eligible.length === 0) {
4280
+ console.log('');
4281
+ console.log(chalk.dim('No finished worktrees are ready to land yet.'));
4282
+ console.log(`${chalk.yellow('next:')} ${chalk.cyan('switchman status')}`);
4283
+ db.close();
4284
+ return;
4285
+ }
4286
+
4287
+ const gate = await evaluateQueueRepoGate(db, repoRoot);
4288
+ if (!gate.ok) {
4289
+ console.log('');
4290
+ console.log(`${chalk.red('✗')} Merge gate blocked landing`);
4291
+ console.log(` ${chalk.dim(gate.summary)}`);
4292
+ console.log(` ${chalk.yellow('next:')} ${chalk.cyan('switchman gate ci')}`);
4293
+ db.close();
4294
+ return;
4295
+ }
4296
+
4297
+ console.log('');
4298
+ const runnableCount = listMergeQueue(db).filter((item) => ['queued', 'retrying'].includes(item.status)).length;
4299
+ const result = await runMergeQueue(db, repoRoot, {
4300
+ targetBranch: opts.target || 'main',
4301
+ maxItems: Math.max(1, runnableCount),
4302
+ mergeBudget: Math.max(1, runnableCount),
4303
+ followPlan: false,
4304
+ });
4305
+
4306
+ const merged = result.processed.filter((item) => item.status === 'merged');
4307
+ for (const mergedItem of merged) {
4308
+ console.log(` ${chalk.green('✓')} Landed ${chalk.cyan(mergedItem.item.source_ref)} into ${chalk.bold(mergedItem.item.target_branch)}`);
4309
+ }
4310
+
4311
+ if (merged.length > 0 && !result.deferred && result.processed.every((item) => item.status === 'merged')) {
4312
+ console.log('');
4313
+ console.log(`${chalk.green('✓')} Done. ${merged.length} worktree(s) landed cleanly.`);
4314
+ db.close();
4315
+ return;
4316
+ }
4317
+
4318
+ const blocked = result.processed.find((item) => item.status !== 'merged')?.item || result.deferred || null;
4319
+ if (blocked) {
4320
+ console.log('');
4321
+ console.log(`${chalk.yellow('!')} Landing stopped at ${chalk.cyan(blocked.source_ref)}`);
4322
+ if (blocked.last_error_summary) console.log(` ${chalk.dim(blocked.last_error_summary)}`);
4323
+ if (blocked.next_action) console.log(` ${chalk.yellow('next:')} ${blocked.next_action}`);
4324
+ }
4325
+
4326
+ db.close();
4327
+ } catch (err) {
4328
+ db.close();
4329
+ console.error(chalk.red(err.message));
4330
+ process.exitCode = 1;
4331
+ }
4332
+ });
4333
+
4334
+ queueCmd
4335
+ .command('retry <itemId>')
3420
4336
  .description('Retry a blocked merge queue item')
3421
4337
  .option('--json', 'Output raw JSON')
3422
4338
  .action((itemId, opts) => {
@@ -3771,6 +4687,7 @@ explainCmd
3771
4687
  // ── pipeline ──────────────────────────────────────────────────────────────────
3772
4688
 
3773
4689
  const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
4690
+ pipelineCmd._switchmanAdvanced = true;
3774
4691
  pipelineCmd.addHelpText('after', `
3775
4692
  Examples:
3776
4693
  switchman pipeline start "Harden auth API permissions"
@@ -4621,6 +5538,7 @@ Examples:
4621
5538
  }
4622
5539
 
4623
5540
  console.log(`${chalk.green('✓')} Lease acquired: ${chalk.bold(task.title)}`);
5541
+ pushSyncEvent('lease_acquired', { task_id: task.id, title: task.title }, { worktree: worktreeName }).catch(() => {});
4624
5542
  console.log(` ${chalk.dim('task:')} ${task.id} ${chalk.dim('lease:')} ${lease.id}`);
4625
5543
  console.log(` ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
4626
5544
  });
@@ -4803,25 +5721,31 @@ wtCmd
4803
5721
  const db = getDb(repoRoot);
4804
5722
  const worktrees = listWorktrees(db);
4805
5723
  const gitWorktrees = listGitWorktrees(repoRoot);
4806
- db.close();
4807
5724
 
4808
5725
  if (!worktrees.length && !gitWorktrees.length) {
5726
+ db.close();
4809
5727
  console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
4810
5728
  return;
4811
5729
  }
4812
5730
 
4813
5731
  // Show git worktrees (source of truth) annotated with db info
5732
+ const complianceReport = evaluateRepoCompliance(db, repoRoot, gitWorktrees);
4814
5733
  console.log('');
4815
5734
  console.log(chalk.bold('Git Worktrees:'));
4816
5735
  for (const wt of gitWorktrees) {
4817
5736
  const dbInfo = worktrees.find(d => d.path === wt.path);
5737
+ const complianceInfo = complianceReport.worktreeCompliance.find((entry) => entry.worktree === wt.name) || null;
4818
5738
  const agent = dbInfo?.agent ? chalk.cyan(dbInfo.agent) : chalk.dim('no agent');
4819
5739
  const status = dbInfo?.status ? statusBadge(dbInfo.status) : chalk.dim('unregistered');
4820
- const compliance = dbInfo?.compliance_state ? statusBadge(dbInfo.compliance_state) : chalk.dim('unknown');
5740
+ const compliance = complianceInfo?.compliance_state ? statusBadge(complianceInfo.compliance_state) : dbInfo?.compliance_state ? statusBadge(dbInfo.compliance_state) : chalk.dim('unknown');
4821
5741
  console.log(` ${chalk.bold(wt.name.padEnd(20))} ${status} ${compliance} branch: ${chalk.cyan(wt.branch || 'unknown')} agent: ${agent}`);
4822
5742
  console.log(` ${chalk.dim(wt.path)}`);
5743
+ if ((complianceInfo?.unclaimed_changed_files || []).length > 0) {
5744
+ console.log(` ${chalk.red('files:')} ${complianceInfo.unclaimed_changed_files.slice(0, 5).join(', ')}${complianceInfo.unclaimed_changed_files.length > 5 ? ` ${chalk.dim(`+${complianceInfo.unclaimed_changed_files.length - 5} more`)}` : ''}`);
5745
+ }
4823
5746
  }
4824
5747
  console.log('');
5748
+ db.close();
4825
5749
  });
4826
5750
 
4827
5751
  wtCmd
@@ -4845,13 +5769,14 @@ program
4845
5769
  .command('claim <taskId> <worktree> [files...]')
4846
5770
  .description('Lock files for a task before editing')
4847
5771
  .option('--agent <name>', 'Agent name')
4848
- .option('--force', 'Claim even if conflicts exist')
5772
+ .option('--force', 'Emergency override for manual recovery when a conflicting claim is known to be stale or wrong')
4849
5773
  .addHelpText('after', `
4850
5774
  Examples:
4851
5775
  switchman claim task-123 agent2 src/auth.js src/server.js
4852
5776
  switchman claim task-123 agent2 src/auth.js --agent cursor
4853
5777
 
4854
5778
  Use this before editing files in a shared repo.
5779
+ Only use --force for operator-led recovery after checking switchman status or switchman explain claim <path>.
4855
5780
  `)
4856
5781
  .action((taskId, worktree, files, opts) => {
4857
5782
  if (!files.length) {
@@ -4870,7 +5795,7 @@ Use this before editing files in a shared repo.
4870
5795
  for (const c of conflicts) {
4871
5796
  console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
4872
5797
  }
4873
- console.log(chalk.dim('\nUse --force to claim anyway, or pick different files first.'));
5798
+ console.log(chalk.dim('\nDo not use --force as a shortcut. Check status or explain the claim first, then only override if the existing claim is known-bad.'));
4874
5799
  console.log(`${chalk.yellow('next:')} switchman status`);
4875
5800
  process.exitCode = 1;
4876
5801
  return;
@@ -4878,6 +5803,12 @@ Use this before editing files in a shared repo.
4878
5803
 
4879
5804
  const lease = claimFiles(db, taskId, worktree, files, opts.agent);
4880
5805
  console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
5806
+ pushSyncEvent('claim_added', {
5807
+ task_id: taskId,
5808
+ lease_id: lease.id,
5809
+ file_count: files.length,
5810
+ files: files.slice(0, 10),
5811
+ }, { worktree }).catch(() => {});
4881
5812
  files.forEach(f => console.log(` ${chalk.dim(f)}`));
4882
5813
  } catch (err) {
4883
5814
  printErrorWithNext(err.message, 'switchman task list --status in_progress');
@@ -4893,9 +5824,11 @@ program
4893
5824
  .action((taskId) => {
4894
5825
  const repoRoot = getRepo();
4895
5826
  const db = getDb(repoRoot);
5827
+ const task = getTask(db, taskId);
4896
5828
  releaseFileClaims(db, taskId);
4897
5829
  db.close();
4898
5830
  console.log(`${chalk.green('✓')} Released all claims for task ${chalk.cyan(taskId)}`);
5831
+ pushSyncEvent('claim_released', { task_id: taskId }, { worktree: task?.worktree || null }).catch(() => {});
4899
5832
  });
4900
5833
 
4901
5834
  program
@@ -5171,12 +6104,14 @@ program
5171
6104
  .description('Show one dashboard view of what is running, blocked, and ready next')
5172
6105
  .option('--json', 'Output raw JSON')
5173
6106
  .option('--watch', 'Keep refreshing status in the terminal')
6107
+ .option('--repair', 'Repair safe interrupted queue and pipeline state before rendering status')
5174
6108
  .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
5175
6109
  .option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
5176
6110
  .addHelpText('after', `
5177
6111
  Examples:
5178
6112
  switchman status
5179
6113
  switchman status --watch
6114
+ switchman status --repair
5180
6115
  switchman status --json
5181
6116
 
5182
6117
  Use this first when the repo feels stuck.
@@ -5194,13 +6129,40 @@ Use this first when the repo feels stuck.
5194
6129
  console.clear();
5195
6130
  }
5196
6131
 
6132
+ let repairResult = null;
6133
+ if (opts.repair) {
6134
+ const repairDb = getDb(repoRoot);
6135
+ try {
6136
+ repairResult = repairRepoState(repairDb, repoRoot);
6137
+ } finally {
6138
+ repairDb.close();
6139
+ }
6140
+ }
6141
+
5197
6142
  const report = await collectStatusSnapshot(repoRoot);
6143
+ const [teamActivity, teamState] = await Promise.all([
6144
+ pullActiveTeamMembers(),
6145
+ pullTeamState(),
6146
+ ]);
6147
+ const myUserId = readCredentials()?.user_id;
6148
+ const otherMembers = teamActivity.filter(e => e.user_id !== myUserId);
6149
+ const teamSummary = summarizeTeamCoordinationState(teamState, myUserId);
5198
6150
  cycles += 1;
5199
6151
 
5200
6152
  if (opts.json) {
5201
- console.log(JSON.stringify(watch ? { ...report, watch: true, cycles } : report, null, 2));
6153
+ const payload = watch ? { ...report, watch: true, cycles } : report;
6154
+ const withTeam = { ...payload, team_sync: { summary: teamSummary, recent_events: teamState.filter((event) => event.user_id !== myUserId).slice(0, 25) } };
6155
+ console.log(JSON.stringify(opts.repair ? { ...withTeam, repair: repairResult } : withTeam, null, 2));
5202
6156
  } else {
5203
- renderUnifiedStatusReport(report);
6157
+ if (opts.repair && repairResult) {
6158
+ printRepairSummary(repairResult, {
6159
+ repairedHeading: `${chalk.green('✓')} Repaired safe interrupted repo state before rendering status`,
6160
+ noRepairHeading: `${chalk.green('✓')} No repo repair action needed before rendering status`,
6161
+ limit: 6,
6162
+ });
6163
+ console.log('');
6164
+ }
6165
+ renderUnifiedStatusReport(report, { teamActivity: otherMembers, teamSummary });
5204
6166
  if (watch) {
5205
6167
  const signature = buildWatchSignature(report);
5206
6168
  const watchState = lastSignature === null
@@ -5267,9 +6229,11 @@ program
5267
6229
  }
5268
6230
  });
5269
6231
 
5270
- program
6232
+ const doctorCmd = program
5271
6233
  .command('doctor')
5272
- .description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
6234
+ .description('Show one operator-focused health view: what is running, what is blocked, and what to do next');
6235
+ doctorCmd._switchmanAdvanced = true;
6236
+ doctorCmd
5273
6237
  .option('--repair', 'Repair safe interrupted queue and pipeline state before reporting health')
5274
6238
  .option('--json', 'Output raw JSON')
5275
6239
  .addHelpText('after', `
@@ -5407,6 +6371,7 @@ Examples:
5407
6371
  `);
5408
6372
 
5409
6373
  const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
6374
+ auditCmd._switchmanAdvanced = true;
5410
6375
 
5411
6376
  auditCmd
5412
6377
  .command('change <pipelineId>')
@@ -5729,6 +6694,7 @@ gateCmd
5729
6694
  const semanticCmd = program
5730
6695
  .command('semantic')
5731
6696
  .description('Inspect or materialize the derived semantic code-object view');
6697
+ semanticCmd._switchmanAdvanced = true;
5732
6698
 
5733
6699
  semanticCmd
5734
6700
  .command('materialize')
@@ -5745,6 +6711,7 @@ semanticCmd
5745
6711
  const objectCmd = program
5746
6712
  .command('object')
5747
6713
  .description('Experimental object-source mode backed by canonical exported code objects');
6714
+ objectCmd._switchmanAdvanced = true;
5748
6715
 
5749
6716
  objectCmd
5750
6717
  .command('import')
@@ -5826,6 +6793,7 @@ objectCmd
5826
6793
  // ── monitor ──────────────────────────────────────────────────────────────────
5827
6794
 
5828
6795
  const monitorCmd = program.command('monitor').description('Observe workspaces for runtime file changes');
6796
+ monitorCmd._switchmanAdvanced = true;
5829
6797
 
5830
6798
  monitorCmd
5831
6799
  .command('once')
@@ -5835,7 +6803,7 @@ monitorCmd
5835
6803
  .action((opts) => {
5836
6804
  const repoRoot = getRepo();
5837
6805
  const db = getDb(repoRoot);
5838
- const worktrees = listGitWorktrees(repoRoot);
6806
+ const worktrees = resolveMonitoredWorktrees(db, repoRoot);
5839
6807
  const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
5840
6808
  db.close();
5841
6809
 
@@ -5851,9 +6819,7 @@ monitorCmd
5851
6819
 
5852
6820
  console.log(`${chalk.green('✓')} Observed ${result.summary.total} file change(s)`);
5853
6821
  for (const event of result.events) {
5854
- const badge = event.status === 'allowed' ? chalk.green('ALLOWED') : chalk.red('DENIED ');
5855
- const action = event.enforcement_action ? ` ${chalk.dim(event.enforcement_action)}` : '';
5856
- console.log(` ${badge} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${event.reason_code ? ` ${chalk.dim(event.reason_code)}` : ''}${action}`);
6822
+ renderMonitorEvent(event);
5857
6823
  }
5858
6824
  });
5859
6825
 
@@ -5887,14 +6853,12 @@ monitorCmd
5887
6853
 
5888
6854
  while (!stopped) {
5889
6855
  const db = getDb(repoRoot);
5890
- const worktrees = listGitWorktrees(repoRoot);
6856
+ const worktrees = resolveMonitoredWorktrees(db, repoRoot);
5891
6857
  const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
5892
6858
  db.close();
5893
6859
 
5894
6860
  for (const event of result.events) {
5895
- const badge = event.status === 'allowed' ? chalk.green('ALLOWED') : chalk.red('DENIED ');
5896
- const action = event.enforcement_action ? ` ${chalk.dim(event.enforcement_action)}` : '';
5897
- console.log(` ${badge} ${chalk.cyan(event.worktree)} ${chalk.yellow(event.file_path)} ${chalk.dim(event.change_type)}${event.reason_code ? ` ${chalk.dim(event.reason_code)}` : ''}${action}`);
6861
+ renderMonitorEvent(event);
5898
6862
  }
5899
6863
 
5900
6864
  if (stopped) break;
@@ -5912,39 +6876,18 @@ monitorCmd
5912
6876
  .action((opts) => {
5913
6877
  const repoRoot = getRepo();
5914
6878
  const intervalMs = Number.parseInt(opts.intervalMs, 10);
5915
- const existingState = readMonitorState(repoRoot);
6879
+ const state = startBackgroundMonitor(repoRoot, {
6880
+ intervalMs,
6881
+ quarantine: Boolean(opts.quarantine),
6882
+ });
5916
6883
 
5917
- if (existingState && isProcessRunning(existingState.pid)) {
5918
- console.log(chalk.yellow(`Monitor already running with pid ${existingState.pid}`));
6884
+ if (state.already_running) {
6885
+ console.log(chalk.yellow(`Monitor already running with pid ${state.state.pid}`));
5919
6886
  return;
5920
6887
  }
5921
6888
 
5922
- const logPath = join(repoRoot, '.switchman', 'monitor.log');
5923
- const child = spawn(process.execPath, [
5924
- process.argv[1],
5925
- 'monitor',
5926
- 'watch',
5927
- '--interval-ms',
5928
- String(intervalMs),
5929
- ...(opts.quarantine ? ['--quarantine'] : []),
5930
- '--daemonized',
5931
- ], {
5932
- cwd: repoRoot,
5933
- detached: true,
5934
- stdio: 'ignore',
5935
- });
5936
- child.unref();
5937
-
5938
- const statePath = writeMonitorState(repoRoot, {
5939
- pid: child.pid,
5940
- interval_ms: intervalMs,
5941
- quarantine: Boolean(opts.quarantine),
5942
- log_path: logPath,
5943
- started_at: new Date().toISOString(),
5944
- });
5945
-
5946
- console.log(`${chalk.green('✓')} Started monitor pid ${chalk.cyan(String(child.pid))}`);
5947
- console.log(`${chalk.dim('State:')} ${statePath}`);
6889
+ console.log(`${chalk.green('✓')} Started monitor pid ${chalk.cyan(String(state.state.pid))}`);
6890
+ console.log(`${chalk.dim('State:')} ${state.state_path}`);
5948
6891
  });
5949
6892
 
5950
6893
  monitorCmd
@@ -5996,9 +6939,38 @@ monitorCmd
5996
6939
  console.log(` ${chalk.dim('started_at')} ${state.started_at}`);
5997
6940
  });
5998
6941
 
6942
+ program
6943
+ .command('watch')
6944
+ .description('Watch worktrees for direct writes and rogue edits in real time')
6945
+ .option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
6946
+ .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
6947
+ .action(async (opts) => {
6948
+ const repoRoot = getRepo();
6949
+ const child = spawn(process.execPath, [
6950
+ process.argv[1],
6951
+ 'monitor',
6952
+ 'watch',
6953
+ '--interval-ms',
6954
+ String(opts.intervalMs || '2000'),
6955
+ ...(opts.quarantine ? ['--quarantine'] : []),
6956
+ ], {
6957
+ cwd: repoRoot,
6958
+ stdio: 'inherit',
6959
+ });
6960
+
6961
+ await new Promise((resolve, reject) => {
6962
+ child.on('exit', (code) => {
6963
+ process.exitCode = code ?? 0;
6964
+ resolve();
6965
+ });
6966
+ child.on('error', reject);
6967
+ });
6968
+ });
6969
+
5999
6970
  // ── policy ───────────────────────────────────────────────────────────────────
6000
6971
 
6001
6972
  const policyCmd = program.command('policy').description('Manage enforcement and change-governance policy');
6973
+ policyCmd._switchmanAdvanced = true;
6002
6974
 
6003
6975
  policyCmd
6004
6976
  .command('init')
@@ -6144,4 +7116,372 @@ policyCmd
6144
7116
  }
6145
7117
  });
6146
7118
 
7119
+
7120
+
7121
+ // ── login ──────────────────────────────────────────────────────────────────────
7122
+
7123
+ program
7124
+ .command('login')
7125
+ .description('Sign in with GitHub to activate Switchman Pro')
7126
+ .option('--invite <token>', 'Join a team with an invite token')
7127
+ .option('--status', 'Show current login status')
7128
+ .addHelpText('after', `
7129
+ Examples:
7130
+ switchman login
7131
+ switchman login --status
7132
+ switchman login --invite tk_8f3a2c
7133
+ `)
7134
+ .action(async (opts) => {
7135
+ // Show current status
7136
+ if (opts.status) {
7137
+ const creds = readCredentials();
7138
+ if (!creds?.access_token) {
7139
+ console.log('');
7140
+ console.log(` ${chalk.dim('Status:')} Not logged in`);
7141
+ console.log(` ${chalk.dim('Run: ')} ${chalk.cyan('switchman login')}`);
7142
+ console.log('');
7143
+ return;
7144
+ }
7145
+
7146
+ const licence = await checkLicence();
7147
+ console.log('');
7148
+ if (licence.valid) {
7149
+ console.log(` ${chalk.green('✓')} Logged in as ${chalk.cyan(creds.email ?? 'unknown')}`);
7150
+ console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
7151
+ if (licence.current_period_end) {
7152
+ console.log(` ${chalk.dim('Renews:')} ${new Date(licence.current_period_end).toLocaleDateString()}`);
7153
+ }
7154
+ if (licence.offline) {
7155
+ console.log(` ${chalk.dim('(offline cache)')}`);
7156
+ }
7157
+ } else {
7158
+ console.log(` ${chalk.yellow('⚠')} Logged in as ${chalk.cyan(creds.email ?? 'unknown')} but no active Pro licence`);
7159
+ console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
7160
+ }
7161
+ console.log('');
7162
+ return;
7163
+ }
7164
+
7165
+ // Handle --invite token
7166
+ if (opts.invite) {
7167
+ const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
7168
+ ?? 'https://afilbolhlkiingnsupgr.supabase.co';
7169
+ const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
7170
+ ?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
7171
+
7172
+ const creds = readCredentials();
7173
+ if (!creds?.access_token) {
7174
+ console.log('');
7175
+ console.log(chalk.yellow(' You need to sign in first before accepting an invite.'));
7176
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman login')} ${chalk.dim('then try again with --invite')}`);
7177
+ console.log('');
7178
+ process.exit(1);
7179
+ }
7180
+
7181
+ const { createClient } = await import('@supabase/supabase-js');
7182
+ const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
7183
+ global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
7184
+ });
7185
+
7186
+ const { data: { user } } = await sb.auth.getUser();
7187
+ if (!user) {
7188
+ console.log(chalk.red(' ✗ Could not verify your account. Run: switchman login'));
7189
+ process.exit(1);
7190
+ }
7191
+
7192
+ // Look up the invite
7193
+ const { data: invite, error: inviteError } = await sb
7194
+ .from('team_invites')
7195
+ .select('id, team_id, email, accepted')
7196
+ .eq('token', opts.invite)
7197
+ .maybeSingle();
7198
+
7199
+ if (inviteError || !invite) {
7200
+ console.log('');
7201
+ console.log(chalk.red(' ✗ Invite token not found or already used.'));
7202
+ console.log(` ${chalk.dim('Ask your teammate to send a new invite.')}`);
7203
+ console.log('');
7204
+ process.exit(1);
7205
+ }
7206
+
7207
+ if (invite.accepted) {
7208
+ console.log('');
7209
+ console.log(chalk.yellow(' ⚠ This invite has already been accepted.'));
7210
+ console.log('');
7211
+ process.exit(1);
7212
+ }
7213
+
7214
+ // Add user to the team
7215
+ const { error: memberError } = await sb
7216
+ .from('team_members')
7217
+ .insert({ team_id: invite.team_id, user_id: user.id, role: 'member' });
7218
+
7219
+ if (memberError && !memberError.message.includes('duplicate')) {
7220
+ console.log(chalk.red(` ✗ Could not join team: ${memberError.message}`));
7221
+ process.exit(1);
7222
+ }
7223
+
7224
+ // Mark invite as accepted
7225
+ await sb
7226
+ .from('team_invites')
7227
+ .update({ accepted: true })
7228
+ .eq('id', invite.id);
7229
+
7230
+ console.log('');
7231
+ console.log(` ${chalk.green('✓')} Joined the team successfully`);
7232
+ console.log(` ${chalk.dim('Your agents now share coordination with your teammates.')}`);
7233
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman status')} ${chalk.dim('to see the shared view.')}`);
7234
+ console.log('');
7235
+ return;
7236
+ }
7237
+
7238
+ // Already logged in?
7239
+ const existing = readCredentials();
7240
+ if (existing?.access_token) {
7241
+ const licence = await checkLicence();
7242
+ if (licence.valid) {
7243
+ console.log('');
7244
+ console.log(` ${chalk.green('✓')} Already logged in as ${chalk.cyan(existing.email ?? 'unknown')}`);
7245
+ console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
7246
+ console.log(` ${chalk.dim('Run')} ${chalk.cyan('switchman login --status')} ${chalk.dim('to see full details')}`);
7247
+ console.log('');
7248
+ return;
7249
+ }
7250
+ }
7251
+
7252
+ console.log('');
7253
+ console.log(chalk.bold(' Switchman Pro — sign in with GitHub'));
7254
+ console.log('');
7255
+
7256
+ const spinner = ora('Waiting for GitHub sign-in...').start();
7257
+ spinner.stop();
7258
+
7259
+ const result = await loginWithGitHub();
7260
+
7261
+ if (!result.success) {
7262
+ console.log(` ${chalk.red('✗')} Sign in failed: ${result.error ?? 'unknown error'}`);
7263
+ console.log(` ${chalk.dim('Try again or visit:')} ${chalk.cyan(PRO_PAGE_URL)}`);
7264
+ console.log('');
7265
+ process.exit(1);
7266
+ }
7267
+
7268
+ // Verify the licence
7269
+ const licence = await checkLicence();
7270
+
7271
+ console.log(` ${chalk.green('✓')} Signed in as ${chalk.cyan(result.email ?? 'unknown')}`);
7272
+
7273
+ if (licence.valid) {
7274
+ console.log(` ${chalk.green('✓')} Switchman Pro active`);
7275
+ console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
7276
+ console.log('');
7277
+ console.log(` ${chalk.dim('Credentials saved · valid 24h · 7-day offline grace')}`);
7278
+ console.log('');
7279
+ console.log(` Run ${chalk.cyan('switchman setup --agents 10')} to use unlimited agents.`);
7280
+ } else {
7281
+ console.log(` ${chalk.yellow('⚠')} No active Pro licence found`);
7282
+ console.log('');
7283
+ console.log(` If you just paid, it may take a moment to activate.`);
7284
+ console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
7285
+ }
7286
+
7287
+ console.log('');
7288
+ });
7289
+
7290
+
7291
+ // ── logout ─────────────────────────────────────────────────────────────────────
7292
+
7293
+ program
7294
+ .command('logout')
7295
+ .description('Sign out and remove saved credentials')
7296
+ .action(() => {
7297
+ clearCredentials();
7298
+ console.log('');
7299
+ console.log(` ${chalk.green('✓')} Signed out — credentials removed`);
7300
+ console.log('');
7301
+ });
7302
+
7303
+
7304
+ // ── upgrade ────────────────────────────────────────────────────────────────────
7305
+
7306
+ program
7307
+ .command('upgrade')
7308
+ .description('Open the Switchman Pro page in your browser')
7309
+ .action(async () => {
7310
+ console.log('');
7311
+ console.log(` Opening ${chalk.cyan(PRO_PAGE_URL)}...`);
7312
+ console.log('');
7313
+ const { default: open } = await import('open');
7314
+ await open(PRO_PAGE_URL);
7315
+ });
7316
+
7317
+ // ── team ───────────────────────────────────────────────────────────────────────
7318
+
7319
+ const teamCmd = program
7320
+ .command('team')
7321
+ .description('Manage your Switchman Pro team');
7322
+
7323
+ teamCmd
7324
+ .command('invite <email>')
7325
+ .description('Invite a teammate to your shared coordination')
7326
+ .addHelpText('after', `
7327
+ Examples:
7328
+ switchman team invite alice@example.com
7329
+ `)
7330
+ .action(async (email) => {
7331
+ const licence = await checkLicence();
7332
+ if (!licence.valid) {
7333
+ console.log('');
7334
+ console.log(chalk.yellow(' ⚠ Team invites require Switchman Pro.'));
7335
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
7336
+ console.log('');
7337
+ process.exit(1);
7338
+ }
7339
+
7340
+ const repoRoot = getRepo();
7341
+ const creds = readCredentials();
7342
+ if (!creds?.access_token) {
7343
+ console.log('');
7344
+ console.log(chalk.yellow(' ⚠ You need to be logged in.'));
7345
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman login')}`);
7346
+ console.log('');
7347
+ process.exit(1);
7348
+ }
7349
+
7350
+ const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
7351
+ ?? 'https://afilbolhlkiingnsupgr.supabase.co';
7352
+ const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
7353
+ ?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
7354
+
7355
+ const { createClient } = await import('@supabase/supabase-js');
7356
+ const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
7357
+ global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
7358
+ });
7359
+
7360
+ const { data: { user } } = await sb.auth.getUser();
7361
+ if (!user) {
7362
+ console.log(chalk.red(' ✗ Could not verify your account. Run: switchman login'));
7363
+ process.exit(1);
7364
+ }
7365
+
7366
+ // Get or create team
7367
+ let teamId;
7368
+ const { data: membership } = await sb
7369
+ .from('team_members')
7370
+ .select('team_id')
7371
+ .eq('user_id', user.id)
7372
+ .maybeSingle();
7373
+
7374
+ if (membership?.team_id) {
7375
+ teamId = membership.team_id;
7376
+ } else {
7377
+ // Create a new team
7378
+ const { data: team, error: teamError } = await sb
7379
+ .from('teams')
7380
+ .insert({ owner_id: user.id, name: 'My Team' })
7381
+ .select('id')
7382
+ .single();
7383
+
7384
+ if (teamError) {
7385
+ console.log(chalk.red(` ✗ Could not create team: ${teamError.message}`));
7386
+ process.exit(1);
7387
+ }
7388
+
7389
+ teamId = team.id;
7390
+
7391
+ // Add the owner as a member
7392
+ await sb.from('team_members').insert({
7393
+ team_id: teamId,
7394
+ user_id: user.id,
7395
+ role: 'owner',
7396
+ });
7397
+ }
7398
+
7399
+ // Create the invite
7400
+ const { data: invite, error: inviteError } = await sb
7401
+ .from('team_invites')
7402
+ .insert({
7403
+ team_id: teamId,
7404
+ invited_by: user.id,
7405
+ email,
7406
+ })
7407
+ .select('token')
7408
+ .single();
7409
+
7410
+ if (inviteError) {
7411
+ console.log(chalk.red(` ✗ Could not create invite: ${inviteError.message}`));
7412
+ process.exit(1);
7413
+ }
7414
+
7415
+ console.log('');
7416
+ console.log(` ${chalk.green('✓')} Invite created for ${chalk.cyan(email)}`);
7417
+ console.log('');
7418
+ console.log(` They can join with:`);
7419
+ console.log(` ${chalk.cyan(`switchman login --invite ${invite.token}`)}`);
7420
+ console.log('');
7421
+ });
7422
+
7423
+ teamCmd
7424
+ .command('list')
7425
+ .description('List your team members')
7426
+ .action(async () => {
7427
+ const licence = await checkLicence();
7428
+ if (!licence.valid) {
7429
+ console.log('');
7430
+ console.log(chalk.yellow(' ⚠ Team features require Switchman Pro.'));
7431
+ console.log(` ${chalk.dim('Run:')} ${chalk.cyan('switchman upgrade')}`);
7432
+ console.log('');
7433
+ process.exit(1);
7434
+ }
7435
+
7436
+ const creds = readCredentials();
7437
+ if (!creds?.access_token) {
7438
+ console.log(chalk.red(' ✗ Not logged in. Run: switchman login'));
7439
+ process.exit(1);
7440
+ }
7441
+
7442
+ const SUPABASE_URL = process.env.SWITCHMAN_SUPABASE_URL
7443
+ ?? 'https://afilbolhlkiingnsupgr.supabase.co';
7444
+ const SUPABASE_ANON = process.env.SWITCHMAN_SUPABASE_ANON
7445
+ ?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmaWxib2xobGtpaW5nbnN1cGdyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM1OTIzOTIsImV4cCI6MjA4OTE2ODM5Mn0.8TBfHfRB0vEyKPMWBd6i1DNwx1nS9UqprIAsJf35n88';
7446
+
7447
+ const { createClient } = await import('@supabase/supabase-js');
7448
+ const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
7449
+ global: { headers: { Authorization: `Bearer ${creds.access_token}` } }
7450
+ });
7451
+
7452
+ const { data: membership } = await sb
7453
+ .from('team_members')
7454
+ .select('team_id')
7455
+ .eq('user_id', (await sb.auth.getUser()).data.user?.id)
7456
+ .maybeSingle();
7457
+
7458
+ if (!membership?.team_id) {
7459
+ console.log('');
7460
+ console.log(` ${chalk.dim('No team yet. Invite someone with:')} ${chalk.cyan('switchman team invite <email>')}`);
7461
+ console.log('');
7462
+ return;
7463
+ }
7464
+
7465
+ const { data: members } = await sb
7466
+ .from('team_members')
7467
+ .select('user_id, role, joined_at')
7468
+ .eq('team_id', membership.team_id);
7469
+
7470
+ const { data: invites } = await sb
7471
+ .from('team_invites')
7472
+ .select('email, token, accepted, created_at')
7473
+ .eq('team_id', membership.team_id)
7474
+ .eq('accepted', false);
7475
+
7476
+ console.log('');
7477
+ for (const m of members ?? []) {
7478
+ const roleLabel = m.role === 'owner' ? chalk.dim('(owner)') : chalk.dim('(member)');
7479
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(m.user_id.slice(0, 8))} ${roleLabel}`);
7480
+ }
7481
+ for (const inv of invites ?? []) {
7482
+ console.log(` ${chalk.dim('○')} ${chalk.cyan(inv.email)} ${chalk.dim('(invited)')}`);
7483
+ }
7484
+ console.log('');
7485
+ });
7486
+
6147
7487
  program.parse();