dual-brain 4.6.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,441 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * agent-templates.mjs — Pre-built specialist agent templates for dual-brain.
4
- *
5
- * Exports:
6
- * TEMPLATES — all template definitions
7
- * getTemplate(name) → template object or null
8
- * listTemplates() → [{name, description, tier, risk}]
9
- * buildAgentPrompt(templateName, flags) → { prompt, model, description, risk, quality_gate, tier, output_contract }
10
- * buildPrompt(templateName, args) → prompt string (legacy compat)
11
- *
12
- * CLI:
13
- * node agent-templates.mjs --list
14
- * node agent-templates.mjs --template explorer --question "where is auth handled?"
15
- * node agent-templates.mjs --run explorer --question "where is auth handled?"
16
- *
17
- * Via npx dual-brain:
18
- * npx dual-brain agents # list all templates
19
- * npx dual-brain agent explorer --question "..."
20
- */
21
-
22
- import { spawnSync } from 'child_process';
23
- import { dirname, resolve } from 'path';
24
- import { fileURLToPath } from 'url';
25
-
26
- const __dirname = dirname(fileURLToPath(import.meta.url));
27
-
28
- // ─── Model Mapping ─────────────────────────────────────────────────────────
29
-
30
- /** Maps tier name → Claude model ID */
31
- const TIER_MODELS = {
32
- search: 'claude-haiku-4-5',
33
- execute: 'claude-sonnet-4-5',
34
- think: 'claude-opus-4-5',
35
- };
36
-
37
- // ─── Template Definitions ──────────────────────────────────────────────────
38
-
39
- /**
40
- * Built-in agent templates.
41
- *
42
- * Fields:
43
- * name - unique slug (also used as CLI argument)
44
- * tier - search | execute | think
45
- * model - haiku | sonnet | opus (short form, for display)
46
- * risk - low | medium | high | critical
47
- * quality_gate - self_check | review_recommended | dual_brain_review
48
- * description - one-line shown in --list
49
- * output_contract - array of items the agent MUST return
50
- * flags - { '--flag': { required, description } }
51
- * prompt_template - string with {var} tokens OR function(args) → string
52
- */
53
- export const TEMPLATES = {
54
- explorer: {
55
- name: 'explorer',
56
- tier: 'search',
57
- model: 'haiku',
58
- risk: 'low',
59
- quality_gate: 'self_check',
60
- description: 'Read-only codebase exploration — find files, symbols, and patterns',
61
- output_contract: [
62
- 'files found (with line numbers)',
63
- 'key symbols/functions discovered',
64
- 'confidence level (high/medium/low)',
65
- 'areas you did NOT check',
66
- ],
67
- flags: {
68
- '--question': { required: true, description: 'What to explore or find' },
69
- '--scope': { required: false, description: 'Limit search to a path (e.g. src/auth)' },
70
- },
71
- prompt_template:
72
- 'You are a codebase exploration agent. Your task: {question}. ' +
73
- '{scope ? \'Focus on: \' + scope : \'Search the entire codebase.\'}. ' +
74
- 'Return a structured report with: ' +
75
- '1) Files found (with line numbers), ' +
76
- '2) Key symbols/functions discovered, ' +
77
- '3) Confidence level (high/medium/low), ' +
78
- '4) Areas you did NOT check. ' +
79
- 'Do not modify any files.',
80
- },
81
-
82
- 'security-review': {
83
- name: 'security-review',
84
- tier: 'think',
85
- model: 'opus',
86
- risk: 'high',
87
- quality_gate: 'dual_brain_review',
88
- description: 'Security audit agent — OWASP Top 10 scan with severity-filtered findings',
89
- output_contract: [
90
- 'vulnerabilities found (severity)',
91
- 'recommendations',
92
- 'files reviewed',
93
- 'OWASP categories checked',
94
- ],
95
- flags: {
96
- '--scope': { required: false, description: 'Path to audit (e.g. src/auth); defaults to entire codebase' },
97
- '--severity': { required: false, description: 'Minimum severity to report: critical|high|medium|low' },
98
- },
99
- prompt_template:
100
- 'You are a security review agent. ' +
101
- 'Audit {scope || \'the current codebase\'} for vulnerabilities. ' +
102
- 'Check OWASP Top 10 categories. ' +
103
- 'For each finding report: severity (critical/high/medium/low), file and line, description, remediation. ' +
104
- '{severity ? \'Only report \' + severity + \' severity and above.\' : \'\'} ' +
105
- 'Do not modify any files.',
106
- },
107
-
108
- 'test-writer': {
109
- name: 'test-writer',
110
- tier: 'execute',
111
- model: 'sonnet',
112
- risk: 'medium',
113
- quality_gate: 'review_recommended',
114
- description: 'Test generation agent — comprehensive tests with edge cases',
115
- output_contract: [
116
- 'test files created',
117
- 'coverage areas',
118
- 'edge cases covered',
119
- 'assumptions made',
120
- ],
121
- flags: {
122
- '--file': { required: true, description: 'Source file to generate tests for (e.g. src/api.ts)' },
123
- '--framework': { required: false, description: 'Test framework to use (e.g. jest); auto-detect if omitted' },
124
- },
125
- prompt_template:
126
- 'You are a test writing agent. Generate comprehensive tests for {file}. ' +
127
- '{framework ? \'Use \' + framework + \' framework.\' : \'Auto-detect the test framework from the project.\'} ' +
128
- 'Cover: happy path, edge cases, error handling, boundary conditions. ' +
129
- 'Create the test file adjacent to the source. ' +
130
- 'Return: files created, test count, coverage areas, assumptions made.',
131
- },
132
-
133
- 'bug-hunter': {
134
- name: 'bug-hunter',
135
- tier: 'execute',
136
- model: 'sonnet',
137
- risk: 'medium',
138
- quality_gate: 'review_recommended',
139
- description: 'Bug finding agent — logic errors, race conditions, edge cases',
140
- output_contract: [
141
- 'bugs found (severity)',
142
- 'reproduction steps',
143
- 'suggested fixes',
144
- 'files examined',
145
- ],
146
- flags: {
147
- '--area': { required: true, description: 'Code area to hunt bugs in (e.g. payments)' },
148
- '--depth': { required: false, description: 'Analysis depth: normal (default) or deep' },
149
- },
150
- prompt_template:
151
- 'You are a bug hunting agent. ' +
152
- 'Search {area} for bugs, logic errors, race conditions, and edge cases. ' +
153
- '{depth === \'deep\' ? \'Do an exhaustive analysis including data flow tracing.\' : \'Focus on the most likely problem areas.\'} ' +
154
- 'For each bug found report: severity, file and line, description, reproduction scenario, suggested fix. ' +
155
- 'Do not modify any files.',
156
- },
157
- };
158
-
159
- // ─── Public API ─────────────────────────────────────────────────────────────
160
-
161
- /**
162
- * Return the template object for the given name, or null if not found.
163
- * @param {string} name
164
- * @returns {object|null}
165
- */
166
- export function getTemplate(name) {
167
- return TEMPLATES[name] ?? null;
168
- }
169
-
170
- /**
171
- * Return a summary list of all templates.
172
- * @returns {{name: string, description: string, tier: string, risk: string, quality_gate: string, model: string}[]}
173
- */
174
- export function listTemplates() {
175
- return Object.values(TEMPLATES).map(({ name, description, tier, risk, quality_gate, model }) => ({
176
- name,
177
- description,
178
- tier,
179
- model,
180
- risk,
181
- quality_gate,
182
- }));
183
- }
184
-
185
- /**
186
- * Build an agent prompt config by interpolating flags into the prompt template.
187
- *
188
- * Supports both string templates (with {var} tokens that may contain JS
189
- * expressions) and function templates (legacy, for backwards compatibility).
190
- *
191
- * @param {string} templateName
192
- * @param {Record<string, string>} flags e.g. { question: 'where is auth?', scope: 'src/auth' }
193
- * @returns {{ prompt, model, description, risk, quality_gate, tier, output_contract }|null}
194
- */
195
- export function buildAgentPrompt(templateName, flags = {}) {
196
- const tmpl = getTemplate(templateName);
197
- if (!tmpl) return null;
198
-
199
- const {
200
- question = '',
201
- scope = '',
202
- severity = '',
203
- framework = '',
204
- file = '',
205
- area = '',
206
- depth = 'normal',
207
- context = '',
208
- } = flags;
209
-
210
- let prompt;
211
-
212
- if (typeof tmpl.prompt_template === 'function') {
213
- // Legacy function-style template
214
- prompt = tmpl.prompt_template({ question, scope, severity, framework, file, area, depth, context });
215
- } else {
216
- // String template: replace {expr} tokens with evaluated JS expressions.
217
- // We convert {expr} → ${expr} and evaluate via template literal inside a
218
- // Function scope that has all flag vars bound. Fallback to naive {var}
219
- // replacement if the Function evaluation throws.
220
- try {
221
- // eslint-disable-next-line no-new-func
222
- const interpolate = new Function(
223
- 'question', 'scope', 'severity', 'framework', 'file', 'area', 'depth', 'context',
224
- // Escape backticks and backslashes in the template before wrapping
225
- `return \`${tmpl.prompt_template
226
- .replace(/\\/g, '\\\\')
227
- .replace(/`/g, '\\`')
228
- .replace(/\{/g, '${')
229
- }\`;`
230
- );
231
- prompt = interpolate(question, scope, severity, framework, file, area, depth, context);
232
- } catch {
233
- // Fallback: naive {var} token replacement
234
- prompt = tmpl.prompt_template
235
- .replace(/\{question\}/g, question)
236
- .replace(/\{scope\}/g, scope)
237
- .replace(/\{severity\}/g, severity)
238
- .replace(/\{framework\}/g, framework)
239
- .replace(/\{file\}/g, file)
240
- .replace(/\{area\}/g, area)
241
- .replace(/\{depth\}/g, depth)
242
- .replace(/\{context\}/g, context);
243
- }
244
- }
245
-
246
- // Normalise whitespace
247
- prompt = prompt.replace(/\s+/g, ' ').trim();
248
-
249
- const modelId = TIER_MODELS[tmpl.tier] || TIER_MODELS.execute;
250
-
251
- return {
252
- prompt,
253
- model: modelId,
254
- description: tmpl.description,
255
- risk: tmpl.risk,
256
- quality_gate: tmpl.quality_gate,
257
- tier: tmpl.tier,
258
- output_contract: tmpl.output_contract || [],
259
- };
260
- }
261
-
262
- /**
263
- * Legacy compat: build just the prompt string.
264
- * Use buildAgentPrompt() for new code.
265
- * @param {string} templateName
266
- * @param {object} args
267
- * @returns {string}
268
- */
269
- export function buildPrompt(templateName, args = {}) {
270
- const template = getTemplate(templateName);
271
- if (!template) throw new Error(`Unknown template: ${templateName}`);
272
- if (typeof template.prompt_template === 'function') {
273
- return template.prompt_template(args);
274
- }
275
- const config = buildAgentPrompt(templateName, args);
276
- return config ? config.prompt : '';
277
- }
278
-
279
- // ─── CLI Helpers ────────────────────────────────────────────────────────────
280
-
281
- function parseCliFlags(argv) {
282
- const flags = {};
283
- for (let i = 0; i < argv.length; i++) {
284
- const a = argv[i];
285
- if (a === '--list') { flags.list = true; continue; }
286
- if (a.startsWith('--')) {
287
- const key = a.slice(2);
288
- const next = argv[i + 1];
289
- if (next !== undefined && !next.startsWith('--')) {
290
- flags[key] = next;
291
- i++;
292
- } else {
293
- flags[key] = true;
294
- }
295
- }
296
- }
297
- return flags;
298
- }
299
-
300
- function printTable(templates) {
301
- const TIER_ICON = { search: '🔍', execute: '⚙️ ', think: '🧠' };
302
- const RISK_ICON = { low: '🟢', medium: '🟡', high: '🔴', critical: '🚨' };
303
-
304
- const maxName = Math.max(...templates.map(t => t.name.length), 14);
305
- const hr = '─'.repeat(maxName + 8 + 10 + 55);
306
-
307
- console.log('');
308
- console.log(' 🧠 dual-brain — Agent Templates');
309
- console.log(` ${hr}`);
310
- console.log(` ${'Template'.padEnd(maxName)} ${'Tier'.padEnd(9)} ${'Risk'.padEnd(8)} Description`);
311
- console.log(` ${hr}`);
312
-
313
- for (const t of templates) {
314
- const tierLabel = (TIER_ICON[t.tier] || ' ') + ' ' + t.tier;
315
- const riskLabel = (RISK_ICON[t.risk] || ' ') + ' ' + t.risk;
316
- console.log(
317
- ` ${t.name.padEnd(maxName)} ${tierLabel.padEnd(12)} ${riskLabel.padEnd(11)} ${t.description}`
318
- );
319
- }
320
-
321
- console.log(` ${hr}`);
322
- console.log('');
323
- console.log(' Usage:');
324
- console.log(' npx dual-brain agent <template> [flags]');
325
- console.log(' npx dual-brain agent explorer --question "where is auth handled?"');
326
- console.log(' npx dual-brain agent security-review --scope src/auth --severity high');
327
- console.log(' npx dual-brain agent test-writer --file src/api.ts --framework jest');
328
- console.log(' npx dual-brain agent bug-hunter --area payments --depth deep');
329
- console.log('');
330
- }
331
-
332
- function printAgentConfig(config, templateName) {
333
- console.log('');
334
- console.log(` Agent: ${templateName}`);
335
- console.log(` Tier: ${config.tier} → model: ${config.model}`);
336
- console.log(` Risk: ${config.risk}`);
337
- console.log(` Quality gate: ${config.quality_gate}`);
338
- console.log('');
339
- console.log(' Prompt:');
340
- console.log(' ' + '─'.repeat(60));
341
- const words = config.prompt.split(' ');
342
- let line = ' ';
343
- for (const w of words) {
344
- if ((line + w).length > 78) { console.log(line.trimEnd()); line = ' ' + w + ' '; }
345
- else line += w + ' ';
346
- }
347
- if (line.trim()) console.log(line.trimEnd());
348
- console.log(' ' + '─'.repeat(60));
349
- console.log('');
350
- if (config.output_contract && config.output_contract.length) {
351
- console.log(' Output contract (agent must return):');
352
- for (const item of config.output_contract) console.log(` • ${item}`);
353
- console.log('');
354
- }
355
- }
356
-
357
- function runAgent(templateName, flags) {
358
- const tmpl = getTemplate(templateName);
359
- if (!tmpl) {
360
- console.error(` Unknown template: ${templateName}`);
361
- console.error(` Available: ${Object.keys(TEMPLATES).join(', ')}`);
362
- console.error(` Run: npx dual-brain agents to see all templates`);
363
- process.exit(1);
364
- }
365
-
366
- // Validate required flags
367
- const missing = Object.entries(tmpl.flags || {})
368
- .filter(([flagName, meta]) => meta.required && !flags[flagName.slice(2)])
369
- .map(([flagName, meta]) => `${flagName} — ${meta.description}`);
370
-
371
- if (missing.length > 0) {
372
- console.error(` Missing required flags for "${templateName}":`);
373
- for (const m of missing) console.error(` ${m}`);
374
- process.exit(1);
375
- }
376
-
377
- const config = buildAgentPrompt(templateName, flags);
378
-
379
- console.log('');
380
- console.log(` 🧠 Spawning agent: ${templateName}`);
381
- console.log(` Tier: ${config.tier} | Model: ${config.model} | Risk: ${config.risk}`);
382
- console.log('');
383
-
384
- const claudeWhich = spawnSync('which', ['claude'], { encoding: 'utf8' });
385
- const claudeBin = (claudeWhich.status === 0 && claudeWhich.stdout.trim())
386
- ? claudeWhich.stdout.trim()
387
- : 'claude';
388
-
389
- const claudeArgs = ['-p', config.prompt, '--model', config.model];
390
-
391
- console.log(` Running: claude -p "<prompt>" --model ${config.model}`);
392
- console.log('');
393
-
394
- const { status } = spawnSync(claudeBin, claudeArgs, {
395
- stdio: 'inherit',
396
- cwd: resolve(process.cwd()),
397
- env: process.env,
398
- });
399
-
400
- process.exit(status ?? 0);
401
- }
402
-
403
- // ─── Main (direct execution only) ──────────────────────────────────────────
404
-
405
- if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
406
- const cliFlags = parseCliFlags(process.argv.slice(2));
407
-
408
- if (cliFlags.list) {
409
- printTable(listTemplates());
410
- process.exit(0);
411
- }
412
-
413
- // --template <name> → build and print the agent prompt config (dry-run)
414
- if (cliFlags.template) {
415
- const config = buildAgentPrompt(cliFlags.template, cliFlags);
416
- if (!config) {
417
- console.error(` Unknown template: ${cliFlags.template}`);
418
- console.error(` Available: ${Object.keys(TEMPLATES).join(', ')}`);
419
- process.exit(1);
420
- }
421
- printAgentConfig(config, cliFlags.template);
422
- process.exit(0);
423
- }
424
-
425
- // --run <name> → actually execute the agent
426
- if (cliFlags.run) {
427
- runAgent(cliFlags.run, cliFlags);
428
- // runAgent calls process.exit internally
429
- }
430
-
431
- // No recognised command
432
- console.log(`
433
- Usage:
434
- node agent-templates.mjs --list
435
- node agent-templates.mjs --template <name> [--flag value ...]
436
- node agent-templates.mjs --run <name> [--flag value ...]
437
-
438
- Templates: ${Object.keys(TEMPLATES).join(', ')}
439
- `);
440
- process.exit(0);
441
- }
@@ -1,109 +0,0 @@
1
- /**
2
- * atomic-write.mjs — Atomic file operations for the dual-brain orchestrator.
3
- *
4
- * Prevents race conditions in read-modify-write patterns under multi-session use.
5
- * No dependencies — uses only Node.js builtins.
6
- *
7
- * Exported API:
8
- * atomicWriteJSON(filePath, data) → write JSON atomically via tmp+rename
9
- * lockedReadModifyWrite(filePath, modifyFn, default) → locked read-modify-write cycle
10
- */
11
-
12
- import { openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, statSync } from 'fs';
13
- import { constants } from 'fs';
14
- import { logHookError } from './error-channel.mjs';
15
-
16
- const LOCK_TIMEOUT_MS = 5_000;
17
- const STALE_LOCK_MS = 10_000;
18
-
19
- /**
20
- * Atomically write JSON data to filePath using tmp-file + rename.
21
- * Tmp file is in the same directory to avoid cross-device rename issues.
22
- */
23
- export function atomicWriteJSON(filePath, data) {
24
- const tmp = filePath + '.tmp.' + process.pid;
25
- writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
26
- renameSync(tmp, filePath);
27
- }
28
-
29
- /**
30
- * Acquire a .lock file using O_EXCL for atomic creation.
31
- * Returns true if lock acquired, false otherwise.
32
- * Steals stale locks (older than STALE_LOCK_MS).
33
- */
34
- function acquireLock(lockPath) {
35
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
36
-
37
- while (Date.now() < deadline) {
38
- try {
39
- const fd = openSync(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
40
- writeFileSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
41
- closeSync(fd);
42
- return true;
43
- } catch (err) {
44
- if (err.code !== 'EEXIST') throw err;
45
-
46
- // Check for stale lock
47
- try {
48
- const stat = statSync(lockPath);
49
- if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
50
- // Stale lock — process likely died, steal it
51
- try { unlinkSync(lockPath); } catch {}
52
- continue;
53
- }
54
- } catch {
55
- // Lock disappeared between our check — retry
56
- continue;
57
- }
58
-
59
- // Wait briefly before retrying
60
- const waitMs = 10 + Math.floor(Math.random() * 20);
61
- const end = Date.now() + waitMs;
62
- while (Date.now() < end) { /* spin */ }
63
- }
64
- }
65
- return false;
66
- }
67
-
68
- function releaseLock(lockPath) {
69
- try { unlinkSync(lockPath); } catch {}
70
- }
71
-
72
- /**
73
- * Locked read-modify-write cycle.
74
- *
75
- * 1. Acquire .lock file (O_EXCL atomic creation)
76
- * 2. Read current JSON (or use defaultValue if missing/corrupt)
77
- * 3. Call modifyFn(currentData) → newData
78
- * 4. Atomic write newData via tmp+rename
79
- * 5. Release lock
80
- *
81
- * @param {string} filePath — JSON file to modify
82
- * @param {function} modifyFn — (currentData) => newData
83
- * @param {*} defaultValue — used if file doesn't exist or is corrupt
84
- */
85
- export function lockedReadModifyWrite(filePath, modifyFn, defaultValue = {}) {
86
- const lockPath = filePath + '.lock';
87
- const locked = acquireLock(lockPath);
88
-
89
- if (!locked) {
90
- const err = new Error(`Lock acquisition timed out after ${LOCK_TIMEOUT_MS}ms for ${filePath}`);
91
- logHookError('atomic-write', 'lockedReadModifyWrite', err, { filePath });
92
- throw err;
93
- }
94
-
95
- try {
96
- let current;
97
- try {
98
- current = JSON.parse(readFileSync(filePath, 'utf8'));
99
- } catch {
100
- current = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
101
- }
102
-
103
- const updated = modifyFn(current);
104
- atomicWriteJSON(filePath, updated);
105
- return updated;
106
- } finally {
107
- releaseLock(lockPath);
108
- }
109
- }