Compare commits

...

7 Commits

Author SHA1 Message Date
e04d0fdaa8 feat(coder): unified Plan/Ask/Bypass permission picker
Replace the raw per-agent mode dropdown in the BooCoder composer with a
curated three-option permission ladder mapped generically onto each
provider's native modes: `plan` id -> Plan, default -> Ask, isUnattended
-> Bypass (claude bypassPermissions, qwen yolo, opencode full-access).
modeId stays the single wire field; the active unified mode is derived
from it (no contracts change).

Native BooCode gains its own mode set: Ask stages to the pending-changes
queue (today's behavior), Bypass auto-applies the queue to disk after the
turn (interactive messages path + task dispatcher path), Plan falls back
to Ask. The shared apps/server inference engine is left untouched.

Also preserve isUnattended on live-probed ACP modes so opencode's bypass
mode stays detectable from the wire.

Coder 373 tests green; coder + web typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:14:21 +00:00
da36344d0b style(web): outline the slash-picker chevron buttons
Give the expand chevrons the BooCoder outline-button look (border-border
bg-background, hover:bg-muted, filled when expanded) instead of the borderless
ghost style. Applies to both BooChat's flat menu and BooCoder's grouped menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:00:49 +00:00
875cae0843 fix(coder): parse YAML block-scalar descriptions in slash command discovery
Most plugin/han SKILL.md and command files write `description:` as a folded
block scalar (`>` / `|`) with the text on the following indented lines. The
old single-line frontmatter reader captured the literal `>`, so the slash
menu showed garbage/blank descriptions for nearly all of them. frontmatterField
now collapses folded blocks (join with spaces) and preserves literal blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:00:49 +00:00
4caa5f91ff docs: CLAUDE.md notes for Orchestrator + gitignored .env.host
Document the in-app Orchestrator engine and its load-bearing read-only
invariant in apps/coder/CLAUDE.md, and note that apps/coder/.env.host is
now gitignored (recreated from .env.example with CLAUDE_SDK_BACKEND=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:48:50 +00:00
1d416d0cf9 docs: refresh README + CURRENT.md for v2.7.17 (Orchestrator)
Bring README current (was v2.2.1): add the Orchestrator, the Files/Git diff
panel, persistent agent sessions + claude Agent-SDK, fix the provider list
(5 — cursor/copilot retired), drop the broken AGENTS.md link, update latest
release + planned. Refresh CURRENT.md to v2.7.17 on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:43:53 +00:00
bfda61e27e chore: stop tracking apps/coder/.env.host
Untrack the host env file (git rm --cached, kept on disk for the boocoder
service) and widen .gitignore to .env.* (re-including .env.example) so env
files no longer get committed. The file's prior contents (dev DB password +
internal Tailscale URLs; no API keys) remain in history — left as-is given the
single-user Tailscale-only threat model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:32:03 +00:00
a734615480 docs: archive shipped openspec changes, refresh roadmap + DEFERRED-WORK
Move openspec/changes/{contracts-ssot,orchestrator} → archived/ (both shipped,
v2.7.13 and v2.7.17). Mark the roadmap's "Write/edit robustness" and "Claude
provider SDK" milestones as shipped (fuzzy-match.ts + checkpoints.ts; the
claude-sdk backend is live via CLAUDE_SDK_BACKEND in .env.host) and add a
v2.7.12–v2.7.17 shipped summary. Flag DEFERRED-WORK.md as superseded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:30:01 +00:00
27 changed files with 247 additions and 57 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
node_modules node_modules
dist dist
.env .env
.env.*
!.env.example
# Claude / Cursor (local agent & IDE config — CLAUDE.md and AGENTS.md stay tracked) # Claude / Cursor (local agent & IDE config — CLAUDE.md and AGENTS.md stay tracked)
.claude/ .claude/

View File

@@ -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. 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.
## v2.7.18-permission-modes — 2026-06-05
Adds a unified **permission picker** to the BooCoder composer — Plan / Ask Permission / Bypass — replacing the old raw per-agent mode dropdown that exposed each agent's full native vocabulary with inconsistent labels. The three options map generically onto every provider's existing mode metadata: the `plan`-id mode → Plan, the default mode → Ask, the `isUnattended` mode → Bypass (claude `bypassPermissions`, qwen `yolo`, opencode `full-access`); goose has no modes so it shows no picker, exactly as before. `modeId` stays the single wire field — the active unified mode is derived from it, so no contracts change was needed. Native BooCode gains its own mode set (registered in the manifest and exposed by the snapshot): **Ask** stages edits to the pending-changes queue as today, **Bypass** auto-applies the queue to disk after the turn (both the interactive messages path and the task-based dispatcher path), and **Plan** falls back to Ask — the shared `apps/server` inference engine is deliberately left untouched. A supporting fix preserves the `isUnattended` flag on live-probed ACP modes (`acp-derive.ts`) so opencode's bypass mode is still detectable from the wire. Coder 373 tests green, coder + web typecheck clean. Built on `v2.7.17-orchestrator`.
## v2.7.17-orchestrator — 2026-06-03 ## v2.7.17-orchestrator — 2026-06-03
Brings the deterministic multi-agent "conductor" into the app as the **Orchestrator**: launch any read-only Han flow (research, code-review, investigate, architectural-analysis, security-review, …) from BooChat or BooCoder and watch each specialist agent stream live in a Paseo-style run pane, ending with an evidence-disciplined, adversarially-validated report — all on free local Qwen, persisted and resumable. Built and audited end-to-end via `paseo-epic` in an isolated worktree, on top of the prior `/opt/boocode/conductor` standalone CLI: the conductor's 22 flow definitions, Spine factory, and Han evidence/YAGNI contracts were re-homed into `apps/coder/src/conductor`, and a new DB-backed flow-runner (`flow_runs`/`flow_steps`) dispatches each step as a real BooCoder task through the existing dispatcher — reusing its streaming→WS-frame pipeline and worktree-as-read-snapshot, with an `onTaskTerminal` hook that advances the wave and a startup resume that re-dispatches in-flight steps after a coder restart. Read-only is enforced hard: every step is dispatched `qwen --approval-mode plan`, an adversarial-security review caught and closed a bypass where a qwen-unavailable task silently fell through to write-capable native inference (now fails closed), and the ACP path's mode-set was made fail-closed too. The UI adds a fourth `orchestrator` pane kind (collapsed agent roster, expand-one live stream, report on top), a Workflow button + slash flows on the shared `ChatInput` for full BooChat/BooCoder parity, a "New Orchestrator" entry in the + and split menus, a category-grouped launcher dialog, runs history, and export (copy / save-to-file / send-to-chat) — fed by two new `flow_run_*` WS frames on a coder user channel. Qwen-only by design (Claude Code remains the Claude path); the existing model-competition Arena stays a separate feature. The flow launcher and the `/` slash menu both carry chevron-expandable per-item explanations (an always-on one-liner expands to a 12 sentence what-it-does / when-to-use blurb, condensed from each Han skill's own description), with a "read-only" pill pinned in the launcher and the fast/concise toggle wired through to the workers. Spec/plan in `openspec/changes/orchestrator`; coder 373 tests green (42 new scheduler/resume/read-only decision tests), contracts/coder/server builds + web tsc clean. Built on `v2.7.16-container-git-safedir`; pairs conceptually with the earlier `v2.7.12-audit-cleanup` multi-agent orchestration. Brings the deterministic multi-agent "conductor" into the app as the **Orchestrator**: launch any read-only Han flow (research, code-review, investigate, architectural-analysis, security-review, …) from BooChat or BooCoder and watch each specialist agent stream live in a Paseo-style run pane, ending with an evidence-disciplined, adversarially-validated report — all on free local Qwen, persisted and resumable. Built and audited end-to-end via `paseo-epic` in an isolated worktree, on top of the prior `/opt/boocode/conductor` standalone CLI: the conductor's 22 flow definitions, Spine factory, and Han evidence/YAGNI contracts were re-homed into `apps/coder/src/conductor`, and a new DB-backed flow-runner (`flow_runs`/`flow_steps`) dispatches each step as a real BooCoder task through the existing dispatcher — reusing its streaming→WS-frame pipeline and worktree-as-read-snapshot, with an `onTaskTerminal` hook that advances the wave and a startup resume that re-dispatches in-flight steps after a coder restart. Read-only is enforced hard: every step is dispatched `qwen --approval-mode plan`, an adversarial-security review caught and closed a bypass where a qwen-unavailable task silently fell through to write-capable native inference (now fails closed), and the ACP path's mode-set was made fail-closed too. The UI adds a fourth `orchestrator` pane kind (collapsed agent roster, expand-one live stream, report on top), a Workflow button + slash flows on the shared `ChatInput` for full BooChat/BooCoder parity, a "New Orchestrator" entry in the + and split menus, a category-grouped launcher dialog, runs history, and export (copy / save-to-file / send-to-chat) — fed by two new `flow_run_*` WS frames on a coder user channel. Qwen-only by design (Claude Code remains the Claude path); the existing model-competition Arena stays a separate feature. The flow launcher and the `/` slash menu both carry chevron-expandable per-item explanations (an always-on one-liner expands to a 12 sentence what-it-does / when-to-use blurb, condensed from each Han skill's own description), with a "read-only" pill pinned in the launcher and the fast/concise toggle wired through to the workers. Spec/plan in `openspec/changes/orchestrator`; coder 373 tests green (42 new scheduler/resume/read-only decision tests), contracts/coder/server builds + web tsc clean. Built on `v2.7.16-container-git-safedir`; pairs conceptually with the earlier `v2.7.12-audit-cleanup` multi-agent orchestration.

View File

@@ -74,7 +74,7 @@ Schema CHECK migration order when renaming allowed values: (1) `ALTER TABLE ...
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only add-existing scope), `BOOTSTRAP_ROOT` (/opt/projects, writable bootstrap mkdir target — host must `mkdir -p` it before container start), `DEFAULT_MODEL`, `LOG_LEVEL`, `SEARXNG_URL` (default `http://100.114.205.53:8888` — internal Tailscale; the public host is behind Authelia, unusable from server context), `BOOCODE_TOOLS` (`core`|`standard`|`all`, default `all`; a ceiling, never expands an agent's whitelist), `MCP_CONFIG_PATH` (default `/data/mcp.json`, opencode `mcpServers` shape; missing = no MCP), `CONTEXT7_API_KEY` (the Context7 MCP key, referenced from `data/mcp.json` as `"{env:CONTEXT7_API_KEY}"`). `data/mcp.json` is **gitignored** but no longer holds secrets — string values support opencode-style `{env:VAR}` substitution (`mcp-config.ts:substituteEnvVars`, applied before Zod validation; unset var → `''` + warn), so real keys live in `.env`; template `data/mcp.example.json`. A config-only edit there needs only `docker compose restart boocode` (data/ is bind-mounted); changing a referenced secret edits `.env`. MCP loads at server startup with per-server graceful degradation; the coder does NOT load MCP (BooChat only). Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only add-existing scope), `BOOTSTRAP_ROOT` (/opt/projects, writable bootstrap mkdir target — host must `mkdir -p` it before container start), `DEFAULT_MODEL`, `LOG_LEVEL`, `SEARXNG_URL` (default `http://100.114.205.53:8888` — internal Tailscale; the public host is behind Authelia, unusable from server context), `BOOCODE_TOOLS` (`core`|`standard`|`all`, default `all`; a ceiling, never expands an agent's whitelist), `MCP_CONFIG_PATH` (default `/data/mcp.json`, opencode `mcpServers` shape; missing = no MCP), `CONTEXT7_API_KEY` (the Context7 MCP key, referenced from `data/mcp.json` as `"{env:CONTEXT7_API_KEY}"`). `data/mcp.json` is **gitignored** but no longer holds secrets — string values support opencode-style `{env:VAR}` substitution (`mcp-config.ts:substituteEnvVars`, applied before Zod validation; unset var → `''` + warn), so real keys live in `.env`; template `data/mcp.example.json`. A config-only edit there needs only `docker compose restart boocode` (data/ is bind-mounted); changing a referenced secret edits `.env`. MCP loads at server startup with per-server graceful degradation; the coder does NOT load MCP (BooChat only).
BooCoder at port 9502: `curl http://100.114.205.53:9502/api/health`. Runs as `boocoder.service` on the host (not Docker). Deploy: `pnpm -C packages/contracts build && pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder`. Health reports tool count: `{"ok":true,"db":true,"tools":33}`. BooCoder at port 9502: `curl http://100.114.205.53:9502/api/health`. Runs as `boocoder.service` on the host (not Docker). Its env file `apps/coder/.env.host` is gitignored (`.env.*`, with `!.env.example`) — a fresh host recreates it from `.env.example` (incl. `CLAUDE_SDK_BACKEND=1` for the Claude Agent-SDK backend). Deploy: `pnpm -C packages/contracts build && pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder`. Health reports tool count: `{"ok":true,"db":true,"tools":33}`.
- `FAST_MODEL` (optional) — cheaper model for titles, summaries, labeling (auto_name.ts, tool-summaries.ts). Falls back to session model or DEFAULT_MODEL. Set to a small llama-swap model (e.g. `nemotron-nano-4b`) to avoid loading the 35B for 20-token calls. - `FAST_MODEL` (optional) — cheaper model for titles, summaries, labeling (auto_name.ts, tool-summaries.ts). Falls back to session model or DEFAULT_MODEL. Set to a small llama-swap model (e.g. `nemotron-nano-4b`) to avoid loading the 35B for 20-token calls.
- Qwen Code dispatch: `OPENAI_BASE_URL=http://100.101.41.16:8401/v1 OPENAI_API_KEY=dummy qwen -p "<task>" --output-format stream-json`. Install: `npm install -g @qwen-code/qwen-code@latest`. Node ≥22 on host (container stays Node 20; BooCoder dispatches via direct spawn on host). No `--yolo` flag — `-p` runs autonomously without prompts. ACP bridge is an HTTP daemon (not stdio); use PTY dispatch. - Qwen Code dispatch: `OPENAI_BASE_URL=http://100.101.41.16:8401/v1 OPENAI_API_KEY=dummy qwen -p "<task>" --output-format stream-json`. Install: `npm install -g @qwen-code/qwen-code@latest`. Node ≥22 on host (container stays Node 20; BooCoder dispatches via direct spawn on host). No `--yolo` flag — `-p` runs autonomously without prompts. ACP bridge is an HTTP daemon (not stdio); use PTY dispatch.

View File

@@ -1,9 +1,9 @@
# Current focus # Current focus
Last updated: 2026-06-02 Last updated: 2026-06-05
- **Last shipped:** `v2.7.8-ember-coder-tabs-model-chips` (2026-06-01) - **Last shipped:** `v2.7.18-permission-modes` (2026-06-05) — unified Plan/Ask/Bypass permission picker in the BooCoder composer (incl. native-BooCode auto-apply on Bypass).
- **Branch:** `codebase-audit-cleanup` (audit + cleanup epic, off main HEAD) - **Branch:** `main`
- **In progress:** Phase 3 — stale comments + docs refresh - **In progress:** nothing committed — dogfooding the Orchestrator to surface the next real backlog. Claude Agent-SDK backend enabled (`CLAUDE_SDK_BACKEND`). Optional/exploratory: verify-gate ensembler over pending changes.
See `CHANGELOG.md` for the full shipped history. That file is always authoritative; this file is a quick orientation pointer only. See `CHANGELOG.md` for the full shipped history. That file is always authoritative; this file is a quick orientation pointer only.

View File

@@ -1,10 +1,10 @@
# boocode # boocode
Self-hosted single-user developer chat app. 3-app monorepo: BooChat (read-only chat), BooCoder (write tools + agent dispatch), BooTerm (PTY terminals). Self-hosted single-user developer chat app. 3-app monorepo: BooChat (read-only chat), BooCoder (write tools + agent dispatch), BooTerm (PTY terminals) — plus the in-app **Orchestrator**, a deterministic multi-agent conductor that runs read-only Han analysis/review flows on local Qwen.
**Latest release:** `v2.2.1-pane-scoped-chats` (2026-05-26) · [`CHANGELOG.md`](CHANGELOG.md) · **Current focus:** [`CURRENT.md`](CURRENT.md) **Latest release:** `v2.7.17-orchestrator` (2026-06-03) · [`CHANGELOG.md`](CHANGELOG.md) · **Current focus:** [`CURRENT.md`](CURRENT.md)
**Agent navigation:** [`AGENTS.md`](AGENTS.md) · **Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) · **Engineering reference:** [`CLAUDE.md`](CLAUDE.md) **Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) · **Engineering reference:** [`CLAUDE.md`](CLAUDE.md) · **Roadmap:** [`boocode_roadmap.md`](boocode_roadmap.md)
## Stack ## Stack
@@ -75,15 +75,16 @@ curl http://100.114.205.53:9502/api/health
## What's shipped ## What's shipped
See [`boocode_roadmap.md`](boocode_roadmap.md) for full version history. Highlights as of **v2.2.1**: See [`boocode_roadmap.md`](boocode_roadmap.md) and [`CHANGELOG.md`](CHANGELOG.md) for full version history. Highlights as of **v2.7.17**:
- **BooChat**: streaming chat, file-read tools, compaction, reasoning support, HTML/Markdown artifact panes, cross-repo read grants, MCP client (multi-server + stdio), tool-cost tracking, skills system, builtin agent registry, multi-pane workspace (chat / terminal / coder) - **BooChat**: streaming chat, file-read tools, compaction, reasoning support, HTML/Markdown artifact panes, cross-repo read grants, MCP client (multi-server + stdio), tool-cost tracking, skills system, builtin agent registry, multi-pane workspace (chat / terminal / coder / orchestrator)
- **BooTerm**: in-browser terminal panes via tmux + xterm.js, per-session tmux sessions, SSH-out support - **BooTerm**: in-browser terminal panes via tmux + xterm.js, per-session tmux sessions, SSH-out support
- **BooCoder (v2.2)**: write tools (`edit_file`, `create_file`, `delete_file`, `apply_pending`, `rewind`), pending-changes queue with diff UI, Paseo-style provider snapshot (7 providers: boocode, cursor, claude, opencode, goose, qwen, copilot), `AgentComposerBar` (provider / mode / model / thinking), ACP dispatch with inline permission prompts + tool/reasoning streaming, PTY fallback, Arena, MCP server (6 tools, stdio), CLI client, human inbox, Boomerang orchestration, path-guard fuzz suite, **pane-scoped chats** (v2.2.1 — each coder/terminal pane owns its chat) - **BooCoder**: write tools (`edit_file` with fuzzy matching, `create_file`, `delete_file`, `apply_pending`, `rewind`, git-ref checkpoints), pending-changes queue + a **Files/Git diff panel** (stage / commit / discard), provider snapshot (5 providers: boocode, claude, opencode, goose, qwen — cursor/copilot retired), `AgentComposerBar`, warm ACP + **persistent agent sessions** (opencode HTTP server; claude via the Agent SDK with native session resume) + PTY fallback, config-backed provider lifecycle, Arena (same task → N models), MCP server, CLI client, human inbox, Boomerang orchestration, pane-scoped chats
- **Orchestrator** (v2.7.17): launch any of 22 read-only Han flows (research, code-review, investigate, architectural-analysis, …) from BooChat or BooCoder via the Workflow button, a slash command, or **+ menu → New Orchestrator**; each step runs as a bounded agent on local Qwen (hard read-only via `qwen --approval-mode plan`), streaming live in a Paseo-style run pane with an evidence-disciplined, adversarially-validated report. Persisted + resumable. `@boocode/contracts` single-sources the cross-app wire contracts (v2.7.13).
## Planned ## Planned
- **v2.3 provider lifecycle** — config-backed provider registry (`/data/coder-providers.json`), enable/disable toggles, two-tier probe (openspec drafted). See [`CURRENT.md`](CURRENT.md). Most prior roadmap milestones have shipped (see [`boocode_roadmap.md`](boocode_roadmap.md)). What remains is optional/exploratory — e.g. a verify-gate ensembler over pending changes (majority-vote diff ranking). No committed milestones currently in flight.
## License ## License

View File

@@ -1,17 +0,0 @@
NODE_ENV=production
PORT=9502
HOST=100.114.205.53
DATABASE_URL=postgres://boocode:devpass@127.0.0.1:5500/boochat
LLAMA_SWAP_URL=http://100.101.41.16:8401
PROJECT_ROOT_WHITELIST=/opt
BOOTSTRAP_ROOT=/opt/projects
DEFAULT_MODEL=qwen3.6-35b-a3b-mxfp4
LOG_LEVEL=info
SEARXNG_URL=http://100.114.205.53:8888
GITEA_BASE_URL=https://git.indifferentketchup.com
GITEA_USER=indifferentketchup
GITEA_SSH_HOST=100.114.205.53:2222
MCP_CONFIG_PATH=/data/mcp.json
SKILLS_ROOT=/opt/boocode/data/skills
CODER_PROVIDERS_PATH=/opt/boocode/data/coder-providers.json
CLAUDE_SDK_BACKEND=1

View File

@@ -32,3 +32,8 @@
- **Claude SDK backend tool RESULTS arrive as `type:'user'` SDK messages** (tool_result content blocks): `mapSdkMessage` (`claude-sdk-map.ts`) MUST map the `user` case → a terminal `tool_update` (completed/failed + output), else the tool_call persists `status:'running'` and the UI spinner never stops. The dispatcher's `tool_update` path then publishes + persists it. - **Claude SDK backend tool RESULTS arrive as `type:'user'` SDK messages** (tool_result content blocks): `mapSdkMessage` (`claude-sdk-map.ts`) MUST map the `user` case → a terminal `tool_update` (completed/failed + output), else the tool_call persists `status:'running'` and the UI spinner never stops. The dispatcher's `tool_update` path then publishes + persists it.
- **ACP command discovery is async**: `acp-probe.ts` must poll after `newSession` for `available_commands_update` (commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk via `claude-command-discovery.ts` (`~/.claude/commands` + `enabledPlugins`, bare names, deduped). `AgentCommand.kind` tags `'command'` vs `'skill'`; `CoderPane`'s `slashGroups` splits them into icon'd groups. `SlashCommandPicker`'s `groups?` prop is opt-in. - **ACP command discovery is async**: `acp-probe.ts` must poll after `newSession` for `available_commands_update` (commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk via `claude-command-discovery.ts` (`~/.claude/commands` + `enabledPlugins`, bare names, deduped). `AgentCommand.kind` tags `'command'` vs `'skill'`; `CoderPane`'s `slashGroups` splits them into icon'd groups. `SlashCommandPicker`'s `groups?` prop is opt-in.
- **A new per-message coder field silently drops unless you update every mapper**: the HTTP read SELECT + `mapCoderMessageRow` (`apps/coder/src/routes/messages.ts`), **the WS `snapshot` SELECT (`apps/coder/src/routes/ws.ts`)** — it has its OWN column list and the client's `snapshot` handler `setMessages`-overwrites the HTTP load, so a field present in the HTTP route but absent here shows live yet vanishes on refresh — `CoderPane.tsx` (`RawCoderMessage`/`CoderMessage`/`mapCoderTimelineRow` + the live `message_complete` WS reducer), `CoderMessageWire` (`CoderMessageList.tsx`), and `api/types.ts`. The client `mapCoderTimelineRow` whitelists fields — easiest to forget. This bit `model` twice: the client chain (`v2.7.9`) and then the WS snapshot SELECT (`v2.7.11`) — the chip showed live but vanished on coder refresh until both were fixed. - **A new per-message coder field silently drops unless you update every mapper**: the HTTP read SELECT + `mapCoderMessageRow` (`apps/coder/src/routes/messages.ts`), **the WS `snapshot` SELECT (`apps/coder/src/routes/ws.ts`)** — it has its OWN column list and the client's `snapshot` handler `setMessages`-overwrites the HTTP load, so a field present in the HTTP route but absent here shows live yet vanishes on refresh — `CoderPane.tsx` (`RawCoderMessage`/`CoderMessage`/`mapCoderTimelineRow` + the live `message_complete` WS reducer), `CoderMessageWire` (`CoderMessageList.tsx`), and `api/types.ts`. The client `mapCoderTimelineRow` whitelists fields — easiest to forget. This bit `model` twice: the client chain (`v2.7.9`) and then the WS snapshot SELECT (`v2.7.11`) — the chip showed live but vanished on coder refresh until both were fixed.
## Orchestrator (v2.7.17)
- **In-app multi-agent conductor**: `services/flow-runner.ts` runs a flow by inserting each step as a `tasks` row (the existing dispatcher runs it) and advancing on a new `onTaskTerminal` dispatcher-deps hook; persisted in `flow_runs`/`flow_steps` (resumed at startup via `initResume`). The 22 conductor flow defs + Spine factory are re-homed under `src/conductor/`. Pure scheduler/resume helpers in `flow-runner-decisions.ts`. Full design: `openspec/changes/archived/orchestrator/`.
- **Read-only is load-bearing — don't add a dispatch path that bypasses it.** Every step dispatches `agent='qwen', mode_id='plan'`; `dispatcher.ts` force-routes qwen+plan to the PTY `--approval-mode plan` gate and HARD-FAILS the task (never falls to write-capable native inference) when qwen is unavailable (`shouldFailOnMissingAgent`). `BOOCODE_TOOLS` gates BooChat's NATIVE inference tools only — it does NOT govern an external CLI agent (qwen/opencode bring their own write tools); read-only for a dispatched agent is the agent-layer mode (PTY `--approval-mode plan`; ACP `setSessionMode` is fail-OPEN by default, fail-CLOSED for `plan` via `READ_ONLY_MODE_IDS` in `acp-dispatch.ts`).

View File

@@ -4,6 +4,7 @@ import type { Sql } from '../db.js';
import type { Broker } from '@boocode/server/broker'; import type { Broker } from '@boocode/server/broker';
import type { WsFrame } from '@boocode/contracts/ws-frames'; import type { WsFrame } from '@boocode/contracts/ws-frames';
import { resolveChatId } from './chat-resolve.js'; import { resolveChatId } from './chat-resolve.js';
import { applyAll } from '../services/pending_changes.js';
const AnswerUserInputBody = z.object({ const AnswerUserInputBody = z.object({
tool_call_id: z.string().min(1), tool_call_id: z.string().min(1),
@@ -247,6 +248,35 @@ export function registerMessageRoutes(
inference.enqueue(sessionId, chatId, assistantMsg!.id, 'default'); inference.enqueue(sessionId, chatId, assistantMsg!.id, 'default');
// Bypass permission mode (native BooCode): auto-apply staged edits to disk
// once the turn settles. `enqueue` registers synchronously, so hasActive is
// true immediately; poll until it clears, apply, then re-publish
// message_complete so the DiffPanel reflects the now-applied (non-pending)
// state. Best-effort — failures stay in the pending queue for manual apply.
if (mode_id === 'bypass') {
const projectId = sessionRows[0]!.project_id;
const assistantId = assistantMsg!.id;
void (async () => {
try {
const [proj] = await sql<{ path: string }[]>`SELECT path FROM projects WHERE id = ${projectId}`;
if (!proj?.path) return;
for (let i = 0; i < 1200 && inference.hasActive(chatId); i++) {
await new Promise((r) => setTimeout(r, 1000));
}
const applied = await applyAll(sql, sessionId, proj.path);
if (applied.length > 0) {
broker.publishFrame(sessionId, {
type: 'message_complete',
message_id: assistantId,
chat_id: chatId,
} as unknown as WsFrame);
}
} catch {
/* best-effort auto-apply — leave staged changes for manual apply */
}
})();
}
reply.code(202); reply.code(202);
return { user_message_id: userMsg!.id, assistant_message_id: assistantMsg!.id }; return { user_message_id: userMsg!.id, assistant_message_id: assistantMsg!.id };
}, },

View File

@@ -68,11 +68,18 @@ export function deriveModesFromACP(
): { modes: ProviderMode[]; currentModeId: string | null } { ): { modes: ProviderMode[]; currentModeId: string | null } {
if (modeState?.availableModes?.length) { if (modeState?.availableModes?.length) {
return { return {
modes: modeState.availableModes.map((mode) => ({ // ACP omits the unattended flag; inherit it from the manifest fallback by
id: mode.id, // id so the unified permission picker can still detect each agent's bypass
label: mode.name, // mode (e.g. opencode `full-access`) from live-probed modes.
description: mode.description ?? undefined, modes: modeState.availableModes.map((mode) => {
})), const fb = fallbackModes.find((f) => f.id === mode.id);
return {
id: mode.id,
label: mode.name,
description: mode.description ?? undefined,
...(fb?.isUnattended ? { isUnattended: true } : {}),
};
}),
currentModeId: modeState.currentModeId ?? null, currentModeId: modeState.currentModeId ?? null,
}; };
} }

View File

@@ -12,12 +12,48 @@ import { homedir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import type { AgentCommand } from './provider-types.js'; import type { AgentCommand } from './provider-types.js';
/** Minimal frontmatter reader — single-line `key: value` between `---` fences. */ /**
* Frontmatter reader between `---` fences. Handles single-line `key: value`
* AND YAML block scalars (`key: >` folded / `key: |` literal) whose value
* spans the following more-indented lines — the shape most plugin SKILL.md
* descriptions use (`description: >`).
*/
function frontmatterField(content: string, field: string): string | undefined { function frontmatterField(content: string, field: string): string | undefined {
const block = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); const block = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!block?.[1]) return undefined; if (!block?.[1]) return undefined;
const m = block[1].match(new RegExp(`^${field}:\\s*(.+)$`, 'm')); const lines = block[1].split(/\r?\n/);
return m?.[1]?.trim().replace(/^["']|["']$/g, '') || undefined; const keyRe = new RegExp(`^(\\s*)${field}:\\s*(.*)$`);
for (let i = 0; i < lines.length; i++) {
const m = lines[i]?.match(keyRe);
if (!m) continue;
const keyIndent = (m[1] ?? '').length;
const inline = (m[2] ?? '').trim();
// Block scalar: `>` (folded) or `|` (literal), optional chomping `+`/`-`.
if (/^[>|][+-]?$/.test(inline)) {
const folded = inline[0] === '>';
const body: string[] = [];
for (let j = i + 1; j < lines.length; j++) {
const line = lines[j] ?? '';
if (line.trim() === '') {
body.push('');
continue;
}
const indent = line.length - line.trimStart().length;
if (indent <= keyIndent) break; // dedent ends the block
body.push(line.slice(keyIndent + 1));
}
const joined = folded
? body
.map((l) => l.trim())
.join(' ')
.replace(/\s+/g, ' ')
.trim()
: body.join('\n').replace(/\n+$/, '');
return joined || undefined;
}
return inline.replace(/^["']|["']$/g, '').trim() || undefined;
}
return undefined;
} }
function readCommandDir(dir: string): AgentCommand[] { function readCommandDir(dir: string): AgentCommand[] {

View File

@@ -4,6 +4,7 @@ import type { Broker } from '@boocode/server/broker';
import type { WsFrame } from '@boocode/contracts/ws-frames'; import type { WsFrame } from '@boocode/contracts/ws-frames';
import type { Config } from '../config.js'; import type { Config } from '../config.js';
import { createWorktree, diffWorktree, cleanupWorktree, ensureSessionWorktree } from './worktrees.js'; import { createWorktree, diffWorktree, cleanupWorktree, ensureSessionWorktree } from './worktrees.js';
import { applyAll } from './pending_changes.js';
import { createCheckpoint } from './checkpoints.js'; import { createCheckpoint } from './checkpoints.js';
import { makeDcpStreamStripper } from './dcp-strip.js'; import { makeDcpStreamStripper } from './dcp-strip.js';
import { dispatchViaAcp } from './acp-dispatch.js'; import { dispatchViaAcp } from './acp-dispatch.js';
@@ -305,7 +306,7 @@ export function createDispatcher(deps: Deps): {
// ─── Path A: Native Inference ─────────────────────────────────────────────── // ─── Path A: Native Inference ───────────────────────────────────────────────
async function runNativeInference(task: { id: string; project_id: string; input: string; agent: string | null; model: string | null; session_id: string | null }): Promise<void> { async function runNativeInference(task: { id: string; project_id: string; input: string; agent: string | null; model: string | null; mode_id: string | null; session_id: string | null }): Promise<void> {
const taskId = task.id; const taskId = task.id;
log.info({ taskId }, 'dispatcher: starting task (path A — native)'); log.info({ taskId }, 'dispatcher: starting task (path A — native)');
@@ -385,6 +386,22 @@ export function createDispatcher(deps: Deps): {
WHERE id = ${taskId} WHERE id = ${taskId}
`; `;
log.info({ taskId, costTokens }, 'dispatcher: task completed (native)'); log.info({ taskId, costTokens }, 'dispatcher: task completed (native)');
// Bypass permission mode: auto-apply the staged edits to disk after the
// turn. Ask/Plan leave them in the pending-changes queue for review.
if (task.mode_id === 'bypass') {
try {
const [proj] = await sql<{ path: string }[]>`SELECT path FROM projects WHERE id = ${task.project_id}`;
if (proj?.path) {
const applied = await applyAll(sql, sessionId, proj.path);
log.info({ taskId, applied: applied.length }, 'dispatcher: native bypass auto-applied pending changes');
}
} catch (applyErr) {
log.warn(
{ taskId, err: applyErr instanceof Error ? applyErr.message : String(applyErr) },
'dispatcher: native bypass auto-apply failed',
);
}
}
} else { } else {
const [msg] = await sql<{ content: string | null }[]>` const [msg] = await sql<{ content: string | null }[]>`
SELECT content FROM messages WHERE id = ${assistantId} SELECT content FROM messages WHERE id = ${assistantId}

View File

@@ -32,6 +32,18 @@ const QWEN_PTY_MODES: ProviderMode[] = [
{ id: 'yolo', label: 'YOLO', description: 'Auto-approve all tools', isUnattended: true }, { id: 'yolo', label: 'YOLO', description: 'Auto-approve all tools', isUnattended: true },
]; ];
// Native BooCode (llama-swap) has no agent-native mode vocabulary, so we define
// one that matches the unified permission ladder. `bypass` is the only mode that
// changes behavior (auto-apply staged edits after the turn — dispatcher.ts);
// `plan` falls back to `ask` semantics for native (writes still stage to the
// pending-changes queue). External agents map the same three unified modes onto
// THEIR native ids via the `plan`-id / default / `isUnattended` shape.
const BOOCODE_MODES: ProviderMode[] = [
{ id: 'plan', label: 'Plan', description: 'Read-only analysis (native BooCode falls back to Ask)' },
{ id: 'ask', label: 'Ask Permission', description: 'Stage edits to the pending-changes queue for review' },
{ id: 'bypass', label: 'Bypass', description: 'Auto-apply edits to disk after the turn', isUnattended: true },
];
const CLAUDE_THINKING = [ const CLAUDE_THINKING = [
{ id: 'low', label: 'Low' }, { id: 'low', label: 'Low' },
{ id: 'medium', label: 'Medium' }, { id: 'medium', label: 'Medium' },
@@ -41,6 +53,10 @@ const CLAUDE_THINKING = [
]; ];
export const PROVIDER_MANIFEST: Record<string, ProviderManifestEntry> = { export const PROVIDER_MANIFEST: Record<string, ProviderManifestEntry> = {
boocode: {
defaultModeId: 'ask',
modes: BOOCODE_MODES,
},
claude: { claude: {
defaultModeId: 'default', defaultModeId: 'default',
modes: CLAUDE_MODES, modes: CLAUDE_MODES,

View File

@@ -122,12 +122,14 @@ async function buildProviderEntry(
}; };
} }
// 2. Native boocode → always ready (llama-swap models). // 2. Native boocode → always ready (llama-swap models). Exposes the unified
// permission modes (plan/ask/bypass) so the composer's permission picker works
// for native BooCode too; `bypass` auto-applies staged edits (dispatcher.ts).
if (isNative) { if (isNative) {
return { return {
name, label: resolved.label, transport, status: 'ready', name, label: resolved.label, transport, status: 'ready',
enabled: true, installed: true, models: withConfigModels(llamaModels), modes: [], enabled: true, installed: true, models: withConfigModels(llamaModels),
defaultModeId: null, commands: manifestCommands, modes: fallbackModes, defaultModeId, commands: manifestCommands,
}; };
} }

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Check, ChevronDown, RefreshCw, Loader2, Shield, Brain, Bot } from 'lucide-react'; import { Check, ChevronDown, RefreshCw, Loader2, Shield, ShieldAlert, Eye, Brain, Bot } from 'lucide-react';
import { api } from '@/api/client'; import { api } from '@/api/client';
import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types'; import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types';
import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot'; import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot';
@@ -14,8 +14,22 @@ import {
import { BottomSheet } from '@/components/BottomSheet'; import { BottomSheet } from '@/components/BottomSheet';
import { useViewport } from '@/hooks/useViewport'; import { useViewport } from '@/hooks/useViewport';
import { formatModelLabel } from '@/lib/model-label'; import { formatModelLabel } from '@/lib/model-label';
import {
availablePermissionModes,
permissionForModeId,
nativeModeForPermission,
type PermissionMode,
} from '@/lib/permission-mode';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// Permission picker icon — varies with the active mode so the (icon-only) control
// is glanceable: Eye = Plan (read-only), Shield = Ask, ShieldAlert = Bypass.
function permissionIcon(mode: PermissionMode): React.ReactNode {
if (mode === 'plan') return <Eye className="size-3 shrink-0" />;
if (mode === 'bypass') return <ShieldAlert className="size-3 shrink-0 text-amber-500" />;
return <Shield className="size-3 shrink-0" />;
}
const PREFS_KEY = 'boocode.coder.agent-prefs'; const PREFS_KEY = 'boocode.coder.agent-prefs';
@@ -350,7 +364,11 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
} }
const providerOptions = entries.map((e) => ({ id: e.name, label: e.label })); const providerOptions = entries.map((e) => ({ id: e.name, label: e.label }));
const modeOptions = (currentEntry?.modes ?? []).map((m) => ({ id: m.id, label: m.label })); // Unified permission ladder (Plan / Ask / Bypass) mapped onto this provider's
// native modes. `value.modeId` stays the wire field; the active unified mode is
// derived from it.
const permissionModes = availablePermissionModes(currentEntry?.modes ?? []);
const currentPermission = permissionForModeId(value.modeId, currentEntry?.modes ?? []);
const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: formatModelLabel(m.label) })); const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: formatModelLabel(m.label) }));
const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label })); const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label }));
@@ -380,15 +398,25 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
</> </>
} }
/> />
{/* Mode (shield) only when the provider actually exposes modes. Native {/* Permission ladder (Plan / Ask / Bypass) — shown when the provider exposes
BooCoder has none, so it's hidden rather than shown disabled. */} modes. Picks the unified mode; we resolve it to the provider's native
{modeOptions.length > 0 && ( modeId. Icon varies with the active mode (Bypass is amber). */}
{permissionModes.length > 0 && (
<CompactPicker <CompactPicker
label="Mode" label="Permission"
value={value.modeId ?? ''} value={currentPermission}
options={modeOptions} options={permissionModes}
onPick={(modeId) => persist({ ...value, modeId })} onPick={(perm) =>
icon={<Shield className="size-3 shrink-0" />} persist({
...value,
modeId: nativeModeForPermission(
perm as PermissionMode,
currentEntry?.modes ?? [],
currentEntry?.defaultModeId ?? null,
),
})
}
icon={permissionIcon(currentPermission)}
iconOnly iconOnly
/> />
)} )}

View File

@@ -225,7 +225,7 @@ export function SlashCommandPicker({
setHighlightIndex(i); setHighlightIndex(i);
setExpandedIndex((prev) => (prev === i ? null : i)); setExpandedIndex((prev) => (prev === i ? null : i));
}} }}
className="-mr-1 -mt-0.5 flex shrink-0 items-center justify-center rounded p-1 text-muted-foreground/60 transition-colors hover:bg-foreground/10 hover:text-foreground max-md:min-h-[36px] max-md:min-w-[36px]" className="-mr-1 -mt-0.5 flex shrink-0 items-center justify-center rounded-md border border-border bg-background p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground max-md:min-h-[36px] max-md:min-w-[36px]"
> >
<ChevronRight <ChevronRight
className={cn( className={cn(

View File

@@ -0,0 +1,55 @@
// Unified permission ladder shown in the composer's permission picker. Maps a
// curated three-option control (Plan / Ask Permission / Bypass) onto each
// provider's native mode vocabulary, derived purely from the snapshot's mode
// metadata (`plan` id, the default mode, and the `isUnattended` bypass mode).
// `modeId` stays the single wire field sent to the dispatcher — there is no
// separate persisted permission field; the active unified mode is derived from
// the current `modeId`.
import type { ProviderMode } from '@/api/types';
export type PermissionMode = 'plan' | 'ask' | 'bypass';
export const PERMISSION_LABELS: Record<PermissionMode, string> = {
plan: 'Plan',
ask: 'Ask Permission',
bypass: 'Bypass',
};
/** The native modeId for a unified permission, or null when the provider has no
* modes (e.g. goose). `plan` → the `plan`-id mode; `bypass` → the `isUnattended`
* mode; `ask` → the non-unattended default. Falls back to defaultModeId. */
export function nativeModeForPermission(
mode: PermissionMode,
modes: ProviderMode[],
defaultModeId: string | null,
): string | null {
if (modes.length === 0) return null;
if (mode === 'plan') return modes.find((m) => m.id === 'plan')?.id ?? defaultModeId;
if (mode === 'bypass') return modes.find((m) => m.isUnattended)?.id ?? defaultModeId;
return (
modes.find((m) => m.id === defaultModeId && !m.isUnattended)?.id ??
modes.find((m) => !m.isUnattended && m.id !== 'plan')?.id ??
defaultModeId
);
}
/** Which unified permission a native modeId corresponds to (for picker state). */
export function permissionForModeId(modeId: string | null, modes: ProviderMode[]): PermissionMode {
if (!modeId) return 'ask';
if (modeId === 'plan') return 'plan';
if (modes.find((m) => m.id === modeId)?.isUnattended) return 'bypass';
return 'ask';
}
/** The unified permission options a provider supports, in fixed Plan→Ask→Bypass
* order. Empty when the provider exposes no modes (no picker shown). */
export function availablePermissionModes(
modes: ProviderMode[],
): Array<{ id: PermissionMode; label: string }> {
if (modes.length === 0) return [];
const out: Array<{ id: PermissionMode; label: string }> = [];
if (modes.some((m) => m.id === 'plan')) out.push({ id: 'plan', label: PERMISSION_LABELS.plan });
out.push({ id: 'ask', label: PERMISSION_LABELS.ask });
if (modes.some((m) => m.isUnattended)) out.push({ id: 'bypass', label: PERMISSION_LABELS.bypass });
return out;
}

View File

@@ -1,9 +1,11 @@
# BooCode roadmap (v1.xv2.x) # BooCode roadmap (v1.xv2.x)
Last updated: 2026-05-31 Last updated: 2026-06-03
> **Companion doc:** `boocode_code_review.md` holds the full external-repo inventory, lift rationale, and license analysis. This document is the canonical source for shipping state, version ordering, and what's planned vs. shipped. > **Companion doc:** `boocode_code_review.md` holds the full external-repo inventory, lift rationale, and license analysis. This document is the canonical source for shipping state, version ordering, and what's planned vs. shipped.
> **Shipped since this doc's body was written (v2.7.12v2.7.17, 2026-06-02→03; see `CHANGELOG.md` for detail):** `v2.7.12-audit-cleanup` (repo-wide dead-code/dedup pass, ~4,600 LOC), `v2.7.13-contracts-ssot` (the `@boocode/contracts` shared wire-contract package — the "unified types" deferred item), `v2.7.14-backlog-hardening` (5 v2-review items incl. external task-cancel, stall-timeout, retire `:9502` SPA), `v2.7.15-git-diff-panel` + `v2.7.16-container-git-safedir` (Files/Git tab), and `v2.7.17-orchestrator` (the in-app multi-agent Orchestrator on local Qwen). The "Write/edit robustness" and "Claude provider SDK" milestones below — previously marked "planned" — are also now shipped (see those sections).
## Overview ## Overview
BooCode is a **3-app monorepo** at `/opt/boocode/` (locked 2026-05-22): BooCode is a **3-app monorepo** at `/opt/boocode/` (locked 2026-05-22):
@@ -452,9 +454,9 @@ The original plan (kept for record): expose `boocoder acp` (JSON-RPC over stdio)
----- -----
## Write/edit robustness (planned) ## Write/edit robustness — SHIPPED
**Status: planned, not started.** From the v2 review (`boocode_code_review_v2.md` §5b; `cline/cline`, Apache-2.0 — code-liftable). Two lifts that harden BooCoder's write surface where it's weakest for local quantized models: **Status: SHIPPED (by v2.7.x).** Both lifts are live: the fuzzy patch applier (`apps/coder/src/services/fuzzy-match.ts`, consumed by `pending_changes.ts``edit_file` is no longer exact-match) and the `git`-ref checkpoint snapshot (`apps/coder/src/services/checkpoints.ts``createCheckpoint`, private `refs/boocode/checkpoints/<id>` ref). The original "planned" note below is retained for provenance. From the v2 review (`boocode_code_review_v2.md` §5b; `cline/cline`, Apache-2.0 — code-liftable). Two lifts that harden BooCoder's write surface where it's weakest for local quantized models:
1. **Fuzzy patch applier for `edit_file`.** BooCoder's `edit_file` is exact-match today (`apps/coder/src/services/pending_changes.ts``if (!content.includes(oldStr)) throw`; no whitespace/unicode tolerance, no multi-occurrence guard). Lift cline's tiered match ladder (exact → `trimEnd``trim` → Levenshtein ≥0.66) + unicode canonicalization (dashes, curly quotes, nbsp) + multi-occurrence guard; unmatched → warning, not throw. `apply-patch-parser.ts:347-431`. 1. **Fuzzy patch applier for `edit_file`.** BooCoder's `edit_file` is exact-match today (`apps/coder/src/services/pending_changes.ts``if (!content.includes(oldStr)) throw`; no whitespace/unicode tolerance, no multi-occurrence guard). Lift cline's tiered match ladder (exact → `trimEnd``trim` → Levenshtein ≥0.66) + unicode canonicalization (dashes, curly quotes, nbsp) + multi-occurrence guard; unmatched → warning, not throw. `apply-patch-parser.ts:347-431`.
2. **`git stash create` + private-ref checkpoint.** A per-turn workspace snapshot that captures **all** state — including edits made by dispatched external agents (opencode/claude/qwen/goose), build artifacts, test side-effects — which BooCoder's current `rewind` cannot (it only reverse-applies BooCoder's own queued `pending_changes`). Snapshot stored under a private `refs/…/checkpoints/…` ref, restorable with conversation-trim in sync. `checkpoint-hooks.ts:177-253`. 2. **`git stash create` + private-ref checkpoint.** A per-turn workspace snapshot that captures **all** state — including edits made by dispatched external agents (opencode/claude/qwen/goose), build artifacts, test side-effects — which BooCoder's current `rewind` cannot (it only reverse-applies BooCoder's own queued `pending_changes`). Snapshot stored under a private `refs/…/checkpoints/…` ref, restorable with conversation-trim in sync. `checkpoint-hooks.ts:177-253`.
@@ -463,9 +465,9 @@ The original plan (kept for record): expose `boocoder acp` (JSON-RPC over stdio)
----- -----
## Claude provider — SDK transport + native session resume (planned) ## Claude provider — SDK transport + native session resume — SHIPPED (enabled 2026-06-03)
**Status: planned, not started.** From the v2 review (`boocode_code_review_v2.md` §5h§5i) + a direct read of the published SDK `.d.ts` (`@anthropic-ai/claude-agent-sdk@0.3.158`, reviewed 2026-05-31). Today BooCoder dispatches `claude` one-shot via PTY (`claude --output-format stream-json`) with no continuity. Plan: **Status: BUILT and ENABLED.** The Agent-SDK backend (`apps/coder/src/services/backends/claude-sdk.ts`) and the `PostgresSessionStore` (`claude-session-store.ts`, keyed `(chat_id, agent)`) are implemented; it was shipped behind the `CLAUDE_SDK_BACKEND` env flag (off by default in code) and is **enabled in `apps/coder/.env.host` (`CLAUDE_SDK_BACKEND=1`, confirmed live in the running host service)** — chat-tab `claude` tasks route through the warm SDK backend with native session resume instead of one-shot PTY. The original "planned" note below is retained for provenance. From the v2 review (`boocode_code_review_v2.md` §5h§5i) + a direct read of the published SDK `.d.ts` (`@anthropic-ai/claude-agent-sdk@0.3.158`, reviewed 2026-05-31). Today BooCoder dispatches `claude` one-shot via PTY (`claude --output-format stream-json`) with no continuity. Plan:
1. **Adopt the Agent SDK** (`@anthropic-ai/claude-agent-sdk`) over the PTY path. `query({ prompt, options })` yields structured `SDKMessage`s — `SDKSystemMessage` (`subtype:'init'`, carries the session id + tool/skill/mcp lists), `SDKPartialAssistantMessage` (`type:'stream_event'` deltas), `SDKResultMessage` (turn end) — no stdout scraping. `happy` (`slopus/happy`) is the working existence-proof. 1. **Adopt the Agent SDK** (`@anthropic-ai/claude-agent-sdk`) over the PTY path. `query({ prompt, options })` yields structured `SDKMessage`s — `SDKSystemMessage` (`subtype:'init'`, carries the session id + tool/skill/mcp lists), `SDKPartialAssistantMessage` (`type:'stream_event'` deltas), `SDKResultMessage` (turn end) — no stdout scraping. `happy` (`slopus/happy`) is the working existence-proof.
2. **Native session resume via a pluggable `SessionStore`.** Implement `PostgresSessionStore implements SessionStore` (5 methods: `append`/`load`/`listSessions`/`delete`/`listSubkeys`) over BooCode's Postgres, keyed by `(chat_id, agent)`; drive turns with `query({ options: { sessionStore, resume } })` and the SDK materializes the stored session for the CLI subprocess. **This supersedes happy's SessionStart-hook + jsonl-watcher** — that was a workaround predating the feature (happy pins SDK `^0.2.96`; the `SessionStore` API is `0.3.x`). `importSessionToStore()` migrates an existing local session; `InMemorySessionStore` is the reference shape. 2. **Native session resume via a pluggable `SessionStore`.** Implement `PostgresSessionStore implements SessionStore` (5 methods: `append`/`load`/`listSessions`/`delete`/`listSubkeys`) over BooCode's Postgres, keyed by `(chat_id, agent)`; drive turns with `query({ options: { sessionStore, resume } })` and the SDK materializes the stored session for the CLI subprocess. **This supersedes happy's SessionStart-hook + jsonl-watcher** — that was a workaround predating the feature (happy pins SDK `^0.2.96`; the `SessionStore` API is `0.3.x`). `importSessionToStore()` migrates an existing local session; `InMemorySessionStore` is the reference shape.

View File

@@ -1,5 +1,7 @@
# Deferred work — post stale cleanup (2026-05-26) # Deferred work — post stale cleanup (2026-05-26)
> **⚠️ SUPERSEDED (2026-06-03): most items in this doc have since shipped.** Task cancel → abort ACP/PTY child (v2.7.14), unified `packages/types` (v2.7.13 `@boocode/contracts`), retire `apps/coder/web/` fallback SPA (v2.7.14), `console.debug`→pino in the xml-parser (v2.7.14), and the large-file splits (v2.7.12) are all done; the ACP cold-probe skip shipped earlier (v2.3). Treat this doc as historical — see `CHANGELOG.md` (v2.7.12v2.7.17) for what actually shipped. Kept for the design rationale in the detail sections below.
This document describes work intentionally **not** shipped in the 2026-05-26 stale/simplify batch. Each item needs a product or architecture decision before implementation. See also [`STALE-DEPRECATED.md`](./STALE-DEPRECATED.md) for what was resolved in that batch. This document describes work intentionally **not** shipped in the 2026-05-26 stale/simplify batch. Each item needs a product or architecture decision before implementation. See also [`STALE-DEPRECATED.md`](./STALE-DEPRECATED.md) for what was resolved in that batch.
Last updated: 2026-05-29 Last updated: 2026-05-29