claudeboard 2.2.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;
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * Developer Agent — powered by Claude Agent SDK
3
- *
4
- * Instead of calling the Claude API and manually writing files,
5
- * this agent spawns Claude Code directly in the project directory.
6
- * Claude Code can read the full codebase, run commands, install deps,
7
- * fix errors, and iterate — all autonomously.
3
+ * Replaces the old API-based developer with Claude Code running directly
4
+ * in the project directory full codebase access, real shell commands.
8
5
  *
9
6
  * Requires: npm install -g @anthropic-ai/claude-code
10
7
  */
@@ -13,43 +10,27 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
13
10
  import { startTask, completeTask, failTask, addLog } from "./board-client.js";
14
11
  import { execSync } from "child_process";
15
12
 
16
- // Resolve the global `claude` binary path at startup
13
+ // ── Resolve the global `claude` binary path at startup ────────────────────────
17
14
  function resolveClaudePath() {
18
- // 1. Explicit override via env var
19
15
  if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
20
-
21
- // 2. Ask the shell where `claude` lives (works after: npm install -g @anthropic-ai/claude-code)
22
- try {
23
- return execSync("which claude", { stdio: "pipe" }).toString().trim();
24
- } catch {}
25
-
26
- // 3. Common Homebrew / nvm / fnm paths
27
- const candidates = [
16
+ try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
17
+ for (const p of [
28
18
  "/opt/homebrew/bin/claude",
29
19
  "/usr/local/bin/claude",
30
20
  `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
31
21
  `${process.env.HOME}/.npm-global/bin/claude`,
32
- ];
33
- for (const p of candidates) {
22
+ ]) {
34
23
  try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
35
24
  }
36
-
37
- return null; // will surface as CLINotFoundError
25
+ return null;
38
26
  }
39
27
 
40
- const CLAUDE_PATH = resolveClaudePath();
41
-
42
- // Build a full PATH for the Claude Code subprocess
43
- // Exit code 127 = "command not found" inside the subprocess — node/npm not in PATH
44
- function buildEnv() {// Exit code 127 = "command not found" inside the subprocess — node/npm not in PATH
28
+ // ── Build a full PATH so node/npm/npx are found inside the subprocess ─────────
29
+ // Exit code 127 = "command not found" — happens when PATH is stripped for global pkgs
45
30
  function buildEnv() {
46
- // Get node/npm directory to ensure they're in PATH
47
31
  let nodeBinDir = "";
48
- try {
49
- nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim();
50
- } catch {}
32
+ try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
51
33
 
52
- // Merge: existing PATH + node bin dir + common locations
53
34
  const pathParts = [
54
35
  process.env.PATH || "",
55
36
  nodeBinDir,
@@ -63,57 +44,49 @@ function buildEnv() {
63
44
  ].filter(Boolean);
64
45
 
65
46
  const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
66
-
67
- return {
68
- ...process.env,
69
- PATH: fullPath,
70
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
71
- HOME: process.env.HOME,
72
- };
47
+ return { ...process.env, PATH: fullPath, HOME: process.env.HOME };
73
48
  }
74
49
 
75
- // Tools Claude Code can use — full developer access
50
+ const CLAUDE_PATH = resolveClaudePath();
51
+
52
+ // ── Tools Claude Code can use ─────────────────────────────────────────────────
76
53
  const DEVELOPER_TOOLS = [
77
- "Read", // Read any file in the project
78
- "Write", // Create new files
79
- "Edit", // Edit existing files (surgical, line-level)
80
- "MultiEdit", // Edit multiple files in one shot
81
- "Bash", // Run npm install, npx expo install, tsc, etc.
82
- "Glob", // Find files by pattern (e.g. "**/*.tsx")
83
- "Grep", // Search file contents
84
- "TodoWrite", // Let Claude track its own subtasks
85
- "TodoRead", // Read its own task list
54
+ "Read", "Write", "Edit", "MultiEdit",
55
+ "Bash", "Glob", "Grep",
56
+ "TodoWrite", "TodoRead",
86
57
  ];
87
58
 
88
59
  const DEV_RULES = `
89
- You are a senior React Native / Expo developer working as part of an autonomous engineering team.
60
+ You are a senior React Native / Expo developer in an autonomous engineering team.
90
61
 
91
62
  RULES:
92
63
  - Write complete, production-ready code — no placeholders, no TODOs
93
64
  - Use TypeScript if the project uses it
94
- - Follow the existing code style and folder structure (read existing files first)
65
+ - Read existing files first to follow the project's patterns
95
66
  - Install packages with: npx expo install <package>
96
- - After writing files, run: npx tsc --noEmit — fix any TypeScript errors
97
- - If you hit an error, read it and fix it — iterate until it works
98
- - Do NOT ask questions. Do NOT ask for confirmation. Make your best judgment.
67
+ - After writing files run: npx tsc --noEmit — fix any errors you find
68
+ - If you hit an error, read it carefully and fix it — iterate until it works
69
+ - Do NOT ask questions or ask for confirmation. Make your best judgment.
99
70
  - When fully done, print EXACTLY this line: TASK_COMPLETE: <one sentence summary>
100
71
  `;
101
72
 
73
+ // ── Main export ───────────────────────────────────────────────────────────────
102
74
  export async function runDeveloperAgent(task, projectPath, techStack, retryContext = null) {
103
75
  console.log(` 🤖 Claude Code working on: ${task.title}`);
104
- if (CLAUDE_PATH) {
105
- console.log(` CLI: ${CLAUDE_PATH}`);
106
- } else {
76
+
77
+ if (!CLAUDE_PATH) {
107
78
  const hint = "Claude Code CLI not found. Run: npm install -g @anthropic-ai/claude-code";
108
79
  await startTask(task.id, hint);
109
80
  await failTask(task.id, hint);
110
81
  console.error(`\n ✗ ${hint}\n`);
111
82
  return { success: false, error: hint };
112
83
  }
84
+
85
+ console.log(` CLI: ${CLAUDE_PATH}`);
113
86
  await startTask(task.id, `Claude Code starting: ${task.title}`);
114
87
 
115
88
  const retryNote = retryContext
116
- ? `\n\n⚠️ RETRY — Previous attempt failed or QA flagged issues:\n${retryContext}\n\nFix these specific issues.`
89
+ ? `\n\n⚠️ RETRY — Previous attempt failed. Fix these issues:\n${retryContext}`
117
90
  : "";
118
91
 
119
92
  const techNote = techStack && Object.keys(techStack).length > 0
@@ -149,7 +122,7 @@ ${retryNote}
149
122
  permissionMode: "bypassPermissions",
150
123
  allowedTools: DEVELOPER_TOOLS,
151
124
  maxTurns: 80,
152
- ...(CLAUDE_PATH ? { pathToClaudeCodeExecutable: CLAUDE_PATH } : {}),
125
+ pathToClaudeCodeExecutable: CLAUDE_PATH,
153
126
  systemPrompt: {
154
127
  type: "preset",
155
128
  preset: "claude_code",
@@ -159,27 +132,21 @@ ${retryNote}
159
132
  },
160
133
  })) {
161
134
 
162
- // ── Assistant messages ─────────────────────────────────────────────────
135
+ // Assistant messages — log tools and text
163
136
  if (message.type === "assistant") {
164
137
  for (const block of message.message.content) {
165
-
166
138
  if (block.type === "text" && block.text?.trim()) {
167
139
  const text = block.text.trim();
168
-
169
- // Detect completion signal
170
140
  if (text.includes("TASK_COMPLETE:")) {
171
141
  completed = true;
172
142
  summary = text.split("TASK_COMPLETE:")[1]?.trim().split("\n")[0] || task.title;
173
143
  }
174
-
175
- // Log meaningful progress
176
144
  if (text.length > 20) {
177
145
  const preview = text.split("\n")[0].slice(0, 120);
178
146
  await addLog(task.id, preview, "progress");
179
147
  console.log(` ${preview}`);
180
148
  }
181
149
  }
182
-
183
150
  if (block.type === "tool_use") {
184
151
  toolCallCount++;
185
152
  const log = formatToolLog(block);
@@ -191,14 +158,13 @@ ${retryNote}
191
158
  }
192
159
  }
193
160
 
194
- // ── Result ─────────────────────────────────────────────────────────────
161
+ // Result — task finished or errored
195
162
  if (message.type === "result") {
196
163
  if (message.subtype === "success") {
197
164
  if (!completed && message.result?.includes("TASK_COMPLETE:")) {
198
165
  completed = true;
199
166
  summary = message.result.split("TASK_COMPLETE:")[1]?.trim().split("\n")[0] || task.title;
200
167
  }
201
-
202
168
  const finalSummary = summary || `Implemented: ${task.title}`;
203
169
  await completeTask(task.id, `✓ ${finalSummary} (${toolCallCount} tool calls)`);
204
170
  console.log(` ✓ Done: ${task.title} — ${toolCallCount} tool calls`);
@@ -209,7 +175,7 @@ ${retryNote}
209
175
  await completeTask(task.id, `✓ ${summary} (hit turn limit)`);
210
176
  return { success: true, summary };
211
177
  }
212
- const err = "Reached max turns without completing increase maxTurns or break task into smaller pieces";
178
+ const err = "Reached max turns (80)consider breaking this task into smaller pieces";
213
179
  await failTask(task.id, err);
214
180
  return { success: false, error: err };
215
181
 
@@ -220,11 +186,11 @@ ${retryNote}
220
186
  }
221
187
  }
222
188
 
223
- // ── System init ────────────────────────────────────────────────────────
189
+ // System init — log session info
224
190
  if (message.type === "system" && message.subtype === "init") {
225
- const sessionShort = message.session_id?.slice(0, 8) || "?";
226
- console.log(` 📡 Session ${sessionShort} | ${message.tools?.length || 0} tools | ${message.model}`);
227
- await addLog(task.id, `Claude Code session ${sessionShort} in ${projectPath}`, "start");
191
+ const s = message.session_id?.slice(0, 8) || "?";
192
+ console.log(` 📡 Session ${s} | ${message.tools?.length || 0} tools | ${message.model}`);
193
+ await addLog(task.id, `Session ${s} started`, "start");
228
194
  }
229
195
  }
230
196
 
@@ -233,7 +199,6 @@ ${retryNote}
233
199
  await completeTask(task.id, `✓ ${summary}`);
234
200
  return { success: true, summary };
235
201
  }
236
-
237
202
  const err = "Session ended without result";
238
203
  await failTask(task.id, err);
239
204
  return { success: false, error: err };
@@ -241,15 +206,11 @@ ${retryNote}
241
206
  } catch (err) {
242
207
  const msg = err.message || String(err);
243
208
  console.error(` ✗ Error:`, msg.slice(0, 200));
244
-
245
- // Missing CLI — helpful message
246
- if (msg.includes("CLINotFoundError") || msg.includes("ENOENT") || msg.includes("claude-code")) {
209
+ if (msg.includes("CLINotFoundError") || msg.includes("ENOENT")) {
247
210
  const hint = "Claude Code CLI not installed. Run: npm install -g @anthropic-ai/claude-code";
248
211
  await failTask(task.id, hint);
249
- console.error(`\n ⚠️ ${hint}\n`);
250
212
  return { success: false, error: hint };
251
213
  }
252
-
253
214
  await failTask(task.id, msg.slice(0, 500));
254
215
  return { success: false, error: msg };
255
216
  }
@@ -257,15 +218,16 @@ ${retryNote}
257
218
 
258
219
  function formatToolLog(block) {
259
220
  const { name, input = {} } = block;
221
+ const p = input.file_path || input.path || "";
260
222
  switch (name) {
261
- case "Read": return `Read: ${input.file_path || input.path || ""}`;
262
- case "Write": return `Write: ${input.file_path || input.path || ""}`;
223
+ case "Read": return `Read: ${p}`;
224
+ case "Write": return `Write: ${p}`;
263
225
  case "Edit":
264
- case "MultiEdit": return `Edit: ${input.file_path || input.path || ""}`;
265
- case "Bash": return `Run: ${(input.command || "").slice(0, 80)}`;
266
- case "Glob": return `Glob: ${input.pattern || ""}`;
267
- case "Grep": return `Grep: "${(input.pattern || "").slice(0, 40)}"`;
268
- case "TodoWrite": return `Planning subtasks...`;
269
- default: return null;
226
+ case "MultiEdit": return `Edit: ${p}`;
227
+ case "Bash": return `Run: ${(input.command || "").slice(0, 80)}`;
228
+ case "Glob": return `Glob: ${input.pattern || ""}`;
229
+ case "Grep": return `Grep: "${(input.pattern || "").slice(0, 40)}"`;
230
+ case "TodoWrite": return `Planning...`;
231
+ default: return null;
270
232
  }
271
233
  }
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.2.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": {