mindforge-cc 2.0.0-alpha.4 → 2.0.0-alpha.7
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/.agent/CLAUDE.md +37 -7
- package/.agent/mindforge/dashboard.md +98 -0
- package/.agent/mindforge/init-project.md +12 -0
- package/.claude/CLAUDE.md +37 -7
- package/.claude/commands/mindforge/dashboard.md +98 -0
- package/.mindforge/dashboard/api-reference.md +122 -0
- package/.mindforge/dashboard/dashboard-spec.md +96 -0
- package/.planning/approvals/v2-architecture-approval.json +15 -0
- package/CHANGELOG.md +12 -2
- package/README.md +18 -2
- package/RELEASENOTES.md +1 -1
- package/bin/change-classifier.js +86 -0
- package/bin/dashboard/api-router.js +198 -0
- package/bin/dashboard/approval-handler.js +134 -0
- package/bin/dashboard/frontend/index.html +511 -0
- package/bin/dashboard/metrics-aggregator.js +296 -0
- package/bin/dashboard/server.js +135 -0
- package/bin/dashboard/sse-bridge.js +178 -0
- package/bin/dashboard/team-tracker.js +0 -0
- package/bin/governance/approve.js +60 -0
- package/bin/installer-core.js +68 -12
- package/bin/mindforge-cli.js +87 -0
- package/bin/wizard/setup-wizard.js +5 -1
- package/docs/Context/Master-Context.md +11 -11
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/decision-records-index.md +20 -20
- package/docs/ci-cd.md +92 -0
- package/docs/commands-reference.md +1 -0
- package/docs/enterprise-setup.md +1 -1
- package/docs/feature-dashboard.md +52 -0
- package/docs/publishing-guide.md +16 -51
- package/docs/reference/commands.md +42 -42
- package/docs/reference/config-reference.md +2 -2
- package/docs/reference/sdk-api.md +1 -1
- package/docs/testing-current-version.md +130 -0
- package/docs/user-guide.md +24 -2
- package/docs/usp-features.md +15 -1
- package/docs/workflow-atlas.md +57 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MindForge — Enterprise Agentic Framework (v2.0.0-alpha.
|
|
1
|
+
# MindForge — Enterprise Agentic Framework (v2.0.0-alpha.7)
|
|
2
2
|
|
|
3
3
|
MindForge turns Claude Code and Antigravity into production-grade engineering
|
|
4
4
|
partners with governance, observability, and a disciplined workflow engine.
|
|
@@ -19,6 +19,7 @@ Decisions get forgotten. MindForge fixes that with:
|
|
|
19
19
|
- **Skills** — just-in-time domain knowledge loaded on demand
|
|
20
20
|
- **Wave execution** — parallelism with dependency safety
|
|
21
21
|
- **Autonomous Engine** — walk-away execution with steerability (v2)
|
|
22
|
+
- **Real-time Dashboard** — web-based observability and governance (v2)
|
|
22
23
|
- **Browser Runtime** — headful/headless visual QA and sessions (v2)
|
|
23
24
|
- **Multi-Model Intelligence** — dynamic routing, adversarial reviews, and deep research (v2)
|
|
24
25
|
- **Persistent Knowledge Graph** — long-term memory across all engineering sessions (v2)
|
|
@@ -39,6 +40,16 @@ npx mindforge-cc@latest --claude --global
|
|
|
39
40
|
npx mindforge-cc@latest --claude --local
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
### Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Install the latest stable version
|
|
47
|
+
npm install -g mindforge-cc
|
|
48
|
+
|
|
49
|
+
# Or try the v2.0.0-alpha (latest features)
|
|
50
|
+
npm install -g mindforge-cc@alpha
|
|
51
|
+
```
|
|
52
|
+
|
|
42
53
|
### Antigravity
|
|
43
54
|
```bash
|
|
44
55
|
npx mindforge-cc@latest --antigravity --global
|
|
@@ -144,6 +155,10 @@ If issues are found, run:
|
|
|
144
155
|
/ mindforge:remember
|
|
145
156
|
→ Manual knowledge management and search (v2)
|
|
146
157
|
→ Persistent knowledge graph retrieval and promotion
|
|
158
|
+
|
|
159
|
+
/ mindforge:dashboard
|
|
160
|
+
→ Real-time web observability and governance at localhost:7339 (v2)
|
|
161
|
+
→ Live audit logs, metrics, activity, and team feed
|
|
147
162
|
```
|
|
148
163
|
|
|
149
164
|
---
|
|
@@ -200,7 +215,8 @@ See `.mindforge/production/token-optimiser.md`.
|
|
|
200
215
|
|
|
201
216
|
---
|
|
202
217
|
|
|
203
|
-
## What ships in v2.0.0-alpha.
|
|
218
|
+
## What ships in v2.0.0-alpha.7
|
|
219
|
+
- **Real-time Dashboard**: `/mindforge:dashboard` and web-based observability.
|
|
204
220
|
- **Persistent Knowledge Graph**: `/mindforge:remember` and long-term memory engine.
|
|
205
221
|
- **Multi-Model Intelligence Layer**: `/mindforge:cross-review`, `/mindforge:research`, and `/mindforge:costs`.
|
|
206
222
|
- **Visual QA Engine**: `/mindforge:qa` and automated regression tests.
|
package/RELEASENOTES.md
CHANGED
|
@@ -21,7 +21,7 @@ Quality gates, security posture, and release readiness are documented for enterp
|
|
|
21
21
|
- Example starter project with MindForge structure ready for onboarding teams.
|
|
22
22
|
|
|
23
23
|
## Quality & Stability
|
|
24
|
-
-
|
|
24
|
+
- production, migration, and e2e test suites added.
|
|
25
25
|
- Full 12-suite regression loop validated across prior-day coverage.
|
|
26
26
|
- Triple-run stability verification completed with all tests passing.
|
|
27
27
|
- Threat model and penetration test results documented.
|
|
@@ -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 };
|