mindforge-cc 1.0.5 → 2.0.0-alpha.6

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 (119) hide show
  1. package/.agent/CLAUDE.md +83 -0
  2. package/.agent/mindforge/auto.md +22 -0
  3. package/.agent/mindforge/browse.md +26 -0
  4. package/.agent/mindforge/costs.md +11 -0
  5. package/.agent/mindforge/cross-review.md +17 -0
  6. package/.agent/mindforge/dashboard.md +98 -0
  7. package/.agent/mindforge/execute-phase.md +5 -3
  8. package/.agent/mindforge/init-project.md +12 -0
  9. package/.agent/mindforge/qa.md +16 -0
  10. package/.agent/mindforge/remember.md +14 -0
  11. package/.agent/mindforge/research.md +11 -0
  12. package/.agent/mindforge/steer.md +13 -0
  13. package/.agent/workflows/publish-release.md +36 -0
  14. package/.claude/CLAUDE.md +83 -0
  15. package/.claude/commands/mindforge/auto.md +22 -0
  16. package/.claude/commands/mindforge/browse.md +26 -0
  17. package/.claude/commands/mindforge/costs.md +11 -0
  18. package/.claude/commands/mindforge/cross-review.md +17 -0
  19. package/.claude/commands/mindforge/dashboard.md +98 -0
  20. package/.claude/commands/mindforge/execute-phase.md +5 -3
  21. package/.claude/commands/mindforge/qa.md +16 -0
  22. package/.claude/commands/mindforge/remember.md +14 -0
  23. package/.claude/commands/mindforge/research.md +11 -0
  24. package/.claude/commands/mindforge/steer.md +13 -0
  25. package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
  26. package/.mindforge/browser/daemon-protocol.md +24 -0
  27. package/.mindforge/browser/qa-engine.md +16 -0
  28. package/.mindforge/browser/session-manager.md +18 -0
  29. package/.mindforge/browser/visual-verify-spec.md +31 -0
  30. package/.mindforge/dashboard/api-reference.md +122 -0
  31. package/.mindforge/dashboard/dashboard-spec.md +96 -0
  32. package/.mindforge/engine/autonomous/auto-executor.md +266 -0
  33. package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
  34. package/.mindforge/engine/autonomous/node-repair.md +190 -0
  35. package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
  36. package/.mindforge/engine/autonomous/steering-manager.md +64 -0
  37. package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
  38. package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
  39. package/.mindforge/memory/decision-library.jsonl +0 -0
  40. package/.mindforge/memory/engine/capture-protocol.md +36 -0
  41. package/.mindforge/memory/engine/global-sync-spec.md +42 -0
  42. package/.mindforge/memory/engine/retrieval-spec.md +44 -0
  43. package/.mindforge/memory/knowledge-base.jsonl +7 -0
  44. package/.mindforge/memory/pattern-library.jsonl +1 -0
  45. package/.mindforge/memory/team-preferences.jsonl +4 -0
  46. package/.mindforge/models/model-registry.md +48 -0
  47. package/.mindforge/models/model-router.md +30 -0
  48. package/.mindforge/personas/research-agent.md +24 -0
  49. package/.planning/approvals/v2-architecture-approval.json +15 -0
  50. package/.planning/browser-daemon.log +32 -0
  51. package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
  52. package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
  53. package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
  54. package/CHANGELOG.md +81 -0
  55. package/MINDFORGE.md +26 -3
  56. package/README.md +70 -18
  57. package/bin/autonomous/auto-runner.js +95 -0
  58. package/bin/autonomous/headless.js +36 -0
  59. package/bin/autonomous/progress-stream.js +49 -0
  60. package/bin/autonomous/repair-operator.js +213 -0
  61. package/bin/autonomous/steer.js +71 -0
  62. package/bin/autonomous/stuck-monitor.js +77 -0
  63. package/bin/browser/browser-daemon.js +139 -0
  64. package/bin/browser/daemon-manager.js +91 -0
  65. package/bin/browser/qa-engine.js +47 -0
  66. package/bin/browser/qa-report-writer.js +32 -0
  67. package/bin/browser/regression-writer.js +27 -0
  68. package/bin/browser/screenshot-store.js +49 -0
  69. package/bin/browser/session-manager.js +93 -0
  70. package/bin/browser/visual-verify-executor.js +89 -0
  71. package/bin/change-classifier.js +86 -0
  72. package/bin/dashboard/api-router.js +198 -0
  73. package/bin/dashboard/approval-handler.js +134 -0
  74. package/bin/dashboard/frontend/index.html +511 -0
  75. package/bin/dashboard/metrics-aggregator.js +296 -0
  76. package/bin/dashboard/server.js +135 -0
  77. package/bin/dashboard/sse-bridge.js +178 -0
  78. package/bin/dashboard/team-tracker.js +0 -0
  79. package/bin/governance/approve.js +60 -0
  80. package/bin/install.js +4 -4
  81. package/bin/installer-core.js +91 -35
  82. package/bin/memory/cli.js +99 -0
  83. package/bin/memory/global-sync.js +107 -0
  84. package/bin/memory/knowledge-capture.js +278 -0
  85. package/bin/memory/knowledge-indexer.js +172 -0
  86. package/bin/memory/knowledge-store.js +319 -0
  87. package/bin/memory/session-memory-loader.js +137 -0
  88. package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
  89. package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
  90. package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
  91. package/bin/migrations/migrate.js +15 -11
  92. package/bin/mindforge-cli.js +87 -0
  93. package/bin/models/anthropic-provider.js +77 -0
  94. package/bin/models/cost-tracker.js +118 -0
  95. package/bin/models/gemini-provider.js +79 -0
  96. package/bin/models/model-client.js +98 -0
  97. package/bin/models/model-router.js +111 -0
  98. package/bin/models/openai-provider.js +78 -0
  99. package/bin/research/research-engine.js +115 -0
  100. package/bin/review/cross-review-engine.js +81 -0
  101. package/bin/review/finding-synthesizer.js +116 -0
  102. package/bin/review/review-report-writer.js +49 -0
  103. package/bin/updater/self-update.js +13 -13
  104. package/bin/wizard/setup-wizard.js +5 -1
  105. package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
  106. package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
  107. package/docs/adr/ADR-026-session-persistence-security.md +20 -0
  108. package/docs/architecture/README.md +6 -2
  109. package/docs/ci-cd.md +92 -0
  110. package/docs/commands-reference.md +1 -0
  111. package/docs/feature-dashboard.md +52 -0
  112. package/docs/publishing-guide.md +43 -0
  113. package/docs/reference/commands.md +17 -2
  114. package/docs/reference/sdk-api.md +6 -1
  115. package/docs/testing-current-version.md +130 -0
  116. package/docs/user-guide.md +115 -9
  117. package/docs/usp-features.md +70 -8
  118. package/docs/workflow-atlas.md +57 -0
  119. package/package.json +7 -3
@@ -0,0 +1,89 @@
1
+ /**
2
+ * MindForge v2 — Visual Verify Executor
3
+ * Parses <verify-visual> blocks and runs them against the daemon.
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const DaemonMgr = require('./daemon-manager');
10
+ const ScreenStore = require('./screenshot-store');
11
+
12
+ const DEV_SERVER = process.env.DEV_SERVER_URL || 'http://localhost:3000';
13
+
14
+ function extractBlock(planContent) {
15
+ const m = planContent.match(/<verify-visual([^>]*)>([\s\S]*?)<\/verify-visual>/);
16
+ if (!m) return null;
17
+ const sessionM = m[1].match(/session\s*=\s*["']([^"']+)["']/);
18
+ return { content: m[2].trim(), session: sessionM?.[1] ?? 'default' };
19
+ }
20
+
21
+ function parseDirectives(content) {
22
+ return content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).map(line => {
23
+ const colon = line.indexOf(':');
24
+ if (colon === -1) return null;
25
+ const directive = line.slice(0, colon).trim();
26
+ const rawArgs = line.slice(colon + 1).trim();
27
+ const args = [];
28
+ const re = /"([^"]*)"|\S+/g;
29
+ let m;
30
+ while ((m = re.exec(rawArgs)) !== null) args.push(m[1] !== undefined ? m[1] : m[0]);
31
+ return { directive, args };
32
+ }).filter(Boolean);
33
+ }
34
+
35
+ async function executeBlock(phaseNum, planId, planContent) {
36
+ const block = extractBlock(planContent);
37
+ if (!block) return { passed: true, steps: [], skipped: true };
38
+
39
+ await DaemonMgr.ensureRunning();
40
+ const directives = parseDirectives(block.content);
41
+ const steps = [];
42
+ const screenshots = [];
43
+ let passed = true;
44
+
45
+ for (const { directive, args } of directives) {
46
+ const step = { directive: `${directive}: ${args.join(' ')}`, status: 'pass', detail: '' };
47
+ try {
48
+ let r;
49
+ switch (directive) {
50
+ case 'navigate':
51
+ r = await DaemonMgr.request('POST', '/navigate', { url: args[0].startsWith('http') ? args[0] : `${DEV_SERVER}${args[0]}`, session: block.session });
52
+ step.detail = `${r.status_code} OK`;
53
+ break;
54
+ case 'wait':
55
+ await new Promise(res => setTimeout(res, parseInt(args[0]) || 500));
56
+ break;
57
+ case 'assert-visible':
58
+ r = await DaemonMgr.request('POST', '/assert', { type: 'visible', selector: args[0], expected_text: args[1], session: block.session });
59
+ if (!r.passed) { step.status = 'fail'; passed = false; }
60
+ break;
61
+ case 'screenshot':
62
+ r = await DaemonMgr.request('POST', '/screenshot', { session: block.session });
63
+ if (r.success) screenshots.push(ScreenStore.save(r.screenshot_b64, phaseNum, planId, args[0]));
64
+ break;
65
+ case 'click':
66
+ r = await DaemonMgr.request('POST', '/click', { selector: args[0], session: block.session });
67
+ if (!r.success) { step.status = 'fail'; passed = false; }
68
+ break;
69
+ }
70
+ } catch (err) {
71
+ step.status = 'fail'; step.detail = err.message; passed = false;
72
+ }
73
+ steps.push(step);
74
+ if (!passed) break;
75
+ }
76
+ return { passed, steps, screenshots, session: block.session };
77
+ }
78
+
79
+ function writeReport(phaseNum, planId, result) {
80
+ const dir = path.join(process.cwd(), '.planning', 'phases', String(phaseNum));
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ const content = `# Visual Verify Result\nStatus: ${result.passed ? '✅ PASS' : '❌ FAIL'}\n\n` +
83
+ result.steps.map(s => `- ${s.directive} [${s.status}] ${s.detail}`).join('\n');
84
+ const file = path.join(dir, `VISUAL-VERIFY-${phaseNum}-${planId}.md`);
85
+ fs.writeFileSync(file, content);
86
+ return file;
87
+ }
88
+
89
+ module.exports = { executeBlock, writeReport };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MindForge Change Classifier
4
+ * Categorizes changes into Tiers based on risk and sensitivity.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { execSync } = require('child_process');
10
+ const fs = require('fs');
11
+
12
+ const SENSITIVE_PATHS = [
13
+ 'auth/',
14
+ 'payment/',
15
+ 'security/',
16
+ '.github/workflows/',
17
+ '.mindforge/governance/',
18
+ 'bin/models/'
19
+ ];
20
+
21
+ const SENSITIVE_PATTERNS = [
22
+ /jwt/i,
23
+ /bcrypt/i,
24
+ /stripe/i,
25
+ /apiKey/i,
26
+ /password/i,
27
+ /secret/i,
28
+ /token/i,
29
+ /PII/
30
+ ];
31
+
32
+ function classify() {
33
+ try {
34
+ // Get list of changed files compared to origin/main or HEAD~1
35
+ const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : 'HEAD~1';
36
+ const diffFiles = execSync(`git diff --name-only ${base}..HEAD`, { encoding: 'utf8' }).split('\n').filter(Boolean);
37
+
38
+ let tier = 1;
39
+ let reasons = [];
40
+
41
+ // 1. Path-based detection (Tier 3)
42
+ const matchedPath = diffFiles.find(file => SENSITIVE_PATHS.some(p => file.startsWith(p)));
43
+ if (matchedPath) {
44
+ tier = 3;
45
+ reasons.push(`Sensitive path modified: ${matchedPath}`);
46
+ }
47
+
48
+ // 2. Pattern-based detection in diff (Tier 3)
49
+ if (tier < 3) {
50
+ const diffContent = execSync(`git diff ${base}..HEAD`, { encoding: 'utf8' });
51
+ for (const pattern of SENSITIVE_PATTERNS) {
52
+ if (pattern.test(diffContent)) {
53
+ tier = 3;
54
+ reasons.push(`Sensitive pattern detected: ${pattern}`);
55
+ break;
56
+ }
57
+ }
58
+ }
59
+
60
+ // 3. Simple change (Tier 1 vs 2)
61
+ if (tier < 3) {
62
+ if (diffFiles.length > 10 || diffFiles.some(f => f.endsWith('.js') || f.endsWith('.ts'))) {
63
+ tier = 2; // Significant logic change
64
+ }
65
+ }
66
+
67
+ console.log(`TIER=${tier}`);
68
+ console.log(`REASONS=${reasons.join('; ')}`);
69
+
70
+ // Write to GITHUB_OUTPUT if available
71
+ if (process.env.GITHUB_OUTPUT) {
72
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `tier=${tier}\n`);
73
+ }
74
+
75
+ return tier;
76
+ } catch (err) {
77
+ console.error(`❌ Classification failed: ${err.message}`);
78
+ // Default to Tier 3 for safety if classification fails
79
+ if (process.env.GITHUB_OUTPUT) {
80
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `tier=3\n`);
81
+ }
82
+ process.exit(0); // Don't fail the pipeline yet, let the gate handle it
83
+ }
84
+ }
85
+
86
+ classify();
@@ -0,0 +1,198 @@
1
+ /**
2
+ * MindForge v2 — Dashboard API Router
3
+ * All REST endpoints for the dashboard.
4
+ */
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const Metrics = require('./metrics-aggregator');
10
+ const Approval = require('./approval-handler');
11
+ const SSE = require('./sse-bridge');
12
+
13
+ // Steering queue path (from Day 8 auto-executor)
14
+ const STEERING_QUEUE = path.join(process.cwd(), '.planning', 'steering-queue.jsonl');
15
+ const AUTO_STATE_PATH = path.join(process.cwd(), '.planning', 'auto-state.json');
16
+
17
+ function register(app) {
18
+
19
+ // ── SSE stream ──────────────────────────────────────────────────────────────
20
+ app.get('/events', (req, res) => {
21
+ res.setHeader('Content-Type', 'text/event-stream');
22
+ res.setHeader('Cache-Control', 'no-cache');
23
+ res.setHeader('Connection', 'keep-alive');
24
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
25
+ res.flushHeaders();
26
+
27
+ // Send current status immediately on connect
28
+ try {
29
+ const status = Metrics.getStatus();
30
+ res.write(`event: status:update\ndata: ${JSON.stringify(status)}\n\n`);
31
+ } catch { /* ignore */ }
32
+
33
+ SSE.addClient(res);
34
+ });
35
+
36
+ // ── Status ──────────────────────────────────────────────────────────────────
37
+ app.get('/api/status', (req, res) => {
38
+ try {
39
+ res.json(Metrics.getStatus());
40
+ } catch (err) {
41
+ res.status(500).json({ error: err.message });
42
+ }
43
+ });
44
+
45
+ // ── Audit entries ───────────────────────────────────────────────────────────
46
+ app.get('/api/audit', (req, res) => {
47
+ try {
48
+ const limit = Math.min(parseInt(req.query.limit || '50', 10), 200);
49
+ const offset = Math.max(parseInt(req.query.offset || '0', 10), 0);
50
+ const event = typeof req.query.event === 'string' ? req.query.event : null;
51
+ res.json(Metrics.getAuditEntries(limit, offset, event));
52
+ } catch (err) {
53
+ res.status(500).json({ error: err.message });
54
+ }
55
+ });
56
+
57
+ // ── Quality metrics ─────────────────────────────────────────────────────────
58
+ app.get('/api/metrics', (req, res) => {
59
+ try {
60
+ res.json(Metrics.getMetrics());
61
+ } catch (err) {
62
+ res.status(500).json({ error: err.message });
63
+ }
64
+ });
65
+
66
+ // ── Approvals ───────────────────────────────────────────────────────────────
67
+ app.get('/api/approvals', (req, res) => {
68
+ try {
69
+ res.json(Metrics.getApprovals());
70
+ } catch (err) {
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ app.post('/api/approve/:id', (req, res) => {
76
+ try {
77
+ const { id } = req.params;
78
+ const { decision, comment, approver } = req.body || {};
79
+
80
+ if (!decision) {
81
+ return res.status(400).json({ error: 'Missing "decision" field (approve|reject)' });
82
+ }
83
+
84
+ const result = Approval.processDecision(id, decision, comment, approver);
85
+
86
+ if (!result.success) {
87
+ return res.status(400).json(result);
88
+ }
89
+
90
+ // Broadcast approval event to all SSE clients
91
+ SSE.broadcast(
92
+ decision === 'approve' ? 'approval:resolved' : 'approval:resolved',
93
+ { approval_id: id, decision, message: result.message }
94
+ );
95
+
96
+ res.json(result);
97
+ } catch (err) {
98
+ res.status(500).json({ error: err.message });
99
+ }
100
+ });
101
+
102
+ // ── Team activity ───────────────────────────────────────────────────────────
103
+ app.get('/api/team', (req, res) => {
104
+ try {
105
+ res.json(Metrics.getTeamActivity());
106
+ } catch (err) {
107
+ res.status(500).json({ error: err.message });
108
+ }
109
+ });
110
+
111
+ // ── Knowledge base query ────────────────────────────────────────────────────
112
+ app.get('/api/memory', (req, res) => {
113
+ try {
114
+ const q = typeof req.query.q === 'string' ? req.query.q.slice(0, 100) : '';
115
+ const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
116
+ res.json(Metrics.getMemory(q, limit));
117
+ } catch (err) {
118
+ res.status(500).json({ error: err.message });
119
+ }
120
+ });
121
+
122
+ // ── Costs ───────────────────────────────────────────────────────────────────
123
+ app.get('/api/costs', (req, res) => {
124
+ try {
125
+ const window = Math.min(parseInt(req.query.window || '7', 10), 90);
126
+ res.json(Metrics.getCosts(window));
127
+ } catch (err) {
128
+ res.status(500).json({ error: err.message });
129
+ }
130
+ });
131
+
132
+ // ── Steering (requires auto mode running) ───────────────────────────────────
133
+ app.post('/api/steer', (req, res) => {
134
+ try {
135
+ const { instruction, priority = 'normal' } = req.body || {};
136
+
137
+ if (!instruction || typeof instruction !== 'string') {
138
+ return res.status(400).json({ error: 'Missing "instruction" field' });
139
+ }
140
+ if (instruction.length > 500) {
141
+ return res.status(400).json({ error: 'Instruction too long (max 500 chars)' });
142
+ }
143
+ if (!['normal', 'urgent', 'stop'].includes(priority)) {
144
+ return res.status(400).json({ error: 'Invalid priority. Use: normal|urgent|stop' });
145
+ }
146
+
147
+ // Check auto mode is running
148
+ const autoState = fs.existsSync(AUTO_STATE_PATH)
149
+ ? JSON.parse(fs.readFileSync(AUTO_STATE_PATH, 'utf8'))
150
+ : null;
151
+
152
+ if (!autoState || autoState.status !== 'running') {
153
+ return res.status(409).json({ error: 'Auto mode is not running. Steering has no effect.' });
154
+ }
155
+
156
+ // Run injection guard
157
+ const INJECTION_PATTERNS = [
158
+ /IGNORE ALL PREVIOUS INSTRUCTIONS/i,
159
+ /DISREGARD YOUR INSTRUCTIONS/i,
160
+ /FORGET YOUR TRAINING/i,
161
+ /YOUR NEW INSTRUCTIONS ARE/i,
162
+ /OVERRIDE:/i,
163
+ ];
164
+ if (INJECTION_PATTERNS.some(p => p.test(instruction))) {
165
+ return res.status(400).json({ error: 'Instruction rejected: contains prohibited patterns' });
166
+ }
167
+
168
+ // Write to steering queue
169
+ const entry = {
170
+ id: require('crypto').randomBytes(8).toString('hex'),
171
+ timestamp: new Date().toISOString(),
172
+ instruction: instruction.trim(),
173
+ priority,
174
+ authored_by: 'dashboard',
175
+ applies_to: 'all',
176
+ status: 'queued',
177
+ applied_at: null,
178
+ applied_to_plan: null,
179
+ };
180
+
181
+ if (!fs.existsSync(path.dirname(STEERING_QUEUE))) {
182
+ fs.mkdirSync(path.dirname(STEERING_QUEUE), { recursive: true });
183
+ }
184
+ fs.appendFileSync(STEERING_QUEUE, JSON.stringify(entry) + '\n');
185
+
186
+ res.json({ success: true, queued: true, id: entry.id, priority });
187
+ } catch (err) {
188
+ res.status(500).json({ error: err.message });
189
+ }
190
+ });
191
+
192
+ // ── Client count (for dashboard connection indicator) ───────────────────────
193
+ app.get('/api/connections', (req, res) => {
194
+ res.json({ clients: SSE.getClientCount() });
195
+ });
196
+ }
197
+
198
+ module.exports = { register };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * MindForge v2 — Approval Handler
3
+ * Handles POST /api/approve/:id — approve or reject governance requests.
4
+ * Reads/writes APPROVAL-*.json files in .planning/approvals/
5
+ */
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // Paths resolved lazily for testing
12
+ const getPaths = () => ({
13
+ approvals: path.join(process.cwd(), '.planning', 'approvals'),
14
+ audit: path.join(process.cwd(), '.planning', 'AUDIT.jsonl'),
15
+ });
16
+
17
+ /**
18
+ * Process an approval decision from the dashboard.
19
+ * @param {string} approvalId - The APPROVAL UUID
20
+ * @param {string} decision - 'approve' or 'reject'
21
+ * @param {string} comment - Optional comment
22
+ * @param {string} approver - Approver identifier (email or name)
23
+ * @returns {{ success, decision, message }}
24
+ */
25
+ function processDecision(approvalId, decision, comment, approver, confirmationId = null) {
26
+ // Input validation
27
+ if (!approvalId || typeof approvalId !== 'string') {
28
+ return { success: false, error: 'Invalid approval ID' };
29
+ }
30
+
31
+ // Sanitize approvalId — only allow UUID characters
32
+ if (!/^[a-f0-9-]{36}$/.test(approvalId)) {
33
+ return { success: false, error: 'Malformed approval ID format' };
34
+ }
35
+
36
+ if (!['approve', 'reject'].includes(decision)) {
37
+ return { success: false, error: 'Decision must be "approve" or "reject"' };
38
+ }
39
+
40
+ // Find the approval file
41
+ const paths = getPaths();
42
+ const filePath = path.join(paths.approvals, `APPROVAL-${approvalId}.json`);
43
+ if (!fs.existsSync(filePath)) {
44
+ return { success: false, error: `Approval not found: ${approvalId}` };
45
+ }
46
+
47
+ let approval;
48
+ try {
49
+ approval = JSON.parse(fs.readFileSync(filePath, 'utf8'));
50
+ } catch {
51
+ return { success: false, error: 'Cannot parse approval file' };
52
+ }
53
+
54
+ // Validate approval is still pending
55
+ if (approval.status !== 'pending') {
56
+ return { success: false, error: `Approval already ${approval.status}` };
57
+ }
58
+
59
+ // Check expiry
60
+ if (approval.expires_at && new Date(approval.expires_at) < new Date()) {
61
+ return { success: false, error: 'Approval has expired' };
62
+ }
63
+
64
+ // TIER 3 CONFIRMATION — require typing the plan ID
65
+ if (approval.tier === 3 && decision === 'approve') {
66
+ const expectedConfirmation = `${approval.phase}-${approval.plan}`;
67
+ if (!confirmationId || confirmationId.trim() !== expectedConfirmation) {
68
+ return {
69
+ success: false,
70
+ error: `Tier 3 approval requires typing the plan ID "${expectedConfirmation}" to confirm.`,
71
+ confirmation_required: true,
72
+ expected: expectedConfirmation,
73
+ tier3_warning: 'This is a Tier 3 change (auth/payment/PII). Review the code diff before approving.',
74
+ };
75
+ }
76
+ }
77
+
78
+ // Write AUDIT entry FIRST (before updating file)
79
+ writeAuditEntry({
80
+ id: require('crypto').randomBytes(8).toString('hex'),
81
+ timestamp: new Date().toISOString(),
82
+ event: decision === 'approve' ? 'approval_granted' : 'approval_rejected',
83
+ approval_id: approvalId,
84
+ tier: approval.tier,
85
+ phase: approval.phase,
86
+ plan: approval.plan,
87
+ resolved_by: approver || 'dashboard',
88
+ comment: comment || null,
89
+ agent: 'mindforge-dashboard',
90
+ session_id: 'dashboard',
91
+ });
92
+
93
+ // Update approval file
94
+ const updated = {
95
+ ...approval,
96
+ status: decision === 'approve' ? 'approved' : 'rejected',
97
+ resolved_at: new Date().toISOString(),
98
+ resolved_by: approver || 'dashboard',
99
+ comment: comment || null,
100
+ resolution_channel: 'mindforge-dashboard',
101
+ };
102
+
103
+ fs.writeFileSync(filePath, JSON.stringify(updated, null, 2));
104
+
105
+ return {
106
+ success: true,
107
+ decision,
108
+ approval_id: approvalId,
109
+ tier: approval.tier,
110
+ message: `${approval.tier === 3 ? 'Tier 3' : 'Tier 2'} approval ${decision}d for Plan ${approval.phase}-${approval.plan}`,
111
+ };
112
+ }
113
+
114
+ function writeAuditEntry(entry) {
115
+ try {
116
+ const paths = getPaths();
117
+ if (!fs.existsSync(path.dirname(paths.audit))) return;
118
+ fs.appendFileSync(paths.audit, JSON.stringify(entry) + '\n');
119
+ } catch { /* ignore AUDIT write failures */ }
120
+ }
121
+
122
+ function listApprovals() {
123
+ const paths = getPaths();
124
+ if (!fs.existsSync(paths.approvals)) return [];
125
+ return fs.readdirSync(paths.approvals)
126
+ .filter(f => f.startsWith('APPROVAL-') && f.endsWith('.json'))
127
+ .map(f => {
128
+ try { return JSON.parse(fs.readFileSync(path.join(paths.approvals, f), 'utf8')); }
129
+ catch { return null; }
130
+ })
131
+ .filter(Boolean);
132
+ }
133
+
134
+ module.exports = { processDecision, listApprovals };