claudeboard 1.0.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/README.md +128 -0
- package/agents/architect.js +76 -0
- package/agents/board-client.js +91 -0
- package/agents/claude-api.js +91 -0
- package/agents/developer.js +161 -0
- package/agents/orchestrator.js +204 -0
- package/agents/qa.js +208 -0
- package/bin/cli.js +199 -0
- package/dashboard/index.html +983 -0
- package/dashboard/server.js +197 -0
- package/package.json +55 -0
- package/sql/setup.sql +57 -0
- package/tools/filesystem.js +95 -0
- package/tools/screenshot.js +74 -0
- package/tools/supabase-reader.js +74 -0
- package/tools/terminal.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ClaudeBoard 🤖
|
|
2
|
+
|
|
3
|
+
**Autonomous coding dashboard for Claude Code.**
|
|
4
|
+
Turn a PRD into tasks → let Claude work autonomously → watch progress in real time.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## How it works
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
claudeboard init → Configure project + Supabase
|
|
12
|
+
claudeboard import-prd → Parse PRD → create tasks automatically
|
|
13
|
+
claudeboard start → Launch dashboard on localhost
|
|
14
|
+
→ Give Claude Code the AGENT.md file
|
|
15
|
+
→ Claude works autonomously 24/7
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g claudeboard
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Setup (one time per project)
|
|
29
|
+
|
|
30
|
+
### 1. Init
|
|
31
|
+
```bash
|
|
32
|
+
cd your-project
|
|
33
|
+
claudeboard init
|
|
34
|
+
```
|
|
35
|
+
You'll be asked for:
|
|
36
|
+
- Project name
|
|
37
|
+
- Supabase URL
|
|
38
|
+
- Supabase anon key
|
|
39
|
+
- Port (default 3131)
|
|
40
|
+
|
|
41
|
+
### 2. Run SQL in Supabase
|
|
42
|
+
Open `claudeboard-setup.sql` and run it in your Supabase SQL Editor.
|
|
43
|
+
This creates the tables `cb_epics`, `cb_tasks`, `cb_logs` with Realtime enabled.
|
|
44
|
+
|
|
45
|
+
### 3. Import your PRD
|
|
46
|
+
```bash
|
|
47
|
+
claudeboard import-prd ./PRD.md
|
|
48
|
+
```
|
|
49
|
+
Claude parses your PRD and creates structured tasks grouped by epic automatically.
|
|
50
|
+
|
|
51
|
+
### 4. Start the dashboard
|
|
52
|
+
```bash
|
|
53
|
+
claudeboard start
|
|
54
|
+
```
|
|
55
|
+
Opens `http://localhost:3131` in your browser.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Running Claude Code autonomously
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude --context AGENT.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `AGENT.md` file (auto-generated by `claudeboard init`) tells Claude to:
|
|
66
|
+
1. Fetch the next pending task from the API
|
|
67
|
+
2. Start it → mark as `in_progress`
|
|
68
|
+
3. Do the work (write code, run tests, fix errors)
|
|
69
|
+
4. Log progress in real time
|
|
70
|
+
5. Mark as `done` or `error`
|
|
71
|
+
6. Repeat until all tasks are complete
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Dashboard features
|
|
76
|
+
|
|
77
|
+
- **Kanban view** — tasks grouped by epic with status colors
|
|
78
|
+
- **Live activity log** — every action Claude takes
|
|
79
|
+
- **Progress bar** — overall completion %
|
|
80
|
+
- **Add tasks** — add new tasks manually (Claude picks them up automatically)
|
|
81
|
+
- **Task detail** — click any task to see its full log
|
|
82
|
+
- **Real-time** — WebSocket updates, no refresh needed
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## API (used by Claude Code)
|
|
87
|
+
|
|
88
|
+
| Method | Endpoint | Description |
|
|
89
|
+
|--------|----------|-------------|
|
|
90
|
+
| GET | `/api/board` | Full board state |
|
|
91
|
+
| GET | `/api/tasks/next` | Next pending task |
|
|
92
|
+
| POST | `/api/tasks/:id/start` | Mark task as in_progress |
|
|
93
|
+
| POST | `/api/tasks/:id/log` | Add a log entry |
|
|
94
|
+
| POST | `/api/tasks/:id/complete` | Mark task as done |
|
|
95
|
+
| POST | `/api/tasks/:id/fail` | Mark task as error |
|
|
96
|
+
| POST | `/api/tasks` | Add a new task |
|
|
97
|
+
| GET | `/api/tasks/:id/logs` | Get logs for a task |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Remote access
|
|
102
|
+
|
|
103
|
+
To monitor from another computer:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# On the notebook running claudeboard:
|
|
107
|
+
# Install Tailscale: https://tailscale.com
|
|
108
|
+
tailscale up
|
|
109
|
+
|
|
110
|
+
# Then from your main computer:
|
|
111
|
+
# Visit http://<notebook-tailscale-ip>:3131
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Or with SSH tunnel:
|
|
115
|
+
```bash
|
|
116
|
+
ssh -L 3131:localhost:3131 user@notebook-ip
|
|
117
|
+
# Then open http://localhost:3131 locally
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Stack
|
|
123
|
+
|
|
124
|
+
- **CLI**: Node.js + Commander + Enquirer
|
|
125
|
+
- **Server**: Express + WebSockets
|
|
126
|
+
- **Database**: Supabase (Postgres + Realtime)
|
|
127
|
+
- **Dashboard**: Vanilla HTML/CSS/JS (no build step)
|
|
128
|
+
- **AI parsing**: Claude claude-sonnet-4-20250514 for PRD analysis
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { callClaudeJSON } from "./claude-api.js";
|
|
2
|
+
import { createEpic, createTask, addLog } from "./board-client.js";
|
|
3
|
+
|
|
4
|
+
const SYSTEM = `You are a senior mobile app architect specializing in React Native / Expo apps.
|
|
5
|
+
Your job is to read a PRD and produce a complete, ordered task breakdown.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- Think like a real engineering team: setup first, core features, then polish, then QA
|
|
9
|
+
- Each task must be self-contained and implementable by a single developer agent
|
|
10
|
+
- Be specific — tasks like "implement auth" are too vague. Break into: "create login screen UI", "implement Supabase auth hook", "add protected route navigation"
|
|
11
|
+
- Always include: project setup, navigation, data layer, each feature screen, error handling, loading states, and final QA tasks
|
|
12
|
+
- Priority: high = blocking/core, medium = main features, low = polish/nice-to-have
|
|
13
|
+
- Types: config (setup/deps), feature (new screen or functionality), bug (fix), refactor, test (QA task)`;
|
|
14
|
+
|
|
15
|
+
export async function runArchitectAgent(prdContent, projectName) {
|
|
16
|
+
console.log(" 🏗️ Architect analyzing PRD...");
|
|
17
|
+
|
|
18
|
+
const result = await callClaudeJSON(SYSTEM, `
|
|
19
|
+
Project: ${projectName}
|
|
20
|
+
|
|
21
|
+
PRD:
|
|
22
|
+
${prdContent}
|
|
23
|
+
|
|
24
|
+
Return this JSON structure:
|
|
25
|
+
{
|
|
26
|
+
"techStack": {
|
|
27
|
+
"framework": "expo",
|
|
28
|
+
"navigation": "expo-router or react-navigation",
|
|
29
|
+
"stateManagement": "...",
|
|
30
|
+
"backend": "supabase / firebase / none",
|
|
31
|
+
"ui": "nativewind / tamagui / stylesheet",
|
|
32
|
+
"otherDeps": ["list of npm packages needed"]
|
|
33
|
+
},
|
|
34
|
+
"epics": [
|
|
35
|
+
{
|
|
36
|
+
"name": "Epic name (e.g. Project Setup, Authentication, Home Screen)",
|
|
37
|
+
"order": 1,
|
|
38
|
+
"tasks": [
|
|
39
|
+
{
|
|
40
|
+
"title": "Specific task title",
|
|
41
|
+
"description": "Detailed description of exactly what needs to be implemented. Include: what file(s) to create/modify, what the component/function should do, any specific requirements from the PRD.",
|
|
42
|
+
"priority": "high|medium|low",
|
|
43
|
+
"type": "config|feature|test",
|
|
44
|
+
"acceptanceCriteria": "How to verify this task is done correctly"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Order epics from first to last in implementation order.
|
|
52
|
+
Include 25-50 tasks total for a complete mobile app.`);
|
|
53
|
+
|
|
54
|
+
console.log(` ✓ Architect created ${result.epics.length} epics`);
|
|
55
|
+
|
|
56
|
+
// Persist to board
|
|
57
|
+
let totalTasks = 0;
|
|
58
|
+
for (let i = 0; i < result.epics.length; i++) {
|
|
59
|
+
const epicData = result.epics[i];
|
|
60
|
+
const epic = await createEpic(epicData.name);
|
|
61
|
+
|
|
62
|
+
for (const task of epicData.tasks) {
|
|
63
|
+
await createTask({
|
|
64
|
+
epicId: epic.id,
|
|
65
|
+
title: task.title,
|
|
66
|
+
description: `${task.description}\n\nAcceptance: ${task.acceptanceCriteria || ""}`,
|
|
67
|
+
priority: task.priority,
|
|
68
|
+
type: task.type,
|
|
69
|
+
});
|
|
70
|
+
totalTasks++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(` ✓ Created ${totalTasks} tasks in board`);
|
|
75
|
+
return { techStack: result.techStack, epics: result.epics, totalTasks };
|
|
76
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
let supabase = null;
|
|
4
|
+
let PROJECT = "default";
|
|
5
|
+
|
|
6
|
+
export function initBoard(url, key, project) {
|
|
7
|
+
supabase = createClient(url, key);
|
|
8
|
+
PROJECT = project;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getNextTask() {
|
|
12
|
+
const { data } = await supabase
|
|
13
|
+
.from("cb_tasks")
|
|
14
|
+
.select("*, cb_epics(name)")
|
|
15
|
+
.eq("project", PROJECT)
|
|
16
|
+
.eq("status", "todo")
|
|
17
|
+
.order("priority_order", { ascending: true })
|
|
18
|
+
.limit(1)
|
|
19
|
+
.single();
|
|
20
|
+
return data || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getAllTasks() {
|
|
24
|
+
const { data } = await supabase
|
|
25
|
+
.from("cb_tasks")
|
|
26
|
+
.select("*, cb_epics(name)")
|
|
27
|
+
.eq("project", PROJECT)
|
|
28
|
+
.order("priority_order");
|
|
29
|
+
return data || [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function startTask(id, log) {
|
|
33
|
+
await supabase.from("cb_tasks").update({ status: "in_progress", started_at: new Date().toISOString() }).eq("id", id);
|
|
34
|
+
if (log) await addLog(id, log, "start");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function completeTask(id, log) {
|
|
38
|
+
await supabase.from("cb_tasks").update({ status: "done", completed_at: new Date().toISOString() }).eq("id", id);
|
|
39
|
+
if (log) await addLog(id, log, "complete");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function failTask(id, log) {
|
|
43
|
+
await supabase.from("cb_tasks").update({ status: "error" }).eq("id", id);
|
|
44
|
+
if (log) await addLog(id, log, "error");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function blockTask(id, log) {
|
|
48
|
+
await supabase.from("cb_tasks").update({ status: "blocked" }).eq("id", id);
|
|
49
|
+
if (log) await addLog(id, log, "error");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function addLog(taskId, message, type = "progress") {
|
|
53
|
+
await supabase.from("cb_logs").insert({ project: PROJECT, task_id: taskId, message, type });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function createEpic(name) {
|
|
57
|
+
const { data } = await supabase.from("cb_epics").insert({ name, project: PROJECT }).select().single();
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function createTask({ epicId, title, description, priority = "medium", type = "feature" }) {
|
|
62
|
+
const priorityOrder = { high: 1, medium: 2, low: 3 };
|
|
63
|
+
const { data } = await supabase
|
|
64
|
+
.from("cb_tasks")
|
|
65
|
+
.insert({
|
|
66
|
+
project: PROJECT,
|
|
67
|
+
epic_id: epicId,
|
|
68
|
+
title,
|
|
69
|
+
description,
|
|
70
|
+
priority,
|
|
71
|
+
priority_order: priorityOrder[priority] || 2,
|
|
72
|
+
type,
|
|
73
|
+
status: "todo",
|
|
74
|
+
})
|
|
75
|
+
.select()
|
|
76
|
+
.single();
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getStats() {
|
|
81
|
+
const tasks = await getAllTasks();
|
|
82
|
+
return {
|
|
83
|
+
total: tasks.length,
|
|
84
|
+
todo: tasks.filter((t) => t.status === "todo").length,
|
|
85
|
+
in_progress: tasks.filter((t) => t.status === "in_progress").length,
|
|
86
|
+
done: tasks.filter((t) => t.status === "done").length,
|
|
87
|
+
error: tasks.filter((t) => t.status === "error").length,
|
|
88
|
+
blocked: tasks.filter((t) => t.status === "blocked").length,
|
|
89
|
+
pct: tasks.length > 0 ? Math.round((tasks.filter((t) => t.status === "done").length / tasks.length) * 100) : 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Claude API caller for all agents
|
|
3
|
+
* All agents use claude-sonnet-4-20250514 with specific system prompts and tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MODEL = "claude-sonnet-4-20250514";
|
|
7
|
+
const MAX_TOKENS = 8096;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Call Claude API and get text response
|
|
11
|
+
*/
|
|
12
|
+
export async function callClaude(systemPrompt, userMessage, options = {}) {
|
|
13
|
+
const body = {
|
|
14
|
+
model: MODEL,
|
|
15
|
+
max_tokens: options.maxTokens || MAX_TOKENS,
|
|
16
|
+
system: systemPrompt,
|
|
17
|
+
messages: [{ role: "user", content: userMessage }],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (options.tools) body.tools = options.tools;
|
|
21
|
+
|
|
22
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const err = await response.text();
|
|
30
|
+
throw new Error(`Claude API error ${response.status}: ${err}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
|
|
35
|
+
// Extract text content
|
|
36
|
+
const text = data.content
|
|
37
|
+
.filter((b) => b.type === "text")
|
|
38
|
+
.map((b) => b.text)
|
|
39
|
+
.join("");
|
|
40
|
+
|
|
41
|
+
return { text, raw: data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Call Claude API expecting JSON response
|
|
46
|
+
* Returns parsed object or throws
|
|
47
|
+
*/
|
|
48
|
+
export async function callClaudeJSON(systemPrompt, userMessage, options = {}) {
|
|
49
|
+
const sys = systemPrompt + "\n\nYou MUST respond with valid JSON only. No markdown, no explanation, no backticks. Pure JSON.";
|
|
50
|
+
const { text } = await callClaude(sys, userMessage, options);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const clean = text.replace(/```json|```/g, "").trim();
|
|
54
|
+
return JSON.parse(clean);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(`Failed to parse JSON from Claude: ${text.slice(0, 200)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Call Claude with an image (for visual QA)
|
|
62
|
+
*/
|
|
63
|
+
export async function callClaudeWithImage(systemPrompt, userMessage, imageBase64, mediaType = "image/png") {
|
|
64
|
+
const body = {
|
|
65
|
+
model: MODEL,
|
|
66
|
+
max_tokens: MAX_TOKENS,
|
|
67
|
+
system: systemPrompt,
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: "user",
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "image",
|
|
74
|
+
source: { type: "base64", media_type: mediaType, data: imageBase64 },
|
|
75
|
+
},
|
|
76
|
+
{ type: "text", text: userMessage },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
const text = data.content?.filter((b) => b.type === "text").map((b) => b.text).join("") || "";
|
|
90
|
+
return { text, raw: data };
|
|
91
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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";
|
|
6
|
+
|
|
7
|
+
const SYSTEM = `You are a senior React Native / Expo developer. You write clean, production-ready code.
|
|
8
|
+
|
|
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
|
|
18
|
+
|
|
19
|
+
When you need to create or modify files, respond with a JSON array of file operations.`;
|
|
20
|
+
|
|
21
|
+
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);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
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 {}
|
|
96
|
+
} else {
|
|
97
|
+
writeFile(fullPath, file.content);
|
|
98
|
+
await addLog(task.id, `${file.action}: ${file.path}`, "progress");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
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
|
|
115
|
+
}
|
|
116
|
+
|
|
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
|
+
);
|
|
123
|
+
|
|
124
|
+
if (buildCheck.stdout.includes("ERROR") || buildCheck.stdout.includes("Cannot find")) {
|
|
125
|
+
lastError = `Build error:\n${buildCheck.stdout}`;
|
|
126
|
+
continue; // retry
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await completeTask(task.id, `✓ Implemented: ${result.plan}`);
|
|
130
|
+
console.log(` ✓ Done: ${task.title}`);
|
|
131
|
+
return { success: true, files: result.files, verificationSteps: result.verificationSteps };
|
|
132
|
+
|
|
133
|
+
} catch (err) {
|
|
134
|
+
lastError = err.message;
|
|
135
|
+
console.error(` ✗ Error on attempt ${attempt}:`, err.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await failTask(task.id, `Failed after ${MAX_RETRIES} attempts. Last error: ${lastError}`);
|
|
140
|
+
return { success: false, error: lastError };
|
|
141
|
+
}
|
|
142
|
+
|
|
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
|
|
161
|
+
}
|