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,100 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ /**
7
+ * Log an execution event as a JSONL line.
8
+ * Auto-adds timestamp and creates logs/ directory if missing.
9
+ *
10
+ * @param {string} brainDir - Path to .brain/ directory
11
+ * @param {number} phaseNumber - Phase number (padded to 2 digits)
12
+ * @param {object} event - Event data to log
13
+ */
14
+ function logEvent(brainDir, phaseNumber, event) {
15
+ const logsDir = path.join(brainDir, 'logs');
16
+ if (!fs.existsSync(logsDir)) {
17
+ fs.mkdirSync(logsDir, { recursive: true });
18
+ }
19
+
20
+ const padded = String(phaseNumber).padStart(2, '0');
21
+ const logPath = path.join(logsDir, `phase-${padded}-execution.jsonl`);
22
+
23
+ const entry = {
24
+ ...event,
25
+ timestamp: new Date().toISOString()
26
+ };
27
+
28
+ fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
29
+ }
30
+
31
+ /**
32
+ * Read all events from a phase execution log.
33
+ * Returns empty array if log file does not exist.
34
+ *
35
+ * @param {string} brainDir - Path to .brain/ directory
36
+ * @param {number} phaseNumber - Phase number
37
+ * @returns {object[]} Array of parsed event objects
38
+ */
39
+ function readLog(brainDir, phaseNumber) {
40
+ const padded = String(phaseNumber).padStart(2, '0');
41
+ const logPath = path.join(brainDir, 'logs', `phase-${padded}-execution.jsonl`);
42
+
43
+ if (!fs.existsSync(logPath)) {
44
+ return [];
45
+ }
46
+
47
+ const content = fs.readFileSync(logPath, 'utf8').trim();
48
+ if (!content) return [];
49
+
50
+ return content.split('\n').reduce((entries, line) => {
51
+ try {
52
+ entries.push(JSON.parse(line));
53
+ } catch {
54
+ // Skip corrupted/incomplete log lines
55
+ }
56
+ return entries;
57
+ }, []);
58
+ }
59
+
60
+ /**
61
+ * Archive a phase log and clean up old archives beyond keepLast.
62
+ *
63
+ * @param {string} brainDir - Path to .brain/ directory
64
+ * @param {number} phaseNumber - Phase number to archive
65
+ * @param {number} [keepLast=3] - Maximum number of archived logs to keep
66
+ */
67
+ function archiveLogs(brainDir, phaseNumber, keepLast = 3) {
68
+ const padded = String(phaseNumber).padStart(2, '0');
69
+ const logFileName = `phase-${padded}-execution.jsonl`;
70
+ const logPath = path.join(brainDir, 'logs', logFileName);
71
+
72
+ if (!fs.existsSync(logPath)) return;
73
+
74
+ const archiveDir = path.join(brainDir, 'logs', 'archive');
75
+ if (!fs.existsSync(archiveDir)) {
76
+ fs.mkdirSync(archiveDir, { recursive: true });
77
+ }
78
+
79
+ // Move log to archive
80
+ const archivePath = path.join(archiveDir, logFileName);
81
+ fs.renameSync(logPath, archivePath);
82
+
83
+ // Clean up old archives beyond keepLast
84
+ const archived = fs.readdirSync(archiveDir)
85
+ .filter(f => f.endsWith('.jsonl'))
86
+ .sort();
87
+
88
+ if (archived.length > keepLast) {
89
+ const toRemove = archived.slice(0, archived.length - keepLast);
90
+ for (const file of toRemove) {
91
+ fs.unlinkSync(path.join(archiveDir, file));
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = {
97
+ logEvent,
98
+ readLog,
99
+ archiveLogs
100
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ /**
7
+ * Detect which AI IDE platform is running.
8
+ * Checks environment variables and directory existence.
9
+ * @param {object} [opts]
10
+ * @param {string} [opts.cwd] - Working directory to check for platform indicators
11
+ * @returns {string} Platform identifier
12
+ */
13
+ function detectPlatform(opts = {}) {
14
+ const cwd = opts.cwd || process.cwd();
15
+
16
+ // Claude Code: CLAUDE_CODE env var or .claude/ directory
17
+ if (process.env.CLAUDE_CODE || safeExists(path.join(cwd, '.claude'))) {
18
+ return 'claude-code';
19
+ }
20
+
21
+ // Codex CLI: CODEX_HOME env var
22
+ if (process.env.CODEX_HOME) {
23
+ return 'codex';
24
+ }
25
+
26
+ // OpenCode: opencode.json in project root
27
+ if (safeExists(path.join(cwd, 'opencode.json'))) {
28
+ return 'opencode';
29
+ }
30
+
31
+ // Gemini CLI: GEMINI_API_KEY env var
32
+ if (process.env.GEMINI_API_KEY) {
33
+ return 'gemini';
34
+ }
35
+
36
+ // Cursor: .cursor/ directory
37
+ if (safeExists(path.join(cwd, '.cursor'))) {
38
+ return 'cursor';
39
+ }
40
+
41
+ // Default to claude-code (primary platform)
42
+ return 'claude-code';
43
+ }
44
+
45
+ /**
46
+ * Safe existence check that does not throw on permission errors.
47
+ * @param {string} p - Path to check
48
+ * @returns {boolean}
49
+ */
50
+ function safeExists(p) {
51
+ try {
52
+ return fs.existsSync(p);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ module.exports = { detectPlatform };
@@ -0,0 +1,158 @@
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
+ * Parse REQUIREMENTS.md from a directory into structured data.
9
+ * @param {string} brainDir - Path to directory containing REQUIREMENTS.md
10
+ * @returns {{ requirements: Array<{id: string, completed: boolean, description: string}>, traceability: Array<{id: string, phase: string, status: string}> }}
11
+ */
12
+ function parseRequirements(brainDir) {
13
+ const mdPath = path.join(brainDir, 'REQUIREMENTS.md');
14
+
15
+ if (!fs.existsSync(mdPath)) {
16
+ return { requirements: [], traceability: [] };
17
+ }
18
+
19
+ let content;
20
+ try {
21
+ content = fs.readFileSync(mdPath, 'utf8');
22
+ } catch {
23
+ return { requirements: [], traceability: [] };
24
+ }
25
+
26
+ // Parse checkbox items: - [x] **ID**: Description or - [ ] **ID**: Description
27
+ const requirements = [];
28
+ const checkboxRegex = /^- \[([ x])\] \*\*(\S+)\*\*:?\s*(.+)$/gm;
29
+ let match;
30
+
31
+ while ((match = checkboxRegex.exec(content)) !== null) {
32
+ requirements.push({
33
+ id: match[2],
34
+ completed: match[1] === 'x',
35
+ description: match[3].trim()
36
+ });
37
+ }
38
+
39
+ // Parse traceability table
40
+ const traceability = [];
41
+ const traceIdx = content.indexOf('## Traceability');
42
+ if (traceIdx >= 0) {
43
+ const traceSection = content.slice(traceIdx);
44
+ const lines = traceSection.split('\n');
45
+
46
+ for (const line of lines) {
47
+ // Skip header row and separator row
48
+ if (!line.startsWith('|') || line.includes('---') || line.includes('Requirement')) {
49
+ continue;
50
+ }
51
+ const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
52
+ if (cells.length >= 3) {
53
+ traceability.push({
54
+ id: cells[0],
55
+ phase: cells[1],
56
+ status: cells[2]
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ return { requirements, traceability };
63
+ }
64
+
65
+ /**
66
+ * Toggle a requirement's checkbox status.
67
+ * @param {string} brainDir - Path to directory containing REQUIREMENTS.md
68
+ * @param {string} reqId - Requirement ID (e.g., 'FOUND-01')
69
+ * @param {boolean} completed - true for [x], false for [ ]
70
+ */
71
+ function updateStatus(brainDir, reqId, completed) {
72
+ const mdPath = path.join(brainDir, 'REQUIREMENTS.md');
73
+ let content = fs.readFileSync(mdPath, 'utf8');
74
+
75
+ const oldChecked = `- [x] **${reqId}**`;
76
+ const oldUnchecked = `- [ ] **${reqId}**`;
77
+ const newMark = completed ? '- [x]' : '- [ ]';
78
+
79
+ if (completed && content.includes(oldUnchecked)) {
80
+ content = content.replace(oldUnchecked, `${newMark} **${reqId}**`);
81
+ } else if (!completed && content.includes(oldChecked)) {
82
+ content = content.replace(oldChecked, `${newMark} **${reqId}**`);
83
+ }
84
+
85
+ atomicWriteSync(mdPath, content);
86
+ }
87
+
88
+ /**
89
+ * Update a row in the traceability table.
90
+ * @param {string} brainDir - Path to directory containing REQUIREMENTS.md
91
+ * @param {string} reqId - Requirement ID
92
+ * @param {object} updates - Fields to update (e.g., { status: 'Complete', phase: 'Phase 2' })
93
+ */
94
+ function updateTraceability(brainDir, reqId, updates) {
95
+ const mdPath = path.join(brainDir, 'REQUIREMENTS.md');
96
+ let content = fs.readFileSync(mdPath, 'utf8');
97
+ const lines = content.split('\n');
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const line = lines[i];
101
+ if (!line.startsWith('|') || !line.includes(reqId)) continue;
102
+ // Skip header/separator
103
+ if (line.includes('---') || line.includes('Requirement')) continue;
104
+
105
+ const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
106
+ // cells: [id, phase, status]
107
+ if (cells.length >= 3 && cells[0] === reqId) {
108
+ if (updates.phase !== undefined) cells[1] = updates.phase;
109
+ if (updates.status !== undefined) cells[2] = updates.status;
110
+ lines[i] = `| ${cells[0]} | ${cells[1]} | ${cells[2]} |`;
111
+ break;
112
+ }
113
+ }
114
+
115
+ atomicWriteSync(mdPath, lines.join('\n'));
116
+ }
117
+
118
+ /**
119
+ * Get requirements for a specific phase number.
120
+ * @param {string} brainDir - Path to directory containing REQUIREMENTS.md
121
+ * @param {number} phaseNumber - Phase number to filter by
122
+ * @returns {Array<{id: string, completed: boolean, description: string}>}
123
+ */
124
+ function getPhaseRequirements(brainDir, phaseNumber) {
125
+ const { requirements, traceability } = parseRequirements(brainDir);
126
+
127
+ // Find requirement IDs belonging to this phase
128
+ const phaseReqIds = new Set(
129
+ traceability
130
+ .filter(t => t.phase === `Phase ${phaseNumber}`)
131
+ .map(t => t.id)
132
+ );
133
+
134
+ return requirements.filter(r => phaseReqIds.has(r.id));
135
+ }
136
+
137
+ /**
138
+ * Get completion statistics for all requirements.
139
+ * @param {string} brainDir - Path to directory containing REQUIREMENTS.md
140
+ * @returns {{ total: number, completed: number, pending: number, percent: number }}
141
+ */
142
+ function getCompletionStats(brainDir) {
143
+ const { requirements } = parseRequirements(brainDir);
144
+ const total = requirements.length;
145
+ const completed = requirements.filter(r => r.completed).length;
146
+ const pending = total - completed;
147
+ const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
148
+
149
+ return { total, completed, pending, percent };
150
+ }
151
+
152
+ module.exports = {
153
+ parseRequirements,
154
+ updateStatus,
155
+ updateTraceability,
156
+ getPhaseRequirements,
157
+ getCompletionStats
158
+ };
@@ -0,0 +1,228 @@
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
+ * Parse ROADMAP.md from a .brain directory into a structured object.
9
+ * @param {string} brainDir - Path to .brain/ directory
10
+ * @returns {{ overview: string, phases: Array<{number: number, name: string, goal: string, dependsOn: number[], requirements: string[], plans: string, status: string}> }}
11
+ */
12
+ function parseRoadmap(brainDir) {
13
+ const mdPath = path.join(brainDir, 'ROADMAP.md');
14
+ let content;
15
+ try {
16
+ content = fs.readFileSync(mdPath, 'utf8');
17
+ } catch {
18
+ return { overview: '', phases: [] };
19
+ }
20
+
21
+ // Extract overview: everything between first heading and first ## or ### Phase
22
+ const overviewMatch = content.match(/^# .+\n([\s\S]*?)(?=\n## |\n### Phase )/m);
23
+ const overview = overviewMatch ? overviewMatch[1].trim() : '';
24
+
25
+ // Extract phases section: try multiple heading patterns
26
+ // Supports: "## Phases", "## Milestone:", or fallback to whole content
27
+ let phasesIdx = content.indexOf('## Phases');
28
+ if (phasesIdx < 0) {
29
+ // Try to find first ### Phase heading directly
30
+ phasesIdx = content.search(/### Phase [\d.]+/);
31
+ }
32
+ const progressIdx = content.indexOf('\n## Progress');
33
+ const summaryIdx = content.indexOf('\n## Phase Summary');
34
+ const endIdx = progressIdx >= 0 ? progressIdx : (summaryIdx >= 0 ? summaryIdx : undefined);
35
+ const phasesSection = phasesIdx >= 0
36
+ ? content.slice(phasesIdx, endIdx)
37
+ : content; // fallback: parse entire content for ### Phase patterns
38
+
39
+ // Parse individual phases: ## Phase N: Name or ### Phase N: Name (both formats)
40
+ const phaseRegex = /#{2,3} Phase ([\d.]+):?\s+(.+)\n([\s\S]*?)(?=\n#{2,3} Phase |\n## (?!Phase)|$)/g;
41
+ const phases = [];
42
+ let match;
43
+
44
+ while ((match = phaseRegex.exec(phasesSection)) !== null) {
45
+ const number = parseFloat(match[1]);
46
+ const name = match[2].trim();
47
+ const body = match[3];
48
+
49
+ // Flexible matching: accept both "- **Goal:**" and "**Goal:**" formats
50
+ const goalMatch = body.match(/-?\s*\*\*Goal:\*\*\s*(.+)/);
51
+ const dependsMatch = body.match(/-?\s*\*\*Depends on:\*\*\s*(.+)/);
52
+ const reqMatch = body.match(/-?\s*\*\*Requirements:\*\*\s*(.+)/);
53
+ const plansMatch = body.match(/-?\s*\*\*Plans:\*\*\s*(.+)/);
54
+ const statusMatch = body.match(/-?\s*\*\*Status:\*\*\s*(.+)/);
55
+
56
+ const dependsStr = dependsMatch ? dependsMatch[1].trim() : 'None';
57
+ const dependsOn = (!dependsStr || dependsStr === 'None' || dependsStr === 'none')
58
+ ? []
59
+ : dependsStr.split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n));
60
+
61
+ const reqStr = reqMatch ? reqMatch[1].trim() : '';
62
+ const requirements = (!reqStr || reqStr === 'None' || reqStr === 'none')
63
+ ? []
64
+ : reqStr.split(',').map(s => s.trim()).filter(s => s.length > 0);
65
+
66
+ phases.push({
67
+ number,
68
+ name,
69
+ goal: goalMatch ? goalMatch[1].trim() : '',
70
+ dependsOn,
71
+ requirements,
72
+ plans: plansMatch ? plansMatch[1].trim() : '',
73
+ status: statusMatch ? statusMatch[1].trim() : 'Pending'
74
+ });
75
+ }
76
+
77
+ // Sort numerically
78
+ phases.sort((a, b) => a.number - b.number);
79
+
80
+ return { overview, phases };
81
+ }
82
+
83
+ /**
84
+ * Write a structured roadmap object back to ROADMAP.md.
85
+ * @param {string} brainDir - Path to .brain/ directory
86
+ * @param {{ overview: string, phases: Array }} roadmapData - Structured roadmap
87
+ */
88
+ function writeRoadmap(brainDir, roadmapData) {
89
+ const lines = ['# Roadmap', '', roadmapData.overview, '', '## Phases', ''];
90
+
91
+ for (const phase of roadmapData.phases) {
92
+ lines.push(`### Phase ${formatPhaseNumber(phase.number)}: ${phase.name}`);
93
+ lines.push('');
94
+ lines.push(`- **Goal:** ${phase.goal}`);
95
+ lines.push(`- **Depends on:** ${phase.dependsOn.length === 0 ? 'None' : phase.dependsOn.map(n => formatPhaseNumber(n)).join(', ')}`);
96
+ lines.push(`- **Requirements:** ${phase.requirements.length === 0 ? 'None' : phase.requirements.join(', ')}`);
97
+ if (phase.plans) {
98
+ lines.push(`- **Plans:** ${phase.plans}`);
99
+ }
100
+ lines.push(`- **Status:** ${phase.status}`);
101
+ lines.push('');
102
+ }
103
+
104
+ // Progress table
105
+ lines.push('## Progress');
106
+ lines.push('');
107
+ lines.push('| Phase | Status | Plans |');
108
+ lines.push('|-------|--------|-------|');
109
+ for (const phase of roadmapData.phases) {
110
+ lines.push(`| ${formatPhaseNumber(phase.number)} | ${phase.status} | ${phase.plans || '0/0'} |`);
111
+ }
112
+ lines.push('');
113
+
114
+ const mdPath = path.join(brainDir, 'ROADMAP.md');
115
+ atomicWriteSync(mdPath, lines.join('\n'));
116
+ }
117
+
118
+ /**
119
+ * Format phase number: integers without decimal, decimals with one decimal place.
120
+ * @param {number} n
121
+ * @returns {string}
122
+ */
123
+ function formatPhaseNumber(n) {
124
+ return Number.isInteger(n) ? String(n) : n.toFixed(1);
125
+ }
126
+
127
+ /**
128
+ * Insert a new phase after a given phase number.
129
+ * Assigns next decimal number (e.g., after 2 creates 2.1, if 2.1 exists creates 2.2).
130
+ * @param {{ overview: string, phases: Array }} roadmapData
131
+ * @param {number} afterNumber - Phase number to insert after
132
+ * @param {{ name: string, goal: string, requirements?: string[], status?: string }} phaseDef
133
+ * @returns {{ overview: string, phases: Array }}
134
+ */
135
+ function insertPhase(roadmapData, afterNumber, phaseDef) {
136
+ const base = Math.floor(afterNumber);
137
+
138
+ // Find existing decimal phases for this base
139
+ const existing = roadmapData.phases
140
+ .filter(p => Math.floor(p.number) === base && p.number !== base)
141
+ .map(p => p.number);
142
+
143
+ // Calculate next decimal
144
+ let nextDecimal;
145
+ if (existing.length === 0) {
146
+ nextDecimal = base + 0.1;
147
+ } else {
148
+ const maxExisting = Math.max(...existing);
149
+ nextDecimal = Math.round((maxExisting + 0.1) * 10) / 10;
150
+ }
151
+
152
+ const newPhase = {
153
+ number: nextDecimal,
154
+ name: phaseDef.name,
155
+ goal: phaseDef.goal,
156
+ dependsOn: [afterNumber],
157
+ requirements: phaseDef.requirements || [],
158
+ plans: phaseDef.plans || '',
159
+ status: phaseDef.status || 'Pending'
160
+ };
161
+
162
+ const phases = [...roadmapData.phases, newPhase];
163
+ phases.sort((a, b) => a.number - b.number);
164
+
165
+ return { overview: roadmapData.overview, phases };
166
+ }
167
+
168
+ /**
169
+ * Remove a phase from the roadmap.
170
+ * @param {{ overview: string, phases: Array }} roadmapData
171
+ * @param {number} number - Phase number to remove
172
+ * @returns {{ roadmap: { overview: string, phases: Array }, orphanedRequirements: string[] }}
173
+ */
174
+ function removePhase(roadmapData, number) {
175
+ const removed = roadmapData.phases.find(p => p.number === number);
176
+ const orphanedRequirements = removed ? removed.requirements : [];
177
+
178
+ const phases = roadmapData.phases
179
+ .filter(p => p.number !== number)
180
+ .map(p => {
181
+ // Update depends_on: remove references to deleted phase
182
+ const dependsOn = p.dependsOn.filter(d => d !== number);
183
+ return { ...p, dependsOn };
184
+ });
185
+
186
+ return { roadmap: { overview: roadmapData.overview, phases }, orphanedRequirements };
187
+ }
188
+
189
+ /**
190
+ * Reorder phases by providing the new order of original phase numbers.
191
+ * Renumbers all phases sequentially and updates depends_on references.
192
+ * @param {{ overview: string, phases: Array }} roadmapData
193
+ * @param {number[]} newOrder - Array of original phase numbers in desired order
194
+ * @returns {{ overview: string, phases: Array }}
195
+ */
196
+ function reorderPhases(roadmapData, newOrder) {
197
+ // Build mapping: old number -> new number (1-based sequential)
198
+ const numberMap = {};
199
+ for (let i = 0; i < newOrder.length; i++) {
200
+ numberMap[newOrder[i]] = i + 1;
201
+ }
202
+
203
+ // Reorder and renumber
204
+ const phases = newOrder.map((oldNum, i) => {
205
+ const phase = roadmapData.phases.find(p => p.number === oldNum);
206
+ if (!phase) return null;
207
+
208
+ const dependsOn = phase.dependsOn
209
+ .map(d => numberMap[d])
210
+ .filter(d => d !== undefined);
211
+
212
+ return {
213
+ ...phase,
214
+ number: i + 1,
215
+ dependsOn
216
+ };
217
+ }).filter(Boolean);
218
+
219
+ return { overview: roadmapData.overview, phases };
220
+ }
221
+
222
+ module.exports = {
223
+ parseRoadmap,
224
+ writeRoadmap,
225
+ insertPhase,
226
+ removePhase,
227
+ reorderPhases
228
+ };