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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Secret detection patterns for security scanning.
|
|
8
|
+
* Each pattern has a name, regex, and severity level.
|
|
9
|
+
*/
|
|
10
|
+
const PATTERNS = [
|
|
11
|
+
{
|
|
12
|
+
name: 'Stripe Key',
|
|
13
|
+
regex: /sk_(?:live|test)_[a-zA-Z0-9]{10,}/,
|
|
14
|
+
severity: 'block'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'OpenAI Key',
|
|
18
|
+
regex: /sk-(?:proj-)?[a-zA-Z0-9_-]{20,}/,
|
|
19
|
+
severity: 'block'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'AWS Access Key',
|
|
23
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
24
|
+
severity: 'block'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'GitHub Token',
|
|
28
|
+
regex: /gh[ps]_[A-Za-z0-9_]{36,}/,
|
|
29
|
+
severity: 'block'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Password Assignment',
|
|
33
|
+
regex: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
34
|
+
severity: 'block'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'PEM Key',
|
|
38
|
+
regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
|
|
39
|
+
severity: 'block'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'MongoDB URI',
|
|
43
|
+
regex: /mongodb(?:\+srv)?:\/\/[^\s]+/,
|
|
44
|
+
severity: 'block'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Postgres URI',
|
|
48
|
+
regex: /postgres(?:ql)?:\/\/[^\s]+/,
|
|
49
|
+
severity: 'block'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Bearer Token',
|
|
53
|
+
regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/,
|
|
54
|
+
severity: 'block'
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Scan content string for secret patterns.
|
|
60
|
+
* @param {string} content - Content to scan
|
|
61
|
+
* @param {string} [filePath] - Optional file path for findings context
|
|
62
|
+
* @returns {{ findings: Array<{ name: string, pattern: string, match: string, file?: string, line?: number }> }}
|
|
63
|
+
*/
|
|
64
|
+
function scanContent(content, filePath) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
|
|
67
|
+
for (const pattern of PATTERNS) {
|
|
68
|
+
const match = content.match(pattern.regex);
|
|
69
|
+
if (match) {
|
|
70
|
+
const finding = {
|
|
71
|
+
name: pattern.name,
|
|
72
|
+
pattern: pattern.regex.source,
|
|
73
|
+
match: match[0]
|
|
74
|
+
};
|
|
75
|
+
if (filePath) {
|
|
76
|
+
finding.file = filePath;
|
|
77
|
+
}
|
|
78
|
+
// Calculate line number if content has newlines
|
|
79
|
+
if (content.includes('\n')) {
|
|
80
|
+
const beforeMatch = content.slice(0, match.index);
|
|
81
|
+
finding.line = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
82
|
+
}
|
|
83
|
+
findings.push(finding);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { findings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse .gitignore file from a root directory.
|
|
92
|
+
* Returns array of non-empty, non-comment patterns.
|
|
93
|
+
* @param {string} rootDir - Directory containing .gitignore
|
|
94
|
+
* @returns {string[]} Array of patterns
|
|
95
|
+
*/
|
|
96
|
+
function parseGitignore(rootDir) {
|
|
97
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
98
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
103
|
+
return content
|
|
104
|
+
.split('\n')
|
|
105
|
+
.map(line => line.trim())
|
|
106
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a file path matches any .gitignore-style pattern.
|
|
111
|
+
* Supports: exact prefix match, glob * matching, directory patterns.
|
|
112
|
+
* @param {string} filePath - Relative file path to check
|
|
113
|
+
* @param {string[]} patterns - Array of gitignore patterns
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function isIgnored(filePath, patterns) {
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
const clean = pattern.replace(/\/$/, '');
|
|
119
|
+
|
|
120
|
+
// Glob pattern with *
|
|
121
|
+
if (clean.includes('*')) {
|
|
122
|
+
const regexStr = clean
|
|
123
|
+
.replace(/\./g, '\\.')
|
|
124
|
+
.replace(/\*\*/g, '{{DOUBLESTAR}}')
|
|
125
|
+
.replace(/\*/g, '[^/]*')
|
|
126
|
+
.replace(/\{\{DOUBLESTAR\}\}/g, '.*');
|
|
127
|
+
const regex = new RegExp(`(^|/)${regexStr}$`);
|
|
128
|
+
if (regex.test(filePath)) return true;
|
|
129
|
+
} else {
|
|
130
|
+
// Exact or prefix match
|
|
131
|
+
if (filePath === clean || filePath.startsWith(clean + '/') || filePath.startsWith(clean + path.sep)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// Also check basename match
|
|
135
|
+
const basename = path.basename(filePath);
|
|
136
|
+
if (basename === clean) return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Recursively walk a directory, returning relative file paths.
|
|
144
|
+
* Skips .git, node_modules, and binary files by default.
|
|
145
|
+
* @param {string} dir - Directory to walk
|
|
146
|
+
* @param {string} rootDir - Root directory for relative paths
|
|
147
|
+
* @param {string[]} ignorePatterns - Gitignore patterns to skip
|
|
148
|
+
* @returns {string[]} Array of relative file paths
|
|
149
|
+
*/
|
|
150
|
+
function walkDir(dir, rootDir, ignorePatterns) {
|
|
151
|
+
const results = [];
|
|
152
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.brain']);
|
|
153
|
+
const BINARY_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.zip', '.tar', '.gz']);
|
|
154
|
+
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
158
|
+
} catch {
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const fullPath = path.join(dir, entry.name);
|
|
164
|
+
const relPath = path.relative(rootDir, fullPath);
|
|
165
|
+
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
168
|
+
if (isIgnored(relPath, ignorePatterns)) continue;
|
|
169
|
+
results.push(...walkDir(fullPath, rootDir, ignorePatterns));
|
|
170
|
+
} else if (entry.isFile()) {
|
|
171
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
172
|
+
if (BINARY_EXTS.has(ext)) continue;
|
|
173
|
+
if (isIgnored(relPath, ignorePatterns)) continue;
|
|
174
|
+
results.push(relPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Scan all files in a directory tree for secrets.
|
|
183
|
+
* Respects .gitignore patterns. Relaxes severity for test directories.
|
|
184
|
+
* @param {string} rootDir - Root directory to scan
|
|
185
|
+
* @param {object} [options] - Scan options
|
|
186
|
+
* @param {string[]} [options.relaxDirs] - Directories where findings are warnings instead of blockers (e.g. ['test/', '__tests__/'])
|
|
187
|
+
* @returns {{ findings: object[], blockers: object[], warnings: object[] }}
|
|
188
|
+
*/
|
|
189
|
+
function scanFiles(rootDir, options = {}) {
|
|
190
|
+
const relaxDirs = options.relaxDirs || ['test/', '__tests__/'];
|
|
191
|
+
const ignorePatterns = parseGitignore(rootDir);
|
|
192
|
+
const files = walkDir(rootDir, rootDir, ignorePatterns);
|
|
193
|
+
|
|
194
|
+
const allFindings = [];
|
|
195
|
+
const blockers = [];
|
|
196
|
+
const warnings = [];
|
|
197
|
+
|
|
198
|
+
for (const relPath of files) {
|
|
199
|
+
const fullPath = path.join(rootDir, relPath);
|
|
200
|
+
let content;
|
|
201
|
+
try {
|
|
202
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = scanContent(content, relPath);
|
|
208
|
+
if (result.findings.length === 0) continue;
|
|
209
|
+
|
|
210
|
+
// Check if file is in a relaxed directory
|
|
211
|
+
const isRelaxed = relaxDirs.some(dir => {
|
|
212
|
+
const clean = dir.replace(/\/$/, '');
|
|
213
|
+
return relPath.startsWith(clean + '/') || relPath.startsWith(clean + path.sep) || relPath === clean;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (const finding of result.findings) {
|
|
217
|
+
finding.severity = isRelaxed ? 'warning' : 'block';
|
|
218
|
+
allFindings.push(finding);
|
|
219
|
+
|
|
220
|
+
if (isRelaxed) {
|
|
221
|
+
warnings.push(finding);
|
|
222
|
+
} else {
|
|
223
|
+
blockers.push(finding);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { findings: allFindings, blockers, warnings };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
PATTERNS,
|
|
233
|
+
scanContent,
|
|
234
|
+
parseGitignore,
|
|
235
|
+
scanFiles,
|
|
236
|
+
isIgnored
|
|
237
|
+
};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const CURRENT_SCHEMA = 'brain/v1';
|
|
7
|
+
const CURRENT_VERSION = '0.7.0';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Atomic write: write to temp file, then rename.
|
|
11
|
+
* Prevents corruption on crash (rename is atomic on all major filesystems).
|
|
12
|
+
* @param {string} filePath - Target file path
|
|
13
|
+
* @param {string} content - Content to write
|
|
14
|
+
*/
|
|
15
|
+
function atomicWriteSync(filePath, content) {
|
|
16
|
+
// Guard: if target is a directory, remove it first
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
19
|
+
fs.rmSync(filePath, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
} catch { /* ignore stat errors */ }
|
|
22
|
+
const dir = path.dirname(filePath);
|
|
23
|
+
const tmpPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
24
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
25
|
+
fs.renameSync(tmpPath, filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read brain.json from the given directory.
|
|
30
|
+
* Returns null if file does not exist.
|
|
31
|
+
* Triggers migration on schema version mismatch.
|
|
32
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
33
|
+
* @returns {object|null}
|
|
34
|
+
*/
|
|
35
|
+
function readState(brainDir) {
|
|
36
|
+
const jsonPath = path.join(brainDir, 'brain.json');
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(jsonPath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Schema migration if version mismatch
|
|
50
|
+
if (data.$schema !== CURRENT_SCHEMA) {
|
|
51
|
+
// Backup before migration (pitfall #6)
|
|
52
|
+
const backupPath = path.join(brainDir, 'brain.json.backup');
|
|
53
|
+
fs.copyFileSync(jsonPath, backupPath);
|
|
54
|
+
|
|
55
|
+
data = migrateState(data);
|
|
56
|
+
|
|
57
|
+
// Write migrated state (both brain.json and STATE.md)
|
|
58
|
+
writeState(brainDir, data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Migrate state to current schema version.
|
|
66
|
+
* Additive only: adds new fields with defaults, never removes fields.
|
|
67
|
+
* @param {object} data - Old state object
|
|
68
|
+
* @returns {object} Migrated state object
|
|
69
|
+
*/
|
|
70
|
+
function migrateState(data) {
|
|
71
|
+
const migrated = { ...data };
|
|
72
|
+
|
|
73
|
+
// Always update schema version
|
|
74
|
+
migrated.$schema = CURRENT_SCHEMA;
|
|
75
|
+
migrated.version = CURRENT_VERSION;
|
|
76
|
+
|
|
77
|
+
// Ensure required fields exist with defaults
|
|
78
|
+
if (!migrated.project) {
|
|
79
|
+
migrated.project = { name: null, created: today() };
|
|
80
|
+
}
|
|
81
|
+
if (!migrated.platform) {
|
|
82
|
+
migrated.platform = 'claude-code';
|
|
83
|
+
}
|
|
84
|
+
if (!migrated.phase || typeof migrated.phase !== 'object') {
|
|
85
|
+
migrated.phase = { current: 0, status: 'initialized' };
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(migrated.phase.phases)) {
|
|
88
|
+
migrated.phase.phases = [];
|
|
89
|
+
}
|
|
90
|
+
if (!('total' in migrated.phase)) {
|
|
91
|
+
migrated.phase.total = migrated.phase.phases.length || 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// v0.2.0 fields
|
|
95
|
+
if (!migrated.milestone) {
|
|
96
|
+
migrated.milestone = { current: 'v1.0', name: null, history: [] };
|
|
97
|
+
}
|
|
98
|
+
if (!migrated.session) {
|
|
99
|
+
migrated.session = { lastPaused: null, snapshotPath: null, contextWarningShown: false };
|
|
100
|
+
}
|
|
101
|
+
if (migrated.project && !('initialized' in migrated.project)) {
|
|
102
|
+
migrated.project.initialized = false;
|
|
103
|
+
}
|
|
104
|
+
if (migrated.project && !('projectType' in migrated.project)) {
|
|
105
|
+
migrated.project.projectType = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// v0.3.0 fields (skill system)
|
|
109
|
+
if (!migrated.skills) {
|
|
110
|
+
migrated.skills = {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// v0.4.0 fields (agent orchestration)
|
|
114
|
+
if (!migrated.agents) {
|
|
115
|
+
migrated.agents = { model: 'inherit', models: {} };
|
|
116
|
+
}
|
|
117
|
+
if (!migrated.workflow) {
|
|
118
|
+
migrated.workflow = { parallelization: false, mapper_parallelization: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// v0.5.0 fields (recovery and monitoring)
|
|
122
|
+
if (!migrated.monitoring) {
|
|
123
|
+
migrated.monitoring = { warning_threshold: 35, critical_threshold: 25, enabled: true };
|
|
124
|
+
}
|
|
125
|
+
if (migrated.workflow && !('auto_recover' in migrated.workflow)) {
|
|
126
|
+
migrated.workflow.auto_recover = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// v0.6.0 fields (differentiators)
|
|
130
|
+
if (migrated.workflow && !('advocate' in migrated.workflow)) {
|
|
131
|
+
migrated.workflow.advocate = true;
|
|
132
|
+
}
|
|
133
|
+
if (!migrated.complexity) {
|
|
134
|
+
migrated.complexity = { default_budget: 60, phase_overrides: {} };
|
|
135
|
+
}
|
|
136
|
+
if (!migrated.storm) {
|
|
137
|
+
migrated.storm = { port: 3456, auto_open: true };
|
|
138
|
+
}
|
|
139
|
+
if (!migrated.adr) {
|
|
140
|
+
migrated.adr = { auto_create: true, status_lifecycle: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// v0.7.0 fields (configuration system)
|
|
144
|
+
if (!migrated.mode) {
|
|
145
|
+
migrated.mode = 'interactive';
|
|
146
|
+
}
|
|
147
|
+
if (!migrated.depth) {
|
|
148
|
+
migrated.depth = 'deep';
|
|
149
|
+
}
|
|
150
|
+
if (!migrated.enforcement) {
|
|
151
|
+
migrated.enforcement = { level: 'hard', business_paths: [], non_business_paths: [] };
|
|
152
|
+
}
|
|
153
|
+
if (migrated.agents && !migrated.agents.profile) {
|
|
154
|
+
migrated.agents.profile = 'quality';
|
|
155
|
+
}
|
|
156
|
+
if (migrated.agents && !migrated.agents.profiles) {
|
|
157
|
+
migrated.agents.profiles = {};
|
|
158
|
+
}
|
|
159
|
+
if (!migrated.quick) {
|
|
160
|
+
migrated.quick = { count: 0 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return migrated;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Write state to brain.json and STATE.md atomically.
|
|
168
|
+
* @param {string} brainDir - Path to .brain/ directory
|
|
169
|
+
* @param {object} state - State object to write
|
|
170
|
+
*/
|
|
171
|
+
function writeState(brainDir, state) {
|
|
172
|
+
// Validate required fields
|
|
173
|
+
const required = ['$schema', 'version', 'project', 'platform', 'phase'];
|
|
174
|
+
for (const field of required) {
|
|
175
|
+
if (!(field in state)) {
|
|
176
|
+
throw new Error(`State missing required field: ${field}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Write brain.json atomically
|
|
181
|
+
const jsonPath = path.join(brainDir, 'brain.json');
|
|
182
|
+
atomicWriteSync(jsonPath, JSON.stringify(state, null, 2));
|
|
183
|
+
|
|
184
|
+
// Generate and write STATE.md atomically
|
|
185
|
+
const mdPath = path.join(brainDir, 'STATE.md');
|
|
186
|
+
atomicWriteSync(mdPath, generateStateMd(state));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate STATE.md content from state object.
|
|
191
|
+
* Uses bullet-list-under-headers format (not YAML frontmatter).
|
|
192
|
+
* @param {object} state - State object
|
|
193
|
+
* @returns {string} Markdown content
|
|
194
|
+
*/
|
|
195
|
+
function generateStateMd(state) {
|
|
196
|
+
const phase = state.phase || {};
|
|
197
|
+
const session = state.session || {};
|
|
198
|
+
const milestone = state.milestone || {};
|
|
199
|
+
const blockers = state.blockers;
|
|
200
|
+
|
|
201
|
+
// Support both old (number/string) and new (object) milestone format
|
|
202
|
+
const milestoneVersion = typeof milestone === 'object' ? milestone.current : milestone;
|
|
203
|
+
const milestoneName = typeof milestone === 'object' ? milestone.name : null;
|
|
204
|
+
|
|
205
|
+
const lines = [
|
|
206
|
+
'# Brain State',
|
|
207
|
+
'',
|
|
208
|
+
'## Current Position',
|
|
209
|
+
`- Phase: ${phase.current ?? 0}`,
|
|
210
|
+
`- Status: ${phase.status || 'initialized'}`,
|
|
211
|
+
`- Milestone: ${milestoneVersion || 'v1.0'}${milestoneName ? ` (${milestoneName})` : ''}`,
|
|
212
|
+
'',
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
// Phases list (v0.2.0)
|
|
216
|
+
if (Array.isArray(phase.phases) && phase.phases.length > 0) {
|
|
217
|
+
lines.push('## Phases');
|
|
218
|
+
for (const p of phase.phases) {
|
|
219
|
+
// Handle both string and object formats
|
|
220
|
+
if (typeof p === 'string') {
|
|
221
|
+
lines.push(`- ${p}`);
|
|
222
|
+
} else if (p && typeof p === 'object') {
|
|
223
|
+
lines.push(`- Phase ${p.number}: ${p.name} [${p.status || 'Pending'}]`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
lines.push('');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Session section (v0.2.0 format)
|
|
230
|
+
lines.push('## Session');
|
|
231
|
+
if (session.lastPaused) {
|
|
232
|
+
lines.push(`- Last paused: ${session.lastPaused}`);
|
|
233
|
+
}
|
|
234
|
+
if (session.snapshotPath) {
|
|
235
|
+
lines.push(`- Snapshot: ${session.snapshotPath}`);
|
|
236
|
+
}
|
|
237
|
+
// Legacy fields
|
|
238
|
+
if (session.stoppedAt) {
|
|
239
|
+
lines.push(`- Stopped at: ${session.stoppedAt}`);
|
|
240
|
+
}
|
|
241
|
+
if (session.resume) {
|
|
242
|
+
lines.push(`- Resume: ${session.resume}`);
|
|
243
|
+
}
|
|
244
|
+
if (!session.lastPaused && !session.snapshotPath && !session.stoppedAt && !session.resume) {
|
|
245
|
+
lines.push('- No active session');
|
|
246
|
+
}
|
|
247
|
+
lines.push('');
|
|
248
|
+
|
|
249
|
+
lines.push('## Blockers');
|
|
250
|
+
|
|
251
|
+
if (Array.isArray(blockers) && blockers.length > 0) {
|
|
252
|
+
for (const b of blockers) {
|
|
253
|
+
lines.push(`- ${b}`);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
lines.push('- None');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
lines.push('');
|
|
260
|
+
return lines.join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create a default state object for a new brain project.
|
|
265
|
+
* @param {string} platform - Detected platform identifier
|
|
266
|
+
* @returns {object} Default brain.json state
|
|
267
|
+
*/
|
|
268
|
+
function createDefaultState(platform) {
|
|
269
|
+
return {
|
|
270
|
+
$schema: CURRENT_SCHEMA,
|
|
271
|
+
version: CURRENT_VERSION,
|
|
272
|
+
project: {
|
|
273
|
+
name: null,
|
|
274
|
+
created: today(),
|
|
275
|
+
initialized: false,
|
|
276
|
+
projectType: null
|
|
277
|
+
},
|
|
278
|
+
platform: platform || 'claude-code',
|
|
279
|
+
phase: {
|
|
280
|
+
current: 0,
|
|
281
|
+
status: 'initialized',
|
|
282
|
+
total: 0,
|
|
283
|
+
phases: []
|
|
284
|
+
},
|
|
285
|
+
milestone: {
|
|
286
|
+
current: 'v1.0',
|
|
287
|
+
name: null,
|
|
288
|
+
history: []
|
|
289
|
+
},
|
|
290
|
+
session: {
|
|
291
|
+
lastPaused: null,
|
|
292
|
+
snapshotPath: null,
|
|
293
|
+
contextWarningShown: false
|
|
294
|
+
},
|
|
295
|
+
mode: 'interactive',
|
|
296
|
+
depth: 'deep',
|
|
297
|
+
agents: {
|
|
298
|
+
model: 'inherit',
|
|
299
|
+
models: {},
|
|
300
|
+
profile: 'quality',
|
|
301
|
+
profiles: {}
|
|
302
|
+
},
|
|
303
|
+
monitoring: {
|
|
304
|
+
warning_threshold: 35,
|
|
305
|
+
critical_threshold: 25,
|
|
306
|
+
enabled: true
|
|
307
|
+
},
|
|
308
|
+
skills: {},
|
|
309
|
+
workflow: {
|
|
310
|
+
parallelization: false,
|
|
311
|
+
mapper_parallelization: true,
|
|
312
|
+
advocate: true,
|
|
313
|
+
auto_recover: false
|
|
314
|
+
},
|
|
315
|
+
enforcement: {
|
|
316
|
+
level: 'hard',
|
|
317
|
+
business_paths: [],
|
|
318
|
+
non_business_paths: []
|
|
319
|
+
},
|
|
320
|
+
complexity: {
|
|
321
|
+
default_budget: 60,
|
|
322
|
+
phase_overrides: {}
|
|
323
|
+
},
|
|
324
|
+
storm: {
|
|
325
|
+
port: 3456,
|
|
326
|
+
auto_open: true
|
|
327
|
+
},
|
|
328
|
+
adr: {
|
|
329
|
+
auto_create: true,
|
|
330
|
+
status_lifecycle: true
|
|
331
|
+
},
|
|
332
|
+
quick: {
|
|
333
|
+
count: 0
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get today's date in ISO format (YYYY-MM-DD).
|
|
340
|
+
* @returns {string}
|
|
341
|
+
*/
|
|
342
|
+
function today() {
|
|
343
|
+
return new Date().toISOString().slice(0, 10);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
atomicWriteSync,
|
|
348
|
+
readState,
|
|
349
|
+
writeState,
|
|
350
|
+
generateStateMd,
|
|
351
|
+
createDefaultState,
|
|
352
|
+
migrateState
|
|
353
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load a template file from bin/templates/{name}.md.
|
|
10
|
+
* @param {string} name - Template name (without .md extension)
|
|
11
|
+
* @returns {string} Template content
|
|
12
|
+
*/
|
|
13
|
+
function loadTemplate(name) {
|
|
14
|
+
const filePath = path.join(TEMPLATES_DIR, `${name}.md`);
|
|
15
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Replace {{variable}} placeholders with values from vars object.
|
|
20
|
+
* Supports dot notation for nested access (e.g., {{project.name}}).
|
|
21
|
+
* @param {string} template - Template string with {{var}} placeholders
|
|
22
|
+
* @param {object} vars - Variables to interpolate
|
|
23
|
+
* @returns {string} Interpolated string
|
|
24
|
+
*/
|
|
25
|
+
function interpolate(template, vars) {
|
|
26
|
+
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, key) => {
|
|
27
|
+
const value = resolvePath(vars, key);
|
|
28
|
+
return value !== undefined ? String(value) : match;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a dot-notation path on an object.
|
|
34
|
+
* @param {object} obj
|
|
35
|
+
* @param {string} dotPath - e.g., "project.name"
|
|
36
|
+
* @returns {*}
|
|
37
|
+
*/
|
|
38
|
+
function resolvePath(obj, dotPath) {
|
|
39
|
+
const parts = dotPath.split('.');
|
|
40
|
+
let current = obj;
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
43
|
+
current = current[part];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { loadTemplate, interpolate };
|