Compare commits
2 Commits
v1.15.0-mc
...
v2.0-propo
| Author | SHA1 | Date | |
|---|---|---|---|
| f2974d6887 | |||
| 29c7d051b6 |
@@ -2,6 +2,10 @@
|
||||
|
||||
All notable changes per release tag. Most recent on top, ordered by tag creation date (which matches the git history). Tag names follow `vMAJOR.MINOR.PATCH-slug` — the slug describes what shipped, so the tag name alone is enough to recall the batch.
|
||||
|
||||
## v1.16.0-codesight-merge — 2026-05-24
|
||||
|
||||
Ports codesight's highest-value analysis capabilities into the codecontext sidecar as 4 new MCP tools. Tier 1 (graph queries on existing edges, no re-parsing): `get_blast_radius` (BFS reverse-edge traversal — "what breaks if I change this file?", with depth tracking) and `get_hot_files` (most-imported files ranked by incoming edge count — change-risk indicators). Tier 2 (tree-sitter AST re-parsing on demand): `get_routes` (Fastify/Express HTTP route extraction with method, path, file, line, inferred tags for db/auth/cache) and `get_middleware` (middleware registration detection via import-name heuristics and app.register/addHook/setErrorHandler patterns, classifying as auth/cors/rate-limit/security/error-handler/logging/validation). All 4 tools use `defer s.graphMu.RUnlock()` for consistent mutex discipline (reviewer caught that the initial implementation released the lock early on the Tier 2 tools). Route object-property extraction delegates to `extractStringValue` for template-literal handling (reviewer catch). codecontext sidecar rebuilt from `/opt/forks/codecontext` commit `b19e646`, tagged `v1.16.0-codesight-merge`. BooCode wrapper tools follow the existing codecontext pattern — 4 new files in `apps/server/src/services/tools/codecontext/`, registered in ALL_TOOLS. 29 new Go tests + 363/363 BooCode server tests passing. No schema changes, no frontend changes.
|
||||
|
||||
## v1.15.0-mcp-multi — 2026-05-24
|
||||
|
||||
Multi-server MCP client with stdio + Streamable HTTP transports, JSON config file, and per-agent tool glob patterns. Generalizes the v1.14.1 single-server Context7 PoC into a registry of named MCP servers with per-server graceful degradation. JSON config at `/data/mcp.json` (bind-mounted alongside `AGENTS.md`) matches opencode's `mcpServers` schema shape so server entries are copy-pasteable. Config file missing = no MCP (opt-in by file presence). Stdio transport spawns a persistent subprocess via the SDK's `StdioClientTransport` with NDJSON framing; Streamable HTTP reuses the v1.14.1 pattern via `StreamableHTTPClientTransport`. Tool prefix generalized from `context7_<name>` to `<serverName>_<toolName>` with a reverse `toolToServer` map for dispatch routing. Per-agent AGENTS.md `tools:` field now supports glob patterns (`context7_*`, `!web_*`) via `matchToolGlob` (last-match-wins, `!` prefix denies); replaces the exact-match `.includes()` in `stream-phase.ts`. Glob patterns bypass `ALL_TOOL_NAMES` validation in the parser since MCP tool names aren't known at parse time. `refreshToolNames()` in `agents.ts` rebuilds the `DEFAULT_TOOLS` snapshot after `appendMcpTools` so agents without explicit `tools:` lists see MCP tools — reviewer caught that the module-load-time snapshot would permanently exclude late-registered tools. Read-only invariant preserved: all MCP tools with `readOnlyHint: false` rejected at discovery. Result size capped at 5MB. Shutdown hook closes all transports. v1.14.1 env vars (`MCP_CONTEXT7_URL`, `MCP_CONTEXT7_API_KEY`) removed — superseded by the config file. Default `data/mcp.json` ships with Context7 disabled; flip `"enabled": true` to activate. 363/363 server tests passing (27 new: multi-server wrapping, glob matching, routing, degradation). No schema changes, no frontend changes.
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
watchChanges,
|
||||
getSemanticNeighborhoods,
|
||||
getFrameworkAnalysis,
|
||||
getBlastRadius,
|
||||
getHotFiles,
|
||||
getRoutes,
|
||||
getMiddleware,
|
||||
} from './tools/codecontext/index.js';
|
||||
// v1.13.17-cross-repo-reads: cross-repo read grant request tool. Paired
|
||||
// with the pause-on-pending-grant branch in inference/tool-phase.ts and the
|
||||
@@ -680,6 +684,11 @@ export let ALL_TOOLS: ToolDef<unknown>[] = [
|
||||
watchChanges as ToolDef<unknown>,
|
||||
getSemanticNeighborhoods as ToolDef<unknown>,
|
||||
getFrameworkAnalysis as ToolDef<unknown>,
|
||||
// v1.16: codesight-merge tools. Backed by the same codecontext sidecar.
|
||||
getBlastRadius as ToolDef<unknown>,
|
||||
getHotFiles as ToolDef<unknown>,
|
||||
getRoutes as ToolDef<unknown>,
|
||||
getMiddleware as ToolDef<unknown>,
|
||||
// v1.13.17-cross-repo-reads: paired with the pause-on-pending-grant
|
||||
// branch in tool-phase.ts. Read-only — only ever READS files; the only
|
||||
// state change is appending to sessions.allowed_read_paths via the
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToolDef } from '../../tools.js';
|
||||
import { callCodecontext, type CodecontextResponse } from '../../codecontext_client.js';
|
||||
|
||||
export const GetBlastRadiusInput = z.object({
|
||||
file_path: z.string().trim().min(1),
|
||||
});
|
||||
export type GetBlastRadiusInputT = z.infer<typeof GetBlastRadiusInput>;
|
||||
|
||||
const DESCRIPTION =
|
||||
'Returns all files that depend (transitively) on the given file, with depth tracking. ' +
|
||||
'Use to assess the impact of changing a file — "what breaks if I modify this?" ' +
|
||||
'Traverses the import graph in reverse via BFS. Results sorted by distance (closest dependents first).';
|
||||
|
||||
export async function executeGetBlastRadius(
|
||||
input: GetBlastRadiusInputT,
|
||||
projectPath: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<CodecontextResponse> {
|
||||
return callCodecontext(
|
||||
{ toolName: 'get_blast_radius', args: { file_path: input.file_path }, projectPath },
|
||||
fetcher,
|
||||
);
|
||||
}
|
||||
|
||||
export const getBlastRadius: ToolDef<GetBlastRadiusInputT> = {
|
||||
name: 'get_blast_radius',
|
||||
description: DESCRIPTION,
|
||||
inputSchema: GetBlastRadiusInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_blast_radius',
|
||||
description: DESCRIPTION,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'Absolute or project-relative path to the file to analyze.',
|
||||
},
|
||||
},
|
||||
required: ['file_path'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input, projectRoot) {
|
||||
return await executeGetBlastRadius(input, projectRoot);
|
||||
},
|
||||
};
|
||||
50
apps/server/src/services/tools/codecontext/get_hot_files.ts
Normal file
50
apps/server/src/services/tools/codecontext/get_hot_files.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToolDef } from '../../tools.js';
|
||||
import { callCodecontext, type CodecontextResponse } from '../../codecontext_client.js';
|
||||
|
||||
export const GetHotFilesInput = z.object({
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
export type GetHotFilesInputT = z.infer<typeof GetHotFilesInput>;
|
||||
|
||||
const DESCRIPTION =
|
||||
'Returns the most-imported files in the project, ranked by incoming import count. ' +
|
||||
'Hot files are high-risk change targets — many other files depend on them. ' +
|
||||
'Use to identify core modules and assess refactoring risk.';
|
||||
|
||||
export async function executeGetHotFiles(
|
||||
input: GetHotFilesInputT,
|
||||
projectPath: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<CodecontextResponse> {
|
||||
return callCodecontext(
|
||||
{ toolName: 'get_hot_files', args: input.limit != null ? { limit: input.limit } : {}, projectPath },
|
||||
fetcher,
|
||||
);
|
||||
}
|
||||
|
||||
export const getHotFiles: ToolDef<GetHotFilesInputT> = {
|
||||
name: 'get_hot_files',
|
||||
description: DESCRIPTION,
|
||||
inputSchema: GetHotFilesInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_hot_files',
|
||||
description: DESCRIPTION,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of files to return (default 20, max 100).',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input, projectRoot) {
|
||||
return await executeGetHotFiles(input, projectRoot);
|
||||
},
|
||||
};
|
||||
41
apps/server/src/services/tools/codecontext/get_middleware.ts
Normal file
41
apps/server/src/services/tools/codecontext/get_middleware.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToolDef } from '../../tools.js';
|
||||
import { callCodecontext, type CodecontextResponse } from '../../codecontext_client.js';
|
||||
|
||||
export const GetMiddlewareInput = z.object({});
|
||||
export type GetMiddlewareInputT = z.infer<typeof GetMiddlewareInput>;
|
||||
|
||||
const DESCRIPTION =
|
||||
'Detects middleware registrations in the project. Identifies auth, CORS, rate-limit, ' +
|
||||
'security-headers, error-handler, logging, and validation middleware by analyzing ' +
|
||||
'import names (@fastify/cors, helmet, etc.) and registration patterns ' +
|
||||
'(app.register, app.addHook, app.setErrorHandler).';
|
||||
|
||||
export async function executeGetMiddleware(
|
||||
_input: GetMiddlewareInputT,
|
||||
projectPath: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<CodecontextResponse> {
|
||||
return callCodecontext({ toolName: 'get_middleware', args: {}, projectPath }, fetcher);
|
||||
}
|
||||
|
||||
export const getMiddleware: ToolDef<GetMiddlewareInputT> = {
|
||||
name: 'get_middleware',
|
||||
description: DESCRIPTION,
|
||||
inputSchema: GetMiddlewareInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_middleware',
|
||||
description: DESCRIPTION,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input, projectRoot) {
|
||||
return await executeGetMiddleware(input, projectRoot);
|
||||
},
|
||||
};
|
||||
50
apps/server/src/services/tools/codecontext/get_routes.ts
Normal file
50
apps/server/src/services/tools/codecontext/get_routes.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToolDef } from '../../tools.js';
|
||||
import { callCodecontext, type CodecontextResponse } from '../../codecontext_client.js';
|
||||
|
||||
export const GetRoutesInput = z.object({
|
||||
framework: z.string().trim().optional(),
|
||||
});
|
||||
export type GetRoutesInputT = z.infer<typeof GetRoutesInput>;
|
||||
|
||||
const DESCRIPTION =
|
||||
'Extracts HTTP routes from the project via tree-sitter AST analysis. ' +
|
||||
'Detects Fastify and Express route registrations (app.get, app.post, app.route, router.use, etc.) ' +
|
||||
'with method, path, file, line number, and inferred tags (db, auth, cache). ' +
|
||||
'Optional framework filter narrows to "fastify" or "express".';
|
||||
|
||||
export async function executeGetRoutes(
|
||||
input: GetRoutesInputT,
|
||||
projectPath: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<CodecontextResponse> {
|
||||
const args: Record<string, unknown> = {};
|
||||
if (input.framework) args.framework = input.framework;
|
||||
return callCodecontext({ toolName: 'get_routes', args, projectPath }, fetcher);
|
||||
}
|
||||
|
||||
export const getRoutes: ToolDef<GetRoutesInputT> = {
|
||||
name: 'get_routes',
|
||||
description: DESCRIPTION,
|
||||
inputSchema: GetRoutesInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_routes',
|
||||
description: DESCRIPTION,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
framework: {
|
||||
type: 'string',
|
||||
description: 'Filter to a specific framework: "fastify" or "express". Omit for all.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input, projectRoot) {
|
||||
return await executeGetRoutes(input, projectRoot);
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// v1.12 Track B.2: codecontext tool registry. Re-exports the 8 ToolDefs so
|
||||
// tools.ts can pull them in one line.
|
||||
// codecontext tool registry. Re-exports ToolDefs so tools.ts can pull them
|
||||
// in one line. v1.12: 8 original tools. v1.16: +4 codesight-merge tools.
|
||||
|
||||
export { getCodebaseOverview } from './get_codebase_overview.js';
|
||||
export { getFileAnalysis } from './get_file_analysis.js';
|
||||
@@ -9,3 +9,7 @@ export { getDependencies } from './get_dependencies.js';
|
||||
export { watchChanges } from './watch_changes.js';
|
||||
export { getSemanticNeighborhoods } from './get_semantic_neighborhoods.js';
|
||||
export { getFrameworkAnalysis } from './get_framework_analysis.js';
|
||||
export { getBlastRadius } from './get_blast_radius.js';
|
||||
export { getHotFiles } from './get_hot_files.js';
|
||||
export { getRoutes } from './get_routes.js';
|
||||
export { getMiddleware } from './get_middleware.js';
|
||||
|
||||
274
openspec/changes/v2.0-boocoder/proposal.md
Normal file
274
openspec/changes/v2.0-boocoder/proposal.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# v2.0 — BooCoder
|
||||
|
||||
Major version bump. New app `apps/coder/` inside the existing monorepo. Lands together with the `boocode_db` → `boochat_db` DB rename and the per-app subdomain split (`code.indifferentketchup.com` → BooChat, `coder.indifferentketchup.com` → BooCoder).
|
||||
|
||||
## What BooCoder is
|
||||
|
||||
A write-capable coding agent surface. Two execution paths, same UI:
|
||||
|
||||
- **Path A (native):** BooCode's own inference loop with write tools (`edit_file`, `create_file`, `delete_file`). Edits queue in `pending_changes` — nothing touches disk until user approves via `/apply`.
|
||||
- **Path B (dispatch):** Shells out to external CLI agents (`opencode`, `goose`, `claude`, `pi`) via ACP (preferred) or raw PTY (fallback). One git worktree per dispatch. Captures events into the same parts taxonomy.
|
||||
|
||||
Both paths feed the same task DAG, same project registry, same pending-changes queue, same UI.
|
||||
|
||||
## Why now
|
||||
|
||||
v1.x proved the read-only loop works end-to-end: inference, tool dispatch, streaming, compaction, MCP client, outer loop, step caps, artifact rendering. The infrastructure is stable. The jump from "read-only chat" to "write-capable agent orchestrator" is the remaining gap between BooCode and having a real development environment.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Three protocol roles (locked 2026-05-22)
|
||||
|
||||
1. **MCP client (write-capable allowed).** Inherits v1.15 client. Write-capable MCP servers (e.g. `@modelcontextprotocol/server-filesystem`) route writes through `pending_changes`. Per-task allow/deny means dispatched tasks can have a different MCP roster.
|
||||
2. **MCP server (BooCoder's own primitives).** Exposes `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, `boocoder.reject`, `boocoder.dispatch_external_agent`, `boocoder.list_worktrees` as MCP tools. Stdio transport for local consumers (Sam's `opencode` in Termius); HTTP deferred until OAuth + secret storage.
|
||||
3. **ACP client (host).** Spawns `opencode acp` and `goose acp` as JSON-RPC stdio subprocesses. Maps ACP events (file operations, tool calls, terminal output) to BooCode's parts taxonomy. MCP servers configured in BooCoder are auto-forwarded to the dispatched agent (per goose docs — `context_servers` is the field).
|
||||
|
||||
### Container layout (post-v2.0)
|
||||
|
||||
| Container | Port | Mount | Purpose |
|
||||
|---|---|---|---|
|
||||
| `boochat` (was `boocode`) | `100.114.205.53:9500` | `/opt:/opt:ro` | Read-only chat + MCP client |
|
||||
| `booterm` | `100.114.205.53:9501` | `/opt:/opt:rw` | PTY/tmux terminal |
|
||||
| `boocoder` | `100.114.205.53:9502` | `/opt:/opt:rw` (policy-gated) | Write tools + ACP host + MCP client + MCP server |
|
||||
| `boochat_db` (was `boocode_db`) | `127.0.0.1:5500` | `boocode_pgdata` | Shared Postgres 16 |
|
||||
| `codecontext` | internal `:8080` | `/opt:/opt:ro` | Analysis sidecar (shared) |
|
||||
|
||||
### Caddy routing
|
||||
|
||||
```
|
||||
code.indifferentketchup.com → boochat:9500
|
||||
coder.indifferentketchup.com → boocoder:9502
|
||||
term.indifferentketchup.com → booterm:9501 (or routed under code.*/term/)
|
||||
```
|
||||
|
||||
## Schema (new tables)
|
||||
|
||||
```sql
|
||||
-- Pending changes: queued writes before /apply
|
||||
CREATE TABLE IF NOT EXISTS pending_changes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id UUID NOT NULL REFERENCES sessions(id),
|
||||
task_id UUID REFERENCES tasks(id),
|
||||
file_path TEXT NOT NULL,
|
||||
operation TEXT NOT NULL CHECK (operation IN ('create', 'edit', 'delete')),
|
||||
diff TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'applied', 'rejected', 'reverted')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- Tasks: the dispatch DAG
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID NOT NULL REFERENCES projects(id),
|
||||
parent_task_id UUID REFERENCES tasks(id),
|
||||
state TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (state IN ('pending', 'running', 'completed', 'failed', 'blocked', 'cancelled')),
|
||||
input TEXT NOT NULL,
|
||||
output_summary TEXT,
|
||||
agent TEXT,
|
||||
model TEXT,
|
||||
execution_path TEXT CHECK (execution_path IN ('native', 'acp', 'pty')),
|
||||
worktree_path TEXT,
|
||||
cost_tokens INTEGER,
|
||||
started_at TIMESTAMPTZ,
|
||||
ended_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
-- Available agents: probed at startup
|
||||
CREATE TABLE IF NOT EXISTS available_agents (
|
||||
name TEXT PRIMARY KEY,
|
||||
install_path TEXT,
|
||||
version TEXT,
|
||||
supports_acp BOOLEAN NOT NULL DEFAULT false,
|
||||
supports_mcp_client BOOLEAN NOT NULL DEFAULT false,
|
||||
last_probed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Human inbox: tasks needing attention
|
||||
CREATE VIEW human_inbox AS
|
||||
SELECT * FROM tasks WHERE state IN ('blocked', 'failed');
|
||||
```
|
||||
|
||||
`task_templates` and `pipelines` deferred to v2.1 — overhead for single-user. The core is `tasks` + `pending_changes` + `available_agents`.
|
||||
|
||||
## Path A — Native write tools
|
||||
|
||||
### Tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `edit_file` | Apply a diff to an existing file. Input: `{file_path, old_string, new_string}`. Queues in `pending_changes` with `operation='edit'`. |
|
||||
| `create_file` | Create a new file. Input: `{file_path, content}`. Queues as `operation='create'`. |
|
||||
| `delete_file` | Delete a file. Input: `{file_path}`. Queues as `operation='delete'`. |
|
||||
| `apply_pending` | Flush all pending changes for the current session to disk. Path-guarded. |
|
||||
| `rewind` | Revert a specific applied change or all changes since a checkpoint. |
|
||||
|
||||
### Path guard for writes
|
||||
|
||||
Same `pathGuard()` function from BooChat, but with a write-path variant:
|
||||
- `resolveWritePath(projectRoot, requested)` — uses `resolve()` (not `realpath()`, since the file may not exist yet for creates), then verifies the result starts with `projectRoot + sep`.
|
||||
- Deny list: everything in `secret_guard.ts` (`.env`, `*.pem`, etc.) — can't write to those either.
|
||||
- Defense-in-depth: the `pending_changes` queue means even a path-guard bypass only queues; it doesn't hit disk until `/apply` (which re-validates).
|
||||
|
||||
### Diff format
|
||||
|
||||
Standard unified diff (what `git diff` produces). The `edit_file` tool takes `old_string` / `new_string` (same as Claude Code's edit tool — the model is trained on this shape). Server computes the unified diff for storage in `pending_changes.diff`.
|
||||
|
||||
### UI: per-pane diff viewer
|
||||
|
||||
Frontend pane type `pending_changes` in BooCoder's workspace. Shows:
|
||||
- List of queued changes with file path + operation
|
||||
- Per-change diff view (syntax-highlighted, side-by-side or unified)
|
||||
- Approve / Reject per change, or Approve All / Reject All
|
||||
|
||||
## Path B — External agent dispatch
|
||||
|
||||
### dispatch_external_agent tool
|
||||
|
||||
```typescript
|
||||
{
|
||||
agent: 'opencode' | 'claude' | 'goose' | 'pi',
|
||||
model: string, // e.g. 'claude-opus-4-7'
|
||||
task: string, // natural-language task description
|
||||
worktree?: string, // optional — auto-creates if not specified
|
||||
}
|
||||
```
|
||||
|
||||
### Transport selection
|
||||
|
||||
Dispatcher checks `available_agents.supports_acp` at runtime:
|
||||
- **ACP** (preferred): `opencode acp`, `goose acp` — JSON-RPC stdio. Native session lifecycle, file-operation events, terminal events, permission prompts.
|
||||
- **PTY** (fallback): `claude`, `pi`, `smallcode` — raw terminal capture via `node-pty`. Captures stdout/stderr/exit-code into PostgreSQL. Less structured than ACP.
|
||||
|
||||
### Worktree management
|
||||
|
||||
Each dispatched task gets its own git worktree:
|
||||
```bash
|
||||
git worktree add /tmp/booworktrees/<task-id> -b task-<task-id> HEAD
|
||||
```
|
||||
|
||||
On completion: diff the worktree against HEAD, queue the diff into `pending_changes` for the same task, clean up the worktree. User approves/rejects the diff the same way as Path A.
|
||||
|
||||
### ACP event mapping
|
||||
|
||||
ACP events → BooCode parts taxonomy:
|
||||
- `file_operation` → `tool_call` part (name: `acp_edit_file`) + `tool_result` part
|
||||
- `tool_call` → `tool_call` part (preserves name)
|
||||
- `terminal_output` → routes into BooTerm pane
|
||||
- `permission_request` → pause inference (same mechanism as `ask_user_input`)
|
||||
- `session_end` → task state → `completed` or `failed`
|
||||
|
||||
### MCP server auto-forward
|
||||
|
||||
Per goose docs, `context_servers` field in the ACP session config auto-forwards BooCoder's configured MCP servers to the dispatched agent. One MCP config drives every agent.
|
||||
|
||||
## Dispatcher worker
|
||||
|
||||
Background process (or in-process `setInterval` for v2.0 simplicity) that:
|
||||
1. Queries `tasks` WHERE `state = 'pending'` ORDER BY `created_at`
|
||||
2. For each ready task (no unmet dependencies):
|
||||
- Mark `state = 'running'`
|
||||
- Resolve execution path (Path A if no agent specified, Path B if agent specified)
|
||||
- Path A: run the inference loop with write tools enabled
|
||||
- Path B: spawn ACP/PTY subprocess, stream events into parts
|
||||
- On completion: mark `state = 'completed'` or `'failed'`
|
||||
- Queue output diff into `pending_changes`
|
||||
3. On failure: mark `state = 'failed'`, surface in `human_inbox` view
|
||||
|
||||
## BooCoder MCP server
|
||||
|
||||
Exposes BooCoder's primitives as MCP tools so external agents (Sam's opencode in Termius) can drive the task queue:
|
||||
|
||||
| MCP Tool | Description |
|
||||
|---|---|
|
||||
| `boocoder.create_task` | Create a new task in the queue |
|
||||
| `boocoder.list_pending_changes` | List queued changes awaiting approval |
|
||||
| `boocoder.apply` | Apply a specific pending change |
|
||||
| `boocoder.reject` | Reject a pending change |
|
||||
| `boocoder.dispatch_external_agent` | Dispatch a task to an external agent |
|
||||
| `boocoder.list_worktrees` | List active git worktrees |
|
||||
|
||||
Stdio transport for local consumers. HTTP transport deferred until OAuth + secret storage.
|
||||
|
||||
**Eval requirement:** run through `anthropics/skills mcp-builder` 10-question evaluation framework before shipping.
|
||||
|
||||
## Code lifts
|
||||
|
||||
### Primary architectural template
|
||||
|
||||
**`Dominic789654/agent-hub`** (Apache-2.0) — task DAG schema, dispatcher worker, project registry, human inbox. Three-process model (board server + dispatcher + assistant terminal). BooCode adapts this into a single-process Fastify app (v2.0.0) with the dispatcher as an in-process worker.
|
||||
|
||||
### Pending-changes UX
|
||||
|
||||
**`plandex-ai/plandex`** (MIT) — diff/apply/rewind vocabulary. The `pending_changes` queue concept, per-file diff view, approve/reject UI pattern. No code lifted — schema and UX design only.
|
||||
|
||||
### ACP client
|
||||
|
||||
**`agentclientprotocol.com` spec + `@zed-industries/agent-client-protocol` SDK** (Apache-2.0) — local-subprocess ACP via stdio JSON-RPC. The SDK handles framing; BooCode maps events to its parts taxonomy.
|
||||
|
||||
**`goose` docs** (`goose-docs.ai/docs/guides/acp-clients/`) — `context_servers` auto-forward pattern. Critical: one MCP config drives every dispatched agent.
|
||||
|
||||
### MCP server
|
||||
|
||||
**`anthropics/skills/mcp-builder`** (MIT) — 4-phase build workflow + 10-question evaluation framework for validating the MCP server before shipping.
|
||||
|
||||
### Dispatcher pattern
|
||||
|
||||
**Paseo (`getpaseo/paseo`)** — AGPL-3.0, **design only, no code lift**. Daemon+clients architecture, `--worktree` flag, CLI verb shape (`run/ls/attach/send`). BooCode reproduces the architecture using only license-clean patterns.
|
||||
|
||||
**Roo Code Boomerang Tasks** — orchestrator with intentional capability restriction. Down-pass/up-pass context discipline (`new_task` message, `attempt_completion` result, no implicit inheritance). Explicit precedence override clause.
|
||||
|
||||
### Write-tool security
|
||||
|
||||
**opencode `permission/evaluate.ts`** — wildcard permission ruleset (already lifted in v1.15). Extended in v2.0 to gate write tools.
|
||||
|
||||
**`covibes/zeroshot`** — blind-validation invariant. Verify gate runs in a separate agent context that only sees the diff and acceptance criteria, not the producing conversation. v2.0+ optional batch.
|
||||
|
||||
## Sub-versions
|
||||
|
||||
| Version | Scope |
|
||||
|---|---|
|
||||
| **v2.0.0** | Schema + Path A (native write tools + pending-changes queue + diff UI) + basic dispatcher |
|
||||
| **v2.0.1** | Path B (ACP client for opencode/goose + PTY fallback for claude/pi + worktree management) |
|
||||
| **v2.0.2** | BooCoder MCP server (stdio transport, `boocoder.*` tools, eval framework) |
|
||||
| **v2.0.3** | Polish: `boocode` CLI (`run/ls/attach/send`), human_inbox UI, cost tracking |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- v1.13 ✅ (parts table — the event taxonomy for everything)
|
||||
- v1.14 ✅ (outer loop + step boundaries for future revert snapshots)
|
||||
- v1.14.x-mcp ✅ (MCP client PoC — proves the protocol)
|
||||
- v1.15 ✅ (full MCP client + tool globs — write-capable MCP servers route through pending_changes)
|
||||
- v1.16 ✅ (codesight merge — codecontext now has blast-radius for impact analysis)
|
||||
|
||||
All dependencies shipped. v2.0 is unblocked.
|
||||
|
||||
## Estimate
|
||||
|
||||
- v2.0.0: ~800 LoC (schema + write tools + pending-changes service + diff pane + dispatcher skeleton)
|
||||
- v2.0.1: ~600 LoC (ACP client + PTY dispatch + worktree management + event mapping)
|
||||
- v2.0.2: ~400 LoC (MCP server + 6 tool handlers + stdio transport + eval)
|
||||
- v2.0.3: ~400 LoC (CLI client + inbox UI + cost aggregation)
|
||||
- **Total: ~2200 LoC** across 4 sub-versions
|
||||
|
||||
## Hard rules
|
||||
|
||||
- BooChat stays read-only. BooCoder is the only surface with write tools.
|
||||
- Path-guard correctness is the #1 test target. Fuzz against every traversal pattern.
|
||||
- Pending-changes queue gates ALL writes (native + MCP). Nothing touches disk without user approval (or explicit auto-apply flag per task).
|
||||
- One shared database. Cross-surface joins are valuable (task → chat → terminal debugging session).
|
||||
- External CLI agents on the host, not in containers. BooCoder shells out via local-exec.
|
||||
- No OAuth in v2.0. MCP server is stdio-only until secret storage lands.
|
||||
- DB rename `boocode_db` → `boochat_db` lands with v2.0.0 (one-time migration).
|
||||
|
||||
## Repos to clone before starting
|
||||
|
||||
```bash
|
||||
cd /opt/forks
|
||||
git clone https://github.com/Dominic789654/agent-hub.git # Apache-2.0, task DAG reference
|
||||
git clone https://github.com/plandex-ai/plandex.git # MIT, pending-changes UX
|
||||
git clone https://github.com/anomalyco/opencode.git # MIT, permission evaluate.ts + MCP client reference
|
||||
```
|
||||
|
||||
ACP SDK is an npm package (`@zed-industries/agent-client-protocol`), installed at implementation time. Paseo is design-only (AGPL, no code lift).
|
||||
130
openspec/changes/v2.0-boocoder/tasks.md
Normal file
130
openspec/changes/v2.0-boocoder/tasks.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# v2.0 — BooCoder task breakdown
|
||||
|
||||
## Phase 0 — Prep (before any code)
|
||||
|
||||
- [ ] Clone lift sources: `agent-hub`, `plandex`, `opencode` to `/opt/forks/`
|
||||
- [ ] Read agent-hub's schema + dispatcher pattern (Apache-2.0)
|
||||
- [ ] Read plandex's pending-changes + diff/apply/rewind flow (MIT)
|
||||
- [ ] Read opencode's `permission/evaluate.ts` for write-gate patterns (MIT)
|
||||
- [ ] Install ACP SDK: `pnpm add @zed-industries/agent-client-protocol`
|
||||
- [ ] Verify `opencode acp` and `goose acp` are available on the host
|
||||
- [ ] Write `openspec/changes/v2.0-boocoder/design.md` with finalized decisions
|
||||
|
||||
## v2.0.0 — Schema + Path A (native write tools + pending-changes + diff UI)
|
||||
|
||||
### Infra
|
||||
|
||||
- [ ] Create `apps/coder/` directory skeleton (Fastify server, mirroring `apps/server/` structure)
|
||||
- [ ] Create `apps/coder/Dockerfile` (Node 20 bookworm-slim, `/opt:/opt:rw` mount)
|
||||
- [ ] Add `boocoder` service to `docker-compose.yml` (port 9502, boocode_net)
|
||||
- [ ] Add Caddy route: `coder.indifferentketchup.com` → boocoder:9502
|
||||
- [ ] DB rename: `boocode_db` → `boochat_db` (one-time ALTER DATABASE + docker-compose volume rename)
|
||||
- [ ] Schema migration: CREATE TABLE `pending_changes`, `tasks`, `available_agents`; CREATE VIEW `human_inbox`
|
||||
- [ ] Container guidance: `BOOCODER.md` (bind-mounted at `/app/BOOCODER.md`)
|
||||
|
||||
### Write tools
|
||||
|
||||
- [ ] `apps/coder/src/services/write_guard.ts` — `resolveWritePath(projectRoot, filePath)` (resolve + prefix-check, no realpath since file may not exist)
|
||||
- [ ] `apps/coder/src/services/pending_changes.ts` — queue, apply, reject, revert operations
|
||||
- [ ] Tool: `edit_file` — takes `{file_path, old_string, new_string}`, computes unified diff, queues in `pending_changes`
|
||||
- [ ] Tool: `create_file` — takes `{file_path, content}`, queues as `operation='create'`
|
||||
- [ ] Tool: `delete_file` — takes `{file_path}`, queues as `operation='delete'`
|
||||
- [ ] Tool: `apply_pending` — flushes pending changes to disk (re-validates write_guard before each write)
|
||||
- [ ] Tool: `rewind` — reverts applied changes by inverse-diff
|
||||
|
||||
### Inference loop
|
||||
|
||||
- [ ] Port the v1.14 outer loop from `apps/server/` into `apps/coder/` (or share via workspace package)
|
||||
- [ ] Register write tools in the coder's tool registry (alongside all read tools from BooChat)
|
||||
- [ ] Permission gate: write tools require `pending_changes` queue (can't bypass to direct disk write)
|
||||
|
||||
### Frontend (diff pane)
|
||||
|
||||
- [ ] Create `apps/coder/web/` SPA (React + Vite, same stack as BooChat's `apps/web/`)
|
||||
- [ ] Diff pane component: shows pending changes with syntax-highlighted diffs
|
||||
- [ ] Approve / Reject per change, Approve All / Reject All buttons
|
||||
- [ ] Workspace splitter integration (chat pane + diff pane side by side)
|
||||
|
||||
### Verification
|
||||
|
||||
- [ ] `pnpm -C apps/coder build` clean
|
||||
- [ ] Write path-guard fuzz tests (traversal patterns, symlinks, non-existent paths, `.env` deny)
|
||||
- [ ] `docker compose up --build -d` — boocoder container starts, healthcheck passes
|
||||
- [ ] Smoke: send a chat requesting a file edit → see it queued in diff pane → approve → file written
|
||||
|
||||
## v2.0.1 — Path B (ACP dispatch + PTY fallback + worktrees)
|
||||
|
||||
### ACP client
|
||||
|
||||
- [ ] `apps/coder/src/services/acp-client.ts` — spawn `opencode acp` / `goose acp` via `@zed-industries/agent-client-protocol` StdioTransport
|
||||
- [ ] Event mapping: ACP `file_operation` → `tool_call` part, `terminal_output` → BooTerm route, `permission_request` → pause
|
||||
- [ ] Session lifecycle: start, mid-session model switch, end
|
||||
- [ ] MCP auto-forward: pass BooCoder's `context_servers` config to the ACP session
|
||||
|
||||
### PTY fallback
|
||||
|
||||
- [ ] `apps/coder/src/services/pty-dispatch.ts` — spawn `claude` / `pi` / `smallcode` via `node-pty`
|
||||
- [ ] Capture stdout/stderr/exit-code into parts (less structured than ACP)
|
||||
- [ ] Worktree setup: `git worktree add /tmp/booworktrees/<task-id> -b task-<task-id> HEAD`
|
||||
- [ ] On completion: diff worktree vs HEAD → queue into `pending_changes`
|
||||
|
||||
### Dispatcher
|
||||
|
||||
- [ ] `apps/coder/src/services/dispatcher.ts` — polls `tasks` WHERE `state='pending'`, picks by priority + creation order
|
||||
- [ ] Transport selection: check `available_agents.supports_acp` at dispatch time
|
||||
- [ ] On failure: mark `state='failed'`, surface in `human_inbox`
|
||||
- [ ] On completion: mark `state='completed'`, queue diff if Path B
|
||||
|
||||
### Agent probing
|
||||
|
||||
- [ ] Startup probe: `which opencode && opencode --version`, `which goose`, `which claude`, `which pi`
|
||||
- [ ] Populate `available_agents` table with version + ACP support
|
||||
|
||||
### Verification
|
||||
|
||||
- [ ] Smoke: dispatch a task to `opencode` via ACP → task completes → diff queued
|
||||
- [ ] Smoke: dispatch to `claude` via PTY fallback → captures output → diff from worktree
|
||||
- [ ] Worktree cleanup after task completion
|
||||
|
||||
## v2.0.2 — BooCoder MCP server
|
||||
|
||||
### Implementation
|
||||
|
||||
- [ ] `apps/coder/src/services/mcp-server.ts` — register 6 tools as MCP tool handlers
|
||||
- [ ] Stdio transport (use `@modelcontextprotocol/sdk` server-side, same SDK as client)
|
||||
- [ ] Tools: `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, `boocoder.reject`, `boocoder.dispatch_external_agent`, `boocoder.list_worktrees`
|
||||
- [ ] Each tool maps to a DB operation or service call
|
||||
|
||||
### Eval
|
||||
|
||||
- [ ] Write 10-question eval per `anthropics/skills/mcp-builder` framework
|
||||
- [ ] Run eval against the MCP server — all 10 must pass before shipping
|
||||
- [ ] Document eval results in openspec
|
||||
|
||||
### Verification
|
||||
|
||||
- [ ] From a terminal: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | boocoder --mcp` → returns 6 tools
|
||||
- [ ] From opencode: configure BooCoder as an MCP server in `~/.opencode/config.json`, verify tool calls work
|
||||
|
||||
## v2.0.3 — Polish
|
||||
|
||||
### CLI client
|
||||
|
||||
- [ ] `apps/coder/src/cli.ts` — thin WebSocket/HTTP client against BooCoder API
|
||||
- [ ] Verbs: `boocode run <task>`, `boocode ls`, `boocode attach <id>`, `boocode send <id> <message>`
|
||||
- [ ] Mirrors Paseo's UX, license-clean implementation
|
||||
|
||||
### Human inbox UI
|
||||
|
||||
- [ ] Frontend route showing tasks in `blocked`/`failed` state
|
||||
- [ ] Per-task: view output, retry, cancel, reassign to different agent
|
||||
|
||||
### Cost tracking
|
||||
|
||||
- [ ] `tasks.cost_tokens` populated from inference usage
|
||||
- [ ] Summary view: per-project, per-agent, per-day token spend
|
||||
|
||||
### Verification
|
||||
|
||||
- [ ] `boocode run "add a health endpoint"` from terminal → task appears in UI → completes → diff in pane
|
||||
- [ ] `boocode ls` shows running/completed/failed tasks
|
||||
Reference in New Issue
Block a user