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.
- package/CLAUDE.md +130 -35
- package/README.md +179 -34
- package/hooks/agent-chains.mjs +369 -0
- package/hooks/agent-templates.mjs +441 -0
- package/hooks/atomic-write.mjs +109 -0
- package/hooks/config-validator.mjs +156 -0
- package/hooks/confirmation-policy.mjs +167 -0
- package/hooks/cost-logger.mjs +53 -11
- package/hooks/cost-report.mjs +60 -114
- package/hooks/decision-ledger.mjs +53 -3
- package/hooks/dual-brain-review.mjs +249 -2
- package/hooks/dual-brain-think.mjs +294 -25
- package/hooks/enforce-tier.mjs +263 -91
- package/hooks/error-channel.mjs +68 -0
- package/hooks/failure-detector.mjs +3 -1
- package/hooks/health-check.mjs +16 -17
- package/hooks/risk-classifier.mjs +135 -2
- package/hooks/session-report.mjs +41 -71
- package/hooks/ship-captain.mjs +1176 -0
- package/hooks/ship-gate.mjs +971 -0
- package/hooks/summary-checkpoint.mjs +35 -8
- package/hooks/test-orchestrator.mjs +2080 -31
- package/install.mjs +1185 -30
- package/orchestrator.json +73 -96
- package/package.json +7 -2
|
@@ -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
|
+
}
|