claudeboard 1.5.0 → 2.1.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.
@@ -1,161 +1,240 @@
1
- import { callClaudeJSON, callClaude } from "./claude-api.js";
2
- import { startTask, completeTask, failTask, addLog } from "./board-client.js";
3
- import { readFile, writeFile, listFiles, projectTree, readFilesAsContext } from "../tools/filesystem.js";
4
- import { runCommand } from "../tools/terminal.js";
5
- import path from "path";
1
+ /**
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.
8
+ *
9
+ * Requires: npm install -g @anthropic-ai/claude-code
10
+ */
6
11
 
7
- const SYSTEM = `You are a senior React Native / Expo developer. You write clean, production-ready code.
12
+ import { query } from "@anthropic-ai/claude-agent-sdk";
13
+ import { startTask, completeTask, failTask, addLog } from "./board-client.js";
14
+ import { execSync } from "child_process";
15
+
16
+ // Resolve the global `claude` binary path at startup
17
+ function resolveClaudePath() {
18
+ // 1. Explicit override via env var
19
+ 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 = [
28
+ "/opt/homebrew/bin/claude",
29
+ "/usr/local/bin/claude",
30
+ `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
31
+ `${process.env.HOME}/.npm-global/bin/claude`,
32
+ ];
33
+ for (const p of candidates) {
34
+ try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
35
+ }
8
36
 
9
- Rules:
10
- - Write complete, working code — never use placeholders or TODOs
11
- - Always use TypeScript when the project uses it
12
- - Follow the existing code style and patterns in the project
13
- - Add console.log statements for key operations so QA can verify functionality
14
- - Add error handling with meaningful error messages
15
- - When creating screens, make them visually polished — proper spacing, colors, typography
16
- - Use the existing navigation structure — never change routing patterns mid-project
17
- - Import only packages that are already installed or that you explicitly install first
37
+ return null; // will surface as CLINotFoundError
38
+ }
18
39
 
19
- When you need to create or modify files, respond with a JSON array of file operations.`;
40
+ const CLAUDE_PATH = resolveClaudePath();
41
+
42
+ // Tools Claude Code can use — full developer access
43
+ const DEVELOPER_TOOLS = [
44
+ "Read", // Read any file in the project
45
+ "Write", // Create new files
46
+ "Edit", // Edit existing files (surgical, line-level)
47
+ "MultiEdit", // Edit multiple files in one shot
48
+ "Bash", // Run npm install, npx expo install, tsc, etc.
49
+ "Glob", // Find files by pattern (e.g. "**/*.tsx")
50
+ "Grep", // Search file contents
51
+ "TodoWrite", // Let Claude track its own subtasks
52
+ "TodoRead", // Read its own task list
53
+ ];
54
+
55
+ const DEV_RULES = `
56
+ You are a senior React Native / Expo developer working as part of an autonomous engineering team.
57
+
58
+ RULES:
59
+ - Write complete, production-ready code — no placeholders, no TODOs
60
+ - Use TypeScript if the project uses it
61
+ - Follow the existing code style and folder structure (read existing files first)
62
+ - Install packages with: npx expo install <package>
63
+ - After writing files, run: npx tsc --noEmit — fix any TypeScript errors
64
+ - If you hit an error, read it and fix it — iterate until it works
65
+ - Do NOT ask questions. Do NOT ask for confirmation. Make your best judgment.
66
+ - When fully done, print EXACTLY this line: TASK_COMPLETE: <one sentence summary>
67
+ `;
20
68
 
21
69
  export async function runDeveloperAgent(task, projectPath, techStack, retryContext = null) {
22
- console.log(` 💻 Developer working on: ${task.title}`);
23
- await startTask(task.id, `Starting implementation: ${task.title}`);
24
-
25
- const MAX_RETRIES = 3;
26
- let attempt = 0;
27
- let lastError = null;
28
-
29
- while (attempt < MAX_RETRIES) {
30
- attempt++;
31
- if (attempt > 1) {
32
- console.log(` 🔄 Retry ${attempt}/${MAX_RETRIES} for: ${task.title}`);
33
- await addLog(task.id, `Retry ${attempt}: fixing — ${lastError}`, "progress");
34
- }
35
-
36
- try {
37
- // Gather context
38
- const tree = projectTree(projectPath);
39
- const relevantFiles = getRelevantFiles(task, projectPath);
40
- const fileContext = readFilesAsContext(relevantFiles, projectPath);
41
-
42
- const retryNote = retryContext
43
- ? `\n\nPREVIOUS ATTEMPT FAILED:\n${retryContext}\n\nFix these issues.`
44
- : "";
45
-
46
- const lastErrorNote = lastError
47
- ? `\n\nLAST ERROR: ${lastError}\nFix this specifically.`
48
- : "";
49
-
50
- const prompt = `
51
- Task: ${task.title}
52
- Description: ${task.description}
53
-
54
- Project structure:
55
- ${tree}
56
-
57
- Relevant existing files:
58
- ${fileContext || "No existing files yet — this may be a fresh setup task."}
59
-
60
- Tech stack: ${JSON.stringify(techStack || {}, null, 2)}
61
- ${retryNote}${lastErrorNote}
62
-
63
- Respond with JSON:
64
- {
65
- "plan": "Brief explanation of your approach",
66
- "installPackages": ["pkg1", "pkg2"],
67
- "files": [
68
- {
69
- "path": "relative/path/from/project/root.tsx",
70
- "action": "create|modify|delete",
71
- "content": "complete file content here"
72
- }
73
- ],
74
- "commands": ["any post-file commands to run, e.g. npx expo install package"],
75
- "verificationSteps": "What to check to verify this works"
76
- }`;
77
-
78
- const result = await callClaudeJSON(SYSTEM, prompt);
79
-
80
- // Install packages first
81
- if (result.installPackages?.length > 0) {
82
- const pkgs = result.installPackages.join(" ");
83
- await addLog(task.id, `Installing: ${pkgs}`, "progress");
84
- const install = await runCommand(`npx expo install ${pkgs}`, projectPath, 120000);
85
- if (install.exitCode !== 0) {
86
- // Try npm install as fallback
87
- await runCommand(`npm install ${pkgs}`, projectPath, 120000);
70
+ console.log(` 🤖 Claude Code working on: ${task.title}`);
71
+ if (CLAUDE_PATH) {
72
+ console.log(` CLI: ${CLAUDE_PATH}`);
73
+ } else {
74
+ const hint = "Claude Code CLI not found. Run: npm install -g @anthropic-ai/claude-code";
75
+ await startTask(task.id, hint);
76
+ await failTask(task.id, hint);
77
+ console.error(`\n ${hint}\n`);
78
+ return { success: false, error: hint };
79
+ }
80
+ await startTask(task.id, `Claude Code starting: ${task.title}`);
81
+
82
+ const retryNote = retryContext
83
+ ? `\n\n⚠️ RETRY — Previous attempt failed or QA flagged issues:\n${retryContext}\n\nFix these specific issues.`
84
+ : "";
85
+
86
+ const techNote = techStack && Object.keys(techStack).length > 0
87
+ ? `\nKnown tech stack: ${JSON.stringify(techStack)}`
88
+ : "";
89
+
90
+ const prompt = `## Task to implement
91
+
92
+ **Title:** ${task.title}
93
+
94
+ **Description:**
95
+ ${task.description || "No additional description provided."}
96
+ ${techNote}
97
+ ${retryNote}
98
+
99
+ ## Steps
100
+ 1. Explore the project structure to understand existing patterns
101
+ 2. Implement the task completely
102
+ 3. Install any missing dependencies with: npx expo install <pkg>
103
+ 4. Run: npx tsc --noEmit and fix any errors
104
+ 5. When done, print: TASK_COMPLETE: <summary>`;
105
+
106
+ try {
107
+ let toolCallCount = 0;
108
+ let completed = false;
109
+ let summary = "";
110
+
111
+ for await (const message of query({
112
+ prompt,
113
+ options: {
114
+ cwd: projectPath,
115
+ model: "claude-sonnet-4-20250514",
116
+ permissionMode: "bypassPermissions",
117
+ allowedTools: DEVELOPER_TOOLS,
118
+ maxTurns: 80,
119
+ ...(CLAUDE_PATH ? { pathToClaudeCodeExecutable: CLAUDE_PATH } : {}),
120
+ systemPrompt: {
121
+ type: "preset",
122
+ preset: "claude_code",
123
+ append: DEV_RULES,
124
+ },
125
+ env: {
126
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
127
+ },
128
+ },
129
+ })) {
130
+
131
+ // ── Assistant messages ─────────────────────────────────────────────────
132
+ if (message.type === "assistant") {
133
+ for (const block of message.message.content) {
134
+
135
+ if (block.type === "text" && block.text?.trim()) {
136
+ const text = block.text.trim();
137
+
138
+ // Detect completion signal
139
+ if (text.includes("TASK_COMPLETE:")) {
140
+ completed = true;
141
+ summary = text.split("TASK_COMPLETE:")[1]?.trim().split("\n")[0] || task.title;
142
+ }
143
+
144
+ // Log meaningful progress
145
+ if (text.length > 20) {
146
+ const preview = text.split("\n")[0].slice(0, 120);
147
+ await addLog(task.id, preview, "progress");
148
+ console.log(` ${preview}`);
149
+ }
150
+ }
151
+
152
+ if (block.type === "tool_use") {
153
+ toolCallCount++;
154
+ const log = formatToolLog(block);
155
+ if (log) {
156
+ await addLog(task.id, log, "progress");
157
+ console.log(` 🔧 ${log}`);
158
+ }
159
+ }
88
160
  }
89
161
  }
90
162
 
91
- // Write files
92
- for (const file of result.files || []) {
93
- const fullPath = path.join(projectPath, file.path);
94
- if (file.action === "delete") {
95
- try { fs.unlinkSync(fullPath); } catch {}
163
+ // ── Result ─────────────────────────────────────────────────────────────
164
+ if (message.type === "result") {
165
+ if (message.subtype === "success") {
166
+ if (!completed && message.result?.includes("TASK_COMPLETE:")) {
167
+ completed = true;
168
+ summary = message.result.split("TASK_COMPLETE:")[1]?.trim().split("\n")[0] || task.title;
169
+ }
170
+
171
+ const finalSummary = summary || `Implemented: ${task.title}`;
172
+ await completeTask(task.id, `✓ ${finalSummary} (${toolCallCount} tool calls)`);
173
+ console.log(` ✓ Done: ${task.title} — ${toolCallCount} tool calls`);
174
+ return { success: true, summary: finalSummary };
175
+
176
+ } else if (message.subtype === "error_max_turns") {
177
+ if (completed) {
178
+ await completeTask(task.id, `✓ ${summary} (hit turn limit)`);
179
+ return { success: true, summary };
180
+ }
181
+ const err = "Reached max turns without completing — increase maxTurns or break task into smaller pieces";
182
+ await failTask(task.id, err);
183
+ return { success: false, error: err };
184
+
96
185
  } else {
97
- writeFile(fullPath, file.content);
98
- await addLog(task.id, `${file.action}: ${file.path}`, "progress");
186
+ const err = message.result || "Claude Code returned an error";
187
+ await failTask(task.id, err.slice(0, 500));
188
+ return { success: false, error: err };
99
189
  }
100
190
  }
101
191
 
102
- // Run any additional commands
103
- for (const cmd of result.commands || []) {
104
- await addLog(task.id, `Running: ${cmd}`, "progress");
105
- await runCommand(cmd, projectPath, 60000);
106
- }
107
-
108
- // Verify it compiles
109
- const typecheck = await runCommand("npx tsc --noEmit 2>&1 | head -20", projectPath, 30000);
110
- const hasTSErrors = typecheck.stdout.includes("error TS");
111
-
112
- if (hasTSErrors) {
113
- lastError = `TypeScript errors:\n${typecheck.stdout}`;
114
- continue; // retry
192
+ // ── System init ────────────────────────────────────────────────────────
193
+ if (message.type === "system" && message.subtype === "init") {
194
+ const sessionShort = message.session_id?.slice(0, 8) || "?";
195
+ console.log(` 📡 Session ${sessionShort} | ${message.tools?.length || 0} tools | ${message.model}`);
196
+ await addLog(task.id, `Claude Code session ${sessionShort} in ${projectPath}`, "start");
115
197
  }
198
+ }
116
199
 
117
- // Check for obvious JS syntax errors
118
- const buildCheck = await runCommand(
119
- "npx expo export --platform web --output-dir /tmp/cb-check 2>&1 | tail -10",
120
- projectPath,
121
- 90000
122
- );
200
+ // Generator ended without a result message
201
+ if (completed) {
202
+ await completeTask(task.id, `✓ ${summary}`);
203
+ return { success: true, summary };
204
+ }
123
205
 
124
- if (buildCheck.stdout.includes("ERROR") || buildCheck.stdout.includes("Cannot find")) {
125
- lastError = `Build error:\n${buildCheck.stdout}`;
126
- continue; // retry
127
- }
206
+ const err = "Session ended without result";
207
+ await failTask(task.id, err);
208
+ return { success: false, error: err };
128
209
 
129
- await completeTask(task.id, `✓ Implemented: ${result.plan}`);
130
- console.log(` ✓ Done: ${task.title}`);
131
- return { success: true, files: result.files, verificationSteps: result.verificationSteps };
210
+ } catch (err) {
211
+ const msg = err.message || String(err);
212
+ console.error(` ✗ Error:`, msg.slice(0, 200));
132
213
 
133
- } catch (err) {
134
- lastError = err.message;
135
- console.error(` ✗ Error on attempt ${attempt}:`, err.message);
214
+ // Missing CLI — helpful message
215
+ if (msg.includes("CLINotFoundError") || msg.includes("ENOENT") || msg.includes("claude-code")) {
216
+ const hint = "Claude Code CLI not installed. Run: npm install -g @anthropic-ai/claude-code";
217
+ await failTask(task.id, hint);
218
+ console.error(`\n ⚠️ ${hint}\n`);
219
+ return { success: false, error: hint };
136
220
  }
137
- }
138
221
 
139
- await failTask(task.id, `Failed after ${MAX_RETRIES} attempts. Last error: ${lastError}`);
140
- return { success: false, error: lastError };
222
+ await failTask(task.id, msg.slice(0, 500));
223
+ return { success: false, error: msg };
224
+ }
141
225
  }
142
226
 
143
- /**
144
- * Get files relevant to this task based on keywords
145
- */
146
- function getRelevantFiles(task, projectPath) {
147
- const keywords = task.title.toLowerCase().split(" ");
148
- const allFiles = listFiles(projectPath, [".ts", ".tsx", ".js", ".jsx", ".json"]);
149
-
150
- // Always include key config files
151
- const alwaysInclude = ["package.json", "app.json", "tsconfig.json", "babel.config.js"];
152
- const result = allFiles.filter((f) => {
153
- const rel = path.relative(projectPath, f);
154
- if (alwaysInclude.some((name) => rel.endsWith(name))) return true;
155
- if (rel.includes("app/") && rel.endsWith("_layout")) return true;
156
- // Include files that match task keywords
157
- return keywords.some((kw) => kw.length > 4 && rel.toLowerCase().includes(kw));
158
- });
159
-
160
- return result.slice(0, 15); // Max 15 files for context
227
+ function formatToolLog(block) {
228
+ const { name, input = {} } = block;
229
+ switch (name) {
230
+ case "Read": return `Read: ${input.file_path || input.path || ""}`;
231
+ case "Write": return `Write: ${input.file_path || input.path || ""}`;
232
+ case "Edit":
233
+ case "MultiEdit": return `Edit: ${input.file_path || input.path || ""}`;
234
+ case "Bash": return `Run: ${(input.command || "").slice(0, 80)}`;
235
+ case "Glob": return `Glob: ${input.pattern || ""}`;
236
+ case "Grep": return `Grep: "${(input.pattern || "").slice(0, 40)}"`;
237
+ case "TodoWrite": return `Planning subtasks...`;
238
+ default: return null;
239
+ }
161
240
  }
package/bin/cli.js CHANGED
@@ -151,6 +151,21 @@ program
151
151
  if (opts.restart) console.log(chalk.yellow(` Mode → FORCE RESTART\n`));
152
152
  else console.log(chalk.dim(` Mode → auto-resume if tasks exist\n`));
153
153
 
154
+ // ── Verify Claude Code CLI is installed ───────────────────────────────────
155
+ const { execSync } = await import("child_process");
156
+ try {
157
+ execSync("claude --version", { stdio: "pipe" });
158
+ const version = execSync("claude --version", { stdio: "pipe" }).toString().trim();
159
+ console.log(chalk.dim(` Claude Code → ${version}\n`));
160
+ } catch {
161
+ console.log(chalk.red("\n ✗ Claude Code CLI not found!\n"));
162
+ console.log(chalk.yellow(" The developer agent requires Claude Code to be installed:"));
163
+ console.log(chalk.bold(" npm install -g @anthropic-ai/claude-code\n"));
164
+ console.log(chalk.dim(" Then authenticate:"));
165
+ console.log(chalk.bold(" claude\n"));
166
+ process.exit(1);
167
+ }
168
+
154
169
  const { runOrchestrator } = await import("../agents/orchestrator.js");
155
170
 
156
171
  await runOrchestrator({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "1.5.0",
3
+ "version": "2.1.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "README.md"
16
16
  ],
17
17
  "dependencies": {
18
+ "@anthropic-ai/claude-agent-sdk": "latest",
18
19
  "@supabase/supabase-js": "^2.43.1",
19
20
  "chalk": "^5.3.0",
20
21
  "commander": "^12.0.0",