claudeboard 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/board-client.js +15 -5
- package/agents/expo-health.js +202 -131
- package/agents/orchestrator.js +9 -0
- package/agents/qa.js +32 -1
- package/package.json +1 -1
package/agents/board-client.js
CHANGED
|
@@ -54,12 +54,20 @@ export async function addLog(taskId, message, type = "progress") {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export async function createEpic(name) {
|
|
57
|
+
// Check if epic with this name already exists for this project
|
|
58
|
+
const { data: existing } = await supabase
|
|
59
|
+
.from("cb_epics")
|
|
60
|
+
.select("id")
|
|
61
|
+
.eq("project", PROJECT)
|
|
62
|
+
.eq("name", name)
|
|
63
|
+
.single();
|
|
64
|
+
if (existing) return existing.id;
|
|
57
65
|
const { data } = await supabase.from("cb_epics").insert({ name, project: PROJECT }).select().single();
|
|
58
|
-
return data;
|
|
66
|
+
return data?.id;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
export async function createTask({ epicId, title, description, priority = "medium", type = "feature" }) {
|
|
62
|
-
const
|
|
69
|
+
export async function createTask({ epicId, title, description, priority = "medium", priorityOrder, type = "feature", status = "todo" }) {
|
|
70
|
+
const defaultOrder = { high: 1, medium: 2, low: 3 };
|
|
63
71
|
const { data } = await supabase
|
|
64
72
|
.from("cb_tasks")
|
|
65
73
|
.insert({
|
|
@@ -68,14 +76,16 @@ export async function createTask({ epicId, title, description, priority = "mediu
|
|
|
68
76
|
title,
|
|
69
77
|
description,
|
|
70
78
|
priority,
|
|
71
|
-
priority_order: priorityOrder[priority]
|
|
79
|
+
priority_order: priorityOrder ?? defaultOrder[priority] ?? 2,
|
|
72
80
|
type,
|
|
73
|
-
status
|
|
81
|
+
status,
|
|
74
82
|
})
|
|
75
83
|
.select()
|
|
76
84
|
.single();
|
|
77
85
|
return data;
|
|
78
86
|
}
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
79
89
|
|
|
80
90
|
/**
|
|
81
91
|
* Check if this project already has tasks in the board
|
package/agents/expo-health.js
CHANGED
|
@@ -1,82 +1,135 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callClaudeJSON } from "./claude-api.js";
|
|
2
2
|
import { runCommand } from "../tools/terminal.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createTask, createEpic, addLog } from "./board-client.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
|
+
import { createConnection } from "net";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
|
-
|
|
11
|
-
const MAX_FIX_ATTEMPTS = 3;
|
|
11
|
+
const MAX_FIX_ATTEMPTS = 5;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Expo Health Check Agent
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Expo Health Check Agent — runs BEFORE the development loop.
|
|
15
|
+
*
|
|
16
|
+
* Strategy:
|
|
17
|
+
* 1. Try npm install (--legacy-peer-deps, then --force if needed)
|
|
18
|
+
* 2. If install fails with ETARGET/missing version → ask Claude for fix → apply → retry
|
|
19
|
+
* 3. Start Expo, wait for Metro to be truly ready (not just port open)
|
|
20
|
+
* 4. If Metro crashes with module/dep errors → ask Claude → apply → retry
|
|
21
|
+
* 5. If still broken after MAX attempts → inject a BLOCKER task into the board
|
|
22
|
+
* so the developer agent fixes it before any other task runs
|
|
23
|
+
*
|
|
24
|
+
* Returns { ready: boolean, process: ChildProcess|null }
|
|
20
25
|
*/
|
|
21
26
|
export async function runExpoHealthCheck(projectPath, port = 8081) {
|
|
22
27
|
console.log(chalk.bold.cyan("\n[ EXPO HEALTH CHECK ]\n"));
|
|
23
28
|
|
|
24
29
|
if (!fs.existsSync(path.join(projectPath, "package.json"))) {
|
|
25
|
-
console.log(chalk.dim(" No package.json
|
|
30
|
+
console.log(chalk.dim(" No package.json — skipping"));
|
|
26
31
|
return { ready: false, process: null };
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
// ──
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"npm install
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Try to fix install errors before continuing
|
|
40
|
-
const fixed = await fixInstallErrors(projectPath, installErrors);
|
|
41
|
-
if (!fixed) {
|
|
42
|
-
console.log(chalk.red(" ✗ Could not fix install errors — continuing without Expo"));
|
|
43
|
-
return { ready: false, process: null };
|
|
44
|
-
}
|
|
34
|
+
// ── 1. Install ─────────────────────────────────────────────────────────────
|
|
35
|
+
const installOk = await installDeps(projectPath);
|
|
36
|
+
if (!installOk) {
|
|
37
|
+
await injectFixTask(projectPath, "npm install fails — cannot start Expo",
|
|
38
|
+
"npm install fails with ETARGET or unresolvable version conflicts. " +
|
|
39
|
+
"Audit package.json, fix all version constraints to be compatible with " +
|
|
40
|
+
"the installed Expo SDK, then run npm install --legacy-peer-deps."
|
|
41
|
+
);
|
|
42
|
+
console.log(chalk.yellow(" ✗ Install failed — injected fix task into board\n"));
|
|
43
|
+
return { ready: false, process: null };
|
|
45
44
|
}
|
|
46
|
-
console.log(chalk.dim(" ✓ Dependencies installed"));
|
|
47
45
|
|
|
48
|
-
// ──
|
|
46
|
+
// ── 2. Start Expo + verify Metro is error-free ─────────────────────────────
|
|
49
47
|
for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
|
|
50
48
|
console.log(chalk.dim(` Starting Expo (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
|
|
51
49
|
|
|
52
50
|
const result = await tryStartExpo(projectPath, port);
|
|
53
51
|
|
|
54
52
|
if (result.ready) {
|
|
55
|
-
console.log(chalk.green(` ✓ Expo running on port ${port}\n`));
|
|
53
|
+
console.log(chalk.green(` ✓ Expo running cleanly on port ${port}\n`));
|
|
56
54
|
return { ready: true, process: result.process };
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
const error = result.error;
|
|
60
|
-
console.log(chalk.yellow(` ✗ Expo
|
|
58
|
+
console.log(chalk.yellow(` ✗ Expo error: ${error?.split("\n")[0]?.slice(0, 120)}`));
|
|
61
59
|
|
|
62
60
|
if (attempt === MAX_FIX_ATTEMPTS) break;
|
|
63
61
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// Ask Claude for a fix and apply it
|
|
63
|
+
const fixed = await applyExpoFix(projectPath, error);
|
|
64
|
+
if (!fixed) break;
|
|
65
|
+
console.log(chalk.dim(" Fix applied — retrying..."));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// All attempts failed — inject task so developer agent fixes it
|
|
69
|
+
const lastError = await getLastExpoError(projectPath);
|
|
70
|
+
await injectFixTask(projectPath,
|
|
71
|
+
"FIX: Expo fails to start — resolve dependency errors",
|
|
72
|
+
`Expo cannot start due to dependency errors. Fix ALL issues so the app boots without errors.\n\n` +
|
|
73
|
+
`Last error:\n${lastError}\n\n` +
|
|
74
|
+
`Steps:\n` +
|
|
75
|
+
`1. Read package.json and identify incompatible versions\n` +
|
|
76
|
+
`2. Fix react-native-worklets, react-native-reanimated and any other conflicting packages\n` +
|
|
77
|
+
`3. Run: npm install --legacy-peer-deps\n` +
|
|
78
|
+
`4. Verify with: npx expo start --web --port ${port} (should not crash)\n` +
|
|
79
|
+
`5. If imports fail (Unable to resolve module), remove or replace the problematic import`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.yellow(" ✗ Expo broken — injected fix task (will be worked on first)\n"));
|
|
83
|
+
return { ready: false, process: null };
|
|
84
|
+
}
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
// ── npm install with progressive fallback ─────────────────────────────────
|
|
87
|
+
async function installDeps(projectPath) {
|
|
88
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
|
|
89
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
90
|
+
|
|
91
|
+
// Step 1: legacy-peer-deps
|
|
92
|
+
console.log(chalk.dim(" npm install --legacy-peer-deps..."));
|
|
93
|
+
let result = await runCommand("npm install --legacy-peer-deps 2>&1", projectPath, 180000);
|
|
94
|
+
if (!hasFatalNpmError(result.stdout)) {
|
|
95
|
+
console.log(chalk.dim(" ✓ Dependencies installed"));
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const error1 = result.stdout;
|
|
100
|
+
console.log(chalk.yellow(` ⚠ Install error: ${getFatalError(error1)}`));
|
|
101
|
+
|
|
102
|
+
// Step 2: ask Claude to fix version conflicts
|
|
103
|
+
console.log(chalk.dim(" Asking Claude to resolve version conflicts..."));
|
|
104
|
+
const fix = await askClaudeForInstallFix(deps, error1);
|
|
105
|
+
if (fix?.commands?.length) {
|
|
106
|
+
console.log(chalk.dim(` Diagnosis: ${fix.diagnosis}`));
|
|
107
|
+
for (const cmd of fix.commands) {
|
|
108
|
+
console.log(chalk.dim(` → ${cmd}`));
|
|
109
|
+
await runCommand(cmd + " 2>&1", projectPath, 180000);
|
|
110
|
+
}
|
|
111
|
+
// Retry install
|
|
112
|
+
result = await runCommand("npm install --legacy-peer-deps 2>&1", projectPath, 180000);
|
|
113
|
+
if (!hasFatalNpmError(result.stdout)) {
|
|
114
|
+
console.log(chalk.dim(" ✓ Dependencies installed after fix"));
|
|
115
|
+
return true;
|
|
71
116
|
}
|
|
72
|
-
console.log(chalk.dim(` Applied fix — retrying...`));
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
|
|
76
|
-
|
|
119
|
+
// Step 3: --force (last resort)
|
|
120
|
+
console.log(chalk.dim(" Retrying with --force..."));
|
|
121
|
+
result = await runCommand("npm install --force 2>&1", projectPath, 180000);
|
|
122
|
+
if (!hasFatalNpmError(result.stdout)) {
|
|
123
|
+
console.log(chalk.dim(" ✓ Dependencies installed (--force)"));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
77
128
|
}
|
|
78
129
|
|
|
79
|
-
// ──
|
|
130
|
+
// ── Start Expo and wait until Metro is TRULY ready (no errors) ─────────────
|
|
131
|
+
// We wait up to 60s. Success = "Metro waiting on" AND no error lines.
|
|
132
|
+
// Failure = any "Unable to resolve module", "Cannot find module", etc.
|
|
80
133
|
async function tryStartExpo(projectPath, port) {
|
|
81
134
|
return new Promise((resolve) => {
|
|
82
135
|
let output = "";
|
|
@@ -86,64 +139,64 @@ async function tryStartExpo(projectPath, port) {
|
|
|
86
139
|
const done = (ready, error) => {
|
|
87
140
|
if (resolved) return;
|
|
88
141
|
resolved = true;
|
|
89
|
-
if (!ready && proc) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
142
|
+
if (!ready && proc) { try { proc.kill(); } catch {} proc = null; }
|
|
143
|
+
// Save error for later
|
|
144
|
+
if (!ready && error) saveLastError(projectPath, error);
|
|
93
145
|
resolve({ ready, process: proc, error });
|
|
94
146
|
};
|
|
95
147
|
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
const timeout = setTimeout(() =>
|
|
149
|
+
done(false, `Expo did not become ready within 60s.\n${output.slice(-800)}`), 60000);
|
|
98
150
|
|
|
99
151
|
try {
|
|
100
152
|
const { spawn } = require("child_process");
|
|
101
153
|
proc = spawn("npx", ["expo", "start", "--web", "--port", String(port)], {
|
|
102
154
|
cwd: projectPath,
|
|
103
|
-
env: {
|
|
104
|
-
...process.env,
|
|
105
|
-
CI: "1",
|
|
106
|
-
EXPO_NO_INTERACTIVE: "1",
|
|
107
|
-
EXPO_NO_DOTENV: "0",
|
|
108
|
-
},
|
|
155
|
+
env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" },
|
|
109
156
|
stdio: "pipe",
|
|
110
157
|
});
|
|
111
158
|
|
|
159
|
+
const checkError = (text) => {
|
|
160
|
+
if (
|
|
161
|
+
text.includes("Unable to resolve module") ||
|
|
162
|
+
text.includes("Cannot find module") ||
|
|
163
|
+
text.includes("Error: Unable") ||
|
|
164
|
+
text.includes("Module not found") ||
|
|
165
|
+
text.includes("SyntaxError") ||
|
|
166
|
+
text.includes("ENOENT: no such file")
|
|
167
|
+
) {
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
setTimeout(() => done(false, output.slice(-2000)), 1500); // collect full error
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
112
173
|
proc.stdout.on("data", (d) => {
|
|
113
174
|
const text = d.toString();
|
|
114
175
|
output += text;
|
|
115
|
-
//
|
|
176
|
+
// Metro ready signal — but wait 3s to make sure no errors follow
|
|
116
177
|
if (
|
|
117
178
|
text.includes("Metro waiting on") ||
|
|
118
|
-
text.includes(`localhost:${port}`) ||
|
|
119
|
-
text.includes("
|
|
120
|
-
text.includes("
|
|
179
|
+
text.includes(`http://localhost:${port}`) ||
|
|
180
|
+
text.includes("Bundling complete") ||
|
|
181
|
+
text.includes("Web is waiting on")
|
|
121
182
|
) {
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
// If we haven't already failed, declare success
|
|
185
|
+
if (!resolved) { clearTimeout(timeout); done(true, null); }
|
|
186
|
+
}, 3000);
|
|
124
187
|
}
|
|
188
|
+
checkError(text);
|
|
125
189
|
});
|
|
126
190
|
|
|
127
191
|
proc.stderr.on("data", (d) => {
|
|
128
192
|
const text = d.toString();
|
|
129
193
|
output += text;
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
text.includes("Unable to resolve module") ||
|
|
133
|
-
text.includes("Cannot find module") ||
|
|
134
|
-
text.includes("Error: Cannot") ||
|
|
135
|
-
text.includes("ENOENT") ||
|
|
136
|
-
text.includes("SyntaxError")
|
|
137
|
-
) {
|
|
138
|
-
clearTimeout(timeout);
|
|
139
|
-
// Small delay to collect full error
|
|
140
|
-
setTimeout(() => done(false, output.slice(-1500)), 1000);
|
|
141
|
-
}
|
|
194
|
+
checkError(text);
|
|
142
195
|
});
|
|
143
196
|
|
|
144
197
|
proc.on("close", (code) => {
|
|
145
198
|
clearTimeout(timeout);
|
|
146
|
-
if (
|
|
199
|
+
if (!resolved) done(false, `Expo exited (code ${code})\n${output.slice(-1000)}`);
|
|
147
200
|
});
|
|
148
201
|
|
|
149
202
|
} catch (e) {
|
|
@@ -153,88 +206,106 @@ async function tryStartExpo(projectPath, port) {
|
|
|
153
206
|
});
|
|
154
207
|
}
|
|
155
208
|
|
|
156
|
-
// ── Ask Claude to
|
|
157
|
-
async function
|
|
158
|
-
const
|
|
159
|
-
const pkg = JSON.parse(pkgRaw);
|
|
209
|
+
// ── Ask Claude how to fix a runtime Expo error ────────────────────────────
|
|
210
|
+
async function applyExpoFix(projectPath, errorText) {
|
|
211
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
|
|
160
212
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
161
213
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
214
|
+
let verdict;
|
|
215
|
+
try {
|
|
216
|
+
verdict = await callClaudeJSON(
|
|
217
|
+
"You are a React Native / Expo dependency expert. Respond only with valid JSON.",
|
|
218
|
+
`Expo failed to start with this error:
|
|
165
219
|
\`\`\`
|
|
166
220
|
${errorText}
|
|
167
221
|
\`\`\`
|
|
168
222
|
|
|
169
|
-
Current
|
|
170
|
-
\`\`\`json
|
|
223
|
+
Current dependencies:
|
|
171
224
|
${JSON.stringify(deps, null, 2)}
|
|
172
|
-
\`\`\`
|
|
173
|
-
|
|
174
|
-
Project path: ${projectPath}
|
|
175
225
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
-
|
|
179
|
-
- Missing modules: npm install <package>
|
|
180
|
-
- Peer dep issues: npm install <pkg>@<compatible-version> --legacy-peer-deps
|
|
181
|
-
- Wrong import paths: patch the source file
|
|
226
|
+
Provide the exact commands to fix this.
|
|
227
|
+
For "Unable to resolve module X from file Y": the fix is usually to install the missing package or fix its version.
|
|
228
|
+
For react-native-worklets version conflict: install react-native-worklets@latest.
|
|
182
229
|
|
|
183
230
|
Respond with JSON:
|
|
184
231
|
{
|
|
185
|
-
"diagnosis": "
|
|
232
|
+
"diagnosis": "one sentence",
|
|
186
233
|
"commands": ["npm install ...", "..."],
|
|
187
234
|
"confidence": 0-100
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let verdict;
|
|
191
|
-
try {
|
|
192
|
-
const { callClaudeJSON } = await import("./claude-api.js");
|
|
193
|
-
verdict = await callClaudeJSON(
|
|
194
|
-
"You are a React Native dependency expert. Respond only with valid JSON.",
|
|
195
|
-
prompt
|
|
235
|
+
}`
|
|
196
236
|
);
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
237
|
+
} catch { return false; }
|
|
200
238
|
|
|
201
|
-
if (!verdict?.commands?.length || verdict.confidence <
|
|
202
|
-
console.log(chalk.dim(` Diagnosis: ${verdict?.diagnosis || "unknown"} (confidence: ${verdict?.confidence}%)`));
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
239
|
+
if (!verdict?.commands?.length || verdict.confidence < 30) return false;
|
|
205
240
|
|
|
206
241
|
console.log(chalk.dim(` Diagnosis: ${verdict.diagnosis}`));
|
|
207
|
-
console.log(chalk.dim(` Applying ${verdict.commands.length} fix command(s)...`));
|
|
208
|
-
|
|
209
242
|
for (const cmd of verdict.commands) {
|
|
210
|
-
console.log(chalk.dim(`
|
|
211
|
-
|
|
212
|
-
if (result.returncode !== 0) {
|
|
213
|
-
const errLine = result.stdout.split("\n").find(l => l.includes("error")) || result.stdout.slice(-200);
|
|
214
|
-
console.log(chalk.yellow(` ⚠ Command failed: ${errLine}`));
|
|
215
|
-
}
|
|
243
|
+
console.log(chalk.dim(` → ${cmd}`));
|
|
244
|
+
await runCommand(cmd + " 2>&1", projectPath, 120000);
|
|
216
245
|
}
|
|
217
|
-
|
|
218
246
|
return true;
|
|
219
247
|
}
|
|
220
248
|
|
|
221
|
-
// ──
|
|
222
|
-
async function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
// ── Ask Claude how to fix npm install failures ────────────────────────────
|
|
250
|
+
async function askClaudeForInstallFix(deps, errorText) {
|
|
251
|
+
try {
|
|
252
|
+
return await callClaudeJSON(
|
|
253
|
+
"You are a React Native dependency expert. Respond only with valid JSON.",
|
|
254
|
+
`npm install failed:
|
|
255
|
+
${errorText}
|
|
256
|
+
|
|
257
|
+
Dependencies: ${JSON.stringify(deps, null, 2)}
|
|
258
|
+
|
|
259
|
+
Provide exact commands to resolve the version conflict.
|
|
260
|
+
For ETARGET (version not found): suggest the nearest valid version.
|
|
261
|
+
For peer dep conflicts: suggest compatible versions for all conflicting packages.
|
|
262
|
+
|
|
263
|
+
JSON: { "diagnosis": "...", "commands": ["..."], "confidence": 0-100 }`
|
|
264
|
+
);
|
|
265
|
+
} catch { return null; }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Create a high-priority fix task in the board ──────────────────────────
|
|
269
|
+
async function injectFixTask(projectPath, title, description) {
|
|
270
|
+
try {
|
|
271
|
+
// Make sure the epic exists
|
|
272
|
+
const epicId = await createEpic("⚠ Expo Fix Required", "expo-fix");
|
|
273
|
+
await createTask({
|
|
274
|
+
epicId,
|
|
275
|
+
title,
|
|
276
|
+
description,
|
|
277
|
+
priority: "high",
|
|
278
|
+
priorityOrder: 0, // Run FIRST
|
|
279
|
+
type: "bug",
|
|
280
|
+
status: "todo",
|
|
281
|
+
});
|
|
282
|
+
console.log(chalk.yellow(` ➕ Injected task: "${title}"`));
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.log(chalk.dim(` Could not inject task: ${e.message}`));
|
|
229
285
|
}
|
|
230
|
-
return false;
|
|
231
286
|
}
|
|
232
287
|
|
|
233
|
-
// ──
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
// ── Save/read last error to disk for later reference ─────────────────────
|
|
289
|
+
function saveLastError(projectPath, error) {
|
|
290
|
+
try {
|
|
291
|
+
fs.writeFileSync(path.join(projectPath, ".claudeboard-expo-error.txt"), error, "utf8");
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function getLastExpoError(projectPath) {
|
|
296
|
+
try {
|
|
297
|
+
const f = path.join(projectPath, ".claudeboard-expo-error.txt");
|
|
298
|
+
return fs.existsSync(f) ? fs.readFileSync(f, "utf8").slice(-1500) : "Unknown error";
|
|
299
|
+
} catch { return "Unknown error"; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
303
|
+
function hasFatalNpmError(output) {
|
|
304
|
+
return output.includes("npm error code ETARGET") ||
|
|
305
|
+
output.includes("notarget No matching version") ||
|
|
306
|
+
(output.includes("npm error") && output.includes("ERESOLVE") && output.includes("unable to resolve"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getFatalError(output) {
|
|
310
|
+
return output.split("\n").find(l => l.includes("npm error") && !l.includes("peer")) || output.slice(-200);
|
|
240
311
|
}
|
package/agents/orchestrator.js
CHANGED
|
@@ -140,6 +140,15 @@ export async function runOrchestrator(config) {
|
|
|
140
140
|
|
|
141
141
|
consecutiveFailures = 0;
|
|
142
142
|
|
|
143
|
+
// ── If this was an Expo fix task, clear the error and re-health-check ────
|
|
144
|
+
if (task.title?.toLowerCase().includes("expo") && task.type === "bug") {
|
|
145
|
+
try { fs.unlinkSync(path.join(projectPath, ".claudeboard-expo-error.txt")); } catch {}
|
|
146
|
+
console.log(chalk.dim(" Expo fix completed — re-running health check..."));
|
|
147
|
+
const recheck = await runExpoHealthCheck(projectPath, expoPort);
|
|
148
|
+
expoReady = recheck.ready;
|
|
149
|
+
expoProcess = recheck.process;
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
// ── Re-check Expo health if it's supposed to be running but isn't ────────
|
|
144
153
|
if (expoReady) {
|
|
145
154
|
const portOpen = await isPortOpen(expoPort);
|
package/agents/qa.js
CHANGED
|
@@ -33,11 +33,22 @@ export async function runQAAgent(task, devResult, projectPath, prdContent, expoP
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// ── 3. Screenshot (only if Expo is running)
|
|
36
|
+
// ── 3. Screenshot + Metro error check (only if Expo is running) ───────────
|
|
37
37
|
let visualVerdict = null;
|
|
38
38
|
const expoRunning = await isPortOpen(expoPort);
|
|
39
39
|
|
|
40
40
|
if (expoRunning) {
|
|
41
|
+
// Verify Metro is actually serving without errors (not just port open)
|
|
42
|
+
const metroCheck = await checkMetroHealth(expoPort, projectPath);
|
|
43
|
+
if (!metroCheck.healthy) {
|
|
44
|
+
await addLog(task.id, `Metro error detected: ${metroCheck.error?.slice(0, 100)}`, "error");
|
|
45
|
+
return {
|
|
46
|
+
passed: false,
|
|
47
|
+
issues: [`Expo/Metro has active errors: ${metroCheck.error?.slice(0, 200)}`],
|
|
48
|
+
fixInstructions: `Metro is crashing with this error:\n${metroCheck.error}\n\nFix this error so the app loads without crashing on the device.`,
|
|
49
|
+
screenshotPath: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
41
52
|
await addLog(task.id, "Taking screenshot...", "progress");
|
|
42
53
|
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
43
54
|
const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
@@ -227,3 +238,23 @@ async function isPortOpen(port) {
|
|
|
227
238
|
});
|
|
228
239
|
} catch { return false; }
|
|
229
240
|
}
|
|
241
|
+
|
|
242
|
+
// Check Metro is serving without errors by fetching the bundle status
|
|
243
|
+
async function checkMetroHealth(port, projectPath) {
|
|
244
|
+
try {
|
|
245
|
+
// Read the expo error file if it exists (written by health check)
|
|
246
|
+
const errorFile = path.join(projectPath, ".claudeboard-expo-error.txt");
|
|
247
|
+
if (fs.existsSync(errorFile)) {
|
|
248
|
+
const errorContent = fs.readFileSync(errorFile, "utf8");
|
|
249
|
+
// Only fail if error is recent (file modified in last 5 min)
|
|
250
|
+
const stat = fs.statSync(errorFile);
|
|
251
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
252
|
+
if (ageMs < 5 * 60 * 1000 && errorContent.includes("Unable to resolve")) {
|
|
253
|
+
return { healthy: false, error: errorContent.slice(-800) };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { healthy: true };
|
|
257
|
+
} catch {
|
|
258
|
+
return { healthy: true }; // Don't fail if we can't check
|
|
259
|
+
}
|
|
260
|
+
}
|