chainlesschain 0.42.3 → 0.43.1

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
@@ -200,7 +200,9 @@ chainlesschain agent --provider openai --api-key sk-...
200
200
 
201
201
  Built-in tools: `read_file`, `write_file`, `edit_file`, `run_shell`, `search_files`, `list_dir`, `run_skill`, `list_skills`, `run_code`
202
202
 
203
- Agent slash commands: `/plan` (plan mode), `/plan interactive <request>` (LLM-driven planning with skill recommendations), `/model`, `/provider`, `/clear`, `/compact`, `/task`, `/session`, `/stats`, `/auto` (autonomous agent), `/cowork` (multi-agent collaboration)
203
+ Agent slash commands: `/plan` (plan mode), `/plan interactive <request>` (LLM-driven planning with skill recommendations), `/model`, `/provider`, `/clear`, `/compact`, `/task`, `/session`, `/stats`, `/auto` (autonomous agent), `/cowork` (multi-agent collaboration), `/sub-agents` (show active/completed sub-agents)
204
+
205
+ **Sub-Agent Isolation v2** (v0.43.0): Complex tasks are automatically decomposed into isolated sub-agents, each with its own namespaced memory, scoped context, and lifecycle tracking. Use `/sub-agents` inside an agent session to inspect active and completed sub-agents, token usage, and average durations.
204
206
 
205
207
  ### `chainlesschain skill <action>`
206
208
 
@@ -845,7 +847,7 @@ chainlesschain serve --allow-remote --token <secret> # Allow remote + auth
845
847
  chainlesschain serve --project /path/to/project # Default project root for sessions
846
848
  ```
847
849
 
848
- **Session Protocol** (v0.41.0): WebSocket clients can create stateful agent/chat sessions via `session-create`, send messages via `session-message`, resume previous sessions via `session-resume`, and manage sessions via `session-list`/`session-close`. Supports `slash-command` for in-session commands and `session-answer` for interactive Q&A (SlotFiller/Planner).
850
+ **Session Protocol** (v0.43.0): WebSocket clients can create stateful agent/chat sessions via `session-create`, send messages via `session-message`, resume previous sessions via `session-resume`, and manage sessions via `session-list`/`session-close`. Supports `slash-command` for in-session commands and `session-answer` for interactive Q&A (SlotFiller/Planner).
849
851
 
850
852
  ---
851
853
 
@@ -922,7 +924,7 @@ Configuration is stored at `~/.chainlesschain/config.json`. The CLI creates and
922
924
  ```bash
923
925
  cd packages/cli
924
926
  npm install
925
- npm test # Run all tests (2503 tests across 113 files)
927
+ npm test # Run all tests (2748 tests across 124 files)
926
928
  npm run test:unit # Unit tests only
927
929
  npm run test:integration # Integration tests
928
930
  npm run test:e2e # End-to-end tests
@@ -932,15 +934,15 @@ npm run test:e2e # End-to-end tests
932
934
 
933
935
  | Category | Files | Tests | Status |
934
936
  | ------------------------ | ------- | -------- | --------------- |
935
- | Unit — lib modules | 56 | 1200+ | All passing |
937
+ | Unit — lib modules | 63 | 1380+ | All passing |
936
938
  | Unit — commands | 15 | 350+ | All passing |
937
939
  | Unit — runtime | 1 | 6 | All passing |
938
- | Integration | 5 | 30+ | All passing |
939
- | E2E | 14 | 150+ | All passing |
940
+ | Integration | 6 | 40+ | All passing |
941
+ | E2E | 15 | 160+ | All passing |
940
942
  | Core packages (external) | — | 118 | All passing |
941
943
  | Unit — WS sessions | 9 | 156 | All passing |
942
944
  | Integration — WS session | 1 | 12 | All passing |
943
- | **CLI Total** | **113** | **2503** | **All passing** |
945
+ | **CLI Total** | **124** | **2748** | **All passing** |
944
946
 
945
947
  ## License
946
948
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.42.3",
3
+ "version": "0.43.1",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,7 +82,7 @@ const TEMPLATES = {
82
82
  skills: ["summarize"],
83
83
  persona: {
84
84
  name: "智能分诊助手",
85
- role: "你是一个医疗分诊AI助手,帮助诊所工作人员根据症状和紧急��度对患者进行优先级分类。",
85
+ role: "你是一个医疗分诊AI助手,帮助诊所工作人员根据症状和紧急程度对患者进行优先级分类。",
86
86
  behaviors: [
87
87
  "始终先询问患者症状再给出建议",
88
88
  "使用标准分诊分类 (ESI 1-5)",
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import crypto from "crypto";
9
+ import { SubAgentContext } from "./sub-agent-context.js";
9
10
 
10
11
  /**
11
12
  * Keyword map for agent type detection.
@@ -271,3 +272,113 @@ export function estimateComplexity(task) {
271
272
  estimatedSubtasks: Math.max(1, matchedTypes),
272
273
  };
273
274
  }
275
+
276
+ // ─── Role-based tool whitelist ──────────────────────────────────────────
277
+
278
+ export const ROLE_TOOL_WHITELIST = {
279
+ "code-review": ["read_file", "search_files", "list_dir"],
280
+ "code-generation": [
281
+ "read_file",
282
+ "write_file",
283
+ "edit_file",
284
+ "run_shell",
285
+ "search_files",
286
+ "list_dir",
287
+ ],
288
+ "data-analysis": [
289
+ "read_file",
290
+ "search_files",
291
+ "list_dir",
292
+ "run_code",
293
+ "run_shell",
294
+ ],
295
+ document: ["read_file", "write_file", "search_files", "list_dir"],
296
+ testing: [
297
+ "read_file",
298
+ "write_file",
299
+ "edit_file",
300
+ "run_shell",
301
+ "search_files",
302
+ "list_dir",
303
+ "run_code",
304
+ ],
305
+ general: null, // all tools
306
+ };
307
+
308
+ /**
309
+ * Execute a decomposed task using isolated sub-agent contexts.
310
+ * Each subtask gets its own SubAgentContext with role-appropriate tool whitelist.
311
+ *
312
+ * @param {{ taskId: string, subtasks: Array }} decomposition - From decomposeTask()
313
+ * @param {object} [options]
314
+ * @param {string} [options.cwd] - Working directory
315
+ * @param {object} [options.db] - Database instance
316
+ * @param {object} [options.llmOptions] - LLM provider options
317
+ * @param {string} [options.parentContext] - Condensed context from parent
318
+ * @returns {Promise<{ taskId: string, status: string, results: Array, summary: string }>}
319
+ */
320
+ export async function executeDecomposedTask(decomposition, options = {}) {
321
+ const { subtasks } = decomposition;
322
+ if (!subtasks || subtasks.length === 0) {
323
+ return {
324
+ taskId: decomposition.taskId,
325
+ status: "empty",
326
+ results: [],
327
+ summary: "No subtasks to execute",
328
+ };
329
+ }
330
+
331
+ const maxConcurrency = options.maxConcurrency || 3;
332
+
333
+ // Run subtasks in parallel batches with concurrency limit
334
+ const results = [];
335
+ for (let i = 0; i < subtasks.length; i += maxConcurrency) {
336
+ const batch = subtasks.slice(i, i + maxConcurrency);
337
+ const batchPromises = batch.map(async (subtask) => {
338
+ const allowedTools = ROLE_TOOL_WHITELIST[subtask.agentType] || null;
339
+
340
+ const subCtx = SubAgentContext.create({
341
+ role: subtask.agentType,
342
+ task: subtask.description,
343
+ inheritedContext: options.parentContext || null,
344
+ allowedTools,
345
+ cwd: options.cwd || process.cwd(),
346
+ db: options.db || null,
347
+ llmOptions: options.llmOptions || {},
348
+ });
349
+
350
+ try {
351
+ const result = await subCtx.run(subtask.description);
352
+ subtask.status = "completed";
353
+ subtask.result = result.summary;
354
+ return {
355
+ id: subtask.id,
356
+ agentType: subtask.agentType,
357
+ status: "completed",
358
+ summary: result.summary,
359
+ toolsUsed: result.toolsUsed,
360
+ };
361
+ } catch (err) {
362
+ subtask.status = "failed";
363
+ subtask.result = err.message;
364
+ return {
365
+ id: subtask.id,
366
+ agentType: subtask.agentType,
367
+ status: "failed",
368
+ error: err.message,
369
+ };
370
+ }
371
+ });
372
+
373
+ const batchResults = await Promise.all(batchPromises);
374
+ results.push(...batchResults);
375
+ }
376
+
377
+ const aggregated = aggregateResults(subtasks);
378
+ return {
379
+ taskId: decomposition.taskId,
380
+ status: aggregated.status,
381
+ results,
382
+ summary: aggregated.summary,
383
+ };
384
+ }
@@ -23,6 +23,7 @@ import { CLISkillLoader } from "./skill-loader.js";
23
23
  import { executeHooks, HookEvents } from "./hook-manager.js";
24
24
  import { detectPython } from "./cli-anything-bridge.js";
25
25
  import { findProjectRoot, loadProjectConfig } from "./project-detector.js";
26
+ import { SubAgentContext } from "./sub-agent-context.js";
26
27
 
27
28
  // ─── Tool definitions ────────────────────────────────────────────────────
28
29
 
@@ -212,6 +213,40 @@ export const AGENT_TOOLS = [
212
213
  },
213
214
  },
214
215
  },
216
+ {
217
+ type: "function",
218
+ function: {
219
+ name: "spawn_sub_agent",
220
+ description:
221
+ "Spawn an isolated sub-agent to handle a subtask. The sub-agent has its own context and message history, and only returns a summary result. Use this for tasks that benefit from focused, independent execution (e.g. code review, summarization, translation).",
222
+ parameters: {
223
+ type: "object",
224
+ properties: {
225
+ role: {
226
+ type: "string",
227
+ description:
228
+ "Sub-agent role (e.g. code-review, summarizer, translator, debugger)",
229
+ },
230
+ task: {
231
+ type: "string",
232
+ description: "Task description for the sub-agent",
233
+ },
234
+ context: {
235
+ type: "string",
236
+ description:
237
+ "Optional condensed context from the parent agent to pass to the sub-agent",
238
+ },
239
+ tools: {
240
+ type: "array",
241
+ items: { type: "string" },
242
+ description:
243
+ 'Optional tool whitelist for the sub-agent (e.g. ["read_file", "search_files"]). If omitted, all tools are available.',
244
+ },
245
+ },
246
+ required: ["role", "task"],
247
+ },
248
+ },
249
+ },
215
250
  ];
216
251
 
217
252
  // ─── Shared skill loader ──────────────────────────────────────────────────
@@ -326,6 +361,16 @@ When the user's problem involves data processing, calculations, file operations,
326
361
 
327
362
  You are not just a chatbot — you are a capable coding agent. Think step by step, write code when needed, and deliver real results.
328
363
 
364
+ ## Sub-Agent Isolation
365
+ When a task involves multiple distinct roles (e.g. code review + code generation), or when you need
366
+ focused analysis without polluting your current context, use the spawn_sub_agent tool. Examples:
367
+ - Code review as a separate perspective while you're implementing
368
+ - Summarizing a large file before incorporating it into your response
369
+ - Running a focused analysis (security, performance) on specific code
370
+ - Translating or reformatting content independently
371
+ The sub-agent has its own message history and only returns a summary — your context stays clean.
372
+ Do NOT spawn sub-agents for trivial tasks that you can handle directly.
373
+
329
374
  ## Environment
330
375
  ${envLines.join("\n")}
331
376
 
@@ -512,7 +557,11 @@ export async function executeTool(name, args, context = {}) {
512
557
 
513
558
  let toolResult;
514
559
  try {
515
- toolResult = await executeToolInner(name, args, { skillLoader, cwd });
560
+ toolResult = await executeToolInner(name, args, {
561
+ skillLoader,
562
+ cwd,
563
+ parentMessages: context.parentMessages,
564
+ });
516
565
  } catch (err) {
517
566
  if (hookDb) {
518
567
  try {
@@ -550,7 +599,11 @@ export async function executeTool(name, args, context = {}) {
550
599
  /**
551
600
  * Inner tool execution — no hooks, no plan-mode checks.
552
601
  */
553
- async function executeToolInner(name, args, { skillLoader, cwd }) {
602
+ async function executeToolInner(
603
+ name,
604
+ args,
605
+ { skillLoader, cwd, parentMessages },
606
+ ) {
554
607
  switch (name) {
555
608
  case "read_file": {
556
609
  const filePath = path.resolve(cwd, args.path);
@@ -613,6 +666,10 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
613
666
  return _executeRunCode(args, cwd);
614
667
  }
615
668
 
669
+ case "spawn_sub_agent": {
670
+ return _executeSpawnSubAgent(args, { skillLoader, cwd, parentMessages });
671
+ }
672
+
616
673
  case "search_files": {
617
674
  const dir = args.directory ? path.resolve(cwd, args.directory) : cwd;
618
675
  try {
@@ -676,6 +733,31 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
676
733
  error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
677
734
  };
678
735
  }
736
+
737
+ // Check if skill requests isolation (via SKILL.md frontmatter)
738
+ const skillIsolation = match.isolation === true;
739
+ if (skillIsolation) {
740
+ // Run skill through isolated sub-agent context
741
+ const subCtx = SubAgentContext.create({
742
+ role: `skill-${args.skill_name}`,
743
+ task: `Execute the "${args.skill_name}" skill with input: ${(args.input || "").substring(0, 200)}`,
744
+ allowedTools: ["read_file", "search_files", "list_dir"],
745
+ cwd,
746
+ });
747
+ try {
748
+ const result = await subCtx.run(args.input);
749
+ return {
750
+ success: true,
751
+ isolated: true,
752
+ skill: args.skill_name,
753
+ summary: result.summary,
754
+ toolsUsed: result.toolsUsed,
755
+ };
756
+ } catch (err) {
757
+ return { error: `Isolated skill execution failed: ${err.message}` };
758
+ }
759
+ }
760
+
679
761
  try {
680
762
  const handlerPath = path.join(match.skillDir, "handler.js");
681
763
  const imported = await import(
@@ -953,6 +1035,86 @@ async function _executeRunCode(args, cwd) {
953
1035
  }
954
1036
  }
955
1037
 
1038
+ // ─── spawn_sub_agent implementation ──────────────────────────────────────
1039
+
1040
+ /**
1041
+ * Execute a spawn_sub_agent tool call.
1042
+ * Creates an isolated SubAgentContext, runs it, and returns only the summary.
1043
+ *
1044
+ * @param {object} args - { role, task, context?, tools? }
1045
+ * @param {object} ctx - { skillLoader, cwd }
1046
+ * @returns {Promise<object>}
1047
+ */
1048
+ async function _executeSpawnSubAgent(args, ctx) {
1049
+ const { role, task, context: inheritedContext, tools: allowedTools } = args;
1050
+
1051
+ if (!role || !task) {
1052
+ return { error: "Both 'role' and 'task' are required for spawn_sub_agent" };
1053
+ }
1054
+
1055
+ // Auto-condense parent context if caller didn't provide explicit context
1056
+ let resolvedContext = inheritedContext || null;
1057
+ if (!resolvedContext && Array.isArray(ctx.parentMessages)) {
1058
+ const recentMsgs = ctx.parentMessages
1059
+ .filter((m) => m.role === "assistant" && typeof m.content === "string")
1060
+ .slice(-3)
1061
+ .map((m) => m.content.substring(0, 200));
1062
+ if (recentMsgs.length > 0) {
1063
+ resolvedContext = recentMsgs.join("\n---\n");
1064
+ }
1065
+ }
1066
+
1067
+ const subCtx = SubAgentContext.create({
1068
+ role,
1069
+ task,
1070
+ inheritedContext: resolvedContext,
1071
+ allowedTools: allowedTools || null,
1072
+ cwd: ctx.cwd,
1073
+ });
1074
+
1075
+ try {
1076
+ // Notify registry if available
1077
+ const { SubAgentRegistry } = await import("./sub-agent-registry.js").catch(
1078
+ () => ({ SubAgentRegistry: null }),
1079
+ );
1080
+ if (SubAgentRegistry) {
1081
+ try {
1082
+ SubAgentRegistry.getInstance().register(subCtx);
1083
+ } catch (_err) {
1084
+ // Registry not available — non-critical
1085
+ }
1086
+ }
1087
+
1088
+ const result = await subCtx.run(task);
1089
+
1090
+ // Complete in registry
1091
+ if (SubAgentRegistry) {
1092
+ try {
1093
+ SubAgentRegistry.getInstance().complete(subCtx.id, result);
1094
+ } catch (_err) {
1095
+ // Non-critical
1096
+ }
1097
+ }
1098
+
1099
+ return {
1100
+ success: true,
1101
+ subAgentId: subCtx.id,
1102
+ role: subCtx.role,
1103
+ summary: result.summary,
1104
+ toolsUsed: result.toolsUsed,
1105
+ iterationCount: result.iterationCount,
1106
+ artifactCount: result.artifacts.length,
1107
+ };
1108
+ } catch (err) {
1109
+ subCtx.forceComplete(err.message);
1110
+ return {
1111
+ error: `Sub-agent failed: ${err.message}`,
1112
+ subAgentId: subCtx.id,
1113
+ role: subCtx.role,
1114
+ };
1115
+ }
1116
+ }
1117
+
956
1118
  // ─── LLM chat with tools ─────────────────────────────────────────────────
957
1119
 
958
1120
  /**
@@ -1157,6 +1319,7 @@ export async function* agentLoop(messages, options) {
1157
1319
  hookDb: options.hookDb || null,
1158
1320
  skillLoader: options.skillLoader || _defaultSkillLoader,
1159
1321
  cwd: options.cwd || process.cwd(),
1322
+ parentMessages: messages, // pass parent messages for sub-agent auto-condensation
1160
1323
  };
1161
1324
 
1162
1325
  // ── Slot-filling phase ──────────────────────────────────────────────
@@ -1292,6 +1455,8 @@ export function formatToolArgs(name, args) {
1292
1455
  return args.category || args.query || "all";
1293
1456
  case "run_code":
1294
1457
  return `${args.language} (${(args.code || "").length} chars)`;
1458
+ case "spawn_sub_agent":
1459
+ return `[${args.role}] ${(args.task || "").substring(0, 60)}`;
1295
1460
  default:
1296
1461
  return JSON.stringify(args).substring(0, 60);
1297
1462
  }
@@ -40,10 +40,15 @@ export class CLIContextEngineering {
40
40
  * @param {object} options
41
41
  * @param {object|null} options.db - Database instance (null for graceful degradation)
42
42
  * @param {object|null} options.permanentMemory - CLIPermanentMemory instance (optional)
43
+ * @param {object|null} options.scope - Scoping options for sub-agent isolation
44
+ * @param {string} [options.scope.taskId] - Task/sub-agent ID
45
+ * @param {string} [options.scope.role] - Sub-agent role
46
+ * @param {string} [options.scope.parentObjective] - Parent task objective
43
47
  */
44
- constructor({ db, permanentMemory } = {}) {
48
+ constructor({ db, permanentMemory, scope } = {}) {
45
49
  this.db = db || null;
46
50
  this.permanentMemory = permanentMemory || null;
51
+ this.scope = scope || null;
47
52
  this.errorHistory = [];
48
53
  this.taskContext = null;
49
54
  this._bm25 = null;
@@ -52,6 +57,15 @@ export class CLIContextEngineering {
52
57
  this._compactionSummaries = [];
53
58
  // Stable prefix cache: { hash, cleanedPrefix }
54
59
  this._prefixCache = null;
60
+
61
+ // When scoped, auto-set task context from scope
62
+ if (this.scope && this.scope.parentObjective) {
63
+ this.taskContext = {
64
+ objective: this.scope.parentObjective,
65
+ steps: [],
66
+ currentStep: 0,
67
+ };
68
+ }
55
69
  }
56
70
 
57
71
  /**
@@ -91,31 +105,46 @@ export class CLIContextEngineering {
91
105
  }
92
106
  }
93
107
 
94
- // 3. Memory injection
108
+ // 3. Memory injection (scoped: higher threshold, namespace-aware)
95
109
  if (this.db && userQuery) {
96
110
  try {
97
- const memories = _deps.recallMemory(this.db, userQuery, { limit: 5 });
111
+ const memoryQuery = this.scope
112
+ ? `[${this.scope.role}] ${userQuery}`
113
+ : userQuery;
114
+ const memoryOpts = { limit: 5 };
115
+ if (this.scope) {
116
+ memoryOpts.namespace = this.scope.taskId;
117
+ }
118
+ const memories = _deps.recallMemory(this.db, memoryQuery, memoryOpts);
98
119
  if (memories && memories.length > 0) {
99
- const lines = memories.map(
100
- (m) =>
101
- `- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
102
- );
103
- result.push({
104
- role: "system",
105
- content: `## Relevant Memories\n${lines.join("\n")}`,
106
- });
120
+ // When scoped, apply higher relevance threshold to reduce noise
121
+ const threshold = this.scope ? 0.6 : 0.3;
122
+ const filtered = memories.filter((m) => m.retention >= threshold);
123
+ if (filtered.length > 0) {
124
+ const lines = filtered.map(
125
+ (m) =>
126
+ `- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
127
+ );
128
+ result.push({
129
+ role: "system",
130
+ content: `## Relevant Memories\n${lines.join("\n")}`,
131
+ });
132
+ }
107
133
  }
108
134
  } catch (_err) {
109
135
  // Memory injection failed — skip silently
110
136
  }
111
137
  }
112
138
 
113
- // 4. Notes injection (BM25 search)
139
+ // 4. Notes injection (BM25 search — scoped: role-prefixed query)
114
140
  if (this.db && userQuery) {
115
141
  try {
116
142
  this._ensureNotesIndex();
117
143
  if (this._bm25 && this._bm25.totalDocs > 0) {
118
- const hits = this._bm25.search(userQuery, {
144
+ const notesQuery = this.scope
145
+ ? `[${this.scope.role}] ${userQuery}`
146
+ : userQuery;
147
+ const hits = this._bm25.search(notesQuery, {
119
148
  topK: 3,
120
149
  threshold: 0.5,
121
150
  });
@@ -135,10 +164,14 @@ export class CLIContextEngineering {
135
164
  }
136
165
  }
137
166
 
138
- // 5. Permanent memory injection
167
+ // 5. Permanent memory injection (scoped: reduced results)
139
168
  if (this.permanentMemory && userQuery) {
140
169
  try {
141
- const pmResults = this.permanentMemory.getRelevantContext(userQuery, 3);
170
+ const pmLimit = this.scope ? 2 : 3;
171
+ const pmResults = this.permanentMemory.getRelevantContext(
172
+ userQuery,
173
+ pmLimit,
174
+ );
142
175
  if (pmResults && pmResults.length > 0) {
143
176
  const lines = pmResults.map(
144
177
  (r) => `- [${r.source || "memory"}] ${r.content}`,
@@ -91,6 +91,16 @@ export async function startDebate({
91
91
  }
92
92
 
93
93
  // Phase 2: Moderator synthesizes final verdict
94
+ // Summarize each reviewer's output to reduce context pollution for the moderator
95
+ const REVIEW_SUMMARY_MAX = 300;
96
+ const reviewSummaries = reviews.map((r) => {
97
+ const summarized =
98
+ r.review.length <= REVIEW_SUMMARY_MAX
99
+ ? r.review
100
+ : r.review.substring(0, REVIEW_SUMMARY_MAX) + "... [truncated]";
101
+ return { ...r, reviewSummary: summarized };
102
+ });
103
+
94
104
  const moderatorMessages = [
95
105
  {
96
106
  role: "system",
@@ -99,8 +109,8 @@ export async function startDebate({
99
109
  },
100
110
  {
101
111
  role: "user",
102
- content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviews
103
- .map((r) => `### ${r.role} (${r.verdict})\n${r.review}`)
112
+ content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviewSummaries
113
+ .map((r) => `### ${r.role} (${r.verdict})\n${r.reviewSummary}`)
104
114
  .join(
105
115
  "\n\n---\n\n",
106
116
  )}\n\nProvide:\n1. Final Verdict: APPROVE / NEEDS_WORK / REJECT\n2. Consensus Score: 0-100 (how much the reviewers agree)\n3. Summary of key findings across all perspectives\n4. Priority action items (if any)`,