agent-collab-mcp 1.2.0 → 1.3.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.
@@ -88,6 +88,9 @@ function apiEpicDetail(db, epicId) {
88
88
  const tasks = db.prepare("SELECT task_id, title, status, owner, context, acceptance, plan, reviews_json, created_at, updated_at FROM epic_tasks WHERE epic_id = ? ORDER BY task_id").all(epicId);
89
89
  return { ...epic, tasks };
90
90
  }
91
+ function apiContextDocs(db) {
92
+ return db.prepare("SELECT key, content, updated_at FROM context_docs ORDER BY key").all();
93
+ }
91
94
  function getProjectName(db) {
92
95
  const row = db.prepare("SELECT value FROM config WHERE key = 'project_name'").get();
93
96
  return row?.value ?? path.basename(process.cwd());
@@ -157,6 +160,9 @@ const server = http.createServer((req, res) => {
157
160
  case "/api/project-name":
158
161
  data = { name: getProjectName(db) };
159
162
  break;
163
+ case "/api/context":
164
+ data = apiContextDocs(db);
165
+ break;
160
166
  default:
161
167
  res.writeHead(404);
162
168
  res.end('{"error":"not found"}');
@@ -400,6 +406,18 @@ main{display:flex;flex-direction:column;gap:1.2rem;padding:1.5rem 2rem;max-width
400
406
  .review-issue{font-size:.75rem;color:var(--fg-1);padding:.2rem 0;font-family:var(--font-mono)}
401
407
  .review-issue .issue-loc{color:var(--accent)}
402
408
 
409
+ /* ── Context Docs ───────────────────────────────────────── */
410
+ .ctx-doc{background:var(--bg-2);border:1px solid var(--bg-3);border-radius:var(--radius-sm);
411
+ padding:.6rem .8rem;margin-bottom:.5rem}
412
+ .ctx-doc .ctx-key{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
413
+ color:var(--accent);margin-bottom:.3rem;cursor:pointer;display:flex;align-items:center;gap:.3rem}
414
+ .ctx-doc .ctx-key::before{content:'\\25B6';font-size:.5rem;transition:transform .2s}
415
+ .ctx-doc.open .ctx-key::before{transform:rotate(90deg)}
416
+ .ctx-doc .ctx-body{font-size:.75rem;color:var(--fg-1);line-height:1.5;display:none;
417
+ white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto;margin-top:.3rem}
418
+ .ctx-doc.open .ctx-body{display:block}
419
+ .ctx-doc .ctx-updated{font-size:.55rem;color:var(--fg-2);font-family:var(--font-mono)}
420
+
403
421
  /* ── Epics ──────────────────────────────────────────────── */
404
422
  .epic-item{
405
423
  background:var(--bg-2);border:1px solid var(--bg-3);border-radius:var(--radius-sm);
@@ -438,6 +456,10 @@ main{display:flex;flex-direction:column;gap:1.2rem;padding:1.5rem 2rem;max-width
438
456
  <h3><span class="icon">&#9881;</span> Strategy</h3>
439
457
  <div id="strategy-content"></div>
440
458
  </div>
459
+ <div class="panel" id="context-panel">
460
+ <h3><span class="icon">&#128196;</span> Context Docs</h3>
461
+ <div id="context-docs"></div>
462
+ </div>
441
463
  <div class="panel" id="epics-panel">
442
464
  <h3><span class="icon">&#128230;</span> Archived Epics</h3>
443
465
  <div id="epics-list"></div>
@@ -602,6 +624,20 @@ document.getElementById('modal-overlay').addEventListener('click', function(e) {
602
624
  });
603
625
  document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
604
626
 
627
+ function renderContextDocs(docs) {
628
+ const el = document.getElementById('context-docs');
629
+ if (!docs || docs.length === 0) {
630
+ el.innerHTML = '<div class="empty-state">No context docs yet</div>';
631
+ return;
632
+ }
633
+ el.innerHTML = docs.map(d =>
634
+ '<div class="ctx-doc" onclick="this.classList.toggle(\\'open\\')">'+
635
+ '<div class="ctx-key">'+escHtml(d.key).toUpperCase()+' <span class="ctx-updated">'+escHtml(d.updated_at||'')+'</span></div>'+
636
+ '<div class="ctx-body">'+escHtml(d.content)+'</div>'+
637
+ '</div>'
638
+ ).join('');
639
+ }
640
+
605
641
  function renderEpics(epics) {
606
642
  const el = document.getElementById('epics-list');
607
643
  if (!epics || epics.length === 0) {
@@ -655,16 +691,18 @@ async function openEpic(id) {
655
691
 
656
692
  async function refresh() {
657
693
  try {
658
- const [overview, tasks, activity, epics, proj] = await Promise.all([
694
+ const [overview, tasks, activity, epics, proj, ctxDocs] = await Promise.all([
659
695
  fetchJSON('/api/overview'),
660
696
  fetchJSON('/api/tasks'),
661
697
  fetchJSON('/api/activity?limit=30'),
662
698
  fetchJSON('/api/epics'),
663
699
  fetchJSON('/api/project-name'),
700
+ fetchJSON('/api/context'),
664
701
  ]);
665
702
  renderStats(overview);
666
703
  renderKanban(tasks);
667
704
  renderStrategy(overview);
705
+ renderContextDocs(ctxDocs);
668
706
  renderActivity(activity);
669
707
  renderEpics(epics);
670
708
  if (proj && proj.name) document.getElementById('h-project').textContent = proj.name;
package/build/index.js CHANGED
@@ -20,7 +20,7 @@ else {
20
20
  instructions = [
21
21
  "This project hasn't been set up for agent collaboration yet.",
22
22
  "Call get_my_status to see setup instructions, or call setup_project directly.",
23
- "Available tools: get_my_status, setup_project, list_strategies, get_dashboard_info.",
23
+ "All tools are visible but most require setup first.",
24
24
  ].join(" ");
25
25
  }
26
26
  else {
@@ -31,32 +31,16 @@ else {
31
31
  }
32
32
  const server = new McpServer({
33
33
  name: "agent-collab",
34
- version: "1.2.0",
34
+ version: "1.3.0",
35
35
  }, { instructions });
36
36
  registerStatusTools(server);
37
37
  registerSetupTools(server);
38
- if (initialized) {
39
- registerTaskTools(server);
40
- registerReviewTools(server);
41
- registerContextTools(server);
42
- registerStrategyTools(server);
43
- registerDispatchTools(server);
44
- registerEpicTools(server);
45
- }
46
- else {
47
- const { getAllStrategies } = await import("./strategies.js");
48
- server.tool("list_strategies", "List all available collaboration strategies.", {}, async () => {
49
- const strategies = getAllStrategies();
50
- let text = "Available strategies:\n\n";
51
- for (const s of strategies) {
52
- text += `## ${s.name} (${s.id})\n`;
53
- text += `${s.description}\n`;
54
- text += `Best for: ${s.best_for}\n`;
55
- text += `Roles: ${s.roles.primary.name} (Primary) + ${s.roles.secondary.name} (Secondary)\n\n`;
56
- }
57
- return { content: [{ type: "text", text }] };
58
- });
59
- }
38
+ registerTaskTools(server);
39
+ registerReviewTools(server);
40
+ registerContextTools(server);
41
+ registerStrategyTools(server);
42
+ registerDispatchTools(server);
43
+ registerEpicTools(server);
60
44
  const transport = new StdioServerTransport();
61
45
  await server.connect(transport);
62
46
  const role = getRole();
@@ -67,6 +51,6 @@ else {
67
51
  process.stderr.write(`agent-collab MCP started | strategy: ${strategy.name} | engine: ${engineMode} | role: ${roleConfig.name} (${role})\n`);
68
52
  }
69
53
  else {
70
- process.stderr.write(`agent-collab MCP started | NOT INITIALIZED | awaiting setup_project\n`);
54
+ process.stderr.write(`agent-collab MCP started | NOT INITIALIZED | all tools visible, awaiting setup_project\n`);
71
55
  }
72
56
  }
@@ -116,31 +116,20 @@ description: Agent collaboration protocol via MCP
116
116
  alwaysApply: true
117
117
  ---
118
118
 
119
- # Agent Collaboration
120
-
121
- You are part of a coordinated agent system. Your role depends on the active **collaboration strategy** and **engine mode**.
122
- All coordination goes through the **agent-collab** MCP server.
123
-
124
- In single-engine mode, you handle both Primary and Secondary roles with all tools available.
125
-
126
- ## Before ANY work
127
-
128
- Call \`get_my_status\` from the agent-collab MCP. Follow its instructions exactly.
129
- Do NOT write code without first claiming a task via \`claim_task\`.
130
-
131
- ## Common Workflow
132
-
133
- 1. \`get_my_status\` — see what to do next
134
- 2. \`claim_task(id)\` — claim it (sets to in-progress)
135
- 3. \`get_task(id)\` — read full details
136
- 4. Implement the task
137
- 5. \`submit_for_review(id, summary)\` — submit for review
138
- 6. \`get_my_status\` — check for next task or review feedback
139
-
140
- Some strategies give you additional tools (e.g., \`create_task\`, \`review_task\`, \`set_engine_mode\`).
141
- Call \`get_active_strategy\` to see your current role, engine mode, and available tools.
142
-
143
- Skipping this — even if the user says "just build it" — is a violation.
119
+ # Agent Collaboration — Automated Workflow
120
+
121
+ You have the agent-collab MCP configured. Follow the MCP workflow strictly:
122
+
123
+ 1. Call \`get_my_status\` to check your role, available tools, and next action
124
+ 2. If setup is needed, ask the user for strategy/engine preferences, then call \`setup_project\`
125
+ 3. If no tasks exist and you can create them, create an HLD with \`set_context\` then create tasks with \`create_task\`
126
+ 4. For each assigned task: \`claim_task\` -> \`get_task\` -> implement -> \`submit_for_review\`
127
+ 5. After submitting for review in "both" mode, call \`trigger_review()\` to auto-invoke the reviewer
128
+ 6. Check \`get_my_status\` after reviews — if changes-requested, \`claim_task\` again, fix, resubmit
129
+ 7. When all tasks are done, suggest \`archive_epic\` to the user to clear the board
130
+ 8. NEVER write code without claiming a task first via \`claim_task\`
131
+ 9. NEVER skip the MCP workflow even if the user says "just build it"
132
+ 10. The \`get_my_status\` response includes your available tools — only call tools listed there
144
133
  `;
145
134
  const AGENTS_MD = `# Agent Instructions
146
135
 
@@ -392,7 +381,7 @@ const CLAUDE_SETTINGS = `{
392
381
  "mcpServers": {
393
382
  "agent-collab": {
394
383
  "command": "npx",
395
- "args": ["-y", "agent-collab-mcp"],
384
+ "args": ["agent-collab-mcp"],
396
385
  "env": {
397
386
  "AGENT_ROLE": "claude-code"
398
387
  }
@@ -1,36 +1,45 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, getToolAccess } from "../db.js";
2
+ import { isInitialized, getDb, getRole, getToolAccess } from "../db.js";
3
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
3
4
  export function registerContextTools(server) {
4
- const access = getToolAccess();
5
- const role = getRole();
6
5
  server.tool("get_context", "Get a project context document (PRD or HLD summary).", { key: z.enum(["prd", "hld"]).describe("Document key: 'prd' or 'hld'") }, async ({ key }) => {
6
+ if (!isInitialized())
7
+ return NOT_SETUP;
7
8
  const db = getDb();
8
9
  const row = db.prepare("SELECT content, updated_at FROM context_docs WHERE key = ?").get(key);
9
10
  if (!row) {
10
- const hint = access.context_write
11
+ const canWrite = getToolAccess().context_write;
12
+ const hint = canWrite
11
13
  ? `Create one with set_context("${key}", ...).`
12
14
  : "The other agent needs to create one first.";
13
15
  return { content: [{ type: "text", text: `No ${key.toUpperCase()} document found. ${hint}` }] };
14
16
  }
15
17
  return { content: [{ type: "text", text: `${key.toUpperCase()} (updated ${row.updated_at}):\n\n${row.content}` }] };
16
18
  });
17
- if (access.context_write) {
18
- server.tool("set_context", "Create or update a project context document (PRD or HLD).", {
19
- key: z.enum(["prd", "hld"]).describe("Document key: 'prd' or 'hld'"),
20
- content: z.string().describe("Document content (keep concise, max ~150 lines)"),
21
- }, async ({ key, content }) => {
22
- const db = getDb();
23
- db.prepare(`
24
- INSERT INTO context_docs (key, content, updated_at)
25
- VALUES (?, ?, datetime('now'))
26
- ON CONFLICT(key) DO UPDATE SET content = excluded.content, updated_at = datetime('now')
27
- `).run(key, content);
28
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Updated ${key.toUpperCase()} document`);
29
- return { content: [{ type: "text", text: `${key.toUpperCase()} document saved.` }] };
30
- });
31
- }
19
+ server.tool("set_context", "Create or update a project context document (PRD or HLD).", {
20
+ key: z.enum(["prd", "hld"]).describe("Document key: 'prd' or 'hld'"),
21
+ content: z.string().describe("Document content (keep concise, max ~150 lines)"),
22
+ }, async ({ key, content }) => {
23
+ if (!isInitialized())
24
+ return NOT_SETUP;
25
+ if (!getToolAccess().context_write) {
26
+ return { content: [{ type: "text", text: "set_context is not available for your current role." }] };
27
+ }
28
+ const db = getDb();
29
+ const role = getRole();
30
+ db.prepare(`
31
+ INSERT INTO context_docs (key, content, updated_at)
32
+ VALUES (?, ?, datetime('now'))
33
+ ON CONFLICT(key) DO UPDATE SET content = excluded.content, updated_at = datetime('now')
34
+ `).run(key, content);
35
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Updated ${key.toUpperCase()} document`);
36
+ return { content: [{ type: "text", text: `${key.toUpperCase()} document saved.` }] };
37
+ });
32
38
  server.tool("log_activity", "Log an activity entry.", { action: z.string().describe("Short description of what was done") }, async ({ action }) => {
39
+ if (!isInitialized())
40
+ return NOT_SETUP;
33
41
  const db = getDb();
42
+ const role = getRole();
34
43
  db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, action);
35
44
  return { content: [{ type: "text", text: "Logged." }] };
36
45
  });
@@ -1,13 +1,16 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, isSingleEngine } from "../db.js";
2
+ import { isInitialized, getDb, isSingleEngine } from "../db.js";
3
3
  import { dispatchReview, dispatchBuilder, formatResult } from "../dispatch.js";
4
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
5
+ const SINGLE_MODE = { content: [{ type: "text", text: "Dispatch tools are only available in 'both' engine mode. In single-engine mode, you handle both roles directly." }] };
4
6
  export function registerDispatchTools(server) {
5
- if (isSingleEngine())
6
- return;
7
- const role = getRole();
8
7
  server.tool("trigger_review", "Invoke the reviewer agent to review tasks. Auto-spawns the counterpart CLI in the background.", {
9
8
  task_id: z.string().optional().describe("Specific task ID to review, or omit to review all tasks in 'review' status"),
10
9
  }, async ({ task_id }) => {
10
+ if (!isInitialized())
11
+ return NOT_SETUP;
12
+ if (isSingleEngine())
13
+ return SINGLE_MODE;
11
14
  const db = getDb();
12
15
  let taskIds;
13
16
  if (task_id) {
@@ -37,6 +40,10 @@ export function registerDispatchTools(server) {
37
40
  server.tool("notify_builder", "Invoke the builder agent to pick up assigned tasks. Auto-spawns the counterpart CLI in the background.", {
38
41
  message: z.string().optional().describe("Optional context message for the builder"),
39
42
  }, async ({ message }) => {
43
+ if (!isInitialized())
44
+ return NOT_SETUP;
45
+ if (isSingleEngine())
46
+ return SINGLE_MODE;
40
47
  const db = getDb();
41
48
  const assigned = db.prepare("SELECT id FROM tasks WHERE status = 'assigned' ORDER BY id").all();
42
49
  if (assigned.length === 0) {
@@ -56,6 +63,10 @@ export function registerDispatchTools(server) {
56
63
  max_tasks: z.number().optional().describe("Max tasks to process (default: all)"),
57
64
  timeout_seconds: z.number().optional().describe("Timeout per agent invocation in seconds (default: 300)"),
58
65
  }, async ({ max_rounds, max_tasks, timeout_seconds }) => {
66
+ if (!isInitialized())
67
+ return NOT_SETUP;
68
+ if (isSingleEngine())
69
+ return SINGLE_MODE;
59
70
  const maxR = max_rounds ?? 3;
60
71
  const maxT = max_tasks ?? 999;
61
72
  const timeout = (timeout_seconds ?? 300) * 1000;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, getActiveStrategy, getEngineMode, nextEpicId } from "../db.js";
2
+ import { isInitialized, getDb, getRole, getActiveStrategy, getEngineMode, nextEpicId } from "../db.js";
3
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
3
4
  export function registerEpicTools(server) {
4
5
  const role = getRole();
5
6
  server.tool("archive_epic", "Archive all current tasks into a named epic. Clears the board for new work. Past epics remain accessible for context.", {
@@ -8,6 +9,8 @@ export function registerEpicTools(server) {
8
9
  include_incomplete: z.boolean().optional().describe("If true, archive even if some tasks aren't done (default: false)"),
9
10
  confirm: z.boolean().optional().describe("Set to true to confirm the archive"),
10
11
  }, async ({ name, description, include_incomplete, confirm }) => {
12
+ if (!isInitialized())
13
+ return NOT_SETUP;
11
14
  const db = getDb();
12
15
  const tasks = db.prepare("SELECT * FROM tasks ORDER BY CAST(SUBSTR(id, 3) AS INTEGER)").all();
13
16
  if (tasks.length === 0) {
@@ -75,6 +78,8 @@ export function registerEpicTools(server) {
75
78
  return { content: [{ type: "text", text }] };
76
79
  });
77
80
  server.tool("list_epics", "List all archived epics with summaries. Shows project history.", {}, async () => {
81
+ if (!isInitialized())
82
+ return NOT_SETUP;
78
83
  const db = getDb();
79
84
  const epics = db.prepare("SELECT id, name, description, task_count, strategy, engine_mode, archived_at FROM epics ORDER BY CAST(SUBSTR(id, 3) AS INTEGER)").all();
80
85
  if (epics.length === 0) {
@@ -90,6 +95,8 @@ export function registerEpicTools(server) {
90
95
  return { content: [{ type: "text", text }] };
91
96
  });
92
97
  server.tool("get_epic", "Get full details of an archived epic: tasks, reviews, context, activity.", { epic_id: z.string().describe("Epic ID, e.g. E-001") }, async ({ epic_id }) => {
98
+ if (!isInitialized())
99
+ return NOT_SETUP;
93
100
  const db = getDb();
94
101
  const epic = db.prepare("SELECT * FROM epics WHERE id = ?").get(epic_id);
95
102
  if (!epic) {
@@ -120,6 +127,8 @@ export function registerEpicTools(server) {
120
127
  return { content: [{ type: "text", text }] };
121
128
  });
122
129
  server.tool("get_codebase_context", "Get current HLD/PRD plus summaries of all past epics. Call this before starting new work to understand the codebase history.", {}, async () => {
130
+ if (!isInitialized())
131
+ return NOT_SETUP;
123
132
  const db = getDb();
124
133
  const contextDocs = db.prepare("SELECT key, content, updated_at FROM context_docs").all();
125
134
  const epics = db.prepare("SELECT id, name, summary, task_count, archived_at FROM epics ORDER BY CAST(SUBSTR(id, 3) AS INTEGER)").all();
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, getToolAccess } from "../db.js";
2
+ import { isInitialized, getDb, getRole, getToolAccess } from "../db.js";
3
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
3
4
  export function registerReviewTools(server) {
4
- const access = getToolAccess();
5
- const role = getRole();
6
5
  server.tool("get_review_feedback", "Get the latest review feedback for a task.", { task_id: z.string().describe("Task ID") }, async ({ task_id }) => {
6
+ if (!isInitialized())
7
+ return NOT_SETUP;
7
8
  const db = getDb();
8
9
  const review = db.prepare("SELECT round, verdict, issues, notes, created_at FROM reviews WHERE task_id = ? ORDER BY round DESC LIMIT 1").get(task_id);
9
10
  if (!review) {
@@ -28,58 +29,62 @@ export function registerReviewTools(server) {
28
29
  }
29
30
  return { content: [{ type: "text", text }] };
30
31
  });
31
- if (access.review_write) {
32
- server.tool("review_task", "Write a review verdict for a task. Sets status to 'done' or 'changes-requested'.", {
33
- task_id: z.string().describe("Task ID to review"),
34
- verdict: z.enum(["approved", "changes-requested"]).describe("Review verdict"),
35
- issues: z.array(z.object({
36
- file: z.string().optional().describe("File path"),
37
- line: z.number().optional().describe("Line number"),
38
- description: z.string().describe("Issue description and how to fix"),
39
- severity: z.enum(["critical", "warning", "note"]).optional().describe("Issue severity"),
40
- })).optional().describe("List of issues found"),
41
- notes: z.string().optional().describe("General feedback or positive observations"),
42
- }, async ({ task_id, verdict, issues, notes }) => {
43
- const db = getDb();
44
- const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
45
- if (!task) {
46
- return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
47
- }
48
- if (task.status !== "review") {
49
- return {
50
- content: [{
51
- type: "text",
52
- text: `Cannot review ${task_id}: status is "${task.status}". Only "review" tasks can be reviewed.`
53
- }]
54
- };
55
- }
56
- const lastReview = db.prepare("SELECT round FROM reviews WHERE task_id = ? ORDER BY round DESC LIMIT 1").get(task_id);
57
- const round = (lastReview?.round || 0) + 1;
58
- const newStatus = verdict === "approved" ? "done" : "changes-requested";
59
- db.prepare(`
60
- INSERT INTO reviews (task_id, round, verdict, issues, notes)
61
- VALUES (?, ?, ?, ?, ?)
62
- `).run(task_id, round, verdict, issues ? JSON.stringify(issues) : null, notes || null);
63
- db.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run(newStatus, task_id);
64
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Reviewed ${task_id} round ${round}: ${verdict}`);
65
- if (verdict === "approved") {
66
- return {
67
- content: [{
68
- type: "text",
69
- text: `${task_id} APPROVED (round ${round}). Status set to done.${notes ? " Notes: " + notes : ""}`
70
- }]
71
- };
72
- }
73
- let text = `${task_id} needs changes (round ${round}). Status set to changes-requested.\n`;
74
- if (issues && issues.length > 0) {
75
- text += "\nIssues to fix:\n";
76
- for (const issue of issues) {
77
- text += ` - [${issue.file || "general"}${issue.line ? ":" + issue.line : ""}] ${issue.description}\n`;
78
- }
32
+ server.tool("review_task", "Write a review verdict for a task. Sets status to 'done' or 'changes-requested'.", {
33
+ task_id: z.string().describe("Task ID to review"),
34
+ verdict: z.enum(["approved", "changes-requested"]).describe("Review verdict"),
35
+ issues: z.array(z.object({
36
+ file: z.string().optional().describe("File path"),
37
+ line: z.number().optional().describe("Line number"),
38
+ description: z.string().describe("Issue description and how to fix"),
39
+ severity: z.enum(["critical", "warning", "note"]).optional().describe("Issue severity"),
40
+ })).optional().describe("List of issues found"),
41
+ notes: z.string().optional().describe("General feedback or positive observations"),
42
+ }, async ({ task_id, verdict, issues, notes }) => {
43
+ if (!isInitialized())
44
+ return NOT_SETUP;
45
+ if (!getToolAccess().review_write) {
46
+ return { content: [{ type: "text", text: "review_task is not available for your current role. Call get_my_status to see your available tools." }] };
47
+ }
48
+ const db = getDb();
49
+ const role = getRole();
50
+ const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
51
+ if (!task) {
52
+ return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
53
+ }
54
+ if (task.status !== "review") {
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: `Cannot review ${task_id}: status is "${task.status}". Only "review" tasks can be reviewed.`
59
+ }]
60
+ };
61
+ }
62
+ const lastReview = db.prepare("SELECT round FROM reviews WHERE task_id = ? ORDER BY round DESC LIMIT 1").get(task_id);
63
+ const round = (lastReview?.round || 0) + 1;
64
+ const newStatus = verdict === "approved" ? "done" : "changes-requested";
65
+ db.prepare(`
66
+ INSERT INTO reviews (task_id, round, verdict, issues, notes)
67
+ VALUES (?, ?, ?, ?, ?)
68
+ `).run(task_id, round, verdict, issues ? JSON.stringify(issues) : null, notes || null);
69
+ db.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run(newStatus, task_id);
70
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Reviewed ${task_id} round ${round}: ${verdict}`);
71
+ if (verdict === "approved") {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `${task_id} APPROVED (round ${round}). Status set to done.${notes ? " Notes: " + notes : ""}`
76
+ }]
77
+ };
78
+ }
79
+ let text = `${task_id} needs changes (round ${round}). Status set to changes-requested.\n`;
80
+ if (issues && issues.length > 0) {
81
+ text += "\nIssues to fix:\n";
82
+ for (const issue of issues) {
83
+ text += ` - [${issue.file || "general"}${issue.line ? ":" + issue.line : ""}] ${issue.description}\n`;
79
84
  }
80
- if (notes)
81
- text += `\nNotes: ${notes}`;
82
- return { content: [{ type: "text", text }] };
83
- });
84
- }
85
+ }
86
+ if (notes)
87
+ text += `\nNotes: ${notes}`;
88
+ return { content: [{ type: "text", text }] };
89
+ });
85
90
  }
@@ -1,4 +1,25 @@
1
1
  import { isInitialized, getDb, getActiveStrategy, getEngineMode, getMyRoleConfig, isSingleEngine } from "../db.js";
2
+ function buildToolList(access, single) {
3
+ const tools = [];
4
+ if (access.task_create)
5
+ tools.push("create_task");
6
+ if (access.task_claim)
7
+ tools.push("claim_task");
8
+ if (access.task_submit)
9
+ tools.push("submit_for_review");
10
+ if (access.review_write)
11
+ tools.push("review_task");
12
+ if (access.context_write)
13
+ tools.push("set_context");
14
+ if (access.save_plan)
15
+ tools.push("save_plan");
16
+ tools.push("get_task", "get_context", "get_review_feedback", "get_project_overview", "log_activity");
17
+ if (!single)
18
+ tools.push("trigger_review", "notify_builder", "run_loop");
19
+ tools.push("archive_epic", "list_epics", "get_epic", "get_codebase_context");
20
+ tools.push("list_strategies", "get_active_strategy", "set_strategy", "set_engine_mode");
21
+ return tools.join(", ");
22
+ }
2
23
  export function registerStatusTools(server) {
3
24
  server.tool("get_my_status", "Get your next action. Call this FIRST before any work.", {}, async () => {
4
25
  if (!isInitialized()) {
@@ -20,7 +41,8 @@ export function registerStatusTools(server) {
20
41
  " - both: Cursor implements, Claude Code reviews",
21
42
  " - claude-code-only: Claude Code handles everything\n",
22
43
  "After getting their choices, call setup_project(strategy, engine_mode).",
23
- "If they just say 'go with defaults' or similar, use architect-builder + cursor-only.",
44
+ "If they just say 'go with defaults' or similar, use architect-builder + cursor-only.\n",
45
+ "Available tools before setup: get_my_status, setup_project, list_strategies, get_dashboard_info",
24
46
  ].join("\n"),
25
47
  }],
26
48
  };
@@ -31,7 +53,8 @@ export function registerStatusTools(server) {
31
53
  const roleConfig = getMyRoleConfig();
32
54
  const single = isSingleEngine();
33
55
  const access = roleConfig.tools;
34
- const header = `[Strategy: ${strategy.name}] [Engine: ${engineMode}] [Role: ${roleConfig.name}]\n`;
56
+ const toolLine = `Your tools: ${buildToolList(access, single)}\n`;
57
+ const header = `[Strategy: ${strategy.name}] [Engine: ${engineMode}] [Role: ${roleConfig.name}]\n${toolLine}`;
35
58
  const inProgress = db.prepare("SELECT id, title FROM tasks WHERE status = 'in-progress' LIMIT 1").get();
36
59
  if (inProgress) {
37
60
  return text(header, `RESUME: Task ${inProgress.id} "${inProgress.title}" is in-progress. Call get_task("${inProgress.id}") for details and continue working.`);
@@ -91,8 +114,9 @@ export function registerStatusTools(server) {
91
114
  const counts = db.prepare("SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status").all();
92
115
  const total = counts.reduce((s, r) => s + r.cnt, 0);
93
116
  const recent = db.prepare("SELECT timestamp, agent, action FROM activity_log ORDER BY id DESC LIMIT 5").all();
117
+ const epicCount = db.prepare("SELECT COUNT(*) as cnt FROM epics").get().cnt;
94
118
  let out = `Strategy: ${strategy.name} | Engine mode: ${engineMode}\n`;
95
- out += `Project: ${total} tasks total\n`;
119
+ out += `Project: ${total} tasks total | ${epicCount} archived epic(s)\n`;
96
120
  for (const r of counts) {
97
121
  out += ` ${r.status}: ${r.cnt}\n`;
98
122
  }
@@ -1,11 +1,20 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, getActiveStrategy, getEngineMode, getMyRoleConfig, setActiveStrategy, setEngineMode, isSingleEngine } from "../db.js";
2
+ import { isInitialized, getDb, getRole, getActiveStrategy, getEngineMode, getMyRoleConfig, setActiveStrategy, setEngineMode, isSingleEngine } from "../db.js";
3
3
  import { getAllStrategies, getStrategyDef } from "../strategies.js";
4
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
4
5
  export function registerStrategyTools(server) {
5
- const role = getRole();
6
- const engineMode = getEngineMode();
7
6
  server.tool("list_strategies", "List all available collaboration strategies with descriptions.", {}, async () => {
8
7
  const strategies = getAllStrategies();
8
+ if (!isInitialized()) {
9
+ let text = "Available strategies:\n\n";
10
+ for (const s of strategies) {
11
+ text += `## ${s.name} (${s.id})\n`;
12
+ text += `${s.description}\n`;
13
+ text += `Best for: ${s.best_for}\n`;
14
+ text += `Roles: ${s.roles.primary.name} (Primary) + ${s.roles.secondary.name} (Secondary)\n\n`;
15
+ }
16
+ return { content: [{ type: "text", text }] };
17
+ }
9
18
  const active = getActiveStrategy();
10
19
  const mode = getEngineMode();
11
20
  let text = `Active strategy: ${active.id} — ${active.name}\n`;
@@ -30,6 +39,8 @@ export function registerStrategyTools(server) {
30
39
  return { content: [{ type: "text", text }] };
31
40
  });
32
41
  server.tool("get_active_strategy", "Get the current collaboration strategy, engine mode, and your role.", {}, async () => {
42
+ if (!isInitialized())
43
+ return NOT_SETUP;
33
44
  const strategy = getActiveStrategy();
34
45
  const mode = getEngineMode();
35
46
  const roleConfig = getMyRoleConfig();
@@ -49,67 +60,78 @@ export function registerStrategyTools(server) {
49
60
  }
50
61
  return { content: [{ type: "text", text }] };
51
62
  });
52
- const canChangeConfig = role === "claude-code" || isSingleEngine() || role === "unknown";
53
- if (canChangeConfig) {
54
- server.tool("set_strategy", "Change the collaboration strategy. Affects how agents work together.", {
55
- strategy_id: z.string().describe("Strategy ID from list_strategies"),
56
- confirm: z.boolean().describe("Set to true to confirm the change"),
57
- }, async ({ strategy_id, confirm }) => {
58
- const def = getStrategyDef(strategy_id);
59
- if (!def) {
60
- const ids = getAllStrategies().map(s => s.id).join(", ");
61
- return { content: [{ type: "text", text: `Unknown strategy "${strategy_id}". Available: ${ids}` }] };
62
- }
63
- if (!confirm) {
64
- let text = `About to switch to: ${def.name}\n\n`;
65
- text += `${def.description}\n\n`;
66
- text += `Primary role: ${def.roles.primary.name}\n`;
67
- text += `Secondary role: ${def.roles.secondary.name}\n\n`;
68
- text += `Call set_strategy("${strategy_id}", confirm=true) to apply.\n`;
69
- text += `NOTE: Agents need to restart their MCP connections to pick up the new roles.`;
70
- return { content: [{ type: "text", text }] };
71
- }
72
- const db = getDb();
73
- setActiveStrategy(db, strategy_id);
74
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Changed strategy to: ${def.name} (${strategy_id})`);
75
- return {
76
- content: [{
77
- type: "text",
78
- text: `Strategy changed to: ${def.name} (${strategy_id}). Restart MCP connections to apply.`
79
- }]
80
- };
81
- });
82
- server.tool("set_engine_mode", "Change which engines are active: 'both', 'cursor-only', or 'claude-code-only'.", {
83
- mode: z.enum(["both", "cursor-only", "claude-code-only"]).describe("Engine mode"),
84
- confirm: z.boolean().describe("Set to true to confirm"),
85
- }, async ({ mode, confirm }) => {
86
- const current = getEngineMode();
87
- if (!confirm) {
88
- let text = `Current engine mode: ${current}\n`;
89
- text += `Switching to: ${mode}\n\n`;
90
- if (mode === "both") {
91
- text += "Both engines active: cursor → Primary role, claude-code → Secondary role.\n";
92
- text += "Each engine sees only its role's tools.\n";
93
- }
94
- else {
95
- text += `Single engine (${mode}): one agent handles BOTH Primary and Secondary roles.\n`;
96
- text += "All tools are available. Self-review is used.\n";
97
- }
98
- text += `\nCall set_engine_mode("${mode}", confirm=true) to apply.`;
99
- return { content: [{ type: "text", text }] };
100
- }
101
- const db = getDb();
102
- setEngineMode(db, mode);
103
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Changed engine mode to: ${mode}`);
104
- let text = `Engine mode changed to: ${mode}.\n`;
63
+ server.tool("set_strategy", "Change the collaboration strategy. Affects how agents work together.", {
64
+ strategy_id: z.string().describe("Strategy ID from list_strategies"),
65
+ confirm: z.boolean().describe("Set to true to confirm the change"),
66
+ }, async ({ strategy_id, confirm }) => {
67
+ if (!isInitialized())
68
+ return NOT_SETUP;
69
+ const role = getRole();
70
+ const canChange = role === "claude-code" || isSingleEngine() || role === "unknown";
71
+ if (!canChange) {
72
+ return { content: [{ type: "text", text: "set_strategy is only available to the secondary role or in single-engine mode." }] };
73
+ }
74
+ const def = getStrategyDef(strategy_id);
75
+ if (!def) {
76
+ const ids = getAllStrategies().map(s => s.id).join(", ");
77
+ return { content: [{ type: "text", text: `Unknown strategy "${strategy_id}". Available: ${ids}` }] };
78
+ }
79
+ if (!confirm) {
80
+ let text = `About to switch to: ${def.name}\n\n`;
81
+ text += `${def.description}\n\n`;
82
+ text += `Primary role: ${def.roles.primary.name}\n`;
83
+ text += `Secondary role: ${def.roles.secondary.name}\n\n`;
84
+ text += `Call set_strategy("${strategy_id}", confirm=true) to apply.\n`;
85
+ text += `NOTE: Agents need to restart their MCP connections to pick up the new roles.`;
86
+ return { content: [{ type: "text", text }] };
87
+ }
88
+ const db = getDb();
89
+ setActiveStrategy(db, strategy_id);
90
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Changed strategy to: ${def.name} (${strategy_id})`);
91
+ return {
92
+ content: [{
93
+ type: "text",
94
+ text: `Strategy changed to: ${def.name} (${strategy_id}). Restart MCP connections to apply.`
95
+ }]
96
+ };
97
+ });
98
+ server.tool("set_engine_mode", "Change which engines are active: 'both', 'cursor-only', or 'claude-code-only'.", {
99
+ mode: z.enum(["both", "cursor-only", "claude-code-only"]).describe("Engine mode"),
100
+ confirm: z.boolean().describe("Set to true to confirm"),
101
+ }, async ({ mode, confirm }) => {
102
+ if (!isInitialized())
103
+ return NOT_SETUP;
104
+ const role = getRole();
105
+ const canChange = role === "claude-code" || isSingleEngine() || role === "unknown";
106
+ if (!canChange) {
107
+ return { content: [{ type: "text", text: "set_engine_mode is only available to the secondary role or in single-engine mode." }] };
108
+ }
109
+ const current = getEngineMode();
110
+ if (!confirm) {
111
+ let text = `Current engine mode: ${current}\n`;
112
+ text += `Switching to: ${mode}\n\n`;
105
113
  if (mode === "both") {
106
- text += "Update .cursor/mcp.json (AGENT_ROLE=cursor) and .claude/settings.json (AGENT_ROLE=claude-code).\n";
114
+ text += "Both engines active: cursor Primary role, claude-code → Secondary role.\n";
115
+ text += "Each engine sees only its role's tools.\n";
107
116
  }
108
117
  else {
109
- text += `Only the ${mode.replace("-only", "")} config file is needed.\n`;
118
+ text += `Single engine (${mode}): one agent handles BOTH Primary and Secondary roles.\n`;
119
+ text += "All tools are available. Self-review is used.\n";
110
120
  }
111
- text += "Restart MCP connections to apply.";
121
+ text += `\nCall set_engine_mode("${mode}", confirm=true) to apply.`;
112
122
  return { content: [{ type: "text", text }] };
113
- });
114
- }
123
+ }
124
+ const db = getDb();
125
+ setEngineMode(db, mode);
126
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Changed engine mode to: ${mode}`);
127
+ let text = `Engine mode changed to: ${mode}.\n`;
128
+ if (mode === "both") {
129
+ text += "Update .cursor/mcp.json (AGENT_ROLE=cursor) and .claude/settings.json (AGENT_ROLE=claude-code).\n";
130
+ }
131
+ else {
132
+ text += `Only the ${mode.replace("-only", "")} config file is needed.\n`;
133
+ }
134
+ text += "Restart MCP connections to apply.";
135
+ return { content: [{ type: "text", text }] };
136
+ });
115
137
  }
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
- import { getDb, getRole, getToolAccess, getDefaultOwner, nextTaskId, isSingleEngine } from "../db.js";
2
+ import { isInitialized, getDb, getRole, getToolAccess, getDefaultOwner, nextTaskId, isSingleEngine } from "../db.js";
3
3
  import { dispatchReview, dispatchBuilder, formatResult } from "../dispatch.js";
4
+ const NOT_SETUP = { content: [{ type: "text", text: "Project not set up. Call setup_project first." }] };
5
+ const NO_ACCESS = (tool) => ({ content: [{ type: "text", text: `${tool} is not available for your current role. Call get_my_status to see your available tools.` }] });
4
6
  export function registerTaskTools(server) {
5
- const access = getToolAccess();
6
- const role = getRole();
7
7
  server.tool("get_task", "Get full details for a task including latest review.", { task_id: z.string().describe("Task ID, e.g. T-001") }, async ({ task_id }) => {
8
+ if (!isInitialized())
9
+ return NOT_SETUP;
8
10
  const db = getDb();
9
11
  const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
10
12
  if (!task) {
@@ -37,109 +39,120 @@ export function registerTaskTools(server) {
37
39
  }
38
40
  return { content: [{ type: "text", text }] };
39
41
  });
40
- if (access.task_create) {
41
- server.tool("create_task", "Create a new task. Owner defaults based on engine mode.", {
42
- title: z.string().describe("Short task title"),
43
- context: z.string().describe("2-3 lines of context from HLD"),
44
- acceptance: z.string().describe("Criteria that define done"),
45
- depends_on: z.string().optional().describe("Comma-separated task IDs this depends on"),
46
- owner: z.enum(["cursor", "claude-code"]).optional().describe("Task owner (defaults based on engine mode)"),
47
- notify_builder: z.boolean().optional().describe("If true, auto-invoke the builder agent to start working. Use on the last task in a batch."),
48
- }, async ({ title, context, acceptance, depends_on, owner, notify_builder: shouldNotify }) => {
49
- const db = getDb();
50
- const id = nextTaskId(db);
51
- const taskOwner = owner || getDefaultOwner();
52
- db.prepare(`
53
- INSERT INTO tasks (id, title, status, owner, depends_on, context, acceptance)
54
- VALUES (?, ?, 'assigned', ?, ?, ?, ?)
55
- `).run(id, title, taskOwner, depends_on || null, context, acceptance);
56
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Created task ${id}: ${title} (owner: ${taskOwner})`);
57
- let text = `Created ${id}: "${title}" (assigned to ${taskOwner}).`;
58
- if (shouldNotify && !isSingleEngine()) {
59
- const assigned = db.prepare("SELECT id FROM tasks WHERE status = 'assigned' ORDER BY id").all();
60
- const result = dispatchBuilder(assigned.map(t => t.id), `${assigned.length} task(s) are ready for you.`);
61
- text += `\nBuilder notification: ${formatResult(result)}`;
62
- }
63
- return { content: [{ type: "text", text }] };
64
- });
65
- }
66
- if (access.task_claim) {
67
- server.tool("claim_task", "Claim a task and set it to in-progress. Works on 'assigned' or 'changes-requested' tasks.", { task_id: z.string().describe("Task ID to claim, e.g. T-001") }, async ({ task_id }) => {
68
- const db = getDb();
69
- const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
70
- if (!task) {
71
- return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
72
- }
73
- if (task.status !== "assigned" && task.status !== "changes-requested") {
74
- return {
75
- content: [{
76
- type: "text",
77
- text: `Cannot claim ${task_id}: status is "${task.status}". Only "assigned" or "changes-requested" tasks can be claimed.`
78
- }]
79
- };
80
- }
81
- db.prepare("UPDATE tasks SET status = 'in-progress', updated_at = datetime('now') WHERE id = ?").run(task_id);
82
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Claimed task ${task_id}`);
83
- let text = `Claimed ${task_id}: "${task.title}" — now in-progress.\n\nContext:\n${task.context || "(none)"}\n\nAcceptance:\n${task.acceptance || "(none)"}`;
84
- if (task.status === "changes-requested") {
85
- const review = db.prepare("SELECT issues, notes FROM reviews WHERE task_id = ? ORDER BY round DESC LIMIT 1").get(task_id);
86
- if (review?.issues) {
87
- text += "\n\nReview issues to fix:\n";
88
- try {
89
- const issues = JSON.parse(review.issues);
90
- for (const issue of issues) {
91
- text += ` - [${issue.file || "general"}${issue.line ? ":" + issue.line : ""}] ${issue.description}\n`;
92
- }
93
- }
94
- catch {
95
- text += ` ${review.issues}\n`;
42
+ server.tool("create_task", "Create a new task. Owner defaults based on engine mode.", {
43
+ title: z.string().describe("Short task title"),
44
+ context: z.string().describe("2-3 lines of context from HLD"),
45
+ acceptance: z.string().describe("Criteria that define done"),
46
+ depends_on: z.string().optional().describe("Comma-separated task IDs this depends on"),
47
+ owner: z.enum(["cursor", "claude-code"]).optional().describe("Task owner (defaults based on engine mode)"),
48
+ notify_builder: z.boolean().optional().describe("If true, auto-invoke the builder agent to start working. Use on the last task in a batch."),
49
+ }, async ({ title, context, acceptance, depends_on, owner, notify_builder: shouldNotify }) => {
50
+ if (!isInitialized())
51
+ return NOT_SETUP;
52
+ if (!getToolAccess().task_create)
53
+ return NO_ACCESS("create_task");
54
+ const db = getDb();
55
+ const role = getRole();
56
+ const id = nextTaskId(db);
57
+ const taskOwner = owner || getDefaultOwner();
58
+ db.prepare(`
59
+ INSERT INTO tasks (id, title, status, owner, depends_on, context, acceptance)
60
+ VALUES (?, ?, 'assigned', ?, ?, ?, ?)
61
+ `).run(id, title, taskOwner, depends_on || null, context, acceptance);
62
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Created task ${id}: ${title} (owner: ${taskOwner})`);
63
+ let text = `Created ${id}: "${title}" (assigned to ${taskOwner}).`;
64
+ if (shouldNotify && !isSingleEngine()) {
65
+ const assigned = db.prepare("SELECT id FROM tasks WHERE status = 'assigned' ORDER BY id").all();
66
+ const result = dispatchBuilder(assigned.map(t => t.id), `${assigned.length} task(s) are ready for you.`);
67
+ text += `\nBuilder notification: ${formatResult(result)}`;
68
+ }
69
+ return { content: [{ type: "text", text }] };
70
+ });
71
+ server.tool("claim_task", "Claim a task and set it to in-progress. Works on 'assigned' or 'changes-requested' tasks.", { task_id: z.string().describe("Task ID to claim, e.g. T-001") }, async ({ task_id }) => {
72
+ if (!isInitialized())
73
+ return NOT_SETUP;
74
+ if (!getToolAccess().task_claim)
75
+ return NO_ACCESS("claim_task");
76
+ const db = getDb();
77
+ const role = getRole();
78
+ const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
79
+ if (!task) {
80
+ return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
81
+ }
82
+ if (task.status !== "assigned" && task.status !== "changes-requested") {
83
+ return {
84
+ content: [{
85
+ type: "text",
86
+ text: `Cannot claim ${task_id}: status is "${task.status}". Only "assigned" or "changes-requested" tasks can be claimed.`
87
+ }]
88
+ };
89
+ }
90
+ db.prepare("UPDATE tasks SET status = 'in-progress', updated_at = datetime('now') WHERE id = ?").run(task_id);
91
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Claimed task ${task_id}`);
92
+ let text = `Claimed ${task_id}: "${task.title}" — now in-progress.\n\nContext:\n${task.context || "(none)"}\n\nAcceptance:\n${task.acceptance || "(none)"}`;
93
+ if (task.status === "changes-requested") {
94
+ const review = db.prepare("SELECT issues, notes FROM reviews WHERE task_id = ? ORDER BY round DESC LIMIT 1").get(task_id);
95
+ if (review?.issues) {
96
+ text += "\n\nReview issues to fix:\n";
97
+ try {
98
+ const issues = JSON.parse(review.issues);
99
+ for (const issue of issues) {
100
+ text += ` - [${issue.file || "general"}${issue.line ? ":" + issue.line : ""}] ${issue.description}\n`;
96
101
  }
97
102
  }
103
+ catch {
104
+ text += ` ${review.issues}\n`;
105
+ }
98
106
  }
99
- return { content: [{ type: "text", text }] };
100
- });
101
- }
102
- if (access.save_plan) {
103
- server.tool("save_plan", "Save your implementation plan for a task.", {
104
- task_id: z.string().describe("Task ID"),
105
- plan: z.string().describe("Your implementation plan"),
106
- }, async ({ task_id, plan }) => {
107
- const db = getDb();
108
- const task = db.prepare("SELECT status FROM tasks WHERE id = ?").get(task_id);
109
- if (!task) {
110
- return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
111
- }
112
- db.prepare("UPDATE tasks SET plan = ?, updated_at = datetime('now') WHERE id = ?").run(plan, task_id);
113
- return { content: [{ type: "text", text: `Plan saved for ${task_id}. Proceed with implementation.` }] };
114
- });
115
- }
116
- if (access.task_submit) {
117
- server.tool("submit_for_review", "Mark a task as complete and submit for review.", {
118
- task_id: z.string().describe("Task ID"),
119
- summary: z.string().describe("1-3 line summary of what was done"),
120
- }, async ({ task_id, summary }) => {
121
- const db = getDb();
122
- const task = db.prepare("SELECT status FROM tasks WHERE id = ?").get(task_id);
123
- if (!task) {
124
- return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
125
- }
126
- if (task.status !== "in-progress") {
127
- return {
128
- content: [{
129
- type: "text",
130
- text: `Cannot submit ${task_id}: status is "${task.status}". Only "in-progress" tasks can be submitted.`
131
- }]
132
- };
133
- }
134
- db.prepare("UPDATE tasks SET status = 'review', updated_at = datetime('now') WHERE id = ?").run(task_id);
135
- db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Submitted ${task_id} for review: ${summary}`);
136
- let text = `${task_id} submitted for review.`;
137
- if (!isSingleEngine()) {
138
- const result = dispatchReview([task_id]);
139
- text += ` Reviewer: ${formatResult(result)}`;
140
- }
141
- text += ` Call get_my_status to see your next action.`;
142
- return { content: [{ type: "text", text }] };
143
- });
144
- }
107
+ }
108
+ return { content: [{ type: "text", text }] };
109
+ });
110
+ server.tool("save_plan", "Save your implementation plan for a task.", {
111
+ task_id: z.string().describe("Task ID"),
112
+ plan: z.string().describe("Your implementation plan"),
113
+ }, async ({ task_id, plan }) => {
114
+ if (!isInitialized())
115
+ return NOT_SETUP;
116
+ if (!getToolAccess().save_plan)
117
+ return NO_ACCESS("save_plan");
118
+ const db = getDb();
119
+ const task = db.prepare("SELECT status FROM tasks WHERE id = ?").get(task_id);
120
+ if (!task) {
121
+ return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
122
+ }
123
+ db.prepare("UPDATE tasks SET plan = ?, updated_at = datetime('now') WHERE id = ?").run(plan, task_id);
124
+ return { content: [{ type: "text", text: `Plan saved for ${task_id}. Proceed with implementation.` }] };
125
+ });
126
+ server.tool("submit_for_review", "Mark a task as complete and submit for review.", {
127
+ task_id: z.string().describe("Task ID"),
128
+ summary: z.string().describe("1-3 line summary of what was done"),
129
+ }, async ({ task_id, summary }) => {
130
+ if (!isInitialized())
131
+ return NOT_SETUP;
132
+ if (!getToolAccess().task_submit)
133
+ return NO_ACCESS("submit_for_review");
134
+ const db = getDb();
135
+ const role = getRole();
136
+ const task = db.prepare("SELECT status FROM tasks WHERE id = ?").get(task_id);
137
+ if (!task) {
138
+ return { content: [{ type: "text", text: `Task ${task_id} not found.` }] };
139
+ }
140
+ if (task.status !== "in-progress") {
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: `Cannot submit ${task_id}: status is "${task.status}". Only "in-progress" tasks can be submitted.`
145
+ }]
146
+ };
147
+ }
148
+ db.prepare("UPDATE tasks SET status = 'review', updated_at = datetime('now') WHERE id = ?").run(task_id);
149
+ db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(role, `Submitted ${task_id} for review: ${summary}`);
150
+ let text = `${task_id} submitted for review.`;
151
+ if (!isSingleEngine()) {
152
+ const result = dispatchReview([task_id]);
153
+ text += ` Reviewer: ${formatResult(result)}`;
154
+ }
155
+ text += ` Call get_my_status to see your next action.`;
156
+ return { content: [{ type: "text", text }] };
157
+ });
145
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-collab-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for multi-agent coordination between Cursor and Claude Code. Strategies, task management, review loops, and a live dashboard.",
5
5
  "type": "module",
6
6
  "main": "build/index.js",