switchman-dev 0.1.0 → 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 +5 -5
- 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/CLAUDE.md +0 -98
- package/examples/taskapi/.switchman/switchman.db +0 -0
- package/examples/taskapi/package-lock.json +0 -4736
- package/examples/taskapi/tests/api.test.js +0 -112
- package/examples/worktrees/agent-rate-limiting/package-lock.json +0 -4736
- package/examples/worktrees/agent-rate-limiting/package.json +0 -18
- package/examples/worktrees/agent-rate-limiting/src/db.js +0 -179
- package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-rate-limiting/src/routes/users.js +0 -38
- package/examples/worktrees/agent-rate-limiting/src/server.js +0 -7
- package/examples/worktrees/agent-rate-limiting/tests/api.test.js +0 -112
- package/examples/worktrees/agent-tests/package-lock.json +0 -4736
- package/examples/worktrees/agent-tests/package.json +0 -18
- package/examples/worktrees/agent-tests/src/db.js +0 -179
- package/examples/worktrees/agent-tests/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-tests/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-tests/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-tests/src/routes/users.js +0 -38
- package/examples/worktrees/agent-tests/src/server.js +0 -7
- package/examples/worktrees/agent-tests/tests/api.test.js +0 -112
- package/examples/worktrees/agent-validation/package-lock.json +0 -4736
- package/examples/worktrees/agent-validation/package.json +0 -18
- package/examples/worktrees/agent-validation/src/db.js +0 -179
- package/examples/worktrees/agent-validation/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-validation/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-validation/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-validation/src/routes/users.js +0 -38
- package/examples/worktrees/agent-validation/src/server.js +0 -7
- package/examples/worktrees/agent-validation/tests/api.test.js +0 -112
- package/tests/test.js +0 -259
package/src/mcp/server.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* switchman_task_claim — claim files for a task (conflict-safe)
|
|
12
12
|
* switchman_task_done — mark a task complete + release file claims
|
|
13
13
|
* switchman_task_fail — mark a task failed + release file claims
|
|
14
|
+
* switchman_lease_heartbeat — refresh a lease heartbeat
|
|
14
15
|
* switchman_scan — scan all worktrees for conflicts right now
|
|
15
16
|
* switchman_status — full system overview (tasks, claims, worktrees)
|
|
16
17
|
*/
|
|
@@ -23,12 +24,15 @@ import { findRepoRoot } from '../core/git.js';
|
|
|
23
24
|
import {
|
|
24
25
|
openDb,
|
|
25
26
|
createTask,
|
|
26
|
-
|
|
27
|
+
startTaskLease,
|
|
27
28
|
completeTask,
|
|
28
29
|
failTask,
|
|
29
30
|
listTasks,
|
|
30
|
-
getTask,
|
|
31
31
|
getNextPendingTask,
|
|
32
|
+
listLeases,
|
|
33
|
+
getActiveLeaseForTask,
|
|
34
|
+
heartbeatLease,
|
|
35
|
+
getStaleLeases,
|
|
32
36
|
claimFiles,
|
|
33
37
|
releaseFileClaims,
|
|
34
38
|
getActiveFileClaims,
|
|
@@ -95,7 +99,10 @@ Returns JSON:
|
|
|
95
99
|
"description": string | null,
|
|
96
100
|
"priority": number, // 1-10, higher = more urgent
|
|
97
101
|
"worktree": string,
|
|
98
|
-
"status": "in_progress"
|
|
102
|
+
"status": "in_progress",
|
|
103
|
+
"lease_id": string, // Active lease/session ID for the task
|
|
104
|
+
"lease_status": "active",
|
|
105
|
+
"heartbeat_at": string
|
|
99
106
|
} | null // null when queue is empty
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -124,17 +131,27 @@ Examples:
|
|
|
124
131
|
return toolOk(JSON.stringify({ task: null }), { task: null });
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
const
|
|
128
|
-
db.close();
|
|
134
|
+
const lease = startTaskLease(db, task.id, worktree, agent ?? null);
|
|
129
135
|
|
|
130
|
-
if (!
|
|
136
|
+
if (!lease) {
|
|
137
|
+
db.close();
|
|
131
138
|
// Race condition: another agent grabbed it first — try again
|
|
132
139
|
return toolOk(
|
|
133
140
|
JSON.stringify({ task: null, message: 'Task was claimed by another agent. Call switchman_task_next again.' }),
|
|
134
141
|
);
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
db.close();
|
|
145
|
+
const result = {
|
|
146
|
+
task: {
|
|
147
|
+
...task,
|
|
148
|
+
worktree,
|
|
149
|
+
status: 'in_progress',
|
|
150
|
+
lease_id: lease.id,
|
|
151
|
+
lease_status: lease.status,
|
|
152
|
+
heartbeat_at: lease.heartbeat_at,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
138
155
|
return toolOk(JSON.stringify(result, null, 2), result);
|
|
139
156
|
} catch (err) {
|
|
140
157
|
return toolError(`${err.message}. Make sure switchman is initialised in this repo (run 'switchman init').`);
|
|
@@ -210,16 +227,20 @@ Args:
|
|
|
210
227
|
- worktree (string): Your git worktree name
|
|
211
228
|
- files (array of strings): File paths you plan to edit, relative to repo root (e.g. ["src/auth/login.js", "tests/auth.test.js"])
|
|
212
229
|
- agent (string, optional): Agent identifier for logging
|
|
230
|
+
- lease_id (string, optional): Active lease ID returned by switchman_task_next
|
|
213
231
|
- force (boolean, optional): If true, claim even if conflicts exist (default: false)
|
|
214
232
|
|
|
215
233
|
Returns JSON:
|
|
216
234
|
{
|
|
235
|
+
"lease_id": string,
|
|
217
236
|
"claimed": string[], // Files successfully claimed
|
|
218
237
|
"conflicts": [ // Files already claimed by other worktrees
|
|
219
238
|
{
|
|
220
239
|
"file": string,
|
|
240
|
+
"claimed_by_task_id": string,
|
|
221
241
|
"claimed_by_worktree": string,
|
|
222
|
-
"claimed_by_task": string
|
|
242
|
+
"claimed_by_task": string,
|
|
243
|
+
"claimed_by_lease_id": string | null
|
|
223
244
|
}
|
|
224
245
|
],
|
|
225
246
|
"safe_to_proceed": boolean // true if no conflicts (or force=true)
|
|
@@ -234,6 +255,7 @@ Examples:
|
|
|
234
255
|
worktree: z.string().min(1).describe('Your git worktree name'),
|
|
235
256
|
files: z.array(z.string().min(1)).min(1).max(500).describe('File paths to claim, relative to repo root'),
|
|
236
257
|
agent: z.string().optional().describe('Agent identifier for logging'),
|
|
258
|
+
lease_id: z.string().optional().describe('Optional lease ID returned by switchman_task_next'),
|
|
237
259
|
force: z.boolean().default(false).describe('Claim even if conflicts exist (use with caution)'),
|
|
238
260
|
}),
|
|
239
261
|
annotations: {
|
|
@@ -243,9 +265,10 @@ Examples:
|
|
|
243
265
|
openWorldHint: false,
|
|
244
266
|
},
|
|
245
267
|
},
|
|
246
|
-
async ({ task_id, worktree, files, agent, force }) => {
|
|
268
|
+
async ({ task_id, worktree, files, agent, lease_id, force }) => {
|
|
269
|
+
let db;
|
|
247
270
|
try {
|
|
248
|
-
|
|
271
|
+
({ db } = getContext());
|
|
249
272
|
|
|
250
273
|
// Check for conflicts first
|
|
251
274
|
const conflicts = checkFileConflicts(db, files, worktree);
|
|
@@ -256,8 +279,10 @@ Examples:
|
|
|
256
279
|
claimed: [],
|
|
257
280
|
conflicts: conflicts.map((c) => ({
|
|
258
281
|
file: c.file,
|
|
282
|
+
claimed_by_task_id: c.claimedBy.task_id,
|
|
259
283
|
claimed_by_worktree: c.claimedBy.worktree,
|
|
260
284
|
claimed_by_task: c.claimedBy.task_title,
|
|
285
|
+
claimed_by_lease_id: c.claimedBy.lease_id ?? null,
|
|
261
286
|
})),
|
|
262
287
|
safe_to_proceed: false,
|
|
263
288
|
};
|
|
@@ -268,15 +293,20 @@ Examples:
|
|
|
268
293
|
);
|
|
269
294
|
}
|
|
270
295
|
|
|
271
|
-
claimFiles(db, task_id, worktree, files, agent ?? null);
|
|
272
|
-
|
|
296
|
+
const lease = claimFiles(db, task_id, worktree, files, agent ?? null);
|
|
297
|
+
if (lease_id && lease.id !== lease_id) {
|
|
298
|
+
return toolError(`Task ${task_id} is active under lease ${lease.id}, not ${lease_id}.`);
|
|
299
|
+
}
|
|
273
300
|
|
|
274
301
|
const result = {
|
|
302
|
+
lease_id: lease.id,
|
|
275
303
|
claimed: files,
|
|
276
304
|
conflicts: conflicts.map((c) => ({
|
|
277
305
|
file: c.file,
|
|
306
|
+
claimed_by_task_id: c.claimedBy.task_id,
|
|
278
307
|
claimed_by_worktree: c.claimedBy.worktree,
|
|
279
308
|
claimed_by_task: c.claimedBy.task_title,
|
|
309
|
+
claimed_by_lease_id: c.claimedBy.lease_id ?? null,
|
|
280
310
|
})),
|
|
281
311
|
safe_to_proceed: true,
|
|
282
312
|
};
|
|
@@ -286,6 +316,8 @@ Examples:
|
|
|
286
316
|
);
|
|
287
317
|
} catch (err) {
|
|
288
318
|
return toolError(err.message);
|
|
319
|
+
} finally {
|
|
320
|
+
db?.close();
|
|
289
321
|
}
|
|
290
322
|
},
|
|
291
323
|
);
|
|
@@ -302,15 +334,18 @@ Call this when you have finished your implementation and committed your changes.
|
|
|
302
334
|
|
|
303
335
|
Args:
|
|
304
336
|
- task_id (string): The task ID to complete
|
|
337
|
+
- lease_id (string, optional): Active lease ID returned by switchman_task_next
|
|
305
338
|
|
|
306
339
|
Returns JSON:
|
|
307
340
|
{
|
|
308
341
|
"task_id": string,
|
|
342
|
+
"lease_id": string | null,
|
|
309
343
|
"status": "done",
|
|
310
344
|
"files_released": true
|
|
311
345
|
}`,
|
|
312
346
|
inputSchema: z.object({
|
|
313
347
|
task_id: z.string().min(1).describe('The task ID to mark complete'),
|
|
348
|
+
lease_id: z.string().optional().describe('Optional lease ID returned by switchman_task_next'),
|
|
314
349
|
}),
|
|
315
350
|
annotations: {
|
|
316
351
|
readOnlyHint: false,
|
|
@@ -319,14 +354,19 @@ Returns JSON:
|
|
|
319
354
|
openWorldHint: false,
|
|
320
355
|
},
|
|
321
356
|
},
|
|
322
|
-
async ({ task_id }) => {
|
|
357
|
+
async ({ task_id, lease_id }) => {
|
|
323
358
|
try {
|
|
324
359
|
const { db } = getContext();
|
|
360
|
+
const activeLease = getActiveLeaseForTask(db, task_id);
|
|
361
|
+
if (lease_id && activeLease && activeLease.id !== lease_id) {
|
|
362
|
+
db.close();
|
|
363
|
+
return toolError(`Task ${task_id} is active under lease ${activeLease.id}, not ${lease_id}.`);
|
|
364
|
+
}
|
|
325
365
|
completeTask(db, task_id);
|
|
326
366
|
releaseFileClaims(db, task_id);
|
|
327
367
|
db.close();
|
|
328
368
|
|
|
329
|
-
const result = { task_id, status: 'done', files_released: true };
|
|
369
|
+
const result = { task_id, lease_id: activeLease?.id ?? lease_id ?? null, status: 'done', files_released: true };
|
|
330
370
|
return toolOk(JSON.stringify(result, null, 2), result);
|
|
331
371
|
} catch (err) {
|
|
332
372
|
return toolError(err.message);
|
|
@@ -346,17 +386,20 @@ Call this if you cannot complete the task — the task will be visible in the qu
|
|
|
346
386
|
|
|
347
387
|
Args:
|
|
348
388
|
- task_id (string): The task ID to mark as failed
|
|
389
|
+
- lease_id (string, optional): Active lease ID returned by switchman_task_next
|
|
349
390
|
- reason (string): Brief explanation of why the task failed
|
|
350
391
|
|
|
351
392
|
Returns JSON:
|
|
352
393
|
{
|
|
353
394
|
"task_id": string,
|
|
395
|
+
"lease_id": string | null,
|
|
354
396
|
"status": "failed",
|
|
355
397
|
"reason": string,
|
|
356
398
|
"files_released": true
|
|
357
399
|
}`,
|
|
358
400
|
inputSchema: z.object({
|
|
359
401
|
task_id: z.string().min(1).describe('The task ID to mark as failed'),
|
|
402
|
+
lease_id: z.string().optional().describe('Optional lease ID returned by switchman_task_next'),
|
|
360
403
|
reason: z.string().min(1).max(500).describe('Brief explanation of why the task failed'),
|
|
361
404
|
}),
|
|
362
405
|
annotations: {
|
|
@@ -366,14 +409,74 @@ Returns JSON:
|
|
|
366
409
|
openWorldHint: false,
|
|
367
410
|
},
|
|
368
411
|
},
|
|
369
|
-
async ({ task_id, reason }) => {
|
|
412
|
+
async ({ task_id, lease_id, reason }) => {
|
|
370
413
|
try {
|
|
371
414
|
const { db } = getContext();
|
|
415
|
+
const activeLease = getActiveLeaseForTask(db, task_id);
|
|
416
|
+
if (lease_id && activeLease && activeLease.id !== lease_id) {
|
|
417
|
+
db.close();
|
|
418
|
+
return toolError(`Task ${task_id} is active under lease ${activeLease.id}, not ${lease_id}.`);
|
|
419
|
+
}
|
|
372
420
|
failTask(db, task_id, reason);
|
|
373
421
|
releaseFileClaims(db, task_id);
|
|
374
422
|
db.close();
|
|
375
423
|
|
|
376
|
-
const result = { task_id, status: 'failed', reason, files_released: true };
|
|
424
|
+
const result = { task_id, lease_id: activeLease?.id ?? lease_id ?? null, status: 'failed', reason, files_released: true };
|
|
425
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
return toolError(err.message);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// ── switchman_lease_heartbeat ─────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
server.registerTool(
|
|
435
|
+
'switchman_lease_heartbeat',
|
|
436
|
+
{
|
|
437
|
+
title: 'Refresh Lease Heartbeat',
|
|
438
|
+
description: `Refreshes the heartbeat timestamp for an active lease.
|
|
439
|
+
|
|
440
|
+
Call this periodically while an agent is still working on a task so stale-session reaping does not recycle the task prematurely.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
- lease_id (string): Active lease ID returned by switchman_task_next
|
|
444
|
+
- agent (string, optional): Agent identifier to attach to the refreshed lease
|
|
445
|
+
|
|
446
|
+
Returns JSON:
|
|
447
|
+
{
|
|
448
|
+
"lease_id": string,
|
|
449
|
+
"task_id": string,
|
|
450
|
+
"worktree": string,
|
|
451
|
+
"heartbeat_at": string
|
|
452
|
+
}`,
|
|
453
|
+
inputSchema: z.object({
|
|
454
|
+
lease_id: z.string().min(1).describe('Active lease ID returned by switchman_task_next'),
|
|
455
|
+
agent: z.string().optional().describe('Agent identifier for logging'),
|
|
456
|
+
}),
|
|
457
|
+
annotations: {
|
|
458
|
+
readOnlyHint: false,
|
|
459
|
+
destructiveHint: false,
|
|
460
|
+
idempotentHint: true,
|
|
461
|
+
openWorldHint: false,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
async ({ lease_id, agent }) => {
|
|
465
|
+
try {
|
|
466
|
+
const { db } = getContext();
|
|
467
|
+
const lease = heartbeatLease(db, lease_id, agent ?? null);
|
|
468
|
+
db.close();
|
|
469
|
+
|
|
470
|
+
if (!lease) {
|
|
471
|
+
return toolError(`Lease ${lease_id} was not found or is no longer active.`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const result = {
|
|
475
|
+
lease_id: lease.id,
|
|
476
|
+
task_id: lease.task_id,
|
|
477
|
+
worktree: lease.worktree,
|
|
478
|
+
heartbeat_at: lease.heartbeat_at,
|
|
479
|
+
};
|
|
377
480
|
return toolOk(JSON.stringify(result, null, 2), result);
|
|
378
481
|
} catch (err) {
|
|
379
482
|
return toolError(err.message);
|
|
@@ -479,13 +582,17 @@ Returns JSON:
|
|
|
479
582
|
"in_progress": number,
|
|
480
583
|
"done": number,
|
|
481
584
|
"failed": number,
|
|
482
|
-
"active": [{ "id": string, "title": string, "worktree": string, "priority": number }]
|
|
585
|
+
"active": [{ "id": string, "title": string, "worktree": string, "priority": number, "lease_id": string | null }]
|
|
483
586
|
},
|
|
484
587
|
"file_claims": {
|
|
485
588
|
"total_active": number,
|
|
486
|
-
"by_worktree": { [worktree: string]: string[] }
|
|
589
|
+
"by_worktree": { [worktree: string]: { "file_path": string, "task_id": string, "lease_id": string | null }[] }
|
|
590
|
+
},
|
|
591
|
+
"leases": {
|
|
592
|
+
"active": [{ "id": string, "task_id": string, "worktree": string, "agent": string | null, "heartbeat_at": string }],
|
|
593
|
+
"stale": [{ "id": string, "task_id": string, "worktree": string, "heartbeat_at": string }]
|
|
487
594
|
},
|
|
488
|
-
"worktrees": [{ "name": string, "branch": string, "agent": string | null, "status": string }],
|
|
595
|
+
"worktrees": [{ "name": string, "branch": string, "agent": string | null, "status": string, "active_lease_id": string | null }],
|
|
489
596
|
"repo_root": string
|
|
490
597
|
}`,
|
|
491
598
|
inputSchema: z.object({}),
|
|
@@ -503,14 +610,27 @@ Returns JSON:
|
|
|
503
610
|
const tasks = listTasks(db);
|
|
504
611
|
const claims = getActiveFileClaims(db);
|
|
505
612
|
const worktrees = listWorktrees(db);
|
|
613
|
+
const leases = listLeases(db);
|
|
614
|
+
const staleLeases = getStaleLeases(db);
|
|
506
615
|
db.close();
|
|
507
616
|
|
|
508
617
|
const byWorktree = {};
|
|
509
618
|
for (const c of claims) {
|
|
510
619
|
if (!byWorktree[c.worktree]) byWorktree[c.worktree] = [];
|
|
511
|
-
byWorktree[c.worktree].push(
|
|
620
|
+
byWorktree[c.worktree].push({
|
|
621
|
+
file_path: c.file_path,
|
|
622
|
+
task_id: c.task_id,
|
|
623
|
+
lease_id: c.lease_id ?? null,
|
|
624
|
+
});
|
|
512
625
|
}
|
|
513
626
|
|
|
627
|
+
const activeLeaseByTask = new Map(
|
|
628
|
+
leases.filter((lease) => lease.status === 'active').map((lease) => [lease.task_id, lease]),
|
|
629
|
+
);
|
|
630
|
+
const activeLeaseByWorktree = new Map(
|
|
631
|
+
leases.filter((lease) => lease.status === 'active').map((lease) => [lease.worktree, lease]),
|
|
632
|
+
);
|
|
633
|
+
|
|
514
634
|
const result = {
|
|
515
635
|
tasks: {
|
|
516
636
|
pending: tasks.filter((t) => t.status === 'pending').length,
|
|
@@ -519,17 +639,45 @@ Returns JSON:
|
|
|
519
639
|
failed: tasks.filter((t) => t.status === 'failed').length,
|
|
520
640
|
active: tasks
|
|
521
641
|
.filter((t) => t.status === 'in_progress')
|
|
522
|
-
.map((t) => ({
|
|
642
|
+
.map((t) => ({
|
|
643
|
+
id: t.id,
|
|
644
|
+
title: t.title,
|
|
645
|
+
worktree: t.worktree,
|
|
646
|
+
priority: t.priority,
|
|
647
|
+
lease_id: activeLeaseByTask.get(t.id)?.id ?? null,
|
|
648
|
+
})),
|
|
523
649
|
},
|
|
524
650
|
file_claims: {
|
|
525
651
|
total_active: claims.length,
|
|
526
652
|
by_worktree: byWorktree,
|
|
653
|
+
claims: claims.map((claim) => ({
|
|
654
|
+
file_path: claim.file_path,
|
|
655
|
+
worktree: claim.worktree,
|
|
656
|
+
task_id: claim.task_id,
|
|
657
|
+
lease_id: claim.lease_id ?? null,
|
|
658
|
+
})),
|
|
659
|
+
},
|
|
660
|
+
leases: {
|
|
661
|
+
active: leases.filter((lease) => lease.status === 'active').map((lease) => ({
|
|
662
|
+
id: lease.id,
|
|
663
|
+
task_id: lease.task_id,
|
|
664
|
+
worktree: lease.worktree,
|
|
665
|
+
agent: lease.agent ?? null,
|
|
666
|
+
heartbeat_at: lease.heartbeat_at,
|
|
667
|
+
})),
|
|
668
|
+
stale: staleLeases.map((lease) => ({
|
|
669
|
+
id: lease.id,
|
|
670
|
+
task_id: lease.task_id,
|
|
671
|
+
worktree: lease.worktree,
|
|
672
|
+
heartbeat_at: lease.heartbeat_at,
|
|
673
|
+
})),
|
|
527
674
|
},
|
|
528
675
|
worktrees: worktrees.map((wt) => ({
|
|
529
676
|
name: wt.name,
|
|
530
677
|
branch: wt.branch,
|
|
531
678
|
agent: wt.agent ?? null,
|
|
532
679
|
status: wt.status,
|
|
680
|
+
active_lease_id: activeLeaseByWorktree.get(wt.name)?.id ?? null,
|
|
533
681
|
})),
|
|
534
682
|
repo_root: repoRoot,
|
|
535
683
|
};
|
|
@@ -552,4 +700,4 @@ async function main() {
|
|
|
552
700
|
main().catch((err) => {
|
|
553
701
|
process.stderr.write(`switchman MCP server fatal error: ${err.message}\n`);
|
|
554
702
|
process.exit(1);
|
|
555
|
-
});
|
|
703
|
+
});
|
package/CLAUDE.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Switchman Agent Instructions
|
|
2
|
-
|
|
3
|
-
This repository uses **Switchman** to coordinate parallel AI coding agents.
|
|
4
|
-
You MUST follow these instructions every session to avoid conflicting with other agents.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Your worktree
|
|
9
|
-
|
|
10
|
-
Find your worktree name by running:
|
|
11
|
-
```bash
|
|
12
|
-
git worktree list
|
|
13
|
-
```
|
|
14
|
-
The path that matches your current directory is your worktree. Use the last path segment as your worktree name (e.g. `/projects/myapp-feature-auth` → `feature-auth`). The main repo root is always named `main`.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Required workflow — follow this every session
|
|
19
|
-
|
|
20
|
-
### 1. Start of session — get your task
|
|
21
|
-
|
|
22
|
-
Call the `switchman_task_next` MCP tool with your worktree name:
|
|
23
|
-
```
|
|
24
|
-
switchman_task_next({ worktree: "<your-worktree-name>", agent: "claude-code" })
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- If `task` is `null` — the queue is empty. Ask the user what to work on, or stop.
|
|
28
|
-
- If you receive a task — note the `task.id`. You'll need it in the next steps.
|
|
29
|
-
|
|
30
|
-
### 2. Before editing any files — claim them
|
|
31
|
-
|
|
32
|
-
Call `switchman_task_claim` with every file you plan to edit **before you edit them**:
|
|
33
|
-
```
|
|
34
|
-
switchman_task_claim({
|
|
35
|
-
task_id: "<task-id>",
|
|
36
|
-
worktree: "<your-worktree-name>",
|
|
37
|
-
files: ["src/auth/login.js", "tests/auth.test.js"]
|
|
38
|
-
})
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
- If `safe_to_proceed` is `false` — there are conflicts. Do NOT edit those files.
|
|
42
|
-
Read the `conflicts` array to see which worktrees own them, then either:
|
|
43
|
-
- Choose different files that accomplish the same goal
|
|
44
|
-
- Ask the user how to proceed
|
|
45
|
-
|
|
46
|
-
- If `safe_to_proceed` is `true` — you are clear to edit.
|
|
47
|
-
|
|
48
|
-
### 3. Do the work
|
|
49
|
-
|
|
50
|
-
Implement the task. Make commits as normal. Other agents will avoid your claimed files.
|
|
51
|
-
|
|
52
|
-
If you discover mid-task that you need to edit additional files, call `switchman_task_claim` again for those files before editing them.
|
|
53
|
-
|
|
54
|
-
### 4. End of session — mark complete or failed
|
|
55
|
-
|
|
56
|
-
**On success:**
|
|
57
|
-
```
|
|
58
|
-
switchman_task_done({ task_id: "<task-id>", release_files: true })
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**On failure (can't complete the task):**
|
|
62
|
-
```
|
|
63
|
-
switchman_task_fail({ task_id: "<task-id>", reason: "Brief explanation of what blocked you" })
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Always call one of these before ending your session. Released file claims allow other agents to proceed.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## Checking for conflicts
|
|
71
|
-
|
|
72
|
-
At any time you can scan for conflicts across all worktrees:
|
|
73
|
-
```
|
|
74
|
-
switchman_scan()
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Run this before merging your branch. If `safe_to_proceed` is `false`, do not merge until conflicts are resolved.
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## Checking system state
|
|
82
|
-
|
|
83
|
-
To see what other agents are doing:
|
|
84
|
-
```
|
|
85
|
-
switchman_status()
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
This shows all pending and in-progress tasks, file claims per worktree, and worktree list.
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Rules
|
|
93
|
-
|
|
94
|
-
1. **Always claim files before editing them** — not after.
|
|
95
|
-
2. **Always call `switchman_task_done` or `switchman_task_fail` at end of session** — never leave tasks as `in_progress` when you stop.
|
|
96
|
-
3. **If `safe_to_proceed` is false, do not edit the conflicting files** — coordinate first.
|
|
97
|
-
4. **Do not claim files you don't need** — over-claiming blocks other agents unnecessarily.
|
|
98
|
-
5. **One task per session** — complete or fail your current task before taking another.
|
|
Binary file
|