brain-dev 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/agents/brain-checker.md +33 -0
- package/agents/brain-debugger.md +35 -0
- package/agents/brain-executor.md +37 -0
- package/agents/brain-mapper.md +44 -0
- package/agents/brain-planner.md +49 -0
- package/agents/brain-researcher.md +47 -0
- package/agents/brain-synthesizer.md +43 -0
- package/agents/brain-verifier.md +41 -0
- package/bin/brain-tools.cjs +185 -0
- package/bin/lib/adr.cjs +283 -0
- package/bin/lib/agents.cjs +152 -0
- package/bin/lib/anti-patterns.cjs +183 -0
- package/bin/lib/audit.cjs +268 -0
- package/bin/lib/commands/adr.cjs +126 -0
- package/bin/lib/commands/complete.cjs +270 -0
- package/bin/lib/commands/config.cjs +306 -0
- package/bin/lib/commands/discuss.cjs +237 -0
- package/bin/lib/commands/execute.cjs +415 -0
- package/bin/lib/commands/health.cjs +103 -0
- package/bin/lib/commands/map.cjs +101 -0
- package/bin/lib/commands/new-project.cjs +885 -0
- package/bin/lib/commands/pause.cjs +142 -0
- package/bin/lib/commands/phase-manage.cjs +357 -0
- package/bin/lib/commands/plan.cjs +451 -0
- package/bin/lib/commands/progress.cjs +167 -0
- package/bin/lib/commands/quick.cjs +447 -0
- package/bin/lib/commands/resume.cjs +196 -0
- package/bin/lib/commands/storm.cjs +590 -0
- package/bin/lib/commands/verify.cjs +504 -0
- package/bin/lib/commands.cjs +263 -0
- package/bin/lib/complexity.cjs +138 -0
- package/bin/lib/complexity.test.cjs +108 -0
- package/bin/lib/config.cjs +452 -0
- package/bin/lib/core.cjs +62 -0
- package/bin/lib/detect.cjs +603 -0
- package/bin/lib/git.cjs +112 -0
- package/bin/lib/health.cjs +356 -0
- package/bin/lib/init.cjs +310 -0
- package/bin/lib/logger.cjs +100 -0
- package/bin/lib/platform.cjs +58 -0
- package/bin/lib/requirements.cjs +158 -0
- package/bin/lib/roadmap.cjs +228 -0
- package/bin/lib/security.cjs +237 -0
- package/bin/lib/state.cjs +353 -0
- package/bin/lib/templates.cjs +48 -0
- package/bin/templates/advocate.md +182 -0
- package/bin/templates/checkpoint.md +55 -0
- package/bin/templates/debugger.md +148 -0
- package/bin/templates/discuss.md +60 -0
- package/bin/templates/executor.md +201 -0
- package/bin/templates/mapper.md +129 -0
- package/bin/templates/plan-checker.md +134 -0
- package/bin/templates/planner.md +165 -0
- package/bin/templates/researcher.md +78 -0
- package/bin/templates/storm.html +376 -0
- package/bin/templates/synthesis.md +30 -0
- package/bin/templates/verifier.md +181 -0
- package/commands/brain/adr.md +34 -0
- package/commands/brain/complete.md +37 -0
- package/commands/brain/config.md +37 -0
- package/commands/brain/discuss.md +35 -0
- package/commands/brain/execute.md +38 -0
- package/commands/brain/health.md +33 -0
- package/commands/brain/map.md +35 -0
- package/commands/brain/new-project.md +38 -0
- package/commands/brain/pause.md +26 -0
- package/commands/brain/plan.md +38 -0
- package/commands/brain/progress.md +28 -0
- package/commands/brain/quick.md +51 -0
- package/commands/brain/resume.md +28 -0
- package/commands/brain/storm.md +30 -0
- package/commands/brain/verify.md +39 -0
- package/hooks/bootstrap.sh +54 -0
- package/hooks/post-tool-use.sh +45 -0
- package/hooks/statusline.sh +130 -0
- package/package.json +36 -0
package/bin/lib/adr.cjs
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { atomicWriteSync } = require('./state.cjs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the next available ADR ID by scanning existing ADR files.
|
|
9
|
+
* @param {string} adrsDir - Path to .brain/adrs/ directory
|
|
10
|
+
* @returns {number} Next available ID (1 if empty or dir missing)
|
|
11
|
+
*/
|
|
12
|
+
function getNextId(adrsDir) {
|
|
13
|
+
if (!fs.existsSync(adrsDir)) return 1;
|
|
14
|
+
|
|
15
|
+
const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
|
|
16
|
+
if (files.length === 0) return 1;
|
|
17
|
+
|
|
18
|
+
const ids = files.map(f => parseInt(f.match(/ADR-(\d+)\.md/)[1], 10));
|
|
19
|
+
return Math.max(...ids) + 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new Architecture Decision Record.
|
|
24
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
25
|
+
* @param {object} options
|
|
26
|
+
* @param {string} options.title - ADR title
|
|
27
|
+
* @param {string} options.context - Why this decision came up
|
|
28
|
+
* @param {string} options.decision - What was chosen
|
|
29
|
+
* @param {string} options.alternatives - What was rejected
|
|
30
|
+
* @param {string} options.consequences - Impact of the decision
|
|
31
|
+
* @param {string} options.phase - Phase number
|
|
32
|
+
* @param {string} options.plan - Plan number
|
|
33
|
+
* @param {string} [options.deciders] - Who made the decision
|
|
34
|
+
* @returns {{ id: string, path: string }}
|
|
35
|
+
*/
|
|
36
|
+
function createADR(brainDir, options) {
|
|
37
|
+
const adrsDir = path.join(brainDir, 'adrs');
|
|
38
|
+
fs.mkdirSync(adrsDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const nextId = getNextId(adrsDir);
|
|
41
|
+
const paddedId = String(nextId).padStart(3, '0');
|
|
42
|
+
const adrId = `ADR-${paddedId}`;
|
|
43
|
+
const filePath = path.join(adrsDir, `${adrId}.md`);
|
|
44
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
45
|
+
|
|
46
|
+
const content = [
|
|
47
|
+
'---',
|
|
48
|
+
`id: ${nextId}`,
|
|
49
|
+
`date: "${date}"`,
|
|
50
|
+
'status: proposed',
|
|
51
|
+
`phase: "${options.phase}"`,
|
|
52
|
+
`plan: "${options.plan}"`,
|
|
53
|
+
`deciders: "${options.deciders || 'unknown'}"`,
|
|
54
|
+
'supersedes: null',
|
|
55
|
+
'superseded_by: null',
|
|
56
|
+
'---',
|
|
57
|
+
'',
|
|
58
|
+
`# ${adrId}: ${options.title}`,
|
|
59
|
+
'',
|
|
60
|
+
'## Context',
|
|
61
|
+
'',
|
|
62
|
+
options.context || '',
|
|
63
|
+
'',
|
|
64
|
+
'## Decision',
|
|
65
|
+
'',
|
|
66
|
+
options.decision || '',
|
|
67
|
+
'',
|
|
68
|
+
'## Alternatives',
|
|
69
|
+
'',
|
|
70
|
+
options.alternatives || '',
|
|
71
|
+
'',
|
|
72
|
+
'## Consequences',
|
|
73
|
+
'',
|
|
74
|
+
options.consequences || '',
|
|
75
|
+
''
|
|
76
|
+
].join('\n');
|
|
77
|
+
|
|
78
|
+
atomicWriteSync(filePath, content);
|
|
79
|
+
|
|
80
|
+
return { id: adrId, path: filePath };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse YAML-ish frontmatter from ADR file content.
|
|
85
|
+
* @param {string} content - File content
|
|
86
|
+
* @returns {object} Parsed frontmatter fields
|
|
87
|
+
*/
|
|
88
|
+
function parseFrontmatter(content) {
|
|
89
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
90
|
+
if (!match) return {};
|
|
91
|
+
|
|
92
|
+
const fm = {};
|
|
93
|
+
for (const line of match[1].split('\n')) {
|
|
94
|
+
const colonIdx = line.indexOf(':');
|
|
95
|
+
if (colonIdx === -1) continue;
|
|
96
|
+
const key = line.slice(0, colonIdx).trim();
|
|
97
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
98
|
+
// Remove quotes (preserve as string if originally quoted)
|
|
99
|
+
let wasQuoted = false;
|
|
100
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
101
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
102
|
+
value = value.slice(1, -1);
|
|
103
|
+
wasQuoted = true;
|
|
104
|
+
}
|
|
105
|
+
if (value === 'null') value = null;
|
|
106
|
+
else if (!wasQuoted && /^\d+$/.test(value)) value = parseInt(value, 10);
|
|
107
|
+
fm[key] = value;
|
|
108
|
+
}
|
|
109
|
+
return fm;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract title from first # heading in ADR content.
|
|
114
|
+
* @param {string} content - File content
|
|
115
|
+
* @returns {string} Title
|
|
116
|
+
*/
|
|
117
|
+
function extractTitle(content) {
|
|
118
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
119
|
+
return match ? match[1] : 'Untitled';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List all ADRs, optionally filtered by phase.
|
|
124
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
125
|
+
* @param {object} [options]
|
|
126
|
+
* @param {string} [options.phase] - Filter by phase
|
|
127
|
+
* @returns {Array<{id: number, date: string, status: string, title: string, path: string}>}
|
|
128
|
+
*/
|
|
129
|
+
function listADRs(brainDir, options) {
|
|
130
|
+
const adrsDir = path.join(brainDir, 'adrs');
|
|
131
|
+
if (!fs.existsSync(adrsDir)) return [];
|
|
132
|
+
|
|
133
|
+
const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
|
|
134
|
+
if (files.length === 0) return [];
|
|
135
|
+
|
|
136
|
+
const adrs = files.map(f => {
|
|
137
|
+
const filePath = path.join(adrsDir, f);
|
|
138
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
139
|
+
const fm = parseFrontmatter(content);
|
|
140
|
+
return {
|
|
141
|
+
id: fm.id || parseInt(f.match(/ADR-(\d+)\.md/)[1], 10),
|
|
142
|
+
date: fm.date || '',
|
|
143
|
+
status: fm.status || 'unknown',
|
|
144
|
+
title: extractTitle(content),
|
|
145
|
+
phase: fm.phase || '',
|
|
146
|
+
path: filePath
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Sort by ID ascending
|
|
151
|
+
adrs.sort((a, b) => a.id - b.id);
|
|
152
|
+
|
|
153
|
+
// Filter by phase if provided
|
|
154
|
+
if (options && options.phase) {
|
|
155
|
+
return adrs.filter(a => String(a.phase) === String(options.phase));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return adrs;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Search ADRs by keyword (case-insensitive).
|
|
163
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
164
|
+
* @param {string} keyword - Search term
|
|
165
|
+
* @returns {Array<{id: number, date: string, status: string, title: string, path: string}>}
|
|
166
|
+
*/
|
|
167
|
+
function searchADRs(brainDir, keyword) {
|
|
168
|
+
const adrsDir = path.join(brainDir, 'adrs');
|
|
169
|
+
if (!fs.existsSync(adrsDir)) return [];
|
|
170
|
+
|
|
171
|
+
const files = fs.readdirSync(adrsDir).filter(f => /^ADR-\d+\.md$/.test(f));
|
|
172
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
173
|
+
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const f of files) {
|
|
176
|
+
const filePath = path.join(adrsDir, f);
|
|
177
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
178
|
+
if (content.toLowerCase().includes(lowerKeyword)) {
|
|
179
|
+
const fm = parseFrontmatter(content);
|
|
180
|
+
results.push({
|
|
181
|
+
id: fm.id || parseInt(f.match(/ADR-(\d+)\.md/)[1], 10),
|
|
182
|
+
date: fm.date || '',
|
|
183
|
+
status: fm.status || 'unknown',
|
|
184
|
+
title: extractTitle(content),
|
|
185
|
+
phase: fm.phase || '',
|
|
186
|
+
path: filePath
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
results.sort((a, b) => a.id - b.id);
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Update the status of an existing ADR.
|
|
197
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
198
|
+
* @param {number} adrId - ADR numeric ID
|
|
199
|
+
* @param {string} newStatus - New status (proposed, accepted, deprecated, superseded)
|
|
200
|
+
* @param {object} [opts]
|
|
201
|
+
* @param {number} [opts.superseded_by] - ID of superseding ADR (when status is 'superseded')
|
|
202
|
+
* @param {number} [opts.supersedes] - ID of superseded ADR (when this is the new ADR)
|
|
203
|
+
*/
|
|
204
|
+
function updateADRStatus(brainDir, adrId, newStatus, opts) {
|
|
205
|
+
const adrsDir = path.join(brainDir, 'adrs');
|
|
206
|
+
const paddedId = String(adrId).padStart(3, '0');
|
|
207
|
+
const filePath = path.join(adrsDir, `ADR-${paddedId}.md`);
|
|
208
|
+
|
|
209
|
+
if (!fs.existsSync(filePath)) {
|
|
210
|
+
throw new Error(`ADR-${paddedId} not found`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
214
|
+
|
|
215
|
+
// Update status
|
|
216
|
+
content = content.replace(/^status:\s*.+$/m, `status: ${newStatus}`);
|
|
217
|
+
|
|
218
|
+
// Update superseded_by if provided
|
|
219
|
+
if (opts && opts.superseded_by != null) {
|
|
220
|
+
content = content.replace(/^superseded_by:\s*.+$/m, `superseded_by: ${opts.superseded_by}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Update supersedes if provided
|
|
224
|
+
if (opts && opts.supersedes != null) {
|
|
225
|
+
content = content.replace(/^supersedes:\s*.+$/m, `supersedes: ${opts.supersedes}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
atomicWriteSync(filePath, content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* ADR-worthy detection keywords.
|
|
233
|
+
*/
|
|
234
|
+
const ADR_KEYWORDS = [
|
|
235
|
+
'chose',
|
|
236
|
+
'decided to use',
|
|
237
|
+
'instead of',
|
|
238
|
+
'alternative was',
|
|
239
|
+
'trade-off',
|
|
240
|
+
'trade off',
|
|
241
|
+
'because of',
|
|
242
|
+
'rejected approach'
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* ADR-worthy context indicators.
|
|
247
|
+
*/
|
|
248
|
+
const ADR_CONTEXTS = [
|
|
249
|
+
'dependency',
|
|
250
|
+
'dependencies',
|
|
251
|
+
'pattern',
|
|
252
|
+
'architecture',
|
|
253
|
+
'architectural',
|
|
254
|
+
'api contract',
|
|
255
|
+
'module structure',
|
|
256
|
+
'performance',
|
|
257
|
+
'simplicity'
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if text contains an ADR-worthy decision.
|
|
262
|
+
* Requires BOTH a keyword match AND a context indicator match.
|
|
263
|
+
* @param {string} text - Text to check
|
|
264
|
+
* @returns {boolean}
|
|
265
|
+
*/
|
|
266
|
+
function isADRWorthy(text) {
|
|
267
|
+
if (!text) return false;
|
|
268
|
+
const lower = text.toLowerCase();
|
|
269
|
+
|
|
270
|
+
const hasKeyword = ADR_KEYWORDS.some(kw => lower.includes(kw));
|
|
271
|
+
const hasContext = ADR_CONTEXTS.some(ctx => lower.includes(ctx));
|
|
272
|
+
|
|
273
|
+
return hasKeyword && hasContext;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
getNextId,
|
|
278
|
+
createADR,
|
|
279
|
+
listADRs,
|
|
280
|
+
searchADRs,
|
|
281
|
+
updateADRStatus,
|
|
282
|
+
isADRWorthy
|
|
283
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent registry for brain orchestration.
|
|
5
|
+
* Defines the 7 core agents and their metadata.
|
|
6
|
+
* Constant registry with discovery and validation functions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MAX_AGENTS = 8;
|
|
10
|
+
|
|
11
|
+
const AGENTS = {
|
|
12
|
+
researcher: {
|
|
13
|
+
template: 'researcher',
|
|
14
|
+
inputs: ['phase_context', 'project_info', 'focus_areas'],
|
|
15
|
+
outputs: ['RESEARCH.md'],
|
|
16
|
+
model: 'inherit',
|
|
17
|
+
description: 'Researches phase domain: codebase patterns, library docs, risks, and recommendations'
|
|
18
|
+
},
|
|
19
|
+
planner: {
|
|
20
|
+
template: 'planner',
|
|
21
|
+
inputs: ['phase_context', 'research', 'context_decisions'],
|
|
22
|
+
outputs: ['PLAN-*.md'],
|
|
23
|
+
model: 'inherit',
|
|
24
|
+
description: 'Creates execution plans with tasks, verification criteria, and dependency graphs'
|
|
25
|
+
},
|
|
26
|
+
'plan-checker': {
|
|
27
|
+
template: 'plan-checker',
|
|
28
|
+
modes: { advocate: { template: 'advocate', max_iterations: 2 } },
|
|
29
|
+
inputs: ['plan_content', 'phase_requirements', 'context_decisions'],
|
|
30
|
+
outputs: ['checker_result'],
|
|
31
|
+
model: 'inherit',
|
|
32
|
+
description: 'Validates plans across 8 dimensions: coverage, completeness, dependencies, ownership, scope, verification, context, testing'
|
|
33
|
+
},
|
|
34
|
+
executor: {
|
|
35
|
+
template: 'executor',
|
|
36
|
+
inputs: ['plan_content', 'summary_path'],
|
|
37
|
+
outputs: ['SUMMARY-*.md'],
|
|
38
|
+
model: 'inherit',
|
|
39
|
+
description: 'Executes plan tasks with per-task commits, deviation handling, and checkpoint protocol'
|
|
40
|
+
},
|
|
41
|
+
verifier: {
|
|
42
|
+
template: 'verifier',
|
|
43
|
+
inputs: ['must_haves', 'summaries'],
|
|
44
|
+
outputs: ['VERIFICATION.md'],
|
|
45
|
+
model: 'inherit',
|
|
46
|
+
description: 'Verifies plan outputs against must_haves with 3-level depth checks'
|
|
47
|
+
},
|
|
48
|
+
debugger: {
|
|
49
|
+
template: 'debugger',
|
|
50
|
+
inputs: ['error_context', 'task_context', 'attempted_fixes'],
|
|
51
|
+
outputs: ['debug_session'],
|
|
52
|
+
model: 'inherit',
|
|
53
|
+
description: 'Diagnoses and fixes failures using 4-phase method with hypothesis tracking'
|
|
54
|
+
},
|
|
55
|
+
synthesizer: {
|
|
56
|
+
template: 'synthesis',
|
|
57
|
+
inputs: ['research_dir'],
|
|
58
|
+
outputs: ['SUMMARY.md'],
|
|
59
|
+
model: 'inherit',
|
|
60
|
+
description: 'Combines findings from parallel research agents into a unified SUMMARY.md'
|
|
61
|
+
},
|
|
62
|
+
mapper: {
|
|
63
|
+
template: 'mapper',
|
|
64
|
+
inputs: ['focus', 'codebase_root'],
|
|
65
|
+
outputs: ['codebase/*.md'],
|
|
66
|
+
model: 'inherit',
|
|
67
|
+
description: 'Maps codebase across focus areas producing structured Markdown documentation',
|
|
68
|
+
focus: ['tech', 'arch', 'quality', 'concerns']
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get agent metadata by name.
|
|
74
|
+
* @param {string} name - Agent name
|
|
75
|
+
* @returns {object} Agent metadata with name field added
|
|
76
|
+
* @throws {Error} If agent name is unknown
|
|
77
|
+
*/
|
|
78
|
+
function getAgent(name) {
|
|
79
|
+
const agent = AGENTS[name];
|
|
80
|
+
if (!agent) {
|
|
81
|
+
throw new Error(`Unknown agent: ${name}`);
|
|
82
|
+
}
|
|
83
|
+
return { name, ...agent };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* List all registered agent names.
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
function listAgents() {
|
|
91
|
+
return Object.keys(AGENTS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate that agent count does not exceed MAX_AGENTS.
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function validateAgentCount() {
|
|
99
|
+
return Object.keys(AGENTS).length <= MAX_AGENTS;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the model for an agent using 4-level priority:
|
|
104
|
+
* 1. Per-agent override (brainConfig.agents.models[agentName])
|
|
105
|
+
* 2. Active profile preset (custom profiles > built-in PROFILES)
|
|
106
|
+
* 3. Global default (brainConfig.agents.model)
|
|
107
|
+
* 4. Fallback to 'inherit' (use session model)
|
|
108
|
+
*
|
|
109
|
+
* @param {string} agentName - Agent name
|
|
110
|
+
* @param {object|null} brainConfig - brain.json config object
|
|
111
|
+
* @returns {string} Resolved model identifier
|
|
112
|
+
*/
|
|
113
|
+
function resolveModel(agentName, brainConfig) {
|
|
114
|
+
if (!brainConfig || !brainConfig.agents) {
|
|
115
|
+
return 'inherit';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const agentsCfg = brainConfig.agents;
|
|
119
|
+
|
|
120
|
+
// Priority 1: per-agent override
|
|
121
|
+
if (agentsCfg.models && agentsCfg.models[agentName]) {
|
|
122
|
+
return agentsCfg.models[agentName];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Priority 2: active profile preset
|
|
126
|
+
if (agentsCfg.profile) {
|
|
127
|
+
const { PROFILES } = require('./config.cjs');
|
|
128
|
+
// Check custom profiles first (user-defined), then built-in
|
|
129
|
+
const profile = (agentsCfg.profiles && agentsCfg.profiles[agentsCfg.profile])
|
|
130
|
+
|| PROFILES[agentsCfg.profile];
|
|
131
|
+
if (profile && profile.models && profile.models[agentName]) {
|
|
132
|
+
return profile.models[agentName];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Priority 3: global default
|
|
137
|
+
if (agentsCfg.model) {
|
|
138
|
+
return agentsCfg.model;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Priority 4: inherit session model
|
|
142
|
+
return 'inherit';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
AGENTS,
|
|
147
|
+
MAX_AGENTS,
|
|
148
|
+
getAgent,
|
|
149
|
+
listAgents,
|
|
150
|
+
validateAgentCount,
|
|
151
|
+
resolveModel
|
|
152
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Anti-pattern detection patterns for JS/CJS source files.
|
|
8
|
+
* Each pattern has a name, regex, and severity level.
|
|
9
|
+
*/
|
|
10
|
+
const JS_PATTERNS = [
|
|
11
|
+
{
|
|
12
|
+
name: 'Empty Function Body',
|
|
13
|
+
regex: /function\s+\w+\s*\([^)]*\)\s*\{\s*\}/,
|
|
14
|
+
severity: 'block'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'TODO/FIXME Comment',
|
|
18
|
+
regex: /\/\/\s*(TODO|FIXME|HACK)\b/,
|
|
19
|
+
severity: 'block'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Log-Only Error Handler',
|
|
23
|
+
regex: /catch\s*\([^)]*\)\s*\{\s*console\.(log|error|warn)\([^)]*\);\s*\}/,
|
|
24
|
+
severity: 'warning'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Not Implemented',
|
|
28
|
+
regex: /throw\s+new\s+Error\s*\(\s*['"]not\s+implemented['"]\s*\)/i,
|
|
29
|
+
severity: 'block'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Empty Return',
|
|
33
|
+
regex: /return\s+(null|undefined|\[\s*\]|\{\s*\})\s*;/,
|
|
34
|
+
severity: 'warning'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Placeholder Content',
|
|
38
|
+
regex: /['"`](placeholder|coming soon|lorem ipsum|sample data|test data|dummy)['"`]/i,
|
|
39
|
+
severity: 'warning'
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Anti-pattern detection patterns for test files.
|
|
45
|
+
*/
|
|
46
|
+
const TEST_PATTERNS = [
|
|
47
|
+
{
|
|
48
|
+
name: 'Skipped Test',
|
|
49
|
+
regex: /(?:it\.skip|xit|xdescribe|describe\.skip)\s*\(/,
|
|
50
|
+
severity: 'block'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Empty Test Body',
|
|
54
|
+
regex: /it\s*\(\s*['"][^'"]*['"]\s*,\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/,
|
|
55
|
+
severity: 'block'
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Anti-pattern detection patterns for Markdown files.
|
|
61
|
+
*/
|
|
62
|
+
const MD_PATTERNS = [
|
|
63
|
+
{
|
|
64
|
+
name: 'TBD/TODO Placeholder',
|
|
65
|
+
regex: /^(TBD|TODO)\s*$/m,
|
|
66
|
+
severity: 'block'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Empty Section',
|
|
70
|
+
regex: /^##\s+.+\n\s*\n(?=##\s|$)/m,
|
|
71
|
+
severity: 'warning'
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the appropriate pattern set for a file path.
|
|
77
|
+
* @param {string} filePath - File path to determine patterns for
|
|
78
|
+
* @returns {Array} Pattern array for the file type
|
|
79
|
+
*/
|
|
80
|
+
function getPatterns(filePath) {
|
|
81
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
82
|
+
const base = path.basename(filePath).toLowerCase();
|
|
83
|
+
|
|
84
|
+
// Test files get test patterns (check before JS patterns)
|
|
85
|
+
if (base.includes('.test.') || base.includes('.spec.')) {
|
|
86
|
+
return TEST_PATTERNS;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// JS/CJS files
|
|
90
|
+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
|
|
91
|
+
return JS_PATTERNS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Markdown files
|
|
95
|
+
if (ext === '.md') {
|
|
96
|
+
return MD_PATTERNS;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Scan content string for anti-patterns.
|
|
104
|
+
* @param {string} content - Content to scan
|
|
105
|
+
* @param {string} [filePath] - Optional file path for pattern selection and findings context
|
|
106
|
+
* @returns {{ findings: Array<{ name: string, severity: string, file?: string, line?: number, match: string }> }}
|
|
107
|
+
*/
|
|
108
|
+
function scanContent(content, filePath) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
const patterns = filePath ? getPatterns(filePath) : JS_PATTERNS;
|
|
111
|
+
|
|
112
|
+
for (const pattern of patterns) {
|
|
113
|
+
const match = content.match(pattern.regex);
|
|
114
|
+
if (match) {
|
|
115
|
+
const finding = {
|
|
116
|
+
name: pattern.name,
|
|
117
|
+
severity: pattern.severity,
|
|
118
|
+
match: match[0]
|
|
119
|
+
};
|
|
120
|
+
if (filePath) {
|
|
121
|
+
finding.file = filePath;
|
|
122
|
+
}
|
|
123
|
+
// Calculate line number
|
|
124
|
+
if (content.includes('\n')) {
|
|
125
|
+
const beforeMatch = content.slice(0, match.index);
|
|
126
|
+
finding.line = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
127
|
+
}
|
|
128
|
+
findings.push(finding);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { findings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scan files for anti-patterns.
|
|
137
|
+
* @param {string} rootDir - Root directory
|
|
138
|
+
* @param {object} [options] - Scan options
|
|
139
|
+
* @param {string[]} [options.files] - Array of relative file paths to scan (phase-scoped)
|
|
140
|
+
* @returns {{ findings: object[], blockers: object[], warnings: object[] }}
|
|
141
|
+
*/
|
|
142
|
+
function scanFiles(rootDir, options = {}) {
|
|
143
|
+
const filesToScan = options.files || [];
|
|
144
|
+
|
|
145
|
+
const allFindings = [];
|
|
146
|
+
const blockers = [];
|
|
147
|
+
const warnings = [];
|
|
148
|
+
|
|
149
|
+
for (const relPath of filesToScan) {
|
|
150
|
+
const fullPath = path.join(rootDir, relPath);
|
|
151
|
+
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
// Skip files that don't exist or can't be read
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const result = scanContent(content, relPath);
|
|
161
|
+
|
|
162
|
+
for (const finding of result.findings) {
|
|
163
|
+
allFindings.push(finding);
|
|
164
|
+
|
|
165
|
+
if (finding.severity === 'block') {
|
|
166
|
+
blockers.push(finding);
|
|
167
|
+
} else {
|
|
168
|
+
warnings.push(finding);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { findings: allFindings, blockers, warnings };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
JS_PATTERNS,
|
|
178
|
+
TEST_PATTERNS,
|
|
179
|
+
MD_PATTERNS,
|
|
180
|
+
scanContent,
|
|
181
|
+
scanFiles,
|
|
182
|
+
getPatterns
|
|
183
|
+
};
|