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/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,