Update version summary: v1.6-mobile-pass and v1.6.1-cleanup are now merged with SHAs; v1.6.2-mobile-ui-fixes added as in-flight with its 4-commit plan. v1.6.1-cleanup details rewritten to reflect what actually shipped (B1) vs what was audited-only (secrets, panes, unused exports, hand-rolled patterns, mount scope, etc.). Closed two open items: session_renamed has a server publisher since v1.4; PATCH /api/panes/:id is moot (endpoint never re-introduced). Dependency graph updated with v1.6.2 node between v1.6.1 and v1.7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 KiB
BooCode v1.x — Roadmap
Last updated: 2026-05-16
Overview
BooCode is a standalone code-chat tool at /opt/boocode/. Read-only by design in v1.x — pick a project, chat with a local LLM that has file-inspection tools, get streaming responses over WebSocket. Built May 2026 after the in-boolab BooCode mode stalled.
v1 shipped in a single Claude Code session. v1.1 onwards is a batched build-out. Original Batch 1–10 plan was reordered mid-stream — chats-inside-sessions, archive, and fork/delete work was prioritized over the mobile pass.
Live at https://code.indifferentketchup.com (Caddy → Authelia → Tailscale → 100.114.205.53:9500).
Version summary
| Version | Theme | Status | Notes |
|---|---|---|---|
| v1.0 | Initial scaffold, read-only tools, WS streaming | ✅ Done | Shipped in one Claude Code session |
| v1.1-batch1 | Markdown, Copy + Regen, tok/s + ctx, AI chat naming | ✅ Merged | — |
| v1.1-batch2 | Sidebar restructure: projects → sessions, max 5 + “view all” | ✅ Merged | — |
| v1.1-batch3 | Pane system, FileBrowserPane + Shiki, chat→file click, cross-tab | ✅ Merged | — |
| v1.1-batch3.5 | Chip infrastructure, @file picker, line-select-attach |
✅ Merged | — |
| v1.2 | Chats inside sessions refactor, right-rail, /compact, archive, force-send |
✅ Merged | Replaced original “Batch 4 = mobile” plan |
| v1.2-project-ux | Project archive UX, sidebar context menu, full-bootstrap, Gitea API | ✅ Merged | — |
| v1.3 | Tab-close + chat-archive | ✅ Merged | — |
| v1.4 | Fork from message + delete message + header polish + housekeeping | ✅ Merged | Was original “Batch 5” |
| v1.5 | Refactor splits, vitest harness (23 tests), error-log surfacing, /opt:ro + BOOTSTRAP_ROOT, persistent context-window tracker |
✅ Merged | — |
| v1.5.1 | Bootstrap hotfix: git in container, SSH keypair, known_hosts, SSH URL rewrite, /opt/projects label | ✅ Merged | 4a9f207 |
| v1.6-mobile-pass | Mobile pass: drawer, pane stacking, long-press, swipe-to-close, pull-to-refresh, IME safety, safe-area, tap targets + H1 path-guard fix | ✅ Merged | 57c883b..943ae7d (6 commits) |
| v1.6.1-cleanup | Mostly audit-only; one fix shipped: RightRail max-md:hidden wrapper. Audit reports for secrets, stale code, panes, mount scope, hand-rolled patterns deferred to follow-ups |
✅ Merged | 6a9fe18 |
| v1.6.2-mobile-ui-fixes | Mobile UI polish from device testing: kill single-pane navigator chrome, header rework, “New chat” in long-press menu, RightRail as mobile drawer (reverts v1.6.1 wrapper) | 🔄 Hand-back received, uncommitted | — |
| v1.7 | Drag-drop + paste-as-attachment (chip infra extension) | Planned | Was Batch 6 |
| v1.8 | Settings drawer (system prompt per project + session, web search toggle) | Planned | Was Batch 7 |
| v1.9 | Web search backend: SearXNG web_search + web_fetch tools |
Planned | Was Batch 8 |
| v1.10 | Agents (Tier 2): AGENTS.md, per-agent model/temp/tools, picker |
Planned | Was Batch 9 |
| v1.11 | BooTerm: separate container, xterm.js + node-pty + tmux, terminal pane | Planned | Was Batch 10 |
Version details
v1.1-batch1 — Message polish ✅
Markdown (react-markdown + remark-gfm), Copy + Regenerate, tok/s + context counter, AI session naming.
Key decisions:
sessions.name(nottitle).enable_thinking: false+max_tokens: 30for Qwen3 utility calls.messages_deletedWS frame added for multi-tab regen.- In-app event bus (
sessionEvents.ts, module-scopeSet<Listener>).
Schema: messages.tokens_used, messages.ctx_used, messages.ctx_max, messages.started_at, messages.finished_at.
v1.1-batch2 — Sidebar restructure ✅
Projects as expandable groups, up to 5 recent sessions per project, “View all (N)”, GET /api/sidebar, useSidebar singleton hook.
Key decisions:
useSidebarmodule-scope singleton.localStorage['boocode.sidebar.expanded'].session_renamedpayload{session_id, name}.
v1.1-batch3 — Pane system ✅
session_panes table, pane CRUD with transactional position-shift, Workspace + tab strip + drag-to-reorder (native HTML5), ChatPane (extracted), FileBrowserPane (tree + Shiki + filter), chat→file click, PaneTab context menu, file_ops + file_index shared services, broker user channel + /ws/user, session_updated, session_loaded, idempotent default-Chat-pane backfill.
Schema: session_panes (id, session_id, position, kind CHECK, state JSONB).
v1.1-batch3.5 — Chips + @file + line-select ✅
Attachment type + flattenToMessage + LANG_MAP, AttachmentChip, AttachmentPreviewModal, ChatInput chip-row, hand-rolled @file mention popover, line-select-attach in FileBrowserPane via local FileViewer, FileBrowserPane filter upgrade (empty=tree, non-empty=flat).
v1.2 — Chats inside sessions ✅
Originally planned as the mobile pass. Reshuffled: structural refactor — chats inside sessions, right-rail, /compact (chat’s own model summarizes via kind='compact' system message), force-send.
Schema: chats table, sessions.status, messages.chat_id, messages.kind (regular | compact).
v1.2-project-ux ✅
Full new-project bootstrap (mkdir + git init + .gitignore + first commit + Gitea remote + push), sidebar context menu (Rename / Archive / Open in Gitea), project landing page archived-list, Gitea API integration. Option B taken: BOOTSTRAP_ROOT env var, /opt stays read-only mount, /opt/projects writable.
Schema: projects.status, projects.archived_at.
Key decisions:
execFileonly, noexecshell strings.- DB INSERT last in bootstrap sequence.
- Soft-fail on Gitea steps.
- Project Delete endpoint exists but stays unexposed (re-add INSERTs fresh row → FK cascade nukes history; archive is the safe pattern).
v1.3 — Tab close + chat archive ✅
Tab close UX cleanup, chat-level archive (separate from session archive).
v1.4 — Fork + delete + header polish ✅
Was originally planned as Batch 5.
Shipped: POST /api/sessions/:id/fork (deep copy messages up to target, new session in same project), DELETE /api/sessions/:id/messages/:id (cascading via messages_deleted frame), header breadcrumb (Projects → Project → Session), inline-editable session name, file path shown when File Browser pane is active, useActivePane hook.
v1.5 — Refactor + tests + security scoping + context tracker ✅
5-commit sequence:
- Refactor: FileBrowserPane (865 → split with FileViewer extracted), Workspace, inference split.
- Vitest harness: 23 tests covering routes + resolveProjectPath. Pinned to v3 (Vite 5 / vitest 4 incompatibility).
- Error-log surfacing: dead-code removal from earlier H1/H2 audit items, structured error logs to client.
- Mount scoping:
/opt:/opt:ro+BOOTSTRAP_ROOTwritable subdir. Container loses write to/optproper. - Persistent context-window tracker: floating popover above chat input right edge, source = latest
message_completeframe’sctx_used/ctx_max, color-coded (neutral <60%, amber 60–85%, red 85%+), hides whenctx_maxnull.
Carried bug: resolveProjectPath whitelist-root bypass — discovered, asserted as “BEHAVIOR GAP” rather than silently patched. Fix landed in v1.6 (H1).
v1.5.1 — Bootstrap hotfix ✅ (4a9f207)
Dockerfile (git installed in container), docker-compose.yml, project_bootstrap.ts (SSH keypair, known_hosts, SSH URL Tailscale rewrite), CreateProjectModal.tsx, .gitignore. /opt/projects label clarified.
Known issue carried forward: dispatch used the in-repo secrets/boocode_gitea SSH key because the agent key was rejected. Key exposure flagged. Audit + rotation tracked in v1.6.1 below.
v1.6-mobile-pass ✅
Merged via 6 commits 57c883b..943ae7d (5 functional + 1 docs):
57c883b chore: fix resolveProjectPath whitelist-root bypass(H1 — droppedreal !== whitelistRealshort-circuit; flipped the v1.5 BEHAVIOR GAP test; 23/23 pass).a643b5f feat(mobile): viewport hook + sidebar drawer + hamburger headers(M1 + M2 + M6-header).cd897d6 feat(mobile): single-pane stack + long-press tab menu + swipe-to-close(M3 + M4 + A2).273eeac feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety(M5 + M6-bottom + M7 + M8).4b5b9b2 feat(mobile): pull-to-refresh sidebar list(A1).943ae7d docs: add v1.x roadmap snapshot(this file).
Decisions:
- H2 (roadmap update) handled in this file rather than by Claude Code.
- M5: mobile = button-only send, Enter inserts newline. Desktop unchanged.
isComposingguard for CJK IME. - M6: kept
max-w-[1000px](mobile naturally full-width below cap). - URL state:
?pane=<paneId>. Bare URL resets activePaneIdx to 0. - Long-press dispatches synthetic
contextmenuon[data-tab-id], opening Radix ContextMenuTrigger at touch coords. iOS callout suppressed. SwipeablePaneTab: 60px threshold, bails if vertical >30px, opacity 1→0.4.- A2 bundled with M3 in Commit 3 (structural coupling).
- Home.tsx no hamburger.
Deferred from v1.6 → rolled into v1.6.1-cleanup:
- RightRail still renders on mobile (~32px column).
- Secrets hygiene audit.
ProjectSidebar.tsxandChatTabBar.tsxshare content from two commits each — usegit add -p.
v1.6.1-cleanup ✅ (6a9fe18)
Shipped: RightRail wrapped in <div className="max-md:hidden contents"> so it's hidden entirely below the md breakpoint on mobile. (Note: v1.6.2 reverses this and replaces with a proper mobile drawer — see below.)
Audited but not shipped (queued for follow-ups):
- Secrets hygiene:
secrets/boocode_giteais NOT tracked; never committed to any branch;.gitignorealready coverssecrets/. Rotation is a Gitea-side action, no repo change needed. .bakfiles: 3 leftover from v1.5.1 (docker-compose.yml.bak-20260516,Dockerfile.bak-20260516,apps/web/src/components/CreateProjectModal.tsx.bak-20260516). Git-invisible via global~/.gitignore_global(*.bak*). Decide per file.- Unused exports: neither
knipnorts-pruneinstalled. Proposal pending. - Dead WS frames:
session_renamedHAS a server publisher (routes/sessions.ts:140, added in v1.4) — the roadmap's "no server publisher" open item is STALE, crossed off. TheInferenceFrameunion still declaressession_renamedas a type variant but no code publishes it on the per-session channel; trivial 1-line cleanup deferred. - Unused imports: web
tsc --noUnusedLocals --noUnusedParametersreturns 0 warnings. useSessionStreamrefcount: opportunity confirmed (~90 lines diff to apply theuseSidebar-style module-scope singleton pattern). Risk LOW. Queued for v1.6.2 or later.- PATCH
/api/panes/:idownership: MOOT — endpoint does not exist (the pane REST API was never re-introduced after pane state moved to client-side localStorage in v1.2). Crossed off open items. - Hand-rolled patterns vs library: 5 hand-rolled hooks/components total 336 lines. None duplicates anything in existing deps; library swap (
@use-gesture,react-pull-to-refresh) not worth the dep cost yet. /opt:/opt:romount tightening: Two-option plan documented for v1.6.2 — Option A (per-project bind-mounts) or Option B (deny.envpattern inpathGuard). Option B is the simpler short-term fix.
v1.6.2-mobile-ui-fixes 🔄
Hand-back received, uncommitted on v1.6.2-mobile-ui-fixes. 4-commit sequence proposed:
fix(mobile): hide Split button + single-pane navigator chrome(G1 — wrap the Workspace Split row in!isMobile).feat(mobile): rework Session and Project headers for narrow viewports(G2 — breadcrumbhidden sm:flex, session name capmax-w-[140px] sm:max-w-[280px], project page headingtext-base sm:text-lg, “New session” icon-only on mobile).feat(mobile): add "New chat" to tab long-press context menu(G3 — top of menu, separator, then existing items).feat(mobile): right-rail as drawer on mobile, header toggle button(G4 option b — newuseRightRailDrawerContext hook,RightRailrenders as fixedw-[85vw] max-w-smdrawer on mobile, FolderTree button in Session header, reverts v1.6.1'smax-md:hiddenwrapper).
Decisions:
- G4 option b chosen: mobile file browsing IS useful; drawer pattern mirrors
useSidebarDrawer. - G2 single-row session-name+model layout (model picker right-aligned), per spec example.
- G3 "New chat" at top, separator, then Rename.
- G2 "New session" button: icon-only on mobile via
<span className="hidden sm:inline">New session</span>.
Adjacent uncommitted change (not part of v1.6.2): MAX_TOOL_LOOP_DEPTH 5 → 15 in apps/server/src/services/inference.ts. Sam-authored, sitting in working tree on v1.6.2-mobile-ui-fixes. NOT on main as of this update. Commit separately.
v1.7 — Drag-drop + paste (planned, was Batch 6)
Depends on: v1.6.1 merged.
Scope (trimmed — chip infra exists from v1.1-batch3.5):
- Drag-drop files onto ChatInput → chip via
addAttachment({kind: 'file', source: 'drop'}). - Paste >8 lines → chip via
addAttachment({kind: 'paste', source: 'paste'}). ≤8 lines inline. - Drop overlay (dashed border + “Drop to attach”).
- Client-side 5 MB cap + binary detection (null-byte check in first 8KB).
- Max 10 attachments shared cap.
- Folder drop rejected. Image paste rejected. Binary files rejected with toast.
Frontend only.
v1.8 — Settings drawer (planned, was Batch 7)
Depends on: header gear (already in v1.4).
Scope:
- Right-side drawer (hand-rolled, no shadcn Sheet). Tabbed: Session + Project.
- Session tab: system prompt, web search toggle, model picker, session name.
- Project tab: default system prompt, default web search, project name, root path (read-only), delete project (consider whether to expose given the cascade concern).
- Resolution:
session.system_prompt OR project.default_system_prompt OR "". - Project defaults applied at session create (copied), not retroactively.
- Web search toggle persistent per session (
sessions.web_search_enabled).
Schema: sessions.web_search_enabled, projects.default_system_prompt, projects.default_web_search.
v1.9 — Web search backend (planned, was Batch 8)
Depends on: v1.8.
Scope:
web_searchtool → SearXNG athttp://100.114.205.53:8888/search?format=json, top-N{title, url, snippet}.web_fetchtool, regex HTML strip (no cheerio), 50KB cap.- Tools conditionally included based on
session.web_search_enabled. ToolCallCard.tsxrenders results as clickable URL list, web_fetch as text preview.- Env:
SEARXNG_URL,WEB_FETCH_TIMEOUT_MS,WEB_FETCH_MAX_BYTES.
v1.10 — Agents (planned, was Batch 9)
Depends on: v1.8.
Scope:
- Tier 2 agents: system prompt + model + temperature + tools whitelist per agent.
AGENTS.md(OpenCode-compatible):## Agent Nameblocks with YAML frontmatter.- Three builtin defaults (Investigator, Architect, Reviewer) when no
AGENTS.md. - If
AGENTS.mdexists, only its agents shown. - Agent picker in ChatInput toolbar + SettingsDrawer.
- Tools whitelist enforced at inference layer. BooChat agents read-only.
- File parsed on demand with mtime cache.
- Mid-conversation agent switch allowed; old messages retain their tool history.
Schema: sessions.agent_id TEXT.
v1.11 — BooTerm (planned, was Batch 10)
Depends on: v1.1-batch3 (pane system), v1.8 (settings drawer pattern).
Scope:
- New container
bootermat100.114.205.53:9501. Fastify + node-pty + tmux. - Caddy path-based routing:
/api/term/*+/ws/term/*→ booterm. - Shared
boocode_db. - Per-session tmux (
bc-<session_id>), per-pane tmux window (term-<pane_id>). - xterm.js terminal pane. New
kind = 'terminal'insession_panesCHECK. - PTY over binary WebSocket. Resize via
tmux resize-window. - Workspace mount:
/opt/repos:/opt/repos:rw. BooCode chat container keeps/opt:/opt:ro. - Send-to-terminal from chat: select text → right-click → “Send to terminal”.
- tmux persistence across WS reconnects, page refreshes, container restarts.
- No chroot/namespace isolation. Acceptable single-user homelab.
New app: apps/booterm/.
Architecture
Containers (current + planned)
| Container | Port | Mount | Purpose | Status |
|---|---|---|---|---|
boocode |
100.114.205.53:9500 |
/opt:/opt:ro + /opt/projects:rw |
Chat + read-only tools + SPA | Live |
boocode_db |
127.0.0.1:5500 |
boocode_pgdata |
Postgres 16-alpine (shared) | Live |
booterm |
100.114.205.53:9501 |
/opt/repos:/opt/repos:rw |
Terminal sessions | v1.11 |
boocoder |
TBD | /opt/repos:/opt/repos:rw |
Write tools | Post-v1.x |
URL routing (target state after v1.11)
code.indifferentketchup.com
├── / → boocode (SPA)
├── /api/chat/*, /ws/chat/* → boocode :9500
├── /api/term/*, /ws/term/* → booterm :9501
├── /api/coder/*, /ws/coder/* → boocoder (future)
└── /ws/user → boocode :9500
Database
Single Postgres boocode_db. All containers share. Projects shared. Sessions container-specific.
Current schema (post v1.5.1):
projects
├── id UUID PK
├── name TEXT
├── root_path TEXT
├── status TEXT (v1.2-project-ux: active | archived)
├── archived_at TIMESTAMPTZ (v1.2-project-ux)
├── default_system_prompt TEXT (v1.8)
├── default_web_search BOOLEAN (v1.8)
└── created_at TIMESTAMPTZ
sessions
├── id UUID PK
├── project_id UUID FK → projects
├── name TEXT
├── model TEXT
├── system_prompt TEXT
├── status TEXT (v1.2: active | archived)
├── web_search_enabled BOOLEAN (v1.8)
├── agent_id TEXT (v1.10)
├── created_at TIMESTAMPTZ
└── updated_at TIMESTAMPTZ
chats (v1.2)
├── id UUID PK
├── session_id UUID FK → sessions
├── name TEXT
├── status TEXT
├── created_at TIMESTAMPTZ
└── updated_at TIMESTAMPTZ
messages
├── id UUID PK
├── session_id UUID FK → sessions
├── chat_id UUID FK → chats (v1.2)
├── kind TEXT (v1.2: regular | compact)
├── role TEXT
├── content TEXT
├── tool_calls JSONB
├── tool_results JSONB
├── status TEXT
├── last_seq INTEGER
├── tokens_used INTEGER (v1.1-batch1)
├── ctx_used INTEGER (v1.1-batch1)
├── ctx_max INTEGER (v1.1-batch1)
├── started_at TIMESTAMPTZ (v1.1-batch1)
├── finished_at TIMESTAMPTZ (v1.1-batch1)
└── created_at TIMESTAMPTZ
session_panes (v1.1-batch3)
├── id UUID PK
├── session_id UUID FK → sessions (CASCADE)
├── position INTEGER
├── kind TEXT CHECK (chat | file_browser | terminal)
├── state JSONB
└── created_at TIMESTAMPTZ
settings
├── k TEXT PK
└── v TEXT
Reusable patterns
| Pattern | Where | Used by |
|---|---|---|
| In-app event bus | sessionEvents.ts |
All batches. Module-scope Set<Listener>. |
| Singleton hooks | useSidebar.ts |
Module-scope shared state. |
| User-channel WS broker | broker.ts + useUserEvents.ts |
Cross-tab lifecycle. One WS per tab. |
clock_timestamp() |
All INSERT/UPDATE | Never NOW() in new code. |
| Additive schema only | schema.sql |
ADD COLUMN IF NOT EXISTS, CREATE TABLE IF NOT EXISTS. |
| Idempotent backfills | schema.sql |
INSERT ... WHERE NOT EXISTS. |
enable_thinking: false |
auto_name.ts |
Required for Qwen3 utility calls. |
pathGuard |
tools/*, file_ops.ts |
Realpath + project root enforcement. |
Shared file_ops.ts |
tools.ts, routes/projects.ts |
Same core for inference tools and UI. |
File index (file_index.ts) |
routes/projects.ts |
rg --files + mtime cache. |
useViewport |
hooks/useViewport.ts (v1.6) |
matchMedia, SSR-safe. |
useSidebarDrawer |
hooks/useSidebarDrawer.tsx (v1.6) |
Context + auto-close on route change. |
| Hand-rolled long-press | hooks/useLongPress.ts (v1.6) |
500ms touchstart timer, dispatches synthetic contextmenu. |
| Hand-rolled pull-to-refresh | hooks/usePullToRefresh.ts (v1.6) |
80px threshold, 600ms min hold. |
| Hand-rolled swipe | components/SwipeablePaneTab.tsx (v1.6) |
60px threshold, vertical bail at 30px. |
Tech stack
| Layer | Tech |
|---|---|
| Backend | Node 20 + Fastify + @fastify/websocket + @fastify/static + zod + pg |
| Frontend | React + Vite + Tailwind v4 + shadcn nova preset |
| Inference | llama-swap http://100.101.41.16:8401 (OpenAI-compatible) |
| Search | SearXNG http://100.114.205.53:8888 (v1.9) |
| Syntax | Shiki (github-dark) |
| Terminal | xterm.js + node-pty + tmux (v1.11) |
| Auth | Remote-User from Authelia via Caddy forward_auth |
| Containerization | Docker Compose, Node 20-alpine, multi-stage, ripgrep apk, git apk (v1.5.1) |
| DB | Postgres 16-alpine, loopback 127.0.0.1:5500 |
| Networking | Tailscale mesh, Caddy (DO droplet), Authelia SSO |
| Code hosting | Gitea git.indifferentketchup.com |
| Tests | vitest v3 (pinned, Vite 5 / vitest 4 incompatible) |
Known open items
useSessionStreamrefcount. Two ChatPanes = two WS. Apply singleton pattern. Audited in v1.6.1, queued./opt:/opt:romount exposes all.envfiles. Whitelist scope before BooCoder. Two-option plan documented in v1.6.1 audit; ship in v1.6.2 or v1.7.secrets/boocode_giteain repo working tree. Never committed (git-invisible via global ignore). Rotate the Gitea-side key when convenient; no repo action required.- Dormant in-boolab BooCode mode. Reference only.
- BooCoder container. Post-v1.x.
Closed since last update:
— server publishes viasession_renamedno server WS publisherbroker.publishUserfromroutes/sessions.ts:140(added in v1.4). Confirmed in v1.6.1 audit.PATCH— endpoint does not exist; the pane REST API was never re-introduced after v1.2 moved pane state to localStorage./api/panes/:idlacks session-ownership check
Dependency graph
v1.0 (initial)
│
▼
v1.1-batch1 (markdown)
│
▼
v1.1-batch2 (sidebar)
│
▼
v1.1-batch3 (panes) ────────────────────────┐
│ │
▼ │
v1.1-batch3.5 (chips) ──────┐ │
│ │ │
▼ │ │
v1.2 (chats-in-sessions) │ │
│ │ │
▼ │ │
v1.2-project-ux │ │
│ │ │
▼ │ │
v1.3 (tab-close) │ │
│ │ │
▼ │ │
v1.4 (fork+delete+header) ◄──┼────────────────┘
│ │
▼ │
v1.5 (refactor+tests+ctx) │
│ │
▼ │
v1.5.1 (bootstrap hotfix) │
│ │
▼ │
v1.6-mobile-pass │
│ │
▼ │
v1.6.1-cleanup │
│ │
▼ │
v1.6.2-mobile-ui-fixes ◄─────┘
│
▼
v1.7 (drag-drop) ◄── v1.1-batch3.5
│
▼
v1.8 (settings)
│
├──▶ v1.9 (web search)
│
├──▶ v1.10 (agents)
│
└──▶ v1.11 (BooTerm) ◄── v1.1-batch3
Workflow
- Verify previous version merged to
main. - Dispatch prompt via Paseo (Claude Code runs at
/opt/boocode). - Claude Code recon → blocking questions → implement → hand back.
- Review hand-back in separate Claude chat (spec compliance, code quality, drift, stale code).
- Deploy:
docker compose up --build -d. - Smoke test per the hand-back’s plan.
- Sam commits manually, pushes to Gitea, merges to
main. - Next version.
Sam reviews all diffs. Sam commits. Never git pull/push/commit on his behalf.