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.
Files changed (74) hide show
  1. package/README.md +670 -0
  2. package/dist/agent-spawner.d.ts +55 -0
  3. package/dist/agent-spawner.d.ts.map +1 -0
  4. package/dist/agent-spawner.js +468 -0
  5. package/dist/agent-spawner.js.map +1 -0
  6. package/dist/chat-manager.d.ts +72 -0
  7. package/dist/chat-manager.d.ts.map +1 -0
  8. package/dist/chat-manager.js +331 -0
  9. package/dist/chat-manager.js.map +1 -0
  10. package/dist/daemon.d.ts +65 -0
  11. package/dist/daemon.d.ts.map +1 -0
  12. package/dist/daemon.js +563 -0
  13. package/dist/daemon.js.map +1 -0
  14. package/dist/file-lock.d.ts +41 -0
  15. package/dist/file-lock.d.ts.map +1 -0
  16. package/dist/file-lock.js +157 -0
  17. package/dist/file-lock.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2433 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/ralph-manager.d.ts +148 -0
  23. package/dist/ralph-manager.d.ts.map +1 -0
  24. package/dist/ralph-manager.js +399 -0
  25. package/dist/ralph-manager.js.map +1 -0
  26. package/dist/tool-registry.d.ts +130 -0
  27. package/dist/tool-registry.d.ts.map +1 -0
  28. package/dist/tool-registry.js +280 -0
  29. package/dist/tool-registry.js.map +1 -0
  30. package/dist/tools/agents.d.ts +7 -0
  31. package/dist/tools/agents.d.ts.map +1 -0
  32. package/dist/tools/agents.js +366 -0
  33. package/dist/tools/agents.js.map +1 -0
  34. package/dist/tools/bugs.d.ts +6 -0
  35. package/dist/tools/bugs.d.ts.map +1 -0
  36. package/dist/tools/bugs.js +184 -0
  37. package/dist/tools/bugs.js.map +1 -0
  38. package/dist/tools/chat.d.ts +10 -0
  39. package/dist/tools/chat.d.ts.map +1 -0
  40. package/dist/tools/chat.js +287 -0
  41. package/dist/tools/chat.js.map +1 -0
  42. package/dist/tools/index.d.ts +33 -0
  43. package/dist/tools/index.d.ts.map +1 -0
  44. package/dist/tools/index.js +51 -0
  45. package/dist/tools/index.js.map +1 -0
  46. package/dist/tools/prd.d.ts +8 -0
  47. package/dist/tools/prd.d.ts.map +1 -0
  48. package/dist/tools/prd.js +275 -0
  49. package/dist/tools/prd.js.map +1 -0
  50. package/dist/tools/progress.d.ts +5 -0
  51. package/dist/tools/progress.d.ts.map +1 -0
  52. package/dist/tools/progress.js +81 -0
  53. package/dist/tools/progress.js.map +1 -0
  54. package/dist/tools/savepoints.d.ts +5 -0
  55. package/dist/tools/savepoints.d.ts.map +1 -0
  56. package/dist/tools/savepoints.js +100 -0
  57. package/dist/tools/savepoints.js.map +1 -0
  58. package/dist/tools/utility.d.ts +4 -0
  59. package/dist/tools/utility.d.ts.map +1 -0
  60. package/dist/tools/utility.js +375 -0
  61. package/dist/tools/utility.js.map +1 -0
  62. package/dist/tools/workflow.d.ts +10 -0
  63. package/dist/tools/workflow.d.ts.map +1 -0
  64. package/dist/tools/workflow.js +321 -0
  65. package/dist/tools/workflow.js.map +1 -0
  66. package/dist/types.d.ts +105 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +2 -0
  69. package/dist/types.js.map +1 -0
  70. package/dist/workflow-manager.d.ts +154 -0
  71. package/dist/workflow-manager.d.ts.map +1 -0
  72. package/dist/workflow-manager.js +356 -0
  73. package/dist/workflow-manager.js.map +1 -0
  74. 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