@a1hvdy/cc-openclaw 0.6.0 → 0.7.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.
Files changed (88) hide show
  1. package/dist/src/engines/persistent-session.js +13 -0
  2. package/dist/src/engines/persistent-session.js.map +1 -1
  3. package/dist/src/lib/config.d.ts +2 -0
  4. package/dist/src/lib/config.js +19 -0
  5. package/dist/src/lib/config.js.map +1 -1
  6. package/dist/src/lib/trajectory.d.ts +1 -1
  7. package/dist/src/lib/trajectory.js.map +1 -1
  8. package/dist/src/lib/vendor-paths.d.ts +6 -4
  9. package/dist/src/lib/vendor-paths.js +21 -14
  10. package/dist/src/lib/vendor-paths.js.map +1 -1
  11. package/dist/src/openai-compat/openai-compat.d.ts +7 -1
  12. package/dist/src/openai-compat/openai-compat.js +35 -4
  13. package/dist/src/openai-compat/openai-compat.js.map +1 -1
  14. package/dist/src/openai-compat/sse-translator.d.ts +23 -3
  15. package/dist/src/openai-compat/sse-translator.js +45 -6
  16. package/dist/src/openai-compat/sse-translator.js.map +1 -1
  17. package/dist/src/types.d.ts +9 -0
  18. package/package.json +2 -3
  19. package/vendor/base-oneshot-session.d.ts +0 -87
  20. package/vendor/base-oneshot-session.js +0 -227
  21. package/vendor/base-oneshot-session.js.map +0 -1
  22. package/vendor/circuit-breaker.d.ts +0 -21
  23. package/vendor/circuit-breaker.js +0 -47
  24. package/vendor/circuit-breaker.js.map +0 -1
  25. package/vendor/consensus.d.ts +0 -20
  26. package/vendor/consensus.js +0 -52
  27. package/vendor/consensus.js.map +0 -1
  28. package/vendor/constants.d.ts +0 -130
  29. package/vendor/constants.js +0 -139
  30. package/vendor/constants.js.map +0 -1
  31. package/vendor/council.d.ts +0 -67
  32. package/vendor/council.js +0 -913
  33. package/vendor/council.js.map +0 -1
  34. package/vendor/embedded-server.d.ts +0 -25
  35. package/vendor/embedded-server.js +0 -373
  36. package/vendor/embedded-server.js.map +0 -1
  37. package/vendor/inbox-manager.d.ts +0 -38
  38. package/vendor/inbox-manager.js +0 -111
  39. package/vendor/inbox-manager.js.map +0 -1
  40. package/vendor/index.d.ts +0 -63
  41. package/vendor/index.js +0 -705
  42. package/vendor/index.js.map +0 -1
  43. package/vendor/logger.d.ts +0 -16
  44. package/vendor/logger.js +0 -44
  45. package/vendor/logger.js.map +0 -1
  46. package/vendor/models.d.ts +0 -69
  47. package/vendor/models.js +0 -289
  48. package/vendor/models.js.map +0 -1
  49. package/vendor/openai-compat.d.ts +0 -197
  50. package/vendor/openai-compat.js +0 -765
  51. package/vendor/openai-compat.js.map +0 -1
  52. package/vendor/persistent-codex-session.d.ts +0 -16
  53. package/vendor/persistent-codex-session.js +0 -105
  54. package/vendor/persistent-codex-session.js.map +0 -1
  55. package/vendor/persistent-cursor-session.d.ts +0 -21
  56. package/vendor/persistent-cursor-session.js +0 -241
  57. package/vendor/persistent-cursor-session.js.map +0 -1
  58. package/vendor/persistent-custom-session.d.ts +0 -78
  59. package/vendor/persistent-custom-session.js +0 -937
  60. package/vendor/persistent-custom-session.js.map +0 -1
  61. package/vendor/persistent-gemini-session.d.ts +0 -21
  62. package/vendor/persistent-gemini-session.js +0 -216
  63. package/vendor/persistent-gemini-session.js.map +0 -1
  64. package/vendor/persistent-session.d.ts +0 -74
  65. package/vendor/persistent-session.js +0 -698
  66. package/vendor/persistent-session.js.map +0 -1
  67. package/vendor/proxy/anthropic-adapter.d.ts +0 -136
  68. package/vendor/proxy/anthropic-adapter.js +0 -392
  69. package/vendor/proxy/anthropic-adapter.js.map +0 -1
  70. package/vendor/proxy/handler.d.ts +0 -39
  71. package/vendor/proxy/handler.js +0 -323
  72. package/vendor/proxy/handler.js.map +0 -1
  73. package/vendor/proxy/schema-cleaner.d.ts +0 -11
  74. package/vendor/proxy/schema-cleaner.js +0 -34
  75. package/vendor/proxy/schema-cleaner.js.map +0 -1
  76. package/vendor/proxy/thought-cache.d.ts +0 -19
  77. package/vendor/proxy/thought-cache.js +0 -53
  78. package/vendor/proxy/thought-cache.js.map +0 -1
  79. package/vendor/session-manager.d.ts +0 -211
  80. package/vendor/session-manager.js +0 -1345
  81. package/vendor/session-manager.js.map +0 -1
  82. package/vendor/skill-resolver.js +0 -107
  83. package/vendor/types.d.ts +0 -466
  84. package/vendor/types.js +0 -8
  85. package/vendor/types.js.map +0 -1
  86. package/vendor/validation.d.ts +0 -31
  87. package/vendor/validation.js +0 -104
  88. package/vendor/validation.js.map +0 -1
package/vendor/council.js DELETED
@@ -1,913 +0,0 @@
1
- /**
2
- * Council — Multi-agent collaboration engine
3
- *
4
- * Ported from three-minds and adapted to use SessionManager + ISession
5
- * directly (no HTTP/SSE to external services).
6
- *
7
- * Key patterns:
8
- * - Git worktree isolation per agent
9
- * - Two-phase protocol: planning round → execution rounds
10
- * - Consensus voting: all agents vote YES to complete
11
- * - Parallel execution via Promise.allSettled
12
- * - Engine-agnostic: agents can use Claude, Codex, or any ISession engine
13
- */
14
- import { randomUUID } from 'node:crypto';
15
- import { spawn } from 'node:child_process';
16
- import { EventEmitter } from 'node:events';
17
- import * as fs from 'node:fs';
18
- import * as path from 'node:path';
19
- import { parseConsensus, stripConsensusTags, hasConsensusMarker } from './consensus.js';
20
- import { DEFAULT_AGENT_TIMEOUT_MS, MIN_TASK_LENGTH, INTER_ROUND_DELAY_MS, EMPTY_RESPONSE_MAX_RETRIES, EMPTY_RESPONSE_RETRY_DELAY_MS, MIN_COMPLETE_RESPONSE_LENGTH, FOLLOWUP_MAX_RETRIES, HISTORY_PREVIEW_CHARS, SUMMARY_PREVIEW_CHARS, SUMMARY_SHORT_CHARS, COMPACT_CONTEXT_CHARS, DEFAULT_MAX_TURNS_PER_AGENT, GIT_CMD_TIMEOUT_MS, WORKTREE_CMD_TIMEOUT_MS, FOLLOWUP_TIMEOUT_MS, GIT_LOG_DEPTH, DEFAULT_MAX_ROUNDS, } from './constants.js';
21
- import { createConsoleLogger } from './logger.js';
22
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23
- // ─── Git Utilities ──────────────────────────────────────────────────────────
24
- function spawnAsync(cmd, args, opts = {}) {
25
- return new Promise((resolve, reject) => {
26
- const child = spawn(cmd, args, {
27
- stdio: ['pipe', 'pipe', 'pipe'],
28
- cwd: opts.cwd,
29
- });
30
- let stdout = '';
31
- let stderr = '';
32
- child.stdout.on('data', (d) => {
33
- stdout += d.toString();
34
- });
35
- child.stderr.on('data', (d) => {
36
- stderr += d.toString();
37
- });
38
- const timer = opts.timeout
39
- ? setTimeout(() => {
40
- child.kill('SIGTERM');
41
- reject(new Error('spawn timeout'));
42
- }, opts.timeout)
43
- : null;
44
- child.on('close', (code) => {
45
- if (timer)
46
- clearTimeout(timer);
47
- if (code !== 0)
48
- reject(new Error(`${cmd} exited with code ${code}: ${stderr.trim()}`));
49
- else
50
- resolve({ stdout, stderr });
51
- });
52
- child.on('error', (err) => {
53
- if (timer)
54
- clearTimeout(timer);
55
- reject(err);
56
- });
57
- });
58
- }
59
- const VALID_AGENT_NAME = /^[a-zA-Z0-9_-]+$/;
60
- /** Best-effort cleanup of already-created worktrees when a batch creation fails */
61
- async function cleanupCreatedWorktrees(worktreeMap, projectDir, logger) {
62
- const log = logger || createConsoleLogger('Council');
63
- for (const [createdAgent, createdPath] of worktreeMap) {
64
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'remove', '--force', createdPath], {
65
- timeout: GIT_CMD_TIMEOUT_MS,
66
- }).catch((err) => {
67
- log.error(`Failed to cleanup worktree for ${createdAgent}:`, err.message);
68
- });
69
- }
70
- }
71
- /** Set up git worktrees — one isolated directory per agent */
72
- async function setupWorktrees(projectDir, agents, logger) {
73
- const log = logger || createConsoleLogger('Council');
74
- const worktreeMap = new Map();
75
- // Validate agent names before using them in git branch names
76
- for (const agent of agents) {
77
- if (!VALID_AGENT_NAME.test(agent.name)) {
78
- throw new Error(`Invalid agent name '${agent.name}': must match /^[a-zA-Z0-9_-]+$/`);
79
- }
80
- }
81
- if (!fs.existsSync(projectDir)) {
82
- fs.mkdirSync(projectDir, { recursive: true });
83
- }
84
- // Ensure git repo
85
- const isGit = await spawnAsync('git', ['-C', projectDir, 'rev-parse', '--git-dir'], { timeout: GIT_CMD_TIMEOUT_MS })
86
- .then(() => true)
87
- .catch(() => false);
88
- if (!isGit) {
89
- await spawnAsync('git', ['-C', projectDir, 'init'], { timeout: GIT_CMD_TIMEOUT_MS });
90
- }
91
- // Git user config
92
- await spawnAsync('git', ['-C', projectDir, 'config', '--local', 'user.email', 'council@openclaw'], {
93
- timeout: GIT_CMD_TIMEOUT_MS,
94
- }).catch((err) => {
95
- log.error('Failed to set git user.email:', err.message);
96
- });
97
- await spawnAsync('git', ['-C', projectDir, 'config', '--local', 'user.name', 'Council'], {
98
- timeout: GIT_CMD_TIMEOUT_MS,
99
- }).catch((err) => {
100
- log.error('Failed to set git user.name:', err.message);
101
- });
102
- // Ensure at least one commit
103
- const hasCommit = await spawnAsync('git', ['-C', projectDir, 'rev-parse', 'HEAD'], { timeout: GIT_CMD_TIMEOUT_MS })
104
- .then(() => true)
105
- .catch(() => false);
106
- if (!hasCommit) {
107
- await spawnAsync('git', ['-C', projectDir, 'add', '-A'], { timeout: GIT_CMD_TIMEOUT_MS }).catch((err) => {
108
- log.error('Failed to git add:', err.message);
109
- });
110
- await spawnAsync('git', ['-C', projectDir, 'commit', '--allow-empty', '-m', 'council: initial'], {
111
- timeout: GIT_CMD_TIMEOUT_MS,
112
- });
113
- }
114
- // Create worktree per agent
115
- for (const agent of agents) {
116
- const wtDir = path.join(projectDir, '.worktrees', agent.name);
117
- const branch = `council/${agent.name}`;
118
- if (fs.existsSync(wtDir)) {
119
- const isValid = await spawnAsync('git', ['-C', wtDir, 'rev-parse', '--git-dir'], { timeout: GIT_CMD_TIMEOUT_MS })
120
- .then(() => true)
121
- .catch(() => false);
122
- if (isValid) {
123
- // Warn: hard reset discards uncommitted changes from any previous run
124
- const dirty = await spawnAsync('git', ['-C', wtDir, 'status', '--porcelain'], { timeout: GIT_CMD_TIMEOUT_MS })
125
- .then((r) => r.stdout.trim().length > 0)
126
- .catch(() => false);
127
- if (dirty) {
128
- log.warn(`Worktree ${wtDir} has uncommitted changes — discarding via hard reset`);
129
- }
130
- try {
131
- await spawnAsync('git', ['-C', wtDir, 'checkout', branch], { timeout: GIT_CMD_TIMEOUT_MS });
132
- await spawnAsync('git', ['-C', wtDir, 'reset', '--hard', 'HEAD'], { timeout: GIT_CMD_TIMEOUT_MS });
133
- worktreeMap.set(agent.name, wtDir);
134
- continue;
135
- }
136
- catch (err) {
137
- log.error(`Failed to reuse worktree ${wtDir} for branch ${branch}:`, err.message);
138
- // Fall through to re-create the worktree below
139
- }
140
- }
141
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'remove', '--force', wtDir], {
142
- timeout: GIT_CMD_TIMEOUT_MS,
143
- }).catch((err) => {
144
- log.error(`Failed to remove worktree ${wtDir}:`, err.message);
145
- });
146
- }
147
- await spawnAsync('git', ['-C', projectDir, 'branch', '-D', branch], { timeout: GIT_CMD_TIMEOUT_MS }).catch((err) => {
148
- log.error(`Failed to delete branch ${branch}:`, err.message);
149
- });
150
- try {
151
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'add', wtDir, '-b', branch], {
152
- timeout: WORKTREE_CMD_TIMEOUT_MS,
153
- });
154
- }
155
- catch (err) {
156
- await cleanupCreatedWorktrees(worktreeMap, projectDir, log);
157
- throw new Error(`Failed to create worktree for ${agent.name} at ${wtDir}: ${err.message}`);
158
- }
159
- if (!fs.existsSync(wtDir)) {
160
- await cleanupCreatedWorktrees(worktreeMap, projectDir, log);
161
- throw new Error(`Worktree directory not created: ${wtDir}`);
162
- }
163
- worktreeMap.set(agent.name, wtDir);
164
- }
165
- // Write CLAUDE.md constraints in each worktree
166
- for (const agent of agents) {
167
- const wtDir = worktreeMap.get(agent.name);
168
- if (wtDir)
169
- writeWorktreeClaudeMd(wtDir, agent.name, agent.emoji, projectDir);
170
- }
171
- return worktreeMap;
172
- }
173
- function writeWorktreeClaudeMd(wtDir, agentName, emoji, projectDir) {
174
- const claudeDir = path.join(wtDir, '.claude');
175
- if (!fs.existsSync(claudeDir))
176
- fs.mkdirSync(claudeDir, { recursive: true });
177
- const content = `# ${emoji} ${agentName}
178
-
179
- > This file is auto-generated by the system and takes priority over all conversation context.
180
-
181
- ## Identity
182
-
183
- You are **${emoji} ${agentName}**.
184
- Your working branch: \`council/${agentName}\`
185
- Your working directory: \`${wtDir}\`
186
-
187
- Only tasks marked \`[Claimed: council/${agentName}]\` in plan.md belong to you.
188
-
189
- ## Workspace Boundary
190
-
191
- Only operate within \`${wtDir}\` and \`${projectDir}\`.
192
- Never access: \`~/\`, \`/Users/\`, \`~/.openclaw/\`, or any path outside your workspace.
193
-
194
- ## Efficiency Rules
195
-
196
- - Complete Round 1 within 2-3 minutes — planning only
197
- - Empty projects: write the plan directly, no exploration needed
198
- - One \`ls\` is enough — never scan repeatedly
199
- `;
200
- fs.writeFileSync(path.join(claudeDir, 'CLAUDE.md'), content);
201
- }
202
- // ─── Prompt Building ────────────────────────────────────────────────────────
203
- function buildAgentPrompt(agent, task, round, previousResponses, allAgents) {
204
- const otherAgents = allAgents.filter((a) => a.name !== agent.name);
205
- // Build history with tail-first truncation (preserve reports and votes)
206
- let history = '';
207
- // Filter out empty responses so they don't pollute the collaboration history
208
- const substantiveResponses = previousResponses.filter((resp) => {
209
- const stripped = resp.content.replace(/^\[Agent completed[^\]]*\]\s*/i, '').trim();
210
- return stripped.length > 0;
211
- });
212
- if (substantiveResponses.length > 0) {
213
- history = '\n\n## Previous Collaboration History\n\n';
214
- let currentRound = 0;
215
- for (const resp of substantiveResponses) {
216
- if (resp.round !== currentRound) {
217
- currentRound = resp.round;
218
- history += `### Round ${currentRound}\n\n`;
219
- }
220
- const clean = stripConsensusTags(resp.content);
221
- const preview = clean.length > HISTORY_PREVIEW_CHARS ? '...' + clean.slice(-HISTORY_PREVIEW_CHARS) : clean;
222
- history += `**${resp.agent}** (${resp.consensus ? 'YES — agree to finish' : 'NO — continue'}):\n${preview}\n\n`;
223
- }
224
- }
225
- if (round === 1) {
226
- return `# Round 1 — Planning Round
227
-
228
- ## Task
229
- ${task}
230
-
231
- ## Your Partners
232
- ${otherAgents.map((a) => `- ${a.emoji} ${a.name}`).join('\n')}
233
- ${history}
234
- ## Rules: Planning Only — No Code
235
-
236
- This is Round 1, a **pure planning round**. All members work independently in parallel to create plan.md.
237
-
238
- **What you must do (in order, complete quickly):**
239
- 1. \`git log --oneline -5\` to check current state
240
- 2. If the project is empty (only initial commit), **no research needed** — write the plan directly from the task description
241
- 3. If the project has existing code, quickly check the file structure in your workspace (one \`ls\` only), then write the plan
242
- 4. Create \`plan.md\` (with task checklist, phase breakdown, claim status) and merge into main
243
- 5. If another member's plan.md already exists on main, merge your improvements into it
244
-
245
- **What you must never do:**
246
- - Do not write any business code
247
- - Do not repeatedly ls / glob / find to explore directories
248
- - Do not read any files outside your workspace
249
- - Do not spend more than 2-3 minutes on this round
250
-
251
- ## Consensus Vote
252
-
253
- At the **end** of your response, you must vote:
254
- - \`[CONSENSUS: NO]\` — normal for Round 1 (execution still needed after planning)
255
- - \`[CONSENSUS: YES]\` — only if the task is extremely simple
256
-
257
- Start writing plan.md now!`;
258
- }
259
- return `# Round ${round} — Execution Round
260
-
261
- ## Task
262
- ${task}
263
-
264
- ## Your Partners
265
- ${otherAgents.map((a) => `- ${a.emoji} ${a.name}`).join('\n')}
266
- ${history}
267
- ## Your Work
268
-
269
- plan.md was created by all members in Round 1. Now execute according to plan:
270
-
271
- 1. **Check current state** — pull main, read plan.md, understand latest progress
272
- 2. **Claim and execute tasks** — pick unclaimed tasks from plan.md, write code, modify files, run tests
273
- 3. **Review others' work** — if other members have output, review and suggest improvements or fix directly
274
- 4. **Report results** — briefly describe what you did
275
-
276
- ## Consensus Vote
277
-
278
- At the **end** of your response, you must vote (pick one):
279
-
280
- - \`[CONSENSUS: YES]\` — task complete, quality meets standards, ready to finish
281
- - \`[CONSENSUS: NO]\` — still work to do or issues to resolve
282
-
283
- Collaboration ends **only when all members vote YES**.
284
-
285
- Start working!`;
286
- }
287
- /** Resolve the path to configs/ relative to this module (works from both src/ and dist/) */
288
- function resolveConfigPath(filename) {
289
- // Try relative to source first, then relative to dist
290
- const candidates = [
291
- path.join(path.dirname(import.meta.url.replace('file://', '')), '..', 'configs', filename),
292
- path.join(path.dirname(import.meta.url.replace('file://', '')), '..', '..', 'configs', filename),
293
- ];
294
- for (const p of candidates) {
295
- if (fs.existsSync(p))
296
- return p;
297
- }
298
- return candidates[0]; // fallback — will error on read
299
- }
300
- function buildSystemPrompt(agent, allAgents, worktreePath) {
301
- const otherAgents = allAgents.filter((a) => a.name !== agent.name);
302
- const otherBranches = otherAgents.map((a) => `\`council/${a.name}\``).join(', ');
303
- const templatePath = resolveConfigPath('council-system-prompt.md');
304
- const template = fs.readFileSync(templatePath, 'utf-8');
305
- return template
306
- .replace(/\{\{emoji\}\}/g, agent.emoji)
307
- .replace(/\{\{name\}\}/g, agent.name)
308
- .replace(/\{\{persona\}\}/g, agent.persona)
309
- .replace(/\{\{workDir\}\}/g, worktreePath)
310
- .replace(/\{\{otherBranches\}\}/g, otherBranches);
311
- }
312
- // ─── Council Engine ─────────────────────────────────────────────────────────
313
- export class Council extends EventEmitter {
314
- config;
315
- manager;
316
- agentTimeoutMs;
317
- _aborted = false;
318
- _activeSessions = new Set();
319
- _session = null;
320
- _pendingInjection = null;
321
- logger;
322
- constructor(config, manager, logger) {
323
- super();
324
- this.config = config;
325
- this.manager = manager;
326
- this.agentTimeoutMs = config.agentTimeoutMs || DEFAULT_AGENT_TIMEOUT_MS;
327
- this.logger = logger || createConsoleLogger('Council');
328
- }
329
- getSession() {
330
- return this._session ?? undefined;
331
- }
332
- injectMessage(message) {
333
- this._pendingInjection = message;
334
- }
335
- abort() {
336
- this._aborted = true;
337
- for (const name of this._activeSessions) {
338
- this.manager.stopSession(name).catch(() => { });
339
- }
340
- this._activeSessions.clear();
341
- }
342
- emitEvent(event) {
343
- const full = { ...event, timestamp: new Date().toISOString() };
344
- this.emit('council-event', full);
345
- }
346
- // ─── Single Agent Execution ───────────────────────────────────────────
347
- async runSingleAgent(agent, prompt, systemPrompt, workDir, round, sessionId) {
348
- this.emitEvent({ type: 'agent-start', sessionId, round, agent: agent.name });
349
- const sessionName = `council-${sessionId.slice(0, 8)}-${agent.name}-r${round}`;
350
- this._activeSessions.add(sessionName);
351
- let content = '';
352
- try {
353
- for (let attempt = 0; attempt <= EMPTY_RESPONSE_MAX_RETRIES; attempt++) {
354
- if (this._aborted)
355
- throw new Error('Council aborted');
356
- if (attempt > 0) {
357
- this.emitEvent({
358
- type: 'agent-chunk',
359
- sessionId,
360
- round,
361
- agent: agent.name,
362
- content: `\n[Empty response, retry ${attempt}/${EMPTY_RESPONSE_MAX_RETRIES}]\n`,
363
- });
364
- await sleep(EMPTY_RESPONSE_RETRY_DELAY_MS);
365
- }
366
- // Start a session for this agent
367
- const engine = agent.engine || 'claude';
368
- await this.manager.startSession({
369
- name: sessionName,
370
- cwd: workDir,
371
- engine,
372
- model: agent.model,
373
- baseUrl: agent.baseUrl,
374
- permissionMode: agent.permissionMode ?? this.config.defaultPermissionMode ?? 'bypassPermissions',
375
- appendSystemPrompt: systemPrompt,
376
- maxTurns: this.config.maxTurnsPerAgent || DEFAULT_MAX_TURNS_PER_AGENT,
377
- maxBudgetUsd: this.config.maxBudgetUsd,
378
- customEngine: agent.customEngine,
379
- });
380
- // Send the prompt and wait for completion
381
- const result = await this.manager.sendMessage(sessionName, prompt, {
382
- timeout: this.agentTimeoutMs,
383
- onChunk: (chunk) => {
384
- this.emitEvent({ type: 'agent-chunk', sessionId, round, agent: agent.name, content: chunk });
385
- },
386
- });
387
- content = result.output;
388
- // Check if response is substantive
389
- const stripped = content.replace(/^\[Agent completed[^\]]*\]\s*/i, '').trim();
390
- if (stripped.length > 0 || hasConsensusMarker(content))
391
- break;
392
- if (attempt === EMPTY_RESPONSE_MAX_RETRIES) {
393
- this.logger.info(`${agent.name}: empty after ${EMPTY_RESPONSE_MAX_RETRIES} retries`);
394
- }
395
- }
396
- // Follow-up if response is too short and has no consensus marker
397
- const strippedContent = content.replace(/^\[Agent completed[^\]]*\]\s*/i, '').trim();
398
- if (!this._aborted && strippedContent.length < MIN_COMPLETE_RESPONSE_LENGTH && !hasConsensusMarker(content)) {
399
- for (let i = 0; i < FOLLOWUP_MAX_RETRIES; i++) {
400
- if (this._aborted)
401
- break;
402
- try {
403
- const followup = await this.manager.sendMessage(sessionName, 'Stop all tool calls. Output your complete report now, including your consensus vote [CONSENSUS: YES] or [CONSENSUS: NO].', { timeout: FOLLOWUP_TIMEOUT_MS });
404
- if (followup.output.trim().length > 0) {
405
- content = followup.output;
406
- if (hasConsensusMarker(content) || content.length >= MIN_COMPLETE_RESPONSE_LENGTH)
407
- break;
408
- }
409
- }
410
- catch {
411
- break;
412
- }
413
- await sleep(EMPTY_RESPONSE_RETRY_DELAY_MS);
414
- }
415
- }
416
- }
417
- finally {
418
- // Stop session — fire-and-forget
419
- this.manager.stopSession(sessionName).catch(() => { });
420
- this._activeSessions.delete(sessionName);
421
- }
422
- const consensus = parseConsensus(content);
423
- const response = {
424
- agent: agent.name,
425
- round,
426
- content,
427
- consensus,
428
- sessionKey: sessionName,
429
- timestamp: new Date().toISOString(),
430
- };
431
- this.emitEvent({ type: 'agent-complete', sessionId, round, agent: agent.name, content, consensus });
432
- return response;
433
- }
434
- // ─── Initialisation (synchronous — returns handle immediately) ──────
435
- init(task) {
436
- if (!task || task.trim().length < MIN_TASK_LENGTH) {
437
- throw new Error(`Task description too short (min ${MIN_TASK_LENGTH} chars)`);
438
- }
439
- const session = {
440
- id: randomUUID(),
441
- task: task.trim(),
442
- config: this.config,
443
- responses: [],
444
- status: 'running',
445
- startTime: new Date().toISOString(),
446
- };
447
- this._session = session;
448
- return session;
449
- }
450
- // ─── Main Orchestration Loop ──────────────────────────────────────────
451
- async run(task) {
452
- // Allow run(task) as shorthand for init(task) + run()
453
- if (task && !this._session)
454
- this.init(task);
455
- const session = this._session;
456
- if (!session)
457
- throw new Error('Council not initialised — call init() first');
458
- const trimmedTask = session.task;
459
- // Safety check: prevent council from running inside the program's own directory
460
- const moduleRoot = path.resolve(path.dirname(import.meta.url.replace('file://', '')), '..');
461
- const resolvedProjectDir = path.resolve(this.config.projectDir);
462
- if (resolvedProjectDir === moduleRoot || resolvedProjectDir.startsWith(moduleRoot + '/')) {
463
- throw new Error(`SAFETY: projectDir (${resolvedProjectDir}) is inside program root (${moduleRoot}). Refusing to start council.`);
464
- }
465
- if (this.config.agents.length === 0) {
466
- throw new Error('Council requires at least one agent');
467
- }
468
- this.logger.info(`Starting: ${this.config.agents.length} agents, max ${this.config.maxRounds} rounds`);
469
- this.logger.info(`Task: ${trimmedTask}`);
470
- this.logger.info(`Dir: ${this.config.projectDir}`);
471
- this.logger.warn('Agents run with permissionMode=bypassPermissions for autonomous execution');
472
- this.emitEvent({ type: 'session-start', sessionId: session.id, task: trimmedTask });
473
- // Set up git worktrees
474
- let worktreeMap;
475
- try {
476
- worktreeMap = await setupWorktrees(this.config.projectDir, this.config.agents, this.logger);
477
- }
478
- catch (err) {
479
- session.status = 'error';
480
- session.endTime = new Date().toISOString();
481
- throw err;
482
- }
483
- this.logger.info('Worktrees:');
484
- for (const [name, wtPath] of worktreeMap) {
485
- this.logger.info(` ${name}: ${wtPath}`);
486
- }
487
- try {
488
- for (let round = 1; round <= this.config.maxRounds; round++) {
489
- if (this._aborted)
490
- break;
491
- this.logger.info(`Round ${round} (${this.config.agents.length} agents parallel)`);
492
- this.emitEvent({ type: 'round-start', sessionId: session.id, round });
493
- // Check for user injection
494
- const injection = this._pendingInjection;
495
- this._pendingInjection = null;
496
- // Build prompts for all agents
497
- const agentTasks = this.config.agents.map((agent) => {
498
- const workDir = worktreeMap.get(agent.name) || this.config.projectDir;
499
- let prompt = buildAgentPrompt(agent, trimmedTask, round, session.responses, this.config.agents);
500
- if (injection) {
501
- prompt += `\n\n## User Injection\n\n${injection}`;
502
- }
503
- const systemPrompt = buildSystemPrompt(agent, this.config.agents, workDir);
504
- return { agent, prompt, systemPrompt, workDir };
505
- });
506
- // Execute all agents in parallel
507
- const results = await Promise.allSettled(agentTasks.map(({ agent, prompt, systemPrompt, workDir }) => this.runSingleAgent(agent, prompt, systemPrompt, workDir, round, session.id)));
508
- // Collect results
509
- const roundVotes = [];
510
- for (let i = 0; i < results.length; i++) {
511
- const result = results[i];
512
- const agent = this.config.agents[i];
513
- if (result.status === 'fulfilled') {
514
- roundVotes.push(result.value.consensus);
515
- session.responses.push(result.value);
516
- }
517
- else {
518
- const errMsg = result.reason?.message || 'Unknown error';
519
- this.logger.error(`${agent.name} failed: ${errMsg}`);
520
- this.emitEvent({ type: 'error', sessionId: session.id, round, agent: agent.name, error: errMsg });
521
- roundVotes.push(false);
522
- session.responses.push({
523
- agent: agent.name,
524
- round,
525
- content: `Error: ${errMsg}`,
526
- consensus: false,
527
- sessionKey: '',
528
- timestamp: new Date().toISOString(),
529
- });
530
- }
531
- }
532
- const allYes = roundVotes.length === this.config.agents.length && roundVotes.every((v) => v);
533
- this.emitEvent({ type: 'round-end', sessionId: session.id, round, status: allYes ? 'consensus' : 'continue' });
534
- if (allYes) {
535
- this.logger.info(`Consensus reached at round ${round}`);
536
- session.status = 'awaiting_user';
537
- break;
538
- }
539
- else {
540
- const yesCount = roundVotes.filter((v) => v).length;
541
- this.logger.info(`Votes: ${yesCount}/${this.config.agents.length} YES`);
542
- }
543
- if (round < this.config.maxRounds) {
544
- await sleep(INTER_ROUND_DELAY_MS);
545
- }
546
- }
547
- if (this._aborted) {
548
- session.status = 'error';
549
- }
550
- else if (session.status === 'running') {
551
- session.status = 'max_rounds';
552
- this.logger.info(`Max rounds (${this.config.maxRounds}) reached`);
553
- }
554
- session.endTime = new Date().toISOString();
555
- session.compactContext = this.generateCompactContext(session);
556
- session.finalSummary = this.generateSummary(session);
557
- this.saveTranscript(session);
558
- this.emitEvent({ type: 'complete', sessionId: session.id, status: session.status });
559
- return session;
560
- }
561
- catch (err) {
562
- session.status = 'error';
563
- session.endTime = new Date().toISOString();
564
- this.emitEvent({ type: 'error', sessionId: session.id, error: err.message });
565
- throw err;
566
- }
567
- }
568
- // ─── Summary & Transcript ─────────────────────────────────────────────
569
- generateSummary(session) {
570
- const maxRound = session.responses.length > 0 ? Math.max(...session.responses.map((r) => r.round)) : 0;
571
- const statusText = session.status === 'awaiting_user' || session.status === 'consensus' ? 'Consensus reached' : 'Max rounds reached';
572
- const lines = [
573
- `# Council Summary\n`,
574
- `- **Task**: ${session.task}`,
575
- `- **Status**: ${statusText}`,
576
- `- **Rounds**: ${maxRound}`,
577
- `- **Directory**: ${session.config.projectDir}\n`,
578
- `## Final Agent Status\n`,
579
- ];
580
- const lastResponses = session.responses.filter((r) => r.round === maxRound);
581
- for (const resp of lastResponses) {
582
- const agent = session.config.agents.find((a) => a.name === resp.agent);
583
- const emoji = agent?.emoji || '';
584
- const clean = stripConsensusTags(resp.content);
585
- const preview = clean.slice(0, SUMMARY_SHORT_CHARS) + (clean.length > SUMMARY_SHORT_CHARS ? '...' : '');
586
- lines.push(`### ${emoji} ${resp.agent}`);
587
- lines.push(`- Vote: ${resp.consensus ? 'YES' : 'NO'}`);
588
- lines.push(`- Summary:\n${preview}\n`);
589
- }
590
- return lines.join('\n');
591
- }
592
- generateCompactContext(session) {
593
- const maxRound = session.responses.length > 0 ? Math.max(...session.responses.map((r) => r.round)) : 0;
594
- const recent = session.responses.filter((r) => r.round >= maxRound - 1);
595
- const summaries = recent.map((resp) => {
596
- const clean = stripConsensusTags(resp.content).replace(/\s+/g, ' ').slice(0, COMPACT_CONTEXT_CHARS);
597
- return `- [R${resp.round}] ${resp.agent}: ${clean}${clean.length >= COMPACT_CONTEXT_CHARS ? '...' : ''}`;
598
- });
599
- return [
600
- `Task: ${session.task}`,
601
- `Progress: round ${maxRound} / max ${session.config.maxRounds}`,
602
- `Status: ${session.status}`,
603
- 'Latest:',
604
- ...summaries,
605
- ].join('\n');
606
- }
607
- saveTranscript(session) {
608
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
609
- const logDir = path.join(process.env.HOME || '/tmp', '.openclaw', 'council-logs');
610
- if (!fs.existsSync(logDir))
611
- fs.mkdirSync(logDir, { recursive: true });
612
- const filepath = path.join(logDir, `council-${ts}.md`);
613
- let content = `# Council Transcript\n\n`;
614
- content += `- **Time**: ${session.startTime}\n`;
615
- content += `- **Task**: ${session.task}\n`;
616
- content += `- **Status**: ${session.status}\n\n---\n\n`;
617
- let currentRound = 0;
618
- for (const resp of session.responses) {
619
- if (resp.round !== currentRound) {
620
- currentRound = resp.round;
621
- content += `## Round ${currentRound}\n\n`;
622
- }
623
- const agent = session.config.agents.find((a) => a.name === resp.agent);
624
- content += `### ${agent?.emoji || ''} ${resp.agent}\n\n${resp.content}\n\n`;
625
- }
626
- content += `---\n\n${session.finalSummary || ''}`;
627
- fs.writeFileSync(filepath, content);
628
- this.logger.info(`Transcript saved: ${filepath}`);
629
- }
630
- // ─── Post-Processing: Review / Accept / Reject ──────────────────────────
631
- /**
632
- * Produce a structured review of the council's output.
633
- * Lists all changed files, branches, worktrees, plan.md status, and agent summaries.
634
- * Does NOT modify any state — purely informational.
635
- */
636
- async review() {
637
- const session = this._session;
638
- if (!session)
639
- throw new Error('Council not initialised');
640
- const dir = session.config.projectDir;
641
- // Gather branches
642
- const branches = await spawnAsync('git', ['-C', dir, 'for-each-ref', '--format=%(refname:short)', 'refs/heads/'], {
643
- timeout: GIT_CMD_TIMEOUT_MS,
644
- })
645
- .then((r) => r.stdout
646
- .trim()
647
- .split('\n')
648
- .filter((b) => b.startsWith('council/')))
649
- .catch(() => []);
650
- // Gather worktrees
651
- const worktrees = await spawnAsync('git', ['-C', dir, 'worktree', 'list', '--porcelain'], {
652
- timeout: GIT_CMD_TIMEOUT_MS,
653
- })
654
- .then((r) => {
655
- const lines = r.stdout.split('\n');
656
- return lines.filter((l) => l.startsWith('worktree ')).map((l) => l.replace('worktree ', '').trim());
657
- })
658
- .catch(() => []);
659
- // Filter to only council worktrees (not the main worktree)
660
- const councilWorktrees = worktrees.filter((w) => w.includes('council') || w.includes('.worktrees'));
661
- // Check plan.md
662
- const planPath = path.join(dir, 'plan.md');
663
- const planExists = fs.existsSync(planPath);
664
- const planContent = planExists ? fs.readFileSync(planPath, 'utf-8') : undefined;
665
- // Check reviews/
666
- const reviewsDir = path.join(dir, 'reviews');
667
- const reviews = fs.existsSync(reviewsDir) ? fs.readdirSync(reviewsDir).filter((f) => f.endsWith('.md')) : [];
668
- // Diff stat: find changed files compared to initial state
669
- const changedFiles = [];
670
- try {
671
- // Ensure git history is available before diffing
672
- await spawnAsync('git', ['-C', dir, 'log', '--oneline', '--all', `-${GIT_LOG_DEPTH}`], {
673
- timeout: GIT_CMD_TIMEOUT_MS,
674
- });
675
- // Get diff stat from recent history (rough heuristic)
676
- const diffResult = await spawnAsync('git', ['-C', dir, 'diff', '--stat', '--numstat', 'HEAD~20', 'HEAD', '--'], {
677
- timeout: WORKTREE_CMD_TIMEOUT_MS,
678
- }).catch(() => ({ stdout: '', stderr: '' }));
679
- if (diffResult.stdout.trim()) {
680
- for (const line of diffResult.stdout.trim().split('\n')) {
681
- const parts = line.split('\t');
682
- if (parts.length >= 3) {
683
- const insertions = parseInt(parts[0], 10) || 0;
684
- const deletions = parseInt(parts[1], 10) || 0;
685
- const file = parts[2];
686
- if (file && !file.startsWith('-')) {
687
- changedFiles.push({ file, status: 'clean', insertions, deletions });
688
- }
689
- }
690
- }
691
- }
692
- // If no numstat, try a simpler approach
693
- if (changedFiles.length === 0) {
694
- const nameOnly = await spawnAsync('git', ['-C', dir, 'diff', '--name-only', 'HEAD~10', 'HEAD', '--'], {
695
- timeout: GIT_CMD_TIMEOUT_MS,
696
- }).catch(() => ({ stdout: '', stderr: '' }));
697
- for (const file of nameOnly.stdout.trim().split('\n').filter(Boolean)) {
698
- changedFiles.push({ file, status: 'clean', insertions: 0, deletions: 0 });
699
- }
700
- }
701
- }
702
- catch {
703
- // Git diff failed — possibly shallow history; skip file listing
704
- }
705
- // Agent summaries from final round
706
- const maxRound = session.responses.length > 0 ? Math.max(...session.responses.map((r) => r.round)) : 0;
707
- const lastResponses = session.responses.filter((r) => r.round === maxRound);
708
- const agentSummaries = lastResponses.map((resp) => {
709
- const clean = stripConsensusTags(resp.content);
710
- return {
711
- agent: resp.agent,
712
- consensus: resp.consensus,
713
- preview: clean.slice(0, SUMMARY_PREVIEW_CHARS) + (clean.length > SUMMARY_PREVIEW_CHARS ? '...' : ''),
714
- };
715
- });
716
- // Load reviewer guidance from config
717
- let reviewerGuidance = '';
718
- try {
719
- const guidancePath = resolveConfigPath('council-reviewer-prompt.md');
720
- reviewerGuidance = fs.readFileSync(guidancePath, 'utf-8');
721
- }
722
- catch {
723
- reviewerGuidance = 'Reviewer guidance not found. Evaluate the council output independently.';
724
- }
725
- return {
726
- councilId: session.id,
727
- projectDir: dir,
728
- status: session.status,
729
- rounds: maxRound,
730
- planExists,
731
- planContent,
732
- changedFiles,
733
- branches,
734
- worktrees: councilWorktrees,
735
- reviews,
736
- agentSummaries,
737
- reviewerGuidance,
738
- };
739
- }
740
- /**
741
- * Internal cleanup helper — removes worktrees, branches, plan.md, and reviews/.
742
- * Each cleanup step is independently gated by the `options` flags.
743
- */
744
- async _cleanup(projectDir, options) {
745
- const result = {
746
- worktreesRemoved: [],
747
- branchesDeleted: [],
748
- planDeleted: false,
749
- reviewsDeleted: false,
750
- };
751
- // Remove council worktrees
752
- if (options.removeWorktrees) {
753
- const wtListResult = await spawnAsync('git', ['-C', projectDir, 'worktree', 'list'], {
754
- timeout: GIT_CMD_TIMEOUT_MS,
755
- }).catch(() => ({
756
- stdout: '',
757
- stderr: '',
758
- }));
759
- for (const line of wtListResult.stdout.split('\n')) {
760
- const wtPath = line.split(/\s+/)[0];
761
- if (wtPath && wtPath.includes('council')) {
762
- // Safety: never remove the project dir itself
763
- if (path.resolve(wtPath) === path.resolve(projectDir))
764
- continue;
765
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'remove', '--force', wtPath], {
766
- timeout: WORKTREE_CMD_TIMEOUT_MS,
767
- }).catch((err) => this.logger.error(`Failed to remove worktree ${wtPath}:`, err.message));
768
- result.worktreesRemoved.push(wtPath);
769
- }
770
- }
771
- // Also remove .worktrees directory if it exists
772
- const dotWorktrees = path.join(projectDir, '.worktrees');
773
- if (fs.existsSync(dotWorktrees)) {
774
- // Remove any remaining worktree dirs via git first
775
- for (const entry of fs.readdirSync(dotWorktrees)) {
776
- const wtPath = path.join(dotWorktrees, entry);
777
- if (fs.statSync(wtPath).isDirectory()) {
778
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'remove', '--force', wtPath], {
779
- timeout: WORKTREE_CMD_TIMEOUT_MS,
780
- }).catch(() => { });
781
- if (!result.worktreesRemoved.includes(wtPath))
782
- result.worktreesRemoved.push(wtPath);
783
- }
784
- }
785
- // Clean up the directory itself if empty
786
- try {
787
- fs.rmSync(dotWorktrees, { recursive: true, force: true });
788
- }
789
- catch {
790
- // May fail if not empty; that's ok
791
- }
792
- }
793
- await spawnAsync('git', ['-C', projectDir, 'worktree', 'prune'], { timeout: GIT_CMD_TIMEOUT_MS }).catch(() => { });
794
- }
795
- // Delete council branches
796
- if (options.deleteBranches) {
797
- const branchResult = await spawnAsync('git', ['-C', projectDir, 'for-each-ref', '--format=%(refname:short)', 'refs/heads/'], { timeout: GIT_CMD_TIMEOUT_MS }).catch(() => ({ stdout: '', stderr: '' }));
798
- for (const branch of branchResult.stdout.trim().split('\n')) {
799
- if (branch.startsWith('council/')) {
800
- await spawnAsync('git', ['-C', projectDir, 'branch', '-D', branch], {
801
- timeout: GIT_CMD_TIMEOUT_MS,
802
- }).catch((err) => this.logger.error(`Failed to delete branch ${branch}:`, err.message));
803
- result.branchesDeleted.push(branch);
804
- }
805
- }
806
- }
807
- // Remove plan.md
808
- if (options.removePlan) {
809
- const planPath = path.join(projectDir, 'plan.md');
810
- result.planDeleted = fs.existsSync(planPath);
811
- if (result.planDeleted)
812
- fs.unlinkSync(planPath);
813
- }
814
- // Remove reviews/
815
- if (options.removeReviews) {
816
- const reviewsDir = path.join(projectDir, 'reviews');
817
- result.reviewsDeleted = fs.existsSync(reviewsDir);
818
- if (result.reviewsDeleted)
819
- fs.rmSync(reviewsDir, { recursive: true, force: true });
820
- }
821
- return result;
822
- }
823
- /**
824
- * Accept the council's work: clean up worktrees, branches, plan.md, and reviews/.
825
- * Should only be called after reviewing via `review()`.
826
- */
827
- async accept() {
828
- const session = this._session;
829
- if (!session)
830
- throw new Error('Council not initialised');
831
- const dir = session.config.projectDir;
832
- // Ensure we're on main
833
- await spawnAsync('git', ['-C', dir, 'checkout', 'main'], { timeout: GIT_CMD_TIMEOUT_MS }).catch(() => spawnAsync('git', ['-C', dir, 'checkout', 'master'], { timeout: GIT_CMD_TIMEOUT_MS }).catch(() => { }));
834
- const { worktreesRemoved, branchesDeleted, planDeleted, reviewsDeleted } = await this._cleanup(dir, {
835
- removeWorktrees: true,
836
- deleteBranches: true,
837
- removePlan: true,
838
- removeReviews: true,
839
- });
840
- // Update session status
841
- session.status = 'accepted';
842
- this.logger.info(`Accepted: ${branchesDeleted.length} branches, ${worktreesRemoved.length} worktrees cleaned up`);
843
- return { councilId: session.id, branchesDeleted, worktreesRemoved, planDeleted, reviewsDeleted };
844
- }
845
- /**
846
- * Reject the council's work: rewrite plan.md with feedback.
847
- * Does NOT delete any worktrees or branches — the council can retry.
848
- */
849
- async reject(feedback) {
850
- const session = this._session;
851
- if (!session)
852
- throw new Error('Council not initialised');
853
- const dir = session.config.projectDir;
854
- const planPath = path.join(dir, 'plan.md');
855
- // Build rejection plan
856
- const rejectionPlan = `# Project Plan (REJECTED & RESTARTED)
857
-
858
- ## Reviewer Feedback
859
- ${feedback}
860
-
861
- ## Previous Status
862
- - **Council ID**: ${session.id}
863
- - **Rounds completed**: ${session.responses.length > 0 ? Math.max(...session.responses.map((r) => r.round)) : 0}
864
- - **Final status**: ${session.status}
865
-
866
- ## Tasks for Council
867
- _Replace the tasks below with specific actionable items based on the feedback above._
868
-
869
- - [ ] Address reviewer feedback
870
- - [ ] Verify all changes compile and pass tests
871
- - [ ] Update plan.md with accurate completion status
872
- `;
873
- fs.writeFileSync(planPath, rejectionPlan);
874
- // Commit the rejection plan
875
- await spawnAsync('git', ['-C', dir, 'add', 'plan.md'], { timeout: GIT_CMD_TIMEOUT_MS }).catch(() => { });
876
- await spawnAsync('git', ['-C', dir, 'commit', '-m', 'council(reject): rewrite plan.md with reviewer feedback'], {
877
- timeout: GIT_CMD_TIMEOUT_MS,
878
- }).catch(() => { });
879
- // Update session status
880
- session.status = 'rejected';
881
- this.logger.info('Rejected: plan.md rewritten with feedback');
882
- return { councilId: session.id, planRewritten: true, feedback };
883
- }
884
- }
885
- // ─── Default Config ─────────────────────────────────────────────────────────
886
- export function getDefaultCouncilConfig(projectDir) {
887
- return {
888
- name: 'Three Minds Council',
889
- agents: [
890
- {
891
- name: 'Planner',
892
- emoji: '🔵',
893
- persona: 'You are a technical planner. You decompose requirements into actionable plans, define product context and constraints, outline high-level architecture decisions, and deliberately avoid premature implementation details. Your goal is a clear, phased blueprint that other agents can execute against.',
894
- role: 'gemini',
895
- },
896
- {
897
- name: 'Generator',
898
- emoji: '🟠',
899
- persona: 'You are an implementation engineer. You execute strictly according to plan.md, delivering working code sprint by sprint. You prioritize correctness, shipping velocity, and minimal deviation from the plan. When the plan is ambiguous, you fill gaps conservatively without reinventing requirements.',
900
- role: 'claude',
901
- },
902
- {
903
- name: 'Evaluator',
904
- emoji: '🟢',
905
- persona: 'You are an independent quality gate. You do not trust that the implementation is correct — you verify it. You validate from real user paths, hunt for broken UX, edge cases, regressions, and inconsistencies. You must give an explicit blocking issue list or a reasoned approval. You are not a polite reviewer; you are the acceptance authority.',
906
- role: 'gpt',
907
- },
908
- ],
909
- maxRounds: DEFAULT_MAX_ROUNDS,
910
- projectDir,
911
- };
912
- }
913
- //# sourceMappingURL=council.js.map