ac-framework 1.9.7 → 1.9.8
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 +25 -2
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/constants.js +3 -1
- package/src/agents/opencode-client.js +101 -5
- package/src/agents/orchestrator.js +199 -6
- package/src/agents/role-prompts.js +34 -1
- package/src/agents/run-state.js +113 -0
- package/src/agents/state-store.js +69 -0
- package/src/commands/agents.js +318 -4
- package/src/mcp/collab-server.js +307 -2
- package/src/mcp/test-harness.mjs +410 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Test Harness - Minimal server for async demo and testing
|
|
5
|
+
*
|
|
6
|
+
* SDK: @modelcontextprotocol/sdk@^1.27.1
|
|
7
|
+
* Transport: StdioServerTransport (stdio only)
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real SDK behavior (not mocks)
|
|
11
|
+
* - In-memory storage for memory_* tools
|
|
12
|
+
* - Mocked collab_* tool responses
|
|
13
|
+
* - Intentional error injection for Demo 3
|
|
14
|
+
* - Lifecycle support for Demo 5
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
|
|
21
|
+
const MEMORY_DB = new Map();
|
|
22
|
+
const COLLAB_SESSIONS = new Map();
|
|
23
|
+
|
|
24
|
+
let requestCount = 0;
|
|
25
|
+
let errorInjectionMode = null;
|
|
26
|
+
|
|
27
|
+
function setErrorInjection(mode) {
|
|
28
|
+
errorInjectionMode = mode;
|
|
29
|
+
console.error(`[harness] Error injection mode: ${mode || 'none'}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleError(errorType, defaultMessage) {
|
|
33
|
+
if (!errorInjectionMode || errorInjectionMode !== errorType) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: JSON.stringify({ error: defaultMessage }) }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createSuccessResponse(data) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createErrorResponse(message) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
51
|
+
isError: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class MCPTestHarness {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.server = new McpServer({
|
|
58
|
+
name: 'acfm-test-harness',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
});
|
|
61
|
+
this.setupTools();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setupTools() {
|
|
65
|
+
// ── memory_save ─────────────────────────────────────────────────────────
|
|
66
|
+
this.server.tool(
|
|
67
|
+
'memory_save',
|
|
68
|
+
'Save a memory observation (test harness)',
|
|
69
|
+
{
|
|
70
|
+
content: z.string().describe('Content to save'),
|
|
71
|
+
type: z.enum([
|
|
72
|
+
'architectural_decision', 'bugfix_pattern', 'api_pattern',
|
|
73
|
+
'performance_insight', 'security_fix', 'refactor_technique',
|
|
74
|
+
'dependency_note', 'workaround', 'convention', 'context_boundary',
|
|
75
|
+
'general_insight', 'session_summary'
|
|
76
|
+
]).default('general_insight'),
|
|
77
|
+
importance: z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
|
78
|
+
tags: z.array(z.string()).optional(),
|
|
79
|
+
projectPath: z.string().optional(),
|
|
80
|
+
changeName: z.string().optional(),
|
|
81
|
+
confidence: z.number().min(0).max(1).default(0.8)
|
|
82
|
+
},
|
|
83
|
+
async ({ content, type, importance, tags, projectPath, changeName, confidence }) => {
|
|
84
|
+
requestCount++;
|
|
85
|
+
const id = Date.now();
|
|
86
|
+
|
|
87
|
+
const validationError = handleError('validation', 'Missing required param: content');
|
|
88
|
+
if (validationError) return validationError;
|
|
89
|
+
|
|
90
|
+
const dbError = handleError('db_failure', 'Database write failed: disk full');
|
|
91
|
+
if (dbError) return dbError;
|
|
92
|
+
|
|
93
|
+
const memory = {
|
|
94
|
+
id,
|
|
95
|
+
content,
|
|
96
|
+
type,
|
|
97
|
+
importance,
|
|
98
|
+
tags: tags || [],
|
|
99
|
+
projectPath,
|
|
100
|
+
changeName,
|
|
101
|
+
confidence,
|
|
102
|
+
createdAt: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
MEMORY_DB.set(id.toString(), memory);
|
|
105
|
+
|
|
106
|
+
return createSuccessResponse({ success: true, id, operation: 'insert', revisionCount: 1 });
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ── memory_search ───────────────────────────────────────────────────────
|
|
111
|
+
this.server.tool(
|
|
112
|
+
'memory_search',
|
|
113
|
+
'Search memories using text query (test harness)',
|
|
114
|
+
{
|
|
115
|
+
query: z.string().describe('Search query'),
|
|
116
|
+
limit: z.number().int().positive().default(10),
|
|
117
|
+
type: z.enum([
|
|
118
|
+
'architectural_decision', 'bugfix_pattern', 'api_pattern',
|
|
119
|
+
'performance_insight', 'security_fix', 'refactor_technique',
|
|
120
|
+
'dependency_note', 'workaround', 'convention', 'context_boundary',
|
|
121
|
+
'general_insight', 'session_summary'
|
|
122
|
+
]).optional(),
|
|
123
|
+
importance: z.enum(['critical', 'high', 'medium', 'low']).optional(),
|
|
124
|
+
projectPath: z.string().optional(),
|
|
125
|
+
minConfidence: z.number().min(0).max(1).default(0)
|
|
126
|
+
},
|
|
127
|
+
async ({ query, limit, type, importance, projectPath, minConfidence }) => {
|
|
128
|
+
requestCount++;
|
|
129
|
+
|
|
130
|
+
const validationError = handleError('validation', 'Invalid query parameter');
|
|
131
|
+
if (validationError) return validationError;
|
|
132
|
+
|
|
133
|
+
const results = [];
|
|
134
|
+
const queryLower = query.toLowerCase();
|
|
135
|
+
for (const [id, memory] of MEMORY_DB) {
|
|
136
|
+
if (memory.content.toLowerCase().includes(queryLower)) {
|
|
137
|
+
if (type && memory.type !== type) continue;
|
|
138
|
+
if (importance && memory.importance !== importance) continue;
|
|
139
|
+
if (projectPath && memory.projectPath !== projectPath) continue;
|
|
140
|
+
if (memory.confidence < minConfidence) continue;
|
|
141
|
+
results.push(memory);
|
|
142
|
+
if (results.length >= limit) break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return createSuccessResponse({ query, count: results.length, results });
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ── memory_recall ───────────────────────────────────────────────────────
|
|
151
|
+
this.server.tool(
|
|
152
|
+
'memory_recall',
|
|
153
|
+
'Recall relevant context for a task (test harness)',
|
|
154
|
+
{
|
|
155
|
+
task: z.string().optional(),
|
|
156
|
+
projectPath: z.string().optional(),
|
|
157
|
+
changeName: z.string().optional(),
|
|
158
|
+
limit: z.number().int().positive().default(5),
|
|
159
|
+
days: z.number().int().positive().default(30)
|
|
160
|
+
},
|
|
161
|
+
async ({ task, projectPath, changeName, limit, days }) => {
|
|
162
|
+
requestCount++;
|
|
163
|
+
|
|
164
|
+
const dbError = handleError('db_failure', 'Database read failed: connection lost');
|
|
165
|
+
if (dbError) return dbError;
|
|
166
|
+
|
|
167
|
+
const memories = Array.from(MEMORY_DB.values())
|
|
168
|
+
.filter(m => {
|
|
169
|
+
if (task && !m.content.toLowerCase().includes(task.toLowerCase())) return false;
|
|
170
|
+
if (projectPath && m.projectPath !== projectPath) return false;
|
|
171
|
+
if (changeName && m.changeName !== changeName) return false;
|
|
172
|
+
return true;
|
|
173
|
+
})
|
|
174
|
+
.slice(0, limit);
|
|
175
|
+
|
|
176
|
+
return createSuccessResponse({ task: task || null, projectPath, count: memories.length, memories });
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── memory_stats ────────────────────────────────────────────────────────
|
|
181
|
+
this.server.tool(
|
|
182
|
+
'memory_stats',
|
|
183
|
+
'Get memory system statistics (test harness)',
|
|
184
|
+
{
|
|
185
|
+
projectPath: z.string().optional(),
|
|
186
|
+
since: z.string().optional()
|
|
187
|
+
},
|
|
188
|
+
async ({ projectPath, since }) => {
|
|
189
|
+
requestCount++;
|
|
190
|
+
|
|
191
|
+
let memories = Array.from(MEMORY_DB.values());
|
|
192
|
+
if (projectPath) {
|
|
193
|
+
memories = memories.filter(m => m.projectPath === projectPath);
|
|
194
|
+
}
|
|
195
|
+
if (since) {
|
|
196
|
+
const sinceDate = new Date(since);
|
|
197
|
+
memories = memories.filter(m => new Date(m.createdAt) >= sinceDate);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const byType = {};
|
|
201
|
+
const byImportance = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
202
|
+
|
|
203
|
+
for (const m of memories) {
|
|
204
|
+
byType[m.type] = (byType[m.type] || 0) + 1;
|
|
205
|
+
byImportance[m.importance]++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return createSuccessResponse({
|
|
209
|
+
totalMemories: memories.length,
|
|
210
|
+
byType,
|
|
211
|
+
byImportance,
|
|
212
|
+
projectPath: projectPath || null,
|
|
213
|
+
since: since || null
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ── collab_start_session (MOCKED) ───────────────────────────────────────
|
|
219
|
+
this.server.tool(
|
|
220
|
+
'collab_start_session',
|
|
221
|
+
'Start a collaboration session (mocked in test harness)',
|
|
222
|
+
{
|
|
223
|
+
task: z.string().describe('Initial task'),
|
|
224
|
+
maxRounds: z.number().int().positive().default(3),
|
|
225
|
+
model: z.string().optional()
|
|
226
|
+
},
|
|
227
|
+
async ({ task, maxRounds, model }) => {
|
|
228
|
+
requestCount++;
|
|
229
|
+
|
|
230
|
+
const serverError = handleError('server_offline', 'Collab server not running');
|
|
231
|
+
if (serverError) return serverError;
|
|
232
|
+
|
|
233
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
234
|
+
const state = {
|
|
235
|
+
sessionId,
|
|
236
|
+
task,
|
|
237
|
+
maxRounds,
|
|
238
|
+
model: model || 'opencode/default',
|
|
239
|
+
status: 'initialized',
|
|
240
|
+
round: 0,
|
|
241
|
+
events: []
|
|
242
|
+
};
|
|
243
|
+
COLLAB_SESSIONS.set(sessionId, state);
|
|
244
|
+
|
|
245
|
+
return createSuccessResponse({
|
|
246
|
+
success: true,
|
|
247
|
+
sessionId,
|
|
248
|
+
status: state.status,
|
|
249
|
+
model: state.model,
|
|
250
|
+
tmuxSessionName: null,
|
|
251
|
+
attachCommand: null
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// ── collab_step (MOCKED) ────────────────────────────────────────────────
|
|
257
|
+
this.server.tool(
|
|
258
|
+
'collab_step',
|
|
259
|
+
'Execute a single collaboration step (mocked)',
|
|
260
|
+
{
|
|
261
|
+
sessionId: z.string().describe('Session ID'),
|
|
262
|
+
role: z.enum(['planner', 'critic', 'coder', 'reviewer']).describe('Role to execute')
|
|
263
|
+
},
|
|
264
|
+
async ({ sessionId, role }) => {
|
|
265
|
+
requestCount++;
|
|
266
|
+
|
|
267
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
268
|
+
if (!session) {
|
|
269
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
session.round++;
|
|
273
|
+
const event = {
|
|
274
|
+
role,
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
status: 'completed',
|
|
277
|
+
output: `[MOCK] ${role} completed round ${session.round}`
|
|
278
|
+
};
|
|
279
|
+
session.events.push(event);
|
|
280
|
+
|
|
281
|
+
return createSuccessResponse({
|
|
282
|
+
sessionId,
|
|
283
|
+
round: session.round,
|
|
284
|
+
event,
|
|
285
|
+
status: session.round >= session.maxRounds ? 'complete' : 'in_progress'
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ── collab_get_result (MOCKED) ──────────────────────────────────────────
|
|
291
|
+
this.server.tool(
|
|
292
|
+
'collab_get_result',
|
|
293
|
+
'Get collaboration session result (mocked)',
|
|
294
|
+
{
|
|
295
|
+
sessionId: z.string().describe('Session ID')
|
|
296
|
+
},
|
|
297
|
+
async ({ sessionId }) => {
|
|
298
|
+
requestCount++;
|
|
299
|
+
|
|
300
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
301
|
+
if (!session) {
|
|
302
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return createSuccessResponse({
|
|
306
|
+
sessionId,
|
|
307
|
+
status: session.status,
|
|
308
|
+
rounds: session.round,
|
|
309
|
+
events: session.events,
|
|
310
|
+
summary: `[MOCK] Completed ${session.round} rounds for: ${session.task}`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// ── collab_status (MOCKED) ─────────────────────────────────────────────
|
|
316
|
+
this.server.tool(
|
|
317
|
+
'collab_status',
|
|
318
|
+
'Get collaboration system status (mocked)',
|
|
319
|
+
{
|
|
320
|
+
sessionId: z.string().optional()
|
|
321
|
+
},
|
|
322
|
+
async ({ sessionId }) => {
|
|
323
|
+
requestCount++;
|
|
324
|
+
|
|
325
|
+
if (sessionId) {
|
|
326
|
+
const session = COLLAB_SESSIONS.get(sessionId);
|
|
327
|
+
if (!session) {
|
|
328
|
+
return createErrorResponse(`Session not found: ${sessionId}`);
|
|
329
|
+
}
|
|
330
|
+
return createSuccessResponse({
|
|
331
|
+
sessionId,
|
|
332
|
+
status: session.status,
|
|
333
|
+
round: session.round,
|
|
334
|
+
maxRounds: session.maxRounds
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return createSuccessResponse({
|
|
339
|
+
activeSessions: COLLAB_SESSIONS.size,
|
|
340
|
+
totalRequests: requestCount,
|
|
341
|
+
uptime: process.uptime()
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// ── harness_control (DEBUG) ─────────────────────────────────────────────
|
|
347
|
+
this.server.tool(
|
|
348
|
+
'harness_control',
|
|
349
|
+
'Control test harness behavior (debug only)',
|
|
350
|
+
{
|
|
351
|
+
action: z.enum(['reset', 'inject_error', 'clear_errors', 'status']).describe('Action to perform'),
|
|
352
|
+
errorType: z.string().optional().describe('Error type: validation, db_failure, server_offline')
|
|
353
|
+
},
|
|
354
|
+
async ({ action, errorType }) => {
|
|
355
|
+
switch (action) {
|
|
356
|
+
case 'reset':
|
|
357
|
+
MEMORY_DB.clear();
|
|
358
|
+
COLLAB_SESSIONS.clear();
|
|
359
|
+
requestCount = 0;
|
|
360
|
+
errorInjectionMode = null;
|
|
361
|
+
return createSuccessResponse({ message: 'Harness reset complete' });
|
|
362
|
+
|
|
363
|
+
case 'inject_error':
|
|
364
|
+
setErrorInjection(errorType);
|
|
365
|
+
return createSuccessResponse({ message: `Error injection set: ${errorType}` });
|
|
366
|
+
|
|
367
|
+
case 'clear_errors':
|
|
368
|
+
setErrorInjection(null);
|
|
369
|
+
return createSuccessResponse({ message: 'Error injection cleared' });
|
|
370
|
+
|
|
371
|
+
case 'status':
|
|
372
|
+
return createSuccessResponse({
|
|
373
|
+
memories: MEMORY_DB.size,
|
|
374
|
+
sessions: COLLAB_SESSIONS.size,
|
|
375
|
+
requestCount,
|
|
376
|
+
errorInjectionMode: errorInjectionMode || 'none'
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
default:
|
|
380
|
+
return createErrorResponse(`Unknown action: ${action}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async run() {
|
|
387
|
+
const transport = new StdioServerTransport();
|
|
388
|
+
await this.server.connect(transport);
|
|
389
|
+
console.error('[harness] Test harness ready on stdio');
|
|
390
|
+
console.error('[harness] Available tools: memory_save, memory_search, memory_recall, memory_stats');
|
|
391
|
+
console.error('[harness] Collab tools (mocked): collab_start_session, collab_step, collab_get_result, collab_status');
|
|
392
|
+
console.error('[harness] Debug tool: harness_control (reset, inject_error, clear_errors, status)');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const harness = new MCPTestHarness();
|
|
397
|
+
harness.run().catch(err => {
|
|
398
|
+
console.error('[harness] Fatal error:', err.message);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
process.on('SIGTERM', () => {
|
|
403
|
+
console.error('[harness] Received SIGTERM, shutting down gracefully');
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
process.on('SIGINT', () => {
|
|
408
|
+
console.error('[harness] Received SIGINT, shutting down');
|
|
409
|
+
process.exit(0);
|
|
410
|
+
});
|