ac-framework 1.9.7 → 1.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -11
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +3 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- package/src/agents/orchestrator.js +225 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/runtime.js +75 -3
- package/src/agents/state-store.js +73 -0
- package/src/commands/agents.js +616 -60
- package/src/commands/init.js +9 -5
- package/src/mcp/collab-server.js +378 -24
- package/src/mcp/test-harness.mjs +410 -0
- package/src/services/dependency-installer.js +72 -4
package/src/commands/init.js
CHANGED
|
@@ -156,7 +156,8 @@ async function setupPersistentMemory() {
|
|
|
156
156
|
async function setupCollaborativeSystem() {
|
|
157
157
|
const hasOpenCode = hasCommand('opencode');
|
|
158
158
|
const hasTmux = hasCommand('tmux');
|
|
159
|
-
const
|
|
159
|
+
const hasZellij = hasCommand('zellij');
|
|
160
|
+
const alreadyReady = hasOpenCode && (hasZellij || hasTmux);
|
|
160
161
|
|
|
161
162
|
console.log();
|
|
162
163
|
await animatedSeparator(60);
|
|
@@ -168,10 +169,10 @@ async function setupCollaborativeSystem() {
|
|
|
168
169
|
console.log(
|
|
169
170
|
chalk.hex('#636E72')(
|
|
170
171
|
` ${COLLAB_SYSTEM_NAME} launches a real-time collaborative agent war-room with\n` +
|
|
171
|
-
' 4 coordinated roles (planner, critic, coder, reviewer) in
|
|
172
|
+
' 4 coordinated roles (planner, critic, coder, reviewer) in multiplexer panes.\n\n' +
|
|
172
173
|
' Each round is turn-based with shared incremental context, so every\n' +
|
|
173
174
|
' contribution from one agent is fed to the next, not isolated fan-out.\n\n' +
|
|
174
|
-
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('tmux')}`
|
|
175
|
+
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('zellij')} (${chalk.hex('#DFE6E9')('tmux')} fallback)`
|
|
175
176
|
)
|
|
176
177
|
);
|
|
177
178
|
console.log();
|
|
@@ -223,7 +224,8 @@ async function setupCollaborativeSystem() {
|
|
|
223
224
|
|
|
224
225
|
if (alreadyReady) {
|
|
225
226
|
console.log();
|
|
226
|
-
|
|
227
|
+
const mux = hasZellij ? 'zellij' : 'tmux';
|
|
228
|
+
console.log(chalk.hex('#00B894')(` ◆ OpenCode and ${mux} are already available.`));
|
|
227
229
|
await installCollabMcpConnections();
|
|
228
230
|
console.log(chalk.hex('#636E72')(' Run `acfm agents start --task "..."` to launch collaboration.'));
|
|
229
231
|
console.log();
|
|
@@ -234,11 +236,13 @@ async function setupCollaborativeSystem() {
|
|
|
234
236
|
console.log(chalk.hex('#B2BEC3')(` Installing ${COLLAB_SYSTEM_NAME} dependencies...`));
|
|
235
237
|
console.log();
|
|
236
238
|
|
|
237
|
-
const result = ensureCollabDependencies();
|
|
239
|
+
const result = ensureCollabDependencies({ installZellij: true, installTmux: true });
|
|
238
240
|
|
|
239
241
|
const oColor = result.opencode.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
242
|
+
const zColor = result.zellij.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
240
243
|
const tColor = result.tmux.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
241
244
|
console.log(oColor(` ◆ OpenCode: ${result.opencode.message}`));
|
|
245
|
+
console.log(zColor(` ◆ zellij: ${result.zellij.message}`));
|
|
242
246
|
console.log(tColor(` ◆ tmux: ${result.tmux.message}`));
|
|
243
247
|
console.log();
|
|
244
248
|
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -7,12 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
10
15
|
import { z } from 'zod';
|
|
11
16
|
import { COLLAB_ROLES } from '../agents/constants.js';
|
|
12
17
|
import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-selection.js';
|
|
13
18
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
14
19
|
import { getSessionDir } from '../agents/state-store.js';
|
|
15
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
spawnTmuxSession,
|
|
22
|
+
spawnZellijSession,
|
|
23
|
+
tmuxSessionExists,
|
|
24
|
+
zellijSessionExists,
|
|
25
|
+
resolveMultiplexer,
|
|
26
|
+
} from '../agents/runtime.js';
|
|
16
27
|
import {
|
|
17
28
|
addUserMessage,
|
|
18
29
|
createSession,
|
|
@@ -24,6 +35,49 @@ import {
|
|
|
24
35
|
stopSession,
|
|
25
36
|
} from '../agents/state-store.js';
|
|
26
37
|
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
38
|
+
import { loadAgentsConfig } from '../agents/config-store.js';
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
42
|
+
|
|
43
|
+
function summarizeRun(state) {
|
|
44
|
+
const run = state.run || null;
|
|
45
|
+
return {
|
|
46
|
+
status: run?.status || 'idle',
|
|
47
|
+
runId: run?.runId || null,
|
|
48
|
+
currentRole: run?.currentRole || state.activeAgent || null,
|
|
49
|
+
round: run?.round || state.round,
|
|
50
|
+
policy: run?.policy || null,
|
|
51
|
+
lastError: run?.lastError || null,
|
|
52
|
+
eventCount: Array.isArray(run?.events) ? run.events.length : 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function latestRunEvent(state) {
|
|
57
|
+
const events = state?.run?.events;
|
|
58
|
+
if (!Array.isArray(events) || events.length === 0) return null;
|
|
59
|
+
return events[events.length - 1];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readSessionArtifact(sessionId, filename) {
|
|
63
|
+
const path = resolve(getSessionDir(sessionId), filename);
|
|
64
|
+
if (!existsSync(path)) return null;
|
|
65
|
+
return readFile(path, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function launchAutopilot(sessionId) {
|
|
69
|
+
const child = spawn('node', [runnerPath, 'agents', 'autopilot', '--session', sessionId], {
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: 'ignore',
|
|
73
|
+
});
|
|
74
|
+
child.unref();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function muxExists(multiplexer, sessionName) {
|
|
78
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName);
|
|
79
|
+
return tmuxSessionExists(sessionName);
|
|
80
|
+
}
|
|
27
81
|
|
|
28
82
|
class MCPCollabServer {
|
|
29
83
|
constructor() {
|
|
@@ -50,9 +104,14 @@ class MCPCollabServer {
|
|
|
50
104
|
reviewer: z.string().optional(),
|
|
51
105
|
}).partial().optional().describe('Optional per-role models (provider/model)'),
|
|
52
106
|
cwd: z.string().optional().describe('Working directory for agents'),
|
|
53
|
-
spawnWorkers: z.boolean().default(true).describe('Create
|
|
107
|
+
spawnWorkers: z.boolean().default(true).describe('Create multiplexer workers and panes'),
|
|
108
|
+
runPolicy: z.object({
|
|
109
|
+
timeoutPerRoleMs: z.number().int().positive().optional(),
|
|
110
|
+
retryOnTimeout: z.number().int().min(0).optional(),
|
|
111
|
+
fallbackOnFailure: z.enum(['retry', 'skip', 'abort']).optional(),
|
|
112
|
+
}).partial().optional().describe('Optional run execution policy'),
|
|
54
113
|
},
|
|
55
|
-
async ({ task, maxRounds, model, roleModels, cwd, spawnWorkers }) => {
|
|
114
|
+
async ({ task, maxRounds, model, roleModels, cwd, spawnWorkers, runPolicy }) => {
|
|
56
115
|
try {
|
|
57
116
|
const workingDirectory = cwd || process.cwd();
|
|
58
117
|
const opencodeBin = resolveCommandPath('opencode');
|
|
@@ -60,8 +119,11 @@ class MCPCollabServer {
|
|
|
60
119
|
throw new Error('OpenCode binary not found in PATH. Run: acfm agents setup');
|
|
61
120
|
}
|
|
62
121
|
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
const config = await loadAgentsConfig();
|
|
123
|
+
const configuredMux = config.agents.multiplexer || 'auto';
|
|
124
|
+
const multiplexer = resolveMultiplexer(configuredMux, hasCommand('tmux'), hasCommand('zellij'));
|
|
125
|
+
if (spawnWorkers && !multiplexer) {
|
|
126
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
65
127
|
}
|
|
66
128
|
|
|
67
129
|
const state = await createSession(task, {
|
|
@@ -71,18 +133,32 @@ class MCPCollabServer {
|
|
|
71
133
|
roleModels: sanitizeRoleModels(roleModels),
|
|
72
134
|
workingDirectory,
|
|
73
135
|
opencodeBin,
|
|
136
|
+
runPolicy,
|
|
137
|
+
multiplexer: multiplexer || configuredMux,
|
|
74
138
|
});
|
|
75
139
|
let updated = state;
|
|
76
140
|
if (spawnWorkers) {
|
|
77
|
-
const
|
|
141
|
+
const sessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
78
142
|
const sessionDir = getSessionDir(state.sessionId);
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
if (multiplexer === 'zellij') {
|
|
144
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
145
|
+
} else {
|
|
146
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
147
|
+
}
|
|
148
|
+
updated = await saveSessionState({
|
|
149
|
+
...state,
|
|
150
|
+
multiplexer,
|
|
151
|
+
multiplexerSessionName: sessionName,
|
|
152
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : null,
|
|
153
|
+
});
|
|
81
154
|
}
|
|
82
155
|
await setCurrentSession(state.sessionId);
|
|
83
156
|
|
|
84
|
-
const
|
|
85
|
-
const
|
|
157
|
+
const mux = updated.multiplexer || null;
|
|
158
|
+
const muxSessionName = updated.multiplexerSessionName || updated.tmuxSessionName || null;
|
|
159
|
+
const attachCommand = muxSessionName
|
|
160
|
+
? (mux === 'zellij' ? `zellij attach ${muxSessionName}` : `tmux attach -t ${muxSessionName}`)
|
|
161
|
+
: null;
|
|
86
162
|
return {
|
|
87
163
|
content: [{
|
|
88
164
|
type: 'text',
|
|
@@ -93,7 +169,9 @@ class MCPCollabServer {
|
|
|
93
169
|
model: updated.model || null,
|
|
94
170
|
roleModels: updated.roleModels || {},
|
|
95
171
|
effectiveRoleModels: buildEffectiveRoleModels(updated, updated.model || null),
|
|
96
|
-
|
|
172
|
+
run: summarizeRun(updated),
|
|
173
|
+
multiplexer: mux,
|
|
174
|
+
multiplexerSessionName: muxSessionName,
|
|
97
175
|
attachCommand,
|
|
98
176
|
}, null, 2),
|
|
99
177
|
}],
|
|
@@ -104,6 +182,191 @@ class MCPCollabServer {
|
|
|
104
182
|
}
|
|
105
183
|
);
|
|
106
184
|
|
|
185
|
+
this.server.tool(
|
|
186
|
+
'collab_invoke_team',
|
|
187
|
+
'Invoke full 4-agent collaborative run and return async run handle',
|
|
188
|
+
{
|
|
189
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
190
|
+
waitMs: z.number().int().min(0).max(30000).default(0).describe('Optional wait for progress before returning'),
|
|
191
|
+
},
|
|
192
|
+
async ({ sessionId, waitMs }) => {
|
|
193
|
+
try {
|
|
194
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
195
|
+
if (!id) throw new Error('No active session found');
|
|
196
|
+
let state = await loadSessionState(id);
|
|
197
|
+
if (state.status !== 'running') {
|
|
198
|
+
throw new Error(`Session is ${state.status}. Resume/start before invoking.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!state.multiplexerSessionName && !state.tmuxSessionName) {
|
|
202
|
+
launchAutopilot(state.sessionId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (waitMs > 0) {
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
const initialEvents = state.run?.events?.length || 0;
|
|
208
|
+
while (Date.now() - started < waitMs) {
|
|
209
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
210
|
+
state = await loadSessionState(id);
|
|
211
|
+
const currentEvents = state.run?.events?.length || 0;
|
|
212
|
+
if (state.status !== 'running' || currentEvents > initialEvents) break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
content: [{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: JSON.stringify({
|
|
220
|
+
success: true,
|
|
221
|
+
sessionId: state.sessionId,
|
|
222
|
+
status: state.status,
|
|
223
|
+
run: summarizeRun(state),
|
|
224
|
+
latestEvent: latestRunEvent(state),
|
|
225
|
+
multiplexer: state.multiplexer || null,
|
|
226
|
+
multiplexerSessionName: state.multiplexerSessionName || state.tmuxSessionName || null,
|
|
227
|
+
attachCommand: state.multiplexerSessionName
|
|
228
|
+
? (state.multiplexer === 'zellij'
|
|
229
|
+
? `zellij attach ${state.multiplexerSessionName}`
|
|
230
|
+
: `tmux attach -t ${state.multiplexerSessionName}`)
|
|
231
|
+
: (state.tmuxSessionName ? `tmux attach -t ${state.tmuxSessionName}` : null),
|
|
232
|
+
}, null, 2),
|
|
233
|
+
}],
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
this.server.tool(
|
|
242
|
+
'collab_wait_run',
|
|
243
|
+
'Wait for collaborative run to complete/fail or timeout',
|
|
244
|
+
{
|
|
245
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
246
|
+
waitMs: z.number().int().positive().max(60000).default(10000).describe('Max wait in milliseconds'),
|
|
247
|
+
},
|
|
248
|
+
async ({ sessionId, waitMs }) => {
|
|
249
|
+
try {
|
|
250
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
251
|
+
if (!id) throw new Error('No active session found');
|
|
252
|
+
const started = Date.now();
|
|
253
|
+
let state = await loadSessionState(id);
|
|
254
|
+
|
|
255
|
+
while (Date.now() - started < waitMs) {
|
|
256
|
+
const runStatus = state.run?.status || 'idle';
|
|
257
|
+
if (state.status !== 'running' || ['completed', 'failed', 'cancelled'].includes(runStatus)) break;
|
|
258
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
259
|
+
state = await loadSessionState(id);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
content: [{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: JSON.stringify({
|
|
266
|
+
success: true,
|
|
267
|
+
sessionId: state.sessionId,
|
|
268
|
+
status: state.status,
|
|
269
|
+
run: summarizeRun(state),
|
|
270
|
+
latestEvent: latestRunEvent(state),
|
|
271
|
+
timedOut: state.status === 'running' && !['completed', 'failed', 'cancelled'].includes(state.run?.status || 'idle'),
|
|
272
|
+
}, null, 2),
|
|
273
|
+
}],
|
|
274
|
+
};
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
this.server.tool(
|
|
282
|
+
'collab_get_result',
|
|
283
|
+
'Get final consolidated team output and run diagnostics',
|
|
284
|
+
{
|
|
285
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
286
|
+
},
|
|
287
|
+
async ({ sessionId }) => {
|
|
288
|
+
try {
|
|
289
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
290
|
+
if (!id) throw new Error('No active session found');
|
|
291
|
+
const state = await loadSessionState(id);
|
|
292
|
+
const transcript = await loadTranscript(id);
|
|
293
|
+
const run = summarizeRun(state);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
content: [{
|
|
297
|
+
type: 'text',
|
|
298
|
+
text: JSON.stringify({
|
|
299
|
+
success: true,
|
|
300
|
+
sessionId: state.sessionId,
|
|
301
|
+
status: state.status,
|
|
302
|
+
run,
|
|
303
|
+
latestEvent: latestRunEvent(state),
|
|
304
|
+
finalSummary: state.run?.finalSummary || null,
|
|
305
|
+
sharedContext: state.run?.sharedContext || null,
|
|
306
|
+
meetingSummary: await readSessionArtifact(id, 'meeting-summary.md'),
|
|
307
|
+
meetingLog: await readSessionArtifact(id, 'meeting-log.md'),
|
|
308
|
+
lastMessage: state.messages?.[state.messages.length - 1] || null,
|
|
309
|
+
transcriptCount: transcript.length,
|
|
310
|
+
}, null, 2),
|
|
311
|
+
}],
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
this.server.tool(
|
|
320
|
+
'collab_cancel_run',
|
|
321
|
+
'Cancel active collaborative run while keeping session state',
|
|
322
|
+
{
|
|
323
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
324
|
+
},
|
|
325
|
+
async ({ sessionId }) => {
|
|
326
|
+
try {
|
|
327
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
328
|
+
if (!id) throw new Error('No active session found');
|
|
329
|
+
const state = await loadSessionState(id);
|
|
330
|
+
const run = state.run || {};
|
|
331
|
+
const updated = await saveSessionState({
|
|
332
|
+
...state,
|
|
333
|
+
status: 'running',
|
|
334
|
+
activeAgent: null,
|
|
335
|
+
run: {
|
|
336
|
+
...run,
|
|
337
|
+
status: 'cancelled',
|
|
338
|
+
currentRole: null,
|
|
339
|
+
finishedAt: new Date().toISOString(),
|
|
340
|
+
lastError: {
|
|
341
|
+
code: 'RUN_CANCELLED',
|
|
342
|
+
message: 'Cancelled by MCP caller',
|
|
343
|
+
},
|
|
344
|
+
events: [
|
|
345
|
+
...(Array.isArray(run.events) ? run.events : []),
|
|
346
|
+
{
|
|
347
|
+
id: `evt-${Date.now()}`,
|
|
348
|
+
type: 'run_cancelled',
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
content: [{
|
|
356
|
+
type: 'text',
|
|
357
|
+
text: JSON.stringify({
|
|
358
|
+
success: true,
|
|
359
|
+
sessionId: updated.sessionId,
|
|
360
|
+
run: summarizeRun(updated),
|
|
361
|
+
}, null, 2),
|
|
362
|
+
}],
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
107
370
|
this.server.tool(
|
|
108
371
|
'collab_send_message',
|
|
109
372
|
'Send a user message to the active collaborative session',
|
|
@@ -150,6 +413,7 @@ class MCPCollabServer {
|
|
|
150
413
|
const state = await runWorkerIteration(id, role, {
|
|
151
414
|
cwd: process.cwd(),
|
|
152
415
|
opencodeBin: loaded.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
416
|
+
timeoutMs: loaded.run?.policy?.timeoutPerRoleMs || 180000,
|
|
153
417
|
});
|
|
154
418
|
|
|
155
419
|
return {
|
|
@@ -164,6 +428,8 @@ class MCPCollabServer {
|
|
|
164
428
|
model: state.model || null,
|
|
165
429
|
roleModels: state.roleModels || {},
|
|
166
430
|
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
431
|
+
run: summarizeRun(state),
|
|
432
|
+
latestEvent: latestRunEvent(state),
|
|
167
433
|
messageCount: state.messages.length,
|
|
168
434
|
}, null, 2),
|
|
169
435
|
}],
|
|
@@ -176,10 +442,10 @@ class MCPCollabServer {
|
|
|
176
442
|
|
|
177
443
|
this.server.tool(
|
|
178
444
|
'collab_resume_session',
|
|
179
|
-
'Resume session and recreate
|
|
445
|
+
'Resume session and recreate workers if needed',
|
|
180
446
|
{
|
|
181
447
|
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
182
|
-
recreateWorkers: z.boolean().default(true).describe('Recreate
|
|
448
|
+
recreateWorkers: z.boolean().default(true).describe('Recreate multiplexer session when missing'),
|
|
183
449
|
},
|
|
184
450
|
async ({ sessionId, recreateWorkers }) => {
|
|
185
451
|
try {
|
|
@@ -187,24 +453,37 @@ class MCPCollabServer {
|
|
|
187
453
|
if (!id) throw new Error('No active session found');
|
|
188
454
|
let state = await loadSessionState(id);
|
|
189
455
|
|
|
190
|
-
const
|
|
191
|
-
|
|
456
|
+
const multiplexer = state.multiplexer || resolveMultiplexer('auto', hasCommand('tmux'), hasCommand('zellij'));
|
|
457
|
+
if (!multiplexer) {
|
|
458
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
459
|
+
}
|
|
460
|
+
const sessionName = state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
461
|
+
const sessionExists = await muxExists(multiplexer, sessionName);
|
|
192
462
|
|
|
193
|
-
if (!
|
|
194
|
-
if (!hasCommand('tmux')) {
|
|
195
|
-
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
196
|
-
}
|
|
463
|
+
if (!sessionExists && recreateWorkers) {
|
|
197
464
|
const sessionDir = getSessionDir(state.sessionId);
|
|
198
|
-
|
|
465
|
+
if (multiplexer === 'zellij') {
|
|
466
|
+
if (!hasCommand('zellij')) throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
467
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
468
|
+
} else {
|
|
469
|
+
if (!hasCommand('tmux')) throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
470
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
471
|
+
}
|
|
199
472
|
}
|
|
200
473
|
|
|
201
474
|
state = await saveSessionState({
|
|
202
475
|
...state,
|
|
203
476
|
status: 'running',
|
|
204
|
-
|
|
477
|
+
multiplexer,
|
|
478
|
+
multiplexerSessionName: sessionName,
|
|
479
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : state.tmuxSessionName || null,
|
|
205
480
|
});
|
|
206
481
|
await setCurrentSession(state.sessionId);
|
|
207
482
|
|
|
483
|
+
const attachCommand = multiplexer === 'zellij'
|
|
484
|
+
? `zellij attach ${sessionName}`
|
|
485
|
+
: `tmux attach -t ${sessionName}`;
|
|
486
|
+
|
|
208
487
|
return {
|
|
209
488
|
content: [{
|
|
210
489
|
type: 'text',
|
|
@@ -212,8 +491,10 @@ class MCPCollabServer {
|
|
|
212
491
|
success: true,
|
|
213
492
|
sessionId: state.sessionId,
|
|
214
493
|
status: state.status,
|
|
215
|
-
|
|
216
|
-
|
|
494
|
+
multiplexer,
|
|
495
|
+
multiplexerSessionName: sessionName,
|
|
496
|
+
recreatedWorkers: !sessionExists && recreateWorkers,
|
|
497
|
+
attachCommand,
|
|
217
498
|
}, null, 2),
|
|
218
499
|
}],
|
|
219
500
|
};
|
|
@@ -244,6 +525,9 @@ class MCPCollabServer {
|
|
|
244
525
|
text: JSON.stringify({
|
|
245
526
|
state,
|
|
246
527
|
effectiveRoleModels: buildEffectiveRoleModels(state, state.model || null),
|
|
528
|
+
run: summarizeRun(state),
|
|
529
|
+
latestEvent: latestRunEvent(state),
|
|
530
|
+
sharedContext: state.run?.sharedContext || null,
|
|
247
531
|
transcript,
|
|
248
532
|
}, null, 2),
|
|
249
533
|
}],
|
|
@@ -254,6 +538,71 @@ class MCPCollabServer {
|
|
|
254
538
|
}
|
|
255
539
|
);
|
|
256
540
|
|
|
541
|
+
this.server.tool(
|
|
542
|
+
'collab_get_transcript',
|
|
543
|
+
'Get transcript messages with optional role filtering',
|
|
544
|
+
{
|
|
545
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
546
|
+
role: z.enum(['planner', 'critic', 'coder', 'reviewer', 'all']).default('all').describe('Role filter'),
|
|
547
|
+
limit: z.number().int().positive().max(500).default(100).describe('Max messages to return'),
|
|
548
|
+
},
|
|
549
|
+
async ({ sessionId, role, limit }) => {
|
|
550
|
+
try {
|
|
551
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
552
|
+
if (!id) throw new Error('No active session found');
|
|
553
|
+
const transcript = await loadTranscript(id);
|
|
554
|
+
const filtered = transcript
|
|
555
|
+
.filter((msg) => role === 'all' || msg.from === role)
|
|
556
|
+
.slice(-limit);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
content: [{
|
|
560
|
+
type: 'text',
|
|
561
|
+
text: JSON.stringify({
|
|
562
|
+
sessionId: id,
|
|
563
|
+
role,
|
|
564
|
+
count: filtered.length,
|
|
565
|
+
transcript: filtered,
|
|
566
|
+
}, null, 2),
|
|
567
|
+
}],
|
|
568
|
+
};
|
|
569
|
+
} catch (error) {
|
|
570
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
this.server.tool(
|
|
576
|
+
'collab_get_meeting_log',
|
|
577
|
+
'Get generated meeting log and summary artifacts',
|
|
578
|
+
{
|
|
579
|
+
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
580
|
+
},
|
|
581
|
+
async ({ sessionId }) => {
|
|
582
|
+
try {
|
|
583
|
+
const id = sessionId || await loadCurrentSessionId();
|
|
584
|
+
if (!id) throw new Error('No active session found');
|
|
585
|
+
const state = await loadSessionState(id);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
content: [{
|
|
589
|
+
type: 'text',
|
|
590
|
+
text: JSON.stringify({
|
|
591
|
+
sessionId: id,
|
|
592
|
+
status: state.status,
|
|
593
|
+
finalSummary: state.run?.finalSummary || null,
|
|
594
|
+
sharedContext: state.run?.sharedContext || null,
|
|
595
|
+
meetingSummary: await readSessionArtifact(id, 'meeting-summary.md'),
|
|
596
|
+
meetingLog: await readSessionArtifact(id, 'meeting-log.md'),
|
|
597
|
+
}, null, 2),
|
|
598
|
+
}],
|
|
599
|
+
};
|
|
600
|
+
} catch (error) {
|
|
601
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
|
|
257
606
|
this.server.tool(
|
|
258
607
|
'collab_stop_session',
|
|
259
608
|
'Stop current collaborative session',
|
|
@@ -271,7 +620,12 @@ class MCPCollabServer {
|
|
|
271
620
|
return {
|
|
272
621
|
content: [{
|
|
273
622
|
type: 'text',
|
|
274
|
-
text: JSON.stringify({
|
|
623
|
+
text: JSON.stringify({
|
|
624
|
+
success: true,
|
|
625
|
+
sessionId: id,
|
|
626
|
+
status: updated.status,
|
|
627
|
+
run: summarizeRun(updated),
|
|
628
|
+
}, null, 2),
|
|
275
629
|
}],
|
|
276
630
|
};
|
|
277
631
|
} catch (error) {
|