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.
- package/CLAUDE.md +35 -130
- package/README.md +34 -179
- package/hooks/control-panel.mjs +379 -8
- package/hooks/cost-logger.mjs +11 -53
- package/hooks/cost-report.mjs +126 -65
- package/hooks/decision-ledger.mjs +3 -53
- package/hooks/dual-brain-review.mjs +25 -261
- package/hooks/dual-brain-think.mjs +37 -300
- package/hooks/enforce-tier.mjs +93 -265
- package/hooks/failure-detector.mjs +1 -3
- package/hooks/gpt-work-dispatcher.mjs +153 -12
- package/hooks/health-check.mjs +25 -17
- package/hooks/quality-gate.mjs +11 -6
- package/hooks/risk-classifier.mjs +2 -135
- package/hooks/session-report.mjs +71 -41
- package/hooks/summary-checkpoint.mjs +8 -35
- package/hooks/test-orchestrator.mjs +31 -2080
- package/install.mjs +628 -1557
- package/orchestrator.json +96 -73
- package/package.json +2 -7
- package/hooks/agent-chains.mjs +0 -369
- package/hooks/agent-templates.mjs +0 -441
- package/hooks/atomic-write.mjs +0 -109
- package/hooks/config-validator.mjs +0 -156
- package/hooks/confirmation-policy.mjs +0 -167
- package/hooks/error-channel.mjs +0 -68
- package/hooks/ship-captain.mjs +0 -1176
- package/hooks/ship-gate.mjs +0 -971
|
@@ -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
|
-
}
|
package/hooks/atomic-write.mjs
DELETED
|
@@ -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
|
-
}
|