echelon-dev 1.0.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/bin/echelon-dev.js +286 -0
- package/lib/agents.js +145 -0
- package/lib/echelon-local-agent.js +683 -0
- package/lib/echelon-pair-program.js +610 -0
- package/lib/session-recorder.js +284 -0
- package/package.json +17 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Echelon Dev Team - Multi-Agent Pair Programming
|
|
5
|
+
*
|
|
6
|
+
* Coordinates Knoxis, Solan, and Astrahelm as a dev team working
|
|
7
|
+
* together on a shared codebase. Each agent is a generalist engineer
|
|
8
|
+
* with a specialty — they contribute what's useful, nothing more.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node echelon-pair-program.js --workspace /path --prompt "task"
|
|
12
|
+
* node echelon-pair-program.js -w my-project --prompt "add auth" --agents knoxis,astrahelm
|
|
13
|
+
* node echelon-pair-program.js --workspace /path --prompt-base64 "..." --timeline-base64 "..."
|
|
14
|
+
*
|
|
15
|
+
* ZERO EXTERNAL DEPENDENCIES
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { spawn, spawnSync } = require('child_process');
|
|
22
|
+
const { getAgent, getAllAgents, getAgentIds, colorize, buildTeamSystemPrompt, RESET } = require('./agents');
|
|
23
|
+
const { SessionRecorder } = require('./session-recorder');
|
|
24
|
+
|
|
25
|
+
// ===== ARGUMENT PARSING =====
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = {};
|
|
29
|
+
const multi = {};
|
|
30
|
+
for (let i = 2; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg.startsWith('--')) {
|
|
33
|
+
const key = arg.slice(2);
|
|
34
|
+
const next = argv[i + 1];
|
|
35
|
+
if (!next || next.startsWith('--')) {
|
|
36
|
+
args[key] = true;
|
|
37
|
+
} else {
|
|
38
|
+
if (multi[key]) {
|
|
39
|
+
multi[key].push(next);
|
|
40
|
+
} else if (args[key]) {
|
|
41
|
+
multi[key] = [args[key], next];
|
|
42
|
+
delete args[key];
|
|
43
|
+
} else {
|
|
44
|
+
args[key] = next;
|
|
45
|
+
}
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Object.entries(multi).forEach(([key, list]) => { args[key] = list; });
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decodeBase64(value) {
|
|
55
|
+
try {
|
|
56
|
+
return Buffer.from(value, 'base64').toString('utf8');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('Failed to decode base64 value:', err.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function decodeJsonBase64(value, label) {
|
|
64
|
+
const decoded = decodeBase64(value);
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(decoded);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Failed to parse ${label} JSON: ${err.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===== UTILITY =====
|
|
74
|
+
|
|
75
|
+
function commandExists(cmd) {
|
|
76
|
+
const detector = process.platform === 'win32' ? 'where' : 'which';
|
|
77
|
+
const result = spawnSync(detector, [cmd], { stdio: 'ignore' });
|
|
78
|
+
return result.status === 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveWorkspacePath(nameOrPath) {
|
|
82
|
+
if (fs.existsSync(nameOrPath)) {
|
|
83
|
+
return path.resolve(nameOrPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try workspace registries (echelon + knoxis)
|
|
87
|
+
const registries = [
|
|
88
|
+
path.join(os.homedir(), '.echelon', 'workspaces.json'),
|
|
89
|
+
path.join(os.homedir(), '.knoxis', 'workspaces.json')
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const registryFile of registries) {
|
|
93
|
+
if (fs.existsSync(registryFile)) {
|
|
94
|
+
try {
|
|
95
|
+
const workspaces = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
|
|
96
|
+
if (workspaces[nameOrPath]) return workspaces[nameOrPath];
|
|
97
|
+
const lower = nameOrPath.toLowerCase();
|
|
98
|
+
for (const [name, wsPath] of Object.entries(workspaces)) {
|
|
99
|
+
if (name.toLowerCase().includes(lower)) {
|
|
100
|
+
console.log(`Matched workspace: ${name} -> ${wsPath}`);
|
|
101
|
+
return wsPath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveAiProvider(preference) {
|
|
112
|
+
const normalized = (preference || 'auto').toLowerCase();
|
|
113
|
+
if (normalized === 'claude' && commandExists('claude')) {
|
|
114
|
+
return { cmd: 'claude', args: ['--dangerously-skip-permissions'], label: 'Claude Code' };
|
|
115
|
+
}
|
|
116
|
+
if (normalized === 'codex' && commandExists('codex')) {
|
|
117
|
+
return { cmd: 'codex', args: [], label: 'Codex' };
|
|
118
|
+
}
|
|
119
|
+
if (normalized === 'auto') {
|
|
120
|
+
if (commandExists('claude')) {
|
|
121
|
+
return { cmd: 'claude', args: ['--dangerously-skip-permissions'], label: 'Claude Code' };
|
|
122
|
+
}
|
|
123
|
+
if (commandExists('codex')) {
|
|
124
|
+
return { cmd: 'codex', args: [], label: 'Codex' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
console.error('No supported AI provider found. Install the Claude or Codex CLI and try again.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toArray(value) {
|
|
132
|
+
if (!value) return [];
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
const result = [];
|
|
135
|
+
value.forEach(item => result.push(...toArray(item)));
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
return [value];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatSection(title, body) {
|
|
142
|
+
const border = '-'.repeat(title.length + 4);
|
|
143
|
+
return `${border}\n| ${title} |\n${border}\n${body.trim()}\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function gatherContext(workspace, inputs) {
|
|
147
|
+
const sections = [];
|
|
148
|
+
const labels = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
|
|
151
|
+
toArray(inputs).forEach(entry => {
|
|
152
|
+
if (typeof entry !== 'string') return;
|
|
153
|
+
const trimmed = entry.trim();
|
|
154
|
+
if (!trimmed) return;
|
|
155
|
+
const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(workspace, trimmed);
|
|
156
|
+
if (!fs.existsSync(absolute)) return;
|
|
157
|
+
if (seen.has(absolute)) return;
|
|
158
|
+
seen.add(absolute);
|
|
159
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
160
|
+
const title = path.relative(workspace, absolute) || path.basename(absolute);
|
|
161
|
+
labels.push(title);
|
|
162
|
+
sections.push(formatSection(title, content));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return { sections, labels };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ===== DEFAULT MULTI-AGENT TIMELINES =====
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the default multi-agent timeline.
|
|
172
|
+
* The timeline adapts based on which agents are included.
|
|
173
|
+
*
|
|
174
|
+
* With all 3 agents: understand → team plan → implement → review
|
|
175
|
+
* With 2 agents: understand → plan → implement → review
|
|
176
|
+
* With 1 agent: falls back to knoxis-style 4-step
|
|
177
|
+
*/
|
|
178
|
+
function buildDefaultTimeline(task, activeAgents) {
|
|
179
|
+
const agentIds = activeAgents.map(a => a.id);
|
|
180
|
+
const hasKnoxis = agentIds.includes('knoxis');
|
|
181
|
+
const hasSolan = agentIds.includes('solan');
|
|
182
|
+
const hasAstrahelm = agentIds.includes('astrahelm');
|
|
183
|
+
|
|
184
|
+
// Pick a lead agent — first available in priority order
|
|
185
|
+
const lead = hasKnoxis ? 'knoxis' : hasSolan ? 'solan' : 'astrahelm';
|
|
186
|
+
const leadAgent = getAgent(lead);
|
|
187
|
+
|
|
188
|
+
const steps = [];
|
|
189
|
+
|
|
190
|
+
// Step 1: Lead agent understands the codebase
|
|
191
|
+
steps.push({
|
|
192
|
+
key: 'understand',
|
|
193
|
+
title: 'Understanding',
|
|
194
|
+
agentId: lead,
|
|
195
|
+
displayName: leadAgent.name,
|
|
196
|
+
persona: leadAgent.persona,
|
|
197
|
+
instruction: `Let's understand what we're working with before diving in.
|
|
198
|
+
|
|
199
|
+
The task: ${task}
|
|
200
|
+
|
|
201
|
+
First:
|
|
202
|
+
1. Read the relevant code to understand the current state
|
|
203
|
+
2. Identify what needs to change
|
|
204
|
+
3. Note any potential issues or dependencies
|
|
205
|
+
|
|
206
|
+
Share your understanding briefly. For any ambiguities, state your assumption and move on - do not ask questions.`,
|
|
207
|
+
includeContext: true
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Step 2: Team weighs in on approach (if multiple agents)
|
|
211
|
+
if (activeAgents.length > 1) {
|
|
212
|
+
const otherAgents = activeAgents.filter(a => a.id !== lead);
|
|
213
|
+
const othersIntro = otherAgents.map(a => `${a.name} (${a.specialty})`).join(' and ');
|
|
214
|
+
|
|
215
|
+
steps.push({
|
|
216
|
+
key: 'team-input',
|
|
217
|
+
title: 'Team Input',
|
|
218
|
+
agentId: otherAgents[0].id,
|
|
219
|
+
displayName: otherAgents.map(a => a.name).join(' & '),
|
|
220
|
+
persona: otherAgents.length === 1
|
|
221
|
+
? otherAgents[0].persona
|
|
222
|
+
: `You are providing perspective from ${othersIntro}. Think from each angle but only contribute what's genuinely useful for this specific task. If your specialty doesn't apply here, say so briefly and focus on general engineering quality.`,
|
|
223
|
+
instruction: `You've seen ${leadAgent.name}'s assessment. Now weigh in from your perspective${otherAgents.length > 1 ? 's' : ''}.
|
|
224
|
+
|
|
225
|
+
Rules:
|
|
226
|
+
- Only raise points that are actually relevant to THIS task
|
|
227
|
+
- If your specialty doesn't apply, say "nothing specific from my angle" and move on
|
|
228
|
+
- Don't repeat what ${leadAgent.name} already covered
|
|
229
|
+
- If you agree with the approach, say so briefly and add any considerations
|
|
230
|
+
- Be concrete — if you flag something, suggest what to do about it`,
|
|
231
|
+
includeContext: false
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Step 3: Lead plans and implements
|
|
236
|
+
steps.push({
|
|
237
|
+
key: 'plan',
|
|
238
|
+
title: 'Planning',
|
|
239
|
+
agentId: lead,
|
|
240
|
+
displayName: leadAgent.name,
|
|
241
|
+
persona: leadAgent.persona,
|
|
242
|
+
instruction: `Good.${activeAgents.length > 1 ? " Incorporate the team's input where it makes sense." : ''} Now create a concrete implementation plan for: ${task}
|
|
243
|
+
|
|
244
|
+
Plan requirements:
|
|
245
|
+
1. What files need to be created or modified?
|
|
246
|
+
2. What's the order of changes?
|
|
247
|
+
3. What could go wrong?
|
|
248
|
+
4. How will we verify it works?
|
|
249
|
+
|
|
250
|
+
For any open questions, make your best judgment call and note the decision. Do not ask - decide and move forward.`,
|
|
251
|
+
includeContext: false
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Step 4: Implement
|
|
255
|
+
steps.push({
|
|
256
|
+
key: 'implement',
|
|
257
|
+
title: 'Implementation',
|
|
258
|
+
agentId: lead,
|
|
259
|
+
displayName: leadAgent.name,
|
|
260
|
+
persona: leadAgent.persona,
|
|
261
|
+
instruction: `Now implement the plan. Work through it step by step.
|
|
262
|
+
|
|
263
|
+
Rules:
|
|
264
|
+
- Make one logical change at a time
|
|
265
|
+
- For any decisions or unknowns, pick the most standard approach and note your choice
|
|
266
|
+
- If you flagged questions earlier, answer them yourself with reasonable defaults and proceed
|
|
267
|
+
- Flag if you hit something truly blocking (missing credentials, broken dependencies)
|
|
268
|
+
- Otherwise, keep building
|
|
269
|
+
|
|
270
|
+
Start implementing now.`,
|
|
271
|
+
includeContext: false
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Step 5: Team review (if multiple agents)
|
|
275
|
+
if (activeAgents.length > 1) {
|
|
276
|
+
// Each non-lead agent reviews from their angle
|
|
277
|
+
const reviewAgents = activeAgents.filter(a => a.id !== lead);
|
|
278
|
+
|
|
279
|
+
steps.push({
|
|
280
|
+
key: 'review',
|
|
281
|
+
title: 'Team Review',
|
|
282
|
+
agentId: reviewAgents[0].id,
|
|
283
|
+
displayName: reviewAgents.map(a => a.name).join(' & '),
|
|
284
|
+
persona: reviewAgents.length === 1
|
|
285
|
+
? reviewAgents[0].persona
|
|
286
|
+
: `You are reviewing the implementation from the perspectives of ${reviewAgents.map(a => `${a.name} (${a.specialty})`).join(' and ')}. Be pragmatic — only flag real issues.`,
|
|
287
|
+
instruction: `Review what ${leadAgent.name} built.
|
|
288
|
+
|
|
289
|
+
For each perspective you represent:
|
|
290
|
+
- Does it solve the original problem correctly?
|
|
291
|
+
- Are there any genuine issues from your angle? (Only flag real problems, not theoretical ones)
|
|
292
|
+
- Any quick improvements that are clearly worth making?
|
|
293
|
+
|
|
294
|
+
If everything looks good from your angle, say so. Don't invent problems.
|
|
295
|
+
If you find real issues, be specific about what to fix and why.`,
|
|
296
|
+
includeContext: false
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Final step: Verify
|
|
301
|
+
steps.push({
|
|
302
|
+
key: 'verify',
|
|
303
|
+
title: 'Final Check',
|
|
304
|
+
agentId: lead,
|
|
305
|
+
displayName: leadAgent.name,
|
|
306
|
+
persona: leadAgent.persona,
|
|
307
|
+
instruction: `Let's do a final check on what we built.
|
|
308
|
+
|
|
309
|
+
Quick checklist:
|
|
310
|
+
- Does it solve the original problem?
|
|
311
|
+
- Any edge cases we missed?
|
|
312
|
+
- Is the code clean and following project patterns?
|
|
313
|
+
- Anything we should test?
|
|
314
|
+
${activeAgents.length > 1 ? `- Address any issues the team flagged in the review\n` : ''}
|
|
315
|
+
Give me the summary and any follow-up recommendations.`,
|
|
316
|
+
includeContext: false
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return steps;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ===== PROMPT BUILDING =====
|
|
323
|
+
|
|
324
|
+
function buildPrompt(options) {
|
|
325
|
+
const {
|
|
326
|
+
systemIntro,
|
|
327
|
+
personaIntro,
|
|
328
|
+
conversation,
|
|
329
|
+
instructionLabel,
|
|
330
|
+
instructionTitle,
|
|
331
|
+
instruction,
|
|
332
|
+
includeContextBlock,
|
|
333
|
+
globalContext,
|
|
334
|
+
stepContext,
|
|
335
|
+
agentLabel
|
|
336
|
+
} = options;
|
|
337
|
+
|
|
338
|
+
const sections = [];
|
|
339
|
+
|
|
340
|
+
if (systemIntro) sections.push(systemIntro);
|
|
341
|
+
if (personaIntro) sections.push(`Your role for this step:\n${personaIntro}`);
|
|
342
|
+
if (includeContextBlock && globalContext) sections.push(`Project context:\n${globalContext}`);
|
|
343
|
+
if (includeContextBlock && stepContext) sections.push(`Step-specific context:\n${stepContext}`);
|
|
344
|
+
if (conversation) sections.push(`Conversation so far:\n${conversation}`);
|
|
345
|
+
|
|
346
|
+
const heading = instructionTitle
|
|
347
|
+
? `${instructionLabel} -> ${agentLabel} (${instructionTitle})`
|
|
348
|
+
: `${instructionLabel} -> ${agentLabel}`;
|
|
349
|
+
|
|
350
|
+
sections.push(`${heading}:\n${instruction}`);
|
|
351
|
+
sections.push(`${agentLabel}:`);
|
|
352
|
+
|
|
353
|
+
return sections.join('\n\n');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ===== AI EXECUTION =====
|
|
357
|
+
|
|
358
|
+
async function callAi(aiConfig, prompt, livePrinter) {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
const proc = spawn(aiConfig.cmd, aiConfig.args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
361
|
+
let stdout = '';
|
|
362
|
+
let stderr = '';
|
|
363
|
+
let pendingLine = '';
|
|
364
|
+
|
|
365
|
+
proc.stdout.on('data', chunk => {
|
|
366
|
+
const text = chunk.toString();
|
|
367
|
+
stdout += text;
|
|
368
|
+
pendingLine += text;
|
|
369
|
+
let index;
|
|
370
|
+
while ((index = pendingLine.indexOf('\n')) !== -1) {
|
|
371
|
+
const line = pendingLine.slice(0, index);
|
|
372
|
+
pendingLine = pendingLine.slice(index + 1);
|
|
373
|
+
livePrinter(line);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
proc.stderr.on('data', chunk => {
|
|
378
|
+
stderr += chunk.toString();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
proc.on('close', code => {
|
|
382
|
+
if (pendingLine.length) {
|
|
383
|
+
livePrinter(pendingLine);
|
|
384
|
+
pendingLine = '';
|
|
385
|
+
}
|
|
386
|
+
if (code === 0) {
|
|
387
|
+
resolve(stdout.trim());
|
|
388
|
+
} else {
|
|
389
|
+
reject(new Error(stderr.trim() || `AI command exited with status ${code}`));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
proc.stdin.write(prompt);
|
|
394
|
+
proc.stdin.end();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ===== MAIN =====
|
|
399
|
+
|
|
400
|
+
async function run() {
|
|
401
|
+
const args = parseArgs(process.argv);
|
|
402
|
+
const workspaceArg = args.workspace || args['workspace-dir'] || args['working-directory'] || args.w;
|
|
403
|
+
|
|
404
|
+
if (!workspaceArg) {
|
|
405
|
+
console.error('Missing required --workspace argument.');
|
|
406
|
+
console.error('Usage: echelon-pair-program --workspace <path> --prompt "task"');
|
|
407
|
+
console.error(' echelon-pair-program -w my-project --prompt "build auth" --agents knoxis,solan');
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let workspace = resolveWorkspacePath(workspaceArg);
|
|
412
|
+
if (!workspace) {
|
|
413
|
+
workspace = path.resolve(workspaceArg);
|
|
414
|
+
console.log(`Creating new workspace: ${workspace}`);
|
|
415
|
+
}
|
|
416
|
+
if (!fs.existsSync(workspace)) {
|
|
417
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Parse timeline (custom steps from backend)
|
|
421
|
+
const timeline = args['timeline-base64'] ? decodeJsonBase64(args['timeline-base64'], 'timeline') : null;
|
|
422
|
+
|
|
423
|
+
// Parse task
|
|
424
|
+
let task = args['prompt-base64'] ? decodeBase64(args['prompt-base64']) : args.prompt;
|
|
425
|
+
if ((!task || !task.trim()) && timeline && typeof timeline.task === 'string') {
|
|
426
|
+
task = timeline.task;
|
|
427
|
+
}
|
|
428
|
+
if (!task || !task.trim()) {
|
|
429
|
+
console.error('A task prompt is required via --prompt, --prompt-base64, or timeline.task.');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
task = task.trim();
|
|
433
|
+
|
|
434
|
+
// Resolve AI provider
|
|
435
|
+
const aiConfig = resolveAiProvider(args['ai-provider'] || args.provider);
|
|
436
|
+
|
|
437
|
+
// Resolve which agents are active
|
|
438
|
+
let activeAgents;
|
|
439
|
+
const agentArg = args.agents;
|
|
440
|
+
if (agentArg) {
|
|
441
|
+
const requested = (Array.isArray(agentArg) ? agentArg.join(',') : agentArg)
|
|
442
|
+
.split(',')
|
|
443
|
+
.map(s => s.trim().toLowerCase())
|
|
444
|
+
.filter(Boolean);
|
|
445
|
+
activeAgents = requested.map(id => getAgent(id)).filter(Boolean);
|
|
446
|
+
} else if (timeline && Array.isArray(timeline.agents)) {
|
|
447
|
+
activeAgents = timeline.agents.map(id => getAgent(id)).filter(Boolean);
|
|
448
|
+
} else {
|
|
449
|
+
activeAgents = getAllAgents();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (activeAgents.length === 0) {
|
|
453
|
+
console.error('No valid agents specified. Available: ' + getAgentIds().join(', '));
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Gather context files
|
|
458
|
+
const globalContextInputs = [];
|
|
459
|
+
if (args['context']) globalContextInputs.push(args['context']);
|
|
460
|
+
if (args['context-file']) globalContextInputs.push(args['context-file']);
|
|
461
|
+
if (timeline && Array.isArray(timeline.sharedContext)) {
|
|
462
|
+
globalContextInputs.push(timeline.sharedContext);
|
|
463
|
+
}
|
|
464
|
+
const globalContext = gatherContext(workspace, globalContextInputs);
|
|
465
|
+
const globalContextBlock = globalContext.sections.join('\n\n');
|
|
466
|
+
|
|
467
|
+
// Build steps
|
|
468
|
+
let scheduledSteps;
|
|
469
|
+
if (timeline && Array.isArray(timeline.steps) && timeline.steps.length) {
|
|
470
|
+
// Custom timeline from backend
|
|
471
|
+
scheduledSteps = timeline.steps.map((step, index) => ({
|
|
472
|
+
key: step.key || `step-${index + 1}`,
|
|
473
|
+
title: step.title || '',
|
|
474
|
+
agentId: step.agentId || step.displayName?.toLowerCase() || activeAgents[0].id,
|
|
475
|
+
displayName: step.displayName || step.agentId || `Agent ${index + 1}`,
|
|
476
|
+
persona: step.persona || null,
|
|
477
|
+
instruction: typeof step.instruction === 'string' && step.instruction.trim().length
|
|
478
|
+
? step.instruction
|
|
479
|
+
: `Proceed with the shared task: ${task}`,
|
|
480
|
+
contextPaths: step.contextFiles || [],
|
|
481
|
+
includeContext: Boolean(step.includeContext)
|
|
482
|
+
}));
|
|
483
|
+
} else {
|
|
484
|
+
// Smart default timeline
|
|
485
|
+
scheduledSteps = buildDefaultTimeline(task, activeAgents);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!scheduledSteps.length) {
|
|
489
|
+
console.error('No steps configured for the session.');
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Disable recording with --no-record
|
|
494
|
+
const recordingEnabled = !args['no-record'];
|
|
495
|
+
|
|
496
|
+
// Initialize session recorder
|
|
497
|
+
let recorder = null;
|
|
498
|
+
if (recordingEnabled) {
|
|
499
|
+
recorder = new SessionRecorder({
|
|
500
|
+
task,
|
|
501
|
+
workspace,
|
|
502
|
+
agents: activeAgents.map(a => a.id),
|
|
503
|
+
aiProvider: aiConfig.label
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Print session header
|
|
508
|
+
const agentNames = activeAgents.map(a => colorize(a.id, a.name)).join(', ');
|
|
509
|
+
console.log('==============================================');
|
|
510
|
+
console.log('Echelon Dev Team - Pair Programming Session');
|
|
511
|
+
console.log(`Workspace: ${workspace}`);
|
|
512
|
+
console.log(`AI Partner: ${aiConfig.label}`);
|
|
513
|
+
console.log(`Team: ${agentNames}${RESET}`);
|
|
514
|
+
console.log(`Task: ${task}`);
|
|
515
|
+
if (recordingEnabled) console.log(`Recording: ON`);
|
|
516
|
+
console.log('==============================================');
|
|
517
|
+
console.log('');
|
|
518
|
+
|
|
519
|
+
if (globalContext.labels.length) {
|
|
520
|
+
console.log(`Shared context files: ${globalContext.labels.join(', ')}`);
|
|
521
|
+
console.log('');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Build team system prompt
|
|
525
|
+
const systemIntro = buildTeamSystemPrompt(activeAgents);
|
|
526
|
+
|
|
527
|
+
// Execute steps
|
|
528
|
+
const history = [];
|
|
529
|
+
const conversationLines = () => history.map(entry => `${entry.role}: ${entry.content}`).join('\n\n');
|
|
530
|
+
|
|
531
|
+
for (let i = 0; i < scheduledSteps.length; i++) {
|
|
532
|
+
const step = scheduledSteps[i];
|
|
533
|
+
const agent = getAgent(step.agentId);
|
|
534
|
+
|
|
535
|
+
// Gather step-specific context
|
|
536
|
+
const stepContext = gatherContext(workspace, step.contextPaths || []);
|
|
537
|
+
if (stepContext.labels.length) {
|
|
538
|
+
console.log(`Step context for ${step.displayName}: ${stepContext.labels.join(', ')}`);
|
|
539
|
+
console.log('');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Print step header
|
|
543
|
+
const titleSuffix = step.title ? ` (${step.title})` : '';
|
|
544
|
+
const agentColor = agent ? agent.color : '';
|
|
545
|
+
console.log(`Coordinator -> ${agentColor}${step.displayName}${RESET}${titleSuffix}:`);
|
|
546
|
+
console.log(step.instruction);
|
|
547
|
+
console.log('');
|
|
548
|
+
|
|
549
|
+
history.push({ role: 'Coordinator', content: step.instruction });
|
|
550
|
+
|
|
551
|
+
console.log(`${agentColor}${step.displayName}${RESET}:`);
|
|
552
|
+
|
|
553
|
+
// Build prompt
|
|
554
|
+
const includeContext = history.length <= 2 || step.includeContext || (stepContext.sections.length > 0);
|
|
555
|
+
const prompt = buildPrompt({
|
|
556
|
+
systemIntro,
|
|
557
|
+
personaIntro: step.persona,
|
|
558
|
+
conversation: conversationLines(),
|
|
559
|
+
instructionLabel: 'Coordinator',
|
|
560
|
+
instructionTitle: step.title,
|
|
561
|
+
instruction: step.instruction,
|
|
562
|
+
includeContextBlock: includeContext,
|
|
563
|
+
globalContext: globalContextBlock,
|
|
564
|
+
stepContext: stepContext.sections.join('\n\n'),
|
|
565
|
+
agentLabel: step.displayName
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Record step
|
|
569
|
+
let stepIndex = -1;
|
|
570
|
+
if (recorder) {
|
|
571
|
+
stepIndex = recorder.startStep(step.key, step.agentId, step.instruction);
|
|
572
|
+
recorder.setStepPrompt(stepIndex, prompt);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const response = await callAi(aiConfig, prompt, line => {
|
|
577
|
+
console.log(` ${line}`);
|
|
578
|
+
});
|
|
579
|
+
history.push({ role: step.displayName, content: response });
|
|
580
|
+
|
|
581
|
+
if (recorder) {
|
|
582
|
+
recorder.completeStep(stepIndex, response, null);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
console.log('');
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error(`Failed to complete step "${step.title || step.key}": ${err.message}`);
|
|
588
|
+
|
|
589
|
+
if (recorder) {
|
|
590
|
+
recorder.completeStep(stepIndex, null, err.message);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Save recording
|
|
598
|
+
if (recorder) {
|
|
599
|
+
const recordPath = recorder.save();
|
|
600
|
+
console.log(`Session recorded: ${recordPath}`);
|
|
601
|
+
console.log('');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
console.log('Session complete. The Echelon dev team is standing by for further instructions.');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
run().catch(err => {
|
|
608
|
+
console.error(err.message);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
});
|