ac-framework 1.9.6 → 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 +27 -1
- package/package.json +1 -1
- package/src/agents/collab-summary.js +120 -0
- package/src/agents/config-store.js +2 -1
- package/src/agents/constants.js +3 -0
- package/src/agents/opencode-client.js +166 -12
- 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/runtime.js +4 -2
- package/src/agents/state-store.js +69 -1
- package/src/commands/agents.js +408 -5
- package/src/mcp/collab-server.js +307 -2
- package/src/mcp/test-harness.mjs +410 -0
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 {
|
|
@@ -9,8 +10,10 @@ import {
|
|
|
9
10
|
COLLAB_SYSTEM_NAME,
|
|
10
11
|
CURRENT_SESSION_FILE,
|
|
11
12
|
DEFAULT_MAX_ROUNDS,
|
|
13
|
+
DEFAULT_SYNAPSE_MODEL,
|
|
12
14
|
SESSION_ROOT_DIR,
|
|
13
15
|
} from '../agents/constants.js';
|
|
16
|
+
import { listOpenCodeModels, runOpenCodePrompt } from '../agents/opencode-client.js';
|
|
14
17
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
15
18
|
import {
|
|
16
19
|
addUserMessage,
|
|
@@ -125,6 +128,55 @@ function printModelConfig(state) {
|
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
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
|
+
|
|
164
|
+
async function preflightModel({ opencodeBin, model, cwd }) {
|
|
165
|
+
const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
|
|
166
|
+
try {
|
|
167
|
+
await runOpenCodePrompt({
|
|
168
|
+
prompt: 'Reply with exactly: OK',
|
|
169
|
+
cwd,
|
|
170
|
+
model: selected,
|
|
171
|
+
binaryPath: opencodeBin,
|
|
172
|
+
timeoutMs: 45000,
|
|
173
|
+
});
|
|
174
|
+
return { ok: true, model: selected };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return { ok: false, model: selected, error: error.message };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
128
180
|
export function agentsCommand() {
|
|
129
181
|
const agents = new Command('agents')
|
|
130
182
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
@@ -276,6 +328,10 @@ export function agentsCommand() {
|
|
|
276
328
|
if (!state.tmuxSessionName) {
|
|
277
329
|
throw new Error('No tmux session registered for active collaborative session');
|
|
278
330
|
}
|
|
331
|
+
const tmuxExists = await tmuxSessionExists(state.tmuxSessionName);
|
|
332
|
+
if (!tmuxExists) {
|
|
333
|
+
throw new Error(`tmux session ${state.tmuxSessionName} no longer exists. Run: acfm agents resume`);
|
|
334
|
+
}
|
|
279
335
|
const args = ['attach'];
|
|
280
336
|
if (opts.readonly) args.push('-r');
|
|
281
337
|
args.push('-t', state.tmuxSessionName);
|
|
@@ -397,6 +453,52 @@ export function agentsCommand() {
|
|
|
397
453
|
.command('model')
|
|
398
454
|
.description('Manage default SynapseGrid model configuration');
|
|
399
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
|
+
|
|
400
502
|
model
|
|
401
503
|
.command('get')
|
|
402
504
|
.description('Show configured default global/per-role models')
|
|
@@ -425,6 +527,114 @@ export function agentsCommand() {
|
|
|
425
527
|
}
|
|
426
528
|
});
|
|
427
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
|
+
|
|
428
638
|
model
|
|
429
639
|
.command('set <modelId>')
|
|
430
640
|
.description('Set default model globally or for a specific role')
|
|
@@ -495,7 +705,7 @@ export function agentsCommand() {
|
|
|
495
705
|
},
|
|
496
706
|
};
|
|
497
707
|
if (role === 'all') {
|
|
498
|
-
next.agents.defaultModel =
|
|
708
|
+
next.agents.defaultModel = DEFAULT_SYNAPSE_MODEL;
|
|
499
709
|
next.agents.defaultRoleModels = {};
|
|
500
710
|
} else {
|
|
501
711
|
const currentRoles = { ...next.agents.defaultRoleModels };
|
|
@@ -523,6 +733,83 @@ export function agentsCommand() {
|
|
|
523
733
|
}
|
|
524
734
|
});
|
|
525
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
|
+
|
|
526
813
|
agents
|
|
527
814
|
.command('export')
|
|
528
815
|
.description('Export collaborative transcript')
|
|
@@ -540,9 +827,11 @@ export function agentsCommand() {
|
|
|
540
827
|
throw new Error('--format must be md or json');
|
|
541
828
|
}
|
|
542
829
|
|
|
830
|
+
const meetingSummary = await readSessionArtifact(sessionId, 'meeting-summary.md');
|
|
831
|
+
const meetingLog = await readSessionArtifact(sessionId, 'meeting-log.md');
|
|
543
832
|
const payload = format === 'json'
|
|
544
|
-
? JSON.stringify({ state, transcript }, null, 2) + '\n'
|
|
545
|
-
: 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`;
|
|
546
835
|
|
|
547
836
|
if (opts.out) {
|
|
548
837
|
const outputPath = resolve(opts.out);
|
|
@@ -611,7 +900,22 @@ export function agentsCommand() {
|
|
|
611
900
|
...defaultRoleModels,
|
|
612
901
|
...cliRoleModels,
|
|
613
902
|
};
|
|
614
|
-
const globalModel = cliModel || config.agents.defaultModel ||
|
|
903
|
+
const globalModel = cliModel || config.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
904
|
+
|
|
905
|
+
const modelsToCheck = new Set([globalModel, ...Object.values(roleModels)]);
|
|
906
|
+
for (const modelToCheck of modelsToCheck) {
|
|
907
|
+
const preflight = await preflightModel({
|
|
908
|
+
opencodeBin,
|
|
909
|
+
model: modelToCheck,
|
|
910
|
+
cwd: resolve(opts.cwd),
|
|
911
|
+
});
|
|
912
|
+
if (!preflight.ok) {
|
|
913
|
+
throw new Error(
|
|
914
|
+
`Model preflight failed for ${preflight.model}: ${preflight.error}. ` +
|
|
915
|
+
'Check OpenCode auth/providers with: opencode auth list, opencode models'
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
615
919
|
|
|
616
920
|
const state = await createSession(opts.task, {
|
|
617
921
|
roles: COLLAB_ROLES,
|
|
@@ -686,6 +990,11 @@ export function agentsCommand() {
|
|
|
686
990
|
console.log(chalk.dim(`Round: ${Math.min(state.round, state.maxRounds)}/${state.maxRounds}`));
|
|
687
991
|
console.log(chalk.dim(`Active agent: ${state.activeAgent || 'none'}`));
|
|
688
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
|
+
}
|
|
689
998
|
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
690
999
|
for (const role of COLLAB_ROLES) {
|
|
691
1000
|
const configured = state.roleModels?.[role] || '-';
|
|
@@ -712,6 +1021,21 @@ export function agentsCommand() {
|
|
|
712
1021
|
const sessionId = await ensureSessionId(true);
|
|
713
1022
|
let state = await loadSessionState(sessionId);
|
|
714
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
|
+
}
|
|
715
1039
|
if (state.tmuxSessionName && hasCommand('tmux')) {
|
|
716
1040
|
try {
|
|
717
1041
|
await runTmux('tmux', ['kill-session', '-t', state.tmuxSessionName]);
|
|
@@ -728,6 +1052,33 @@ export function agentsCommand() {
|
|
|
728
1052
|
}
|
|
729
1053
|
});
|
|
730
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
|
+
|
|
731
1082
|
agents
|
|
732
1083
|
.command('worker')
|
|
733
1084
|
.description('Internal worker loop for a single collaborative role')
|
|
@@ -749,6 +1100,7 @@ export function agentsCommand() {
|
|
|
749
1100
|
|
|
750
1101
|
while (true) {
|
|
751
1102
|
try {
|
|
1103
|
+
console.log(`[${role}] polling session ${opts.session}`);
|
|
752
1104
|
const state = await loadSessionState(opts.session);
|
|
753
1105
|
if (!state.roles.includes(role)) {
|
|
754
1106
|
console.log(`[${role}] role not configured in session. exiting.`);
|
|
@@ -759,14 +1111,20 @@ export function agentsCommand() {
|
|
|
759
1111
|
process.exit(0);
|
|
760
1112
|
}
|
|
761
1113
|
|
|
1114
|
+
if (state.activeAgent === role) {
|
|
1115
|
+
console.log(`[${role}] executing turn with model=${state.roleModels?.[role] || state.model || DEFAULT_SYNAPSE_MODEL}`);
|
|
1116
|
+
}
|
|
762
1117
|
const nextState = await runWorkerIteration(opts.session, role, {
|
|
763
1118
|
cwd: state.workingDirectory || process.cwd(),
|
|
764
1119
|
model: state.model || null,
|
|
765
1120
|
opencodeBin: state.opencodeBin || resolveCommandPath('opencode') || undefined,
|
|
1121
|
+
timeoutMs: 180000,
|
|
766
1122
|
});
|
|
767
1123
|
const latest = nextState.messages[nextState.messages.length - 1];
|
|
768
1124
|
if (latest?.from === role) {
|
|
769
1125
|
console.log(`[${role}] message emitted (${latest.content.length} chars)`);
|
|
1126
|
+
} else if (nextState.activeAgent && nextState.activeAgent !== role) {
|
|
1127
|
+
console.log(`[${role}] waiting for active role=${nextState.activeAgent}`);
|
|
770
1128
|
}
|
|
771
1129
|
} catch (error) {
|
|
772
1130
|
console.error(`[${role}] loop error: ${error.message}`);
|
|
@@ -776,5 +1134,50 @@ export function agentsCommand() {
|
|
|
776
1134
|
}
|
|
777
1135
|
});
|
|
778
1136
|
|
|
1137
|
+
agents
|
|
1138
|
+
.command('doctor')
|
|
1139
|
+
.description('Run diagnostics for SynapseGrid/OpenCode runtime')
|
|
1140
|
+
.option('--json', 'Output as JSON')
|
|
1141
|
+
.action(async (opts) => {
|
|
1142
|
+
try {
|
|
1143
|
+
const opencodeBin = resolveCommandPath('opencode');
|
|
1144
|
+
const tmuxInstalled = hasCommand('tmux');
|
|
1145
|
+
const cfg = await loadAgentsConfig();
|
|
1146
|
+
const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
1147
|
+
const result = {
|
|
1148
|
+
opencodeBin,
|
|
1149
|
+
tmuxInstalled,
|
|
1150
|
+
defaultModel,
|
|
1151
|
+
defaultRoleModels: cfg.agents.defaultRoleModels,
|
|
1152
|
+
preflight: null,
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
if (opencodeBin) {
|
|
1156
|
+
result.preflight = await preflightModel({
|
|
1157
|
+
opencodeBin,
|
|
1158
|
+
model: defaultModel,
|
|
1159
|
+
cwd: process.cwd(),
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
result.preflight = { ok: false, error: 'OpenCode binary not found' };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
output(result, opts.json);
|
|
1166
|
+
if (!opts.json) {
|
|
1167
|
+
console.log(chalk.bold('SynapseGrid doctor'));
|
|
1168
|
+
console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
|
|
1169
|
+
console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
|
|
1170
|
+
console.log(chalk.dim(`default model: ${defaultModel}`));
|
|
1171
|
+
console.log(chalk.dim(`preflight: ${result.preflight?.ok ? 'ok' : `failed - ${result.preflight?.error || 'unknown error'}`}`));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!result.preflight?.ok) process.exit(1);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
output({ error: error.message }, opts.json);
|
|
1177
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
779
1182
|
return agents;
|
|
780
1183
|
}
|