@a1hvdy/cc-openclaw 0.5.2 → 0.7.0
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/dist/src/command-router/cc-handler.js +72 -0
- package/dist/src/command-router/cc-handler.js.map +1 -1
- package/dist/src/constants.d.ts +9 -0
- package/dist/src/constants.js +10 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/engines/persistent-session.d.ts +2 -0
- package/dist/src/engines/persistent-session.js +41 -11
- package/dist/src/engines/persistent-session.js.map +1 -1
- package/dist/src/lib/config.d.ts +2 -0
- package/dist/src/lib/config.js +19 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/sysprompt-strip.js +12 -12
- package/dist/src/lib/sysprompt-strip.js.map +1 -1
- package/dist/src/lib/trajectory.d.ts +1 -1
- package/dist/src/lib/trajectory.js.map +1 -1
- package/dist/src/lib/vendor-paths.d.ts +6 -4
- package/dist/src/lib/vendor-paths.js +21 -14
- package/dist/src/lib/vendor-paths.js.map +1 -1
- package/dist/src/openai-compat/openai-compat.d.ts +7 -1
- package/dist/src/openai-compat/openai-compat.js +8 -1
- package/dist/src/openai-compat/openai-compat.js.map +1 -1
- package/dist/src/openai-compat/sse-translator.d.ts +23 -3
- package/dist/src/openai-compat/sse-translator.js +45 -6
- package/dist/src/openai-compat/sse-translator.js.map +1 -1
- package/dist/src/session-bootstrap/cwd-patch.js +59 -28
- package/dist/src/session-bootstrap/cwd-patch.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/package.json +2 -3
- package/vendor/base-oneshot-session.d.ts +0 -87
- package/vendor/base-oneshot-session.js +0 -227
- package/vendor/base-oneshot-session.js.map +0 -1
- package/vendor/circuit-breaker.d.ts +0 -21
- package/vendor/circuit-breaker.js +0 -47
- package/vendor/circuit-breaker.js.map +0 -1
- package/vendor/consensus.d.ts +0 -20
- package/vendor/consensus.js +0 -52
- package/vendor/consensus.js.map +0 -1
- package/vendor/constants.d.ts +0 -130
- package/vendor/constants.js +0 -139
- package/vendor/constants.js.map +0 -1
- package/vendor/council.d.ts +0 -67
- package/vendor/council.js +0 -913
- package/vendor/council.js.map +0 -1
- package/vendor/embedded-server.d.ts +0 -25
- package/vendor/embedded-server.js +0 -373
- package/vendor/embedded-server.js.map +0 -1
- package/vendor/inbox-manager.d.ts +0 -38
- package/vendor/inbox-manager.js +0 -111
- package/vendor/inbox-manager.js.map +0 -1
- package/vendor/index.d.ts +0 -63
- package/vendor/index.js +0 -705
- package/vendor/index.js.map +0 -1
- package/vendor/logger.d.ts +0 -16
- package/vendor/logger.js +0 -44
- package/vendor/logger.js.map +0 -1
- package/vendor/models.d.ts +0 -69
- package/vendor/models.js +0 -289
- package/vendor/models.js.map +0 -1
- package/vendor/openai-compat.d.ts +0 -197
- package/vendor/openai-compat.js +0 -765
- package/vendor/openai-compat.js.map +0 -1
- package/vendor/persistent-codex-session.d.ts +0 -16
- package/vendor/persistent-codex-session.js +0 -105
- package/vendor/persistent-codex-session.js.map +0 -1
- package/vendor/persistent-cursor-session.d.ts +0 -21
- package/vendor/persistent-cursor-session.js +0 -241
- package/vendor/persistent-cursor-session.js.map +0 -1
- package/vendor/persistent-custom-session.d.ts +0 -78
- package/vendor/persistent-custom-session.js +0 -937
- package/vendor/persistent-custom-session.js.map +0 -1
- package/vendor/persistent-gemini-session.d.ts +0 -21
- package/vendor/persistent-gemini-session.js +0 -216
- package/vendor/persistent-gemini-session.js.map +0 -1
- package/vendor/persistent-session.d.ts +0 -74
- package/vendor/persistent-session.js +0 -684
- package/vendor/persistent-session.js.map +0 -1
- package/vendor/proxy/anthropic-adapter.d.ts +0 -136
- package/vendor/proxy/anthropic-adapter.js +0 -392
- package/vendor/proxy/anthropic-adapter.js.map +0 -1
- package/vendor/proxy/handler.d.ts +0 -39
- package/vendor/proxy/handler.js +0 -323
- package/vendor/proxy/handler.js.map +0 -1
- package/vendor/proxy/schema-cleaner.d.ts +0 -11
- package/vendor/proxy/schema-cleaner.js +0 -34
- package/vendor/proxy/schema-cleaner.js.map +0 -1
- package/vendor/proxy/thought-cache.d.ts +0 -19
- package/vendor/proxy/thought-cache.js +0 -53
- package/vendor/proxy/thought-cache.js.map +0 -1
- package/vendor/session-manager.d.ts +0 -211
- package/vendor/session-manager.js +0 -1345
- package/vendor/session-manager.js.map +0 -1
- package/vendor/skill-resolver.js +0 -107
- package/vendor/types.d.ts +0 -466
- package/vendor/types.js +0 -8
- package/vendor/types.js.map +0 -1
- package/vendor/validation.d.ts +0 -31
- package/vendor/validation.js +0 -104
- 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
|