dual-brain 4.1.0 → 4.5.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.
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-chains.mjs — Opinionated multi-step agent workflows.
4
+ *
5
+ * Three built-in chains that map to how developers actually work:
6
+ *
7
+ * explore-then-fix — Understand the problem, then fix it
8
+ * review-and-test — Review the code, then write tests for weak spots
9
+ * audit-and-plan — Audit the architecture, then create an execution plan
10
+ *
11
+ * Each chain:
12
+ * 1. Prints a banner showing the steps
13
+ * 2. Runs step 1 (capture output)
14
+ * 3. Prints step 1 results
15
+ * 4. Asks for confirmation (or auto-continues with --yes)
16
+ * 5. Runs step 2 with step 1 output as context
17
+ * 6. Prints final results
18
+ *
19
+ * Export: getChain(name), listChains()
20
+ * CLI: node agent-chains.mjs --list
21
+ * node agent-chains.mjs --run <chain> [flags]
22
+ */
23
+
24
+ import { spawnSync } from 'child_process';
25
+ import { createInterface } from 'readline';
26
+ import { dirname, resolve } from 'path';
27
+ import { fileURLToPath } from 'url';
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const TEMPLATES_SCRIPT = resolve(__dirname, 'agent-templates.mjs');
31
+
32
+ // ─── Chain Definitions ─────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Built-in chains.
36
+ *
37
+ * Each chain has:
38
+ * name - unique identifier
39
+ * description - one-line description
40
+ * steps - array of step descriptors
41
+ * step.label - human-readable step label
42
+ * step.template - agent-templates.mjs template name
43
+ * step.tier - search | execute | think
44
+ * step.model - haiku | sonnet | opus
45
+ * step.args - function(flags, prevOutput) → args object for the template
46
+ * step.stop_after - if true, pause here for confirmation before continuing
47
+ * step.stop_label - label for the stop-point prompt
48
+ */
49
+ export const CHAINS = {
50
+ 'explore-then-fix': {
51
+ name: 'explore-then-fix',
52
+ description: 'Understand the problem, then fix it',
53
+ steps: [
54
+ {
55
+ label: 'Step 1: Explore',
56
+ template: 'explorer',
57
+ tier: 'search',
58
+ model: 'haiku',
59
+ args: (flags) => ({
60
+ question: flags.question,
61
+ scope: flags.scope,
62
+ }),
63
+ stop_after: true,
64
+ stop_label: 'Exploration complete. Review findings above, then confirm the fix.',
65
+ },
66
+ {
67
+ label: 'Step 2: Fix',
68
+ template: 'bug-hunter',
69
+ tier: 'execute',
70
+ model: 'sonnet',
71
+ args: (flags, prevOutput) => ({
72
+ question: flags.question,
73
+ scope: flags.scope,
74
+ context: prevOutput,
75
+ }),
76
+ },
77
+ ],
78
+ },
79
+
80
+ 'review-and-test': {
81
+ name: 'review-and-test',
82
+ description: 'Review the code, then write tests for weak spots',
83
+ steps: [
84
+ {
85
+ label: 'Step 1: Security Review',
86
+ template: 'security-review',
87
+ tier: 'think',
88
+ model: 'opus',
89
+ args: (flags) => ({
90
+ scope: flags.scope,
91
+ file: flags.file,
92
+ }),
93
+ stop_after: true,
94
+ stop_label: 'Review complete. Review findings above, then confirm test writing.',
95
+ },
96
+ {
97
+ label: 'Step 2: Write Tests',
98
+ template: 'test-writer',
99
+ tier: 'execute',
100
+ model: 'sonnet',
101
+ args: (flags, prevOutput) => ({
102
+ scope: flags.scope,
103
+ file: flags.file,
104
+ context: prevOutput,
105
+ }),
106
+ },
107
+ ],
108
+ },
109
+
110
+ 'audit-and-plan': {
111
+ name: 'audit-and-plan',
112
+ description: 'Audit the architecture, then create an execution plan',
113
+ steps: [
114
+ {
115
+ label: 'Step 1: Architecture Audit',
116
+ template: 'explorer',
117
+ tier: 'search',
118
+ model: 'haiku',
119
+ args: (flags) => ({
120
+ question: flags.question,
121
+ scope: flags.scope,
122
+ }),
123
+ stop_after: true,
124
+ stop_label: 'Audit complete. Review findings above, then confirm planning.',
125
+ },
126
+ {
127
+ label: 'Step 2: Create Plan',
128
+ template: null, // Think tier — runs via plan-generator concept inline
129
+ tier: 'think',
130
+ model: 'opus',
131
+ args: (flags, prevOutput) => ({
132
+ question: flags.question,
133
+ scope: flags.scope,
134
+ context: prevOutput,
135
+ }),
136
+ // Uses a custom prompt builder instead of an agent template
137
+ custom_prompt: (flags, prevOutput) => {
138
+ const scopeLine = flags.scope ? `\nScope: ${flags.scope}` : '';
139
+ return [
140
+ `You are an architect. Based on the audit findings below, create a detailed execution plan.`,
141
+ ``,
142
+ `Question / Goal: ${flags.question || 'Improve the architecture based on audit findings'}`,
143
+ scopeLine,
144
+ ``,
145
+ `Audit findings:`,
146
+ prevOutput,
147
+ ``,
148
+ `Produce a structured plan with:`,
149
+ `- Decision: the recommended approach`,
150
+ `- Rationale: why this approach over alternatives`,
151
+ `- Alternatives considered (and why rejected)`,
152
+ `- Risks and mitigations`,
153
+ `- Verification plan: how to confirm success`,
154
+ `- Task table: dependency-ordered list of concrete tasks with tier/risk`,
155
+ ].join('\n');
156
+ },
157
+ },
158
+ ],
159
+ },
160
+ };
161
+
162
+ // ─── Exports ───────────────────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Get a chain by name. Returns null if not found.
166
+ */
167
+ export function getChain(name) {
168
+ return CHAINS[name] || null;
169
+ }
170
+
171
+ /**
172
+ * List all chains as { name, description, steps }.
173
+ */
174
+ export function listChains() {
175
+ return Object.values(CHAINS).map(({ name, description, steps }) => ({
176
+ name,
177
+ description,
178
+ steps: steps.map(s => ({
179
+ label: s.label,
180
+ template: s.template,
181
+ tier: s.tier,
182
+ model: s.model,
183
+ })),
184
+ }));
185
+ }
186
+
187
+ // ─── Chain Execution ───────────────────────────────────────────────────────
188
+
189
+ function banner(chain) {
190
+ const width = 60;
191
+ const line = '─'.repeat(width);
192
+ console.log(`\n ${line}`);
193
+ console.log(` Chain: ${chain.name} — ${chain.description}`);
194
+ console.log(` ${line}`);
195
+ for (let i = 0; i < chain.steps.length; i++) {
196
+ const s = chain.steps[i];
197
+ const model = s.model || (s.tier === 'think' ? 'opus' : s.tier === 'search' ? 'haiku' : 'sonnet');
198
+ const tmpl = s.template || 'custom';
199
+ console.log(` ${i + 1}. ${s.label} [${s.tier} / ${model} / ${tmpl}]`);
200
+ }
201
+ console.log(` ${line}\n`);
202
+ }
203
+
204
+ function separator(label) {
205
+ const width = 60;
206
+ const line = '─'.repeat(width);
207
+ console.log(`\n ${line}`);
208
+ if (label) console.log(` ${label}`);
209
+ console.log(` ${line}\n`);
210
+ }
211
+
212
+ function runTemplate(templateName, templateArgs) {
213
+ const argsList = ['--run', templateName];
214
+ if (templateArgs.question) argsList.push('--question', templateArgs.question);
215
+ if (templateArgs.scope) argsList.push('--scope', templateArgs.scope);
216
+ if (templateArgs.file) argsList.push('--file', templateArgs.file);
217
+ if (templateArgs.context) argsList.push('--context', templateArgs.context);
218
+
219
+ const result = spawnSync(process.execPath, [TEMPLATES_SCRIPT, ...argsList], {
220
+ encoding: 'utf8',
221
+ stdio: ['pipe', 'pipe', 'inherit'],
222
+ timeout: 30_000,
223
+ });
224
+
225
+ return {
226
+ output: result.stdout || '',
227
+ status: result.status,
228
+ stderr: result.stderr || '',
229
+ };
230
+ }
231
+
232
+ function runCustomStep(step, flags, prevOutput) {
233
+ const prompt = step.custom_prompt(flags, prevOutput);
234
+ const model = step.model || 'opus';
235
+ const tier = step.tier || 'think';
236
+
237
+ console.log(`\n [${step.label}] tier=${tier} model=${model} custom\n`);
238
+ console.log('─'.repeat(60));
239
+ console.log(prompt);
240
+ console.log('─'.repeat(60));
241
+
242
+ return {
243
+ output: prompt,
244
+ status: 0,
245
+ };
246
+ }
247
+
248
+ async function askConfirmation(stopLabel) {
249
+ console.log(`\n ${stopLabel}`);
250
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
251
+ return new Promise((resolve) => {
252
+ rl.question('\n Continue to step 2? [Y/n] ', (answer) => {
253
+ rl.close();
254
+ const ans = answer.trim().toLowerCase();
255
+ resolve(ans === '' || ans === 'y' || ans === 'yes');
256
+ });
257
+ });
258
+ }
259
+
260
+ async function executeChain(chain, flags) {
261
+ banner(chain);
262
+
263
+ let prevOutput = '';
264
+
265
+ for (let i = 0; i < chain.steps.length; i++) {
266
+ const step = chain.steps[i];
267
+ const isLast = i === chain.steps.length - 1;
268
+
269
+ console.log(`\n Running ${step.label}...`);
270
+
271
+ let result;
272
+ if (step.template) {
273
+ const templateArgs = step.args(flags, prevOutput);
274
+ result = runTemplate(step.template, templateArgs);
275
+ } else if (step.custom_prompt) {
276
+ result = runCustomStep(step, flags, prevOutput);
277
+ } else {
278
+ console.error(` Step "${step.label}" has no template or custom_prompt.`);
279
+ process.exit(1);
280
+ }
281
+
282
+ if (result.status !== 0) {
283
+ console.error(`\n Step failed (exit ${result.status}). Chain aborted.`);
284
+ process.exit(result.status || 1);
285
+ }
286
+
287
+ // Print step output
288
+ separator(`Results: ${step.label}`);
289
+ if (result.output.trim()) {
290
+ console.log(result.output);
291
+ } else {
292
+ console.log(' (no output)');
293
+ }
294
+
295
+ prevOutput = result.output;
296
+
297
+ // Stop point: ask for confirmation before next step
298
+ if (step.stop_after && !isLast) {
299
+ if (flags.yes) {
300
+ console.log(`\n [--yes] Auto-continuing to next step...`);
301
+ } else {
302
+ const confirmed = await askConfirmation(step.stop_label || `Step ${i + 1} complete.`);
303
+ if (!confirmed) {
304
+ console.log('\n Aborted by user. Findings saved above.\n');
305
+ process.exit(0);
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ separator('Chain Complete');
312
+ console.log(' All steps finished. Review results above.\n');
313
+ }
314
+
315
+ // ─── CLI ───────────────────────────────────────────────────────────────────
316
+
317
+ function parseArgs(argv) {
318
+ const flags = { yes: false };
319
+ for (let i = 0; i < argv.length; i++) {
320
+ const a = argv[i];
321
+ if (a === '--list') flags.list = true;
322
+ else if (a === '--run') flags.run = argv[++i];
323
+ else if (a === '--question') flags.question = argv[++i];
324
+ else if (a === '--scope') flags.scope = argv[++i];
325
+ else if (a === '--file') flags.file = argv[++i];
326
+ else if (a === '--yes' || a === '-y') flags.yes = true;
327
+ }
328
+ return flags;
329
+ }
330
+
331
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
332
+ const flags = parseArgs(process.argv.slice(2));
333
+
334
+ if (flags.list) {
335
+ console.log('\n Agent Chains:\n');
336
+ for (const c of listChains()) {
337
+ console.log(` ${c.name.padEnd(22)} ${c.description}`);
338
+ for (const s of c.steps) {
339
+ const tmpl = s.template || 'custom';
340
+ console.log(` ${s.label.padEnd(30)} [${s.tier}/${s.model}/${tmpl}]`);
341
+ }
342
+ console.log('');
343
+ }
344
+ process.exit(0);
345
+ }
346
+
347
+ if (flags.run) {
348
+ const chain = getChain(flags.run);
349
+ if (!chain) {
350
+ console.error(` Unknown chain: ${flags.run}`);
351
+ console.error(` Available: ${Object.keys(CHAINS).join(', ')}`);
352
+ process.exit(1);
353
+ }
354
+
355
+ executeChain(chain, flags).catch((err) => {
356
+ console.error(` Chain error: ${err.message}`);
357
+ process.exit(1);
358
+ });
359
+ } else {
360
+ console.log(`
361
+ Usage:
362
+ node agent-chains.mjs --list
363
+ node agent-chains.mjs --run explore-then-fix --question "what's wrong with auth" [--scope "src/auth"] [--yes]
364
+ node agent-chains.mjs --run review-and-test [--scope "src/api"] [--file "src/api.ts"] [--yes]
365
+ node agent-chains.mjs --run audit-and-plan --question "how should we restructure auth" [--scope "src/"] [--yes]
366
+ `);
367
+ process.exit(0);
368
+ }
369
+ }