claudeboard 2.3.0 → 2.4.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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  const MODEL = "claude-sonnet-4-20250514";
7
- const MAX_TOKENS = 16000; // Increasedlarge codegen responses need more 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;
package/agents/qa.js CHANGED
@@ -1,133 +1,103 @@
1
1
  import { callClaude, callClaudeJSON, callClaudeWithImage } from "./claude-api.js";
2
- import { addLog, createTask, startTask, completeTask, failTask } from "./board-client.js";
3
- import { readFile, listFiles, projectTree } from "../tools/filesystem.js";
4
- import { runCommand, waitForPort } from "../tools/terminal.js";
2
+ import { addLog } from "./board-client.js";
3
+ import { readFile, listFiles } from "../tools/filesystem.js";
4
+ import { runCommand } from "../tools/terminal.js";
5
5
  import { screenshotExpoWeb } from "../tools/screenshot.js";
6
- import { readRecentLogs, readErrorLogs } from "../tools/supabase-reader.js";
6
+ import { readErrorLogs } from "../tools/supabase-reader.js";
7
7
  import path from "path";
8
8
  import fs from "fs";
9
9
 
10
- const SYSTEM_QA = `You are a senior QA engineer for mobile apps built with React Native / Expo.
11
- Your job is to verify that implemented features work correctly and look good.
12
-
13
- You receive:
14
- - The original PRD description of a feature
15
- - Console logs from the app
16
- - A screenshot of the current state
17
- - TypeScript/build errors if any
18
-
19
- You must determine:
20
- 1. Does the feature work as described in the PRD?
21
- 2. Does the UI look polished and correct?
22
- 3. Are there any errors in the logs?
23
- 4. What bugs or issues need to be fixed?
24
-
25
- Be specific about what's wrong and what needs to change.`;
10
+ const SYSTEM_QA = `You are a senior QA engineer for React Native / Expo apps.
11
+ Verify that implemented features work correctly. Be specific about actual issues.
12
+ If Expo is not running, evaluate purely on code quality and completeness.`;
26
13
 
27
14
  export async function runQAAgent(task, devResult, projectPath, prdContent, expoPort = 8081) {
28
15
  console.log(` 🔍 QA checking: ${task.title}`);
29
- await addLog(task.id, "QA agent starting verification", "progress");
30
-
31
- const qaReport = {
32
- passed: false,
33
- issues: [],
34
- screenshotPath: null,
35
- fixInstructions: null,
36
- };
37
-
38
- // 1. Check for TypeScript errors
39
- const tsResult = await runCommand("npx tsc --noEmit 2>&1 | head -30", projectPath, 30000);
40
- if (tsResult.stdout.includes("error TS")) {
41
- qaReport.issues.push(`TypeScript errors found:\n${tsResult.stdout}`);
42
- await addLog(task.id, `QA: TypeScript errors detected`, "error");
16
+ await addLog(task.id, "QA starting", "progress");
17
+
18
+ // ── 1. TypeScript check ────────────────────────────────────────────────────
19
+ const tsResult = await runCommand("npx tsc --noEmit 2>&1", projectPath, 60000);
20
+ const tsErrors = tsResult.stdout.includes("error TS") ? tsResult.stdout : null;
21
+ if (tsErrors) await addLog(task.id, `TypeScript errors detected`, "error");
22
+
23
+ // ── 2. Check for truncated/incomplete files ────────────────────────────────
24
+ const truncationIssues = await checkForTruncatedFiles(task, projectPath);
25
+ if (truncationIssues.length > 0) {
26
+ const msg = `Incomplete files: ${truncationIssues.join(", ")}`;
27
+ await addLog(task.id, msg, "error");
28
+ return {
29
+ passed: false,
30
+ issues: truncationIssues,
31
+ fixInstructions: `These files are truncated/incomplete:\n${truncationIssues.join("\n")}\n\nRe-implement them completely from scratch.`,
32
+ screenshotPath: null,
33
+ };
43
34
  }
44
35
 
45
- // 2. Read console/app logs
46
- let appLogs = "";
47
- try {
48
- // Read from Expo CLI output (saved to temp file by runner)
49
- const logFile = path.join(projectPath, ".claudeboard-logs.txt");
50
- if (fs.existsSync(logFile)) {
51
- appLogs = fs.readFileSync(logFile, "utf8").slice(-3000);
36
+ // ── 3. Screenshot (only if Expo is running) ────────────────────────────────
37
+ let visualVerdict = null;
38
+ const expoRunning = await isPortOpen(expoPort);
39
+
40
+ if (expoRunning) {
41
+ await addLog(task.id, "Taking screenshot...", "progress");
42
+ const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
43
+ const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
44
+ if (screenshot.success && screenshot.base64) {
45
+ const visionResult = await callClaudeWithImage(
46
+ SYSTEM_QA,
47
+ `Task: ${task.title}\nExpected: ${task.description}\n\nDoes the UI look correct? Any visual bugs?`,
48
+ screenshot.base64
49
+ );
50
+ visualVerdict = visionResult.text;
51
+ await addLog(task.id, `Visual: ${visualVerdict}`, "progress");
52
+ } else {
53
+ await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
52
54
  }
53
- } catch {}
54
-
55
- // 3. Read Supabase error logs if project uses Supabase
56
- let supabaseLogs = [];
57
- try {
58
- supabaseLogs = await readErrorLogs("logs", 10);
59
- } catch {}
60
-
61
- // 4. Take screenshot
62
- await addLog(task.id, "Taking screenshot of Expo Web...", "progress");
63
- const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
64
- const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
65
-
66
- if (screenshot.success) {
67
- qaReport.screenshotPath = screenshot.imagePath;
68
- await addLog(task.id, `Screenshot captured: ${path.basename(screenshot.imagePath)}`, "progress");
69
55
  } else {
70
- await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
71
- }
72
-
73
- // 5. Visual QA via Claude Vision
74
- let visualVerdict = null;
75
- if (screenshot.success && screenshot.base64) {
76
- const visualPrompt = `
77
- This is a screenshot of a React Native / Expo app screen.
78
-
79
- Task that was just implemented: ${task.title}
80
- Expected behavior from PRD: ${task.description}
81
-
82
- Evaluate:
83
- 1. Does the screen look implemented? (not blank, not error screen)
84
- 2. Does the UI look polished? (proper layout, readable text, no obvious visual bugs)
85
- 3. Does it appear to match what was described in the task?
86
- 4. Any visual issues? (overlapping elements, cut off text, missing components)
87
-
88
- Be concise and specific.`;
89
-
90
- const visionResult = await callClaudeWithImage(SYSTEM_QA, visualPrompt, screenshot.base64);
91
- visualVerdict = visionResult.text;
92
- await addLog(task.id, `Visual QA: ${visualVerdict.slice(0, 200)}`, "progress");
56
+ await addLog(task.id, "Expo not running — code-only QA", "progress");
93
57
  }
94
58
 
95
- // 6. Functional QAread code and verify logic
96
- const relevantFiles = listFiles(projectPath, [".ts", ".tsx"])
97
- .filter((f) => {
98
- const rel = path.relative(projectPath, f).toLowerCase();
99
- return task.title.toLowerCase().split(" ").some((w) => w.length > 4 && rel.includes(w));
100
- })
101
- .slice(0, 5);
102
-
59
+ // ── 4. Read ALL relevant code no artificial limits ──────────────────────
60
+ // Claude Code already wrote these files — we read them fully for QA review
61
+ const relevantFiles = getRelevantFiles(task, projectPath);
103
62
  let codeContext = "";
104
63
  for (const f of relevantFiles) {
105
64
  const content = readFile(f);
106
65
  if (content) {
107
- codeContext += `\n### ${path.relative(projectPath, f)}\n${content.slice(0, 1500)}\n`;
66
+ // No slice read the complete file
67
+ codeContext += `\n### ${path.relative(projectPath, f)}\n${content}\n`;
108
68
  }
109
69
  }
110
70
 
111
- const functionalVerdict = await callClaudeJSON(
71
+ // ── 5. App logs (full) ─────────────────────────────────────────────────────
72
+ let appLogs = "";
73
+ try {
74
+ const logFile = path.join(projectPath, ".claudeboard-logs.txt");
75
+ if (fs.existsSync(logFile)) appLogs = fs.readFileSync(logFile, "utf8");
76
+ } catch {}
77
+
78
+ // ── 6. Supabase errors ─────────────────────────────────────────────────────
79
+ let supabaseLogs = [];
80
+ try { supabaseLogs = await readErrorLogs("logs", 20); } catch {}
81
+
82
+ // ── 7. Functional verdict ─────────────────────────────────────────────────
83
+ const verdict = await callClaudeJSON(
112
84
  SYSTEM_QA,
113
- `
114
- Task: ${task.title}
85
+ `Task: ${task.title}
115
86
  Description: ${task.description}
116
87
 
117
- Code implemented:
88
+ Implemented code (complete files):
118
89
  ${codeContext || "Could not read relevant files"}
119
90
 
120
- Console logs:
121
- ${appLogs || "No logs captured"}
122
-
123
- Supabase errors:
124
- ${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs) : "None"}
91
+ TypeScript: ${tsErrors ? `ERRORS:\n${tsErrors}` : "Clean — no errors"}
92
+ App logs: ${appLogs || "None"}
93
+ Supabase errors: ${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs, null, 2) : "None"}
94
+ Visual QA: ${visualVerdict || "No screenshot (Expo not running — evaluate code only)"}
125
95
 
126
- TypeScript status:
127
- ${tsResult.stdout.includes("error TS") ? tsResult.stdout : "No TypeScript errors"}
128
-
129
- Visual QA notes:
130
- ${visualVerdict || "No screenshot available"}
96
+ EVALUATION RULES:
97
+ - PASS if: code is complete, TypeScript is clean, implementation matches description
98
+ - FAIL only if: code is incomplete/truncated, has TS errors, or implementation is clearly wrong
99
+ - Do NOT fail just because Expo isn't running
100
+ - Do NOT fail for minor style preferences
131
101
 
132
102
  Respond with JSON:
133
103
  {
@@ -135,74 +105,125 @@ Respond with JSON:
135
105
  "confidence": 0-100,
136
106
  "issues": ["specific issue 1", "specific issue 2"],
137
107
  "summary": "One sentence verdict",
138
- "fixInstructions": "If failed: specific instructions for the developer to fix. If passed: null"
139
- }`,
140
- { maxTokens: 2000 }
108
+ "fixInstructions": "Specific fix instructions if failed, null if passed"
109
+ }`
141
110
  );
142
111
 
143
- qaReport.passed = functionalVerdict.passed;
144
- qaReport.issues = functionalVerdict.issues || [];
145
- qaReport.fixInstructions = functionalVerdict.fixInstructions;
112
+ const qaReport = {
113
+ passed: verdict.passed,
114
+ issues: verdict.issues || [],
115
+ fixInstructions: verdict.fixInstructions,
116
+ screenshotPath: null,
117
+ };
146
118
 
147
119
  if (qaReport.passed) {
148
- await addLog(task.id, `✓ QA passed (${functionalVerdict.confidence}%): ${functionalVerdict.summary}`, "complete");
120
+ await addLog(task.id, `✓ QA passed (${verdict.confidence}%): ${verdict.summary}`, "complete");
149
121
  console.log(` ✓ QA passed: ${task.title}`);
150
122
  } else {
151
- await addLog(
152
- task.id,
153
- `✗ QA failed: ${functionalVerdict.summary}. Issues: ${qaReport.issues.slice(0, 2).join("; ")}`,
154
- "error"
155
- );
156
- console.log(` ✗ QA failed: ${task.title}`);
157
- console.log(` Issues: ${qaReport.issues.join(", ")}`);
123
+ await addLog(task.id, `✗ QA failed: ${verdict.summary}`, "error");
124
+ console.log(` ✗ QA failed: ${task.title} — ${verdict.issues?.slice(0,2).join(", ")}`);
158
125
  }
159
126
 
160
127
  return qaReport;
161
128
  }
162
129
 
163
- /**
164
- * Full app QA — run at end of development phase
165
- * Goes through every major screen and validates against PRD
166
- */
130
+ // ── Detect truncated/incomplete files ──────────────────────────────────────────
131
+ async function checkForTruncatedFiles(task, projectPath) {
132
+ const issues = [];
133
+ const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
134
+
135
+ const files = listFiles(projectPath, [".ts", ".tsx"])
136
+ .filter(f => {
137
+ const rel = path.relative(projectPath, f).toLowerCase();
138
+ if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
139
+ return keywords.some(kw => rel.includes(kw));
140
+ });
141
+
142
+ for (const f of files) {
143
+ const content = readFile(f);
144
+ if (!content) continue;
145
+ const rel = path.relative(projectPath, f);
146
+ const lastLines = content.split("\n").slice(-5).join("\n").trim();
147
+ const openBraces = (content.match(/\{/g) || []).length;
148
+ const closeBraces = (content.match(/\}/g) || []).length;
149
+
150
+ const truncated =
151
+ lastLines.endsWith("{") || lastLines.endsWith("(") || lastLines.endsWith(",") ||
152
+ lastLines.match(/^(import|\/\/) *$/) ||
153
+ openBraces > closeBraces + 3 ||
154
+ (content.length < 80 && (rel.includes("store") || rel.includes("hook") || rel.includes("screen")));
155
+
156
+ if (truncated) {
157
+ issues.push(`${rel} (ends: "${lastLines.slice(-80)}")`);
158
+ }
159
+ }
160
+ return issues;
161
+ }
162
+
163
+ // ── Full app QA ────────────────────────────────────────────────────────────────
167
164
  export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
168
165
  console.log("\n 🔍 Running full app QA...");
169
166
 
170
- // Detect routes from expo-router structure
171
- const routeFiles = listFiles(path.join(projectPath, "app"), [".tsx", ".ts"])
172
- .filter((f) => !f.includes("_layout") && !f.includes("_error"))
173
- .map((f) => {
174
- const rel = path.relative(path.join(projectPath, "app"), f);
175
- return "/" + rel.replace(/\.(tsx|ts)$/, "").replace(/index$/, "");
176
- });
167
+ if (!await isPortOpen(expoPort)) {
168
+ console.log(" ⚠️ Expo not running skipping visual QA");
169
+ return { passed: true, routes: [], screenshotsCaptures: 0, issues: [] };
170
+ }
171
+
172
+ const appDir = path.join(projectPath, "app");
173
+ if (!fs.existsSync(appDir)) return { passed: true, routes: [], screenshotsCaptures: 0, issues: [] };
174
+
175
+ const routeFiles = listFiles(appDir, [".tsx", ".ts"])
176
+ .filter(f => !f.includes("_layout") && !f.includes("_error"))
177
+ .map(f => "/" + path.relative(appDir, f).replace(/\.(tsx|ts)$/, "").replace(/index$/, ""));
177
178
 
178
179
  const screenshotDir = path.join(projectPath, ".claudeboard-screenshots", "full-qa");
179
180
  const screenshots = [];
180
181
 
181
- for (const route of routeFiles.slice(0, 10)) {
182
+ for (const route of routeFiles) {
182
183
  const shot = await screenshotExpoWeb(expoPort, screenshotDir, route);
183
184
  if (shot.success) screenshots.push({ route, ...shot });
184
- await new Promise((r) => setTimeout(r, 1000));
185
+ await new Promise(r => setTimeout(r, 800));
185
186
  }
186
187
 
187
- // Send all screenshots + PRD to Claude for final verdict
188
- const report = {
189
- passed: screenshots.length > 0,
190
- routes: routeFiles,
191
- screenshotsCaptures: screenshots.length,
192
- issues: [],
193
- };
188
+ const report = { passed: true, routes: routeFiles, screenshotsCaptures: screenshots.length, issues: [] };
194
189
 
195
190
  for (const shot of screenshots) {
196
191
  if (!shot.base64) continue;
197
192
  const verdict = await callClaudeWithImage(
198
193
  SYSTEM_QA,
199
- `Route: ${shot.route}\n\nPRD for reference:\n${prdContent.slice(0, 2000)}\n\nDoes this screen look complete and working? Any issues?`,
194
+ `Route: ${shot.route}\n\nFull PRD:\n${prdContent}\n\nAny obvious issues?`,
200
195
  shot.base64
201
196
  );
202
197
  if (verdict.text.toLowerCase().includes("issue") || verdict.text.toLowerCase().includes("problem")) {
203
- report.issues.push({ route: shot.route, note: verdict.text.slice(0, 200) });
198
+ report.issues.push({ route: shot.route, note: verdict.text });
204
199
  }
205
200
  }
206
201
 
207
202
  return report;
208
203
  }
204
+
205
+ // ── Helpers ───────────────────────────────────────────────────────────────────
206
+ function getRelevantFiles(task, projectPath) {
207
+ const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
208
+ return listFiles(projectPath, [".ts", ".tsx"])
209
+ .filter(f => {
210
+ const rel = path.relative(projectPath, f).toLowerCase();
211
+ if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
212
+ return keywords.some(kw => rel.includes(kw));
213
+ });
214
+ // No .slice() — return all matching files
215
+ }
216
+
217
+ async function isPortOpen(port) {
218
+ try {
219
+ const { default: net } = await import("net");
220
+ return new Promise(resolve => {
221
+ const sock = new net.Socket();
222
+ sock.setTimeout(800);
223
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
224
+ sock.once("error", () => resolve(false));
225
+ sock.once("timeout", () => resolve(false));
226
+ sock.connect(port, "127.0.0.1");
227
+ });
228
+ } catch { return false; }
229
+ }
package/bin/cli.js CHANGED
@@ -166,11 +166,21 @@ program
166
166
  process.exit(1);
167
167
  }
168
168
 
169
+ const resolvedProject = path.resolve(opts.project);
170
+
171
+ // Persist the project path so `claudeboard start` uses the right directory for Expo
172
+ const configPath = path.join(process.cwd(), ".claudeboard.json");
173
+ try {
174
+ const saved = JSON.parse(fs.readFileSync(configPath, "utf8"));
175
+ saved.projectDir = resolvedProject;
176
+ fs.writeFileSync(configPath, JSON.stringify(saved, null, 2));
177
+ } catch {}
178
+
169
179
  const { runOrchestrator } = await import("../agents/orchestrator.js");
170
180
 
171
181
  await runOrchestrator({
172
182
  prdPath: path.resolve(opts.prd),
173
- projectPath: path.resolve(opts.project),
183
+ projectPath: resolvedProject,
174
184
  supabaseUrl: config.supabaseUrl,
175
185
  supabaseKey: config.supabaseKey,
176
186
  projectName: config.projectName,
@@ -929,7 +929,14 @@
929
929
  <button class="stab" id="tab-detail" onclick="switchTab('detail')">Detail</button>
930
930
  </div>
931
931
  <div class="sidebar-body" id="sidebarBody">
932
- <div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
932
+ <!-- Activity pane always in DOM, shown/hidden -->
933
+ <div id="activityPane">
934
+ <div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
935
+ </div>
936
+ <!-- Detail pane — always in DOM, shown/hidden -->
937
+ <div id="detailPane" style="display:none">
938
+ <div class="detail-empty">Click any task card<br>to see its details.</div>
939
+ </div>
933
940
  </div>
934
941
  </div>
935
942
 
@@ -1102,6 +1109,7 @@ function connectWS() {
1102
1109
  if (event === 'log') {
1103
1110
  board.logs.unshift(data);
1104
1111
  if (activeTab === 'activity') renderLogs();
1112
+ // If on detail tab, don't touch activityPane — it'll refresh next time they switch
1105
1113
  }
1106
1114
  if (event === 'expo_status') {
1107
1115
  setExpoStatus(data.status, data.url);
@@ -1265,10 +1273,6 @@ async function onDrop(e, status) {
1265
1273
 
1266
1274
  // ── CARD SELECT ───────────────────────────────────────────────────────────────
1267
1275
  async function selectCard(id) {
1268
- activeTab = 'detail';
1269
- document.getElementById('tab-activity').className = 'stab';
1270
- document.getElementById('tab-detail').className = 'stab active';
1271
-
1272
1276
  const task = allTasks().find(t => t.id === id);
1273
1277
  if (!task) return;
1274
1278
  selectedTask = task;
@@ -1276,8 +1280,8 @@ async function selectCard(id) {
1276
1280
  const res = await fetch(`/api/tasks/${id}/logs`);
1277
1281
  const { logs } = await res.json();
1278
1282
 
1279
- const el = document.getElementById('sidebarBody');
1280
- el.innerHTML = `
1283
+ // Write into the dedicated detail pane (never touches activityPane)
1284
+ document.getElementById('detailPane').innerHTML = `
1281
1285
  <div>
1282
1286
  <div class="detail-title">${esc(task.title)}</div>
1283
1287
  <div class="detail-tags">
@@ -1295,16 +1299,23 @@ async function selectCard(id) {
1295
1299
  }).join('')
1296
1300
  }
1297
1301
  </div>`;
1302
+
1303
+ // Switch to detail tab
1304
+ activeTab = 'detail';
1305
+ document.getElementById('tab-activity').className = 'stab';
1306
+ document.getElementById('tab-detail').className = 'stab active';
1307
+ document.getElementById('activityPane').style.display = 'none';
1308
+ document.getElementById('detailPane').style.display = 'block';
1298
1309
  }
1299
1310
 
1300
1311
  // ── LOGS ──────────────────────────────────────────────────────────────────────
1301
1312
  function renderLogs() {
1302
- const el = document.getElementById('sidebarBody');
1313
+ // Only update the activity pane — never overwrites detail pane
1314
+ const el = document.getElementById('activityPane');
1303
1315
  if (!board.logs?.length) {
1304
1316
  el.innerHTML = `<div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>`;
1305
1317
  return;
1306
1318
  }
1307
-
1308
1319
  const icons = { start: '▶', complete: '✓', error: '✕', progress: '·', info: '·' };
1309
1320
  el.innerHTML = board.logs.map(l => {
1310
1321
  const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
@@ -1325,6 +1336,9 @@ function switchTab(tab) {
1325
1336
  activeTab = tab;
1326
1337
  document.getElementById('tab-activity').className = 'stab' + (tab === 'activity' ? ' active' : '');
1327
1338
  document.getElementById('tab-detail').className = 'stab' + (tab === 'detail' ? ' active' : '');
1339
+ // Show/hide panes — never overwrite the other pane's content
1340
+ document.getElementById('activityPane').style.display = tab === 'activity' ? 'block' : 'none';
1341
+ document.getElementById('detailPane').style.display = tab === 'detail' ? 'block' : 'none';
1328
1342
  if (tab === 'activity') renderLogs();
1329
1343
  }
1330
1344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {