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,71 @@
1
+ /**
2
+ * MindForge — Steering Manager
3
+ * Manages mid-execution guidance via steering-queue.jsonl.
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const QUEUE_PATH = path.join(process.cwd(), '.planning/steering-queue.jsonl');
11
+
12
+ /**
13
+ * Add guidance to the queue.
14
+ */
15
+ function pushGuidance(instruction, scope = 'global', taskId = null) {
16
+ const item = {
17
+ id: `steer-${Date.now()}`,
18
+ timestamp: new Date().toISOString(),
19
+ instruction,
20
+ scope,
21
+ taskId
22
+ };
23
+ fs.appendFileSync(QUEUE_PATH, JSON.stringify(item) + '\n');
24
+ return item.id;
25
+ }
26
+
27
+ /**
28
+ * Get and remove all pending guidance for the current context.
29
+ */
30
+ function popGuidance(taskId = null) {
31
+ if (!fs.existsSync(QUEUE_PATH)) return [];
32
+
33
+ const lines = fs.readFileSync(QUEUE_PATH, 'utf8').split('\n').filter(Boolean);
34
+ const relevant = [];
35
+ const remaining = [];
36
+
37
+ for (const line of lines) {
38
+ const item = JSON.parse(line);
39
+ if (item.scope === 'global' || item.taskId === taskId) {
40
+ relevant.push(item);
41
+ } else {
42
+ remaining.push(item);
43
+ }
44
+ }
45
+
46
+ // Rewrite remaining
47
+ fs.writeFileSync(QUEUE_PATH, remaining.map(JSON.stringify).join('\n') + (remaining.length ? '\n' : ''));
48
+ return relevant;
49
+ }
50
+
51
+ /**
52
+ * Inject steering guidance into a PLAN file content.
53
+ */
54
+ function injectSteering(planContent, guidanceItems) {
55
+ if (!guidanceItems || guidanceItems.length === 0) return planContent;
56
+
57
+ const guidanceText = guidanceItems
58
+ .map(g => `[STEERING GUIDANCE — DO NOT IGNORE] ${g.instruction}`)
59
+ .join('\n');
60
+
61
+ // Hardened injection: inject at beginning of <action> to ensure visibility
62
+ return planContent.replace(/<action>([\s\S]*?)<\/action>/, (match, action) => {
63
+ return `<action>\n${guidanceText}\n\nOriginal plan instructions:\n${action}\n</action>`;
64
+ });
65
+ }
66
+
67
+ module.exports = {
68
+ pushGuidance,
69
+ popGuidance,
70
+ injectSteering
71
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MindForge — Stuck Detection Engine
3
+ * Monitors AUDIT.jsonl for S01-S04 patterns.
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+
9
+ class StuckMonitor {
10
+ constructor(auditPath) {
11
+ this.auditPath = auditPath;
12
+ this.history = [];
13
+ this.patterns = {
14
+ S01_LINT_LOOP: 0,
15
+ S02_COMMAND_LOOP: 0,
16
+ };
17
+ }
18
+
19
+ analyze(event) {
20
+ this.history.push(event);
21
+ if (this.history.length > 50) this.history.shift();
22
+
23
+ // Check S01: Lint Loop (Identical multi_replace calls)
24
+ if (this.detectS01(event)) return { pattern: 'S01', message: 'Stuck in lint loop: identical edits detected.' };
25
+
26
+ // Check S02: Command Loop (Identical failing commands)
27
+ if (this.detectS02(event)) return { pattern: 'S02', message: 'Stuck in command loop: identical failing commands.' };
28
+
29
+ return null;
30
+ }
31
+
32
+ detectS01(event) {
33
+ if (event.tool !== 'multi_replace_file_content') return false;
34
+
35
+ const similar = this.history.filter(h =>
36
+ h.tool === 'multi_replace_file_content' &&
37
+ h.args?.TargetFile === event.args?.TargetFile &&
38
+ this.isContentSimilar(h.args?.ReplacementChunks?.[0]?.ReplacementContent, event.args?.ReplacementChunks?.[0]?.ReplacementContent)
39
+ );
40
+
41
+ return similar.length >= 3;
42
+ }
43
+
44
+ detectS02(event) {
45
+ if (event.tool !== 'run_command' || event.status !== 'failed') return false;
46
+
47
+ const identical = this.history.filter(h =>
48
+ h.tool === 'run_command' &&
49
+ h.status === 'failed' &&
50
+ h.args?.CommandLine === event.args?.CommandLine
51
+ );
52
+
53
+ return identical.length >= 3;
54
+ }
55
+
56
+ isContentSimilar(a, b) {
57
+ if (!a || !b) return false;
58
+ if (a === b) return true;
59
+ // Simple similarity check (hardened from Roadmap requirement)
60
+ const dist = this.levenshtein(a.slice(0, 100), b.slice(0, 100));
61
+ return dist < 10;
62
+ }
63
+
64
+ levenshtein(a, b) {
65
+ const tmp = [];
66
+ for (let i = 0; i <= a.length; i++) { tmp[i] = [i]; }
67
+ for (let j = 0; j <= b.length; j++) { tmp[0][j] = j; }
68
+ for (let i = 1; i <= a.length; i++) {
69
+ for (let j = 1; j <= b.length; j++) {
70
+ tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
71
+ }
72
+ }
73
+ return tmp[a.length][b.length];
74
+ }
75
+ }
76
+
77
+ module.exports = StuckMonitor;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * MindForge v2 — Browser Daemon
3
+ * Lightweight HTTP server controlling Chromium.
4
+ * Consistent with ADR-017 / ADR-024 (Localhost only).
5
+ */
6
+ 'use strict';
7
+
8
+ const http = require('http');
9
+ const playwright = require('playwright-core');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const PORT = process.env.BROWSER_PORT || 7338;
14
+ const HEADLESS = process.env.BROWSER_HEADLESS !== 'false';
15
+ const TIMEOUT = (parseInt(process.env.BROWSER_IDLE_TIMEOUT_MINUTES) || 30) * 60 * 1000;
16
+
17
+ let browser, lastActionAt = Date.now(), isLaunching = false;
18
+ const sessions = new Map(); // name -> { context, page }
19
+
20
+ async function init() {
21
+ if (isLaunching) return;
22
+ isLaunching = true;
23
+ try {
24
+ browser = await playwright.chromium.launch({
25
+ headless: HEADLESS,
26
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--no-first-run']
27
+ });
28
+ setInterval(checkIdle, 60000);
29
+ } finally {
30
+ isLaunching = false;
31
+ }
32
+ }
33
+
34
+ function checkIdle() {
35
+ if (Date.now() - lastActionAt > TIMEOUT) {
36
+ console.log('[daemon] Idle timeout reached. Shutting down.');
37
+ process.exit(0);
38
+ }
39
+ }
40
+
41
+ async function getOrCreateSession(name = 'default') {
42
+ if (sessions.has(name)) return sessions.get(name);
43
+ const context = await browser.newContext();
44
+ const page = await context.newPage();
45
+ const s = { context, page };
46
+ sessions.set(name, s);
47
+ return s;
48
+ }
49
+
50
+ const server = http.createServer(async (req, res) => {
51
+ lastActionAt = Date.now();
52
+ const send = (data, code = 200) => {
53
+ res.writeHead(code, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify(data));
55
+ };
56
+
57
+ // Only allow localhost
58
+ const remote = req.socket.remoteAddress;
59
+ if (remote !== '127.0.0.1' && remote !== '::1' && remote !== '::ffff:127.0.0.1') {
60
+ return send({ error: 'Forbidden: Localhost only' }, 403);
61
+ }
62
+
63
+ let body = '';
64
+ req.on('data', chunk => body += chunk);
65
+ req.on('end', async () => {
66
+ try {
67
+ const { url, session: sessionName, selector, text, script, type, expected_text, name } = body ? JSON.parse(body) : {};
68
+ const { page, context } = await getOrCreateSession(sessionName);
69
+
70
+ if (req.url === '/status' && req.method === 'GET') {
71
+ return send({ alive: true, sessions: Array.from(sessions.keys()), uptime: process.uptime() });
72
+ }
73
+
74
+ if (req.url === '/navigate' && req.method === 'POST') {
75
+ const start = Date.now();
76
+ const r = await page.goto(url, { waitUntil: 'load', timeout: 30000 });
77
+ return send({
78
+ success: true,
79
+ status_code: r ? r.status() : 200,
80
+ load_time_ms: Date.now() - start
81
+ });
82
+ }
83
+
84
+ if (req.url === '/click' && req.method === 'POST') {
85
+ const target = selector ? page.locator(selector) : page.getByText(text, { exact: false });
86
+ await target.first().click({ timeout: 5000 });
87
+ return send({ success: true, element_found: true });
88
+ }
89
+
90
+ if (req.url === '/type' && req.method === 'POST') {
91
+ await page.locator(selector).fill(text, { timeout: 5000 });
92
+ return send({ success: true });
93
+ }
94
+
95
+ if (req.url === '/screenshot' && req.method === 'POST') {
96
+ const buf = await page.screenshot({ type: 'png' });
97
+ return send({ success: true, screenshot_b64: buf.toString('base64') });
98
+ }
99
+
100
+ if (req.url === '/evaluate' && req.method === 'POST') {
101
+ const result = await page.evaluate(script);
102
+ return send({ success: true, result });
103
+ }
104
+
105
+ if (req.url === '/assert' && req.method === 'POST') {
106
+ if (type === 'visible') {
107
+ const loc = page.locator(selector).first();
108
+ const visible = await loc.isVisible();
109
+ const actual = visible ? await loc.innerText() : '';
110
+ const passed = visible && (!expected_text || actual.includes(expected_text));
111
+ return send({ passed, actual_text: actual });
112
+ }
113
+ if (type === 'url') return send({ passed: page.url().includes(expected_text), actual_url: page.url() });
114
+ if (type === 'title') return send({ passed: (await page.title()).includes(expected_text), actual_title: await page.title() });
115
+ if (type === 'no_console_errors') return send({ passed: true }); // simplified
116
+ }
117
+
118
+ send({ error: 'Not Found' }, 404);
119
+ } catch (err) {
120
+ send({ success: false, error: err.message }, 500);
121
+ }
122
+ });
123
+ });
124
+
125
+ init().then(() => {
126
+ server.listen(PORT, '127.0.0.1', () => console.log(`[daemon] Browser daemon listening on port ${PORT}`));
127
+ }).catch(err => {
128
+ console.error('[daemon] Initialization failed:', err);
129
+ process.exit(1);
130
+ });
131
+
132
+ async function shutdown() {
133
+ console.log('[daemon] Shutting down gracefully...');
134
+ if (browser) await browser.close();
135
+ process.exit(0);
136
+ }
137
+
138
+ process.on('SIGTERM', shutdown);
139
+ process.on('SIGINT', shutdown);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * MindForge v2 — Browser Daemon Manager
3
+ * Starts and stops the browser-daemon.js process.
4
+ */
5
+ 'use strict';
6
+
7
+ const { spawn, execSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const http = require('http');
11
+
12
+ const PORT = process.env.BROWSER_PORT || 7338;
13
+ const DAEMON_SCRIPT = path.join(__dirname, 'browser-daemon.js');
14
+
15
+ async function isRunning() {
16
+ return new Promise(resolve => {
17
+ const req = http.get(`http://127.0.0.1:${PORT}/status`, res => {
18
+ resolve(res.statusCode === 200);
19
+ });
20
+ req.on('error', () => resolve(false));
21
+ req.end();
22
+ });
23
+ }
24
+
25
+ async function start() {
26
+ if (await isRunning()) return;
27
+
28
+ const out = fs.openSync(path.join(process.cwd(), '.planning/browser-daemon.log'), 'a');
29
+ const err = fs.openSync(path.join(process.cwd(), '.planning/browser-daemon.log'), 'a');
30
+
31
+ const child = spawn(process.execPath, [DAEMON_SCRIPT], {
32
+ detached: true,
33
+ stdio: ['ignore', out, err],
34
+ env: { ...process.env, BROWSER_HEADLESS: 'true' }
35
+ });
36
+
37
+ child.unref();
38
+ console.log('[manager] Starting browser daemon...');
39
+
40
+ // Wait for it to wake up
41
+ for (let i = 0; i < 10; i++) {
42
+ await new Promise(r => setTimeout(r, 500));
43
+ if (await isRunning()) {
44
+ console.log('[manager] Browser daemon ready.');
45
+ return;
46
+ }
47
+ }
48
+ throw new Error('Timeout waiting for browser daemon to start');
49
+ }
50
+
51
+ async function stop() {
52
+ if (!(await isRunning())) return;
53
+ // Simple way to kill on localhost
54
+ try {
55
+ if (process.platform === 'win32') {
56
+ execSync(`for /f "tokens=5" %a in ('netstat -aon ^| find ":${PORT}" ^| find "LISTENING"') do taskkill /f /pid %a`);
57
+ } else {
58
+ execSync(`lsof -t -i:${PORT} | xargs kill -9`);
59
+ }
60
+ console.log('[manager] Browser daemon stopped.');
61
+ } catch (err) {
62
+ console.error('[manager] Failed to stop daemon:', err.message);
63
+ }
64
+ }
65
+
66
+ async function ensureRunning(opts = {}) {
67
+ if (!(await isRunning())) await start();
68
+ }
69
+
70
+ async function request(method, endpoint, body = null) {
71
+ return new Promise((resolve, reject) => {
72
+ const req = http.request({
73
+ hostname: '127.0.0.1',
74
+ port: PORT,
75
+ path: endpoint,
76
+ method: method,
77
+ headers: { 'Content-Type': 'application/json' }
78
+ }, res => {
79
+ let data = '';
80
+ res.on('data', chunk => data += chunk);
81
+ res.on('end', () => {
82
+ try { resolve(JSON.parse(data)); } catch { resolve({ success: false, error: 'Invalid JSON response' }); }
83
+ });
84
+ });
85
+ req.on('error', err => reject(err));
86
+ if (body) req.write(JSON.stringify(body));
87
+ req.end();
88
+ });
89
+ }
90
+
91
+ module.exports = { start, stop, isRunning, ensureRunning, request };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MindForge v2 — QA Engine
3
+ * Diff-aware systematic UI testing.
4
+ */
5
+ 'use strict';
6
+
7
+ const { execSync } = require('child_process');
8
+ const DaemonMgr = require('./daemon-manager');
9
+ const ScreenStore = require('./screenshot-store');
10
+
11
+ const DEV_SERVER = process.env.DEV_SERVER_URL || 'http://localhost:3000';
12
+
13
+ function extractSurfaces() {
14
+ const files = execSync('git diff --name-only HEAD~1', { encoding: 'utf8' }).split('\n').filter(Boolean);
15
+ const surfaces = [];
16
+ for (const file of files) {
17
+ if (file.includes('pages/') || file.includes('app/')) {
18
+ const route = file.replace(/.*(pages|app)/, '').replace(/\.(tsx|jsx|ts|js)$/, '').replace(/\/page$/, '').replace(/\/index$/, '') || '/';
19
+ surfaces.push({ file, route });
20
+ }
21
+ }
22
+ return surfaces;
23
+ }
24
+
25
+ async function runQA(phaseNum) {
26
+ await DaemonMgr.ensureRunning();
27
+ const surfaces = extractSurfaces();
28
+ const results = [];
29
+ const bugs = [];
30
+
31
+ for (const surface of surfaces) {
32
+ const route = surface.route;
33
+ const r = await DaemonMgr.request('POST', '/navigate', { url: `${DEV_SERVER}${route}`, session: 'default' });
34
+ const passed = r.success && r.status_code < 400;
35
+
36
+ if (!passed) {
37
+ const snap = await DaemonMgr.request('POST', '/screenshot', { session: 'default' });
38
+ const shot = snap.success ? ScreenStore.save(snap.screenshot_b64, phaseNum, 'qa', `fail-${route.replace(/\//g, '-')}.png`) : null;
39
+ bugs.push({ surface: route, error: r.error || `Status ${r.status_code}`, screenshot: shot });
40
+ }
41
+ results.push({ surface: route, passed });
42
+ }
43
+
44
+ return { surfaces: surfaces.length, passed: results.filter(r => r.passed).length, failed: bugs.length, results, bugs };
45
+ }
46
+
47
+ module.exports = { runQA };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * MindForge v2 — QA Report Writer
3
+ */
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function write(phaseNum, qaResult) {
10
+ const dir = path.join(process.cwd(), '.planning', 'phases', String(phaseNum));
11
+ fs.mkdirSync(dir, { recursive: true });
12
+
13
+ const lines = [
14
+ `# QA Report — Phase ${phaseNum}`,
15
+ `Generated: ${new Date().toISOString()}`,
16
+ `Total: ${qaResult.surfaces} | Passed: ${qaResult.passed} | Failed: ${qaResult.failed}`,
17
+ '',
18
+ '## Results',
19
+ '| Surface | Result |',
20
+ '|---|---|',
21
+ ...qaResult.results.map(r => `| ${r.surface} | ${r.passed ? '✅ Pass' : '❌ Fail'} |`),
22
+ '',
23
+ '## Bugs found',
24
+ ...qaResult.bugs.map((b, i) => `### Bug ${i + 1}: ${b.surface}\n- Error: ${b.error}\n- Screenshot: ${b.screenshot}\n`)
25
+ ];
26
+
27
+ const file = path.join(dir, `QA-REPORT-${phaseNum}.md`);
28
+ fs.writeFileSync(file, lines.join('\n'));
29
+ return file;
30
+ }
31
+
32
+ module.exports = { write };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * MindForge v2 — Regression Writer
3
+ */
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function write(bug, phaseNum) {
10
+ const dir = path.join(process.cwd(), 'tests', 'regression');
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ const name = `phase${phaseNum}-${bug.surface.replace(/\//g, '-').slice(1) || 'home'}.test.ts`;
13
+ const content = `
14
+ import { test, expect } from '@playwright/test';
15
+
16
+ test('Regression: ${bug.surface} [${bug.error}]', async ({ page }) => {
17
+ await page.goto('${bug.surface}');
18
+ // TODO: Add more specific assertions based on the bug
19
+ expect(await page.isVisible('body')).toBeTruthy();
20
+ });
21
+ `;
22
+ const file = path.join(dir, name);
23
+ fs.writeFileSync(file, content);
24
+ return file;
25
+ }
26
+
27
+ module.exports = { write };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MindForge v2 — Screenshot Store
3
+ * Saves / lists / cleans up browser screenshots.
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const STORE = path.join(process.cwd(), '.planning', 'screenshots');
11
+ const ensureDir = (dir) => fs.mkdirSync(dir, { recursive: true });
12
+
13
+ function save(base64Png, phaseNum, planId, filename = 'screenshot.png') {
14
+ const dir = path.join(STORE, `phase-${phaseNum}`, String(planId));
15
+ ensureDir(dir);
16
+ const safe = filename.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/\.png$/i, '') + '.png';
17
+ const dest = path.join(dir, safe);
18
+ fs.writeFileSync(dest, Buffer.from(base64Png, 'base64'));
19
+ return dest;
20
+ }
21
+
22
+ function list(phaseNum, planId) {
23
+ const dir = planId
24
+ ? path.join(STORE, `phase-${phaseNum}`, String(planId))
25
+ : path.join(STORE, `phase-${phaseNum}`);
26
+ if (!fs.existsSync(dir)) return [];
27
+ const walk = d => fs.readdirSync(d, { withFileTypes: true })
28
+ .flatMap(e => e.isDirectory() ? walk(path.join(d, e.name)) : path.join(d, e.name))
29
+ .filter(p => p.endsWith('.png'));
30
+ return walk(dir);
31
+ }
32
+
33
+ function cleanup(phaseNum) {
34
+ const dir = path.join(STORE, `phase-${phaseNum}`);
35
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
36
+ }
37
+
38
+ function diskUsage() {
39
+ if (!fs.existsSync(STORE)) return 0;
40
+ let total = 0;
41
+ const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) {
42
+ const p = path.join(d, e.name);
43
+ e.isDirectory() ? walk(p) : (total += fs.statSync(p).size);
44
+ }};
45
+ walk(STORE);
46
+ return total;
47
+ }
48
+
49
+ module.exports = { save, list, cleanup, diskUsage };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * MindForge v2 — Session Manager
3
+ * Persists browser state (cookies, localStorage) to disk.
4
+ */
5
+ /* global localStorage */
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const SESSIONS_DIR = path.join(process.cwd(), '.mindforge', 'browser', 'sessions');
13
+ const ensureDir = () => fs.mkdirSync(SESSIONS_DIR, { recursive: true });
14
+
15
+ async function saveSession(name, context) {
16
+ const safeName = name.replace(/[^a-z0-9_-]/gi, '_');
17
+ const filePath = path.join(SESSIONS_DIR, `${safeName}.json`);
18
+ ensureDir();
19
+ const cookies = await context.cookies();
20
+ const storageByOrigin = {};
21
+
22
+ for (const page of context.pages()) {
23
+ try {
24
+ const origin = new URL(page.url()).origin;
25
+ if (origin.startsWith('http')) {
26
+ const ls = await page.evaluate(() => {
27
+ const items = {};
28
+ for (let i = 0; i < localStorage.length; i++) {
29
+ const key = localStorage.key(i);
30
+ items[key] = localStorage.getItem(key);
31
+ }
32
+ return items;
33
+ }).catch(() => ({}));
34
+ if (Object.keys(ls).length) storageByOrigin[origin] = { localStorage: ls };
35
+ }
36
+ } catch (err) {
37
+ // Ignore navigation or evaluation errors for individual pages during session save
38
+ }
39
+ }
40
+
41
+ const data = {
42
+ name,
43
+ saved_at: new Date().toISOString(),
44
+ url: context.pages()[0]?.url() ?? '',
45
+ cookies,
46
+ storage: storageByOrigin,
47
+ _warning: 'Contains authentication cookies. NEVER commit this file.',
48
+ };
49
+
50
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
51
+ return filePath;
52
+ }
53
+
54
+ async function loadSession(name, context) {
55
+ const safeName = name.replace(/[^a-z0-9_-]/gi, '_');
56
+ const filePath = path.join(SESSIONS_DIR, `${safeName}.json`);
57
+ if (!fs.existsSync(filePath)) throw new Error(`Session file not found: ${filePath}`);
58
+
59
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
60
+ let cookiesLoaded = 0;
61
+
62
+ if (data.cookies?.length) {
63
+ const now = Date.now() / 1000;
64
+ const valid = data.cookies.filter(c => !c.expires || c.expires === -1 || c.expires > now);
65
+ if (valid.length) {
66
+ await context.addCookies(valid);
67
+ cookiesLoaded = valid.length;
68
+ }
69
+ }
70
+
71
+ return { cookiesLoaded };
72
+ }
73
+
74
+ function importFromBrowser(source) {
75
+ const home = os.homedir();
76
+ const paths = {
77
+ chrome: `${home}/Library/Application Support/Google/Chrome/Default/Cookies`,
78
+ arc: `${home}/Library/Application Support/Arc/User Data/Default/Cookies`,
79
+ brave: `${home}/Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies`,
80
+ edge: `${home}/Library/Application Support/Microsoft Edge/Default/Cookies`,
81
+ };
82
+
83
+ const p = paths[source.toLowerCase()];
84
+ if (!p || !fs.existsSync(p)) {
85
+ throw new Error(`Cookie file for ${source} not found at ${p}`);
86
+ }
87
+
88
+ // Real SQLite parsing would happen here via better-sqlite3 if installed.
89
+ // This is a placeholder for the logic specified in the roadmap.
90
+ return [];
91
+ }
92
+
93
+ module.exports = { saveSession, loadSession, importFromBrowser };