claudeboard 2.8.0 → 2.9.1

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.
@@ -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 priorityOrder = { high: 1, medium: 2, low: 3 };
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,9 +76,9 @@ export async function createTask({ epicId, title, description, priority = "mediu
68
76
  title,
69
77
  description,
70
78
  priority,
71
- priority_order: priorityOrder[priority] || 2,
79
+ priority_order: priorityOrder ?? defaultOrder[priority] ?? 2,
72
80
  type,
73
- status: "todo",
81
+ status,
74
82
  })
75
83
  .select()
76
84
  .single();
@@ -1,82 +1,135 @@
1
- import { callClaude } from "./claude-api.js";
1
+ import { callClaudeJSON } from "./claude-api.js";
2
2
  import { runCommand } from "../tools/terminal.js";
3
- import { startProcess, waitForPort } from "../tools/terminal.js";
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
- * 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 }
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 found — skipping Expo health check"));
30
+ console.log(chalk.dim(" No package.json — skipping"));
26
31
  return { ready: false, process: null };
27
32
  }
28
33
 
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
- }
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
- // ── Step 2: Attempt Expo start with error detection ───────────────────────
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 failed to start:\n ${error?.slice(0, 200)}`));
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
- // ── 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);
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
- if (!fixed) {
69
- console.log(chalk.red(" Could not determine fix — skipping Expo"));
70
- break;
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
- console.log(chalk.yellow(" ⚠ Expo not available continuing without visual QA\n"));
76
- return { ready: false, process: null };
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
- // ── Try to start Expo and detect if it crashes or succeeds ─────────────────
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
- try { proc.kill(); } catch {}
91
- proc = null;
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
- // 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);
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
- // Success signals
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("Web is waiting") ||
120
- text.includes("Bundling complete")
179
+ text.includes(`http://localhost:${port}`) ||
180
+ text.includes("Bundling complete") ||
181
+ text.includes("Web is waiting on")
121
182
  ) {
122
- clearTimeout(timeout);
123
- done(true, null);
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
- // 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
- }
194
+ checkError(text);
142
195
  });
143
196
 
144
197
  proc.on("close", (code) => {
145
198
  clearTimeout(timeout);
146
- if (code !== 0) done(false, `Process exited with code ${code}.\n${output.slice(-1000)}`);
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 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);
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
- const prompt = `You are a React Native / Expo dependency expert.
163
-
164
- The app failed to start with this error:
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 package.json dependencies:
170
- \`\`\`json
223
+ Current dependencies:
171
224
  ${JSON.stringify(deps, null, 2)}
172
- \`\`\`
173
-
174
- Project path: ${projectPath}
175
225
 
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
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": "One sentence explanation of the root cause",
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 < 40) {
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(` → ${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
- }
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
- // ── 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;
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
- // ── 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;
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
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {