Pattern lift from eyaltoledano/claude-task-master (MIT + Commons Clause
— pattern only, no code lift). Adds BOOCODE_TOOLS env var with three
tiers:
- core (4 tools): view_file, list_dir, grep, find_files. ~2k token
schema cost.
- standard (15 tools): core + web_search, web_fetch, git_status, all
8 codecontext_* tools. ~10k token schema cost.
- all (default; current behavior): every tool in ALL_TOOLS (20). ~21k
token schema cost.
The env var is a CEILING — narrows agent whitelists, never expands.
Default behavior unchanged when var is unset. resolveToolTier is
case-insensitive and falls back to 'all' on unknown values.
CORE_TOOL_NAMES + STANDARD_TOOL_NAMES validated at module load against
TOOLS_BY_NAME via two top-level for-loops that throw on the first
missing name. Module fails to import if a tier references a tool that
doesn't exist in the registry — catches typos and stale tier
definitions at boot rather than silently filtering valid tools out of
agent whitelists.
Wiring: agents.ts parseAgentBlock now reads BOOCODE_TOOLS from
process.env per parse, intersects with the agent's declared frontmatter
tools (or DEFAULT_TOOLS when frontmatter omits the field). Per-parse
read is fine — agents are re-parsed on the existing 60s cache TTL.
Tests: tools.test.ts grows from 1 to 10 tests. Covers resolveToolTier
across tiers/case/unknown values + the CORE-subset-of-STANDARD invariant
+ TOOLS_BY_NAME existence for both tier sets. 204/204 pass (was 195;
+9 new).
Deviation from the brief: the codecontext tools in the actual registry
have NO codecontext_* prefix (the brief's STANDARD list assumed it).
Used the actual names (get_codebase_overview, search_symbols, etc.).
Module-load validation would have failed boot with the prefixed names.
Smoke: with BOOCODE_TOOLS unset, agents return their full 12-tool
whitelists. With BOOCODE_TOOLS=core in .env + container restart, the
same agents narrow to 4 tools (find_files, grep, list_dir, view_file)
— intersection of declared whitelist ∩ core tier. Reverted after
confirmation.
CLAUDE.md updated with BOOCODE_TOOLS in the Environment section's
Optional list. .env.example gained a commented BOOCODE_TOOLS=all line
with the per-tier token-cost table.
~110 LoC across 5 files (4 modified + 1 test expansion). Under the
brief's ~30 LoC estimate for code; the test suite expansion drove
most of the growth.
- New services/truncate.ts. Tmpfs storage at /tmp/boocode-truncations/
(BOOCODE_TRUNCATION_DIR env var overrides for tests). 12-char base32
opaque ids (~60 bits entropy, "tr_<id>"). Three exports: storeTruncation,
readTruncation, truncateIfNeeded (wrap-or-passthrough helper).
cleanupTruncations does TTL-pass (7 days) + orphan-reap (parts query on
payload->'output'->>'outputPath') in one shot.
- Wired four tools through truncateIfNeeded: view_file (raw full file),
list_dir (full filtered+secret-filtered entries serialized one-per-line),
web_fetch (textRaw pre-slice), codecontext_client (body.result pre-slice).
Each returns the existing sliced view plus an optional outputPath field
when truncation fires.
- New view_truncated_output ToolDef. Resolves opaque id → on-disk content
internally; model never sees the truncation dir. Same start_line /
end_line slicing semantics as view_file. Registered in ALL_TOOLS (alpha
sort places it after view_file automatically) and READ_ONLY_TOOL_NAMES.
- cleanupTruncations piggybacks on the v1.13.3 stuck-row sweeper's 60s
setInterval. No-op when truncation dir is empty.
Not wired (TODO follow-up): grep and find_files. file_ops returns post-cap
results to the tool execute path, so the "full content" isn't recoverable
without a refactor of fileOps.grep / fileOps.findFiles to expose the
uncapped result. web_search is silent-slice (no truncated flag); outside
scope. Five sites of seven covered; the remaining two are the only ones
needing a file_ops change.
Tests: 7 new in truncate.test.ts (roundtrip, unknown id, malformed id,
truncateIfNeeded false/true/over-cap/storage-failure paths). 186 total
(was 179). cleanupTruncations file-system half implicitly via TTL pass;
orphan-reap branch covered by the live container smoke.
Smoke verified end-to-end against the live container:
- view_file with start_line=1, end_line=3 on CLAUDE.md → tool_result part
carried outputPath "tr_cdpn1o04k6ma" + truncated=true.
- /tmp/boocode-truncations/tr_cdpn1o04k6ma exists, 15876 bytes, mode 0o600,
parent dir mode 0o700.
- Follow-up view_truncated_output(id, start_line=50, end_line=55) returned
the actual lines 50-55 of CLAUDE.md (the 808notes/BooCode bullets).
- ALL_TOOLS count=20 (was 19); alpha sort places view_truncated_output
between view_file and watch_changes.
Closes a v1.12 catalog row that was scoped but deferred. The v1.13 parts
table made outputPath ride on the existing tool_result payload with no
schema change beyond the storage helper itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four independent items, all owed from prior dispatches.
- statement_timeout at the database level via:
ALTER DATABASE boocode SET statement_timeout = '30s';
Applied operationally; documented as a comment at the top of schema.sql
(ALTER DATABASE can't run inside a DO block, so it's not idempotent
inside applySchema). Re-apply after a volume reset.
- Tool registry alpha-sorted at module load. llama.cpp's prompt cache
hits on byte-identical prefixes; any reordering of the tool list near
the top of the system prompt would invalidate every cached turn.
Single-source sort at the ALL_TOOLS export so toolJsonSchemas() and
TOOLS_BY_NAME inherit the order automatically. New tools.test.ts
asserts the invariant; total tests 173 (was 172).
- Periodic in-process stuck-row sweeper. Runs every 60s, marks
'streaming' rows older than 5 minutes as 'failed', and publishes
chat_status='idle' on the user channel so the UI dot drops without a
refresh. Closes the mid-session crash UX gap; the v1.12.1 boot sweep
only fires once at startup, so sessions used to stay stuck until next
reboot. setInterval cleaned up via app.addHook('onClose'). Mirrors
handleAbortOrError's publish pattern.
- experimental_repairToolCall wired through AI SDK v6 streamText. Pass-
through implementation: log + return the original toolCall so the
stream keeps going. executeToolPhase's existing error paths (unknown
tool name → 'unknown tool: X' result; zod-reject → 'tool X rejected
— field: required') already surface bad calls to the model; the value
here is preventing the AI SDK from THROWING on parse errors and
killing the whole stream. Owed since v1.13.1-A.
Smoke verified:
- statement_timeout = '30s' confirmed via SHOW.
- Tool path normal flow intact (list_dir prompt → tool_call → result
→ final assistant). No malformed tool calls in the test run; repair
log will surface them when qwen3.6 actually emits one.
- Alpha order verified at runtime via the dist bundle: match: true.
- Sweeper logic not traffic-tested (no stuck rows to find), but the
SQL UPDATE + broker.publishUser pattern is identical to handleAbort
and the boot sweep — synthesis-only verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new tools registered through the existing ALL_TOOLS registry:
- web_search hits SearXNG's JSON API (Fathom, internal Tailscale URL,
no auth) and returns top results
- web_fetch retrieves a URL's text content, gated by isPublicUrl
(url_guard.ts) which blocks loopback / RFC1918 / Tailscale CGNAT /
link-local / .local / .internal / non-http schemes
Both tools are opt-in via the existing session.web_search_enabled flag
(plumbed in v1.9, activated here). Default off. UI labels updated to
"Enable web search and fetch" / "Web search and fetch" since fetch joins
the same store. Counts against the v1.8.2 per-turn budget; covered by
the v1.11.6 doom-loop guard.
Native Node 20 fetch — no new prod dep. HTML stripping via regex (script
and style content elided wholesale). 5MB body cap, 15s fetch timeout,
8000-char default output, 32000-char cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports continue.dev's DEFAULT_SECURITY_IGNORE_FILETYPES + ignored-dir lists
into apps/server/src/services/secret_guard.ts plus a small BooCode
additions block (id_rsa*, *credentials*, .netrc, *.kdbx). Tiny glob-to-
regex matcher; no new prod dep.
view_file hard-refuses via SecretBlockedError. list_dir / grep /
find_files filter their results and surface a pathguard_note string
field with the hidden count — never list the offending paths back.
Named secret_guard.ts (not safety/pathGuard.ts) to avoid collision with
the existing path_guard.ts which already exports a pathGuard() function.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /data/skills mount (host: /opt/skills)
- skill_find, skill_use, skill_resource added to default read-only
tool set; opt-in for agents with explicit tools: whitelist
- AGENTS.md builtin agents drop explicit tools: arrays to inherit
the new default (now includes skill tools)
- POST /api/chats/:id/skill_invoke for slash-command flow
- 19 SKILL.md files seeded at /opt/skills/ across 6 source groups
Old hardcoded MAX_TOOL_LOOP_DEPTH=15 replaced by per-agent
max_tool_calls (1-100, AGENTS.md frontmatter) with defaults: 30 for
read-only-only agents, 10 for agents that include any non-read-only
tool, 15 for raw chat. When the loop hits cap, fire one final summary
call with tools disabled, stream the wrap-up into the in-flight
assistant message, then insert a system sentinel with
metadata.kind='cap_hit'. The sentinel renders an amber bubble with a
Continue button (latest sentinel only) that POSTs to a new
/api/chats/:id/continue route to extend. Hard ceiling: 3 cap-hits per
chat (2 continues max) — third sentinel reports can_continue=false.
Error frames carry a machine-readable reason code alongside human
error text. Failed messages persist the reason via
metadata.kind='error' so the bubble renders specifics on reload (WS
error frame is one-shot).
Tool call UI rewired: ToolCallLine renders inline (↳ name args
spinner/check/✗, expand-on-tap for args+result); ToolCallGroup
collapses 3+ consecutive same-tool runs into a compact card.
MessageList owns a three-pass pre-render (flatten + fold tool
results onto matching runs by id + group same-tool runs + number
sentinels). MessageBubble drops tool rendering and adds the
sentinel / error-reason branches. ToolCallCard deleted.
Roadmap follow-up logged: add explicit max_tool_calls: 30 to the 6
agents in /data/AGENTS.md and /opt/boocode/AGENTS.md post-ship for
discoverability (defaults handle behavior identically).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile header is now two rows. Row 1: hamburger | project · branch
indicator (live via GET /api/projects/:id/git, 30s poll) | ModelPicker |
FolderTree. Row 2: pane-switcher pill (hand-rolled BottomSheet) +
NewPaneMenu. Chat-within-pane navigation hidden on mobile; users switch
panes via the sheet. Cross-tab status sync via chat_status frames
published from inference.ts at working/idle/error transitions; StatusDot
component renders amber-pulse/green/red/gray on each pane row and on
desktop ChatTabBar tabs. Level 1 git awareness exposes a read-only
git_status tool to the model, backed by services/git_meta.ts (execFile
+ 2s timeout + 30s cache). Workspace.tsx now receives panes/chats hooks
as props (hoisted into Session.tsx) so the header pill shares state
with the pane grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- file_ops.MAX_FIND_RESULTS: 1000 -> 200 to match existing tool cap and
preserve LLM behavior
- tools.find_files now delegates to file_ops.findFiles (parallels how
grep already delegates); drops ~50 LOC of duplicated path resolution
and rg subprocess
- Drop unused basename import in file_ops
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>