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.
- package/agents/claude-api.js +1 -1
- package/agents/developer.js +46 -84
- package/agents/qa.js +160 -139
- package/bin/cli.js +11 -1
- package/dashboard/index.html +23 -9
- package/package.json +1 -1
package/agents/claude-api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const MODEL = "claude-sonnet-4-20250514";
|
|
7
|
-
const MAX_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/developer.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Developer Agent — powered by Claude Agent SDK
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
const CLAUDE_PATH = resolveClaudePath();
|
|
51
|
+
|
|
52
|
+
// ── Tools Claude Code can use ─────────────────────────────────────────────────
|
|
76
53
|
const DEVELOPER_TOOLS = [
|
|
77
|
-
"Read",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
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
|
|
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
|
-
-
|
|
65
|
+
- Read existing files first to follow the project's patterns
|
|
95
66
|
- Install packages with: npx expo install <package>
|
|
96
|
-
- After writing files
|
|
97
|
-
- If you hit an error, read it and fix it — iterate until it works
|
|
98
|
-
- Do NOT ask questions
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
189
|
+
// System init — log session info
|
|
224
190
|
if (message.type === "system" && message.subtype === "init") {
|
|
225
|
-
const
|
|
226
|
-
console.log(` 📡 Session ${
|
|
227
|
-
await addLog(task.id, `
|
|
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":
|
|
262
|
-
case "Write":
|
|
223
|
+
case "Read": return `Read: ${p}`;
|
|
224
|
+
case "Write": return `Write: ${p}`;
|
|
263
225
|
case "Edit":
|
|
264
|
-
case "MultiEdit":
|
|
265
|
-
case "Bash":
|
|
266
|
-
case "Glob":
|
|
267
|
-
case "Grep":
|
|
268
|
-
case "TodoWrite":
|
|
269
|
-
default:
|
|
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
|
|
3
|
-
import { readFile, listFiles
|
|
4
|
-
import { runCommand
|
|
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 {
|
|
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
|
|
11
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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,
|
|
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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
66
|
+
// No slice — read the complete file
|
|
67
|
+
codeContext += `\n### ${path.relative(projectPath, f)}\n${content}\n`;
|
|
108
68
|
}
|
|
109
69
|
}
|
|
110
70
|
|
|
111
|
-
|
|
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
|
-
|
|
88
|
+
Implemented code (complete files):
|
|
118
89
|
${codeContext || "Could not read relevant files"}
|
|
119
90
|
|
|
120
|
-
|
|
121
|
-
${appLogs || "
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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": "
|
|
139
|
-
}
|
|
140
|
-
{ maxTokens: 2000 }
|
|
108
|
+
"fixInstructions": "Specific fix instructions if failed, null if passed"
|
|
109
|
+
}`
|
|
141
110
|
);
|
|
142
111
|
|
|
143
|
-
qaReport
|
|
144
|
-
|
|
145
|
-
|
|
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 (${
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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(
|
|
185
|
+
await new Promise(r => setTimeout(r, 800));
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
|
|
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\
|
|
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
|
|
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:
|
|
183
|
+
projectPath: resolvedProject,
|
|
174
184
|
supabaseUrl: config.supabaseUrl,
|
|
175
185
|
supabaseKey: config.supabaseKey,
|
|
176
186
|
projectName: config.projectName,
|
package/dashboard/index.html
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
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
|
|