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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/agents/brain-checker.md +33 -0
  4. package/agents/brain-debugger.md +35 -0
  5. package/agents/brain-executor.md +37 -0
  6. package/agents/brain-mapper.md +44 -0
  7. package/agents/brain-planner.md +49 -0
  8. package/agents/brain-researcher.md +47 -0
  9. package/agents/brain-synthesizer.md +43 -0
  10. package/agents/brain-verifier.md +41 -0
  11. package/bin/brain-tools.cjs +185 -0
  12. package/bin/lib/adr.cjs +283 -0
  13. package/bin/lib/agents.cjs +152 -0
  14. package/bin/lib/anti-patterns.cjs +183 -0
  15. package/bin/lib/audit.cjs +268 -0
  16. package/bin/lib/commands/adr.cjs +126 -0
  17. package/bin/lib/commands/complete.cjs +270 -0
  18. package/bin/lib/commands/config.cjs +306 -0
  19. package/bin/lib/commands/discuss.cjs +237 -0
  20. package/bin/lib/commands/execute.cjs +415 -0
  21. package/bin/lib/commands/health.cjs +103 -0
  22. package/bin/lib/commands/map.cjs +101 -0
  23. package/bin/lib/commands/new-project.cjs +885 -0
  24. package/bin/lib/commands/pause.cjs +142 -0
  25. package/bin/lib/commands/phase-manage.cjs +357 -0
  26. package/bin/lib/commands/plan.cjs +451 -0
  27. package/bin/lib/commands/progress.cjs +167 -0
  28. package/bin/lib/commands/quick.cjs +447 -0
  29. package/bin/lib/commands/resume.cjs +196 -0
  30. package/bin/lib/commands/storm.cjs +590 -0
  31. package/bin/lib/commands/verify.cjs +504 -0
  32. package/bin/lib/commands.cjs +263 -0
  33. package/bin/lib/complexity.cjs +138 -0
  34. package/bin/lib/complexity.test.cjs +108 -0
  35. package/bin/lib/config.cjs +452 -0
  36. package/bin/lib/core.cjs +62 -0
  37. package/bin/lib/detect.cjs +603 -0
  38. package/bin/lib/git.cjs +112 -0
  39. package/bin/lib/health.cjs +356 -0
  40. package/bin/lib/init.cjs +310 -0
  41. package/bin/lib/logger.cjs +100 -0
  42. package/bin/lib/platform.cjs +58 -0
  43. package/bin/lib/requirements.cjs +158 -0
  44. package/bin/lib/roadmap.cjs +228 -0
  45. package/bin/lib/security.cjs +237 -0
  46. package/bin/lib/state.cjs +353 -0
  47. package/bin/lib/templates.cjs +48 -0
  48. package/bin/templates/advocate.md +182 -0
  49. package/bin/templates/checkpoint.md +55 -0
  50. package/bin/templates/debugger.md +148 -0
  51. package/bin/templates/discuss.md +60 -0
  52. package/bin/templates/executor.md +201 -0
  53. package/bin/templates/mapper.md +129 -0
  54. package/bin/templates/plan-checker.md +134 -0
  55. package/bin/templates/planner.md +165 -0
  56. package/bin/templates/researcher.md +78 -0
  57. package/bin/templates/storm.html +376 -0
  58. package/bin/templates/synthesis.md +30 -0
  59. package/bin/templates/verifier.md +181 -0
  60. package/commands/brain/adr.md +34 -0
  61. package/commands/brain/complete.md +37 -0
  62. package/commands/brain/config.md +37 -0
  63. package/commands/brain/discuss.md +35 -0
  64. package/commands/brain/execute.md +38 -0
  65. package/commands/brain/health.md +33 -0
  66. package/commands/brain/map.md +35 -0
  67. package/commands/brain/new-project.md +38 -0
  68. package/commands/brain/pause.md +26 -0
  69. package/commands/brain/plan.md +38 -0
  70. package/commands/brain/progress.md +28 -0
  71. package/commands/brain/quick.md +51 -0
  72. package/commands/brain/resume.md +28 -0
  73. package/commands/brain/storm.md +30 -0
  74. package/commands/brain/verify.md +39 -0
  75. package/hooks/bootstrap.sh +54 -0
  76. package/hooks/post-tool-use.sh +45 -0
  77. package/hooks/statusline.sh +130 -0
  78. 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 };