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/LICENSE +21 -0
- package/README.md +669 -0
- package/dist/cli.js +367 -0
- package/dist/config.js +48 -0
- package/dist/dashboard.js +1982 -0
- package/dist/install.js +252 -0
- package/dist/macos.js +55 -0
- package/dist/prompts.js +173 -0
- package/dist/server.js +1942 -0
- package/dist/storage.js +1235 -0
- package/dist/tmux.js +87 -0
- package/dist/utils.js +35 -0
- package/dist/worktree.js +356 -0
- package/package.json +66 -0
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
|
+
}
|