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.
- package/build/dashboard.js +39 -1
- package/build/index.js +9 -25
- package/build/templates.js +15 -26
- package/build/tools/context.js +28 -19
- package/build/tools/dispatch.js +15 -4
- package/build/tools/epic.js +10 -1
- package/build/tools/reviews.js +61 -56
- package/build/tools/status.js +27 -3
- package/build/tools/strategy.js +83 -61
- package/build/tools/tasks.js +118 -105
- package/package.json +1 -1
package/build/dashboard.js
CHANGED
|
@@ -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">⚙</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">📄</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">📦</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
|
-
"
|
|
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.
|
|
34
|
+
version: "1.3.0",
|
|
35
35
|
}, { instructions });
|
|
36
36
|
registerStatusTools(server);
|
|
37
37
|
registerSetupTools(server);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
}
|
package/build/templates.js
CHANGED
|
@@ -116,31 +116,20 @@ description: Agent collaboration protocol via MCP
|
|
|
116
116
|
alwaysApply: true
|
|
117
117
|
---
|
|
118
118
|
|
|
119
|
-
# Agent Collaboration
|
|
120
|
-
|
|
121
|
-
You
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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": ["
|
|
384
|
+
"args": ["agent-collab-mcp"],
|
|
396
385
|
"env": {
|
|
397
386
|
"AGENT_ROLE": "claude-code"
|
|
398
387
|
}
|
package/build/tools/context.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
});
|
package/build/tools/dispatch.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
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;
|
package/build/tools/epic.js
CHANGED
|
@@ -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();
|
package/build/tools/reviews.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
+
}
|
|
86
|
+
if (notes)
|
|
87
|
+
text += `\nNotes: ${notes}`;
|
|
88
|
+
return { content: [{ type: "text", text }] };
|
|
89
|
+
});
|
|
85
90
|
}
|
package/build/tools/status.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/build/tools/strategy.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
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 += "
|
|
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 += `
|
|
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 += "
|
|
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
|
}
|
package/build/tools/tasks.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { content: [{ type: "text", text: `
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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",
|