switchman-dev 0.1.1 → 0.1.2
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/README.md +27 -10
- package/package.json +1 -1
- package/src/cli/index.js +227 -28
- package/src/core/db.js +452 -68
- package/src/core/git.js +8 -1
- package/src/mcp/server.js +170 -22
package/README.md
CHANGED
|
@@ -107,10 +107,11 @@ Paste this into your agent at the start of each session:
|
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
Before starting any work:
|
|
110
|
-
1. Run `switchman
|
|
110
|
+
1. Run `switchman lease next --json` to get your assigned task and lease
|
|
111
111
|
2. Run `switchman claim <taskId> <worktreeName> <files...>` to lock the files you'll edit
|
|
112
112
|
- If a file is already claimed, pick a different approach or different files
|
|
113
|
-
3.
|
|
113
|
+
3. If the task runs for a while, refresh the lease with `switchman lease heartbeat <leaseId>`
|
|
114
|
+
4. When finished, run `switchman task done <taskId>`
|
|
114
115
|
|
|
115
116
|
Never edit a file you haven't claimed. If a claim fails, do not use --force.
|
|
116
117
|
```
|
|
@@ -129,16 +130,16 @@ Here's what a normal session looks like with Switchman running:
|
|
|
129
130
|
|
|
130
131
|
```
|
|
131
132
|
# Agent 1 picks up a task
|
|
132
|
-
switchman
|
|
133
|
-
✓
|
|
133
|
+
switchman lease next
|
|
134
|
+
✓ Lease acquired: "Add rate limiting to all routes" [task-abc-123 / lease-xyz-123]
|
|
134
135
|
|
|
135
136
|
# Agent 1 locks its files
|
|
136
137
|
switchman claim task-abc-123 agent1 src/middleware/auth.js src/server.js
|
|
137
138
|
✓ 2 files locked — no conflicts
|
|
138
139
|
|
|
139
140
|
# Agent 2 picks up a different task
|
|
140
|
-
switchman
|
|
141
|
-
✓
|
|
141
|
+
switchman lease next
|
|
142
|
+
✓ Lease acquired: "Add validation to POST /tasks" [task-def-456 / lease-xyz-456]
|
|
142
143
|
|
|
143
144
|
# Agent 2 tries to claim a file already locked by Agent 1
|
|
144
145
|
switchman claim task-def-456 agent2 src/middleware/auth.js
|
|
@@ -175,10 +176,25 @@ Add a task to the queue.
|
|
|
175
176
|
List all tasks. Filter with `--status pending|in_progress|done|failed`.
|
|
176
177
|
|
|
177
178
|
### `switchman task next`
|
|
178
|
-
Get and assign the next pending task. Use `--json` for agent automation.
|
|
179
|
+
Get and assign the next pending task. This is a compatibility shim over the lease workflow. Use `--json` for agent automation.
|
|
179
180
|
- `--worktree <name>` — worktree to assign to (defaults to current folder name)
|
|
180
181
|
- `--agent <name>` — agent identifier for logging
|
|
181
182
|
|
|
183
|
+
### `switchman lease next`
|
|
184
|
+
Acquire the next pending task as a first-class lease. Use `--json` for agent automation.
|
|
185
|
+
- `--worktree <name>` — worktree to assign to (defaults to current folder name)
|
|
186
|
+
- `--agent <name>` — agent identifier for logging
|
|
187
|
+
|
|
188
|
+
### `switchman lease list`
|
|
189
|
+
List active and historical leases. Filter with `--status active|completed|failed|expired`.
|
|
190
|
+
|
|
191
|
+
### `switchman lease heartbeat <leaseId>`
|
|
192
|
+
Refresh the heartbeat timestamp for a long-running lease so it does not get treated as stale.
|
|
193
|
+
|
|
194
|
+
### `switchman lease reap`
|
|
195
|
+
Expire stale leases, release their claims, and return their tasks to `pending`.
|
|
196
|
+
- `--stale-after-minutes <n>` — staleness threshold (default: 15)
|
|
197
|
+
|
|
182
198
|
### `switchman task done <taskId>`
|
|
183
199
|
Mark a task complete and release all file claims.
|
|
184
200
|
|
|
@@ -195,7 +211,7 @@ Release all file claims for a task.
|
|
|
195
211
|
Check all worktrees for conflicts — both uncommitted file overlaps and branch-level merge conflicts. Run this before merging.
|
|
196
212
|
|
|
197
213
|
### `switchman status`
|
|
198
|
-
Full overview: task counts, active
|
|
214
|
+
Full overview: task counts, active leases, stale leases, locked files, and a quick conflict scan.
|
|
199
215
|
|
|
200
216
|
### `switchman worktree list`
|
|
201
217
|
List all git worktrees with their registered agents and status.
|
|
@@ -214,6 +230,7 @@ Re-sync git worktrees into the Switchman database (useful if you add worktrees a
|
|
|
214
230
|
| `switchman_task_claim` | Claim files before editing (conflict check) |
|
|
215
231
|
| `switchman_task_done` | Mark task complete, release file claims |
|
|
216
232
|
| `switchman_task_fail` | Mark task failed, release file claims |
|
|
233
|
+
| `switchman_lease_heartbeat` | Refresh a long-running lease heartbeat |
|
|
217
234
|
| `switchman_scan` | Scan all worktrees for conflicts |
|
|
218
235
|
| `switchman_status` | Full system overview |
|
|
219
236
|
|
|
@@ -222,7 +239,7 @@ Re-sync git worktrees into the Switchman database (useful if you add worktrees a
|
|
|
222
239
|
## Roadmap
|
|
223
240
|
|
|
224
241
|
- [ ] Merge queue — serialize worktree→main merges with auto-retry
|
|
225
|
-
- [ ]
|
|
242
|
+
- [ ] Automatic stale-lease policies — configurable heartbeat/reap behaviour
|
|
226
243
|
- [ ] Cursor and Windsurf native MCP integration
|
|
227
244
|
- [ ] Web dashboard
|
|
228
245
|
- [ ] `brew install switchman`
|
|
@@ -240,4 +257,4 @@ Building this in public — if you're running parallel agents and hit something
|
|
|
240
257
|
|
|
241
258
|
## License
|
|
242
259
|
|
|
243
|
-
MIT — free to use, modify, and distribute.
|
|
260
|
+
MIT — free to use, modify, and distribute.
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -27,10 +27,11 @@ import { execSync } from 'child_process';
|
|
|
27
27
|
import { findRepoRoot, listGitWorktrees, createGitWorktree } from '../core/git.js';
|
|
28
28
|
import {
|
|
29
29
|
initDb, openDb,
|
|
30
|
-
|
|
30
|
+
DEFAULT_STALE_LEASE_MINUTES,
|
|
31
|
+
createTask, startTaskLease, completeTask, failTask, listTasks, getTask, getNextPendingTask,
|
|
32
|
+
listLeases, heartbeatLease, getStaleLeases, reapStaleLeases,
|
|
31
33
|
registerWorktree, listWorktrees,
|
|
32
34
|
claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts,
|
|
33
|
-
logConflict,
|
|
34
35
|
} from '../core/db.js';
|
|
35
36
|
import { scanAllWorktrees } from '../core/detector.js';
|
|
36
37
|
|
|
@@ -60,14 +61,34 @@ function statusBadge(status) {
|
|
|
60
61
|
const colors = {
|
|
61
62
|
pending: chalk.yellow,
|
|
62
63
|
in_progress: chalk.blue,
|
|
64
|
+
active: chalk.blue,
|
|
65
|
+
completed: chalk.green,
|
|
63
66
|
done: chalk.green,
|
|
64
67
|
failed: chalk.red,
|
|
68
|
+
expired: chalk.red,
|
|
65
69
|
idle: chalk.gray,
|
|
66
70
|
busy: chalk.blue,
|
|
67
71
|
};
|
|
68
72
|
return (colors[status] || chalk.white)(status.toUpperCase().padEnd(11));
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function getCurrentWorktreeName(explicitWorktree) {
|
|
76
|
+
return explicitWorktree || process.cwd().split('/').pop();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function taskJsonWithLease(task, worktree, lease) {
|
|
80
|
+
return {
|
|
81
|
+
task: {
|
|
82
|
+
...task,
|
|
83
|
+
worktree,
|
|
84
|
+
status: 'in_progress',
|
|
85
|
+
lease_id: lease?.id ?? null,
|
|
86
|
+
lease_status: lease?.status ?? null,
|
|
87
|
+
heartbeat_at: lease?.heartbeat_at ?? null,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function printTable(rows, columns) {
|
|
72
93
|
if (!rows.length) return;
|
|
73
94
|
const widths = columns.map(col =>
|
|
@@ -263,15 +284,15 @@ taskCmd
|
|
|
263
284
|
|
|
264
285
|
taskCmd
|
|
265
286
|
.command('assign <taskId> <worktree>')
|
|
266
|
-
.description('Assign a task to a worktree')
|
|
287
|
+
.description('Assign a task to a worktree (compatibility shim for lease acquire)')
|
|
267
288
|
.option('--agent <name>', 'Agent name (e.g. claude-code)')
|
|
268
289
|
.action((taskId, worktree, opts) => {
|
|
269
290
|
const repoRoot = getRepo();
|
|
270
291
|
const db = getDb(repoRoot);
|
|
271
|
-
const
|
|
292
|
+
const lease = startTaskLease(db, taskId, worktree, opts.agent);
|
|
272
293
|
db.close();
|
|
273
|
-
if (
|
|
274
|
-
console.log(`${chalk.green('✓')} Assigned ${chalk.cyan(taskId)} → ${chalk.cyan(worktree)}`);
|
|
294
|
+
if (lease) {
|
|
295
|
+
console.log(`${chalk.green('✓')} Assigned ${chalk.cyan(taskId)} → ${chalk.cyan(worktree)} (${chalk.dim(lease.id)})`);
|
|
275
296
|
} else {
|
|
276
297
|
console.log(chalk.red(`Could not assign task. It may not exist or is not in 'pending' status.`));
|
|
277
298
|
}
|
|
@@ -303,7 +324,7 @@ taskCmd
|
|
|
303
324
|
|
|
304
325
|
taskCmd
|
|
305
326
|
.command('next')
|
|
306
|
-
.description('Get and assign the next pending task (for
|
|
327
|
+
.description('Get and assign the next pending task (compatibility shim for lease next)')
|
|
307
328
|
.option('--json', 'Output as JSON')
|
|
308
329
|
.option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
|
|
309
330
|
.option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
|
|
@@ -320,11 +341,11 @@ taskCmd
|
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
// Determine worktree name: explicit flag, or derive from cwd
|
|
323
|
-
const worktreeName = opts.worktree
|
|
324
|
-
const
|
|
344
|
+
const worktreeName = getCurrentWorktreeName(opts.worktree);
|
|
345
|
+
const lease = startTaskLease(db, task.id, worktreeName, opts.agent || null);
|
|
325
346
|
db.close();
|
|
326
347
|
|
|
327
|
-
if (!
|
|
348
|
+
if (!lease) {
|
|
328
349
|
// Race condition: another agent grabbed it between get and assign
|
|
329
350
|
if (opts.json) console.log(JSON.stringify({ task: null, message: 'Task claimed by another agent — try again' }));
|
|
330
351
|
else console.log(chalk.yellow('Task was just claimed by another agent. Run again to get the next one.'));
|
|
@@ -332,10 +353,168 @@ taskCmd
|
|
|
332
353
|
}
|
|
333
354
|
|
|
334
355
|
if (opts.json) {
|
|
335
|
-
console.log(JSON.stringify(
|
|
356
|
+
console.log(JSON.stringify(taskJsonWithLease(task, worktreeName, lease), null, 2));
|
|
336
357
|
} else {
|
|
337
358
|
console.log(`${chalk.green('✓')} Assigned: ${chalk.bold(task.title)}`);
|
|
338
|
-
console.log(` ${chalk.dim('id:')} ${task.id} ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
359
|
+
console.log(` ${chalk.dim('id:')} ${task.id} ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('lease:')} ${chalk.dim(lease.id)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ── lease ────────────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
const leaseCmd = program.command('lease').description('Manage active work leases');
|
|
366
|
+
|
|
367
|
+
leaseCmd
|
|
368
|
+
.command('acquire <taskId> <worktree>')
|
|
369
|
+
.description('Acquire a lease for a pending task')
|
|
370
|
+
.option('--agent <name>', 'Agent identifier for logging')
|
|
371
|
+
.option('--json', 'Output as JSON')
|
|
372
|
+
.action((taskId, worktree, opts) => {
|
|
373
|
+
const repoRoot = getRepo();
|
|
374
|
+
const db = getDb(repoRoot);
|
|
375
|
+
const task = getTask(db, taskId);
|
|
376
|
+
const lease = startTaskLease(db, taskId, worktree, opts.agent || null);
|
|
377
|
+
db.close();
|
|
378
|
+
|
|
379
|
+
if (!lease || !task) {
|
|
380
|
+
if (opts.json) console.log(JSON.stringify({ lease: null, task: null }));
|
|
381
|
+
else console.log(chalk.red(`Could not acquire lease. The task may not exist or is not pending.`));
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (opts.json) {
|
|
387
|
+
console.log(JSON.stringify({
|
|
388
|
+
lease,
|
|
389
|
+
task: taskJsonWithLease(task, worktree, lease).task,
|
|
390
|
+
}, null, 2));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(`${chalk.green('✓')} Lease acquired ${chalk.dim(lease.id)}`);
|
|
395
|
+
console.log(` ${chalk.dim('task:')} ${chalk.bold(task.title)}`);
|
|
396
|
+
console.log(` ${chalk.dim('worktree:')} ${chalk.cyan(worktree)}`);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
leaseCmd
|
|
400
|
+
.command('next')
|
|
401
|
+
.description('Claim the next pending task and acquire its lease')
|
|
402
|
+
.option('--json', 'Output as JSON')
|
|
403
|
+
.option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
|
|
404
|
+
.option('--agent <name>', 'Agent identifier for logging')
|
|
405
|
+
.action((opts) => {
|
|
406
|
+
const repoRoot = getRepo();
|
|
407
|
+
const db = getDb(repoRoot);
|
|
408
|
+
const task = getNextPendingTask(db);
|
|
409
|
+
|
|
410
|
+
if (!task) {
|
|
411
|
+
db.close();
|
|
412
|
+
if (opts.json) console.log(JSON.stringify({ task: null, lease: null }));
|
|
413
|
+
else console.log(chalk.dim('No pending tasks.'));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const worktreeName = getCurrentWorktreeName(opts.worktree);
|
|
418
|
+
const lease = startTaskLease(db, task.id, worktreeName, opts.agent || null);
|
|
419
|
+
db.close();
|
|
420
|
+
|
|
421
|
+
if (!lease) {
|
|
422
|
+
if (opts.json) console.log(JSON.stringify({ task: null, lease: null, message: 'Task claimed by another agent — try again' }));
|
|
423
|
+
else console.log(chalk.yellow('Task was just claimed by another agent. Run again to get the next one.'));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (opts.json) {
|
|
428
|
+
console.log(JSON.stringify({
|
|
429
|
+
lease,
|
|
430
|
+
...taskJsonWithLease(task, worktreeName, lease),
|
|
431
|
+
}, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(`${chalk.green('✓')} Lease acquired: ${chalk.bold(task.title)}`);
|
|
436
|
+
console.log(` ${chalk.dim('task:')} ${task.id} ${chalk.dim('lease:')} ${lease.id}`);
|
|
437
|
+
console.log(` ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
leaseCmd
|
|
441
|
+
.command('list')
|
|
442
|
+
.description('List leases, newest first')
|
|
443
|
+
.option('-s, --status <status>', 'Filter by status (active|completed|failed|expired)')
|
|
444
|
+
.action((opts) => {
|
|
445
|
+
const repoRoot = getRepo();
|
|
446
|
+
const db = getDb(repoRoot);
|
|
447
|
+
const leases = listLeases(db, opts.status);
|
|
448
|
+
db.close();
|
|
449
|
+
|
|
450
|
+
if (!leases.length) {
|
|
451
|
+
console.log(chalk.dim('No leases found.'));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log('');
|
|
456
|
+
for (const lease of leases) {
|
|
457
|
+
console.log(`${statusBadge(lease.status)} ${chalk.bold(lease.task_title)}`);
|
|
458
|
+
console.log(` ${chalk.dim('lease:')} ${lease.id} ${chalk.dim('task:')} ${lease.task_id}`);
|
|
459
|
+
console.log(` ${chalk.dim('worktree:')} ${chalk.cyan(lease.worktree)} ${chalk.dim('agent:')} ${lease.agent || 'unknown'}`);
|
|
460
|
+
console.log(` ${chalk.dim('started:')} ${lease.started_at} ${chalk.dim('heartbeat:')} ${lease.heartbeat_at}`);
|
|
461
|
+
if (lease.failure_reason) console.log(` ${chalk.red(lease.failure_reason)}`);
|
|
462
|
+
console.log('');
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
leaseCmd
|
|
467
|
+
.command('heartbeat <leaseId>')
|
|
468
|
+
.description('Refresh the heartbeat timestamp for an active lease')
|
|
469
|
+
.option('--agent <name>', 'Agent identifier for logging')
|
|
470
|
+
.option('--json', 'Output as JSON')
|
|
471
|
+
.action((leaseId, opts) => {
|
|
472
|
+
const repoRoot = getRepo();
|
|
473
|
+
const db = getDb(repoRoot);
|
|
474
|
+
const lease = heartbeatLease(db, leaseId, opts.agent || null);
|
|
475
|
+
db.close();
|
|
476
|
+
|
|
477
|
+
if (!lease) {
|
|
478
|
+
if (opts.json) console.log(JSON.stringify({ lease: null }));
|
|
479
|
+
else console.log(chalk.red(`No active lease found for ${leaseId}`));
|
|
480
|
+
process.exitCode = 1;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (opts.json) {
|
|
485
|
+
console.log(JSON.stringify({ lease }, null, 2));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.log(`${chalk.green('✓')} Heartbeat refreshed for ${chalk.dim(lease.id)}`);
|
|
490
|
+
console.log(` ${chalk.dim('task:')} ${lease.task_title} ${chalk.dim('worktree:')} ${chalk.cyan(lease.worktree)}`);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
leaseCmd
|
|
494
|
+
.command('reap')
|
|
495
|
+
.description('Expire stale leases, release their claims, and return their tasks to pending')
|
|
496
|
+
.option('--stale-after-minutes <minutes>', 'Age threshold for staleness', String(DEFAULT_STALE_LEASE_MINUTES))
|
|
497
|
+
.option('--json', 'Output as JSON')
|
|
498
|
+
.action((opts) => {
|
|
499
|
+
const repoRoot = getRepo();
|
|
500
|
+
const db = getDb(repoRoot);
|
|
501
|
+
const staleAfterMinutes = Number.parseInt(opts.staleAfterMinutes, 10);
|
|
502
|
+
const expired = reapStaleLeases(db, staleAfterMinutes);
|
|
503
|
+
db.close();
|
|
504
|
+
|
|
505
|
+
if (opts.json) {
|
|
506
|
+
console.log(JSON.stringify({ stale_after_minutes: staleAfterMinutes, expired }, null, 2));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!expired.length) {
|
|
511
|
+
console.log(chalk.dim(`No stale leases older than ${staleAfterMinutes} minute(s).`));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(`${chalk.green('✓')} Reaped ${expired.length} stale lease(s)`);
|
|
516
|
+
for (const lease of expired) {
|
|
517
|
+
console.log(` ${chalk.dim(lease.id)} ${chalk.cyan(lease.worktree)} → ${lease.task_title}`);
|
|
339
518
|
}
|
|
340
519
|
});
|
|
341
520
|
|
|
@@ -412,23 +591,27 @@ program
|
|
|
412
591
|
const repoRoot = getRepo();
|
|
413
592
|
const db = getDb(repoRoot);
|
|
414
593
|
|
|
415
|
-
|
|
416
|
-
|
|
594
|
+
try {
|
|
595
|
+
const conflicts = checkFileConflicts(db, files, worktree);
|
|
417
596
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
597
|
+
if (conflicts.length > 0 && !opts.force) {
|
|
598
|
+
console.log(chalk.red(`\n⚠ Claim conflicts detected:`));
|
|
599
|
+
for (const c of conflicts) {
|
|
600
|
+
console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
|
|
601
|
+
}
|
|
602
|
+
console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
|
|
603
|
+
return;
|
|
422
604
|
}
|
|
423
|
-
|
|
605
|
+
|
|
606
|
+
const lease = claimFiles(db, taskId, worktree, files, opts.agent);
|
|
607
|
+
console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
|
|
608
|
+
files.forEach(f => console.log(` ${chalk.dim(f)}`));
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.error(chalk.red(err.message));
|
|
611
|
+
process.exitCode = 1;
|
|
612
|
+
} finally {
|
|
424
613
|
db.close();
|
|
425
|
-
return;
|
|
426
614
|
}
|
|
427
|
-
|
|
428
|
-
claimFiles(db, taskId, worktree, files, opts.agent);
|
|
429
|
-
db.close();
|
|
430
|
-
console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)}`);
|
|
431
|
-
files.forEach(f => console.log(` ${chalk.dim(f)}`));
|
|
432
615
|
});
|
|
433
616
|
|
|
434
617
|
program
|
|
@@ -536,6 +719,8 @@ program
|
|
|
536
719
|
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
537
720
|
const done = tasks.filter(t => t.status === 'done');
|
|
538
721
|
const failed = tasks.filter(t => t.status === 'failed');
|
|
722
|
+
const activeLeases = listLeases(db, 'active');
|
|
723
|
+
const staleLeases = getStaleLeases(db);
|
|
539
724
|
|
|
540
725
|
console.log(chalk.bold('Tasks:'));
|
|
541
726
|
console.log(` ${chalk.yellow('Pending')} ${pending.length}`);
|
|
@@ -543,14 +728,28 @@ program
|
|
|
543
728
|
console.log(` ${chalk.green('Done')} ${done.length}`);
|
|
544
729
|
console.log(` ${chalk.red('Failed')} ${failed.length}`);
|
|
545
730
|
|
|
546
|
-
if (
|
|
731
|
+
if (activeLeases.length > 0) {
|
|
547
732
|
console.log('');
|
|
548
|
-
console.log(chalk.bold('Active
|
|
733
|
+
console.log(chalk.bold('Active Leases:'));
|
|
734
|
+
for (const lease of activeLeases) {
|
|
735
|
+
console.log(` ${chalk.cyan(lease.worktree)} → ${lease.task_title} ${chalk.dim(lease.id)}`);
|
|
736
|
+
}
|
|
737
|
+
} else if (inProgress.length > 0) {
|
|
738
|
+
console.log('');
|
|
739
|
+
console.log(chalk.bold('In-Progress Tasks Without Lease:'));
|
|
549
740
|
for (const t of inProgress) {
|
|
550
741
|
console.log(` ${chalk.cyan(t.worktree || 'unassigned')} → ${t.title}`);
|
|
551
742
|
}
|
|
552
743
|
}
|
|
553
744
|
|
|
745
|
+
if (staleLeases.length > 0) {
|
|
746
|
+
console.log('');
|
|
747
|
+
console.log(chalk.bold('Stale Leases:'));
|
|
748
|
+
for (const lease of staleLeases) {
|
|
749
|
+
console.log(` ${chalk.red(lease.worktree)} → ${lease.task_title} ${chalk.dim(lease.id)} ${chalk.dim(lease.heartbeat_at)}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
554
753
|
if (pending.length > 0) {
|
|
555
754
|
console.log('');
|
|
556
755
|
console.log(chalk.bold('Next Up:'));
|
|
@@ -599,4 +798,4 @@ program
|
|
|
599
798
|
console.log('');
|
|
600
799
|
});
|
|
601
800
|
|
|
602
|
-
program.parse();
|
|
801
|
+
program.parse();
|