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.
- package/.agent/CLAUDE.md +83 -0
- package/.agent/mindforge/auto.md +22 -0
- package/.agent/mindforge/browse.md +26 -0
- package/.agent/mindforge/costs.md +11 -0
- package/.agent/mindforge/cross-review.md +17 -0
- package/.agent/mindforge/dashboard.md +98 -0
- package/.agent/mindforge/execute-phase.md +5 -3
- package/.agent/mindforge/init-project.md +12 -0
- package/.agent/mindforge/qa.md +16 -0
- package/.agent/mindforge/remember.md +14 -0
- package/.agent/mindforge/research.md +11 -0
- package/.agent/mindforge/steer.md +13 -0
- package/.agent/workflows/publish-release.md +36 -0
- package/.claude/CLAUDE.md +83 -0
- package/.claude/commands/mindforge/auto.md +22 -0
- package/.claude/commands/mindforge/browse.md +26 -0
- package/.claude/commands/mindforge/costs.md +11 -0
- package/.claude/commands/mindforge/cross-review.md +17 -0
- package/.claude/commands/mindforge/dashboard.md +98 -0
- package/.claude/commands/mindforge/execute-phase.md +5 -3
- package/.claude/commands/mindforge/qa.md +16 -0
- package/.claude/commands/mindforge/remember.md +14 -0
- package/.claude/commands/mindforge/research.md +11 -0
- package/.claude/commands/mindforge/steer.md +13 -0
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
- package/.mindforge/browser/daemon-protocol.md +24 -0
- package/.mindforge/browser/qa-engine.md +16 -0
- package/.mindforge/browser/session-manager.md +18 -0
- package/.mindforge/browser/visual-verify-spec.md +31 -0
- package/.mindforge/dashboard/api-reference.md +122 -0
- package/.mindforge/dashboard/dashboard-spec.md +96 -0
- package/.mindforge/engine/autonomous/auto-executor.md +266 -0
- package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
- package/.mindforge/engine/autonomous/node-repair.md +190 -0
- package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
- package/.mindforge/engine/autonomous/steering-manager.md +64 -0
- package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
- package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
- package/.mindforge/memory/decision-library.jsonl +0 -0
- package/.mindforge/memory/engine/capture-protocol.md +36 -0
- package/.mindforge/memory/engine/global-sync-spec.md +42 -0
- package/.mindforge/memory/engine/retrieval-spec.md +44 -0
- package/.mindforge/memory/knowledge-base.jsonl +7 -0
- package/.mindforge/memory/pattern-library.jsonl +1 -0
- package/.mindforge/memory/team-preferences.jsonl +4 -0
- package/.mindforge/models/model-registry.md +48 -0
- package/.mindforge/models/model-router.md +30 -0
- package/.mindforge/personas/research-agent.md +24 -0
- package/.planning/approvals/v2-architecture-approval.json +15 -0
- package/.planning/browser-daemon.log +32 -0
- package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
- package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
- package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
- package/CHANGELOG.md +81 -0
- package/MINDFORGE.md +26 -3
- package/README.md +70 -18
- package/bin/autonomous/auto-runner.js +95 -0
- package/bin/autonomous/headless.js +36 -0
- package/bin/autonomous/progress-stream.js +49 -0
- package/bin/autonomous/repair-operator.js +213 -0
- package/bin/autonomous/steer.js +71 -0
- package/bin/autonomous/stuck-monitor.js +77 -0
- package/bin/browser/browser-daemon.js +139 -0
- package/bin/browser/daemon-manager.js +91 -0
- package/bin/browser/qa-engine.js +47 -0
- package/bin/browser/qa-report-writer.js +32 -0
- package/bin/browser/regression-writer.js +27 -0
- package/bin/browser/screenshot-store.js +49 -0
- package/bin/browser/session-manager.js +93 -0
- package/bin/browser/visual-verify-executor.js +89 -0
- 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/install.js +4 -4
- package/bin/installer-core.js +91 -35
- package/bin/memory/cli.js +99 -0
- package/bin/memory/global-sync.js +107 -0
- package/bin/memory/knowledge-capture.js +278 -0
- package/bin/memory/knowledge-indexer.js +172 -0
- package/bin/memory/knowledge-store.js +319 -0
- package/bin/memory/session-memory-loader.js +137 -0
- package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
- package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
- package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
- package/bin/migrations/migrate.js +15 -11
- package/bin/mindforge-cli.js +87 -0
- package/bin/models/anthropic-provider.js +77 -0
- package/bin/models/cost-tracker.js +118 -0
- package/bin/models/gemini-provider.js +79 -0
- package/bin/models/model-client.js +98 -0
- package/bin/models/model-router.js +111 -0
- package/bin/models/openai-provider.js +78 -0
- package/bin/research/research-engine.js +115 -0
- package/bin/review/cross-review-engine.js +81 -0
- package/bin/review/finding-synthesizer.js +116 -0
- package/bin/review/review-report-writer.js +49 -0
- package/bin/updater/self-update.js +13 -13
- package/bin/wizard/setup-wizard.js +5 -1
- package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
- package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
- package/docs/adr/ADR-026-session-persistence-security.md +20 -0
- package/docs/architecture/README.md +6 -2
- package/docs/ci-cd.md +92 -0
- package/docs/commands-reference.md +1 -0
- package/docs/feature-dashboard.md +52 -0
- package/docs/publishing-guide.md +43 -0
- package/docs/reference/commands.md +17 -2
- package/docs/reference/sdk-api.md +6 -1
- package/docs/testing-current-version.md +130 -0
- package/docs/user-guide.md +115 -9
- package/docs/usp-features.md +70 -8
- package/docs/workflow-atlas.md +57 -0
- 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 };
|