lockstep-mcp 0.1.0

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/dist/server.js ADDED
@@ -0,0 +1,1942 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawn } from "node:child_process";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
10
+ import { loadConfig } from "./config.js";
11
+ import { resolvePath, ensureDir } from "./utils.js";
12
+ import { createStore } from "./storage.js";
13
+ import { createWorktree, removeWorktree, getWorktreeStatus, mergeWorktree, listWorktrees, cleanupOrphanedWorktrees, getWorktreeDiff, isGitRepo, } from "./worktree.js";
14
+ const config = loadConfig();
15
+ const store = createStore(config);
16
+ function getString(value) {
17
+ return typeof value === "string" ? value : undefined;
18
+ }
19
+ function getNumber(value) {
20
+ return typeof value === "number" ? value : undefined;
21
+ }
22
+ function getBoolean(value) {
23
+ return typeof value === "boolean" ? value : undefined;
24
+ }
25
+ function getStringArray(value) {
26
+ if (!Array.isArray(value))
27
+ return undefined;
28
+ if (!value.every((item) => typeof item === "string"))
29
+ return undefined;
30
+ return value;
31
+ }
32
+ function getObject(value) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value))
34
+ return undefined;
35
+ return value;
36
+ }
37
+ function jsonResponse(data) {
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: JSON.stringify(data, null, 2),
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ function errorResponse(message) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: message,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ function isCommandAllowed(command) {
59
+ if (config.command.mode === "open")
60
+ return true;
61
+ const commandName = command.trim().split(/\s+/)[0];
62
+ return config.command.allow.includes(commandName);
63
+ }
64
+ async function runCommand(command, options = {}) {
65
+ if (!isCommandAllowed(command)) {
66
+ throw new Error(`Command not allowed: ${command}`);
67
+ }
68
+ const cwd = options.cwd ? resolvePath(options.cwd, config.mode, config.roots) : undefined;
69
+ const maxOutputBytes = options.maxOutputBytes ?? 1024 * 1024;
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn(command, {
72
+ shell: true,
73
+ cwd,
74
+ env: { ...process.env, ...(options.env ?? {}) },
75
+ });
76
+ let stdout = Buffer.alloc(0);
77
+ let stderr = Buffer.alloc(0);
78
+ let stdoutTruncated = false;
79
+ let stderrTruncated = false;
80
+ let timedOut = false;
81
+ const append = (buffer, chunk, setTruncated) => {
82
+ if (buffer.length + chunk.length > maxOutputBytes) {
83
+ setTruncated(true);
84
+ const remaining = maxOutputBytes - buffer.length;
85
+ if (remaining > 0) {
86
+ return Buffer.concat([buffer, chunk.subarray(0, remaining)]);
87
+ }
88
+ return buffer;
89
+ }
90
+ return Buffer.concat([buffer, chunk]);
91
+ };
92
+ child.stdout?.on("data", (chunk) => {
93
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
94
+ stdout = append(stdout, data, (val) => {
95
+ stdoutTruncated = val;
96
+ });
97
+ });
98
+ child.stderr?.on("data", (chunk) => {
99
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
100
+ stderr = append(stderr, data, (val) => {
101
+ stderrTruncated = val;
102
+ });
103
+ });
104
+ let timeoutId;
105
+ if (options.timeoutMs && options.timeoutMs > 0) {
106
+ timeoutId = setTimeout(() => {
107
+ timedOut = true;
108
+ child.kill("SIGTERM");
109
+ }, options.timeoutMs);
110
+ }
111
+ child.on("error", (error) => {
112
+ if (timeoutId)
113
+ clearTimeout(timeoutId);
114
+ reject(error);
115
+ });
116
+ child.on("close", (code, signal) => {
117
+ if (timeoutId)
118
+ clearTimeout(timeoutId);
119
+ resolve({
120
+ stdout: stdout.toString("utf8"),
121
+ stderr: stderr.toString("utf8"),
122
+ exitCode: code,
123
+ signal,
124
+ timedOut,
125
+ stdoutTruncated,
126
+ stderrTruncated,
127
+ });
128
+ });
129
+ });
130
+ }
131
+ async function readFileSafe(filePath, options = {}) {
132
+ const resolved = resolvePath(filePath, config.mode, config.roots);
133
+ const maxBytes = options.maxBytes ?? 1024 * 1024;
134
+ const data = await fs.readFile(resolved);
135
+ const sliced = data.length > maxBytes ? data.subarray(0, maxBytes) : data;
136
+ if (options.binary) {
137
+ return { path: resolved, truncated: data.length > maxBytes, content: sliced.toString("base64") };
138
+ }
139
+ return {
140
+ path: resolved,
141
+ truncated: data.length > maxBytes,
142
+ content: sliced.toString(options.encoding ?? "utf8"),
143
+ };
144
+ }
145
+ async function writeFileSafe(filePath, content, options = {}) {
146
+ const resolved = resolvePath(filePath, config.mode, config.roots);
147
+ if (options.createDirs) {
148
+ await ensureDir(path.dirname(resolved));
149
+ }
150
+ if (options.mode === "append") {
151
+ await fs.appendFile(resolved, content, options.encoding ?? "utf8");
152
+ }
153
+ else {
154
+ await fs.writeFile(resolved, content, options.encoding ?? "utf8");
155
+ }
156
+ return { path: resolved, bytes: Buffer.byteLength(content, options.encoding ?? "utf8") };
157
+ }
158
+ const tools = [
159
+ {
160
+ name: "status_get",
161
+ description: "Get coordinator config and state summary",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {},
165
+ additionalProperties: false,
166
+ },
167
+ },
168
+ {
169
+ name: "task_create",
170
+ description: "Create a task. Complexity determines review requirements: simple=no review, medium=verify on completion, complex/critical=planner approval required. Isolation determines whether implementer works in shared directory or isolated git worktree.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ title: { type: "string" },
175
+ description: { type: "string" },
176
+ status: { type: "string", enum: ["todo", "in_progress", "blocked", "review", "done"] },
177
+ complexity: { type: "string", enum: ["simple", "medium", "complex", "critical"], description: "simple=1-2 files obvious fix, medium=3-5 files some ambiguity, complex=6+ files architectural decisions, critical=database/security/cross-product" },
178
+ isolation: { type: "string", enum: ["shared", "worktree"], description: "shared=work in main directory with locks, worktree=isolated git worktree with branch. Default: shared for simple/medium, consider worktree for complex/critical." },
179
+ owner: { type: "string" },
180
+ tags: { type: "array", items: { type: "string" } },
181
+ metadata: { type: "object" },
182
+ },
183
+ required: ["title", "complexity"],
184
+ additionalProperties: false,
185
+ },
186
+ },
187
+ {
188
+ name: "task_claim",
189
+ description: "Claim a task and set status to in_progress",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ id: { type: "string" },
194
+ owner: { type: "string" },
195
+ },
196
+ required: ["id", "owner"],
197
+ additionalProperties: false,
198
+ },
199
+ },
200
+ {
201
+ name: "task_update",
202
+ description: "Update a task",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ id: { type: "string" },
207
+ title: { type: "string" },
208
+ description: { type: "string" },
209
+ status: { type: "string", enum: ["todo", "in_progress", "blocked", "review", "done"] },
210
+ complexity: { type: "string", enum: ["simple", "medium", "complex", "critical"] },
211
+ owner: { type: "string" },
212
+ tags: { type: "array", items: { type: "string" } },
213
+ metadata: { type: "object" },
214
+ },
215
+ required: ["id"],
216
+ additionalProperties: false,
217
+ },
218
+ },
219
+ {
220
+ name: "task_submit_for_review",
221
+ description: "Submit a completed task for planner review (required for complex/critical tasks, recommended for medium)",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ id: { type: "string", description: "Task ID" },
226
+ owner: { type: "string", description: "Your implementer name" },
227
+ reviewNotes: { type: "string", description: "Summary of changes made, files modified, and any concerns or decisions made" },
228
+ },
229
+ required: ["id", "owner", "reviewNotes"],
230
+ additionalProperties: false,
231
+ },
232
+ },
233
+ {
234
+ name: "task_approve",
235
+ description: "PLANNER ONLY: Approve a task that is in review status, marking it done",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ id: { type: "string", description: "Task ID" },
240
+ feedback: { type: "string", description: "Optional feedback or notes on the approved work" },
241
+ },
242
+ required: ["id"],
243
+ additionalProperties: false,
244
+ },
245
+ },
246
+ {
247
+ name: "task_request_changes",
248
+ description: "PLANNER ONLY: Request changes on a task in review, sending it back to in_progress",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ id: { type: "string", description: "Task ID" },
253
+ feedback: { type: "string", description: "What needs to be changed or fixed" },
254
+ },
255
+ required: ["id", "feedback"],
256
+ additionalProperties: false,
257
+ },
258
+ },
259
+ {
260
+ name: "task_approve_batch",
261
+ description: "PLANNER ONLY: Approve multiple tasks at once. More efficient than approving one by one.",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ ids: { type: "array", items: { type: "string" }, description: "Array of task IDs to approve" },
266
+ feedback: { type: "string", description: "Optional feedback for all approved tasks" },
267
+ },
268
+ required: ["ids"],
269
+ additionalProperties: false,
270
+ },
271
+ },
272
+ {
273
+ name: "task_summary",
274
+ description: "Get task counts by status. Lighter than task_list - use when you just need to know how many tasks are in each state.",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {},
278
+ additionalProperties: false,
279
+ },
280
+ },
281
+ {
282
+ name: "task_list",
283
+ description: "List tasks with optional filters. For active work, defaults to excluding done tasks to reduce response size. Use includeDone=true or status='done' to see completed tasks.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {
287
+ status: { type: "string", enum: ["todo", "in_progress", "blocked", "review", "done"] },
288
+ owner: { type: "string" },
289
+ tag: { type: "string" },
290
+ limit: { type: "number" },
291
+ includeDone: { type: "boolean", description: "Include done tasks in results (default: false for smaller responses)" },
292
+ offset: { type: "number", description: "Skip first N tasks (for pagination)" },
293
+ },
294
+ additionalProperties: false,
295
+ },
296
+ },
297
+ {
298
+ name: "lock_acquire",
299
+ description: "Acquire a named lock",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ path: { type: "string" },
304
+ owner: { type: "string" },
305
+ note: { type: "string" },
306
+ },
307
+ required: ["path"],
308
+ additionalProperties: false,
309
+ },
310
+ },
311
+ {
312
+ name: "lock_release",
313
+ description: "Release a lock",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ path: { type: "string" },
318
+ owner: { type: "string" },
319
+ },
320
+ required: ["path"],
321
+ additionalProperties: false,
322
+ },
323
+ },
324
+ {
325
+ name: "lock_list",
326
+ description: "List locks with optional filters",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ status: { type: "string", enum: ["active", "resolved"] },
331
+ owner: { type: "string" },
332
+ },
333
+ additionalProperties: false,
334
+ },
335
+ },
336
+ {
337
+ name: "note_append",
338
+ description: "Append a note",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ text: { type: "string" },
343
+ author: { type: "string" },
344
+ },
345
+ required: ["text"],
346
+ additionalProperties: false,
347
+ },
348
+ },
349
+ {
350
+ name: "note_list",
351
+ description: "List recent notes",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ limit: { type: "number" },
356
+ },
357
+ additionalProperties: false,
358
+ },
359
+ },
360
+ {
361
+ name: "artifact_read",
362
+ description: "Read an artifact file",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ path: { type: "string" },
367
+ encoding: { type: "string" },
368
+ maxBytes: { type: "number" },
369
+ binary: { type: "boolean" },
370
+ },
371
+ required: ["path"],
372
+ additionalProperties: false,
373
+ },
374
+ },
375
+ {
376
+ name: "artifact_write",
377
+ description: "Write an artifact file",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ path: { type: "string" },
382
+ content: { type: "string" },
383
+ encoding: { type: "string" },
384
+ mode: { type: "string", enum: ["overwrite", "append"] },
385
+ createDirs: { type: "boolean" },
386
+ },
387
+ required: ["path", "content"],
388
+ additionalProperties: false,
389
+ },
390
+ },
391
+ {
392
+ name: "file_read",
393
+ description: "Read a file",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ path: { type: "string" },
398
+ encoding: { type: "string" },
399
+ maxBytes: { type: "number" },
400
+ binary: { type: "boolean" },
401
+ },
402
+ required: ["path"],
403
+ additionalProperties: false,
404
+ },
405
+ },
406
+ {
407
+ name: "file_write",
408
+ description: "Write a file",
409
+ inputSchema: {
410
+ type: "object",
411
+ properties: {
412
+ path: { type: "string" },
413
+ content: { type: "string" },
414
+ encoding: { type: "string" },
415
+ mode: { type: "string", enum: ["overwrite", "append"] },
416
+ createDirs: { type: "boolean" },
417
+ },
418
+ required: ["path", "content"],
419
+ additionalProperties: false,
420
+ },
421
+ },
422
+ {
423
+ name: "command_run",
424
+ description: "Run a shell command",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ command: { type: "string" },
429
+ cwd: { type: "string" },
430
+ timeoutMs: { type: "number" },
431
+ maxOutputBytes: { type: "number" },
432
+ env: { type: "object" },
433
+ },
434
+ required: ["command"],
435
+ additionalProperties: false,
436
+ },
437
+ },
438
+ {
439
+ name: "tool_install",
440
+ description: "Install a tool using a package manager",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ manager: { type: "string" },
445
+ args: { type: "array", items: { type: "string" } },
446
+ cwd: { type: "string" },
447
+ timeoutMs: { type: "number" },
448
+ env: { type: "object" },
449
+ },
450
+ required: ["manager"],
451
+ additionalProperties: false,
452
+ },
453
+ },
454
+ {
455
+ name: "log_append",
456
+ description: "Append a log entry",
457
+ inputSchema: {
458
+ type: "object",
459
+ properties: {
460
+ event: { type: "string" },
461
+ payload: { type: "object" },
462
+ },
463
+ required: ["event"],
464
+ additionalProperties: false,
465
+ },
466
+ },
467
+ {
468
+ name: "coordination_init",
469
+ description: "Initialize coordination session. Call this first to set up your role (planner or implementer). Returns guidance based on your role and current project state.",
470
+ inputSchema: {
471
+ type: "object",
472
+ properties: {
473
+ role: { type: "string", enum: ["planner", "implementer"] },
474
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
475
+ },
476
+ required: ["role"],
477
+ additionalProperties: false,
478
+ },
479
+ },
480
+ {
481
+ name: "project_context_set",
482
+ description: "Store project context (description, goals, tech stack, acceptance criteria, tests, implementation plan). Called by planner to define what the project is about.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
487
+ description: { type: "string", description: "What is this project?" },
488
+ endState: { type: "string", description: "What is the desired end state/goal?" },
489
+ techStack: { type: "array", items: { type: "string" }, description: "Technologies being used" },
490
+ constraints: { type: "array", items: { type: "string" }, description: "Any constraints or requirements" },
491
+ acceptanceCriteria: { type: "array", items: { type: "string" }, description: "Acceptance criteria that must be met" },
492
+ tests: { type: "array", items: { type: "string" }, description: "Tests that should pass when complete" },
493
+ implementationPlan: { type: "array", items: { type: "string" }, description: "High-level implementation steps/phases" },
494
+ preferredImplementer: { type: "string", enum: ["claude", "codex"], description: "Which agent type to use for implementers" },
495
+ status: { type: "string", enum: ["planning", "ready", "in_progress", "complete", "stopped"], description: "Project status" },
496
+ },
497
+ required: ["description", "endState"],
498
+ additionalProperties: false,
499
+ },
500
+ },
501
+ {
502
+ name: "project_context_get",
503
+ description: "Get stored project context. Returns the project description, goals, and other context set by the planner.",
504
+ inputSchema: {
505
+ type: "object",
506
+ properties: {
507
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
508
+ },
509
+ additionalProperties: false,
510
+ },
511
+ },
512
+ {
513
+ name: "project_status_set",
514
+ description: "Set the project status. Use 'stopped' to signal all implementers to stop working.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
519
+ status: { type: "string", enum: ["planning", "ready", "in_progress", "complete", "stopped"], description: "New project status" },
520
+ },
521
+ required: ["status"],
522
+ additionalProperties: false,
523
+ },
524
+ },
525
+ {
526
+ name: "launch_implementer",
527
+ description: "Launch a new implementer agent (Claude or Codex) in a new terminal window. The planner uses this to spawn workers. Set isolation='worktree' to give the implementer its own git worktree for isolated changes.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ type: { type: "string", enum: ["claude", "codex"], description: "Type of agent to launch" },
532
+ name: { type: "string", description: "Name for this implementer (e.g., 'impl-1'). If not provided, auto-generates as 'impl-N'" },
533
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
534
+ isolation: { type: "string", enum: ["shared", "worktree"], description: "shared=work in main directory, worktree=create isolated git worktree. Default: shared" },
535
+ },
536
+ required: ["type"],
537
+ additionalProperties: false,
538
+ },
539
+ },
540
+ {
541
+ name: "implementer_list",
542
+ description: "List all registered implementers and their status.",
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {
546
+ projectRoot: { type: "string", description: "Filter by project root (optional)" },
547
+ },
548
+ additionalProperties: false,
549
+ },
550
+ },
551
+ {
552
+ name: "implementer_reset",
553
+ description: "Reset all implementers to 'stopped' status. Use this when starting a fresh session or when implementers are stale/not actually running.",
554
+ inputSchema: {
555
+ type: "object",
556
+ properties: {
557
+ projectRoot: { type: "string", description: "Project root to reset implementers for (defaults to first configured root)" },
558
+ },
559
+ additionalProperties: false,
560
+ },
561
+ },
562
+ {
563
+ name: "session_reset",
564
+ description: "PLANNER ONLY: Reset the coordination session for a fresh start. Clears all tasks, locks, notes, and archives discussions. Use this when starting a new project or when data from previous sessions is cluttering the dashboard.",
565
+ inputSchema: {
566
+ type: "object",
567
+ properties: {
568
+ projectRoot: { type: "string", description: "Project root (defaults to first configured root)" },
569
+ keepProjectContext: { type: "boolean", description: "If true, keeps the project description/goals but resets status to 'planning'. Default: false (clears everything)" },
570
+ confirm: { type: "boolean", description: "Must be true to confirm the reset. This prevents accidental resets." },
571
+ },
572
+ required: ["confirm"],
573
+ additionalProperties: false,
574
+ },
575
+ },
576
+ {
577
+ name: "dashboard_open",
578
+ description: "Open the lockstep dashboard in a browser. Call this to monitor progress visually.",
579
+ inputSchema: {
580
+ type: "object",
581
+ properties: {
582
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
583
+ },
584
+ additionalProperties: false,
585
+ },
586
+ },
587
+ // Discussion tools
588
+ {
589
+ name: "discussion_start",
590
+ description: "Start a new discussion thread. Use this when you need input from other agents or want to discuss an architectural/implementation decision.",
591
+ inputSchema: {
592
+ type: "object",
593
+ properties: {
594
+ topic: { type: "string", description: "Topic of the discussion (e.g., 'Database choice for user storage')" },
595
+ category: { type: "string", enum: ["architecture", "implementation", "blocker", "question", "other"], description: "Category of discussion" },
596
+ priority: { type: "string", enum: ["low", "medium", "high", "blocking"], description: "Priority level" },
597
+ message: { type: "string", description: "Initial message explaining the topic and your thoughts" },
598
+ author: { type: "string", description: "Your agent name (e.g., 'planner', 'impl-1')" },
599
+ waitingOn: { type: "string", description: "Which agent should respond (optional)" },
600
+ projectRoot: { type: "string", description: "Project root (defaults to first configured root)" },
601
+ },
602
+ required: ["topic", "message", "author"],
603
+ additionalProperties: false,
604
+ },
605
+ },
606
+ {
607
+ name: "discussion_reply",
608
+ description: "Reply to an existing discussion thread.",
609
+ inputSchema: {
610
+ type: "object",
611
+ properties: {
612
+ discussionId: { type: "string", description: "ID of the discussion to reply to" },
613
+ message: { type: "string", description: "Your reply message" },
614
+ author: { type: "string", description: "Your agent name" },
615
+ recommendation: { type: "string", description: "Your recommendation/vote if applicable" },
616
+ waitingOn: { type: "string", description: "Which agent should respond next (optional)" },
617
+ },
618
+ required: ["discussionId", "message", "author"],
619
+ additionalProperties: false,
620
+ },
621
+ },
622
+ {
623
+ name: "discussion_resolve",
624
+ description: "Resolve a discussion with a final decision. Creates an auditable record of the decision.",
625
+ inputSchema: {
626
+ type: "object",
627
+ properties: {
628
+ discussionId: { type: "string", description: "ID of the discussion to resolve" },
629
+ decision: { type: "string", description: "The final decision" },
630
+ reasoning: { type: "string", description: "Why this decision was made" },
631
+ decidedBy: { type: "string", description: "Who made the final decision" },
632
+ linkedTaskId: { type: "string", description: "Optional task ID spawned from this decision" },
633
+ },
634
+ required: ["discussionId", "decision", "reasoning", "decidedBy"],
635
+ additionalProperties: false,
636
+ },
637
+ },
638
+ {
639
+ name: "discussion_get",
640
+ description: "Get a discussion thread with all its messages.",
641
+ inputSchema: {
642
+ type: "object",
643
+ properties: {
644
+ discussionId: { type: "string", description: "ID of the discussion" },
645
+ },
646
+ required: ["discussionId"],
647
+ additionalProperties: false,
648
+ },
649
+ },
650
+ {
651
+ name: "discussion_list",
652
+ description: "List discussions. Use this to check for discussions waiting on you.",
653
+ inputSchema: {
654
+ type: "object",
655
+ properties: {
656
+ status: { type: "string", enum: ["open", "waiting", "resolved", "archived"], description: "Filter by status" },
657
+ category: { type: "string", enum: ["architecture", "implementation", "blocker", "question", "other"], description: "Filter by category" },
658
+ waitingOn: { type: "string", description: "Filter by who is expected to respond" },
659
+ projectRoot: { type: "string", description: "Filter by project" },
660
+ limit: { type: "number", description: "Max results to return" },
661
+ },
662
+ additionalProperties: false,
663
+ },
664
+ },
665
+ {
666
+ name: "discussion_inbox",
667
+ description: "Check for discussions waiting on you. Call this between tasks to see if anyone needs your input.",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ agent: { type: "string", description: "Your agent name to check inbox for" },
672
+ projectRoot: { type: "string", description: "Filter by project" },
673
+ },
674
+ required: ["agent"],
675
+ additionalProperties: false,
676
+ },
677
+ },
678
+ {
679
+ name: "discussion_archive",
680
+ description: "Archive a resolved discussion. Archived discussions can be deleted later.",
681
+ inputSchema: {
682
+ type: "object",
683
+ properties: {
684
+ discussionId: { type: "string", description: "ID of the discussion to archive" },
685
+ },
686
+ required: ["discussionId"],
687
+ additionalProperties: false,
688
+ },
689
+ },
690
+ {
691
+ name: "discussion_cleanup",
692
+ description: "Archive old resolved discussions and optionally delete old archived ones. Use for maintenance.",
693
+ inputSchema: {
694
+ type: "object",
695
+ properties: {
696
+ archiveOlderThanDays: { type: "number", description: "Archive resolved discussions older than X days (default: 7)" },
697
+ deleteOlderThanDays: { type: "number", description: "Delete archived discussions older than X days (default: 30)" },
698
+ projectRoot: { type: "string", description: "Limit to specific project" },
699
+ },
700
+ additionalProperties: false,
701
+ },
702
+ },
703
+ // Worktree tools
704
+ {
705
+ name: "worktree_status",
706
+ description: "Get the status of a worktree including commits ahead/behind main, modified files, and untracked files. Use this to check an implementer's progress before merging.",
707
+ inputSchema: {
708
+ type: "object",
709
+ properties: {
710
+ implementerId: { type: "string", description: "Implementer ID to check worktree status for" },
711
+ },
712
+ required: ["implementerId"],
713
+ additionalProperties: false,
714
+ },
715
+ },
716
+ {
717
+ name: "worktree_merge",
718
+ description: "Merge an implementer's worktree changes back to main. This should be called after a task is approved. If there are conflicts, returns conflict information for manual resolution.",
719
+ inputSchema: {
720
+ type: "object",
721
+ properties: {
722
+ implementerId: { type: "string", description: "Implementer ID whose worktree to merge" },
723
+ targetBranch: { type: "string", description: "Branch to merge into (default: main or master)" },
724
+ },
725
+ required: ["implementerId"],
726
+ additionalProperties: false,
727
+ },
728
+ },
729
+ {
730
+ name: "worktree_list",
731
+ description: "List all active lockstep worktrees in the project.",
732
+ inputSchema: {
733
+ type: "object",
734
+ properties: {
735
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
736
+ },
737
+ additionalProperties: false,
738
+ },
739
+ },
740
+ {
741
+ name: "worktree_cleanup",
742
+ description: "Clean up orphaned worktrees that no longer have active implementers. Call this during maintenance.",
743
+ inputSchema: {
744
+ type: "object",
745
+ properties: {
746
+ projectRoot: { type: "string", description: "Project root path (defaults to first configured root)" },
747
+ },
748
+ additionalProperties: false,
749
+ },
750
+ },
751
+ ];
752
+ const server = new Server({ name: config.serverName, version: config.serverVersion }, { capabilities: { tools: {} } });
753
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
754
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
755
+ try {
756
+ const { name, arguments: rawArgs } = request.params;
757
+ const args = (rawArgs ?? {});
758
+ switch (name) {
759
+ case "status_get": {
760
+ const state = await store.status();
761
+ return jsonResponse({
762
+ config: {
763
+ mode: config.mode,
764
+ roots: config.roots,
765
+ dataDir: config.dataDir,
766
+ logDir: config.logDir,
767
+ storage: config.storage,
768
+ dbPath: config.dbPath,
769
+ command: config.command,
770
+ },
771
+ stateSummary: {
772
+ tasks: state.tasks.length,
773
+ locks: state.locks.length,
774
+ notes: state.notes.length,
775
+ },
776
+ });
777
+ }
778
+ case "task_create": {
779
+ const title = getString(args.title);
780
+ const complexity = getString(args.complexity);
781
+ if (!title)
782
+ throw new Error("title is required");
783
+ if (!complexity)
784
+ throw new Error("complexity is required (simple/medium/complex/critical)");
785
+ const isolation = getString(args.isolation);
786
+ const task = await store.createTask({
787
+ title,
788
+ description: getString(args.description),
789
+ status: getString(args.status),
790
+ complexity,
791
+ isolation: isolation ?? "shared",
792
+ owner: getString(args.owner),
793
+ tags: getStringArray(args.tags),
794
+ metadata: getObject(args.metadata),
795
+ });
796
+ return jsonResponse(task);
797
+ }
798
+ case "task_claim": {
799
+ const id = getString(args.id);
800
+ const owner = getString(args.owner);
801
+ if (!id || !owner)
802
+ throw new Error("id and owner are required");
803
+ const task = await store.claimTask({ id, owner });
804
+ // Return complexity-based instructions
805
+ const complexityInstructions = {
806
+ simple: "Simple task - implement and mark done directly.",
807
+ medium: "Medium task - implement carefully, submit_for_review when complete.",
808
+ complex: "Complex task - discuss approach first if unclear, get planner approval via submit_for_review.",
809
+ critical: "CRITICAL task - discuss approach with planner BEFORE starting, get approval at each step."
810
+ };
811
+ return jsonResponse({
812
+ ...task,
813
+ _instruction: complexityInstructions[task.complexity] ?? complexityInstructions.medium
814
+ });
815
+ }
816
+ case "task_update": {
817
+ const id = getString(args.id);
818
+ if (!id)
819
+ throw new Error("id is required");
820
+ const newStatus = getString(args.status);
821
+ const task = await store.updateTask({
822
+ id,
823
+ title: getString(args.title),
824
+ description: getString(args.description),
825
+ status: newStatus,
826
+ complexity: getString(args.complexity),
827
+ owner: getString(args.owner),
828
+ tags: getStringArray(args.tags),
829
+ metadata: getObject(args.metadata),
830
+ });
831
+ // Check if all tasks are now complete
832
+ if (newStatus === "done") {
833
+ const todoTasks = await store.listTasks({ status: "todo" });
834
+ const inProgressTasks = await store.listTasks({ status: "in_progress" });
835
+ const reviewTasks = await store.listTasks({ status: "review" });
836
+ if (todoTasks.length === 0 && inProgressTasks.length === 0 && reviewTasks.length === 0) {
837
+ // All tasks complete - notify planner
838
+ await store.appendNote({
839
+ text: "[SYSTEM] ALL TASKS COMPLETE! Planner: review the work and call project_status_set({ status: 'complete' }) if satisfied, or create more tasks.",
840
+ author: "system"
841
+ });
842
+ }
843
+ }
844
+ return jsonResponse(task);
845
+ }
846
+ case "task_submit_for_review": {
847
+ const id = getString(args.id);
848
+ const owner = getString(args.owner);
849
+ const reviewNotes = getString(args.reviewNotes);
850
+ if (!id || !owner || !reviewNotes) {
851
+ throw new Error("id, owner, and reviewNotes are required");
852
+ }
853
+ const task = await store.submitTaskForReview({ id, owner, reviewNotes });
854
+ // Notify planner
855
+ await store.appendNote({
856
+ text: `[REVIEW] Task "${task.title}" submitted for review by ${owner}. Planner: use task_approve or task_request_changes.`,
857
+ author: "system"
858
+ });
859
+ return jsonResponse({
860
+ ...task,
861
+ _instruction: "Task submitted for planner review. Continue with other tasks while waiting."
862
+ });
863
+ }
864
+ case "task_approve": {
865
+ const id = getString(args.id);
866
+ if (!id)
867
+ throw new Error("id is required");
868
+ const feedback = getString(args.feedback);
869
+ const task = await store.approveTask({ id, feedback });
870
+ // Notify implementer
871
+ await store.appendNote({
872
+ text: `[APPROVED] Task "${task.title}" approved by planner.${feedback ? ` Feedback: ${feedback}` : ""}`,
873
+ author: "system"
874
+ });
875
+ // Check if all tasks are now complete
876
+ const todoTasks = await store.listTasks({ status: "todo" });
877
+ const inProgressTasks = await store.listTasks({ status: "in_progress" });
878
+ const reviewTasks = await store.listTasks({ status: "review" });
879
+ if (todoTasks.length === 0 && inProgressTasks.length === 0 && reviewTasks.length === 0) {
880
+ await store.appendNote({
881
+ text: "[SYSTEM] ALL TASKS COMPLETE! Planner: review the work and call project_status_set({ status: 'complete' }) if satisfied, or create more tasks.",
882
+ author: "system"
883
+ });
884
+ }
885
+ return jsonResponse(task);
886
+ }
887
+ case "task_request_changes": {
888
+ const id = getString(args.id);
889
+ const feedback = getString(args.feedback);
890
+ if (!id || !feedback)
891
+ throw new Error("id and feedback are required");
892
+ const task = await store.requestTaskChanges({ id, feedback });
893
+ // Notify implementer
894
+ await store.appendNote({
895
+ text: `[CHANGES REQUESTED] Task "${task.title}" needs changes: ${feedback}`,
896
+ author: "system"
897
+ });
898
+ return jsonResponse({
899
+ ...task,
900
+ _instruction: `Changes requested by planner: ${feedback}. Task returned to in_progress.`
901
+ });
902
+ }
903
+ case "task_approve_batch": {
904
+ const ids = getStringArray(args.ids);
905
+ if (!ids || ids.length === 0)
906
+ throw new Error("ids array is required and must not be empty");
907
+ const feedback = getString(args.feedback);
908
+ const results = [];
909
+ for (const id of ids) {
910
+ try {
911
+ const task = await store.approveTask({ id, feedback });
912
+ results.push({ id, success: true, title: task.title });
913
+ // Notify for each task
914
+ await store.appendNote({
915
+ text: `[APPROVED] Task "${task.title}" approved by planner.${feedback ? ` Feedback: ${feedback}` : ""}`,
916
+ author: "system"
917
+ });
918
+ }
919
+ catch (error) {
920
+ const message = error instanceof Error ? error.message : "Unknown error";
921
+ results.push({ id, success: false, error: message });
922
+ }
923
+ }
924
+ // Check if all tasks are now complete
925
+ const todoTasks = await store.listTasks({ status: "todo" });
926
+ const inProgressTasks = await store.listTasks({ status: "in_progress" });
927
+ const reviewTasks = await store.listTasks({ status: "review" });
928
+ if (todoTasks.length === 0 && inProgressTasks.length === 0 && reviewTasks.length === 0) {
929
+ await store.appendNote({
930
+ text: "[SYSTEM] ALL TASKS COMPLETE! Planner: review the work and call project_status_set({ status: 'complete' }) if satisfied, or create more tasks.",
931
+ author: "system"
932
+ });
933
+ }
934
+ const successCount = results.filter(r => r.success).length;
935
+ return jsonResponse({
936
+ total: ids.length,
937
+ approved: successCount,
938
+ failed: ids.length - successCount,
939
+ results,
940
+ _instruction: successCount === ids.length
941
+ ? `All ${successCount} tasks approved successfully.`
942
+ : `Approved ${successCount}/${ids.length} tasks. Check results for failures.`
943
+ });
944
+ }
945
+ case "task_summary": {
946
+ const allTasks = await store.listTasks({});
947
+ const summary = {
948
+ total: allTasks.length,
949
+ todo: allTasks.filter(t => t.status === "todo").length,
950
+ in_progress: allTasks.filter(t => t.status === "in_progress").length,
951
+ blocked: allTasks.filter(t => t.status === "blocked").length,
952
+ review: allTasks.filter(t => t.status === "review").length,
953
+ done: allTasks.filter(t => t.status === "done").length,
954
+ };
955
+ // Calculate completion percentage
956
+ const completionPercent = summary.total > 0
957
+ ? Math.round((summary.done / summary.total) * 100)
958
+ : 0;
959
+ // Get project status
960
+ const projectRoot = config.roots[0] ?? process.cwd();
961
+ const context = await store.getProjectContext(projectRoot);
962
+ return jsonResponse({
963
+ summary,
964
+ completionPercent,
965
+ projectStatus: context?.status ?? "unknown",
966
+ _hint: summary.review > 0
967
+ ? `${summary.review} task(s) pending review - use task_list({ status: "review" }) to see them`
968
+ : summary.todo === 0 && summary.in_progress === 0 && summary.review === 0 && summary.total > 0
969
+ ? "All tasks complete!"
970
+ : undefined
971
+ });
972
+ }
973
+ case "task_list": {
974
+ const statusFilter = getString(args.status);
975
+ const includeDone = getBoolean(args.includeDone) ?? false;
976
+ const offset = getNumber(args.offset) ?? 0;
977
+ const limit = getNumber(args.limit);
978
+ let tasks = await store.listTasks({
979
+ status: statusFilter,
980
+ owner: getString(args.owner),
981
+ tag: getString(args.tag),
982
+ limit: undefined, // We'll handle pagination ourselves
983
+ });
984
+ // If no specific status filter and includeDone is false, exclude done tasks for smaller responses
985
+ if (!statusFilter && !includeDone) {
986
+ tasks = tasks.filter(t => t.status !== "done");
987
+ }
988
+ // Apply pagination
989
+ const totalBeforePagination = tasks.length;
990
+ if (offset > 0) {
991
+ tasks = tasks.slice(offset);
992
+ }
993
+ if (limit !== undefined && limit > 0) {
994
+ tasks = tasks.slice(0, limit);
995
+ }
996
+ // Include project status so implementers can check if they should stop
997
+ const projectRoot = config.roots[0] ?? process.cwd();
998
+ const context = await store.getProjectContext(projectRoot);
999
+ // Get counts for summary
1000
+ const allTasks = await store.listTasks({});
1001
+ const doneTasks = allTasks.filter(t => t.status === "done").length;
1002
+ return jsonResponse({
1003
+ tasks,
1004
+ total: totalBeforePagination,
1005
+ offset,
1006
+ hasMore: offset + tasks.length < totalBeforePagination,
1007
+ doneCount: doneTasks,
1008
+ projectStatus: context?.status ?? "unknown",
1009
+ _hint: context?.status === "stopped" ? "PROJECT STOPPED - cease work immediately" :
1010
+ context?.status === "complete" ? "PROJECT COMPLETE - no more work needed" :
1011
+ (!includeDone && doneTasks > 0) ? `${doneTasks} done task(s) hidden. Use includeDone=true or task_summary to see counts.` : undefined
1012
+ });
1013
+ }
1014
+ case "lock_acquire": {
1015
+ const pathValue = getString(args.path);
1016
+ if (!pathValue)
1017
+ throw new Error("path is required");
1018
+ const lock = await store.acquireLock({
1019
+ path: pathValue,
1020
+ owner: getString(args.owner),
1021
+ note: getString(args.note),
1022
+ });
1023
+ return jsonResponse(lock);
1024
+ }
1025
+ case "lock_release": {
1026
+ const pathValue = getString(args.path);
1027
+ if (!pathValue)
1028
+ throw new Error("path is required");
1029
+ const lock = await store.releaseLock({ path: pathValue, owner: getString(args.owner) });
1030
+ return jsonResponse(lock);
1031
+ }
1032
+ case "lock_list": {
1033
+ const locks = await store.listLocks({
1034
+ status: getString(args.status),
1035
+ owner: getString(args.owner),
1036
+ });
1037
+ return jsonResponse(locks);
1038
+ }
1039
+ case "note_append": {
1040
+ const text = getString(args.text);
1041
+ if (!text)
1042
+ throw new Error("text is required");
1043
+ const note = await store.appendNote({ text, author: getString(args.author) });
1044
+ return jsonResponse(note);
1045
+ }
1046
+ case "note_list": {
1047
+ const notes = await store.listNotes(getNumber(args.limit));
1048
+ return jsonResponse(notes);
1049
+ }
1050
+ case "artifact_read": {
1051
+ const filePath = getString(args.path);
1052
+ if (!filePath)
1053
+ throw new Error("path is required");
1054
+ const result = await readFileSafe(filePath, {
1055
+ encoding: getString(args.encoding),
1056
+ maxBytes: getNumber(args.maxBytes),
1057
+ binary: getBoolean(args.binary),
1058
+ });
1059
+ return jsonResponse(result);
1060
+ }
1061
+ case "artifact_write": {
1062
+ const filePath = getString(args.path);
1063
+ const content = getString(args.content);
1064
+ if (!filePath || content === undefined)
1065
+ throw new Error("path and content are required");
1066
+ const result = await writeFileSafe(filePath, content, {
1067
+ encoding: getString(args.encoding),
1068
+ mode: getString(args.mode),
1069
+ createDirs: getBoolean(args.createDirs),
1070
+ });
1071
+ return jsonResponse(result);
1072
+ }
1073
+ case "file_read": {
1074
+ const filePath = getString(args.path);
1075
+ if (!filePath)
1076
+ throw new Error("path is required");
1077
+ const result = await readFileSafe(filePath, {
1078
+ encoding: getString(args.encoding),
1079
+ maxBytes: getNumber(args.maxBytes),
1080
+ binary: getBoolean(args.binary),
1081
+ });
1082
+ return jsonResponse(result);
1083
+ }
1084
+ case "file_write": {
1085
+ const filePath = getString(args.path);
1086
+ const content = getString(args.content);
1087
+ if (!filePath || content === undefined)
1088
+ throw new Error("path and content are required");
1089
+ const result = await writeFileSafe(filePath, content, {
1090
+ encoding: getString(args.encoding),
1091
+ mode: getString(args.mode),
1092
+ createDirs: getBoolean(args.createDirs),
1093
+ });
1094
+ return jsonResponse(result);
1095
+ }
1096
+ case "command_run": {
1097
+ const command = getString(args.command);
1098
+ if (!command)
1099
+ throw new Error("command is required");
1100
+ const result = await runCommand(command, {
1101
+ cwd: getString(args.cwd),
1102
+ timeoutMs: getNumber(args.timeoutMs),
1103
+ maxOutputBytes: getNumber(args.maxOutputBytes),
1104
+ env: getObject(args.env),
1105
+ });
1106
+ return jsonResponse(result);
1107
+ }
1108
+ case "tool_install": {
1109
+ const manager = getString(args.manager);
1110
+ if (!manager)
1111
+ throw new Error("manager is required");
1112
+ const installArgs = getStringArray(args.args) ?? [];
1113
+ const command = [manager, ...installArgs].join(" ");
1114
+ const result = await runCommand(command, {
1115
+ cwd: getString(args.cwd),
1116
+ timeoutMs: getNumber(args.timeoutMs),
1117
+ env: getObject(args.env),
1118
+ });
1119
+ return jsonResponse(result);
1120
+ }
1121
+ case "log_append": {
1122
+ const event = getString(args.event);
1123
+ if (!event)
1124
+ throw new Error("event is required");
1125
+ await store.appendLogEntry(event, getObject(args.payload));
1126
+ return jsonResponse({ ok: true });
1127
+ }
1128
+ case "coordination_init": {
1129
+ const role = getString(args.role);
1130
+ if (!role || (role !== "planner" && role !== "implementer")) {
1131
+ throw new Error("role must be 'planner' or 'implementer'");
1132
+ }
1133
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1134
+ const context = await store.getProjectContext(projectRoot);
1135
+ const tasks = await store.listTasks({ status: "todo" });
1136
+ const inProgressTasks = await store.listTasks({ status: "in_progress" });
1137
+ const doneTasks = await store.listTasks({ status: "done" });
1138
+ if (role === "planner") {
1139
+ // Phase 1: No project context - need to gather information
1140
+ if (!context) {
1141
+ return jsonResponse({
1142
+ role: "planner",
1143
+ status: "needs_context",
1144
+ phase: "gather_info",
1145
+ message: "No project context found. Follow these steps IN ORDER:",
1146
+ steps: [
1147
+ "1. If user already said what to work on, acknowledge it. Otherwise ASK.",
1148
+ "2. EXPLORE: Scan README.md, package.json, etc. to understand the codebase",
1149
+ "3. SUMMARIZE: Tell the user what you found",
1150
+ "4. ⛔ MANDATORY - ASK these questions and WAIT for answers:",
1151
+ " - What is the desired END STATE? (What does 'done' look like?)",
1152
+ " - What are your ACCEPTANCE CRITERIA?",
1153
+ " - Any CONSTRAINTS I should know about?",
1154
+ " - Should I use CLAUDE or CODEX as implementer?",
1155
+ "5. ONLY AFTER user answers: Call project_context_set"
1156
+ ],
1157
+ instruction: `CRITICAL: You MUST ask the user these questions and WAIT for their answers before proceeding.
1158
+
1159
+ Step 1: Explore the codebase (README, package.json, etc.)
1160
+
1161
+ Step 2: Summarize what you found to the user
1162
+
1163
+ Step 3: ⛔ STOP AND ASK - These questions are MANDATORY (do not skip or infer):
1164
+ "Before I create a plan, I need your input on a few things:
1165
+
1166
+ 1. What is the END STATE you want? What does 'done' look like?
1167
+ 2. What are your ACCEPTANCE CRITERIA? How will we know it's complete?
1168
+ 3. Are there any CONSTRAINTS or things I should avoid?
1169
+ 4. Should I use CLAUDE or CODEX as the implementer?"
1170
+
1171
+ Step 4: WAIT for the user to answer
1172
+
1173
+ Step 5: Only AFTER getting answers, call project_context_set
1174
+
1175
+ DO NOT skip the questions. DO NOT infer the answers. The user must explicitly tell you.`
1176
+ });
1177
+ }
1178
+ // Phase 2: Have context but no implementation plan
1179
+ if (!context.implementationPlan?.length) {
1180
+ return jsonResponse({
1181
+ role: "planner",
1182
+ status: "needs_plan",
1183
+ phase: "create_plan",
1184
+ projectContext: context,
1185
+ message: "Project context exists. Now create and review the implementation plan WITH THE USER.",
1186
+ instruction: `Based on the project context, create a detailed implementation plan. Then BEFORE saving it:
1187
+
1188
+ 1. EXPLAIN THE PLAN to the user:
1189
+ - Present each step/phase clearly
1190
+ - Explain your reasoning for the approach
1191
+ - Mention any trade-offs or decisions you made
1192
+
1193
+ 2. ASK FOR FEEDBACK:
1194
+ - "Is there any additional context I should know?"
1195
+ - "Do you want me to change or add anything to this plan?"
1196
+ - "Any specific instructions or preferences for implementation?"
1197
+
1198
+ 3. ASK FOR PERMISSION:
1199
+ - "Do I have your permission to proceed with implementation?"
1200
+
1201
+ 4. ONLY AFTER user approves:
1202
+ - Call project_context_set with the implementationPlan array
1203
+ - Set status to 'ready'
1204
+
1205
+ DO NOT proceed to implementation without explicit user approval.`
1206
+ });
1207
+ }
1208
+ // Phase 3: Have plan but no tasks created
1209
+ if (tasks.length === 0 && inProgressTasks.length === 0 && doneTasks.length === 0) {
1210
+ const implType = context.preferredImplementer ?? "codex";
1211
+ return jsonResponse({
1212
+ role: "planner",
1213
+ status: "needs_tasks",
1214
+ phase: "create_tasks",
1215
+ projectContext: context,
1216
+ preferredImplementer: implType,
1217
+ message: "Implementation plan exists. Now create tasks from the plan.",
1218
+ instruction: `Create tasks using task_create based on the implementation plan. Each task should be specific and actionable. After creating tasks, use launch_implementer with type="${implType}" to spawn workers. Recommend 1-2 implementers for simple projects, more for complex ones (but avoid too many to prevent conflicts).`
1219
+ });
1220
+ }
1221
+ // Phase 4: Tasks exist - monitor progress
1222
+ const implementers = await store.listImplementers(projectRoot);
1223
+ const activeImplementers = implementers.filter(i => i.status === "active");
1224
+ const implType = context.preferredImplementer ?? "codex";
1225
+ return jsonResponse({
1226
+ role: "planner",
1227
+ status: "monitoring",
1228
+ phase: "monitor",
1229
+ projectContext: context,
1230
+ preferredImplementer: implType,
1231
+ taskSummary: {
1232
+ todo: tasks.length,
1233
+ inProgress: inProgressTasks.length,
1234
+ done: doneTasks.length
1235
+ },
1236
+ implementers: {
1237
+ total: implementers.length,
1238
+ active: activeImplementers.length
1239
+ },
1240
+ instruction: tasks.length === 0 && inProgressTasks.length === 0
1241
+ ? "All tasks complete! Ask the user to verify the work. If satisfied, call project_status_set with status 'complete'. Otherwise create more tasks."
1242
+ : `FIRST STEPS (do these IN ORDER):
1243
+ 1. Call dashboard_open to launch the monitoring dashboard
1244
+ 2. Call implementer_reset to clear stale implementers from previous sessions
1245
+ 3. ASK THE USER: "Should I use Claude or Codex as the implementer?"
1246
+ 4. WAIT for their answer before launching any implementers
1247
+ 5. After user answers, call launch_implementer with their chosen type
1248
+
1249
+ ${activeImplementers.length === 0
1250
+ ? `⚠️ NO ACTIVE IMPLEMENTERS - but ASK USER first which type they want before launching!`
1251
+ : `Active implementers: ${activeImplementers.length}. If they seem stale (not responding), call implementer_reset first.`}
1252
+
1253
+ ⛔ CRITICAL REMINDERS:
1254
+ - You MUST ask the user about implementer type before launching
1255
+ - You are PROHIBITED from writing code or running builds
1256
+ - DO NOT assume or infer the user's preferences - ASK THEM
1257
+
1258
+ Your allowed actions:
1259
+ 1. dashboard_open - Open monitoring dashboard
1260
+ 2. implementer_reset - Clear stale implementers
1261
+ 3. ASK user which implementer type they want (claude or codex)
1262
+ 4. launch_implementer - ONLY after user tells you which type
1263
+ 5. task_list, note_list - Monitor progress
1264
+ 6. task_approve, task_request_changes - Review submitted work
1265
+ 7. project_status_set - Mark complete when done`
1266
+ });
1267
+ }
1268
+ else {
1269
+ // IMPLEMENTER ROLE
1270
+ // Check if project is stopped or complete
1271
+ if (context?.status === "stopped") {
1272
+ return jsonResponse({
1273
+ role: "implementer",
1274
+ status: "stopped",
1275
+ message: "Project has been STOPPED by the planner. Cease all work.",
1276
+ instruction: "Stop working on tasks. The planner has halted the project. Wait for further instructions from the user."
1277
+ });
1278
+ }
1279
+ if (context?.status === "complete") {
1280
+ return jsonResponse({
1281
+ role: "implementer",
1282
+ status: "complete",
1283
+ message: "Project is COMPLETE. No more work needed.",
1284
+ instruction: "The project has been marked complete. No further action needed."
1285
+ });
1286
+ }
1287
+ // No tasks available
1288
+ if (tasks.length === 0 && inProgressTasks.length === 0) {
1289
+ return jsonResponse({
1290
+ role: "implementer",
1291
+ status: "waiting",
1292
+ message: "No tasks available yet. Waiting for planner to create tasks.",
1293
+ projectContext: context,
1294
+ instruction: "Wait briefly, then call task_list to check for new tasks. Keep checking periodically. Also check project status - if 'stopped' or 'complete', stop working."
1295
+ });
1296
+ }
1297
+ // Tasks available - work loop
1298
+ return jsonResponse({
1299
+ role: "implementer",
1300
+ status: "ready",
1301
+ projectContext: context,
1302
+ availableTasks: tasks.length,
1303
+ inProgressTasks: inProgressTasks.length,
1304
+ instruction: `CONTINUOUS WORK LOOP:
1305
+ 1. Call task_list to see available tasks
1306
+ 2. Call discussion_inbox({ agent: "YOUR_NAME" }) to check for discussions needing your input
1307
+ 3. If discussions waiting -> respond with discussion_reply before continuing
1308
+ 4. Call task_claim to take a 'todo' task
1309
+ 5. Call lock_acquire before editing any file
1310
+ 6. Do the work
1311
+ 7. Call lock_release when done with file
1312
+ 8. Call task_update to mark task 'done'
1313
+ 9. REPEAT: Go back to step 1 and get the next task
1314
+ 10. STOP CONDITIONS: If project status becomes 'stopped' or 'complete', cease work
1315
+
1316
+ DISCUSSIONS:
1317
+ - Use discussion_start to ask questions about architecture or implementation
1318
+ - Check discussion_inbox between tasks
1319
+ - Respond with discussion_reply including your recommendation
1320
+
1321
+ IMPORTANT: Keep working until all tasks are done or project is stopped. Do not wait for user input between tasks.`
1322
+ });
1323
+ }
1324
+ }
1325
+ case "project_context_set": {
1326
+ const description = getString(args.description);
1327
+ const endState = getString(args.endState);
1328
+ if (!description || !endState) {
1329
+ throw new Error("description and endState are required");
1330
+ }
1331
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1332
+ const statusValue = getString(args.status);
1333
+ const preferredImpl = getString(args.preferredImplementer);
1334
+ const context = await store.setProjectContext({
1335
+ projectRoot,
1336
+ description,
1337
+ endState,
1338
+ techStack: getStringArray(args.techStack),
1339
+ constraints: getStringArray(args.constraints),
1340
+ acceptanceCriteria: getStringArray(args.acceptanceCriteria),
1341
+ tests: getStringArray(args.tests),
1342
+ implementationPlan: getStringArray(args.implementationPlan),
1343
+ preferredImplementer: preferredImpl,
1344
+ status: statusValue,
1345
+ });
1346
+ // Determine next instruction based on what's provided
1347
+ let instruction = "Project context saved.";
1348
+ if (!context.acceptanceCriteria?.length) {
1349
+ instruction += " Consider adding acceptance criteria.";
1350
+ }
1351
+ if (!context.implementationPlan?.length) {
1352
+ instruction += " Create an implementation plan, then create tasks with task_create.";
1353
+ }
1354
+ else {
1355
+ instruction += " Create tasks from the implementation plan with task_create, or use launch_implementer to spawn workers.";
1356
+ }
1357
+ return jsonResponse({
1358
+ success: true,
1359
+ context,
1360
+ instruction
1361
+ });
1362
+ }
1363
+ case "project_context_get": {
1364
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1365
+ const context = await store.getProjectContext(projectRoot);
1366
+ if (!context) {
1367
+ return jsonResponse({
1368
+ found: false,
1369
+ message: "No project context found. The planner needs to set it using project_context_set."
1370
+ });
1371
+ }
1372
+ return jsonResponse({
1373
+ found: true,
1374
+ context
1375
+ });
1376
+ }
1377
+ case "project_status_set": {
1378
+ const status = getString(args.status);
1379
+ if (!status) {
1380
+ throw new Error("status is required");
1381
+ }
1382
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1383
+ const context = await store.updateProjectStatus(projectRoot, status);
1384
+ let message = `Project status updated to '${status}'.`;
1385
+ if (status === "stopped") {
1386
+ message += " All implementers should stop working and check back.";
1387
+ // Add a note to communicate the stop signal
1388
+ await store.appendNote({
1389
+ text: `[SYSTEM] Project status changed to STOPPED. All implementers should cease work.`,
1390
+ author: "system"
1391
+ });
1392
+ }
1393
+ else if (status === "complete") {
1394
+ message += " Project is marked as complete.";
1395
+ await store.appendNote({
1396
+ text: `[SYSTEM] Project marked as COMPLETE. Great work!`,
1397
+ author: "system"
1398
+ });
1399
+ }
1400
+ return jsonResponse({
1401
+ success: true,
1402
+ context,
1403
+ message
1404
+ });
1405
+ }
1406
+ case "launch_implementer": {
1407
+ const type = getString(args.type);
1408
+ if (!type) {
1409
+ throw new Error("type is required");
1410
+ }
1411
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1412
+ // Auto-generate name if not provided
1413
+ let name = getString(args.name);
1414
+ if (!name) {
1415
+ const existingImplementers = await store.listImplementers(projectRoot);
1416
+ // Find the highest impl-N number
1417
+ let maxNum = 0;
1418
+ for (const impl of existingImplementers) {
1419
+ const match = impl.name.match(/^impl-(\d+)$/);
1420
+ if (match) {
1421
+ maxNum = Math.max(maxNum, parseInt(match[1], 10));
1422
+ }
1423
+ }
1424
+ name = `impl-${maxNum + 1}`;
1425
+ }
1426
+ const isolation = getString(args.isolation) ?? "shared";
1427
+ // Check if this is the first implementer - if so, launch dashboard too
1428
+ const existingImplementers = await store.listImplementers(projectRoot);
1429
+ const isFirstImplementer = existingImplementers.filter(i => i.status === "active").length === 0;
1430
+ // Handle worktree creation if isolation is worktree
1431
+ let worktreePath;
1432
+ let branchName;
1433
+ let workingDirectory = projectRoot;
1434
+ if (isolation === "worktree") {
1435
+ // Check if this is a git repo
1436
+ const isGit = await isGitRepo(projectRoot);
1437
+ if (!isGit) {
1438
+ return jsonResponse({
1439
+ success: false,
1440
+ error: "Cannot use worktree isolation: project is not a git repository. Use isolation='shared' instead."
1441
+ });
1442
+ }
1443
+ try {
1444
+ const wtResult = await createWorktree(projectRoot, name);
1445
+ worktreePath = wtResult.worktreePath;
1446
+ branchName = wtResult.branchName;
1447
+ workingDirectory = worktreePath;
1448
+ }
1449
+ catch (error) {
1450
+ const message = error instanceof Error ? error.message : "Unknown error";
1451
+ return jsonResponse({
1452
+ success: false,
1453
+ error: `Failed to create worktree: ${message}. Try isolation='shared' instead.`
1454
+ });
1455
+ }
1456
+ }
1457
+ // Build the prompt that will be injected
1458
+ const worktreeNote = isolation === "worktree" ? ` You are working in an isolated worktree at ${worktreePath}. Your changes are on branch ${branchName}.` : "";
1459
+ const prompt = `You are implementer ${name}.${worktreeNote} Run: coordination_init({ role: "implementer" })`;
1460
+ // Determine the command to run
1461
+ let terminalCmd;
1462
+ if (type === "claude") {
1463
+ // Claude: use --dangerously-skip-permissions for autonomous work
1464
+ // Use -p for initial prompt, which will start an interactive session
1465
+ terminalCmd = `claude --dangerously-skip-permissions "${prompt}"`;
1466
+ }
1467
+ else {
1468
+ // Codex: --full-auto for autonomous work, pass prompt as quoted argument
1469
+ terminalCmd = `codex --full-auto "${prompt}"`;
1470
+ }
1471
+ try {
1472
+ // Helper to escape strings for AppleScript inside shell
1473
+ const escapeForAppleScript = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1474
+ // Launch dashboard first if this is the first implementer
1475
+ if (isFirstImplementer) {
1476
+ const cliPath = path.resolve(__dirname, "cli.js");
1477
+ // Pass --roots so dashboard knows which project to display
1478
+ const dashCmd = `node "${escapeForAppleScript(cliPath)}" dashboard --roots "${escapeForAppleScript(projectRoot)}"`;
1479
+ // Use spawn instead of execSync to avoid blocking/session issues
1480
+ spawn("osascript", ["-e", `tell application "Terminal" to do script "${escapeForAppleScript(dashCmd)}"`], {
1481
+ detached: true,
1482
+ stdio: "ignore"
1483
+ }).unref();
1484
+ // Open browser after a brief delay (in background)
1485
+ spawn("sh", ["-c", "sleep 3 && open http://127.0.0.1:8787"], {
1486
+ detached: true,
1487
+ stdio: "ignore"
1488
+ }).unref();
1489
+ }
1490
+ // Launch the implementer terminal (in worktree directory if applicable)
1491
+ const implCmd = `cd "${escapeForAppleScript(workingDirectory)}" && ${terminalCmd}`;
1492
+ // Use spawn instead of execSync to avoid blocking/session issues
1493
+ spawn("osascript", ["-e", `tell application "Terminal" to do script "${escapeForAppleScript(implCmd)}"`], {
1494
+ detached: true,
1495
+ stdio: "ignore"
1496
+ }).unref();
1497
+ // Register the implementer with worktree info
1498
+ const implementer = await store.registerImplementer({
1499
+ name,
1500
+ type,
1501
+ projectRoot,
1502
+ pid: undefined,
1503
+ isolation,
1504
+ worktreePath,
1505
+ branchName,
1506
+ });
1507
+ // Update project status to in_progress if it was ready
1508
+ const context = await store.getProjectContext(projectRoot);
1509
+ if (context?.status === "ready") {
1510
+ await store.updateProjectStatus(projectRoot, "in_progress");
1511
+ }
1512
+ const worktreeMsg = isolation === "worktree" ? ` with isolated worktree (branch: ${branchName})` : "";
1513
+ await store.appendNote({
1514
+ text: `[SYSTEM] Launched implementer "${name}" (${type})${worktreeMsg}${isFirstImplementer ? " and dashboard" : ""}`,
1515
+ author: "system"
1516
+ });
1517
+ return jsonResponse({
1518
+ success: true,
1519
+ implementer,
1520
+ dashboardLaunched: isFirstImplementer,
1521
+ isolation,
1522
+ worktreePath,
1523
+ branchName,
1524
+ message: `Launched ${type} implementer "${name}"${worktreeMsg} in a new terminal window.${isFirstImplementer ? " Dashboard also launched at http://127.0.0.1:8787" : ""}`
1525
+ });
1526
+ }
1527
+ catch (error) {
1528
+ // Clean up worktree if launch failed
1529
+ if (worktreePath) {
1530
+ try {
1531
+ await removeWorktree(worktreePath);
1532
+ }
1533
+ catch {
1534
+ // Ignore cleanup errors
1535
+ }
1536
+ }
1537
+ const message = error instanceof Error ? error.message : "Unknown error";
1538
+ return jsonResponse({
1539
+ success: false,
1540
+ error: `Failed to launch implementer: ${message}. You may need to launch manually: cd '${workingDirectory}' && ${terminalCmd}`
1541
+ });
1542
+ }
1543
+ }
1544
+ case "implementer_list": {
1545
+ const projectRoot = getString(args.projectRoot);
1546
+ const implementers = await store.listImplementers(projectRoot);
1547
+ const active = implementers.filter(i => i.status === "active");
1548
+ return jsonResponse({
1549
+ total: implementers.length,
1550
+ active: active.length,
1551
+ implementers
1552
+ });
1553
+ }
1554
+ case "implementer_reset": {
1555
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1556
+ const count = await store.resetImplementers(projectRoot);
1557
+ await store.appendNote({
1558
+ text: `[SYSTEM] Reset ${count} implementer(s) to stopped status for fresh session`,
1559
+ author: "system"
1560
+ });
1561
+ return jsonResponse({
1562
+ success: true,
1563
+ resetCount: count,
1564
+ message: `Reset ${count} implementer(s) to stopped status. You can now launch fresh implementers.`
1565
+ });
1566
+ }
1567
+ case "session_reset": {
1568
+ const confirm = getBoolean(args.confirm);
1569
+ if (!confirm) {
1570
+ return jsonResponse({
1571
+ success: false,
1572
+ error: "Session reset requires confirm: true to proceed. This will clear all tasks, locks, notes, and archive discussions."
1573
+ });
1574
+ }
1575
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1576
+ const keepProjectContext = getBoolean(args.keepProjectContext) ?? false;
1577
+ const result = await store.resetSession(projectRoot, { keepProjectContext });
1578
+ return jsonResponse({
1579
+ success: true,
1580
+ ...result,
1581
+ message: `Session reset complete. Cleared ${result.tasksCleared} tasks, ${result.locksCleared} locks, ${result.notesCleared} notes. Reset ${result.implementersReset} implementers, archived ${result.discussionsArchived} discussions.${keepProjectContext ? " Project context preserved (status reset to planning)." : " Project context cleared."}`,
1582
+ nextSteps: [
1583
+ "1. Call coordination_init({ role: 'planner' }) to start fresh",
1584
+ "2. Set up project context with project_context_set",
1585
+ "3. Create tasks and launch implementers"
1586
+ ]
1587
+ });
1588
+ }
1589
+ case "dashboard_open": {
1590
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1591
+ try {
1592
+ const escapeForAppleScript = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1593
+ const cliPath = path.resolve(__dirname, "cli.js");
1594
+ const dashCmd = `node "${escapeForAppleScript(cliPath)}" dashboard --roots "${escapeForAppleScript(projectRoot)}"`;
1595
+ // Launch dashboard in new terminal
1596
+ spawn("osascript", ["-e", `tell application "Terminal" to do script "${escapeForAppleScript(dashCmd)}"`], {
1597
+ detached: true,
1598
+ stdio: "ignore"
1599
+ }).unref();
1600
+ // Open browser after a brief delay
1601
+ spawn("sh", ["-c", "sleep 2 && open http://127.0.0.1:8787"], {
1602
+ detached: true,
1603
+ stdio: "ignore"
1604
+ }).unref();
1605
+ return jsonResponse({
1606
+ success: true,
1607
+ message: "Dashboard launching at http://127.0.0.1:8787"
1608
+ });
1609
+ }
1610
+ catch (error) {
1611
+ const message = error instanceof Error ? error.message : "Unknown error";
1612
+ return jsonResponse({
1613
+ success: false,
1614
+ error: `Failed to launch dashboard: ${message}`
1615
+ });
1616
+ }
1617
+ }
1618
+ // Discussion handlers
1619
+ case "discussion_start": {
1620
+ const topic = getString(args.topic);
1621
+ const message = getString(args.message);
1622
+ const author = getString(args.author);
1623
+ if (!topic || !message || !author) {
1624
+ throw new Error("topic, message, and author are required");
1625
+ }
1626
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1627
+ const category = (getString(args.category) ?? "other");
1628
+ const priority = (getString(args.priority) ?? "medium");
1629
+ const waitingOn = getString(args.waitingOn);
1630
+ const result = await store.createDiscussion({
1631
+ topic,
1632
+ category,
1633
+ priority,
1634
+ message,
1635
+ createdBy: author,
1636
+ projectRoot,
1637
+ waitingOn,
1638
+ });
1639
+ // Also post a note so other agents see the new discussion
1640
+ await store.appendNote({
1641
+ text: `[DISCUSSION] New thread: "${topic}" (${result.discussion.id}) - ${author} is waiting for input${waitingOn ? ` from ${waitingOn}` : ""}`,
1642
+ author: "system"
1643
+ });
1644
+ return jsonResponse({
1645
+ success: true,
1646
+ discussion: result.discussion,
1647
+ message: result.message,
1648
+ instruction: waitingOn
1649
+ ? `Discussion started. Waiting for ${waitingOn} to respond. Continue with other work and check back later.`
1650
+ : "Discussion started. Other agents can reply using discussion_reply."
1651
+ });
1652
+ }
1653
+ case "discussion_reply": {
1654
+ const discussionId = getString(args.discussionId);
1655
+ const message = getString(args.message);
1656
+ const author = getString(args.author);
1657
+ if (!discussionId || !message || !author) {
1658
+ throw new Error("discussionId, message, and author are required");
1659
+ }
1660
+ const result = await store.replyToDiscussion({
1661
+ discussionId,
1662
+ author,
1663
+ message,
1664
+ recommendation: getString(args.recommendation),
1665
+ waitingOn: getString(args.waitingOn),
1666
+ });
1667
+ return jsonResponse({
1668
+ success: true,
1669
+ discussion: result.discussion,
1670
+ message: result.message,
1671
+ instruction: result.discussion.waitingOn
1672
+ ? `Reply posted. Now waiting for ${result.discussion.waitingOn} to respond.`
1673
+ : "Reply posted. Discussion is open for further replies or resolution."
1674
+ });
1675
+ }
1676
+ case "discussion_resolve": {
1677
+ const discussionId = getString(args.discussionId);
1678
+ const decision = getString(args.decision);
1679
+ const reasoning = getString(args.reasoning);
1680
+ const decidedBy = getString(args.decidedBy);
1681
+ if (!discussionId || !decision || !reasoning || !decidedBy) {
1682
+ throw new Error("discussionId, decision, reasoning, and decidedBy are required");
1683
+ }
1684
+ const discussion = await store.resolveDiscussion({
1685
+ discussionId,
1686
+ decision,
1687
+ reasoning,
1688
+ decidedBy,
1689
+ linkedTaskId: getString(args.linkedTaskId),
1690
+ });
1691
+ // Post a note about the resolution
1692
+ await store.appendNote({
1693
+ text: `[DECISION] "${discussion.topic}" resolved: ${decision} (by ${decidedBy})`,
1694
+ author: "system"
1695
+ });
1696
+ return jsonResponse({
1697
+ success: true,
1698
+ discussion,
1699
+ instruction: "Discussion resolved and decision recorded. This creates an audit trail for future reference."
1700
+ });
1701
+ }
1702
+ case "discussion_get": {
1703
+ const discussionId = getString(args.discussionId);
1704
+ if (!discussionId)
1705
+ throw new Error("discussionId is required");
1706
+ const result = await store.getDiscussion(discussionId);
1707
+ if (!result) {
1708
+ return jsonResponse({ found: false, message: "Discussion not found" });
1709
+ }
1710
+ return jsonResponse({
1711
+ found: true,
1712
+ discussion: result.discussion,
1713
+ messages: result.messages,
1714
+ messageCount: result.messages.length
1715
+ });
1716
+ }
1717
+ case "discussion_list": {
1718
+ const discussions = await store.listDiscussions({
1719
+ status: getString(args.status),
1720
+ category: getString(args.category),
1721
+ projectRoot: getString(args.projectRoot),
1722
+ waitingOn: getString(args.waitingOn),
1723
+ limit: getNumber(args.limit),
1724
+ });
1725
+ return jsonResponse({
1726
+ count: discussions.length,
1727
+ discussions
1728
+ });
1729
+ }
1730
+ case "discussion_inbox": {
1731
+ const agent = getString(args.agent);
1732
+ if (!agent)
1733
+ throw new Error("agent is required");
1734
+ const projectRoot = getString(args.projectRoot);
1735
+ const discussions = await store.listDiscussions({
1736
+ status: "waiting",
1737
+ waitingOn: agent,
1738
+ projectRoot,
1739
+ });
1740
+ return jsonResponse({
1741
+ agent,
1742
+ waitingCount: discussions.length,
1743
+ discussions,
1744
+ instruction: discussions.length > 0
1745
+ ? `You have ${discussions.length} discussion(s) waiting for your input. Use discussion_get to see full thread, then discussion_reply to respond.`
1746
+ : "No discussions waiting for your input."
1747
+ });
1748
+ }
1749
+ case "discussion_archive": {
1750
+ const discussionId = getString(args.discussionId);
1751
+ if (!discussionId)
1752
+ throw new Error("discussionId is required");
1753
+ const discussion = await store.archiveDiscussion(discussionId);
1754
+ return jsonResponse({
1755
+ success: true,
1756
+ discussion,
1757
+ message: "Discussion archived. It will be deleted after the retention period."
1758
+ });
1759
+ }
1760
+ case "discussion_cleanup": {
1761
+ const archiveDays = getNumber(args.archiveOlderThanDays) ?? 7;
1762
+ const deleteDays = getNumber(args.deleteOlderThanDays) ?? 30;
1763
+ const projectRoot = getString(args.projectRoot);
1764
+ const archived = await store.archiveOldDiscussions({
1765
+ olderThanDays: archiveDays,
1766
+ projectRoot,
1767
+ });
1768
+ const deleted = await store.deleteArchivedDiscussions({
1769
+ olderThanDays: deleteDays,
1770
+ projectRoot,
1771
+ });
1772
+ return jsonResponse({
1773
+ success: true,
1774
+ archived,
1775
+ deleted,
1776
+ message: `Archived ${archived} resolved discussions older than ${archiveDays} days. Deleted ${deleted} archived discussions older than ${deleteDays} days.`
1777
+ });
1778
+ }
1779
+ // Worktree handlers
1780
+ case "worktree_status": {
1781
+ const implementerId = getString(args.implementerId);
1782
+ if (!implementerId)
1783
+ throw new Error("implementerId is required");
1784
+ const implementers = await store.listImplementers();
1785
+ const impl = implementers.find(i => i.id === implementerId);
1786
+ if (!impl) {
1787
+ return jsonResponse({ found: false, error: "Implementer not found" });
1788
+ }
1789
+ if (impl.isolation !== "worktree" || !impl.worktreePath) {
1790
+ return jsonResponse({
1791
+ found: true,
1792
+ implementer: impl,
1793
+ isolation: impl.isolation,
1794
+ message: "Implementer is not using worktree isolation"
1795
+ });
1796
+ }
1797
+ try {
1798
+ const status = await getWorktreeStatus(impl.worktreePath);
1799
+ const diff = await getWorktreeDiff(impl.worktreePath);
1800
+ return jsonResponse({
1801
+ found: true,
1802
+ implementer: impl,
1803
+ worktreeStatus: status,
1804
+ diff,
1805
+ instruction: status.hasUncommittedChanges
1806
+ ? "Implementer has uncommitted changes. They should commit before merge."
1807
+ : status.ahead > 0
1808
+ ? `Implementer has ${status.ahead} commit(s) ready to merge.`
1809
+ : "No changes to merge."
1810
+ });
1811
+ }
1812
+ catch (error) {
1813
+ const message = error instanceof Error ? error.message : "Unknown error";
1814
+ return jsonResponse({
1815
+ found: true,
1816
+ implementer: impl,
1817
+ error: `Failed to get worktree status: ${message}`
1818
+ });
1819
+ }
1820
+ }
1821
+ case "worktree_merge": {
1822
+ const implementerId = getString(args.implementerId);
1823
+ if (!implementerId)
1824
+ throw new Error("implementerId is required");
1825
+ const implementers = await store.listImplementers();
1826
+ const impl = implementers.find(i => i.id === implementerId);
1827
+ if (!impl) {
1828
+ return jsonResponse({ success: false, error: "Implementer not found" });
1829
+ }
1830
+ if (impl.isolation !== "worktree" || !impl.worktreePath) {
1831
+ return jsonResponse({
1832
+ success: false,
1833
+ error: "Implementer is not using worktree isolation"
1834
+ });
1835
+ }
1836
+ const targetBranch = getString(args.targetBranch);
1837
+ try {
1838
+ const result = await mergeWorktree(impl.worktreePath, targetBranch);
1839
+ if (result.success && result.merged) {
1840
+ // Optionally clean up the worktree after successful merge
1841
+ await store.appendNote({
1842
+ text: `[SYSTEM] Merged ${impl.name}'s worktree (${impl.branchName}) to ${targetBranch ?? "main"}`,
1843
+ author: "system"
1844
+ });
1845
+ }
1846
+ return jsonResponse({
1847
+ success: result.success,
1848
+ merged: result.merged,
1849
+ conflicts: result.conflicts,
1850
+ error: result.error,
1851
+ instruction: result.conflicts
1852
+ ? `Merge has conflicts in: ${result.conflicts.join(", ")}. Resolve manually or use task_request_changes to have implementer fix.`
1853
+ : result.merged
1854
+ ? "Changes merged successfully."
1855
+ : result.error
1856
+ ? `Merge failed: ${result.error}`
1857
+ : "Nothing to merge."
1858
+ });
1859
+ }
1860
+ catch (error) {
1861
+ const message = error instanceof Error ? error.message : "Unknown error";
1862
+ return jsonResponse({
1863
+ success: false,
1864
+ error: `Failed to merge worktree: ${message}`
1865
+ });
1866
+ }
1867
+ }
1868
+ case "worktree_list": {
1869
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1870
+ const isGit = await isGitRepo(projectRoot);
1871
+ if (!isGit) {
1872
+ return jsonResponse({
1873
+ worktrees: [],
1874
+ message: "Project is not a git repository"
1875
+ });
1876
+ }
1877
+ try {
1878
+ const worktrees = await listWorktrees(projectRoot);
1879
+ const implementers = await store.listImplementers(projectRoot);
1880
+ // Enrich worktree info with implementer data
1881
+ const enriched = worktrees.map(wt => {
1882
+ const impl = implementers.find(i => i.worktreePath === wt.path);
1883
+ return {
1884
+ ...wt,
1885
+ implementer: impl ? { id: impl.id, name: impl.name, status: impl.status } : null
1886
+ };
1887
+ });
1888
+ return jsonResponse({
1889
+ count: worktrees.length,
1890
+ worktrees: enriched
1891
+ });
1892
+ }
1893
+ catch (error) {
1894
+ const message = error instanceof Error ? error.message : "Unknown error";
1895
+ return jsonResponse({
1896
+ worktrees: [],
1897
+ error: `Failed to list worktrees: ${message}`
1898
+ });
1899
+ }
1900
+ }
1901
+ case "worktree_cleanup": {
1902
+ const projectRoot = getString(args.projectRoot) ?? config.roots[0] ?? process.cwd();
1903
+ const isGit = await isGitRepo(projectRoot);
1904
+ if (!isGit) {
1905
+ return jsonResponse({
1906
+ success: false,
1907
+ error: "Project is not a git repository"
1908
+ });
1909
+ }
1910
+ try {
1911
+ const cleaned = await cleanupOrphanedWorktrees(projectRoot);
1912
+ return jsonResponse({
1913
+ success: true,
1914
+ cleanedCount: cleaned.length,
1915
+ cleaned,
1916
+ message: cleaned.length > 0
1917
+ ? `Cleaned up ${cleaned.length} orphaned worktree(s)`
1918
+ : "No orphaned worktrees found"
1919
+ });
1920
+ }
1921
+ catch (error) {
1922
+ const message = error instanceof Error ? error.message : "Unknown error";
1923
+ return jsonResponse({
1924
+ success: false,
1925
+ error: `Failed to cleanup worktrees: ${message}`
1926
+ });
1927
+ }
1928
+ }
1929
+ default:
1930
+ return errorResponse(`Unknown tool: ${name}`);
1931
+ }
1932
+ }
1933
+ catch (error) {
1934
+ const message = error instanceof Error ? error.message : "Unknown error";
1935
+ return errorResponse(message);
1936
+ }
1937
+ });
1938
+ export async function startServer() {
1939
+ await store.init();
1940
+ const transport = new StdioServerTransport();
1941
+ await server.connect(transport);
1942
+ }