Implement task execution with Claude Code#23
Conversation
…status tracking and output handling
There was a problem hiding this comment.
Pull request overview
Adds “Claude Code task execution” to Forge so a task’s description can be run via the Claude Code CLI, streamed back to the UI, and recorded in SQLite for later review.
Changes:
- Add server-side execution service that spawns the
claudeCLI and streamsstream-jsonoutput. - Add task execution API endpoints for start (SSE), cancel, and history retrieval.
- Add
task_executionstable + frontend UI panel to start/cancel execution and display output.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| server/services/claude-code.js | Spawns Claude Code CLI, tracks active executions for cancellation, persists execution metadata. |
| server/routes/execute.js | Adds endpoints to start/cancel executions and list recent execution history (with SSE streaming). |
| server/index.js | Mounts the new task execution routes under /api/tasks. |
| server/db.js | Adds task_executions schema + index. |
| package.json | Adds @anthropic-ai/claude-code dependency. |
| package-lock.json | Locks the new dependency and its transitive optional deps. |
| forge-fe/src/types/index.ts | Adds execution status + TaskExecution types for FE consumption. |
| forge-fe/src/components/board/task-detail-modal.tsx | Adds “Execute with Claude Code” UI with streaming output + cancel button. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CREATE TABLE IF NOT EXISTS task_executions ( | ||
| id TEXT PRIMARY KEY, | ||
| task_id TEXT NOT NULL, | ||
| status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed', 'cancelled')), | ||
| prompt TEXT NOT NULL, | ||
| output TEXT NOT NULL DEFAULT '', | ||
| error TEXT, | ||
| started_at TEXT DEFAULT (datetime('now')), | ||
| completed_at TEXT | ||
| ); |
| const [isDeleting, setIsDeleting] = useState(false) | ||
| const [executionStatus, setExecutionStatus] = useState<ExecutionStatus | 'idle'>('idle') | ||
| const [executionOutput, setExecutionOutput] = useState<Array<{ type: string; content: string }>>([]) | ||
| const [executionId, setExecutionId] = useState<string | null>(null) |
| } else if (event.type === 'done') { | ||
| setExecutionStatus('completed') | ||
| } else if (event.type === 'error') { | ||
| setExecutionOutput((prev) => [ | ||
| ...prev, | ||
| { type: 'error', content: event.error || 'Unknown error' }, | ||
| ]) | ||
| setExecutionStatus('failed') |
| const child = spawn( | ||
| 'claude', | ||
| [ | ||
| '-p', prompt, | ||
| '--output-format', 'stream-json', | ||
| '--dangerously-skip-permissions', | ||
| ], |
| const child = spawn( | ||
| 'claude', | ||
| [ | ||
| '-p', prompt, | ||
| '--output-format', 'stream-json', | ||
| '--dangerously-skip-permissions', | ||
| ], | ||
| { | ||
| cwd, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| signal, | ||
| }, | ||
| ) |
| child.on('close', (code) => { | ||
| // flush remaining buffer | ||
| if (buffer.trim()) { | ||
| try { | ||
| const msg = JSON.parse(buffer.trim()) | ||
| enqueue({ type: 'message', data: msg }) | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
| if (code !== 0 && code !== null) { | ||
| exitError = new Error(`claude exited with code ${code}`) | ||
| } | ||
| done = true | ||
| enqueue({ type: 'end' }) | ||
| }) |
| const send = (obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`) | ||
|
|
||
| send({ type: 'execution_start', execution_id: executionId, task_id: taskId }) | ||
|
|
||
| // Cancel on client disconnect | ||
| req.on('close', () => { | ||
| cancelExecution(executionId) | ||
| }) |
| }) | ||
| res.json({ ok: true, execution_id: execution.id }) | ||
| } else { | ||
| res.status(404).json({ error: 'Execution not found or already finished' }) |
Backend Security ReviewCritical findings — fix before merging 1. Reflected XSS — server/routes/githubOAuth.js:160 2. SSRF — server/routes/connectors.js:129-130 3. Prompt injection + --dangerously-skip-permissions — server/services/claude-code.js:39,87 High severity 4. Secrets returned in plaintext — server/routes/connectors.js:16-26 5. No authentication on any endpoint Medium severity 6. Unbounded numeric query params — routes/local-git.js:74, routes/github.js:51-52 7. column_id not validated in PUT /api/tasks/reorder — routes/tasks.js:118-119 8. Missing global error handler — server/index.js Low / Informational
What looks good
|
Frontend Code Review - forge-fe/ Overall this is a well-structured React 19 + TypeScript app. See full review details in the PR thread. |
|
test body no hash lines here just plain text to see if this works without heading lines |
Frontend Code Review — forge-fe/Overall this is a well-structured React 19 + TypeScript app with clean component organization, good use of Tailwind, and solid TypeScript typing throughout. Below are the issues found, grouped by severity. Security1. XSS via javascript: protocol links in Markdown (markdown-content.tsx) react-markdown without rehype-raw is safe from raw HTML injection, but markdown links using the javascript: protocol still render as clickable anchor tags. An AI response containing click me would execute in the user browser. Add a custom link renderer that allowlists safe protocols: The renderer should check href?.startsWith for http://, https://, or hash prefix and render a span for unsafe URLs. This applies anywhere MarkdownContent is used: chat messages, task descriptions (task-detail-modal.tsx), and AI command output. Accessibility2. span used as interactive delete button (Chat.tsx line 142) The session delete button is a span — it is not reachable by keyboard and does not announce its role to screen readers. Replace with a button element with type=button. 3. Missing aria-label on icon-only interactive elements
4. label elements not associated with inputs (Connectors.tsx) Labels in the add-connector forms lack htmlFor attributes, and inputs lack id attributes. Screen readers cannot associate them. 5. Missing aria-label on chat textarea (Chat.tsx line 597) Placeholders are not reliable labels for assistive technology. Add aria-label=Message input. Missing Fetch Abort on Unmount6. Streaming fetches have no AbortController (Chat.tsx line 380, task-detail-modal.tsx line 132) If the user navigates away mid-stream, the ReadableStream reader continues consuming network and calling state setters on an unmounted component. Use an AbortController and tie its abort() to a useEffect cleanup. Console Statements Left In
For handleSave and handleDelete in the task modal especially, the user should see an inline error message if the operation fails. React and TypeScript7. Reused key in INITIAL_MESSAGES after chat reset (Chat.tsx lines 38, 357) INITIAL_MESSAGES is a module-level constant with a fixed ID generated once at module load. startNewChat() resets to this same object, so every new chat session reuses the same React key. Generate a fresh ID each time instead of reusing the constant. 8. Weak typing via as casts in kanban-board.tsx lines 36-40 RawTask types type, column_id, and priority as plain string, then casts with as. If the API returns an unexpected value the cast silently lies to TypeScript. Consider a type guard or runtime validation at the API boundary. 9. activeSessionId stale closure in streaming handler (Chat.tsx line 411) The streaming loop captures activeSessionId from the outer render closure. This works correctly today but is fragile — worth a comment explaining the intentional snapshot. Error Handling and UX10. No error boundary There is no ErrorBoundary wrapping any route or subtree. A rendering crash (e.g., malformed task data from API) will white-screen the whole app with no user feedback. 11. Connector fetch failures are silent (Connectors.tsx) handleAdd, handleToggle, and handleDelete all .catch(console.error) without surfacing errors to the user. If the server is down the UI appears to succeed but silently fails. Positives
Priority fixes: (1) javascript: link XSS in markdown renderer, (2) replace the span delete button with a proper button element, (3) aria-label on icon-only interactive elements, (4) streaming fetch AbortController cleanup. |
Frontend Code Review —
|
| Severity | Count |
|---|---|
| 🔴 Bugs | 2 |
| 🟠 Accessibility | 3 |
| 🟡 Error handling / UX | 4 |
| 🟡 Code quality | 3 |
| 🟢 Security (informational) | 1 |
The two bugs (#1 and #2) are the most important to fix before merging. The accessibility issues (#3–#5) are straightforward to address. Everything else is good engineering hygiene.
🤖 Generated with Claude Code
…and refactor task execution error handling
Frontend Code Review — forge-fe/Reviewed the following changed files: ✅ Good Changes
🔴 Issues — Must Fix1.
|
| Severity | Count |
|---|---|
| 🔴 Must Fix | 3 |
| 🟡 Should Fix | 4 |
| 🟢 Minor | 3 |
The most important fix is #1 (missing abort on unmount) — it will cause a React warning in development and a real memory/connection leak in production. Issues #2 and #3 are also important for correctness.
Introduce functionality for executing tasks using Claude Code, including tracking execution status and handling output. This update adds new routes for task execution, cancellation, and retrieval of execution history. Additionally, it establishes a database schema for task executions and integrates the Claude Code CLI for processing tasks.