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,113 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ROLE_RETRIES,
|
|
4
|
+
DEFAULT_ROLE_TIMEOUT_MS,
|
|
5
|
+
DEFAULT_MAX_ROUNDS,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
|
|
8
|
+
export function normalizeRunPolicy(policy = {}, maxRounds = DEFAULT_MAX_ROUNDS) {
|
|
9
|
+
const timeoutPerRoleMs = Number.isInteger(policy.timeoutPerRoleMs) && policy.timeoutPerRoleMs > 0
|
|
10
|
+
? policy.timeoutPerRoleMs
|
|
11
|
+
: DEFAULT_ROLE_TIMEOUT_MS;
|
|
12
|
+
const retryOnTimeout = Number.isInteger(policy.retryOnTimeout) && policy.retryOnTimeout >= 0
|
|
13
|
+
? policy.retryOnTimeout
|
|
14
|
+
: DEFAULT_ROLE_RETRIES;
|
|
15
|
+
const fallbackOnFailure = ['retry', 'skip', 'abort'].includes(policy.fallbackOnFailure)
|
|
16
|
+
? policy.fallbackOnFailure
|
|
17
|
+
: 'abort';
|
|
18
|
+
const rounds = Number.isInteger(maxRounds) && maxRounds > 0 ? maxRounds : DEFAULT_MAX_ROUNDS;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
timeoutPerRoleMs,
|
|
22
|
+
retryOnTimeout,
|
|
23
|
+
fallbackOnFailure,
|
|
24
|
+
maxRounds: rounds,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createRunState(policy = {}, maxRounds = DEFAULT_MAX_ROUNDS) {
|
|
29
|
+
return {
|
|
30
|
+
runId: randomUUID(),
|
|
31
|
+
status: 'idle',
|
|
32
|
+
startedAt: null,
|
|
33
|
+
finishedAt: null,
|
|
34
|
+
currentRole: null,
|
|
35
|
+
retriesUsed: {},
|
|
36
|
+
round: 1,
|
|
37
|
+
events: [],
|
|
38
|
+
finalSummary: null,
|
|
39
|
+
sharedContext: {
|
|
40
|
+
decisions: [],
|
|
41
|
+
openIssues: [],
|
|
42
|
+
risks: [],
|
|
43
|
+
actionItems: [],
|
|
44
|
+
notes: [],
|
|
45
|
+
},
|
|
46
|
+
lastError: null,
|
|
47
|
+
policy: normalizeRunPolicy(policy, maxRounds),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function appendRunEvent(run, type, details = {}) {
|
|
52
|
+
const event = {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
type,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
...details,
|
|
57
|
+
};
|
|
58
|
+
const events = [...(run.events || []), event];
|
|
59
|
+
return {
|
|
60
|
+
...run,
|
|
61
|
+
events,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function roleRetryCount(run, role) {
|
|
66
|
+
return Number(run?.retriesUsed?.[role] || 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function incrementRoleRetry(run, role) {
|
|
70
|
+
return {
|
|
71
|
+
...run,
|
|
72
|
+
retriesUsed: {
|
|
73
|
+
...(run.retriesUsed || {}),
|
|
74
|
+
[role]: roleRetryCount(run, role) + 1,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extractFinalSummary(messages = [], run = null) {
|
|
80
|
+
const agentMessages = messages.filter((msg) => msg?.from && msg.from !== 'user');
|
|
81
|
+
if (agentMessages.length === 0) return '';
|
|
82
|
+
const orderedRoles = ['planner', 'critic', 'coder', 'reviewer'];
|
|
83
|
+
const lastByRole = new Map();
|
|
84
|
+
for (const msg of agentMessages) lastByRole.set(msg.from, msg.content || '');
|
|
85
|
+
|
|
86
|
+
const sections = ['# SynapseGrid Final Summary', ''];
|
|
87
|
+
sections.push('## Per-role last contributions');
|
|
88
|
+
for (const role of orderedRoles) {
|
|
89
|
+
const content = String(lastByRole.get(role) || '').trim();
|
|
90
|
+
sections.push(`- ${role}: ${content ? content.slice(0, 500) : '(none)'}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const shared = run?.sharedContext;
|
|
94
|
+
if (shared && typeof shared === 'object') {
|
|
95
|
+
const writeList = (title, items) => {
|
|
96
|
+
sections.push('');
|
|
97
|
+
sections.push(`## ${title}`);
|
|
98
|
+
const list = Array.isArray(items) ? items.slice(-10) : [];
|
|
99
|
+
if (list.length === 0) {
|
|
100
|
+
sections.push('- (none)');
|
|
101
|
+
} else {
|
|
102
|
+
for (const item of list) sections.push(`- ${item}`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
writeList('Decisions', shared.decisions);
|
|
107
|
+
writeList('Open issues', shared.openIssues);
|
|
108
|
+
writeList('Risks', shared.risks);
|
|
109
|
+
writeList('Action items', shared.actionItems);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sections.join('\n').trim();
|
|
113
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SESSION_ROOT_DIR,
|
|
9
9
|
CURRENT_SESSION_FILE,
|
|
10
10
|
} from './constants.js';
|
|
11
|
+
import { createRunState } from './run-state.js';
|
|
11
12
|
import { sanitizeRoleModels } from './model-selection.js';
|
|
12
13
|
|
|
13
14
|
function sleep(ms) {
|
|
@@ -38,6 +39,22 @@ function getTranscriptPath(sessionId) {
|
|
|
38
39
|
return join(getSessionDir(sessionId), 'transcript.jsonl');
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function getTurnsDir(sessionId) {
|
|
43
|
+
return join(getSessionDir(sessionId), 'turns');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getMeetingLogPath(sessionId) {
|
|
47
|
+
return join(getSessionDir(sessionId), 'meeting-log.md');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMeetingLogJsonlPath(sessionId) {
|
|
51
|
+
return join(getSessionDir(sessionId), 'meeting-log.jsonl');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getMeetingSummaryPath(sessionId) {
|
|
55
|
+
return join(getSessionDir(sessionId), 'meeting-summary.md');
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
function initialState(task, options = {}) {
|
|
42
59
|
const sessionId = randomUUID();
|
|
43
60
|
const createdAt = new Date().toISOString();
|
|
@@ -57,6 +74,7 @@ function initialState(task, options = {}) {
|
|
|
57
74
|
roleModels: sanitizeRoleModels(options.roleModels),
|
|
58
75
|
opencodeBin: options.opencodeBin || null,
|
|
59
76
|
tmuxSessionName: options.tmuxSessionName || null,
|
|
77
|
+
run: createRunState(options.runPolicy, Number.isInteger(options.maxRounds) ? options.maxRounds : DEFAULT_MAX_ROUNDS),
|
|
60
78
|
messages: [
|
|
61
79
|
{
|
|
62
80
|
from: 'user',
|
|
@@ -91,6 +109,57 @@ export async function appendTranscript(sessionId, message) {
|
|
|
91
109
|
await appendFile(transcriptPath, line, 'utf8');
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
export async function appendMeetingTurn(sessionId, turnRecord) {
|
|
113
|
+
const sessionDir = getSessionDir(sessionId);
|
|
114
|
+
const turnsDir = getTurnsDir(sessionId);
|
|
115
|
+
await mkdir(sessionDir, { recursive: true });
|
|
116
|
+
await mkdir(turnsDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const safeRole = String(turnRecord?.role || 'unknown').replace(/[^a-z0-9_-]/gi, '_');
|
|
119
|
+
const safeRound = Number.isInteger(turnRecord?.round) ? turnRecord.round : 0;
|
|
120
|
+
const turnFilePath = join(turnsDir, `${String(safeRound).padStart(3, '0')}-${safeRole}.json`);
|
|
121
|
+
await writeFile(turnFilePath, JSON.stringify(turnRecord, null, 2) + '\n', 'utf8');
|
|
122
|
+
|
|
123
|
+
const mdPath = getMeetingLogPath(sessionId);
|
|
124
|
+
const jsonlPath = getMeetingLogJsonlPath(sessionId);
|
|
125
|
+
const snippet = (turnRecord?.snippet || '').trim() || '(empty output)';
|
|
126
|
+
const keyPoints = Array.isArray(turnRecord?.keyPoints) ? turnRecord.keyPoints : [];
|
|
127
|
+
|
|
128
|
+
const mdBlock = [
|
|
129
|
+
`## Round ${safeRound} - ${safeRole}`,
|
|
130
|
+
`- timestamp: ${turnRecord?.timestamp || new Date().toISOString()}`,
|
|
131
|
+
`- model: ${turnRecord?.model || '(default)'}`,
|
|
132
|
+
`- events: ${turnRecord?.eventCount ?? 0}`,
|
|
133
|
+
'',
|
|
134
|
+
'### Output snippet',
|
|
135
|
+
snippet,
|
|
136
|
+
'',
|
|
137
|
+
'### Key points',
|
|
138
|
+
...(keyPoints.length > 0 ? keyPoints.map((line) => `- ${line.replace(/^[-*]\s+/, '')}`) : ['- (none)']),
|
|
139
|
+
'',
|
|
140
|
+
].join('\n');
|
|
141
|
+
|
|
142
|
+
if (!existsSync(mdPath)) {
|
|
143
|
+
const header = `# SynapseGrid Meeting Log\n\nSession: ${sessionId}\n\n`;
|
|
144
|
+
await writeFile(mdPath, header + mdBlock, 'utf8');
|
|
145
|
+
} else {
|
|
146
|
+
await appendFile(mdPath, mdBlock, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await appendFile(jsonlPath, JSON.stringify(turnRecord) + '\n', 'utf8');
|
|
150
|
+
return {
|
|
151
|
+
turnFilePath,
|
|
152
|
+
meetingLogPath: mdPath,
|
|
153
|
+
meetingJsonlPath: jsonlPath,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function writeMeetingSummary(sessionId, summaryMarkdown) {
|
|
158
|
+
const outputPath = getMeetingSummaryPath(sessionId);
|
|
159
|
+
await writeFile(outputPath, String(summaryMarkdown || '').trimEnd() + '\n', 'utf8');
|
|
160
|
+
return outputPath;
|
|
161
|
+
}
|
|
162
|
+
|
|
94
163
|
export async function loadCurrentSessionId() {
|
|
95
164
|
if (!existsSync(CURRENT_SESSION_FILE)) return null;
|
|
96
165
|
const raw = await readFile(CURRENT_SESSION_FILE, 'utf8');
|
package/src/commands/agents.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
3
4
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
6
|
import { readFileSync } from 'node:fs';
|
|
6
7
|
import { dirname, resolve } from 'node:path';
|
|
7
8
|
import {
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
DEFAULT_SYNAPSE_MODEL,
|
|
13
14
|
SESSION_ROOT_DIR,
|
|
14
15
|
} from '../agents/constants.js';
|
|
15
|
-
import { runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
16
|
+
import { listOpenCodeModels, runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
16
17
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
17
18
|
import {
|
|
18
19
|
addUserMessage,
|
|
@@ -127,6 +128,39 @@ function printModelConfig(state) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
function groupModelsByProvider(models) {
|
|
132
|
+
const grouped = new Map();
|
|
133
|
+
for (const model of models) {
|
|
134
|
+
const [provider, ...rest] = model.split('/');
|
|
135
|
+
if (!provider || rest.length === 0) continue;
|
|
136
|
+
const modelName = rest.join('/');
|
|
137
|
+
if (!grouped.has(provider)) grouped.set(provider, []);
|
|
138
|
+
grouped.get(provider).push(modelName);
|
|
139
|
+
}
|
|
140
|
+
for (const [provider, modelNames] of grouped.entries()) {
|
|
141
|
+
grouped.set(provider, [...new Set(modelNames)].sort((a, b) => a.localeCompare(b)));
|
|
142
|
+
}
|
|
143
|
+
return grouped;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runSummary(state) {
|
|
147
|
+
const run = state.run || {};
|
|
148
|
+
const events = Array.isArray(run.events) ? run.events.length : 0;
|
|
149
|
+
return {
|
|
150
|
+
status: run.status || 'idle',
|
|
151
|
+
runId: run.runId || null,
|
|
152
|
+
currentRole: run.currentRole || null,
|
|
153
|
+
lastError: run.lastError || null,
|
|
154
|
+
events,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readSessionArtifact(sessionId, filename) {
|
|
159
|
+
const path = resolve(getSessionDir(sessionId), filename);
|
|
160
|
+
if (!existsSync(path)) return null;
|
|
161
|
+
return readFile(path, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
|
|
130
164
|
async function preflightModel({ opencodeBin, model, cwd }) {
|
|
131
165
|
const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
|
|
132
166
|
try {
|
|
@@ -419,6 +453,52 @@ export function agentsCommand() {
|
|
|
419
453
|
.command('model')
|
|
420
454
|
.description('Manage default SynapseGrid model configuration');
|
|
421
455
|
|
|
456
|
+
model
|
|
457
|
+
.command('list')
|
|
458
|
+
.description('List available OpenCode models grouped by provider')
|
|
459
|
+
.option('--refresh', 'Refresh model cache from providers', false)
|
|
460
|
+
.option('--json', 'Output as JSON')
|
|
461
|
+
.action(async (opts) => {
|
|
462
|
+
try {
|
|
463
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
464
|
+
if (!opencodeBin) {
|
|
465
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const models = await listOpenCodeModels({
|
|
469
|
+
binaryPath: opencodeBin,
|
|
470
|
+
refresh: Boolean(opts.refresh),
|
|
471
|
+
});
|
|
472
|
+
const grouped = groupModelsByProvider(models);
|
|
473
|
+
const providers = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
|
|
474
|
+
|
|
475
|
+
const payload = {
|
|
476
|
+
count: models.length,
|
|
477
|
+
providers: providers.map((provider) => ({
|
|
478
|
+
provider,
|
|
479
|
+
models: grouped.get(provider).map((name) => `${provider}/${name}`),
|
|
480
|
+
})),
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
output(payload, opts.json);
|
|
484
|
+
if (!opts.json) {
|
|
485
|
+
console.log(chalk.bold('Available OpenCode models'));
|
|
486
|
+
console.log(chalk.dim(`Total: ${models.length}`));
|
|
487
|
+
for (const provider of providers) {
|
|
488
|
+
const providerModels = grouped.get(provider) || [];
|
|
489
|
+
console.log(chalk.cyan(`\n${provider}`));
|
|
490
|
+
for (const modelName of providerModels) {
|
|
491
|
+
console.log(chalk.dim(`- ${provider}/${modelName}`));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
output({ error: error.message }, opts.json);
|
|
497
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
422
502
|
model
|
|
423
503
|
.command('get')
|
|
424
504
|
.description('Show configured default global/per-role models')
|
|
@@ -447,6 +527,114 @@ export function agentsCommand() {
|
|
|
447
527
|
}
|
|
448
528
|
});
|
|
449
529
|
|
|
530
|
+
model
|
|
531
|
+
.command('choose')
|
|
532
|
+
.description('Interactively choose a default model by provider and role')
|
|
533
|
+
.option('--refresh', 'Refresh model cache from providers', false)
|
|
534
|
+
.option('--json', 'Output as JSON')
|
|
535
|
+
.action(async (opts) => {
|
|
536
|
+
try {
|
|
537
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
538
|
+
if (!opencodeBin) {
|
|
539
|
+
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const models = await listOpenCodeModels({
|
|
543
|
+
binaryPath: opencodeBin,
|
|
544
|
+
refresh: Boolean(opts.refresh),
|
|
545
|
+
});
|
|
546
|
+
if (models.length === 0) {
|
|
547
|
+
throw new Error('No models returned by OpenCode. Run: opencode auth list, opencode models --refresh');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const grouped = groupModelsByProvider(models);
|
|
551
|
+
const providerChoices = [...grouped.keys()]
|
|
552
|
+
.sort((a, b) => a.localeCompare(b))
|
|
553
|
+
.map((provider) => ({
|
|
554
|
+
name: `${provider} (${(grouped.get(provider) || []).length})`,
|
|
555
|
+
value: provider,
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
const { provider } = await inquirer.prompt([
|
|
559
|
+
{
|
|
560
|
+
type: 'list',
|
|
561
|
+
name: 'provider',
|
|
562
|
+
message: 'Select model provider',
|
|
563
|
+
choices: providerChoices,
|
|
564
|
+
},
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
const selectedProviderModels = grouped.get(provider) || [];
|
|
568
|
+
const { modelName } = await inquirer.prompt([
|
|
569
|
+
{
|
|
570
|
+
type: 'list',
|
|
571
|
+
name: 'modelName',
|
|
572
|
+
message: `Select model from ${provider}`,
|
|
573
|
+
pageSize: 20,
|
|
574
|
+
choices: selectedProviderModels.map((name) => ({ name, value: name })),
|
|
575
|
+
},
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
const roleChoices = [
|
|
579
|
+
{ name: 'Global fallback (all roles)', value: 'all' },
|
|
580
|
+
...COLLAB_ROLES.map((role) => ({ name: `Role: ${role}`, value: role })),
|
|
581
|
+
];
|
|
582
|
+
const { role } = await inquirer.prompt([
|
|
583
|
+
{
|
|
584
|
+
type: 'list',
|
|
585
|
+
name: 'role',
|
|
586
|
+
message: 'Apply model to',
|
|
587
|
+
choices: roleChoices,
|
|
588
|
+
},
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
const modelId = `${provider}/${modelName}`;
|
|
592
|
+
const updated = await updateAgentsConfig((current) => {
|
|
593
|
+
const next = {
|
|
594
|
+
agents: {
|
|
595
|
+
defaultModel: current.agents.defaultModel,
|
|
596
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
if (role === 'all') {
|
|
601
|
+
next.agents.defaultModel = modelId;
|
|
602
|
+
} else {
|
|
603
|
+
next.agents.defaultRoleModels = {
|
|
604
|
+
...next.agents.defaultRoleModels,
|
|
605
|
+
[role]: modelId,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return next;
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const payload = {
|
|
613
|
+
success: true,
|
|
614
|
+
selected: {
|
|
615
|
+
role,
|
|
616
|
+
provider,
|
|
617
|
+
model: modelId,
|
|
618
|
+
},
|
|
619
|
+
configPath: getAgentsConfigPath(),
|
|
620
|
+
defaultModel: updated.agents.defaultModel,
|
|
621
|
+
defaultRoleModels: updated.agents.defaultRoleModels,
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
output(payload, opts.json);
|
|
625
|
+
if (!opts.json) {
|
|
626
|
+
console.log(chalk.green('✓ SynapseGrid model selected and saved'));
|
|
627
|
+
console.log(chalk.dim(` Target: ${role === 'all' ? 'global fallback' : `role ${role}`}`));
|
|
628
|
+
console.log(chalk.dim(` Model: ${modelId}`));
|
|
629
|
+
console.log(chalk.dim(` Config: ${payload.configPath}`));
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
output({ error: error.message }, opts.json);
|
|
633
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
450
638
|
model
|
|
451
639
|
.command('set <modelId>')
|
|
452
640
|
.description('Set default model globally or for a specific role')
|
|
@@ -545,6 +733,83 @@ export function agentsCommand() {
|
|
|
545
733
|
}
|
|
546
734
|
});
|
|
547
735
|
|
|
736
|
+
agents
|
|
737
|
+
.command('transcript')
|
|
738
|
+
.description('Show collaborative transcript (optionally filtered by role)')
|
|
739
|
+
.option('--session <id>', 'Session ID (defaults to current)')
|
|
740
|
+
.option('--role <role>', 'Role filter (planner|critic|coder|reviewer|all)', 'all')
|
|
741
|
+
.option('--limit <n>', 'Max messages to display', '40')
|
|
742
|
+
.option('--json', 'Output as JSON')
|
|
743
|
+
.action(async (opts) => {
|
|
744
|
+
try {
|
|
745
|
+
const sessionId = opts.session || await ensureSessionId(true);
|
|
746
|
+
const role = String(opts.role || 'all');
|
|
747
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
748
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
749
|
+
throw new Error('--limit must be a positive integer');
|
|
750
|
+
}
|
|
751
|
+
if (role !== 'all' && !COLLAB_ROLES.includes(role)) {
|
|
752
|
+
throw new Error('--role must be planner|critic|coder|reviewer|all');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const transcript = await loadTranscript(sessionId);
|
|
756
|
+
const filtered = transcript
|
|
757
|
+
.filter((msg) => role === 'all' || msg.from === role)
|
|
758
|
+
.slice(-limit);
|
|
759
|
+
|
|
760
|
+
output({ sessionId, count: filtered.length, transcript: filtered }, opts.json);
|
|
761
|
+
if (!opts.json) {
|
|
762
|
+
console.log(chalk.bold(`SynapseGrid transcript (${filtered.length})`));
|
|
763
|
+
for (const msg of filtered) {
|
|
764
|
+
console.log(chalk.cyan(`\n[${msg.from}] ${msg.timestamp || ''}`));
|
|
765
|
+
console.log(String(msg.content || '').trim() || chalk.dim('(empty)'));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
} catch (error) {
|
|
769
|
+
output({ error: error.message }, opts.json);
|
|
770
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
agents
|
|
776
|
+
.command('summary')
|
|
777
|
+
.description('Show meeting summary generated from collaborative run')
|
|
778
|
+
.option('--session <id>', 'Session ID (defaults to current)')
|
|
779
|
+
.option('--json', 'Output as JSON')
|
|
780
|
+
.action(async (opts) => {
|
|
781
|
+
try {
|
|
782
|
+
const sessionId = opts.session || await ensureSessionId(true);
|
|
783
|
+
const state = await loadSessionState(sessionId);
|
|
784
|
+
const summaryFile = await readSessionArtifact(sessionId, 'meeting-summary.md');
|
|
785
|
+
const meetingLogFile = await readSessionArtifact(sessionId, 'meeting-log.md');
|
|
786
|
+
const payload = {
|
|
787
|
+
sessionId,
|
|
788
|
+
status: state.status,
|
|
789
|
+
finalSummary: state.run?.finalSummary || null,
|
|
790
|
+
sharedContext: state.run?.sharedContext || null,
|
|
791
|
+
summaryFile,
|
|
792
|
+
meetingLogFile,
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
output(payload, opts.json);
|
|
796
|
+
if (!opts.json) {
|
|
797
|
+
console.log(chalk.bold('SynapseGrid meeting summary'));
|
|
798
|
+
if (summaryFile) {
|
|
799
|
+
process.stdout.write(summaryFile.endsWith('\n') ? summaryFile : `${summaryFile}\n`);
|
|
800
|
+
} else if (payload.finalSummary) {
|
|
801
|
+
process.stdout.write(`${payload.finalSummary}\n`);
|
|
802
|
+
} else {
|
|
803
|
+
console.log(chalk.dim('No summary generated yet.'));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch (error) {
|
|
807
|
+
output({ error: error.message }, opts.json);
|
|
808
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
548
813
|
agents
|
|
549
814
|
.command('export')
|
|
550
815
|
.description('Export collaborative transcript')
|
|
@@ -562,9 +827,11 @@ export function agentsCommand() {
|
|
|
562
827
|
throw new Error('--format must be md or json');
|
|
563
828
|
}
|
|
564
829
|
|
|
830
|
+
const meetingSummary = await readSessionArtifact(sessionId, 'meeting-summary.md');
|
|
831
|
+
const meetingLog = await readSessionArtifact(sessionId, 'meeting-log.md');
|
|
565
832
|
const payload = format === 'json'
|
|
566
|
-
? JSON.stringify({ state, transcript }, null, 2) + '\n'
|
|
567
|
-
: toMarkdownTranscript(state, transcript)
|
|
833
|
+
? JSON.stringify({ state, transcript, meetingSummary, meetingLog }, null, 2) + '\n'
|
|
834
|
+
: `${toMarkdownTranscript(state, transcript)}\n\n## Meeting Summary\n\n${meetingSummary || state.run?.finalSummary || 'No summary generated yet.'}\n\n## Meeting Log\n\n${meetingLog || 'No meeting log generated yet.'}\n`;
|
|
568
835
|
|
|
569
836
|
if (opts.out) {
|
|
570
837
|
const outputPath = resolve(opts.out);
|
|
@@ -723,6 +990,11 @@ export function agentsCommand() {
|
|
|
723
990
|
console.log(chalk.dim(`Round: ${Math.min(state.round, state.maxRounds)}/${state.maxRounds}`));
|
|
724
991
|
console.log(chalk.dim(`Active agent: ${state.activeAgent || 'none'}`));
|
|
725
992
|
console.log(chalk.dim(`Messages: ${state.messages.length}`));
|
|
993
|
+
const summary = runSummary(state);
|
|
994
|
+
console.log(chalk.dim(`Run: ${summary.status}${summary.currentRole ? ` (role=${summary.currentRole})` : ''}, events=${summary.events}`));
|
|
995
|
+
if (summary.lastError?.message) {
|
|
996
|
+
console.log(chalk.dim(`Run error: ${summary.lastError.message}`));
|
|
997
|
+
}
|
|
726
998
|
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
727
999
|
for (const role of COLLAB_ROLES) {
|
|
728
1000
|
const configured = state.roleModels?.[role] || '-';
|
|
@@ -749,6 +1021,21 @@ export function agentsCommand() {
|
|
|
749
1021
|
const sessionId = await ensureSessionId(true);
|
|
750
1022
|
let state = await loadSessionState(sessionId);
|
|
751
1023
|
state = await stopSession(state, 'stopped');
|
|
1024
|
+
if (state.run && state.run.status === 'running') {
|
|
1025
|
+
state = await saveSessionState({
|
|
1026
|
+
...state,
|
|
1027
|
+
run: {
|
|
1028
|
+
...state.run,
|
|
1029
|
+
status: 'cancelled',
|
|
1030
|
+
finishedAt: new Date().toISOString(),
|
|
1031
|
+
currentRole: null,
|
|
1032
|
+
lastError: {
|
|
1033
|
+
code: 'RUN_CANCELLED',
|
|
1034
|
+
message: 'Run cancelled by user',
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
752
1039
|
if (state.tmuxSessionName && hasCommand('tmux')) {
|
|
753
1040
|
try {
|
|
754
1041
|
await runTmux('tmux', ['kill-session', '-t', state.tmuxSessionName]);
|
|
@@ -765,6 +1052,33 @@ export function agentsCommand() {
|
|
|
765
1052
|
}
|
|
766
1053
|
});
|
|
767
1054
|
|
|
1055
|
+
agents
|
|
1056
|
+
.command('autopilot')
|
|
1057
|
+
.description('Internal headless collaborative driver (non-tmux)')
|
|
1058
|
+
.requiredOption('--session <id>', 'Session id')
|
|
1059
|
+
.option('--poll-ms <n>', 'Polling interval in ms', '900')
|
|
1060
|
+
.action(async (opts) => {
|
|
1061
|
+
const pollMs = Number.parseInt(opts.pollMs, 10);
|
|
1062
|
+
while (true) {
|
|
1063
|
+
try {
|
|
1064
|
+
const state = await loadSessionState(opts.session);
|
|
1065
|
+
if (state.status !== 'running') process.exit(0);
|
|
1066
|
+
|
|
1067
|
+
for (const role of state.roles || COLLAB_ROLES) {
|
|
1068
|
+
await runWorkerIteration(opts.session, role, {
|
|
1069
|
+
cwd: state.workingDirectory || process.cwd(),
|
|
1070
|
+
model: state.model || null,
|
|
1071
|
+
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
1072
|
+
timeoutMs: state.run?.policy?.timeoutPerRoleMs || 180000,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
console.error(`[autopilot] ${error.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, Number.isInteger(pollMs) ? pollMs : 900));
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
768
1082
|
agents
|
|
769
1083
|
.command('worker')
|
|
770
1084
|
.description('Internal worker loop for a single collaborative role')
|