claudeboard 2.13.0 → 2.14.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/developer.js +1 -0
- package/agents/expo-health.js +235 -5
- package/agents/orchestrator.js +27 -1
- package/dashboard/server.js +5 -1
- package/package.json +1 -1
package/agents/developer.js
CHANGED
|
@@ -70,6 +70,7 @@ RULES:
|
|
|
70
70
|
- After writing files run: npx tsc --noEmit — fix any errors you find
|
|
71
71
|
- If you hit an error, read it carefully and fix it — iterate until it works
|
|
72
72
|
- Do NOT ask questions or ask for confirmation. Make your best judgment.
|
|
73
|
+
- Do NOT run npx expo run:ios or npx expo run:android — the orchestrator handles native builds
|
|
73
74
|
- When fully done, print EXACTLY this line: TASK_COMPLETE: <one sentence summary>
|
|
74
75
|
`;
|
|
75
76
|
|
package/agents/expo-health.js
CHANGED
|
@@ -4,12 +4,15 @@ 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
|
+
import crypto from "crypto";
|
|
7
8
|
import { createRequire } from "module";
|
|
8
9
|
import { createConnection } from "net";
|
|
9
10
|
import { spawn as _spawn, execSync } from "child_process";
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const MAX_FIX_ATTEMPTS = 5;
|
|
14
|
+
const BUILD_HASH_FILE = ".claudeboard-build-hash.txt";
|
|
15
|
+
const BUILD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min max for expo run:ios
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Expo Health Check Agent — runs BEFORE the development loop.
|
|
@@ -17,9 +20,10 @@ const MAX_FIX_ATTEMPTS = 5;
|
|
|
17
20
|
* Strategy:
|
|
18
21
|
* 1. Try npm install (--legacy-peer-deps, then --force if needed)
|
|
19
22
|
* 2. If install fails with ETARGET/missing version → ask Claude for fix → apply → retry
|
|
20
|
-
* 3.
|
|
21
|
-
* 4.
|
|
22
|
-
* 5. If
|
|
23
|
+
* 3. If iOS mode: ensure a dev build exists in the simulator (builds if needed)
|
|
24
|
+
* 4. Start Expo, wait for Metro to be truly ready (not just port open)
|
|
25
|
+
* 5. If Metro crashes with module/dep errors → ask Claude → apply → retry
|
|
26
|
+
* 6. If still broken after MAX attempts → inject a BLOCKER task into the board
|
|
23
27
|
* so the developer agent fixes it before any other task runs
|
|
24
28
|
*
|
|
25
29
|
* Returns { ready: boolean, process: ChildProcess|null }
|
|
@@ -44,7 +48,22 @@ export async function runExpoHealthCheck(projectPath, port = 8081) {
|
|
|
44
48
|
return { ready: false, process: null };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
// ── 2.
|
|
51
|
+
// ── 2. Dev build (iOS mode only) ───────────────────────────────────────────
|
|
52
|
+
const useIOS = process.env.CLAUDEBOARD_IOS === "1" || isSimulatorAvailableSync();
|
|
53
|
+
if (useIOS) {
|
|
54
|
+
const buildOk = await ensureDevBuild(projectPath);
|
|
55
|
+
if (!buildOk) {
|
|
56
|
+
await injectFixTask(projectPath,
|
|
57
|
+
"FIX: Dev build failed — fix native compilation errors",
|
|
58
|
+
"npx expo run:ios failed. Fix any native dependency or Podfile issues so the app compiles.\n" +
|
|
59
|
+
"Run: npx expo run:ios --simulator and fix all errors."
|
|
60
|
+
);
|
|
61
|
+
console.log(chalk.yellow(" ✗ Dev build failed — injected fix task into board\n"));
|
|
62
|
+
return { ready: false, process: null };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── 3. Start Expo + verify Metro is error-free ─────────────────────────────
|
|
48
67
|
for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS; attempt++) {
|
|
49
68
|
console.log(chalk.dim(` Starting Expo (attempt ${attempt}/${MAX_FIX_ATTEMPTS})...`));
|
|
50
69
|
|
|
@@ -84,6 +103,217 @@ export async function runExpoHealthCheck(projectPath, port = 8081) {
|
|
|
84
103
|
return { ready: false, process: null };
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
// ── Dev Build management ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compute a short hash of package.json to detect dependency changes.
|
|
110
|
+
*/
|
|
111
|
+
export function packageJsonHash(projectPath) {
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(path.join(projectPath, "package.json"), "utf8");
|
|
114
|
+
return crypto.createHash("md5").update(content).digest("hex").slice(0, 12);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns true if a rebuild is needed:
|
|
122
|
+
* - No saved hash (never built), OR
|
|
123
|
+
* - package.json changed since last build
|
|
124
|
+
*/
|
|
125
|
+
export function needsRebuild(projectPath) {
|
|
126
|
+
const hashFile = path.join(projectPath, BUILD_HASH_FILE);
|
|
127
|
+
if (!fs.existsSync(hashFile)) return true;
|
|
128
|
+
try {
|
|
129
|
+
const saved = fs.readFileSync(hashFile, "utf8").trim();
|
|
130
|
+
return saved !== packageJsonHash(projectPath);
|
|
131
|
+
} catch {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Persist the current package.json hash after a successful build.
|
|
138
|
+
*/
|
|
139
|
+
function saveBuildHash(projectPath) {
|
|
140
|
+
try {
|
|
141
|
+
const hash = packageJsonHash(projectPath);
|
|
142
|
+
if (hash) fs.writeFileSync(path.join(projectPath, BUILD_HASH_FILE), hash, "utf8");
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if the app's dev build is already installed in the booted simulator.
|
|
148
|
+
*/
|
|
149
|
+
function isDevBuildInstalled(projectPath) {
|
|
150
|
+
try {
|
|
151
|
+
const bundleId = getBundleId(projectPath);
|
|
152
|
+
if (!bundleId) return false;
|
|
153
|
+
const output = execSync(`xcrun simctl get_app_container booted "${bundleId}" 2>&1`, { encoding: "utf8" });
|
|
154
|
+
return output.trim().length > 0 && !output.includes("No such file");
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Read the iOS bundle identifier from app.json / app.config.js.
|
|
162
|
+
* Falls back to deriving it from the project slug.
|
|
163
|
+
*/
|
|
164
|
+
function getBundleId(projectPath) {
|
|
165
|
+
try {
|
|
166
|
+
const appJsonPath = path.join(projectPath, "app.json");
|
|
167
|
+
if (fs.existsSync(appJsonPath)) {
|
|
168
|
+
const cfg = JSON.parse(fs.readFileSync(appJsonPath, "utf8"));
|
|
169
|
+
const bundleId = cfg?.expo?.ios?.bundleIdentifier;
|
|
170
|
+
if (bundleId) return bundleId;
|
|
171
|
+
// Derive from slug if no explicit bundleIdentifier
|
|
172
|
+
const slug = cfg?.expo?.slug;
|
|
173
|
+
if (slug) return `com.anonymous.${slug.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Boot the first available iOS simulator if none is booted.
|
|
181
|
+
*/
|
|
182
|
+
function ensureSimulatorBooted() {
|
|
183
|
+
try {
|
|
184
|
+
const booted = execSync("xcrun simctl list devices booted 2>&1", { encoding: "utf8" });
|
|
185
|
+
if (booted.includes("Booted")) return true;
|
|
186
|
+
|
|
187
|
+
// Find a suitable device to boot
|
|
188
|
+
const devices = execSync("xcrun simctl list devices available 2>&1", { encoding: "utf8" });
|
|
189
|
+
const match = devices.match(/iPhone \d[^(]*\(([A-F0-9-]{36})\)/i);
|
|
190
|
+
if (!match) return false;
|
|
191
|
+
|
|
192
|
+
console.log(chalk.dim(` Booting simulator ${match[1]}...`));
|
|
193
|
+
execSync(`xcrun simctl boot "${match[1]}" 2>&1`, { encoding: "utf8" });
|
|
194
|
+
// Give the simulator a moment to fully boot
|
|
195
|
+
execSync("sleep 5");
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build and install the dev build in the simulator using `expo run:ios`.
|
|
204
|
+
* Only rebuilds when package.json changed or the app is not installed.
|
|
205
|
+
* Returns true on success, false on failure.
|
|
206
|
+
*/
|
|
207
|
+
export async function ensureDevBuild(projectPath) {
|
|
208
|
+
const installed = isDevBuildInstalled(projectPath);
|
|
209
|
+
const rebuild = needsRebuild(projectPath);
|
|
210
|
+
|
|
211
|
+
if (installed && !rebuild) {
|
|
212
|
+
console.log(chalk.dim(" ✓ Dev build up to date — skipping rebuild"));
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const reason = !installed ? "app not installed" : "dependencies changed";
|
|
217
|
+
console.log(chalk.cyan(` Building dev build (${reason})...`));
|
|
218
|
+
console.log(chalk.dim(" This may take several minutes on first run."));
|
|
219
|
+
|
|
220
|
+
// Make sure a simulator is running
|
|
221
|
+
const simReady = ensureSimulatorBooted();
|
|
222
|
+
if (!simReady) {
|
|
223
|
+
console.log(chalk.yellow(" ✗ No iOS simulator available"));
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
let output = "";
|
|
229
|
+
let resolved = false;
|
|
230
|
+
|
|
231
|
+
const done = (ok) => {
|
|
232
|
+
if (resolved) return;
|
|
233
|
+
resolved = true;
|
|
234
|
+
if (ok) {
|
|
235
|
+
saveBuildHash(projectPath);
|
|
236
|
+
console.log(chalk.green(" ✓ Dev build installed in simulator"));
|
|
237
|
+
}
|
|
238
|
+
resolve(ok);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const timeout = setTimeout(() => {
|
|
242
|
+
console.log(chalk.yellow(" ✗ Dev build timed out after 10 minutes"));
|
|
243
|
+
done(false);
|
|
244
|
+
}, BUILD_TIMEOUT_MS);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const proc = _spawn("npx", ["expo", "run:ios", "--simulator"], {
|
|
248
|
+
cwd: projectPath,
|
|
249
|
+
env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1" },
|
|
250
|
+
stdio: "pipe",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const onData = (d) => {
|
|
254
|
+
const text = d.toString();
|
|
255
|
+
output += text;
|
|
256
|
+
// Log progress lines that are meaningful
|
|
257
|
+
const lines = text.split("\n").filter(l => l.trim() && !l.includes("\u001b["));
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (
|
|
260
|
+
line.includes("Installing") ||
|
|
261
|
+
line.includes("Building") ||
|
|
262
|
+
line.includes("Compiling") ||
|
|
263
|
+
line.includes("Linking") ||
|
|
264
|
+
line.includes("error:") ||
|
|
265
|
+
line.includes("warning:")
|
|
266
|
+
) {
|
|
267
|
+
console.log(chalk.dim(` ${line.trim().slice(0, 100)}`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
text.includes("Installed on") ||
|
|
273
|
+
text.includes("Opening on") ||
|
|
274
|
+
text.includes("Successfully built") ||
|
|
275
|
+
text.includes("BUILD SUCCEEDED")
|
|
276
|
+
) {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
done(true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
text.includes("BUILD FAILED") ||
|
|
283
|
+
text.includes("error: ") ||
|
|
284
|
+
text.includes("Command failed")
|
|
285
|
+
) {
|
|
286
|
+
// Wait a bit to collect full error output
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
clearTimeout(timeout);
|
|
289
|
+
saveLastError(projectPath, output.slice(-3000));
|
|
290
|
+
done(false);
|
|
291
|
+
}, 2000);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
proc.stdout.on("data", onData);
|
|
296
|
+
proc.stderr.on("data", onData);
|
|
297
|
+
|
|
298
|
+
proc.on("close", (code) => {
|
|
299
|
+
clearTimeout(timeout);
|
|
300
|
+
if (!resolved) {
|
|
301
|
+
if (code === 0) {
|
|
302
|
+
done(true);
|
|
303
|
+
} else {
|
|
304
|
+
saveLastError(projectPath, output.slice(-3000));
|
|
305
|
+
done(false);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
} catch (e) {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
done(false);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
87
317
|
// ── npm install with progressive fallback ─────────────────────────────────
|
|
88
318
|
async function installDeps(projectPath) {
|
|
89
319
|
const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -150,7 +380,7 @@ async function tryStartExpo(projectPath, port) {
|
|
|
150
380
|
done(false, `Expo did not become ready within 60s.\n${output.slice(-800)}`), 60000);
|
|
151
381
|
|
|
152
382
|
try {
|
|
153
|
-
//
|
|
383
|
+
// iOS mode: use dev build (--go is intentionally omitted so the installed dev build opens)
|
|
154
384
|
const useIOS = process.env.CLAUDEBOARD_IOS === "1" || isSimulatorAvailableSync();
|
|
155
385
|
const expoArgs = useIOS
|
|
156
386
|
? ["expo", "start", "--ios", "--port", String(port)]
|
package/agents/orchestrator.js
CHANGED
|
@@ -5,7 +5,7 @@ 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";
|
|
8
|
+
import { runExpoHealthCheck, ensureDevBuild, packageJsonHash } from "./expo-health.js";
|
|
9
9
|
import { createConnection } from "net";
|
|
10
10
|
|
|
11
11
|
function isPortOpen(port) {
|
|
@@ -112,6 +112,9 @@ export async function runOrchestrator(config) {
|
|
|
112
112
|
let consecutiveFailures = 0;
|
|
113
113
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
114
114
|
|
|
115
|
+
// Track package.json hash to detect native dependency changes that need a rebuild
|
|
116
|
+
let lastPkgHash = packageJsonHash(projectPath);
|
|
117
|
+
|
|
115
118
|
while (true) {
|
|
116
119
|
const task = await getNextTask();
|
|
117
120
|
|
|
@@ -153,6 +156,29 @@ export async function runOrchestrator(config) {
|
|
|
153
156
|
expoProcess = recheck.process;
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// ── Detect native dependency changes → rebuild dev build if needed ────────
|
|
160
|
+
if (useIOS) {
|
|
161
|
+
const currentHash = packageJsonHash(projectPath);
|
|
162
|
+
if (currentHash && currentHash !== lastPkgHash) {
|
|
163
|
+
lastPkgHash = currentHash;
|
|
164
|
+
console.log(chalk.cyan(" 📦 package.json changed — checking if dev build rebuild is needed..."));
|
|
165
|
+
if (expoProcess) {
|
|
166
|
+
try { expoProcess.kill(); } catch {}
|
|
167
|
+
expoProcess = null;
|
|
168
|
+
expoReady = false;
|
|
169
|
+
}
|
|
170
|
+
const buildOk = await ensureDevBuild(projectPath);
|
|
171
|
+
if (buildOk) {
|
|
172
|
+
// Restart Metro after rebuild
|
|
173
|
+
const recheck = await runExpoHealthCheck(projectPath, expoPort);
|
|
174
|
+
expoReady = recheck.ready;
|
|
175
|
+
expoProcess = recheck.process;
|
|
176
|
+
} else {
|
|
177
|
+
console.log(chalk.yellow(" ✗ Rebuild failed — continuing without live preview"));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
156
182
|
// ── Re-check Expo health if it's supposed to be running but isn't ────────
|
|
157
183
|
if (expoReady) {
|
|
158
184
|
const portOpen = await isPortOpen(expoPort);
|
package/dashboard/server.js
CHANGED
|
@@ -59,7 +59,11 @@ function broadcastExpoStatus() {
|
|
|
59
59
|
// ── SUPABASE REALTIME ─────────────────────────────────────────────────────────
|
|
60
60
|
supabase
|
|
61
61
|
.channel("cb_changes")
|
|
62
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "cb_tasks" }, (p) =>
|
|
62
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "cb_tasks" }, (p) => {
|
|
63
|
+
if (p.eventType === "UPDATE" && p.new) broadcast("task_update", p.new);
|
|
64
|
+
else if (p.eventType === "INSERT" && p.new) broadcast("task_added", p.new);
|
|
65
|
+
else if (p.eventType === "DELETE") broadcast("task_deleted", { id: p.old?.id });
|
|
66
|
+
})
|
|
63
67
|
.on("postgres_changes", { event: "*", schema: "public", table: "cb_logs" }, (p) => broadcast("log", p.new))
|
|
64
68
|
.subscribe();
|
|
65
69
|
|