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 CHANGED
@@ -150,24 +150,47 @@ Each role runs in turn against a shared, accumulating context so outputs from on
150
150
  | `acfm agents attach` | Attach directly to the SynapseGrid tmux session |
151
151
  | `acfm agents live` | Attach to full live tmux view (all agents) |
152
152
  | `acfm agents logs` | Show recent worker logs (all roles or one role) |
153
+ | `acfm agents transcript --role all --limit 40` | Show captured cross-agent transcript |
154
+ | `acfm agents summary` | Show generated collaboration meeting summary |
153
155
  | `acfm agents export --format md --out file.md` | Export transcript in Markdown or JSON |
154
156
  | `acfm agents send "..."` | Send a new user message into the active session |
155
157
  | `acfm agents status` | Show current collaborative session state |
158
+ | `acfm agents model list` | List available models grouped by provider |
159
+ | `acfm agents model choose` | Interactively pick provider/model and save target role |
156
160
  | `acfm agents model get` | Show default model config (global and per-role) |
157
161
  | `acfm agents model set --role coder provider/model` | Persist a default model for one role |
158
162
  | `acfm agents model clear --role all` | Clear persisted model defaults |
159
163
  | `acfm agents stop` | Stop the active collaborative session |
160
164
 
165
+ ### MCP collaborative run mode (recommended)
166
+
167
+ When driving SynapseGrid from another agent via MCP, prefer asynchronous run tools over role-by-role stepping:
168
+
169
+ - `collab_start_session` to initialize session and optional tmux workers
170
+ - `collab_invoke_team` to launch full 4-role collaboration run
171
+ - `collab_wait_run` to wait for completion/failure with bounded timeout
172
+ - `collab_get_result` to fetch final consolidated output and run diagnostics
173
+ - `collab_cancel_run` to cancel a running collaboration safely
174
+
175
+ `collab_step` remains available for manual/debug control, but is less robust for long tasks.
176
+
161
177
  ### SynapseGrid troubleshooting
162
178
 
163
179
  - If transcript entries show `Agent failed: spawn opencode ENOENT`, run `acfm agents setup` to install dependencies and then retry.
164
180
  - Attach to worker panes with `acfm agents live` (or `acfm agents attach`) to see real-time role discussion.
165
181
  - Inspect worker errors quickly with `acfm agents logs --role all --lines 120`.
182
+ - Inspect collaborative discussion with `acfm agents transcript` and `acfm agents summary`.
166
183
  - MCP starts can now create tmux workers directly; if your assistant used headless steps before, start a new session and ensure worker spawning is enabled.
167
- - Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model set`.
168
- - Default SynapseGrid model fallback is `opencode/minimax-m2.5-free`.
184
+ - Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model choose` / `acfm agents model set`.
185
+ - Default SynapseGrid model fallback is `opencode/mimo-v2-pro-free`.
169
186
  - Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
170
187
 
188
+ Each collaborative session now keeps human-readable artifacts under `~/.acfm/synapsegrid/<sessionId>/`:
189
+ - `transcript.jsonl`: full chronological message stream
190
+ - `turns/*.json`: one file per round/role turn with captured output metadata
191
+ - `meeting-log.md`: incremental meeting notes generated per turn
192
+ - `meeting-summary.md`: final consolidated summary (roles, decisions, open issues, risks, action items)
193
+
171
194
  ### Spec Workflow
172
195
 
173
196
  | Command | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-framework",
3
- "version": "1.9.7",
3
+ "version": "1.9.8",
4
4
  "description": "Agentic Coding Framework - Multi-assistant configuration system with OpenSpec workflows",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -0,0 +1,120 @@
1
+ export function summarizeText(text, maxLen = 700) {
2
+ if (typeof text !== 'string') return '';
3
+ const normalized = text.replace(/\r\n/g, '\n').trim();
4
+ if (!normalized) return '';
5
+ if (normalized.length <= maxLen) return normalized;
6
+ return `${normalized.slice(0, Math.max(0, maxLen - 3)).trimEnd()}...`;
7
+ }
8
+
9
+ export function extractBulletLines(text, limit = 5) {
10
+ if (typeof text !== 'string') return [];
11
+ const bullets = text
12
+ .split('\n')
13
+ .map((line) => line.trim())
14
+ .filter((line) => /^[-*]\s+/.test(line) || /^\d+\)\s+/.test(line));
15
+ return bullets.slice(0, Math.max(0, limit));
16
+ }
17
+
18
+ export function createTurnRecord({ round, role, model, content, events }) {
19
+ const snippet = summarizeText(content, 1000);
20
+ const keyPoints = extractBulletLines(content, 6);
21
+ return {
22
+ round,
23
+ role,
24
+ model: model || null,
25
+ timestamp: new Date().toISOString(),
26
+ snippet,
27
+ keyPoints,
28
+ eventCount: Array.isArray(events) ? events.length : 0,
29
+ };
30
+ }
31
+
32
+ export function updateSharedContext(prev, turn) {
33
+ const base = prev && typeof prev === 'object'
34
+ ? prev
35
+ : { decisions: [], openIssues: [], risks: [], actionItems: [], notes: [] };
36
+
37
+ const next = {
38
+ decisions: [...(base.decisions || [])],
39
+ openIssues: [...(base.openIssues || [])],
40
+ risks: [...(base.risks || [])],
41
+ actionItems: [...(base.actionItems || [])],
42
+ notes: [...(base.notes || [])],
43
+ };
44
+
45
+ const summary = summarizeText(turn?.snippet || turn?.content || '', 300);
46
+ if (summary) {
47
+ next.notes.push(`[r${turn.round}] ${turn.role}: ${summary}`);
48
+ }
49
+
50
+ const lines = String(turn?.snippet || turn?.content || '')
51
+ .split('\n')
52
+ .map((line) => line.trim())
53
+ .filter(Boolean);
54
+
55
+ for (const line of lines) {
56
+ const lower = line.toLowerCase();
57
+ if (lower.includes('risk') || lower.includes('failure') || lower.includes('blind spot')) {
58
+ next.risks.push(`[r${turn.round}] ${turn.role}: ${line}`);
59
+ }
60
+ if (lower.includes('open issue') || lower.includes('blocker') || lower.includes('unresolved')) {
61
+ next.openIssues.push(`[r${turn.round}] ${turn.role}: ${line}`);
62
+ }
63
+ if (lower.includes('action') || lower.includes('next step') || lower.includes('implement')) {
64
+ next.actionItems.push(`[r${turn.round}] ${turn.role}: ${line}`);
65
+ }
66
+ if (lower.includes('decision') || lower.includes('agreed') || lower.includes('approve')) {
67
+ next.decisions.push(`[r${turn.round}] ${turn.role}: ${line}`);
68
+ }
69
+ }
70
+
71
+ const dedupe = (arr) => [...new Set(arr)].slice(-30);
72
+ return {
73
+ decisions: dedupe(next.decisions),
74
+ openIssues: dedupe(next.openIssues),
75
+ risks: dedupe(next.risks),
76
+ actionItems: dedupe(next.actionItems),
77
+ notes: dedupe(next.notes),
78
+ };
79
+ }
80
+
81
+ export function buildMeetingSummary(messages = [], run = null, sharedContext = null) {
82
+ const byRole = new Map();
83
+ for (const msg of messages) {
84
+ if (!msg?.from || msg.from === 'user') continue;
85
+ if (!byRole.has(msg.from)) byRole.set(msg.from, []);
86
+ byRole.get(msg.from).push(msg.content || '');
87
+ }
88
+
89
+ const roles = ['planner', 'critic', 'coder', 'reviewer'];
90
+ const lines = [];
91
+ lines.push('# SynapseGrid Meeting Summary');
92
+ if (run?.runId) lines.push(`Run: ${run.runId}`);
93
+ if (run?.status) lines.push(`Status: ${run.status}`);
94
+ lines.push('');
95
+ lines.push('## Per-role recap');
96
+
97
+ for (const role of roles) {
98
+ const items = byRole.get(role) || [];
99
+ const last = items.length > 0 ? items[items.length - 1] : '';
100
+ lines.push(`- ${role}: ${summarizeText(last, 500) || '(no contribution captured)'}`);
101
+ }
102
+
103
+ const ctx = sharedContext && typeof sharedContext === 'object' ? sharedContext : null;
104
+ if (ctx) {
105
+ lines.push('');
106
+ lines.push('## Decisions');
107
+ for (const entry of (ctx.decisions || []).slice(-8)) lines.push(`- ${entry}`);
108
+ lines.push('');
109
+ lines.push('## Open issues');
110
+ for (const entry of (ctx.openIssues || []).slice(-8)) lines.push(`- ${entry}`);
111
+ lines.push('');
112
+ lines.push('## Risks');
113
+ for (const entry of (ctx.risks || []).slice(-8)) lines.push(`- ${entry}`);
114
+ lines.push('');
115
+ lines.push('## Action items');
116
+ for (const entry of (ctx.actionItems || []).slice(-8)) lines.push(`- ${entry}`);
117
+ }
118
+
119
+ return lines.join('\n').trim() + '\n';
120
+ }
@@ -4,6 +4,8 @@ import { join } from 'node:path';
4
4
  export const COLLAB_SYSTEM_NAME = 'SynapseGrid';
5
5
  export const COLLAB_ROLES = ['planner', 'critic', 'coder', 'reviewer'];
6
6
  export const DEFAULT_MAX_ROUNDS = 3;
7
- export const DEFAULT_SYNAPSE_MODEL = 'opencode/minimax-m2.5-free';
7
+ export const DEFAULT_SYNAPSE_MODEL = 'opencode/mimo-v2-pro-free';
8
+ export const DEFAULT_ROLE_TIMEOUT_MS = 180000;
9
+ export const DEFAULT_ROLE_RETRIES = 1;
8
10
  export const SESSION_ROOT_DIR = join(homedir(), '.acfm', 'synapsegrid');
9
11
  export const CURRENT_SESSION_FILE = join(SESSION_ROOT_DIR, 'current-session.json');
@@ -42,7 +42,24 @@ function parseOpenCodeRunOutput(stdout) {
42
42
  return stdout.trim();
43
43
  }
44
44
 
45
- export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs = 180000, binaryPath }) {
45
+ function parseOpenCodeEvents(stdout) {
46
+ const lines = stdout
47
+ .split('\n')
48
+ .map((line) => line.trim())
49
+ .filter(Boolean);
50
+
51
+ const events = [];
52
+ for (const line of lines) {
53
+ try {
54
+ events.push(JSON.parse(line));
55
+ } catch {
56
+ // ignore malformed event lines
57
+ }
58
+ }
59
+ return events;
60
+ }
61
+
62
+ export async function runOpenCodePromptDetailed({ prompt, cwd, model, agent, timeoutMs = 180000, binaryPath }) {
46
63
  const binary = binaryPath || process.env.ACFM_OPENCODE_BIN || 'opencode';
47
64
  const args = ['run', '--format', 'json'];
48
65
  if (model) {
@@ -97,9 +114,88 @@ export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs =
97
114
  });
98
115
  });
99
116
 
100
- const parsed = parseOpenCodeRunOutput(stdout);
101
- if (!parsed && stderr?.trim()) {
102
- return stderr.trim();
117
+ const text = parseOpenCodeRunOutput(stdout);
118
+ const events = parseOpenCodeEvents(stdout);
119
+
120
+ return {
121
+ text: text || (stderr?.trim() || ''),
122
+ stdout,
123
+ stderr,
124
+ events,
125
+ };
126
+ }
127
+
128
+ export async function runOpenCodePrompt({ prompt, cwd, model, agent, timeoutMs = 180000, binaryPath }) {
129
+ const detailed = await runOpenCodePromptDetailed({
130
+ prompt,
131
+ cwd,
132
+ model,
133
+ agent,
134
+ timeoutMs,
135
+ binaryPath,
136
+ });
137
+ return detailed.text;
138
+ }
139
+
140
+ export async function listOpenCodeModels({ provider = null, binaryPath, refresh = false, timeoutMs = 45000 }) {
141
+ const binary = binaryPath || process.env.ACFM_OPENCODE_BIN || 'opencode';
142
+ const args = ['models'];
143
+ if (provider) {
144
+ args.push(provider);
145
+ }
146
+ if (refresh) {
147
+ args.push('--refresh');
103
148
  }
104
- return parsed;
149
+
150
+ const stdout = await new Promise((resolvePromise, rejectPromise) => {
151
+ const child = spawn(binary, args, {
152
+ cwd: process.cwd(),
153
+ env: process.env,
154
+ stdio: ['ignore', 'pipe', 'pipe'],
155
+ });
156
+
157
+ let out = '';
158
+ let err = '';
159
+ let timedOut = false;
160
+
161
+ const timer = setTimeout(() => {
162
+ timedOut = true;
163
+ child.kill('SIGTERM');
164
+ setTimeout(() => child.kill('SIGKILL'), 1500).unref();
165
+ }, timeoutMs);
166
+
167
+ child.stdout.on('data', (chunk) => {
168
+ out += chunk.toString();
169
+ });
170
+ child.stderr.on('data', (chunk) => {
171
+ err += chunk.toString();
172
+ });
173
+
174
+ child.on('error', (error) => {
175
+ clearTimeout(timer);
176
+ rejectPromise(error);
177
+ });
178
+
179
+ child.on('close', (code, signal) => {
180
+ clearTimeout(timer);
181
+ if (timedOut) {
182
+ rejectPromise(new Error(`opencode models timed out after ${timeoutMs}ms`));
183
+ return;
184
+ }
185
+ if (code !== 0) {
186
+ const details = [err.trim(), out.trim()].filter(Boolean).join(' | ');
187
+ rejectPromise(new Error(details || `opencode models exited with code ${code}${signal ? ` (${signal})` : ''}`));
188
+ return;
189
+ }
190
+ resolvePromise(out);
191
+ });
192
+ });
193
+
194
+ const models = stdout
195
+ .split('\n')
196
+ .map((line) => line.trim())
197
+ .filter(Boolean)
198
+ .filter((line) => line.includes('/'));
199
+
200
+ return [...new Set(models)].sort((a, b) => a.localeCompare(b));
105
201
  }
@@ -1,12 +1,21 @@
1
1
  import { buildAgentPrompt, ROLE_SYSTEM_PROMPTS } from './role-prompts.js';
2
- import { runOpenCodePrompt } from './opencode-client.js';
2
+ import { runOpenCodePromptDetailed } from './opencode-client.js';
3
3
  import { nextRole, shouldStop } from './scheduler.js';
4
4
  import { resolveRoleModel } from './model-selection.js';
5
+ import { buildMeetingSummary, createTurnRecord, updateSharedContext } from './collab-summary.js';
6
+ import {
7
+ appendRunEvent,
8
+ extractFinalSummary,
9
+ incrementRoleRetry,
10
+ roleRetryCount,
11
+ } from './run-state.js';
5
12
  import {
6
13
  addAgentMessage,
14
+ appendMeetingTurn,
7
15
  loadSessionState,
8
16
  saveSessionState,
9
17
  stopSession,
18
+ writeMeetingSummary,
10
19
  withSessionLock,
11
20
  } from './state-store.js';
12
21
 
@@ -17,11 +26,115 @@ function buildRuntimePrompt({ state, role }) {
17
26
  task: state.task,
18
27
  round: state.round,
19
28
  messages: state.messages,
29
+ sharedContext: state.run?.sharedContext || null,
20
30
  });
21
31
 
22
32
  return [roleContext, '', collaborativePrompt].join('\n');
23
33
  }
24
34
 
35
+ function ensureRunState(state) {
36
+ if (state.run && typeof state.run === 'object') {
37
+ return {
38
+ ...state.run,
39
+ sharedContext: state.run.sharedContext && typeof state.run.sharedContext === 'object'
40
+ ? {
41
+ decisions: Array.isArray(state.run.sharedContext.decisions) ? state.run.sharedContext.decisions : [],
42
+ openIssues: Array.isArray(state.run.sharedContext.openIssues) ? state.run.sharedContext.openIssues : [],
43
+ risks: Array.isArray(state.run.sharedContext.risks) ? state.run.sharedContext.risks : [],
44
+ actionItems: Array.isArray(state.run.sharedContext.actionItems) ? state.run.sharedContext.actionItems : [],
45
+ notes: Array.isArray(state.run.sharedContext.notes) ? state.run.sharedContext.notes : [],
46
+ }
47
+ : {
48
+ decisions: [],
49
+ openIssues: [],
50
+ risks: [],
51
+ actionItems: [],
52
+ notes: [],
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ runId: null,
58
+ status: 'idle',
59
+ startedAt: null,
60
+ finishedAt: null,
61
+ currentRole: null,
62
+ retriesUsed: {},
63
+ round: state.round || 1,
64
+ events: [],
65
+ finalSummary: null,
66
+ lastError: null,
67
+ policy: {
68
+ timeoutPerRoleMs: 180000,
69
+ retryOnTimeout: 1,
70
+ fallbackOnFailure: 'abort',
71
+ maxRounds: state.maxRounds,
72
+ },
73
+ };
74
+ }
75
+
76
+ function applyRoleFailurePolicy(state, role, errorMessage) {
77
+ let run = ensureRunState(state);
78
+ const policy = run.policy || {};
79
+ const currentRetries = roleRetryCount(run, role);
80
+ const canRetry = currentRetries < (policy.retryOnTimeout ?? 0);
81
+
82
+ run = appendRunEvent(run, 'role_failed', {
83
+ role,
84
+ retry: canRetry,
85
+ error: errorMessage,
86
+ });
87
+
88
+ if (canRetry) {
89
+ run = incrementRoleRetry(run, role);
90
+ return {
91
+ ...state,
92
+ run: {
93
+ ...run,
94
+ currentRole: role,
95
+ status: 'running',
96
+ lastError: null,
97
+ },
98
+ activeAgent: null,
99
+ // retry same role by rewinding index
100
+ nextRoleIndex: state.roles.indexOf(role),
101
+ };
102
+ }
103
+
104
+ const fallback = policy.fallbackOnFailure || 'abort';
105
+ if (fallback === 'skip') {
106
+ const skipped = appendRunEvent(run, 'role_skipped', { role, error: errorMessage });
107
+ return {
108
+ ...state,
109
+ run: {
110
+ ...skipped,
111
+ currentRole: null,
112
+ status: 'running',
113
+ lastError: null,
114
+ },
115
+ activeAgent: null,
116
+ };
117
+ }
118
+
119
+ const failed = appendRunEvent(run, 'run_failed', { role, error: errorMessage });
120
+ return {
121
+ ...state,
122
+ status: 'failed',
123
+ activeAgent: null,
124
+ run: {
125
+ ...failed,
126
+ status: 'failed',
127
+ currentRole: null,
128
+ finishedAt: new Date().toISOString(),
129
+ lastError: {
130
+ code: 'ROLE_FAILURE',
131
+ message: errorMessage,
132
+ role,
133
+ },
134
+ },
135
+ };
136
+ }
137
+
25
138
  export async function runTurn(sessionId, options = {}) {
26
139
  return withSessionLock(sessionId, async () => {
27
140
  let state = await loadSessionState(sessionId);
@@ -44,7 +157,7 @@ export async function runTurn(sessionId, options = {}) {
44
157
  let content;
45
158
  try {
46
159
  const effectiveModel = resolveRoleModel(state, scheduled.role, options.model);
47
- content = await runOpenCodePrompt({
160
+ const output = await runOpenCodePromptDetailed({
48
161
  prompt,
49
162
  cwd: options.cwd || process.cwd(),
50
163
  model: effectiveModel,
@@ -52,6 +165,7 @@ export async function runTurn(sessionId, options = {}) {
52
165
  binaryPath: options.opencodeBin,
53
166
  timeoutMs: options.timeoutMs,
54
167
  });
168
+ content = output.text;
55
169
  } catch (error) {
56
170
  content = `Agent failed: ${error.message}`;
57
171
  }
@@ -109,7 +223,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
109
223
  let content;
110
224
  try {
111
225
  const effectiveModel = resolveRoleModel(state, role, options.model);
112
- content = await runOpenCodePrompt({
226
+ const output = await runOpenCodePromptDetailed({
113
227
  prompt,
114
228
  cwd: options.cwd || process.cwd(),
115
229
  model: effectiveModel,
@@ -117,6 +231,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
117
231
  binaryPath: options.opencodeBin,
118
232
  timeoutMs: options.timeoutMs,
119
233
  });
234
+ content = output.text;
120
235
  } catch (error) {
121
236
  content = `Agent failed: ${error.message}`;
122
237
  }
@@ -141,13 +256,33 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
141
256
  if (state.status !== 'running') return state;
142
257
  if (!state.roles.includes(role)) return state;
143
258
 
259
+ const run = ensureRunState(state);
260
+ if (run.status === 'cancelled' || run.status === 'failed' || run.status === 'completed') {
261
+ return state;
262
+ }
263
+
144
264
  if (!state.activeAgent) {
145
265
  const scheduled = nextRole(state);
266
+ const startedRun = run.status === 'idle'
267
+ ? appendRunEvent({
268
+ ...run,
269
+ status: 'running',
270
+ startedAt: new Date().toISOString(),
271
+ currentRole: scheduled.role,
272
+ round: scheduled.round,
273
+ }, 'run_started', { round: scheduled.round })
274
+ : appendRunEvent({
275
+ ...run,
276
+ currentRole: scheduled.role,
277
+ round: scheduled.round,
278
+ }, 'role_scheduled', { role: scheduled.role, round: scheduled.round });
279
+
146
280
  state = await saveSessionState({
147
281
  ...state,
148
282
  activeAgent: scheduled.role,
149
283
  nextRoleIndex: scheduled.nextRoleIndex,
150
284
  round: scheduled.round,
285
+ run: startedRun,
151
286
  });
152
287
  }
153
288
 
@@ -157,28 +292,86 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
157
292
 
158
293
  const prompt = buildRuntimePrompt({ state, role });
159
294
  let content;
295
+ let outputEvents = [];
296
+ let effectiveModel = null;
297
+ let failed = false;
298
+ let errorMessage = '';
160
299
  try {
161
- const effectiveModel = resolveRoleModel(state, role, options.model);
162
- content = await runOpenCodePrompt({
300
+ effectiveModel = resolveRoleModel(state, role, options.model);
301
+ state = await saveSessionState({
302
+ ...state,
303
+ run: appendRunEvent({
304
+ ...ensureRunState(state),
305
+ currentRole: role,
306
+ status: 'running',
307
+ }, 'role_started', { role, model: effectiveModel }),
308
+ });
309
+ const output = await runOpenCodePromptDetailed({
163
310
  prompt,
164
311
  cwd: options.cwd || process.cwd(),
165
312
  model: effectiveModel,
166
313
  agent: options.agent,
167
314
  binaryPath: options.opencodeBin,
168
- timeoutMs: options.timeoutMs,
315
+ timeoutMs: options.timeoutMs || ensureRunState(state).policy?.timeoutPerRoleMs || 180000,
169
316
  });
317
+ content = output.text;
318
+ outputEvents = output.events || [];
170
319
  } catch (error) {
320
+ failed = true;
321
+ errorMessage = error.message;
171
322
  content = `Agent failed: ${error.message}`;
172
323
  }
173
324
 
174
325
  state = await addAgentMessage(state, role, content);
326
+ if (failed) {
327
+ await appendMeetingTurn(sessionId, createTurnRecord({
328
+ round: state.round,
329
+ role,
330
+ model: effectiveModel,
331
+ content,
332
+ events: outputEvents,
333
+ }));
334
+ state = await saveSessionState(applyRoleFailurePolicy(state, role, errorMessage));
335
+ return state;
336
+ }
337
+
338
+ const turnRecord = createTurnRecord({
339
+ round: state.round,
340
+ role,
341
+ model: effectiveModel,
342
+ content,
343
+ events: outputEvents,
344
+ });
345
+ await appendMeetingTurn(sessionId, turnRecord);
346
+
347
+ const updatedShared = updateSharedContext(ensureRunState(state).sharedContext, turnRecord);
348
+ const succeededRun = appendRunEvent({
349
+ ...ensureRunState(state),
350
+ currentRole: null,
351
+ lastError: null,
352
+ sharedContext: updatedShared,
353
+ }, 'role_succeeded', { role, chars: content.length, events: outputEvents.length });
354
+
175
355
  state = await saveSessionState({
176
356
  ...state,
177
357
  activeAgent: null,
358
+ run: succeededRun,
178
359
  });
179
360
 
180
361
  if (shouldStop(state)) {
181
362
  state = await stopSession(state, 'completed');
363
+ const summaryMd = buildMeetingSummary(state.messages, ensureRunState(state), ensureRunState(state).sharedContext);
364
+ await writeMeetingSummary(sessionId, summaryMd);
365
+ const finalRun = appendRunEvent({
366
+ ...ensureRunState(state),
367
+ status: 'completed',
368
+ finishedAt: new Date().toISOString(),
369
+ finalSummary: extractFinalSummary(state.messages, ensureRunState(state)),
370
+ }, 'run_completed', { round: state.round });
371
+ state = await saveSessionState({
372
+ ...state,
373
+ run: finalRun,
374
+ });
182
375
  }
183
376
 
184
377
  return state;
@@ -25,11 +25,41 @@ export const ROLE_SYSTEM_PROMPTS = {
25
25
  ].join(' '),
26
26
  };
27
27
 
28
- export function buildAgentPrompt({ role, task, round, messages, maxMessages = 24 }) {
28
+ function formatSharedContext(sharedContext) {
29
+ if (!sharedContext || typeof sharedContext !== 'object') {
30
+ return 'No shared summary yet.';
31
+ }
32
+
33
+ const sections = [
34
+ ['Decisions', sharedContext.decisions],
35
+ ['Open issues', sharedContext.openIssues],
36
+ ['Risks', sharedContext.risks],
37
+ ['Action items', sharedContext.actionItems],
38
+ ['Notes', sharedContext.notes],
39
+ ];
40
+
41
+ const lines = [];
42
+ for (const [name, list] of sections) {
43
+ const items = Array.isArray(list) ? list.slice(-6) : [];
44
+ lines.push(`${name}:`);
45
+ if (items.length === 0) {
46
+ lines.push('- (none)');
47
+ } else {
48
+ for (const item of items) {
49
+ lines.push(`- ${item}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ return lines.join('\n');
55
+ }
56
+
57
+ export function buildAgentPrompt({ role, task, round, messages, sharedContext = null, maxMessages = 18 }) {
29
58
  const recent = messages.slice(-maxMessages);
30
59
  const transcript = recent.length
31
60
  ? recent.map((msg, idx) => `${idx + 1}. [${msg.from}] ${msg.content}`).join('\n')
32
61
  : 'No previous messages.';
62
+ const shared = formatSharedContext(sharedContext);
33
63
 
34
64
  return [
35
65
  `ROLE: ${role}`,
@@ -37,6 +67,9 @@ export function buildAgentPrompt({ role, task, round, messages, maxMessages = 24
37
67
  '',
38
68
  `TASK: ${task}`,
39
69
  '',
70
+ 'SHARED CONTEXT SUMMARY:',
71
+ shared,
72
+ '',
40
73
  'TEAM TRANSCRIPT (latest first-order history):',
41
74
  transcript,
42
75
  '',