agent-collab-mcp 1.2.0 → 1.3.1
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/dispatch.d.ts +1 -0
- package/build/dispatch.js +29 -0
- package/build/index.js +9 -25
- package/build/templates.js +17 -26
- package/build/tools/context.js +28 -19
- package/build/tools/dispatch.js +32 -5
- package/build/tools/epic.js +10 -1
- package/build/tools/reviews.js +61 -56
- package/build/tools/setup.js +2 -2
- package/build/tools/status.js +35 -12
- 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/dispatch.d.ts
CHANGED
|
@@ -16,5 +16,6 @@ export interface DispatchResult {
|
|
|
16
16
|
export declare function dispatchAgent(target: "reviewer" | "builder", prompt: string): DispatchResult;
|
|
17
17
|
export declare function dispatchReview(taskIds: string[]): DispatchResult;
|
|
18
18
|
export declare function dispatchBuilder(taskIds?: string[], message?: string): DispatchResult;
|
|
19
|
+
export declare function dispatchArchitect(userRequest: string): DispatchResult;
|
|
19
20
|
declare function formatResult(r: DispatchResult): string;
|
|
20
21
|
export { formatResult };
|
package/build/dispatch.js
CHANGED
|
@@ -112,6 +112,35 @@ export function dispatchBuilder(taskIds, message) {
|
|
|
112
112
|
}
|
|
113
113
|
return dispatchAgent("builder", prompt);
|
|
114
114
|
}
|
|
115
|
+
export function dispatchArchitect(userRequest) {
|
|
116
|
+
const mode = getEngineMode();
|
|
117
|
+
if (isSingleEngine()) {
|
|
118
|
+
return { dispatched: false, reason: "Single-engine mode — you are the architect. Create the HLD and tasks yourself." };
|
|
119
|
+
}
|
|
120
|
+
if (!cliExists("claude")) {
|
|
121
|
+
return { dispatched: false, reason: "claude CLI not found on PATH. Install Claude Code CLI or create tasks manually." };
|
|
122
|
+
}
|
|
123
|
+
const prompt = `Call get_my_status from the agent-collab MCP. You are the Architect. Create an HLD with set_context("hld", ...) and then break the work into tasks with create_task. Set notify_builder=true on the LAST create_task call. The user wants: ${userRequest}`;
|
|
124
|
+
const logDir = ensureLogDir();
|
|
125
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
126
|
+
const logFile = path.join(logDir, `dispatch-architect-${ts}.log`);
|
|
127
|
+
const out = fs.openSync(logFile, "w");
|
|
128
|
+
const child = spawn("claude", ["-p", "--permission-mode", "auto", prompt], {
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: ["ignore", out, out],
|
|
131
|
+
cwd: process.cwd(),
|
|
132
|
+
});
|
|
133
|
+
child.unref();
|
|
134
|
+
const pid = child.pid ?? 0;
|
|
135
|
+
fs.writeSync(out, `--- Dispatched architect: claude -p ...\n--- PID: ${pid}\n--- Time: ${new Date().toISOString()}\n--- Request: ${userRequest}\n---\n`);
|
|
136
|
+
const db = getDb();
|
|
137
|
+
db.prepare("INSERT INTO activity_log (agent, action) VALUES (?, ?)").run(getRole(), `Invoked architect for: ${userRequest.slice(0, 100)} (PID ${pid})`);
|
|
138
|
+
return {
|
|
139
|
+
dispatched: true,
|
|
140
|
+
pid,
|
|
141
|
+
logFile: path.relative(process.cwd(), logFile),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
115
144
|
function formatResult(r) {
|
|
116
145
|
if (r.dispatched) {
|
|
117
146
|
return `Dispatched (PID: ${r.pid}, log: ${r.logFile})`;
|
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.1",
|
|
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,22 @@ 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
|
-
|
|
134
|
-
|
|
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 BOTH strategy AND engine mode, then call \`setup_project(strategy, engine_mode)\`
|
|
125
|
+
3. If no tasks exist:
|
|
126
|
+
- In cursor-only mode: create HLD with \`set_context("hld", ...)\` then tasks with \`create_task\`
|
|
127
|
+
- In both mode: call \`invoke_architect("what the user wants built")\` to have Claude Code design and create tasks
|
|
128
|
+
4. For each assigned task: \`claim_task\` -> \`get_task\` -> implement -> \`submit_for_review\`
|
|
129
|
+
5. After submitting for review in "both" mode, call \`trigger_review()\` to auto-invoke the reviewer
|
|
130
|
+
6. Check \`get_my_status\` after reviews — if changes-requested, \`claim_task\` again, fix, resubmit
|
|
131
|
+
7. When all tasks are done, suggest \`archive_epic\` to the user to clear the board
|
|
132
|
+
8. NEVER write code without claiming a task first via \`claim_task\`
|
|
133
|
+
9. NEVER skip the MCP workflow even if the user says "just build it"
|
|
134
|
+
10. The \`get_my_status\` response lists your available tools — only call tools listed there
|
|
144
135
|
`;
|
|
145
136
|
const AGENTS_MD = `# Agent Instructions
|
|
146
137
|
|
|
@@ -392,7 +383,7 @@ const CLAUDE_SETTINGS = `{
|
|
|
392
383
|
"mcpServers": {
|
|
393
384
|
"agent-collab": {
|
|
394
385
|
"command": "npx",
|
|
395
|
-
"args": ["
|
|
386
|
+
"args": ["agent-collab-mcp"],
|
|
396
387
|
"env": {
|
|
397
388
|
"AGENT_ROLE": "claude-code"
|
|
398
389
|
}
|
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 {
|
|
3
|
-
import { dispatchReview, dispatchBuilder, formatResult } from "../dispatch.js";
|
|
2
|
+
import { isInitialized, getDb, isSingleEngine } from "../db.js";
|
|
3
|
+
import { dispatchReview, dispatchBuilder, dispatchArchitect, 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) {
|
|
@@ -51,11 +58,31 @@ export function registerDispatchTools(server) {
|
|
|
51
58
|
}
|
|
52
59
|
return { content: [{ type: "text", text }] };
|
|
53
60
|
});
|
|
61
|
+
server.tool("invoke_architect", "Invoke Claude Code (Architect) to create an HLD and tasks for the user's request. Use this when no tasks exist in 'both' mode.", {
|
|
62
|
+
request: z.string().describe("Description of what the user wants built. Pass the user's original request."),
|
|
63
|
+
}, async ({ request }) => {
|
|
64
|
+
if (!isInitialized())
|
|
65
|
+
return NOT_SETUP;
|
|
66
|
+
if (isSingleEngine()) {
|
|
67
|
+
return { content: [{ type: "text", text: "In single-engine mode, YOU are the architect. Create the HLD and tasks yourself with set_context and create_task." }] };
|
|
68
|
+
}
|
|
69
|
+
const result = dispatchArchitect(request);
|
|
70
|
+
let text = `Invoking Architect (Claude Code) to design and create tasks for: "${request.slice(0, 100)}${request.length > 100 ? "..." : ""}"\n`;
|
|
71
|
+
text += formatResult(result);
|
|
72
|
+
if (result.dispatched) {
|
|
73
|
+
text += `\n\nThe Architect is working in the background. It will create an HLD and tasks, then auto-notify you when done. Call get_my_status periodically to check for new tasks.`;
|
|
74
|
+
}
|
|
75
|
+
return { content: [{ type: "text", text }] };
|
|
76
|
+
});
|
|
54
77
|
server.tool("run_loop", "Run the full implement-review-fix loop for all tasks. Dispatches agents and polls until done or max rounds reached.", {
|
|
55
78
|
max_rounds: z.number().optional().describe("Max review rounds per task (default: 3)"),
|
|
56
79
|
max_tasks: z.number().optional().describe("Max tasks to process (default: all)"),
|
|
57
80
|
timeout_seconds: z.number().optional().describe("Timeout per agent invocation in seconds (default: 300)"),
|
|
58
81
|
}, async ({ max_rounds, max_tasks, timeout_seconds }) => {
|
|
82
|
+
if (!isInitialized())
|
|
83
|
+
return NOT_SETUP;
|
|
84
|
+
if (isSingleEngine())
|
|
85
|
+
return SINGLE_MODE;
|
|
59
86
|
const maxR = max_rounds ?? 3;
|
|
60
87
|
const maxT = max_tasks ?? 999;
|
|
61
88
|
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/setup.js
CHANGED
|
@@ -8,11 +8,11 @@ export function registerSetupTools(server) {
|
|
|
8
8
|
const role = getRole();
|
|
9
9
|
server.tool("setup_project", "Initialize agent collaboration for this project. Creates the database, config files, hooks, and rules.", {
|
|
10
10
|
strategy: z.string().optional().describe("Strategy ID (default: architect-builder). Call list_strategies to see options."),
|
|
11
|
-
engine_mode: z.enum(["both", "cursor-only", "claude-code-only"]).
|
|
11
|
+
engine_mode: z.enum(["both", "cursor-only", "claude-code-only"]).describe("REQUIRED. Engine mode: 'both' (Cursor builds, Claude Code reviews), 'cursor-only' (Cursor does everything), or 'claude-code-only'."),
|
|
12
12
|
project_name: z.string().optional().describe("Project name (default: current directory name)"),
|
|
13
13
|
}, async ({ strategy, engine_mode, project_name }) => {
|
|
14
14
|
const strategyId = strategy || getDefaultStrategyId();
|
|
15
|
-
const mode = engine_mode
|
|
15
|
+
const mode = engine_mode;
|
|
16
16
|
const projName = project_name || path.basename(process.cwd());
|
|
17
17
|
const def = getStrategyDef(strategyId);
|
|
18
18
|
if (!def) {
|
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", "invoke_architect", "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()) {
|
|
@@ -7,20 +28,20 @@ export function registerStatusTools(server) {
|
|
|
7
28
|
type: "text",
|
|
8
29
|
text: [
|
|
9
30
|
"SETUP_NEEDED: This project hasn't been configured for agent collaboration yet.\n",
|
|
10
|
-
"
|
|
11
|
-
"1.
|
|
12
|
-
" -
|
|
31
|
+
"You MUST ask the user TWO questions before calling setup_project:\n",
|
|
32
|
+
"1. Engine mode (REQUIRED — ask the user explicitly):",
|
|
33
|
+
" - cursor-only: You handle everything alone (design + implement + review)",
|
|
34
|
+
" - both: Cursor implements, Claude Code designs and reviews (recommended for quality)",
|
|
35
|
+
" - claude-code-only: Claude Code handles everything alone\n",
|
|
36
|
+
"2. Strategy:",
|
|
37
|
+
" - architect-builder — One agent designs, the other builds",
|
|
13
38
|
" - tdd-red-green — One writes tests, the other makes them pass",
|
|
14
39
|
" - writer-reviewer — One writes code, the other critiques",
|
|
15
40
|
" - parallel-specialist — Domain split, cross-review",
|
|
16
41
|
" - planner-executor — Detailed specs, mechanical execution",
|
|
17
42
|
" - sequential-pipeline — Multi-stage quality review\n",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
" - both: Cursor implements, Claude Code reviews",
|
|
21
|
-
" - claude-code-only: Claude Code handles everything\n",
|
|
22
|
-
"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.",
|
|
43
|
+
"Then call setup_project(strategy, engine_mode) with BOTH values.\n",
|
|
44
|
+
"Available tools before setup: get_my_status, setup_project, list_strategies, get_dashboard_info",
|
|
24
45
|
].join("\n"),
|
|
25
46
|
}],
|
|
26
47
|
};
|
|
@@ -31,7 +52,8 @@ export function registerStatusTools(server) {
|
|
|
31
52
|
const roleConfig = getMyRoleConfig();
|
|
32
53
|
const single = isSingleEngine();
|
|
33
54
|
const access = roleConfig.tools;
|
|
34
|
-
const
|
|
55
|
+
const toolLine = `Your tools: ${buildToolList(access, single)}\n`;
|
|
56
|
+
const header = `[Strategy: ${strategy.name}] [Engine: ${engineMode}] [Role: ${roleConfig.name}]\n${toolLine}`;
|
|
35
57
|
const inProgress = db.prepare("SELECT id, title FROM tasks WHERE status = 'in-progress' LIMIT 1").get();
|
|
36
58
|
if (inProgress) {
|
|
37
59
|
return text(header, `RESUME: Task ${inProgress.id} "${inProgress.title}" is in-progress. Call get_task("${inProgress.id}") for details and continue working.`);
|
|
@@ -77,7 +99,7 @@ export function registerStatusTools(server) {
|
|
|
77
99
|
if (single) {
|
|
78
100
|
return text(header, `No tasks on the board.${historyHint} You have all tools — start by creating tasks with create_task(...).`);
|
|
79
101
|
}
|
|
80
|
-
return text(header, `
|
|
102
|
+
return text(header, `No tasks exist.${historyHint} Call invoke_architect("describe what the user wants built") to have Claude Code create the HLD and tasks. Pass the user's original request as the argument.`);
|
|
81
103
|
}
|
|
82
104
|
return text(header, "All tasks are done. Consider archiving this work with archive_epic(\"<name>\") to clear the board for the next feature. Or create new tasks if there are more requirements.");
|
|
83
105
|
});
|
|
@@ -91,8 +113,9 @@ export function registerStatusTools(server) {
|
|
|
91
113
|
const counts = db.prepare("SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status").all();
|
|
92
114
|
const total = counts.reduce((s, r) => s + r.cnt, 0);
|
|
93
115
|
const recent = db.prepare("SELECT timestamp, agent, action FROM activity_log ORDER BY id DESC LIMIT 5").all();
|
|
116
|
+
const epicCount = db.prepare("SELECT COUNT(*) as cnt FROM epics").get().cnt;
|
|
94
117
|
let out = `Strategy: ${strategy.name} | Engine mode: ${engineMode}\n`;
|
|
95
|
-
out += `Project: ${total} tasks total\n`;
|
|
118
|
+
out += `Project: ${total} tasks total | ${epicCount} archived epic(s)\n`;
|
|
96
119
|
for (const r of counts) {
|
|
97
120
|
out += ` ${r.status}: ${r.cnt}\n`;
|
|
98
121
|
}
|
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.1",
|
|
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",
|