nightshift-mcp 1.0.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/README.md +670 -0
- package/dist/agent-spawner.d.ts +55 -0
- package/dist/agent-spawner.d.ts.map +1 -0
- package/dist/agent-spawner.js +468 -0
- package/dist/agent-spawner.js.map +1 -0
- package/dist/chat-manager.d.ts +72 -0
- package/dist/chat-manager.d.ts.map +1 -0
- package/dist/chat-manager.js +331 -0
- package/dist/chat-manager.js.map +1 -0
- package/dist/daemon.d.ts +65 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +563 -0
- package/dist/daemon.js.map +1 -0
- package/dist/file-lock.d.ts +41 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +157 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2433 -0
- package/dist/index.js.map +1 -0
- package/dist/ralph-manager.d.ts +148 -0
- package/dist/ralph-manager.d.ts.map +1 -0
- package/dist/ralph-manager.js +399 -0
- package/dist/ralph-manager.js.map +1 -0
- package/dist/tool-registry.d.ts +130 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +280 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/agents.d.ts +7 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/agents.js +366 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/bugs.d.ts +6 -0
- package/dist/tools/bugs.d.ts.map +1 -0
- package/dist/tools/bugs.js +184 -0
- package/dist/tools/bugs.js.map +1 -0
- package/dist/tools/chat.d.ts +10 -0
- package/dist/tools/chat.d.ts.map +1 -0
- package/dist/tools/chat.js +287 -0
- package/dist/tools/chat.js.map +1 -0
- package/dist/tools/index.d.ts +33 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +51 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/prd.d.ts +8 -0
- package/dist/tools/prd.d.ts.map +1 -0
- package/dist/tools/prd.js +275 -0
- package/dist/tools/prd.js.map +1 -0
- package/dist/tools/progress.d.ts +5 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +81 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/savepoints.d.ts +5 -0
- package/dist/tools/savepoints.d.ts.map +1 -0
- package/dist/tools/savepoints.js +100 -0
- package/dist/tools/savepoints.js.map +1 -0
- package/dist/tools/utility.d.ts +4 -0
- package/dist/tools/utility.d.ts.map +1 -0
- package/dist/tools/utility.js +375 -0
- package/dist/tools/utility.js.map +1 -0
- package/dist/tools/workflow.d.ts +10 -0
- package/dist/tools/workflow.d.ts.map +1 -0
- package/dist/tools/workflow.js +321 -0
- package/dist/tools/workflow.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-manager.d.ts +154 -0
- package/dist/workflow-manager.d.ts.map +1 -0
- package/dist/workflow-manager.js +356 -0
- package/dist/workflow-manager.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { ChatManager } from "./chat-manager.js";
|
|
8
|
+
import { RalphManager } from "./ralph-manager.js";
|
|
9
|
+
import { WorkflowManager } from "./workflow-manager.js";
|
|
10
|
+
import { spawnAgent, spawnAgentBackground, getAvailableAgents, getAgentStatus, } from "./agent-spawner.js";
|
|
11
|
+
import { toolRegistry } from "./tool-registry.js";
|
|
12
|
+
import { allTools, toolCounts } from "./tools/index.js";
|
|
13
|
+
// Get project path from environment or command line argument
|
|
14
|
+
const PROJECT_PATH = process.env.ROBOT_CHAT_PROJECT_PATH || process.argv[2];
|
|
15
|
+
if (!PROJECT_PATH) {
|
|
16
|
+
console.error("Error: Project path required. Set ROBOT_CHAT_PROJECT_PATH environment variable or pass as argument.");
|
|
17
|
+
console.error("Usage: nightshift-mcp <project-path>");
|
|
18
|
+
console.error(" or: ROBOT_CHAT_PROJECT_PATH=/path/to/project nightshift-mcp");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Tool registration mode:
|
|
23
|
+
* - 'minimal': Only nightshift + nightshift_help (2 tools, ~200 tokens)
|
|
24
|
+
* - 'hybrid': Meta-tools + individual tools (41 tools) - default for backwards compat
|
|
25
|
+
* - 'legacy': Only individual tools (39 tools, ~2,300 tokens)
|
|
26
|
+
*/
|
|
27
|
+
const REGISTRATION_MODE = (process.env.NIGHTSHIFT_TOOLS || "hybrid");
|
|
28
|
+
const chatManager = new ChatManager(PROJECT_PATH);
|
|
29
|
+
const ralphManager = new RalphManager(PROJECT_PATH);
|
|
30
|
+
const workflowManager = new WorkflowManager(PROJECT_PATH);
|
|
31
|
+
// Tool execution context
|
|
32
|
+
const toolContext = {
|
|
33
|
+
chatManager,
|
|
34
|
+
ralphManager,
|
|
35
|
+
workflowManager,
|
|
36
|
+
projectPath: PROJECT_PATH,
|
|
37
|
+
};
|
|
38
|
+
// Register all tools with the registry
|
|
39
|
+
toolRegistry.registerAll(allTools);
|
|
40
|
+
// Create MCP server
|
|
41
|
+
const server = new McpServer({
|
|
42
|
+
name: "nightshift",
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
});
|
|
45
|
+
// Valid message types for schema validation
|
|
46
|
+
const MESSAGE_TYPES = [
|
|
47
|
+
"FAILOVER_NEEDED",
|
|
48
|
+
"FAILOVER_CLAIMED",
|
|
49
|
+
"TASK_COMPLETE",
|
|
50
|
+
"STATUS_UPDATE",
|
|
51
|
+
"HANDOFF",
|
|
52
|
+
"INFO",
|
|
53
|
+
"ERROR",
|
|
54
|
+
"QUESTION",
|
|
55
|
+
"ANSWER",
|
|
56
|
+
"TASK_ASSIGNMENT",
|
|
57
|
+
"ITERATION_START",
|
|
58
|
+
"STORY_COMPLETE",
|
|
59
|
+
"STORY_CLAIMED",
|
|
60
|
+
"READY_TO_TEST",
|
|
61
|
+
"BUG_CLAIMED",
|
|
62
|
+
"BUG_FIXED",
|
|
63
|
+
];
|
|
64
|
+
// ============================================
|
|
65
|
+
// Meta-Tools (Context-Optimized)
|
|
66
|
+
// ============================================
|
|
67
|
+
/**
|
|
68
|
+
* Register meta-tools for context optimization
|
|
69
|
+
*/
|
|
70
|
+
function registerMetaTools() {
|
|
71
|
+
// nightshift - Universal dispatcher
|
|
72
|
+
server.tool("nightshift", "Execute any NightShift action. Use nightshift_help for available actions.", {
|
|
73
|
+
action: z.string().describe("Action name (e.g., 'claim_story', 'read_prd', 'spawn_agent')"),
|
|
74
|
+
params: z.record(z.any()).optional().describe("Action parameters as key-value pairs"),
|
|
75
|
+
}, async ({ action, params }) => {
|
|
76
|
+
const result = await toolRegistry.execute(action, params || {}, toolContext);
|
|
77
|
+
return result;
|
|
78
|
+
});
|
|
79
|
+
// nightshift_help - Dynamic documentation
|
|
80
|
+
server.tool("nightshift_help", "Get help on NightShift actions and their parameters.", {
|
|
81
|
+
action: z.string().optional().describe("Specific action name for detailed help"),
|
|
82
|
+
category: z.string().optional().describe("Filter by category: chat, prd, workflow, agents, progress, bugs, savepoints, utility"),
|
|
83
|
+
}, async ({ action, category }) => {
|
|
84
|
+
const help = toolRegistry.generateHelp(action, category);
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: help }],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ============================================
|
|
91
|
+
// Individual Tool Registration (Legacy)
|
|
92
|
+
// ============================================
|
|
93
|
+
const AGENT_TYPES = ["claude", "codex", "gemini", "vibe"];
|
|
94
|
+
/**
|
|
95
|
+
* Register all individual tools for backwards compatibility
|
|
96
|
+
*/
|
|
97
|
+
function registerIndividualTools() {
|
|
98
|
+
// Tool: read_robot_chat
|
|
99
|
+
server.tool("read_robot_chat", "Read recent messages from the robot chat file. Returns structured data with agent name, timestamp, message type, and content.", {
|
|
100
|
+
limit: z
|
|
101
|
+
.number()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Maximum number of messages to return (default: 20)"),
|
|
104
|
+
agent: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Filter messages by agent name (e.g., 'Claude', 'Codex', 'Gemini')"),
|
|
108
|
+
type: z
|
|
109
|
+
.enum(MESSAGE_TYPES)
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Filter messages by type"),
|
|
112
|
+
}, async ({ limit, agent, type }) => {
|
|
113
|
+
try {
|
|
114
|
+
const messages = chatManager.readMessages({
|
|
115
|
+
limit: limit ?? 20,
|
|
116
|
+
agent,
|
|
117
|
+
type: type,
|
|
118
|
+
});
|
|
119
|
+
if (messages.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: "No messages found in robot chat.",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const formatted = messages.map((m) => ({
|
|
130
|
+
agent: m.agent,
|
|
131
|
+
timestamp: m.timestamp,
|
|
132
|
+
type: m.type,
|
|
133
|
+
content: m.content,
|
|
134
|
+
lineNumber: m.lineNumber,
|
|
135
|
+
}));
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify(formatted, null, 2),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `Error reading chat: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Tool: write_robot_chat
|
|
158
|
+
server.tool("write_robot_chat", "Write a message to the robot chat file. The message will be formatted with your agent name, current timestamp, and message type.", {
|
|
159
|
+
agent: z
|
|
160
|
+
.string()
|
|
161
|
+
.describe("Your agent name (e.g., 'Claude', 'Codex', 'Gemini')"),
|
|
162
|
+
type: z
|
|
163
|
+
.enum(MESSAGE_TYPES)
|
|
164
|
+
.describe("Message type (e.g., 'STATUS_UPDATE', 'FAILOVER_NEEDED', 'TASK_COMPLETE')"),
|
|
165
|
+
content: z
|
|
166
|
+
.string()
|
|
167
|
+
.describe("Message content. For FAILOVER_NEEDED, include: Status, Current Task, Progress, Files Modified."),
|
|
168
|
+
}, async ({ agent, type, content }) => {
|
|
169
|
+
try {
|
|
170
|
+
const message = await chatManager.writeMessage({
|
|
171
|
+
agent,
|
|
172
|
+
type: type,
|
|
173
|
+
content,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: `Message written successfully:\n\n${message.raw}`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: `Error writing message: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
isError: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Tool: check_failovers
|
|
197
|
+
server.tool("check_failovers", "Find unclaimed FAILOVER_NEEDED messages. Use this to see if another agent needs help continuing a task.", {}, async () => {
|
|
198
|
+
try {
|
|
199
|
+
const failovers = chatManager.findUnclaimedFailovers();
|
|
200
|
+
if (failovers.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: "No unclaimed failovers found.",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const formatted = failovers.map((f) => ({
|
|
211
|
+
requestingAgent: f.requestingAgent,
|
|
212
|
+
timestamp: f.message.timestamp,
|
|
213
|
+
task: f.task || "Not specified",
|
|
214
|
+
progress: f.progress || "Not specified",
|
|
215
|
+
filesModified: f.filesModified || [],
|
|
216
|
+
fullContent: f.message.content,
|
|
217
|
+
}));
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: `Found ${failovers.length} unclaimed failover(s):\n\n${JSON.stringify(formatted, null, 2)}`,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: `Error checking failovers: ${error instanceof Error ? error.message : String(error)}`,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// Tool: claim_failover
|
|
240
|
+
server.tool("claim_failover", "Claim a failover request by posting a FAILOVER_CLAIMED message. Use this after checking for failovers with check_failovers.", {
|
|
241
|
+
agent: z
|
|
242
|
+
.string()
|
|
243
|
+
.describe("Your agent name (the one claiming the failover)"),
|
|
244
|
+
originalAgent: z
|
|
245
|
+
.string()
|
|
246
|
+
.describe("The agent who requested the failover"),
|
|
247
|
+
task: z
|
|
248
|
+
.string()
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Description of the task you're continuing (optional)"),
|
|
251
|
+
}, async ({ agent, originalAgent, task }) => {
|
|
252
|
+
try {
|
|
253
|
+
const message = await chatManager.claimFailover(agent, originalAgent, task);
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: `Failover claimed successfully:\n\n${message.raw}`,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `Error claiming failover: ${error instanceof Error ? error.message : String(error)}`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
isError: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// Tool: get_chat_path
|
|
276
|
+
server.tool("get_chat_path", "Get the path to the robot chat file. Useful for debugging or manual inspection.", {}, async () => {
|
|
277
|
+
try {
|
|
278
|
+
const chatPath = chatManager.getChatFilePath();
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: `Robot chat file: ${chatPath}`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: `Error getting chat path: ${error instanceof Error ? error.message : String(error)}`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
isError: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
// Tool: list_agents
|
|
301
|
+
server.tool("list_agents", "List all agents who have posted to the chat, with their last activity time and message count.", {}, async () => {
|
|
302
|
+
try {
|
|
303
|
+
const agents = chatManager.listAgents();
|
|
304
|
+
if (agents.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: "No agents have posted yet.",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: JSON.stringify(agents, null, 2),
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: `Error listing agents: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
isError: true,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
// Tool: watch_chat
|
|
336
|
+
server.tool("watch_chat", "Get new messages since a specific cursor (line number). Use this for polling/watching the chat. Returns new messages and the updated cursor.", {
|
|
337
|
+
cursor: z
|
|
338
|
+
.number()
|
|
339
|
+
.optional()
|
|
340
|
+
.describe("Line number cursor from previous watch call. Omit to get current cursor without messages."),
|
|
341
|
+
}, async ({ cursor }) => {
|
|
342
|
+
try {
|
|
343
|
+
const currentCursor = chatManager.getLastLineNumber();
|
|
344
|
+
// If no cursor provided, just return current cursor position
|
|
345
|
+
if (cursor === undefined) {
|
|
346
|
+
return {
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: JSON.stringify({
|
|
351
|
+
cursor: currentCursor,
|
|
352
|
+
messages: [],
|
|
353
|
+
info: "Use this cursor value in your next watch_chat call to get new messages.",
|
|
354
|
+
}, null, 2),
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const newMessages = chatManager.getMessagesSince(cursor);
|
|
360
|
+
const formatted = newMessages.map((m) => ({
|
|
361
|
+
agent: m.agent,
|
|
362
|
+
timestamp: m.timestamp,
|
|
363
|
+
type: m.type,
|
|
364
|
+
content: m.content,
|
|
365
|
+
lineNumber: m.lineNumber,
|
|
366
|
+
}));
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: JSON.stringify({
|
|
372
|
+
cursor: currentCursor,
|
|
373
|
+
messageCount: newMessages.length,
|
|
374
|
+
messages: formatted,
|
|
375
|
+
}, null, 2),
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return {
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: `Error watching chat: ${error instanceof Error ? error.message : String(error)}`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
isError: true,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
// Tool: archive_chat
|
|
393
|
+
server.tool("archive_chat", "Archive old messages to a separate file, keeping only recent messages in the main chat. Useful for managing chat file size.", {
|
|
394
|
+
keepRecent: z
|
|
395
|
+
.number()
|
|
396
|
+
.optional()
|
|
397
|
+
.describe("Number of recent messages to keep in main chat (default: 50)"),
|
|
398
|
+
}, async ({ keepRecent }) => {
|
|
399
|
+
try {
|
|
400
|
+
const result = await chatManager.archiveMessages(keepRecent ?? 50);
|
|
401
|
+
if (result.archived === 0) {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: "No messages to archive. Chat file is small enough.",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
content: [
|
|
413
|
+
{
|
|
414
|
+
type: "text",
|
|
415
|
+
text: `Archived ${result.archived} messages to: ${result.archiveFile}`,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text",
|
|
425
|
+
text: `Error archiving chat: ${error instanceof Error ? error.message : String(error)}`,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
isError: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
// ============================================
|
|
433
|
+
// Agent Spawning Tools
|
|
434
|
+
// ============================================
|
|
435
|
+
// Tool: list_available_agents
|
|
436
|
+
server.tool("list_available_agents", "Check which AI agent CLIs are installed and available to spawn. Returns list of available agents (claude, codex, gemini, vibe) with their status including whether they can actually run.", {}, async () => {
|
|
437
|
+
try {
|
|
438
|
+
const status = await getAgentStatus();
|
|
439
|
+
const available = await getAvailableAgents();
|
|
440
|
+
// Build a more informative response
|
|
441
|
+
const agentInfo = {};
|
|
442
|
+
for (const agent of AGENT_TYPES) {
|
|
443
|
+
agentInfo[agent] = {
|
|
444
|
+
installed: status[agent].available,
|
|
445
|
+
canRun: status[agent].canRun,
|
|
446
|
+
reason: status[agent].reason,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: JSON.stringify({
|
|
454
|
+
available: available.filter((a) => status[a].canRun),
|
|
455
|
+
installed: available,
|
|
456
|
+
unavailable: AGENT_TYPES.filter((a) => !available.includes(a)),
|
|
457
|
+
details: agentInfo,
|
|
458
|
+
}, null, 2),
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: "text",
|
|
468
|
+
text: `Error checking agents: ${error instanceof Error ? error.message : String(error)}`,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
isError: true,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// Tool: spawn_agent
|
|
476
|
+
server.tool("spawn_agent", "Spawn another AI agent (Claude, Codex, Gemini, or Vibe) as a subprocess to work on a task. The agent runs to completion and returns results. Use this to delegate work to other agents.", {
|
|
477
|
+
agent: z.enum(AGENT_TYPES).describe("Which agent to spawn (claude, codex, gemini, or vibe)"),
|
|
478
|
+
prompt: z.string().describe("The prompt/task to send to the agent"),
|
|
479
|
+
timeout: z
|
|
480
|
+
.number()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe("Timeout in seconds (default: 300 = 5 minutes)"),
|
|
483
|
+
}, async ({ agent, prompt, timeout }) => {
|
|
484
|
+
try {
|
|
485
|
+
// Post to chat that we're spawning an agent
|
|
486
|
+
chatManager.writeMessage({
|
|
487
|
+
agent: "NightShift",
|
|
488
|
+
type: "INFO",
|
|
489
|
+
content: `Spawning ${agent} agent to work on task...`,
|
|
490
|
+
});
|
|
491
|
+
const result = await spawnAgent({
|
|
492
|
+
agent: agent,
|
|
493
|
+
prompt,
|
|
494
|
+
projectPath: PROJECT_PATH,
|
|
495
|
+
timeout: timeout ? timeout * 1000 : undefined,
|
|
496
|
+
});
|
|
497
|
+
// Post result to chat
|
|
498
|
+
chatManager.writeMessage({
|
|
499
|
+
agent: "NightShift",
|
|
500
|
+
type: result.success ? "INFO" : "ERROR",
|
|
501
|
+
content: `${agent} agent ${result.success ? "completed" : "failed"}: ${result.output.substring(0, 500)}${result.output.length > 500 ? "..." : ""}`,
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: "text",
|
|
507
|
+
text: JSON.stringify({
|
|
508
|
+
success: result.success,
|
|
509
|
+
agent,
|
|
510
|
+
output: result.output,
|
|
511
|
+
exitCode: result.exitCode,
|
|
512
|
+
error: result.error,
|
|
513
|
+
}, null, 2),
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
isError: !result.success,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
return {
|
|
521
|
+
content: [
|
|
522
|
+
{
|
|
523
|
+
type: "text",
|
|
524
|
+
text: `Error spawning agent: ${error instanceof Error ? error.message : String(error)}`,
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
isError: true,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
// Tool: spawn_agent_background
|
|
532
|
+
server.tool("spawn_agent_background", "Spawn an AI agent in the background (non-blocking). Returns immediately with the output file path. Use this when you want agents to work in parallel.", {
|
|
533
|
+
agent: z.enum(AGENT_TYPES).describe("Which agent to spawn (claude, codex, gemini, or vibe)"),
|
|
534
|
+
prompt: z.string().describe("The prompt/task to send to the agent"),
|
|
535
|
+
}, async ({ agent, prompt }) => {
|
|
536
|
+
try {
|
|
537
|
+
const result = spawnAgentBackground({
|
|
538
|
+
agent: agent,
|
|
539
|
+
prompt,
|
|
540
|
+
projectPath: PROJECT_PATH,
|
|
541
|
+
});
|
|
542
|
+
// Post to chat
|
|
543
|
+
chatManager.writeMessage({
|
|
544
|
+
agent: "NightShift",
|
|
545
|
+
type: "INFO",
|
|
546
|
+
content: `Spawned ${agent} in background (PID: ${result.pid})\nOutput: ${result.outputFile}`,
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: JSON.stringify({
|
|
553
|
+
agent,
|
|
554
|
+
pid: result.pid,
|
|
555
|
+
outputFile: result.outputFile,
|
|
556
|
+
status: "running in background",
|
|
557
|
+
}, null, 2),
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
return {
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: `Error spawning background agent: ${error instanceof Error ? error.message : String(error)}`,
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
isError: true,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
// Tool: delegate_story
|
|
575
|
+
server.tool("delegate_story", "Delegate a user story to another AI agent. Claims the story, spawns the agent with full context, and tracks completion. The spawned agent works autonomously.", {
|
|
576
|
+
agent: z.enum(AGENT_TYPES).describe("Which agent to delegate to"),
|
|
577
|
+
storyId: z.string().optional().describe("Specific story ID (defaults to next available)"),
|
|
578
|
+
background: z.boolean().optional().describe("Run in background (default: false)"),
|
|
579
|
+
}, async ({ agent, storyId, background }) => {
|
|
580
|
+
try {
|
|
581
|
+
// Get the story
|
|
582
|
+
let story;
|
|
583
|
+
if (storyId) {
|
|
584
|
+
const prd = ralphManager.readPRD();
|
|
585
|
+
story = prd?.userStories.find((s) => s.id === storyId && !s.passes);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
story = ralphManager.getNextStory();
|
|
589
|
+
}
|
|
590
|
+
if (!story) {
|
|
591
|
+
const summary = ralphManager.getCompletionSummary();
|
|
592
|
+
if (summary.incomplete === 0) {
|
|
593
|
+
return {
|
|
594
|
+
content: [
|
|
595
|
+
{
|
|
596
|
+
type: "text",
|
|
597
|
+
text: "<promise>COMPLETE</promise>\nAll stories are complete!",
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: storyId ? `Story ${storyId} not found or already complete.` : "No stories available.",
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
isError: true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
// Read progress for context
|
|
613
|
+
const progress = ralphManager.readProgress();
|
|
614
|
+
const recentChat = chatManager.readMessages({ limit: 5 });
|
|
615
|
+
// Build delegation prompt
|
|
616
|
+
const delegationPrompt = `You are an autonomous coding agent. Complete this user story:
|
|
617
|
+
|
|
618
|
+
## Story: ${story.id} - ${story.title}
|
|
619
|
+
|
|
620
|
+
${story.description}
|
|
621
|
+
|
|
622
|
+
### Acceptance Criteria:
|
|
623
|
+
${story.acceptanceCriteria.map((c) => `- ${c}`).join("\n")}
|
|
624
|
+
|
|
625
|
+
### Context from Progress File:
|
|
626
|
+
${progress ? progress.substring(0, 2000) : "No previous progress."}
|
|
627
|
+
|
|
628
|
+
### Recent Chat:
|
|
629
|
+
${recentChat.map((m) => `[${m.agent}] ${m.type}: ${m.content.substring(0, 200)}`).join("\n")}
|
|
630
|
+
|
|
631
|
+
### Instructions:
|
|
632
|
+
1. Implement the acceptance criteria
|
|
633
|
+
2. Run quality checks (typecheck, lint, test)
|
|
634
|
+
3. Commit with message: "feat: ${story.id} - ${story.title}"
|
|
635
|
+
4. Use the nightshift MCP tools to:
|
|
636
|
+
- Post STATUS_UPDATE messages as you work
|
|
637
|
+
- Use complete_story when done (include learnings)
|
|
638
|
+
- Post FAILOVER_NEEDED if you hit limits
|
|
639
|
+
|
|
640
|
+
Begin implementation now.`;
|
|
641
|
+
// Post delegation to chat
|
|
642
|
+
chatManager.writeMessage({
|
|
643
|
+
agent: "NightShift",
|
|
644
|
+
type: "TASK_ASSIGNMENT",
|
|
645
|
+
content: `Delegating ${story.id} to ${agent}:\n${story.title}`,
|
|
646
|
+
});
|
|
647
|
+
if (background) {
|
|
648
|
+
const result = spawnAgentBackground({
|
|
649
|
+
agent: agent,
|
|
650
|
+
prompt: delegationPrompt,
|
|
651
|
+
projectPath: PROJECT_PATH,
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
content: [
|
|
655
|
+
{
|
|
656
|
+
type: "text",
|
|
657
|
+
text: JSON.stringify({
|
|
658
|
+
status: "delegated",
|
|
659
|
+
agent,
|
|
660
|
+
story: story.id,
|
|
661
|
+
title: story.title,
|
|
662
|
+
background: true,
|
|
663
|
+
pid: result.pid,
|
|
664
|
+
outputFile: result.outputFile,
|
|
665
|
+
}, null, 2),
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
const result = await spawnAgent({
|
|
672
|
+
agent: agent,
|
|
673
|
+
prompt: delegationPrompt,
|
|
674
|
+
projectPath: PROJECT_PATH,
|
|
675
|
+
timeout: 10 * 60 * 1000, // 10 minutes for story work
|
|
676
|
+
});
|
|
677
|
+
return {
|
|
678
|
+
content: [
|
|
679
|
+
{
|
|
680
|
+
type: "text",
|
|
681
|
+
text: JSON.stringify({
|
|
682
|
+
status: result.success ? "completed" : "failed",
|
|
683
|
+
agent,
|
|
684
|
+
story: story.id,
|
|
685
|
+
title: story.title,
|
|
686
|
+
output: result.output,
|
|
687
|
+
error: result.error,
|
|
688
|
+
}, null, 2),
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
isError: !result.success,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
content: [
|
|
698
|
+
{
|
|
699
|
+
type: "text",
|
|
700
|
+
text: `Error delegating story: ${error instanceof Error ? error.message : String(error)}`,
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
isError: true,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// Tool: delegate_research
|
|
708
|
+
server.tool("delegate_research", "Delegate a research or planning task to Gemini. Gemini excels at read-only tasks: codebase analysis, architecture planning, code review, and documentation. Returns findings that can inform implementation decisions.", {
|
|
709
|
+
task: z.string().describe("The research/planning task (e.g., 'Analyze authentication patterns in codebase', 'Review PR for security issues', 'Plan architecture for feature X')"),
|
|
710
|
+
context: z.string().optional().describe("Additional context to provide"),
|
|
711
|
+
background: z.boolean().optional().describe("Run in background (default: false)"),
|
|
712
|
+
}, async ({ task, context, background }) => {
|
|
713
|
+
try {
|
|
714
|
+
// Read codebase context
|
|
715
|
+
const progress = ralphManager.readProgress();
|
|
716
|
+
const recentChat = chatManager.readMessages({ limit: 5 });
|
|
717
|
+
const prd = ralphManager.readPRD();
|
|
718
|
+
// Build research prompt optimized for Gemini's read-only capabilities
|
|
719
|
+
const researchPrompt = `You are a research and planning agent. Your task is to analyze and provide recommendations.
|
|
720
|
+
|
|
721
|
+
## Task
|
|
722
|
+
${task}
|
|
723
|
+
|
|
724
|
+
${context ? `## Additional Context\n${context}\n` : ""}
|
|
725
|
+
## Project Context
|
|
726
|
+
${prd ? `Project: ${prd.projectName || prd.project || "Unknown"}\nDescription: ${prd.description}` : "No PRD found."}
|
|
727
|
+
|
|
728
|
+
## Progress/Learnings from Previous Work
|
|
729
|
+
${progress ? progress.substring(0, 2000) : "No previous progress."}
|
|
730
|
+
|
|
731
|
+
## Recent Agent Communication
|
|
732
|
+
${recentChat.map((m) => `[${m.agent}] ${m.type}: ${m.content.substring(0, 200)}`).join("\n")}
|
|
733
|
+
|
|
734
|
+
## Instructions
|
|
735
|
+
1. Analyze the codebase and gather relevant information
|
|
736
|
+
2. Use read_file, glob, and other read-only tools to explore
|
|
737
|
+
3. Provide clear findings and actionable recommendations
|
|
738
|
+
4. Structure your response as:
|
|
739
|
+
- **Summary**: Brief overview of findings
|
|
740
|
+
- **Details**: Detailed analysis
|
|
741
|
+
- **Recommendations**: Specific actionable suggestions
|
|
742
|
+
- **Considerations**: Trade-offs or things to watch out for
|
|
743
|
+
|
|
744
|
+
5. Post your findings via nightshift write_robot_chat with type INFO
|
|
745
|
+
|
|
746
|
+
Focus on analysis and recommendations. Do NOT attempt to write or modify files.`;
|
|
747
|
+
// Post delegation to chat
|
|
748
|
+
await chatManager.writeMessage({
|
|
749
|
+
agent: "NightShift",
|
|
750
|
+
type: "TASK_ASSIGNMENT",
|
|
751
|
+
content: `Research task delegated to Gemini:\n${task}`,
|
|
752
|
+
});
|
|
753
|
+
if (background) {
|
|
754
|
+
const result = spawnAgentBackground({
|
|
755
|
+
agent: "gemini",
|
|
756
|
+
prompt: researchPrompt,
|
|
757
|
+
projectPath: PROJECT_PATH,
|
|
758
|
+
});
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: JSON.stringify({
|
|
764
|
+
status: "delegated",
|
|
765
|
+
agent: "gemini",
|
|
766
|
+
task,
|
|
767
|
+
background: true,
|
|
768
|
+
pid: result.pid,
|
|
769
|
+
outputFile: result.outputFile,
|
|
770
|
+
}, null, 2),
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
const result = await spawnAgent({
|
|
777
|
+
agent: "gemini",
|
|
778
|
+
prompt: researchPrompt,
|
|
779
|
+
projectPath: PROJECT_PATH,
|
|
780
|
+
timeout: 5 * 60 * 1000, // 5 minutes for research
|
|
781
|
+
});
|
|
782
|
+
return {
|
|
783
|
+
content: [
|
|
784
|
+
{
|
|
785
|
+
type: "text",
|
|
786
|
+
text: JSON.stringify({
|
|
787
|
+
status: result.success ? "completed" : "failed",
|
|
788
|
+
agent: "gemini",
|
|
789
|
+
task,
|
|
790
|
+
findings: result.output,
|
|
791
|
+
error: result.error,
|
|
792
|
+
}, null, 2),
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
isError: !result.success,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
return {
|
|
801
|
+
content: [
|
|
802
|
+
{
|
|
803
|
+
type: "text",
|
|
804
|
+
text: `Error delegating research: ${error instanceof Error ? error.message : String(error)}`,
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
isError: true,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
// ============================================
|
|
812
|
+
// Ralph PRD/Progress Tools
|
|
813
|
+
// ============================================
|
|
814
|
+
// Tool: read_prd
|
|
815
|
+
server.tool("read_prd", "Read the PRD (Product Requirements Document) file. Returns structured task list with user stories and their completion status.", {}, async () => {
|
|
816
|
+
try {
|
|
817
|
+
if (!ralphManager.hasPRD()) {
|
|
818
|
+
return {
|
|
819
|
+
content: [
|
|
820
|
+
{
|
|
821
|
+
type: "text",
|
|
822
|
+
text: "No prd.json found in project. Create one to use Ralph-style task management.",
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const prd = ralphManager.readPRD();
|
|
828
|
+
const summary = ralphManager.getCompletionSummary();
|
|
829
|
+
return {
|
|
830
|
+
content: [
|
|
831
|
+
{
|
|
832
|
+
type: "text",
|
|
833
|
+
text: JSON.stringify({ prd, summary }, null, 2),
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
content: [
|
|
841
|
+
{
|
|
842
|
+
type: "text",
|
|
843
|
+
text: `Error reading PRD: ${error instanceof Error ? error.message : String(error)}`,
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
isError: true,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
// Tool: get_next_story
|
|
851
|
+
server.tool("get_next_story", "Get the next user story to work on (highest priority incomplete story). Use this to claim your next task.", {}, async () => {
|
|
852
|
+
try {
|
|
853
|
+
const story = ralphManager.getNextStory();
|
|
854
|
+
if (!story) {
|
|
855
|
+
const summary = ralphManager.getCompletionSummary();
|
|
856
|
+
if (summary.total === 0) {
|
|
857
|
+
return {
|
|
858
|
+
content: [
|
|
859
|
+
{
|
|
860
|
+
type: "text",
|
|
861
|
+
text: "No PRD found or no stories defined.",
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
content: [
|
|
868
|
+
{
|
|
869
|
+
type: "text",
|
|
870
|
+
text: "<promise>COMPLETE</promise>\nAll stories are complete!",
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
content: [
|
|
877
|
+
{
|
|
878
|
+
type: "text",
|
|
879
|
+
text: JSON.stringify(story, null, 2),
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
return {
|
|
886
|
+
content: [
|
|
887
|
+
{
|
|
888
|
+
type: "text",
|
|
889
|
+
text: `Error getting next story: ${error instanceof Error ? error.message : String(error)}`,
|
|
890
|
+
},
|
|
891
|
+
],
|
|
892
|
+
isError: true,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
// Tool: mark_story_complete
|
|
897
|
+
server.tool("mark_story_complete", "Mark a user story as complete (sets passes: true). Use after successfully implementing and testing a story.", {
|
|
898
|
+
storyId: z.string().describe("The story ID to mark complete (e.g., 'US-001')"),
|
|
899
|
+
notes: z.string().optional().describe("Optional notes about the implementation"),
|
|
900
|
+
}, async ({ storyId, notes }) => {
|
|
901
|
+
try {
|
|
902
|
+
const story = ralphManager.markStoryComplete(storyId, notes);
|
|
903
|
+
if (!story) {
|
|
904
|
+
return {
|
|
905
|
+
content: [
|
|
906
|
+
{
|
|
907
|
+
type: "text",
|
|
908
|
+
text: `Story ${storyId} not found in PRD.`,
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
isError: true,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const summary = ralphManager.getCompletionSummary();
|
|
915
|
+
return {
|
|
916
|
+
content: [
|
|
917
|
+
{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: `Story ${storyId} marked complete!\n\nProgress: ${summary.complete}/${summary.total} (${summary.percentComplete}%)${summary.incomplete === 0 ? "\n\n<promise>COMPLETE</promise>\nAll stories are complete!" : ""}`,
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
return {
|
|
926
|
+
content: [
|
|
927
|
+
{
|
|
928
|
+
type: "text",
|
|
929
|
+
text: `Error marking story complete: ${error instanceof Error ? error.message : String(error)}`,
|
|
930
|
+
},
|
|
931
|
+
],
|
|
932
|
+
isError: true,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
// Tool: get_incomplete_stories
|
|
937
|
+
server.tool("get_incomplete_stories", "Get all incomplete stories from the PRD, sorted by priority.", {}, async () => {
|
|
938
|
+
try {
|
|
939
|
+
const stories = ralphManager.getIncompleteStories();
|
|
940
|
+
if (stories.length === 0) {
|
|
941
|
+
return {
|
|
942
|
+
content: [
|
|
943
|
+
{
|
|
944
|
+
type: "text",
|
|
945
|
+
text: "No incomplete stories found. All done!",
|
|
946
|
+
},
|
|
947
|
+
],
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
content: [
|
|
952
|
+
{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: JSON.stringify(stories, null, 2),
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
return {
|
|
961
|
+
content: [
|
|
962
|
+
{
|
|
963
|
+
type: "text",
|
|
964
|
+
text: `Error getting incomplete stories: ${error instanceof Error ? error.message : String(error)}`,
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
isError: true,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
// Tool: read_progress
|
|
972
|
+
server.tool("read_progress", "Read the progress.txt file containing learnings and patterns from previous iterations.", {}, async () => {
|
|
973
|
+
try {
|
|
974
|
+
const content = ralphManager.readProgress();
|
|
975
|
+
if (!content) {
|
|
976
|
+
return {
|
|
977
|
+
content: [
|
|
978
|
+
{
|
|
979
|
+
type: "text",
|
|
980
|
+
text: "No progress.txt found. One will be created when you append progress.",
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
content: [
|
|
987
|
+
{
|
|
988
|
+
type: "text",
|
|
989
|
+
text: content,
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
return {
|
|
996
|
+
content: [
|
|
997
|
+
{
|
|
998
|
+
type: "text",
|
|
999
|
+
text: `Error reading progress: ${error instanceof Error ? error.message : String(error)}`,
|
|
1000
|
+
},
|
|
1001
|
+
],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
// Tool: append_progress
|
|
1007
|
+
server.tool("append_progress", "Append a progress entry to progress.txt. Include what was implemented, files changed, and learnings for future iterations.", {
|
|
1008
|
+
content: z.string().describe("Progress entry content including: what was implemented, files changed, learnings/gotchas discovered"),
|
|
1009
|
+
}, async ({ content }) => {
|
|
1010
|
+
try {
|
|
1011
|
+
ralphManager.appendProgress(content);
|
|
1012
|
+
return {
|
|
1013
|
+
content: [
|
|
1014
|
+
{
|
|
1015
|
+
type: "text",
|
|
1016
|
+
text: "Progress entry added successfully.",
|
|
1017
|
+
},
|
|
1018
|
+
],
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
catch (error) {
|
|
1022
|
+
return {
|
|
1023
|
+
content: [
|
|
1024
|
+
{
|
|
1025
|
+
type: "text",
|
|
1026
|
+
text: `Error appending progress: ${error instanceof Error ? error.message : String(error)}`,
|
|
1027
|
+
},
|
|
1028
|
+
],
|
|
1029
|
+
isError: true,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
// Tool: add_codebase_pattern
|
|
1034
|
+
server.tool("add_codebase_pattern", "Add a reusable pattern to the Codebase Patterns section of progress.txt. Use for general patterns future iterations should know.", {
|
|
1035
|
+
pattern: z.string().describe("A reusable pattern (e.g., 'Use sql<number> template for aggregations')"),
|
|
1036
|
+
}, async ({ pattern }) => {
|
|
1037
|
+
try {
|
|
1038
|
+
ralphManager.addCodebasePattern(pattern);
|
|
1039
|
+
return {
|
|
1040
|
+
content: [
|
|
1041
|
+
{
|
|
1042
|
+
type: "text",
|
|
1043
|
+
text: `Pattern added: ${pattern}`,
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
return {
|
|
1050
|
+
content: [
|
|
1051
|
+
{
|
|
1052
|
+
type: "text",
|
|
1053
|
+
text: `Error adding pattern: ${error instanceof Error ? error.message : String(error)}`,
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
isError: true,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
// =============================================================================
|
|
1061
|
+
// Workflow Management Tools
|
|
1062
|
+
// =============================================================================
|
|
1063
|
+
// Tool: init_workflow
|
|
1064
|
+
server.tool("init_workflow", "Initialize a new workflow with project goal and phases. Creates workflow.json for tracking state.", {
|
|
1065
|
+
projectGoal: z.string().describe("High-level goal of the project"),
|
|
1066
|
+
phases: z.array(z.enum(["research", "decisions", "planning", "build", "test", "report", "complete"]))
|
|
1067
|
+
.optional()
|
|
1068
|
+
.describe("Custom phases (default: research, decisions, planning, build, test, report)"),
|
|
1069
|
+
}, async ({ projectGoal, phases }) => {
|
|
1070
|
+
try {
|
|
1071
|
+
const state = await workflowManager.initWorkflow(projectGoal, phases);
|
|
1072
|
+
return {
|
|
1073
|
+
content: [
|
|
1074
|
+
{
|
|
1075
|
+
type: "text",
|
|
1076
|
+
text: `Workflow initialized!\n\nProject Goal: ${projectGoal}\nPhases: ${state.phases.map(p => p.name).join(" → ")}\nCurrent Phase: ${state.currentPhase}\n\nWorkflow file: ${workflowManager.getWorkflowPath()}`,
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
content: [
|
|
1084
|
+
{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: `Error initializing workflow: ${error instanceof Error ? error.message : String(error)}`,
|
|
1087
|
+
},
|
|
1088
|
+
],
|
|
1089
|
+
isError: true,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
// Tool: get_workflow_state
|
|
1094
|
+
server.tool("get_workflow_state", "Get the current workflow state including phase, assignments, and decisions.", {}, async () => {
|
|
1095
|
+
try {
|
|
1096
|
+
const state = workflowManager.readWorkflow();
|
|
1097
|
+
if (!state) {
|
|
1098
|
+
return {
|
|
1099
|
+
content: [
|
|
1100
|
+
{
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: "No workflow initialized. Use init_workflow to start a new workflow.",
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
const summary = workflowManager.getWorkflowSummary();
|
|
1108
|
+
const activeAssignments = workflowManager.getActiveAssignments();
|
|
1109
|
+
let response = `# Workflow State\n\n`;
|
|
1110
|
+
response += `**Project Goal:** ${state.projectGoal}\n`;
|
|
1111
|
+
response += `**Current Phase:** ${state.currentPhase}\n`;
|
|
1112
|
+
response += `**Progress:** ${summary?.phaseProgress}\n\n`;
|
|
1113
|
+
response += `## Phases\n`;
|
|
1114
|
+
for (const phase of state.phases) {
|
|
1115
|
+
const status = phase.status === "completed" ? "✓" : phase.status === "in_progress" ? "→" : "○";
|
|
1116
|
+
response += `${status} ${phase.name}`;
|
|
1117
|
+
if (phase.startedAt)
|
|
1118
|
+
response += ` (started: ${phase.startedAt})`;
|
|
1119
|
+
if (phase.completedAt)
|
|
1120
|
+
response += ` (completed: ${phase.completedAt})`;
|
|
1121
|
+
response += `\n`;
|
|
1122
|
+
}
|
|
1123
|
+
response += `\n## Active Assignments\n`;
|
|
1124
|
+
const assignmentEntries = Object.entries(activeAssignments);
|
|
1125
|
+
if (assignmentEntries.length === 0) {
|
|
1126
|
+
response += `No active assignments.\n`;
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
for (const [storyId, assignment] of assignmentEntries) {
|
|
1130
|
+
response += `- ${storyId}: ${assignment.agent} (since ${assignment.claimedAt})\n`;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
response += `\n## Decisions (${state.decisions.length} total)\n`;
|
|
1134
|
+
if (state.decisions.length === 0) {
|
|
1135
|
+
response += `No decisions recorded yet.\n`;
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
for (const decision of state.decisions.slice(-5)) {
|
|
1139
|
+
response += `- ${decision.id}: ${decision.topic} → ${decision.chosen} (by ${decision.decidedBy})\n`;
|
|
1140
|
+
}
|
|
1141
|
+
if (state.decisions.length > 5) {
|
|
1142
|
+
response += `... and ${state.decisions.length - 5} more\n`;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
content: [
|
|
1147
|
+
{
|
|
1148
|
+
type: "text",
|
|
1149
|
+
text: response,
|
|
1150
|
+
},
|
|
1151
|
+
],
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
return {
|
|
1156
|
+
content: [
|
|
1157
|
+
{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: `Error reading workflow: ${error instanceof Error ? error.message : String(error)}`,
|
|
1160
|
+
},
|
|
1161
|
+
],
|
|
1162
|
+
isError: true,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
// Tool: advance_phase
|
|
1167
|
+
server.tool("advance_phase", "Advance to the next workflow phase. Use when current phase's exit criteria are met.", {}, async () => {
|
|
1168
|
+
try {
|
|
1169
|
+
const result = await workflowManager.advancePhase();
|
|
1170
|
+
if (!result) {
|
|
1171
|
+
const currentPhase = workflowManager.getCurrentPhase();
|
|
1172
|
+
return {
|
|
1173
|
+
content: [
|
|
1174
|
+
{
|
|
1175
|
+
type: "text",
|
|
1176
|
+
text: currentPhase
|
|
1177
|
+
? `Already at final phase: ${currentPhase}. Cannot advance further.`
|
|
1178
|
+
: "No workflow initialized. Use init_workflow first.",
|
|
1179
|
+
},
|
|
1180
|
+
],
|
|
1181
|
+
isError: !currentPhase,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
// Post to chat about phase change
|
|
1185
|
+
await chatManager.writeMessage({
|
|
1186
|
+
agent: "Workflow",
|
|
1187
|
+
type: "STATUS_UPDATE",
|
|
1188
|
+
content: `Phase advanced: ${result.previousPhase} → ${result.newPhase}`,
|
|
1189
|
+
});
|
|
1190
|
+
return {
|
|
1191
|
+
content: [
|
|
1192
|
+
{
|
|
1193
|
+
type: "text",
|
|
1194
|
+
text: `Phase advanced!\n\nPrevious: ${result.previousPhase}\nCurrent: ${result.newPhase}`,
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
return {
|
|
1201
|
+
content: [
|
|
1202
|
+
{
|
|
1203
|
+
type: "text",
|
|
1204
|
+
text: `Error advancing phase: ${error instanceof Error ? error.message : String(error)}`,
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
isError: true,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
// Tool: set_phase
|
|
1212
|
+
server.tool("set_phase", "Set the workflow to a specific phase (for manual control or recovery).", {
|
|
1213
|
+
phase: z.enum(["research", "decisions", "planning", "build", "test", "report", "complete"])
|
|
1214
|
+
.describe("Target phase to set"),
|
|
1215
|
+
}, async ({ phase }) => {
|
|
1216
|
+
try {
|
|
1217
|
+
const success = await workflowManager.setPhase(phase);
|
|
1218
|
+
if (!success) {
|
|
1219
|
+
return {
|
|
1220
|
+
content: [
|
|
1221
|
+
{
|
|
1222
|
+
type: "text",
|
|
1223
|
+
text: "Failed to set phase. Ensure workflow is initialized and phase is valid.",
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
isError: true,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
// Post to chat about phase change
|
|
1230
|
+
await chatManager.writeMessage({
|
|
1231
|
+
agent: "Workflow",
|
|
1232
|
+
type: "STATUS_UPDATE",
|
|
1233
|
+
content: `Phase manually set to: ${phase}`,
|
|
1234
|
+
});
|
|
1235
|
+
return {
|
|
1236
|
+
content: [
|
|
1237
|
+
{
|
|
1238
|
+
type: "text",
|
|
1239
|
+
text: `Phase set to: ${phase}`,
|
|
1240
|
+
},
|
|
1241
|
+
],
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
return {
|
|
1246
|
+
content: [
|
|
1247
|
+
{
|
|
1248
|
+
type: "text",
|
|
1249
|
+
text: `Error setting phase: ${error instanceof Error ? error.message : String(error)}`,
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
isError: true,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
// Tool: record_decision
|
|
1257
|
+
server.tool("record_decision", "Record a strategic decision made during the project. Helps track rationale for future reference.", {
|
|
1258
|
+
topic: z.string().describe("What the decision is about (e.g., 'Authentication method')"),
|
|
1259
|
+
options: z.array(z.string()).describe("Options that were considered"),
|
|
1260
|
+
chosen: z.string().describe("The option that was chosen"),
|
|
1261
|
+
rationale: z.string().describe("Why this option was chosen"),
|
|
1262
|
+
decidedBy: z.string().describe("Agent or person who made the decision"),
|
|
1263
|
+
}, async ({ topic, options, chosen, rationale, decidedBy }) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const decision = await workflowManager.recordDecision(topic, options, chosen, rationale, decidedBy);
|
|
1266
|
+
if (!decision) {
|
|
1267
|
+
return {
|
|
1268
|
+
content: [
|
|
1269
|
+
{
|
|
1270
|
+
type: "text",
|
|
1271
|
+
text: "Failed to record decision. Ensure workflow is initialized with init_workflow first.",
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1274
|
+
isError: true,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
// Post to chat about decision
|
|
1278
|
+
await chatManager.writeMessage({
|
|
1279
|
+
agent: decidedBy,
|
|
1280
|
+
type: "INFO",
|
|
1281
|
+
content: `Decision recorded: ${decision.id}\nTopic: ${topic}\nChosen: ${chosen}\nRationale: ${rationale}`,
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
content: [
|
|
1285
|
+
{
|
|
1286
|
+
type: "text",
|
|
1287
|
+
text: `Decision recorded!\n\nID: ${decision.id}\nTopic: ${topic}\nOptions: ${options.join(", ")}\nChosen: ${chosen}\nRationale: ${rationale}\nDecided by: ${decidedBy}`,
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
catch (error) {
|
|
1293
|
+
return {
|
|
1294
|
+
content: [
|
|
1295
|
+
{
|
|
1296
|
+
type: "text",
|
|
1297
|
+
text: `Error recording decision: ${error instanceof Error ? error.message : String(error)}`,
|
|
1298
|
+
},
|
|
1299
|
+
],
|
|
1300
|
+
isError: true,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
// Tool: get_decisions
|
|
1305
|
+
server.tool("get_decisions", "Get all recorded decisions, optionally filtered by topic.", {
|
|
1306
|
+
topic: z.string().optional().describe("Filter decisions by topic (partial match)"),
|
|
1307
|
+
}, async ({ topic }) => {
|
|
1308
|
+
try {
|
|
1309
|
+
const decisions = topic
|
|
1310
|
+
? workflowManager.getDecisionsByTopic(topic)
|
|
1311
|
+
: workflowManager.getDecisions();
|
|
1312
|
+
if (decisions.length === 0) {
|
|
1313
|
+
return {
|
|
1314
|
+
content: [
|
|
1315
|
+
{
|
|
1316
|
+
type: "text",
|
|
1317
|
+
text: topic
|
|
1318
|
+
? `No decisions found matching topic: ${topic}`
|
|
1319
|
+
: "No decisions recorded yet.",
|
|
1320
|
+
},
|
|
1321
|
+
],
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
let response = `# Recorded Decisions (${decisions.length})\n\n`;
|
|
1325
|
+
for (const d of decisions) {
|
|
1326
|
+
response += `## ${d.id}: ${d.topic}\n`;
|
|
1327
|
+
response += `- **Options:** ${d.options.join(", ")}\n`;
|
|
1328
|
+
response += `- **Chosen:** ${d.chosen}\n`;
|
|
1329
|
+
response += `- **Rationale:** ${d.rationale}\n`;
|
|
1330
|
+
response += `- **Decided by:** ${d.decidedBy} at ${d.decidedAt}\n\n`;
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
content: [
|
|
1334
|
+
{
|
|
1335
|
+
type: "text",
|
|
1336
|
+
text: response,
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
return {
|
|
1343
|
+
content: [
|
|
1344
|
+
{
|
|
1345
|
+
type: "text",
|
|
1346
|
+
text: `Error getting decisions: ${error instanceof Error ? error.message : String(error)}`,
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
isError: true,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
// Tool: get_active_assignments
|
|
1354
|
+
server.tool("get_active_assignments", "Get all stories currently being worked on by agents.", {}, async () => {
|
|
1355
|
+
try {
|
|
1356
|
+
const assignments = workflowManager.getActiveAssignments();
|
|
1357
|
+
const entries = Object.entries(assignments);
|
|
1358
|
+
if (entries.length === 0) {
|
|
1359
|
+
return {
|
|
1360
|
+
content: [
|
|
1361
|
+
{
|
|
1362
|
+
type: "text",
|
|
1363
|
+
text: "No active assignments. All agents are free to claim stories.",
|
|
1364
|
+
},
|
|
1365
|
+
],
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
let response = `# Active Assignments\n\n`;
|
|
1369
|
+
for (const [storyId, assignment] of entries) {
|
|
1370
|
+
response += `- **${storyId}**: ${assignment.agent} (since ${assignment.claimedAt})\n`;
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
content: [
|
|
1374
|
+
{
|
|
1375
|
+
type: "text",
|
|
1376
|
+
text: response,
|
|
1377
|
+
},
|
|
1378
|
+
],
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
return {
|
|
1383
|
+
content: [
|
|
1384
|
+
{
|
|
1385
|
+
type: "text",
|
|
1386
|
+
text: `Error getting assignments: ${error instanceof Error ? error.message : String(error)}`,
|
|
1387
|
+
},
|
|
1388
|
+
],
|
|
1389
|
+
isError: true,
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
// Tool: clear_assignment
|
|
1394
|
+
server.tool("clear_assignment", "Clear a story assignment (for abandonment/failover scenarios).", {
|
|
1395
|
+
storyId: z.string().describe("The story ID to clear assignment for"),
|
|
1396
|
+
}, async ({ storyId }) => {
|
|
1397
|
+
try {
|
|
1398
|
+
const cleared = await workflowManager.clearAssignment(storyId);
|
|
1399
|
+
if (!cleared) {
|
|
1400
|
+
return {
|
|
1401
|
+
content: [
|
|
1402
|
+
{
|
|
1403
|
+
type: "text",
|
|
1404
|
+
text: `No active assignment found for story ${storyId}.`,
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
content: [
|
|
1411
|
+
{
|
|
1412
|
+
type: "text",
|
|
1413
|
+
text: `Assignment cleared for story ${storyId}. It can now be claimed by another agent.`,
|
|
1414
|
+
},
|
|
1415
|
+
],
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
return {
|
|
1420
|
+
content: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: `Error clearing assignment: ${error instanceof Error ? error.message : String(error)}`,
|
|
1424
|
+
},
|
|
1425
|
+
],
|
|
1426
|
+
isError: true,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
// =============================================================================
|
|
1431
|
+
// Story Management Tools
|
|
1432
|
+
// =============================================================================
|
|
1433
|
+
// Tool: claim_story
|
|
1434
|
+
server.tool("claim_story", "Claim a story to work on and notify other agents via chat. Combines getting next story and posting STORY_CLAIMED message.", {
|
|
1435
|
+
agent: z.string().describe("Your agent name (e.g., 'Claude', 'Codex', 'Gemini')"),
|
|
1436
|
+
storyId: z.string().optional().describe("Specific story ID to claim (optional - defaults to next available)"),
|
|
1437
|
+
}, async ({ agent, storyId }) => {
|
|
1438
|
+
try {
|
|
1439
|
+
let story;
|
|
1440
|
+
if (storyId) {
|
|
1441
|
+
const prd = ralphManager.readPRD();
|
|
1442
|
+
story = prd?.userStories.find((s) => s.id === storyId && !s.passes);
|
|
1443
|
+
if (!story) {
|
|
1444
|
+
return {
|
|
1445
|
+
content: [
|
|
1446
|
+
{
|
|
1447
|
+
type: "text",
|
|
1448
|
+
text: `Story ${storyId} not found or already complete.`,
|
|
1449
|
+
},
|
|
1450
|
+
],
|
|
1451
|
+
isError: true,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
story = ralphManager.getNextStory();
|
|
1457
|
+
}
|
|
1458
|
+
if (!story) {
|
|
1459
|
+
return {
|
|
1460
|
+
content: [
|
|
1461
|
+
{
|
|
1462
|
+
type: "text",
|
|
1463
|
+
text: "<promise>COMPLETE</promise>\nAll stories are complete!",
|
|
1464
|
+
},
|
|
1465
|
+
],
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
// Assign in workflow state (atomic operation to prevent race conditions)
|
|
1469
|
+
const assignResult = await workflowManager.assignStory(story.id, agent);
|
|
1470
|
+
if (!assignResult.success) {
|
|
1471
|
+
return {
|
|
1472
|
+
content: [
|
|
1473
|
+
{
|
|
1474
|
+
type: "text",
|
|
1475
|
+
text: assignResult.existingAgent
|
|
1476
|
+
? `Story ${story.id} is already being worked on by ${assignResult.existingAgent}.`
|
|
1477
|
+
: assignResult.error || `Failed to assign story ${story.id} - it may have been claimed by another agent.`,
|
|
1478
|
+
},
|
|
1479
|
+
],
|
|
1480
|
+
isError: true,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
// Post claim message to chat
|
|
1484
|
+
await chatManager.writeMessage({
|
|
1485
|
+
agent,
|
|
1486
|
+
type: "STORY_CLAIMED",
|
|
1487
|
+
content: `Claiming story: ${story.id} - ${story.title}\n\nDescription: ${story.description}\n\nAcceptance Criteria:\n${story.acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`,
|
|
1488
|
+
});
|
|
1489
|
+
return {
|
|
1490
|
+
content: [
|
|
1491
|
+
{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
text: `Claimed story ${story.id}: ${story.title}\n\n${JSON.stringify(story, null, 2)}`,
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
catch (error) {
|
|
1499
|
+
return {
|
|
1500
|
+
content: [
|
|
1501
|
+
{
|
|
1502
|
+
type: "text",
|
|
1503
|
+
text: `Error claiming story: ${error instanceof Error ? error.message : String(error)}`,
|
|
1504
|
+
},
|
|
1505
|
+
],
|
|
1506
|
+
isError: true,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
// Tool: complete_story
|
|
1511
|
+
server.tool("complete_story", "Mark a story complete and notify other agents via chat. Combines marking complete and posting STORY_COMPLETE message.", {
|
|
1512
|
+
agent: z.string().describe("Your agent name"),
|
|
1513
|
+
storyId: z.string().describe("The story ID to mark complete"),
|
|
1514
|
+
summary: z.string().describe("Brief summary of what was implemented"),
|
|
1515
|
+
filesModified: z.array(z.string()).optional().describe("List of files modified"),
|
|
1516
|
+
learnings: z.string().optional().describe("Learnings/gotchas for future iterations"),
|
|
1517
|
+
}, async ({ agent, storyId, summary, filesModified, learnings }) => {
|
|
1518
|
+
try {
|
|
1519
|
+
// Validate that this agent owns the story assignment
|
|
1520
|
+
const isOwner = workflowManager.isAgentOwner(storyId, agent);
|
|
1521
|
+
const existingAssignment = workflowManager.isStoryAssigned(storyId);
|
|
1522
|
+
if (existingAssignment && !isOwner) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [
|
|
1525
|
+
{
|
|
1526
|
+
type: "text",
|
|
1527
|
+
text: `Cannot complete story ${storyId} - it is assigned to ${existingAssignment.agent}, not ${agent}.`,
|
|
1528
|
+
},
|
|
1529
|
+
],
|
|
1530
|
+
isError: true,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const story = ralphManager.markStoryComplete(storyId, summary);
|
|
1534
|
+
if (!story) {
|
|
1535
|
+
return {
|
|
1536
|
+
content: [
|
|
1537
|
+
{
|
|
1538
|
+
type: "text",
|
|
1539
|
+
text: `Story ${storyId} not found.`,
|
|
1540
|
+
},
|
|
1541
|
+
],
|
|
1542
|
+
isError: true,
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
// Mark assignment as completed in workflow state
|
|
1546
|
+
await workflowManager.completeAssignment(storyId);
|
|
1547
|
+
// Build progress entry
|
|
1548
|
+
let progressEntry = `Story: ${storyId} - ${story.title}\nAgent: ${agent}\n\n${summary}`;
|
|
1549
|
+
if (filesModified && filesModified.length > 0) {
|
|
1550
|
+
progressEntry += `\n\nFiles Modified:\n${filesModified.map((f) => `- ${f}`).join("\n")}`;
|
|
1551
|
+
}
|
|
1552
|
+
if (learnings) {
|
|
1553
|
+
progressEntry += `\n\n**Learnings for future iterations:**\n${learnings}`;
|
|
1554
|
+
}
|
|
1555
|
+
// Append to progress file
|
|
1556
|
+
ralphManager.appendProgress(progressEntry);
|
|
1557
|
+
// Post completion message to chat
|
|
1558
|
+
let chatContent = `Completed: ${storyId} - ${story.title}\n\n${summary}`;
|
|
1559
|
+
if (filesModified && filesModified.length > 0) {
|
|
1560
|
+
chatContent += `\n\nFiles: ${filesModified.join(", ")}`;
|
|
1561
|
+
}
|
|
1562
|
+
await chatManager.writeMessage({
|
|
1563
|
+
agent,
|
|
1564
|
+
type: "STORY_COMPLETE",
|
|
1565
|
+
content: chatContent,
|
|
1566
|
+
});
|
|
1567
|
+
// Auto-create savepoint
|
|
1568
|
+
const savepoint = ralphManager.createSavepoint(storyId, `Completed: ${storyId} - ${story.title}`);
|
|
1569
|
+
const prdSummary = ralphManager.getCompletionSummary();
|
|
1570
|
+
const allWorkDone = ralphManager.isAllWorkComplete();
|
|
1571
|
+
// If all work is done, notify READY_TO_TEST
|
|
1572
|
+
if (allWorkDone) {
|
|
1573
|
+
await chatManager.writeMessage({
|
|
1574
|
+
agent: "NightShift",
|
|
1575
|
+
type: "READY_TO_TEST",
|
|
1576
|
+
content: `All stories and bugs complete!\n\nStories: ${prdSummary.complete}/${prdSummary.total}\n\nReady for human review and testing.`,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
let response = `Story ${storyId} completed!\n\nProgress: ${prdSummary.complete}/${prdSummary.total} (${prdSummary.percentComplete}%)`;
|
|
1580
|
+
if (savepoint.success) {
|
|
1581
|
+
response += `\nSavepoint: ${savepoint.tag}`;
|
|
1582
|
+
}
|
|
1583
|
+
if (allWorkDone) {
|
|
1584
|
+
response += "\n\n<promise>COMPLETE</promise>\nAll work complete! Ready to test.";
|
|
1585
|
+
}
|
|
1586
|
+
return {
|
|
1587
|
+
content: [
|
|
1588
|
+
{
|
|
1589
|
+
type: "text",
|
|
1590
|
+
text: response,
|
|
1591
|
+
},
|
|
1592
|
+
],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
catch (error) {
|
|
1596
|
+
return {
|
|
1597
|
+
content: [
|
|
1598
|
+
{
|
|
1599
|
+
type: "text",
|
|
1600
|
+
text: `Error completing story: ${error instanceof Error ? error.message : String(error)}`,
|
|
1601
|
+
},
|
|
1602
|
+
],
|
|
1603
|
+
isError: true,
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
// =============================================================================
|
|
1608
|
+
// Bug Management Tools
|
|
1609
|
+
// =============================================================================
|
|
1610
|
+
// Tool: read_bugs
|
|
1611
|
+
server.tool("read_bugs", "Read the bugs.json file. Returns list of bugs with their fixed status.", {}, async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
if (!ralphManager.hasBugs()) {
|
|
1614
|
+
return {
|
|
1615
|
+
content: [
|
|
1616
|
+
{
|
|
1617
|
+
type: "text",
|
|
1618
|
+
text: "No bugs.json found. Create one to track bugs.",
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const bugList = ralphManager.readBugs();
|
|
1624
|
+
const summary = ralphManager.getBugsSummary();
|
|
1625
|
+
return {
|
|
1626
|
+
content: [
|
|
1627
|
+
{
|
|
1628
|
+
type: "text",
|
|
1629
|
+
text: JSON.stringify({ bugs: bugList, summary }, null, 2),
|
|
1630
|
+
},
|
|
1631
|
+
],
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
return {
|
|
1636
|
+
content: [
|
|
1637
|
+
{
|
|
1638
|
+
type: "text",
|
|
1639
|
+
text: `Error reading bugs: ${error instanceof Error ? error.message : String(error)}`,
|
|
1640
|
+
},
|
|
1641
|
+
],
|
|
1642
|
+
isError: true,
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
// Tool: get_next_bug
|
|
1647
|
+
server.tool("get_next_bug", "Get the next bug to fix (highest priority unfixed bug).", {}, async () => {
|
|
1648
|
+
try {
|
|
1649
|
+
const bug = ralphManager.getNextBug();
|
|
1650
|
+
if (!bug) {
|
|
1651
|
+
const summary = ralphManager.getBugsSummary();
|
|
1652
|
+
if (summary.total === 0) {
|
|
1653
|
+
return {
|
|
1654
|
+
content: [
|
|
1655
|
+
{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: "No bugs.json found or no bugs defined.",
|
|
1658
|
+
},
|
|
1659
|
+
],
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
content: [
|
|
1664
|
+
{
|
|
1665
|
+
type: "text",
|
|
1666
|
+
text: "All bugs are fixed!",
|
|
1667
|
+
},
|
|
1668
|
+
],
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
content: [
|
|
1673
|
+
{
|
|
1674
|
+
type: "text",
|
|
1675
|
+
text: JSON.stringify(bug, null, 2),
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
return {
|
|
1682
|
+
content: [
|
|
1683
|
+
{
|
|
1684
|
+
type: "text",
|
|
1685
|
+
text: `Error getting next bug: ${error instanceof Error ? error.message : String(error)}`,
|
|
1686
|
+
},
|
|
1687
|
+
],
|
|
1688
|
+
isError: true,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
// Tool: claim_bug
|
|
1693
|
+
server.tool("claim_bug", "Claim a bug to work on and notify other agents via chat.", {
|
|
1694
|
+
agent: z.string().describe("Your agent name"),
|
|
1695
|
+
bugId: z.string().optional().describe("Specific bug ID to claim (optional - defaults to next available)"),
|
|
1696
|
+
}, async ({ agent, bugId }) => {
|
|
1697
|
+
try {
|
|
1698
|
+
let bug;
|
|
1699
|
+
if (bugId) {
|
|
1700
|
+
bug = ralphManager.getBug(bugId);
|
|
1701
|
+
if (!bug || bug.fixed) {
|
|
1702
|
+
return {
|
|
1703
|
+
content: [
|
|
1704
|
+
{
|
|
1705
|
+
type: "text",
|
|
1706
|
+
text: `Bug ${bugId} not found or already fixed.`,
|
|
1707
|
+
},
|
|
1708
|
+
],
|
|
1709
|
+
isError: true,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
bug = ralphManager.getNextBug();
|
|
1715
|
+
}
|
|
1716
|
+
if (!bug) {
|
|
1717
|
+
return {
|
|
1718
|
+
content: [
|
|
1719
|
+
{
|
|
1720
|
+
type: "text",
|
|
1721
|
+
text: "No unfixed bugs available.",
|
|
1722
|
+
},
|
|
1723
|
+
],
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
// Post claim message to chat
|
|
1727
|
+
await chatManager.writeMessage({
|
|
1728
|
+
agent,
|
|
1729
|
+
type: "BUG_CLAIMED",
|
|
1730
|
+
content: `Claiming bug: ${bug.id} - ${bug.title}\n\nDescription: ${bug.description}${bug.stepsToReproduce ? `\n\nSteps to reproduce:\n${bug.stepsToReproduce.map((s) => `- ${s}`).join("\n")}` : ""}`,
|
|
1731
|
+
});
|
|
1732
|
+
return {
|
|
1733
|
+
content: [
|
|
1734
|
+
{
|
|
1735
|
+
type: "text",
|
|
1736
|
+
text: `Claimed bug ${bug.id}: ${bug.title}\n\n${JSON.stringify(bug, null, 2)}`,
|
|
1737
|
+
},
|
|
1738
|
+
],
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
catch (error) {
|
|
1742
|
+
return {
|
|
1743
|
+
content: [
|
|
1744
|
+
{
|
|
1745
|
+
type: "text",
|
|
1746
|
+
text: `Error claiming bug: ${error instanceof Error ? error.message : String(error)}`,
|
|
1747
|
+
},
|
|
1748
|
+
],
|
|
1749
|
+
isError: true,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
// Tool: mark_bug_fixed
|
|
1754
|
+
server.tool("mark_bug_fixed", "Mark a bug as fixed, create savepoint, and notify via chat. If all work is done, sends READY_TO_TEST.", {
|
|
1755
|
+
agent: z.string().describe("Your agent name"),
|
|
1756
|
+
bugId: z.string().describe("The bug ID to mark fixed"),
|
|
1757
|
+
summary: z.string().describe("Brief summary of the fix"),
|
|
1758
|
+
filesModified: z.array(z.string()).optional().describe("List of files modified"),
|
|
1759
|
+
}, async ({ agent, bugId, summary, filesModified }) => {
|
|
1760
|
+
try {
|
|
1761
|
+
const bug = ralphManager.markBugFixed(bugId, summary);
|
|
1762
|
+
if (!bug) {
|
|
1763
|
+
return {
|
|
1764
|
+
content: [
|
|
1765
|
+
{
|
|
1766
|
+
type: "text",
|
|
1767
|
+
text: `Bug ${bugId} not found.`,
|
|
1768
|
+
},
|
|
1769
|
+
],
|
|
1770
|
+
isError: true,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
// Log to progress
|
|
1774
|
+
let progressEntry = `Bug Fixed: ${bugId} - ${bug.title}\nAgent: ${agent}\n\n${summary}`;
|
|
1775
|
+
if (filesModified && filesModified.length > 0) {
|
|
1776
|
+
progressEntry += `\n\nFiles Modified:\n${filesModified.map((f) => `- ${f}`).join("\n")}`;
|
|
1777
|
+
}
|
|
1778
|
+
ralphManager.appendProgress(progressEntry);
|
|
1779
|
+
// Post to chat
|
|
1780
|
+
let chatContent = `Fixed: ${bugId} - ${bug.title}\n\n${summary}`;
|
|
1781
|
+
if (filesModified && filesModified.length > 0) {
|
|
1782
|
+
chatContent += `\n\nFiles: ${filesModified.join(", ")}`;
|
|
1783
|
+
}
|
|
1784
|
+
await chatManager.writeMessage({
|
|
1785
|
+
agent,
|
|
1786
|
+
type: "BUG_FIXED",
|
|
1787
|
+
content: chatContent,
|
|
1788
|
+
});
|
|
1789
|
+
// Auto-create savepoint
|
|
1790
|
+
const savepoint = ralphManager.createSavepoint(bugId, `Fixed: ${bugId} - ${bug.title}`);
|
|
1791
|
+
const bugsSummary = ralphManager.getBugsSummary();
|
|
1792
|
+
const allWorkDone = ralphManager.isAllWorkComplete();
|
|
1793
|
+
// If all work is done, notify READY_TO_TEST
|
|
1794
|
+
if (allWorkDone) {
|
|
1795
|
+
const prdSummary = ralphManager.getCompletionSummary();
|
|
1796
|
+
await chatManager.writeMessage({
|
|
1797
|
+
agent: "NightShift",
|
|
1798
|
+
type: "READY_TO_TEST",
|
|
1799
|
+
content: `All stories and bugs complete!\n\nStories: ${prdSummary.complete}/${prdSummary.total}\nBugs: ${bugsSummary.fixed}/${bugsSummary.total}\n\nReady for human review and testing.`,
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
let response = `Bug ${bugId} fixed!\n\nBugs: ${bugsSummary.fixed}/${bugsSummary.total} (${bugsSummary.percentFixed}%)`;
|
|
1803
|
+
if (savepoint.success) {
|
|
1804
|
+
response += `\nSavepoint: ${savepoint.tag}`;
|
|
1805
|
+
}
|
|
1806
|
+
if (allWorkDone) {
|
|
1807
|
+
response += "\n\n<promise>COMPLETE</promise>\nAll work complete! Ready to test.";
|
|
1808
|
+
}
|
|
1809
|
+
return {
|
|
1810
|
+
content: [
|
|
1811
|
+
{
|
|
1812
|
+
type: "text",
|
|
1813
|
+
text: response,
|
|
1814
|
+
},
|
|
1815
|
+
],
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
catch (error) {
|
|
1819
|
+
return {
|
|
1820
|
+
content: [
|
|
1821
|
+
{
|
|
1822
|
+
type: "text",
|
|
1823
|
+
text: `Error marking bug fixed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1824
|
+
},
|
|
1825
|
+
],
|
|
1826
|
+
isError: true,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
// =============================================================================
|
|
1831
|
+
// Savepoint Tools
|
|
1832
|
+
// =============================================================================
|
|
1833
|
+
// Tool: create_savepoint
|
|
1834
|
+
server.tool("create_savepoint", "Create a manual savepoint (git commit + tag). Use for checkpoints during work.", {
|
|
1835
|
+
label: z.string().describe("Label for the savepoint (e.g., 'auth-working', 'pre-refactor')"),
|
|
1836
|
+
message: z.string().optional().describe("Optional commit message"),
|
|
1837
|
+
}, async ({ label, message }) => {
|
|
1838
|
+
try {
|
|
1839
|
+
const result = ralphManager.createSavepoint(label, message);
|
|
1840
|
+
if (!result.success) {
|
|
1841
|
+
return {
|
|
1842
|
+
content: [
|
|
1843
|
+
{
|
|
1844
|
+
type: "text",
|
|
1845
|
+
text: `Failed to create savepoint: ${result.error}`,
|
|
1846
|
+
},
|
|
1847
|
+
],
|
|
1848
|
+
isError: true,
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
content: [
|
|
1853
|
+
{
|
|
1854
|
+
type: "text",
|
|
1855
|
+
text: `Savepoint created: ${result.tag}\n\nRollback with: rollback_savepoint("${label}")`,
|
|
1856
|
+
},
|
|
1857
|
+
],
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
return {
|
|
1862
|
+
content: [
|
|
1863
|
+
{
|
|
1864
|
+
type: "text",
|
|
1865
|
+
text: `Error creating savepoint: ${error instanceof Error ? error.message : String(error)}`,
|
|
1866
|
+
},
|
|
1867
|
+
],
|
|
1868
|
+
isError: true,
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
// Tool: list_savepoints
|
|
1873
|
+
server.tool("list_savepoints", "List all savepoints (git tags with savepoint/ prefix).", {}, async () => {
|
|
1874
|
+
try {
|
|
1875
|
+
const savepoints = ralphManager.listSavepoints();
|
|
1876
|
+
if (savepoints.length === 0) {
|
|
1877
|
+
return {
|
|
1878
|
+
content: [
|
|
1879
|
+
{
|
|
1880
|
+
type: "text",
|
|
1881
|
+
text: "No savepoints found.",
|
|
1882
|
+
},
|
|
1883
|
+
],
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
return {
|
|
1887
|
+
content: [
|
|
1888
|
+
{
|
|
1889
|
+
type: "text",
|
|
1890
|
+
text: `Savepoints:\n${savepoints.map((s) => ` - ${s}`).join("\n")}`,
|
|
1891
|
+
},
|
|
1892
|
+
],
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
catch (error) {
|
|
1896
|
+
return {
|
|
1897
|
+
content: [
|
|
1898
|
+
{
|
|
1899
|
+
type: "text",
|
|
1900
|
+
text: `Error listing savepoints: ${error instanceof Error ? error.message : String(error)}`,
|
|
1901
|
+
},
|
|
1902
|
+
],
|
|
1903
|
+
isError: true,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
// Tool: rollback_savepoint
|
|
1908
|
+
server.tool("rollback_savepoint", "Rollback to a previous savepoint. WARNING: This discards all changes after the savepoint.", {
|
|
1909
|
+
label: z.string().describe("Savepoint label to rollback to (e.g., 'US-001' or 'auth-working')"),
|
|
1910
|
+
}, async ({ label }) => {
|
|
1911
|
+
try {
|
|
1912
|
+
const result = ralphManager.rollbackToSavepoint(label);
|
|
1913
|
+
if (!result.success) {
|
|
1914
|
+
return {
|
|
1915
|
+
content: [
|
|
1916
|
+
{
|
|
1917
|
+
type: "text",
|
|
1918
|
+
text: `Failed to rollback: ${result.error}`,
|
|
1919
|
+
},
|
|
1920
|
+
],
|
|
1921
|
+
isError: true,
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
// Notify via chat
|
|
1925
|
+
await chatManager.writeMessage({
|
|
1926
|
+
agent: "NightShift",
|
|
1927
|
+
type: "INFO",
|
|
1928
|
+
content: `Rolled back to savepoint: ${label}`,
|
|
1929
|
+
});
|
|
1930
|
+
return {
|
|
1931
|
+
content: [
|
|
1932
|
+
{
|
|
1933
|
+
type: "text",
|
|
1934
|
+
text: `Rolled back to savepoint/${label}\n\nAll changes after this point have been discarded.`,
|
|
1935
|
+
},
|
|
1936
|
+
],
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
catch (error) {
|
|
1940
|
+
return {
|
|
1941
|
+
content: [
|
|
1942
|
+
{
|
|
1943
|
+
type: "text",
|
|
1944
|
+
text: `Error rolling back: ${error instanceof Error ? error.message : String(error)}`,
|
|
1945
|
+
},
|
|
1946
|
+
],
|
|
1947
|
+
isError: true,
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
// =============================================================================
|
|
1952
|
+
// Setup & Debug Tools
|
|
1953
|
+
// =============================================================================
|
|
1954
|
+
// Tool: nightshift_setup
|
|
1955
|
+
server.tool("nightshift_setup", "Get setup instructions and check project configuration for NightShift MCP. Use this to understand how to configure agents or troubleshoot setup issues.", {
|
|
1956
|
+
showExamples: z.boolean().optional().describe("Include example prd.json and bugs.json templates"),
|
|
1957
|
+
}, async ({ showExamples }) => {
|
|
1958
|
+
try {
|
|
1959
|
+
const checks = [];
|
|
1960
|
+
const issues = [];
|
|
1961
|
+
const suggestions = [];
|
|
1962
|
+
// Check project path
|
|
1963
|
+
checks.push(`Project path: ${PROJECT_PATH}`);
|
|
1964
|
+
// Check for prd.json
|
|
1965
|
+
if (ralphManager.hasPRD()) {
|
|
1966
|
+
const summary = ralphManager.getCompletionSummary();
|
|
1967
|
+
checks.push(`✓ prd.json found (${summary.complete}/${summary.total} stories complete)`);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
issues.push("✗ No prd.json found");
|
|
1971
|
+
suggestions.push("Create prd.json with user stories to enable task management");
|
|
1972
|
+
}
|
|
1973
|
+
// Check for bugs.json
|
|
1974
|
+
if (ralphManager.hasBugs()) {
|
|
1975
|
+
const summary = ralphManager.getBugsSummary();
|
|
1976
|
+
checks.push(`✓ bugs.json found (${summary.fixed}/${summary.total} bugs fixed)`);
|
|
1977
|
+
}
|
|
1978
|
+
else {
|
|
1979
|
+
checks.push("○ No bugs.json (optional - create when testing finds issues)");
|
|
1980
|
+
}
|
|
1981
|
+
// Check for .robot-chat directory
|
|
1982
|
+
const chatPath = chatManager.getChatFilePath();
|
|
1983
|
+
if (fs.existsSync(chatPath)) {
|
|
1984
|
+
checks.push("✓ Chat file exists");
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
checks.push("○ Chat file will be created on first message");
|
|
1988
|
+
}
|
|
1989
|
+
// Check for .gitignore entry
|
|
1990
|
+
const gitignorePath = path.join(PROJECT_PATH, ".gitignore");
|
|
1991
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1992
|
+
const gitignore = fs.readFileSync(gitignorePath, "utf-8");
|
|
1993
|
+
if (gitignore.includes(".robot-chat")) {
|
|
1994
|
+
checks.push("✓ .robot-chat in .gitignore");
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
issues.push("✗ .robot-chat not in .gitignore");
|
|
1998
|
+
suggestions.push("Add '.robot-chat/' to .gitignore to prevent committing chat logs");
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
else {
|
|
2002
|
+
issues.push("✗ No .gitignore file");
|
|
2003
|
+
suggestions.push("Create .gitignore and add '.robot-chat/'");
|
|
2004
|
+
}
|
|
2005
|
+
// Check git repo
|
|
2006
|
+
try {
|
|
2007
|
+
const { execSync } = await import("child_process");
|
|
2008
|
+
execSync("git rev-parse --git-dir", { cwd: PROJECT_PATH, stdio: "pipe" });
|
|
2009
|
+
checks.push("✓ Git repository initialized");
|
|
2010
|
+
}
|
|
2011
|
+
catch {
|
|
2012
|
+
issues.push("✗ Not a git repository");
|
|
2013
|
+
suggestions.push("Initialize git with 'git init' for savepoints to work");
|
|
2014
|
+
}
|
|
2015
|
+
// Check available agents
|
|
2016
|
+
const agents = await getAvailableAgents();
|
|
2017
|
+
if (agents.length > 0) {
|
|
2018
|
+
checks.push(`✓ Available agents: ${agents.join(", ")}`);
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
issues.push("✗ No agents available");
|
|
2022
|
+
suggestions.push("Install at least one agent CLI (claude, codex, gemini, or vibe)");
|
|
2023
|
+
}
|
|
2024
|
+
// Build response
|
|
2025
|
+
let response = `# NightShift Setup Status\n\n`;
|
|
2026
|
+
response += `## Checks\n${checks.join("\n")}\n\n`;
|
|
2027
|
+
if (issues.length > 0) {
|
|
2028
|
+
response += `## Issues\n${issues.join("\n")}\n\n`;
|
|
2029
|
+
}
|
|
2030
|
+
if (suggestions.length > 0) {
|
|
2031
|
+
response += `## Suggestions\n${suggestions.map(s => `- ${s}`).join("\n")}\n\n`;
|
|
2032
|
+
}
|
|
2033
|
+
response += `## Agent Configuration\n\n`;
|
|
2034
|
+
response += `**Claude Code** (~/.claude.json):\n\`\`\`json
|
|
2035
|
+
{
|
|
2036
|
+
"mcpServers": {
|
|
2037
|
+
"nightshift": {
|
|
2038
|
+
"command": "nightshift-mcp",
|
|
2039
|
+
"args": ["${PROJECT_PATH}"]
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
\`\`\`\n\n`;
|
|
2044
|
+
response += `**Codex** (~/.codex/config.toml):\n\`\`\`toml
|
|
2045
|
+
[mcp_servers.nightshift]
|
|
2046
|
+
command = "nightshift-mcp"
|
|
2047
|
+
args = ["${PROJECT_PATH}"]
|
|
2048
|
+
\`\`\`\n\n`;
|
|
2049
|
+
if (showExamples) {
|
|
2050
|
+
response += `## Example Templates\n\n`;
|
|
2051
|
+
response += `**prd.json**:\n\`\`\`json
|
|
2052
|
+
{
|
|
2053
|
+
"project": "MyProject",
|
|
2054
|
+
"description": "Project description",
|
|
2055
|
+
"userStories": [
|
|
2056
|
+
{
|
|
2057
|
+
"id": "US-001",
|
|
2058
|
+
"title": "First feature",
|
|
2059
|
+
"description": "Implement the first feature",
|
|
2060
|
+
"acceptanceCriteria": [
|
|
2061
|
+
"Criteria 1",
|
|
2062
|
+
"Criteria 2"
|
|
2063
|
+
],
|
|
2064
|
+
"priority": 1,
|
|
2065
|
+
"passes": false,
|
|
2066
|
+
"notes": ""
|
|
2067
|
+
}
|
|
2068
|
+
]
|
|
2069
|
+
}
|
|
2070
|
+
\`\`\`\n\n`;
|
|
2071
|
+
response += `**bugs.json**:\n\`\`\`json
|
|
2072
|
+
{
|
|
2073
|
+
"project": "MyProject",
|
|
2074
|
+
"bugs": [
|
|
2075
|
+
{
|
|
2076
|
+
"id": "BUG-001",
|
|
2077
|
+
"title": "Bug description",
|
|
2078
|
+
"description": "Detailed bug description",
|
|
2079
|
+
"stepsToReproduce": [
|
|
2080
|
+
"Step 1",
|
|
2081
|
+
"Step 2"
|
|
2082
|
+
],
|
|
2083
|
+
"priority": 1,
|
|
2084
|
+
"fixed": false
|
|
2085
|
+
}
|
|
2086
|
+
]
|
|
2087
|
+
}
|
|
2088
|
+
\`\`\`\n`;
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
content: [
|
|
2092
|
+
{
|
|
2093
|
+
type: "text",
|
|
2094
|
+
text: response,
|
|
2095
|
+
},
|
|
2096
|
+
],
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
catch (error) {
|
|
2100
|
+
return {
|
|
2101
|
+
content: [
|
|
2102
|
+
{
|
|
2103
|
+
type: "text",
|
|
2104
|
+
text: `Error getting setup info: ${error instanceof Error ? error.message : String(error)}`,
|
|
2105
|
+
},
|
|
2106
|
+
],
|
|
2107
|
+
isError: true,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
// Tool: nightshift_debug
|
|
2112
|
+
server.tool("nightshift_debug", "Diagnose issues with NightShift MCP. Checks file permissions, validates JSON files, reviews recent errors, and provides troubleshooting guidance.", {}, async () => {
|
|
2113
|
+
try {
|
|
2114
|
+
const diagnostics = [];
|
|
2115
|
+
const errors = [];
|
|
2116
|
+
const fixes = [];
|
|
2117
|
+
diagnostics.push(`# NightShift Debug Report\n`);
|
|
2118
|
+
diagnostics.push(`Timestamp: ${new Date().toISOString()}`);
|
|
2119
|
+
diagnostics.push(`Project: ${PROJECT_PATH}\n`);
|
|
2120
|
+
// 1. Check file system
|
|
2121
|
+
diagnostics.push(`## File System Checks`);
|
|
2122
|
+
// Check project directory exists and is writable
|
|
2123
|
+
try {
|
|
2124
|
+
fs.accessSync(PROJECT_PATH, fs.constants.W_OK);
|
|
2125
|
+
diagnostics.push(`✓ Project directory is writable`);
|
|
2126
|
+
}
|
|
2127
|
+
catch {
|
|
2128
|
+
errors.push(`✗ Project directory is not writable`);
|
|
2129
|
+
fixes.push(`Check permissions on ${PROJECT_PATH}`);
|
|
2130
|
+
}
|
|
2131
|
+
// Check .robot-chat directory
|
|
2132
|
+
const robotChatDir = path.join(PROJECT_PATH, ".robot-chat");
|
|
2133
|
+
if (fs.existsSync(robotChatDir)) {
|
|
2134
|
+
try {
|
|
2135
|
+
fs.accessSync(robotChatDir, fs.constants.W_OK);
|
|
2136
|
+
diagnostics.push(`✓ .robot-chat directory is writable`);
|
|
2137
|
+
}
|
|
2138
|
+
catch {
|
|
2139
|
+
errors.push(`✗ .robot-chat directory is not writable`);
|
|
2140
|
+
fixes.push(`Run: chmod 755 ${robotChatDir}`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
else {
|
|
2144
|
+
diagnostics.push(`○ .robot-chat directory doesn't exist (will be created)`);
|
|
2145
|
+
}
|
|
2146
|
+
// 2. Validate JSON files
|
|
2147
|
+
diagnostics.push(`\n## JSON Validation`);
|
|
2148
|
+
// Validate prd.json
|
|
2149
|
+
if (ralphManager.hasPRD()) {
|
|
2150
|
+
try {
|
|
2151
|
+
const prd = ralphManager.readPRD();
|
|
2152
|
+
if (prd && prd.userStories && Array.isArray(prd.userStories)) {
|
|
2153
|
+
diagnostics.push(`✓ prd.json is valid (${prd.userStories.length} stories)`);
|
|
2154
|
+
// Check for common issues
|
|
2155
|
+
const storiesWithoutId = prd.userStories.filter(s => !s.id);
|
|
2156
|
+
if (storiesWithoutId.length > 0) {
|
|
2157
|
+
errors.push(`✗ ${storiesWithoutId.length} stories missing 'id' field`);
|
|
2158
|
+
fixes.push(`Add unique 'id' to each story (e.g., "US-001")`);
|
|
2159
|
+
}
|
|
2160
|
+
const storiesWithoutPriority = prd.userStories.filter(s => s.priority === undefined);
|
|
2161
|
+
if (storiesWithoutPriority.length > 0) {
|
|
2162
|
+
errors.push(`✗ ${storiesWithoutPriority.length} stories missing 'priority' field`);
|
|
2163
|
+
fixes.push(`Add 'priority' number to each story (lower = higher priority)`);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
else {
|
|
2167
|
+
errors.push(`✗ prd.json missing 'userStories' array`);
|
|
2168
|
+
fixes.push(`Add "userStories": [] to prd.json`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
catch (e) {
|
|
2172
|
+
errors.push(`✗ prd.json parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
2173
|
+
fixes.push(`Check prd.json for JSON syntax errors`);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
else {
|
|
2177
|
+
diagnostics.push(`○ No prd.json found`);
|
|
2178
|
+
}
|
|
2179
|
+
// Validate bugs.json
|
|
2180
|
+
if (ralphManager.hasBugs()) {
|
|
2181
|
+
try {
|
|
2182
|
+
const bugs = ralphManager.readBugs();
|
|
2183
|
+
if (bugs && bugs.bugs && Array.isArray(bugs.bugs)) {
|
|
2184
|
+
diagnostics.push(`✓ bugs.json is valid (${bugs.bugs.length} bugs)`);
|
|
2185
|
+
}
|
|
2186
|
+
else {
|
|
2187
|
+
errors.push(`✗ bugs.json missing 'bugs' array`);
|
|
2188
|
+
fixes.push(`Add "bugs": [] to bugs.json`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
catch (e) {
|
|
2192
|
+
errors.push(`✗ bugs.json parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
2193
|
+
fixes.push(`Check bugs.json for JSON syntax errors`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
// 3. Check daemon lock
|
|
2197
|
+
diagnostics.push(`\n## Daemon Status`);
|
|
2198
|
+
const lockFile = path.join(robotChatDir, "daemon.lock");
|
|
2199
|
+
if (fs.existsSync(lockFile)) {
|
|
2200
|
+
try {
|
|
2201
|
+
const pid = fs.readFileSync(lockFile, "utf-8").trim();
|
|
2202
|
+
try {
|
|
2203
|
+
process.kill(parseInt(pid), 0);
|
|
2204
|
+
diagnostics.push(`⚠ Daemon is running (PID: ${pid})`);
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
errors.push(`✗ Stale daemon lock file (PID ${pid} not running)`);
|
|
2208
|
+
fixes.push(`Remove stale lock: rm ${lockFile}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
catch {
|
|
2212
|
+
diagnostics.push(`○ No daemon running`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
diagnostics.push(`○ No daemon running`);
|
|
2217
|
+
}
|
|
2218
|
+
// 4. Check recent chat errors
|
|
2219
|
+
diagnostics.push(`\n## Recent Chat Activity`);
|
|
2220
|
+
try {
|
|
2221
|
+
const recentMessages = chatManager.readMessages({ limit: 10 });
|
|
2222
|
+
const errorMessages = recentMessages.filter(m => m.type === "ERROR");
|
|
2223
|
+
const failovers = recentMessages.filter(m => m.type === "FAILOVER_NEEDED");
|
|
2224
|
+
diagnostics.push(`Messages in last 10: ${recentMessages.length}`);
|
|
2225
|
+
if (errorMessages.length > 0) {
|
|
2226
|
+
errors.push(`✗ ${errorMessages.length} ERROR messages in recent chat`);
|
|
2227
|
+
errorMessages.forEach(m => {
|
|
2228
|
+
diagnostics.push(` - [${m.agent}] ${m.content.substring(0, 100)}...`);
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
if (failovers.length > 0) {
|
|
2232
|
+
diagnostics.push(`⚠ ${failovers.length} FAILOVER_NEEDED messages`);
|
|
2233
|
+
const unclaimed = chatManager.findUnclaimedFailovers();
|
|
2234
|
+
if (unclaimed.length > 0) {
|
|
2235
|
+
errors.push(`✗ ${unclaimed.length} unclaimed failovers need attention`);
|
|
2236
|
+
fixes.push(`Claim failovers with claim_failover tool`);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
catch {
|
|
2241
|
+
diagnostics.push(`○ No chat history yet`);
|
|
2242
|
+
}
|
|
2243
|
+
// 5. Agent availability
|
|
2244
|
+
diagnostics.push(`\n## Agent Status`);
|
|
2245
|
+
const status = await getAgentStatus();
|
|
2246
|
+
for (const [agent, info] of Object.entries(status)) {
|
|
2247
|
+
if (info.canRun) {
|
|
2248
|
+
diagnostics.push(`✓ ${agent}: available`);
|
|
2249
|
+
}
|
|
2250
|
+
else if (info.available) {
|
|
2251
|
+
errors.push(`✗ ${agent}: installed but cannot run`);
|
|
2252
|
+
if (info.reason) {
|
|
2253
|
+
fixes.push(`${agent}: ${info.reason}`);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
else {
|
|
2257
|
+
diagnostics.push(`○ ${agent}: not installed`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
// 6. Git status
|
|
2261
|
+
diagnostics.push(`\n## Git Status`);
|
|
2262
|
+
try {
|
|
2263
|
+
const { execSync } = await import("child_process");
|
|
2264
|
+
execSync("git rev-parse --git-dir", { cwd: PROJECT_PATH, stdio: "pipe" });
|
|
2265
|
+
diagnostics.push(`✓ Git repository`);
|
|
2266
|
+
// Check for uncommitted changes
|
|
2267
|
+
const gitStatus = execSync("git status --porcelain", {
|
|
2268
|
+
cwd: PROJECT_PATH,
|
|
2269
|
+
encoding: "utf-8",
|
|
2270
|
+
}).trim();
|
|
2271
|
+
if (gitStatus) {
|
|
2272
|
+
const lines = gitStatus.split("\n").length;
|
|
2273
|
+
diagnostics.push(`⚠ ${lines} uncommitted changes`);
|
|
2274
|
+
}
|
|
2275
|
+
else {
|
|
2276
|
+
diagnostics.push(`✓ Working directory clean`);
|
|
2277
|
+
}
|
|
2278
|
+
// List savepoints
|
|
2279
|
+
const savepoints = ralphManager.listSavepoints();
|
|
2280
|
+
diagnostics.push(`Savepoints: ${savepoints.length}`);
|
|
2281
|
+
}
|
|
2282
|
+
catch {
|
|
2283
|
+
errors.push(`✗ Not a git repository`);
|
|
2284
|
+
fixes.push(`Initialize git: git init`);
|
|
2285
|
+
}
|
|
2286
|
+
// Build response
|
|
2287
|
+
let response = diagnostics.join("\n");
|
|
2288
|
+
if (errors.length > 0) {
|
|
2289
|
+
response += `\n\n## Errors Found (${errors.length})\n${errors.join("\n")}`;
|
|
2290
|
+
}
|
|
2291
|
+
if (fixes.length > 0) {
|
|
2292
|
+
response += `\n\n## Suggested Fixes\n${fixes.map((f, i) => `${i + 1}. ${f}`).join("\n")}`;
|
|
2293
|
+
}
|
|
2294
|
+
if (errors.length === 0) {
|
|
2295
|
+
response += `\n\n✅ No issues found. NightShift is ready to use.`;
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
content: [
|
|
2299
|
+
{
|
|
2300
|
+
type: "text",
|
|
2301
|
+
text: response,
|
|
2302
|
+
},
|
|
2303
|
+
],
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
return {
|
|
2308
|
+
content: [
|
|
2309
|
+
{
|
|
2310
|
+
type: "text",
|
|
2311
|
+
text: `Error running diagnostics: ${error instanceof Error ? error.message : String(error)}`,
|
|
2312
|
+
},
|
|
2313
|
+
],
|
|
2314
|
+
isError: true,
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
// ANSI color codes
|
|
2320
|
+
const colors = {
|
|
2321
|
+
reset: "\x1b[0m",
|
|
2322
|
+
bold: "\x1b[1m",
|
|
2323
|
+
dim: "\x1b[2m",
|
|
2324
|
+
cyan: "\x1b[36m",
|
|
2325
|
+
yellow: "\x1b[33m",
|
|
2326
|
+
green: "\x1b[32m",
|
|
2327
|
+
red: "\x1b[31m",
|
|
2328
|
+
magenta: "\x1b[35m",
|
|
2329
|
+
blue: "\x1b[34m",
|
|
2330
|
+
white: "\x1b[37m",
|
|
2331
|
+
bgBlue: "\x1b[44m",
|
|
2332
|
+
};
|
|
2333
|
+
// Agent installation instructions
|
|
2334
|
+
const AGENT_INSTALL_HINTS = {
|
|
2335
|
+
claude: "npm install -g @anthropic-ai/claude-code",
|
|
2336
|
+
codex: "npm install -g @openai/codex",
|
|
2337
|
+
gemini: "npm install -g @google/gemini-cli",
|
|
2338
|
+
vibe: "pip install mistral-vibe",
|
|
2339
|
+
};
|
|
2340
|
+
// Agent descriptions for the welcome screen
|
|
2341
|
+
const AGENT_ROLES = {
|
|
2342
|
+
claude: "orchestration, complex logic",
|
|
2343
|
+
codex: "code generation, implementation",
|
|
2344
|
+
gemini: "research, planning, review",
|
|
2345
|
+
vibe: "follows detailed specs",
|
|
2346
|
+
};
|
|
2347
|
+
// Print welcome banner to stderr
|
|
2348
|
+
async function printWelcomeBanner() {
|
|
2349
|
+
const c = colors;
|
|
2350
|
+
const status = await getAgentStatus();
|
|
2351
|
+
const agents = Object.entries(status);
|
|
2352
|
+
const installed = agents.filter(([_, s]) => s.canRun).map(([name]) => name);
|
|
2353
|
+
const asciiArt = `
|
|
2354
|
+
${c.blue}${c.bold}
|
|
2355
|
+
███╗ ██╗██╗ ██████╗ ██╗ ██╗████████╗███████╗██╗ ██╗███████╗████████╗
|
|
2356
|
+
████╗ ██║██║██╔════╝ ██║ ██║╚══██╔══╝██╔════╝██║ ██║██╔════╝╚══██╔══╝
|
|
2357
|
+
██╔██╗ ██║██║██║ ███╗███████║ ██║ ███████╗███████║█████╗ ██║
|
|
2358
|
+
██║╚██╗██║██║██║ ██║██╔══██║ ██║ ╚════██║██╔══██║██╔══╝ ██║
|
|
2359
|
+
██║ ╚████║██║╚██████╔╝██║ ██║ ██║ ███████║██║ ██║██║ ██║
|
|
2360
|
+
╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${c.reset}
|
|
2361
|
+
|
|
2362
|
+
${c.dim} The responsible kind of multi-agent chaos.${c.reset}
|
|
2363
|
+
`;
|
|
2364
|
+
const projectInfo = `
|
|
2365
|
+
${c.yellow}${c.bold} ⚡ Project${c.reset}
|
|
2366
|
+
${c.dim}Path:${c.reset} ${PROJECT_PATH}
|
|
2367
|
+
${c.dim}Chat:${c.reset} .robot-chat/chat.txt
|
|
2368
|
+
${c.dim}Mode:${c.reset} ${REGISTRATION_MODE} (${toolCounts.total} actions via ${REGISTRATION_MODE === "minimal" ? "2" : REGISTRATION_MODE === "legacy" ? toolCounts.total : toolCounts.total + 2} tools)
|
|
2369
|
+
`;
|
|
2370
|
+
const agentSection = `
|
|
2371
|
+
${c.green}${c.bold} 🤖 Agents${c.reset} ${c.dim}(${installed.length}/${agents.length} available)${c.reset}
|
|
2372
|
+
${agents.map(([name, s]) => {
|
|
2373
|
+
const icon = s.canRun ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
|
|
2374
|
+
const nameStr = s.canRun ? `${c.bold}${name}${c.reset}` : `${c.dim}${name}${c.reset}`;
|
|
2375
|
+
const roleStr = `${c.dim}${AGENT_ROLES[name]}${c.reset}`;
|
|
2376
|
+
const installHint = !s.available ? `\n ${c.dim}└─ ${AGENT_INSTALL_HINTS[name]}${c.reset}` : '';
|
|
2377
|
+
return ` ${icon} ${nameStr.padEnd(20)} ${roleStr}${installHint}`;
|
|
2378
|
+
}).join('\n')}
|
|
2379
|
+
`;
|
|
2380
|
+
// Check if .robot-chat is in .gitignore
|
|
2381
|
+
let gitignoreWarning = '';
|
|
2382
|
+
try {
|
|
2383
|
+
const gitignorePath = path.join(PROJECT_PATH, '.gitignore');
|
|
2384
|
+
if (fs.existsSync(gitignorePath)) {
|
|
2385
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
2386
|
+
if (!gitignoreContent.includes('.robot-chat')) {
|
|
2387
|
+
gitignoreWarning = `\n${c.yellow} ⚠️ Consider adding .robot-chat/ to your .gitignore${c.reset}\n`;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
else {
|
|
2391
|
+
gitignoreWarning = `\n${c.yellow} ⚠️ No .gitignore found - consider adding .robot-chat/ to prevent committing chat logs${c.reset}\n`;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
catch {
|
|
2395
|
+
// Ignore errors reading .gitignore
|
|
2396
|
+
}
|
|
2397
|
+
const footer = `
|
|
2398
|
+
${c.cyan}─────────────────────────────────────────────────────────────────────────────${c.reset}
|
|
2399
|
+
${c.dim} Ready for multi-agent collaboration!${c.reset}
|
|
2400
|
+
`;
|
|
2401
|
+
console.error(asciiArt + projectInfo + agentSection + gitignoreWarning + footer);
|
|
2402
|
+
}
|
|
2403
|
+
// Register tools based on mode
|
|
2404
|
+
function registerTools() {
|
|
2405
|
+
switch (REGISTRATION_MODE) {
|
|
2406
|
+
case "minimal":
|
|
2407
|
+
// Only meta-tools (2 tools, ~200 tokens)
|
|
2408
|
+
registerMetaTools();
|
|
2409
|
+
break;
|
|
2410
|
+
case "legacy":
|
|
2411
|
+
// Only individual tools (39 tools, ~2,300 tokens)
|
|
2412
|
+
registerIndividualTools();
|
|
2413
|
+
break;
|
|
2414
|
+
case "hybrid":
|
|
2415
|
+
default:
|
|
2416
|
+
// Both meta-tools and individual tools (41 tools)
|
|
2417
|
+
registerMetaTools();
|
|
2418
|
+
registerIndividualTools();
|
|
2419
|
+
break;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
// Start the server
|
|
2423
|
+
async function main() {
|
|
2424
|
+
registerTools();
|
|
2425
|
+
const transport = new StdioServerTransport();
|
|
2426
|
+
await server.connect(transport);
|
|
2427
|
+
await printWelcomeBanner();
|
|
2428
|
+
}
|
|
2429
|
+
main().catch((error) => {
|
|
2430
|
+
console.error("Failed to start server:", error);
|
|
2431
|
+
process.exit(1);
|
|
2432
|
+
});
|
|
2433
|
+
//# sourceMappingURL=index.js.map
|