claudeboard 2.3.0 → 2.8.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/claude-api.js +1 -1
- package/agents/expo-health.js +240 -0
- package/agents/orchestrator.js +28 -22
- package/agents/qa.js +160 -139
- package/bin/cli.js +11 -1
- package/dashboard/index.html +213 -70
- package/dashboard/server.js +89 -35
- package/package.json +1 -1
package/agents/claude-api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const MODEL = "claude-sonnet-4-20250514";
|
|
7
|
-
const MAX_TOKENS =
|
|
7
|
+
const MAX_TOKENS = 8096; // Max output tokens — input context window is 200k, no limits there
|
|
8
8
|
|
|
9
9
|
function getHeaders() {
|
|
10
10
|
const key = process.env.ANTHROPIC_API_KEY;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { callClaude } from "./claude-api.js";
|
|
2
|
+
import { runCommand } from "../tools/terminal.js";
|
|
3
|
+
import { startProcess, waitForPort } from "../tools/terminal.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const MAX_FIX_ATTEMPTS = 3;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Expo Health Check Agent
|
|
15
|
+
* Runs before development loop starts.
|
|
16
|
+
* 1. Install deps
|
|
17
|
+
* 2. Try to start Expo
|
|
18
|
+
* 3. If it crashes, read the error and fix it (up to 3 attempts)
|
|
19
|
+
* 4. Return { ready: true/false, process, port }
|
|
20
|
+
*/
|
|
21
|
+
export async function runExpoHealthCheck(projectPath, port = 8081) {
|
|
22
|
+
console.log(chalk.bold.cyan("\n[ EXPO HEALTH CHECK ]\n"));
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(path.join(projectPath, "package.json"))) {
|
|
25
|
+
console.log(chalk.dim(" No package.json found — skipping Expo health check"));
|
|
26
|
+
return { ready: false, process: null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Step 1: Install dependencies ──────────────────────────────────────────
|
|
30
|
+
console.log(chalk.dim(" Installing dependencies (--legacy-peer-deps)..."));
|
|
31
|
+
const installResult = await runCommand(
|
|
32
|
+
"npm install --legacy-peer-deps 2>&1",
|
|
33
|
+
projectPath,
|
|
34
|
+
120000
|
|
35
|
+
);
|
|
36
|
+
const installErrors = extractNpmErrors(installResult.stdout);
|
|
37
|
+
if (installErrors) {
|
|
38
|
+
console.log(chalk.yellow(` ⚠ npm install issues detected:\n${installErrors}`));
|
|
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
|
+
}
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.dim(" ✓ Dependencies installed"));
|
|
47
|
+
|
|
48
|
+
// ── Step 2: Attempt Expo start with error detection ───────────────────────
|
|
49
|
+
for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
|
|
50
|
+
console.log(chalk.dim(` Starting Expo (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
|
|
51
|
+
|
|
52
|
+
const result = await tryStartExpo(projectPath, port);
|
|
53
|
+
|
|
54
|
+
if (result.ready) {
|
|
55
|
+
console.log(chalk.green(` ✓ Expo running on port ${port}\n`));
|
|
56
|
+
return { ready: true, process: result.process };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const error = result.error;
|
|
60
|
+
console.log(chalk.yellow(` ✗ Expo failed to start:\n ${error?.slice(0, 200)}`));
|
|
61
|
+
|
|
62
|
+
if (attempt === MAX_FIX_ATTEMPTS) break;
|
|
63
|
+
|
|
64
|
+
// ── Step 3: Ask Claude to fix the error ─────────────────────────────────
|
|
65
|
+
console.log(chalk.dim(` Asking Claude to fix the error...`));
|
|
66
|
+
const fixed = await fixExpoError(projectPath, error);
|
|
67
|
+
|
|
68
|
+
if (!fixed) {
|
|
69
|
+
console.log(chalk.red(" Could not determine fix — skipping Expo"));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.dim(` Applied fix — retrying...`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(chalk.yellow(" ⚠ Expo not available — continuing without visual QA\n"));
|
|
76
|
+
return { ready: false, process: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Try to start Expo and detect if it crashes or succeeds ─────────────────
|
|
80
|
+
async function tryStartExpo(projectPath, port) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let output = "";
|
|
83
|
+
let resolved = false;
|
|
84
|
+
let proc = null;
|
|
85
|
+
|
|
86
|
+
const done = (ready, error) => {
|
|
87
|
+
if (resolved) return;
|
|
88
|
+
resolved = true;
|
|
89
|
+
if (!ready && proc) {
|
|
90
|
+
try { proc.kill(); } catch {}
|
|
91
|
+
proc = null;
|
|
92
|
+
}
|
|
93
|
+
resolve({ ready, process: proc, error });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Collect logs for 20 seconds, then give up if no success
|
|
97
|
+
const timeout = setTimeout(() => done(false, `Expo did not start within 20s. Last output:\n${output.slice(-500)}`), 20000);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const { spawn } = require("child_process");
|
|
101
|
+
proc = spawn("npx", ["expo", "start", "--web", "--port", String(port)], {
|
|
102
|
+
cwd: projectPath,
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
CI: "1",
|
|
106
|
+
EXPO_NO_INTERACTIVE: "1",
|
|
107
|
+
EXPO_NO_DOTENV: "0",
|
|
108
|
+
},
|
|
109
|
+
stdio: "pipe",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
proc.stdout.on("data", (d) => {
|
|
113
|
+
const text = d.toString();
|
|
114
|
+
output += text;
|
|
115
|
+
// Success signals
|
|
116
|
+
if (
|
|
117
|
+
text.includes("Metro waiting on") ||
|
|
118
|
+
text.includes(`localhost:${port}`) ||
|
|
119
|
+
text.includes("Web is waiting") ||
|
|
120
|
+
text.includes("Bundling complete")
|
|
121
|
+
) {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
done(true, null);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
proc.stderr.on("data", (d) => {
|
|
128
|
+
const text = d.toString();
|
|
129
|
+
output += text;
|
|
130
|
+
// Fatal crash signals
|
|
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
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
proc.on("close", (code) => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
if (code !== 0) done(false, `Process exited with code ${code}.\n${output.slice(-1000)}`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
} catch (e) {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
done(false, e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Ask Claude to diagnose and apply the fix ──────────────────────────────
|
|
157
|
+
async function fixExpoError(projectPath, errorText) {
|
|
158
|
+
const pkgRaw = fs.readFileSync(path.join(projectPath, "package.json"), "utf8");
|
|
159
|
+
const pkg = JSON.parse(pkgRaw);
|
|
160
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
161
|
+
|
|
162
|
+
const prompt = `You are a React Native / Expo dependency expert.
|
|
163
|
+
|
|
164
|
+
The app failed to start with this error:
|
|
165
|
+
\`\`\`
|
|
166
|
+
${errorText}
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
Current package.json dependencies:
|
|
170
|
+
\`\`\`json
|
|
171
|
+
${JSON.stringify(deps, null, 2)}
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
Project path: ${projectPath}
|
|
175
|
+
|
|
176
|
+
Diagnose the root cause and provide the EXACT shell commands to fix it.
|
|
177
|
+
Common fixes:
|
|
178
|
+
- Version conflicts: upgrade/downgrade specific packages
|
|
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
|
|
182
|
+
|
|
183
|
+
Respond with JSON:
|
|
184
|
+
{
|
|
185
|
+
"diagnosis": "One sentence explanation of the root cause",
|
|
186
|
+
"commands": ["npm install ...", "..."],
|
|
187
|
+
"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
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!verdict?.commands?.length || verdict.confidence < 40) {
|
|
202
|
+
console.log(chalk.dim(` Diagnosis: ${verdict?.diagnosis || "unknown"} (confidence: ${verdict?.confidence}%)`));
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(chalk.dim(` Diagnosis: ${verdict.diagnosis}`));
|
|
207
|
+
console.log(chalk.dim(` Applying ${verdict.commands.length} fix command(s)...`));
|
|
208
|
+
|
|
209
|
+
for (const cmd of verdict.commands) {
|
|
210
|
+
console.log(chalk.dim(` → ${cmd}`));
|
|
211
|
+
const result = await runCommand(cmd + " 2>&1", projectPath, 120000);
|
|
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
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Fix npm install errors ──────────────────────────────────────────────────
|
|
222
|
+
async function fixInstallErrors(projectPath, errorText) {
|
|
223
|
+
// Try --force as a last resort for install errors
|
|
224
|
+
if (errorText.includes("ERESOLVE") || errorText.includes("peer")) {
|
|
225
|
+
console.log(chalk.dim(" Retrying with --force..."));
|
|
226
|
+
const result = await runCommand("npm install --force 2>&1", projectPath, 120000);
|
|
227
|
+
const stillBroken = extractNpmErrors(result.stdout);
|
|
228
|
+
return !stillBroken;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Extract real npm errors (ignore peer dep warnings) ─────────────────────
|
|
234
|
+
function extractNpmErrors(output) {
|
|
235
|
+
const lines = output.split("\n");
|
|
236
|
+
const errors = lines.filter(l =>
|
|
237
|
+
l.includes("npm error") && !l.includes("ERESOLVE") && !l.includes("peer")
|
|
238
|
+
);
|
|
239
|
+
return errors.length ? errors.join("\n") : null;
|
|
240
|
+
}
|
package/agents/orchestrator.js
CHANGED
|
@@ -5,6 +5,18 @@ import path from "path";
|
|
|
5
5
|
import { runArchitectAgent } from "./architect.js";
|
|
6
6
|
import { runDeveloperAgent } from "./developer.js";
|
|
7
7
|
import { runQAAgent, runFullAppQA } from "./qa.js";
|
|
8
|
+
import { runExpoHealthCheck } from "./expo-health.js";
|
|
9
|
+
import { createConnection } from "net";
|
|
10
|
+
|
|
11
|
+
function isPortOpen(port) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
14
|
+
sock.setTimeout(600);
|
|
15
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
16
|
+
sock.once("error", () => resolve(false));
|
|
17
|
+
sock.once("timeout", () => resolve(false));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
8
20
|
import {
|
|
9
21
|
initBoard,
|
|
10
22
|
getNextTask,
|
|
@@ -84,28 +96,11 @@ export async function runOrchestrator(config) {
|
|
|
84
96
|
const logFile = path.join(projectPath, ".claudeboard-logs.txt");
|
|
85
97
|
const logStream = fs.createWriteStream(logFile, { flags: "a" });
|
|
86
98
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
expoProcess = startProcess(
|
|
93
|
-
"npx",
|
|
94
|
-
["expo", "start", "--web", "--port", String(expoPort)],
|
|
95
|
-
projectPath,
|
|
96
|
-
(log) => {
|
|
97
|
-
logStream.write(log);
|
|
98
|
-
if (log.includes("Metro waiting") || log.includes("localhost:" + expoPort)) {
|
|
99
|
-
expoReady = true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Wait up to 30s for expo to start
|
|
105
|
-
await waitForPort(expoPort, 30000);
|
|
106
|
-
expoReady = true;
|
|
107
|
-
console.log(chalk.dim(` Expo Web running at http://localhost:${expoPort}\n`));
|
|
108
|
-
}
|
|
99
|
+
// ── EXPO HEALTH CHECK: install deps + start + auto-fix errors ─────────────
|
|
100
|
+
// Runs before development loop so the app is visible from task #1
|
|
101
|
+
const expoHealth = await runExpoHealthCheck(projectPath, expoPort);
|
|
102
|
+
expoReady = expoHealth.ready;
|
|
103
|
+
expoProcess = expoHealth.process;
|
|
109
104
|
|
|
110
105
|
// ── PHASE 2: DEVELOPMENT LOOP ─────────────────────────────────────────────
|
|
111
106
|
console.log(chalk.bold.cyan("[ PHASE 2: DEVELOPMENT ]\n"));
|
|
@@ -145,6 +140,17 @@ export async function runOrchestrator(config) {
|
|
|
145
140
|
|
|
146
141
|
consecutiveFailures = 0;
|
|
147
142
|
|
|
143
|
+
// ── Re-check Expo health if it's supposed to be running but isn't ────────
|
|
144
|
+
if (expoReady) {
|
|
145
|
+
const portOpen = await isPortOpen(expoPort);
|
|
146
|
+
if (!portOpen) {
|
|
147
|
+
console.log(chalk.yellow(" ⚠ Expo seems to have crashed — running health check..."));
|
|
148
|
+
const recheck = await runExpoHealthCheck(projectPath, expoPort);
|
|
149
|
+
expoReady = recheck.ready;
|
|
150
|
+
expoProcess = recheck.process;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
148
154
|
// Run QA agent (only if expo is running and this is a UI/feature task)
|
|
149
155
|
const shouldRunQA = expoReady && ["feature", "bug"].includes(task.type);
|
|
150
156
|
|