Compare commits

...

14 Commits

Author SHA1 Message Date
3992a9fcb7 v1.13.15-codecontext-synth: forced second-inference synthesis for codecontext overview tools
After a codecontext overview-class tool call lands (get_codebase_overview,
get_framework_analysis, get_semantic_neighborhoods), the pipeline runs a
second inference pass that replaces the recursive runAssistantTurn. The
synth pass auto-fetches the top-N source files referenced in the
codecontext output plus project docs (BOOCHAT.md, AGENTS.md,
*roadmap*.md, CONTEXT.md), applies a 32k-token budget with explicit
drop-priority, and streams a structured response that grounds the model
in real load-bearing code rather than relying on the codecontext summary
alone. Smoke #1 (default) and #2 (Architect) both cite the correct
inference/turn.ts + tool-phase.ts + stream-phase.ts files; smoke #6
(fault injection) verifies the fall-through path marks the synth message
status='failed' and yields cleanly to the recursive turn.

## Truncation-aware extraction

codecontext's wrapper inline-truncates results at 32k chars. Without the
expansion step, the top-N file selection only saw the alphabetical head
of the codebase (apps/booterm/dist/*) and auto-fetched the wrong sources.
The pipeline now calls in-process readTruncation(outputPath) before
extracting referenced files, so top-N selection sees the full 80k+ char
output. The 32k truncated head still ships to the synth model — the
expansion is reference-extraction-only, preserving the token-budget
contract. Graceful degradation on readTruncation null/throw: log warn,
fall back to the truncated head.

## Schema deviation from dispatch

The dispatch claimed no schema migration was needed for the new
'synthesis' part kind. Reality: message_parts.kind has an explicit
CHECK constraint (schema.sql:54) that would reject the new value. Added
a DROP CONSTRAINT IF EXISTS + DO $$ pg_constraint idempotency-guarded
re-add matching the CLAUDE.md migration pattern. The inline CREATE TABLE
constraint also updated so fresh installs land with the extended enum.

## User-abort marks synth-message failed

Deviation from review-time spec ("user-abort path does NOT mark the
message failed"). The outer abort handler in error-handler.ts operates
on the parent turn's assistantMessageId, not the new synth row that
runSynthesisPass created. Without explicit marking, the synth row would
sit in status='streaming' until the 5-min stale-streaming sweeper
(v1.13.1-cleanup-bundle), tripping the frontend's 60s no-token-activity
banner in the meantime — exactly the UX bug class the v1.13.1 sweeper
was added to handle. Marking failed on every catch path (including
user-abort) closes the gap. Cost: one extra DB write + one publish on
the rare user-abort-during-synth path.

## Race-safe synth-tool capture

tool-phase.ts uses synthEntries: Array<{tc, output, error?}> with
per-callback push under Promise.all. find() picks the first non-error
entry by call-order (toolCalls array index). Multiple synth-tools in
one batch are uncommon but handled deterministically.

## Roadmap rebase

Updated boocode_roadmap.md retrospective section + cleanup-order tracker
+ schema-changes summary to use the new vMAJOR.MINOR.PATCH-slug tag
names per the 2026-05-22 retag (CHANGELOG.md is the canonical record).
v1.13.15 listed as "this batch, tag pending"; a one-line follow-up
commit will remove that qualifier after the tag lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:08:47 +00:00
0fa46cd06c v1.13.12: skills audit + token-tracking fix + codecontext + cap50 + UI cleanups
Multi-topic batch. The big-ticket item is the skills audit; the rest are
smaller patches that compounded during the audit work.

## Skills audit (rules→recipes split)

Vendored all 26 skills from /home/samkintop/opt/skills/ into data/skills/
(the boocode-repo-local skill library — see docker-compose change below).
Audited via 5 parallel Claude Code agent-teams running the
mgechev/skills-best-practices 4-step protocol (Discovery → Logic → Edge
Case → self-Architecture-Refinement) per skill, ~2 min wall-clock vs the
~3.7-hour serial estimate.

Result: 14 skills surviving (renamed to gerund form, frontmatter matched),
11 deleted (duplicates, BooCode-irrelevant patterns, Claude-already-does-
natively), 1 migrated to BOOCHAT.md/BOOCODER.md as an always-true rule
(verification-before-completion). Each surviving skill had its description
refined to fix specific trigger gaps surfaced by the protocol — 4
real-bug findings landed (dead refs, stale tags, broken sub-file
references in the original vendored content).

Audit decisions documented in openspec/changes/v1.13.12-skills-audit/
audit-notes.md. Convention codified in BOOCHAT.md/BOOCODER.md "rules vs
recipes" sections — future workflow rules go to those files (100%
present), recipes stay in data/skills/ (~6% invoke rate in multi-turn
per the Codeminer42 measurement).

## Token tracking + stale-stream banner fix (same root cause)

ws-frames.ts IsoTimestamp was z.string().min(1) but postgres returns
timestamp columns as JS Date objects. Every message_complete /
session_updated / chat_updated frame was failing the v1.13.11 Zod gate
and being silently dropped. Symptoms: token tracking blank in the UI
(no usage frames landed); the 60s no-token-activity timer tripped the
stale-stream banner because the frontend's local message state never
saw status='streaming' flip to 'complete'.

Fix: z.preprocess(v => v instanceof Date ? v.toISOString() : v,
z.string().min(1)) applied to the IsoTimestamp primitive. Centralized,
no publisher changes, works identically server + web (the parity test
still passes).

## Codecontext .codecontextignore auto-install

services/codecontext_client.ts now copies the
codecontext/.codecontextignore.template into any project's root on the
first call to that project if no .codecontextignore exists. One file
written per project, idempotent (in-memory Set guard + access-check),
silent fallback on read-only project. Stops the upstream empty-source-
file parser crash on foreign projects' node_modules — previously
required manually copying the template per project.

## Tool-call budget cap 30 → 50

services/inference/budget.ts: BUDGET_READ_ONLY and BUDGET_NO_AGENT
bumped to 50 (from 30). BUDGET_NON_READ_ONLY stays at 10 (no write
tools landed yet). Real recon sessions were hitting 30 with ~3 turns
wasted on codecontext parse failures; legitimate need was ~27, and
Architect-class system overviews want deeper recon. Headroom of 20
absorbs failure-retry turns without changing the safety floor — the
doom-loop guard (3 identical calls → abort) catches the actual
failure mode this cap was guarding against.

v1.14 (Phase C outer agent loop) will supersede this via per-agent
agent.steps. Throwaway-ish patch but unblocks deeper recon today.

## UI cleanups

- ChatPane queued-message dropdown removed. Each queued message now
  has three buttons: edit (pop back into ChatInput via sendToChat
  event), force-send (was the dropdown's only useful action), and
  cancel. Default behavior (send when streaming completes) needs no
  UI — it's the implicit do-nothing path.
- ChatThroughput removed from desktop tab strip (ChatTabBar.tsx).
  Mobile tab switcher still shows it.

## Plumbing

- .gitignore: data/* + !data/AGENTS.md + !data/skills/ negation
  patterns so the vendored skill library + agent registry become
  git-tracked while session DB state stays out.
- docker-compose.yml: removed /opt/skills:/data/skills override
  mount. Skills now live in the boocode repo at data/skills/,
  auditable per-batch. The host-level /opt/skills/ is preserved
  untouched for any other tools that read from it.
- .codecontextignore at repo root: auto-installed when codecontext
  was first called against /opt/boocode itself; matches the template.
- CLAUDE.md: updated to document the v1.13.11 publishFrame wrapper +
  message_parts table + tool_cost_stats view + DB-integration test
  pattern + host-side smoke endpoint quirk. (Pre-existing in working
  tree before this batch; shipped here for completeness.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:58:30 +00:00
bc376c878d v1.13.11-b: convert raw broker.publish call sites to typed publishFrame
Second half of the WebSocket-frame-typing batch. Phase A (8b568b3)
landed the schemas + frontend receive validation + publishFrame /
publishUserFrame wrappers. This commit converts the existing publish
call sites so every server-emitted WS frame now goes through Zod
validation at the broker boundary.

Conversion strategy: change once in the inference / skills adapters in
index.ts (so ctx.publish / ctx.publishUser propagate to publishFrame /
publishUserFrame for ALL ~50 inference + auto_name call sites in one
move), then bulk-replace the ~30 direct broker.publish* call sites in
the routes + compaction.

Files touched:
- index.ts: inference + skills route adapters now call publishFrame /
  publishUserFrame internally; raw broker.publishUser('default', ...)
  call in the stale-row sweeper also converted.
- routes/projects.ts (7 sites), routes/chats.ts (9 sites),
  routes/sessions.ts (8 sites): all broker.publishUser(...) → broker.
  publishUserFrame(...).
- services/compaction.ts (3 sites): 2 publishUser, 1 publish.

Real protocol drift surfaced by Zod, fixed in the same commit:

  services/compaction.ts:442 was publishing chat_status with status:
  'working' — the v1.12.1 chat_status widening (CLAUDE.md:55) dropped
  this enum value in favor of streaming|tool_running|waiting_for_input|
  idle|error. The compaction.ts site was missed during v1.12.1; the
  frame had been published with an unknown enum value ever since (the
  frontend useChatStatus quietly ignored it). Corrected to 'streaming'
  — compaction's LLM call has the same dot-state semantic as an
  inference turn. This is exactly the class of bug v1.13.11 exists to
  catch.

Schema relaxation: OpaqueObject (the bag type for nested entities like
Project / Chat / Session / WorkspacePane embedded in WS frames) was
z.object({}).passthrough(), which Zod outputs as {} & {[k:string]:
unknown}. The strict-typed entities don't have index signatures so
TypeScript rejected them at publishFrame call sites. Relaxed to
z.unknown() — runtime validation still accepts the value, dev-time
narrowing happens via the existing hand-maintained types. Trade-off:
frame-level drift detection stays sharp; nested-payload validation
goes to follow-up work as the brief intended.

Schema audit:
  grep -rn "broker\.publish(\|broker\.publishUser(" apps/server/src \
    --include="*.ts" | grep -v "broker.ts\|__tests__\|.bak"
  → 0 results. Every server publish goes through publishFrame /
  publishUserFrame. The remaining ctx.publish / ctx.publishUser sites
  in services/inference/* + services/auto_name.ts route through the
  index.ts adapter, which calls publishFrame internally.

Tests: 219/219 pass (unchanged from v1.13.11-a; the Phase B conversion
is mechanical and doesn't add test cases).

Smoke: clean container boot, no ws-frame-validation-failed entries
under normal traffic. Sidebar list refresh + agent picker open both
pass through useUserEvents without drops.

~70 LoC across 7 files. v1.13.11 closed.
2026-05-22 15:54:00 +00:00
8b568b36d3 v1.13.11-a: WS frame schemas + frontend receive validation
First half of the WebSocket-frame-typing batch (split per recon — total
scope was ~535 LoC, larger than the roadmap's ~300 estimate, so the
server-side publish-site conversion lands separately in v1.13.11-b).

Phase A scope:

(1) apps/server/src/types/ws-frames.ts (NEW) — Zod schemas for all 27
wire-format WS frame types. Discriminated union (WsFrameSchema) plus
KNOWN_FRAME_TYPES const for diagnostic lookup. UUIDs are z.string().
uuid(); model-emitted tool_call_id stays z.string().min(1) since OpenAI-
compatible APIs emit "call_<random>" not UUID. Per-kind payload narrowing
(tool args, message_parts payloads) intentionally stays z.unknown() —
frame-level drift detection is the goal; deep payload validation is
follow-up work.

(2) apps/web/src/api/ws-frames.ts (NEW) — byte-identical mirror of the
authoritative server file. No path alias from web→server in the existing
tsconfig setup; sync-by-hand was chosen over a new packages/shared/ dir.
A ws-frames.test.ts test asserts the two files match.

(3) apps/server/src/services/broker.ts — adds publishFrame() and
publishUserFrame() methods to the Broker interface. Both validate via
WsFrameSchema and fail-closed: log + drop on invalid. createBroker now
accepts an optional FastifyBaseLogger so validation failures land in
the pino stream (with console.error fallback for unit tests). The
existing publish() / publishUser() raw methods stay legal — they get
converted to the typed variants in v1.13.11-b.

(4) apps/web/src/hooks/useSessionStream.ts + useUserEvents.ts — wrap
ws.onmessage with WsFrameSchema.safeParse. Fail-closed: invalid frames
log + return without dispatching. Hand-maintained WsFrame and
SessionEvent types stay in place; one cast bridges Zod-typed → narrowed
shape (Zod uses OpaqueObject for nested Message[] / WorkspacePane[] etc.,
which are dev-time-narrowed via the existing hand-maintained types).

(5) apps/web/package.json — adds zod ^3.23.8 as a direct dep. Was a
transitive dep via ai-sdk / postgres; promotion makes the import legal.

(6) Tests: 15 new in ws-frames.test.ts covering happy-path per major
frame type, drift-catchers (unknown type, invalid enum, non-UUID, negative
tokens), parts-authoritative read variants, the mirror-file diff check,
and four broker fail-closed scenarios. 219/219 server tests pass (was
204; +15 new).

Two recon corrections to the dispatch brief, both flagged before
implementation:

- No 'parts_appended' frame exists. The brief assumed one; the codebase
  reads parts via the messages_with_parts view after message_complete
  triggers a refetch. MessagePartSchema is therefore unused this batch.
- No 'tool_running' frame exists. The brief listed it as standalone; it
  is in fact a 'chat_status' variant ({ status: 'tool_running' }), already
  covered by ChatStatusFrame.

Smoke: clean container boot, no validation errors in the server log. Real
production frames pass validation (the schemas were derived from the
existing hand-maintained types in api/types.ts and sessionEvents.ts).

v1.13.11-b will follow immediately: convert all ~85 raw broker.publish /
ctx.publish call sites across 11 server files to publishFrame /
publishUserFrame. Mechanical edit; the wiring done here means the diff
in -b is just the call-site swaps.

~310 LoC across 9 files (4 new + 5 modified).
2026-05-22 15:48:32 +00:00
34cbecf975 v1.13.15-tools: tiered tool loading via BOOCODE_TOOLS env var
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.
2026-05-22 14:59:01 +00:00
5a3f357ce9 v1.13.15-openspec: reformat batch docs to OpenSpec directory structure
Adopt Fission-AI/OpenSpec's openspec/changes/<change-name>/{proposal,
specs,design,tasks}.md shape for BooCode's own batch docs. Zero-dep
documentation reformat; replaces ad-hoc boocode_batchN.md /
handoff_vN.N.N.md convention.

Existing batch docs moved into openspec/changes/archived/ via git mv
(preserves history):
- boocode_batch10.md
- handoff_v1.13.8_prefix_verify.md
- handoff_v1.13.10_per_tool_cost.md

Pre-v1.13.15 docs were NOT split into proposal/tasks/design files. The
work was already shipped; the originals are preserved as archived
snapshots. New v1.13.15+ batches land directly in
openspec/changes/<slug>/proposal.md (+ tasks.md, + design.md when
applicable) per the convention documented in openspec/README.md.

CLAUDE.md gained a one-line pointer to the convention (workflow
section). File grew from 153 → 154 lines, 27,682 → 27,925 chars; both
remain well under the AgentLint hard caps.

specs/ directory is reserved for future OpenSpec CLI adoption (v1.14+).
No CLI dep added in this batch — directory structure only. If/when the
full OpenSpec lifecycle is adopted, that lands as a separate batch.
2026-05-22 14:54:17 +00:00
fc11e8dc91 v1.13.15-agentlint: instruction-file audit against AgentLint 31-check standard
Manual audit pass against 0xmariowu/AgentLint's evidence-backed checks
(MIT, drawn from 265 versions of Anthropic's internal Claude Code
system prompt).

Findings and fixes:
- Identity sections ("You are the assistant running inside ...") removed
  from BOOCHAT.md (line 3) and BOOCODER.md (line 5). The model already
  knows where it's running; the openers were emphatic decoration.
- CLAUDE.local.md added to .gitignore (.env was already covered).
  Claude Code's Glob tool ignores .gitignore by default, which means
  any local override file was otherwise readable by any agent walking
  the workspace.
- CLAUDE.md unchanged — already passes all 10 checks. Emphasis density
  0.58/1000 words (under Anthropic's 1.4/1000 endpoint); two IMPORTANT/
  MUST references are load-bearing (tsc-noEmit footgun, v1.13.7
  includeUsage invariant); zero identity sections; zero --no-verify
  references; 27,682 chars (under the 40,000-char silent-drop limit).
  Line count (153) is over the 60-120 target band, but the brief
  explicitly forbids structural rewrites in the audit pass.

Targets not in scope:
- /opt/boocode/AGENTS.md does not exist in this repo (removed in v1.12,
  per CLAUDE.md:152). The global agent registry lives at /data/AGENTS.md
  (bind-mounted from outside the repo); can't be touched by this batch.
- No .github/workflows/ directory — SHA-pin audit (step 8) skipped.

Cumulative effect: model spends fewer tokens parsing instruction-file
ceremony in BOOCHAT/BOOCODER and receives sharper priority signal per
Anthropic's measured-evolution data. Zero code changes.
2026-05-22 14:52:37 +00:00
9ce638c916 v1.13.10: per-tool token cost accounting (rolling 100-call view)
Surfaces per-tool prompt/completion-token rolling averages in
AgentPicker for at-a-glance agent-cost hints. Implementation is a
SQL view on top of messages_with_parts plus a read endpoint and
AgentPicker tooltip extension. No new write site; all source data
already lands via the existing tool-phase.ts:94-95 / error-handler.ts:
109-110 / sentinel-summaries.ts UPDATEs that v1.13.7's includeUsage:
true fix made non-NULL.

(1) schema.sql — new tool_cost_stats view. Window-functions over
messages_with_parts.tool_calls with LATERAL jsonb_array_elements.
Attribution: equal split — multi-tool turn divides tokens N-ways;
the 100-call rolling mean absorbs split noise. Filters: status=
'complete' + metadata.kind NOT IN ('cap_hit','doom_loop') exclude
failed turns and sentinels respectively; tool_calls IS NOT NULL is
defense-in-depth since sentinels are role='system' rows. CREATE OR
REPLACE means schema apply is idempotent.

(2) routes/tools.ts NEW + index.ts wire-in. GET /api/tools/cost_stats
returns { stats: ToolCostStat[] } with mean_prompt_tokens / mean_
completion_tokens computed at read time (sum / n_calls). Sorted by
tool_name ASC. No pagination — ≤30 tools.

(3) __tests__/tool_cost_stats.test.ts NEW — 7 integration tests
keyed off DATABASE_URL env var. Tests skip gracefully when unset
(no-DB default). beforeAll applies the schema via sql.unsafe(read
FileSync(schema.sql)) for self-contained runs. Helper insertAssistant
Turn shared across cases. Covers: empty state, single-tool attribution,
multi-tool equal split, 100-call FIFO window, NULL-tokens exclusion,
parts-authoritative read via messages_with_parts, failed/sentinel
exclusion.

(4) web/api/types.ts + client.ts — ToolCostStat interface + api.tools.
costStats() method binding.

(5) AgentPicker.tsx — fetch costStats on mount, compute per-agent
sum-of-means across whitelisted tools, render muted cost line below
description: "~5.2k prompt / 280 completion · 6/8 tools · last call
3h ago". Skips line entirely when no tool history; preserves existing
native title= for layout backward-compat. formatK/formatAgo colocated.

Tests: 202/202 pass (195 prior + 7 new view-integration). Server +
web tsc clean.

Smoke: schema applied cleanly; GET /api/tools/cost_stats returns
canonical JSON; view + endpoint agree. Single-row result expected
given the v1.13.1-A → v1.13.7 NULL latent regression window; new
traffic populates organically.

Roadmap row at boocode_roadmap.md:114 plus schema row at :474 both
match. View vs table decision documented in handoff_v1.13.10_per_
tool_cost.md (rollback-safe, microsecond-fast at BooCode scale).

~270 LoC across 8 files (5 modified + 3 new).
2026-05-22 14:42:09 +00:00
8126d78b34 docs: capture v1.13.7-v1.13.9 invariants in CLAUDE.md
Five additions surfacing session-discovered constraints future Claude
sessions need:
- AI SDK v6 includeUsage:true requirement (avoids re-introducing the
  v1.13.1-A→v1.13.7 NULL-tokens regression)
- \n text-delta trim guards in MessageList/MessageBubble + payload.ts
  failed/empty-assistant skip rules (avoid undoing v1.13.7)
- 0.85 × ctx_max overflow formula (v1.13.9) replacing the stale
  ctx_max - 20k line
- New services/system-prompt.ts bullet documenting the v1.13.8
  fingerprint instrumentation surface
- New services/inference/budget.ts bullet with current BUDGET_NO_AGENT=30
  and read-only-tools rationale
2026-05-22 14:07:11 +00:00
b06a4a8e55 v1.13.9: compaction overflow trigger — 0.85 × ctx_max early trigger
Opencode pattern (session/overflow.ts): fire compaction at 85% of
ctx_max, replacing the v1.11.0-era `ctx_max - 20_000` formula.

Old formula: usable = ctx_max - 20_000
  - ctx=262144 → trigger at 242144 (92.4%) — only 7.6% headroom
  - ctx=100000 → trigger at  80000 (80.0%)
  - ctx= 32000 → trigger at  12000 (37.5%) — over-eager
  - ctx<=20000 → trigger at      0 — never fires

New formula: usable = floor(0.85 * ctx_max)
  - ctx=262144 → trigger at 222822 (85.0%) — 15% headroom for summarizer
  - ctx=100000 → trigger at  85000 (85.0%)
  - ctx= 32000 → trigger at  27200 (85.0%)
  - ctx=  8192 → trigger at   6963 (85.0%)

Ratio gives consistent headroom at any context scale. The qwen3.6
daily driver gets ~19k tokens more breathing room before overflow;
small-ctx models no longer degenerate to never-triggering.

usable() is the only consumer of COMPACTION_BUFFER → constant deleted.
New EARLY_TRIGGER_RATIO constant takes its place.

isOverflow() and the maybeFlagForCompaction() call site at
payload.ts:184 are unchanged — formula swap is internal to compaction.ts.
payload.ts comment touched only to drop the stale COMPACTION_BUFFER
reference (PRUNE_TRIGGER_TOKENS stays at 20k as the prune-freed
threshold; independent of the overflow formula).

Tests: 4 new usable() corner cases (262k/100k/8k/zero+negative), plus
5 isOverflow() numbers shifted to match the 85k budget at ctx=100k.
195/195 server tests pass (was 194).

Smoke: ratio math verified by unit tests at all four corners. Live
cap-hit verification deferred — requires accumulating >222k tokens
in a session under qwen3.6-35b-a3b-mxfp4 (was >242k pre-fix); will
surface organically in extended use.
2026-05-22 13:59:14 +00:00
a0c8d212cb v1.13.8: system-prompt prefix stability verify-and-measure
Recon during planning disproved the original v1.13.7 (DB-cache) premise:
buildSystemPrompt already runs over inputs mtime-cached at the file layer
(BOOCHAT.md in system-prompt.ts:25, AGENTS.md global+per-project in
agents.ts:245), and DB scalars are byte-stable until edited. The output
is microsecond pure-string concat with no I/O. Skills aren't in the
prefix; tools live in a separate request body field alpha-sorted by
v1.13.3.

This batch closes the verification gap with instrumentation, not
implementation:

- system-prompt.ts: buildSystemPromptWithFingerprint canonical impl
  computes SHA-256 over the assembled prefix, runs a per-session
  Map<sessionId, lastHash> observer, emits PrefixFingerprint per call
  and PrefixDrift (with field-level changed_inputs) on hash change.
  buildSystemPrompt is now a thin shim returning .prompt.
- agents.ts: getAgentsMtimes accessor — cache-read only, no I/O.
- payload.ts: buildMessagesPayload takes optional log argument; when
  passed, emits prefix-fingerprint (info) + prefix-drift (warn).
- turn.ts + sentinel-summaries.ts: pass ctx.log at 3 production call
  sites; sentinel summaries log too so any drift across cap-hit /
  doom-loop paths surfaces.
- system-prompt.test.ts: 4 new tests (byte-identical, no-drift-on-
  stable, drift-fires-with-changed-inputs, cross-session-no-drift).

194/194 tests pass (was 190).

Smoke: 5 messages in a fresh session produced 7 prefix-fingerprint
logs (extras from buildMessagesPayload being called from sentinel
summary paths), all with identical prefix_hash and prefix_length=2907,
zero prefix-drift. Prefix is byte-stable in steady-state.

Decision: original system_prompt_cache DB table from the roadmap is
permanently dropped. The v1.12.0 mtime caches at the input layer plus
alpha tool ordering at the request body (v1.13.3) already address the
load-bearing cache-stability surfaces. Instrumentation stays so the
claim can be re-verified at any time.
2026-05-22 13:42:18 +00:00
0ce6115976 docs: renumber v1.13.8 to verify-and-measure, drop system_prompt_cache table, add v1.13.8 dispatch brief 2026-05-22 13:24:29 +00:00
ff29b48e3a v1.13.7: stability bundle — usage capture + payload/UI sanitization
Five fixes for latent regressions surfaced during the v1.13.x.cosmetic
revert investigation. None alter schema or compaction; all cleanup
against the v1.13.1-A AI SDK migration's hidden surface.

(1) provider.ts — includeUsage: true on createOpenAICompatible.
@ai-sdk/openai-compatible defaults this false, omitting
stream_options.include_usage from the request body; llama-swap never
emitted the usage block, so result.usage.inputTokens/outputTokens
resolved undefined and tokens_used / ctx_used landed NULL in every
assistant row since v1.13.1-A. No historical backfill.

(2) MessageList.tsx — hasText = m.content.trim().length > 0.
AI SDK v6 streaming occasionally emits a leading "\n" text-delta on
tool-call-only turns; the literal newline passed length > 0 and
rendered an empty bubble + ActionRow between every tool call. Trim
catches it without changing semantics for genuine content.

(3) MessageBubble.tsx — same trim on hasContent for the no-tool-calls
path. Defensive symmetry with MessageList.flatten.

(4) payload.ts — buildMessagesPayload skips assistant rows with
status='failed' AND assistant rows with status='complete' + empty
content + no tool_calls. Without this, a trailing empty/failed
assistant + the next attempt's placeholder produced "Cannot have 2
or more assistant messages at the end of the list" rejections from
the OpenAI-compatible upstream after cap-hit + Continue.

(5) budget.ts — BUDGET_NO_AGENT 15 → 30. Every tool in ALL_TOOLS is
read-only today; the 15-cap was forward-looking for write tools that
haven't landed. No-agent mode now matches BUDGET_READ_ONLY.

47 LoC across 5 files. 190/190 server tests pass.

Verified live: new assistant turns populate StatsLine token data;
single-tool-call turns no longer render the stray empty-bubble +
ActionRow between tool calls; Continue after cap-hit no longer hits
the trailing-assistant API rejection.
2026-05-22 13:24:19 +00:00
81d837c04e v1.13.6: compaction head-assembly audit + reasoning fix
Audit traced compaction's summary path post-v1.13.1-B read flip:
- Q1: reads from messages_with_parts (view) — clean
- Q2: parts shape correctly threaded through buildHeadPayload — clean
- Q3: reasoning omitted from summary input — FIX NEEDED

v1.13.1-C wired reasoning end-to-end into inference/payload.ts but
missed this read site. Summarizer model couldn't see the reasoning
trail for tool-bearing turns, quietly degrading summary quality for
reasoning-channel models (qwen3.6).

Fix:
- CompactionMessage extended with reasoning_parts field
- SELECT pulls reasoning_parts from messages_with_parts
- buildHeadPayload (now exported for tests) prefixes assistant content
  with <reasoning>...</reasoning>\n\n<content>... when reasoning is
  present; standalone <reasoning>...</reasoning> for tool-call-only
  turns; omits the tag when reasoning is null or empty

4 new render branch tests (190 total).

Smoke deferred: forcing real compaction requires either threshold
pollution or building up a >40k-token chat with reasoning_parts.
Render branches are unit-covered; integration would only re-prove
structural correctness.
2026-05-22 08:18:47 +00:00
122 changed files with 11400 additions and 367 deletions

33
.codecontextignore Normal file
View File

@@ -0,0 +1,33 @@
# .codecontextignore — paths codecontext skips during analysis
# Copy to your project root and customize. Same syntax as .gitignore.
# Dependencies / vendored code
node_modules/
vendor/
.venv/
venv/
__pycache__/
target/
# Build artifacts
dist/
build/
out/
.next/
.nuxt/
.svelte-kit/
# IDE / tooling
.opencode/
.vscode/
.idea/
# Test artifacts / coverage
coverage/
.nyc_output/
.pytest_cache/
# Lock files (rarely have meaningful symbols)
package-lock.json
yarn.lock
pnpm-lock.yaml

View File

@@ -10,3 +10,12 @@ POSTGRES_PASSWORD=CHANGE_ME
# Internal Tailscale address that bypasses Authelia. Override if you
# point BooCode at a different SearXNG instance.
SEARXNG_URL=http://100.114.205.53:8888
# v1.13.15-tools: BOOCODE_TOOLS narrows the tool whitelist sent to the LLM.
# Unset (default) → all tools (~21k schema). Useful primarily for single-purpose
# sessions where the model only needs read-only filesystem access.
#
# core → view_file, list_dir, grep, find_files (~2k)
# standard → core + web_*, git_status, all 8 codecontext_* tools (~10k)
# all → every tool in ALL_TOOLS (~21k)
# BOOCODE_TOOLS=all

5
.gitignore vendored
View File

@@ -1,9 +1,12 @@
node_modules
dist
.env
CLAUDE.local.md
*.log
.DS_Store
.vite
coverage
secrets/
data/
data/*
!data/AGENTS.md
!data/skills/

View File

@@ -1,7 +1,5 @@
# BooChat
You are the assistant running inside BooChat — a self-hosted developer chat app.
## Capabilities
- Read-only file tools: `view_file`, `list_dir`, `grep`, `find_files`
@@ -28,6 +26,11 @@ You are the assistant running inside BooChat — a self-hosted developer chat ap
- Cite file paths + line numbers for any claim about the codebase
- When uncertain about scope or intent, surface options via `ask_user_input` rather than guessing
- Prefer codecontext (`search_symbols`, `get_symbol_info`, `get_dependencies`) over `grep` for symbol-level questions. Fall back to `grep` / `view_file` when codecontext returns degraded or empty results — that signals an unsupported language or parse failure.
- Verify before reporting work complete: run the relevant test/build/smoke command and confirm output matches the claim. Evidence first, assertion second.
## Convention: rules vs recipes
Always-true rules (process discipline, refusals, behavior contracts) live here in `BOOCHAT.md` — and in `BOOCODER.md` / `CLAUDE.md` per their scopes — where they are 100% present in every turn. On-demand recipes (specific procedures, scaffolds, checklists) live in `/data/skills/` and invoke roughly 6% of the time in clean multi-turn flow (Codeminer42 measurement, 2026). Don't file workflow rules as skills — they silently misfire. See Anthropic agent-skills best-practices (platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for the canonical conventions.
## Known limitations

View File

@@ -2,8 +2,6 @@
> (Stub. v2.0 implementation pending. This file documents the intended contract.)
You are the assistant running inside BooCoder — the write-capable companion to BooChat.
## Capabilities
- Everything in `BOOCHAT.md`
@@ -22,3 +20,8 @@ You are the assistant running inside BooCoder — the write-capable companion to
- Show a diff preview before any write
- Group related edits into a single `/apply` batch
- If a tool fails, surface the error verbatim — don't paper over it
- Verify before reporting work complete: run the relevant test/build/smoke and confirm output matches the claim. Evidence first, assertion second.
## Convention: rules vs recipes
Always-true rules live here, in `BOOCHAT.md`, and in `CLAUDE.md` (100% present each turn). On-demand recipes live in `/data/skills/` (roughly 6% invoke rate in multi-turn per Codeminer42, 2026). Don't file workflow rules as skills — they misfire. See Anthropic agent-skills best-practices (platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices).

163
CHANGELOG.md Normal file
View File

@@ -0,0 +1,163 @@
# Changelog
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.13.15-codecontext-synth — 2026-05-22
Forced second-inference synthesis pass for codecontext overview-class tools (`get_codebase_overview`, `get_framework_analysis`, `get_semantic_neighborhoods`). After the tool result lands, the pipeline expands the truncated head via in-process `readTruncation`, extracts referenced file paths from the full content, auto-fetches top-N files + project docs (BOOCHAT.md, AGENTS.md, *roadmap*.md, CONTEXT.md) under a 32k-token budget with explicit drop-priority order, then streams a synthesis turn that replaces the recursive `runAssistantTurn`. The 32k truncated head still ships to the synth model (token-budget contract preserved); the expansion is reference-extraction-only. Falls through to recursion on timeout (90s), model error, or non-2xx; user-abort marks the synth message `status='failed'` and re-throws (the outer abort handler operates on the parent turn's message, not the new synth row — without explicit marking, the row would sit `streaming` until the 5-min sweeper, tripping the 60s stale-stream banner). Adds `'synthesis'` to `message_parts.kind` CHECK constraint via `DROP CONSTRAINT IF EXISTS` + `DO $$ pg_constraint` idempotency-guarded re-add. Smokes #1, #2, #6 all clean; smokes #3#5 are content-quality checks for UI review.
## v1.13.14-skills-audit — 2026-05-22
Multi-topic batch. **Skills audit (headline):** vendored all 26 skills from `/home/samkintop/opt/skills/` into repo-local `data/skills/` (the `/opt/skills:/data/skills` override mount removed from `docker-compose.yml` so skills are auditable per-batch in git). Audited via 5 parallel Claude Code agent-teams running mgechev's 4-step protocol per skill — 14 survive with gerund-form names + refined triggers; 11 dropped (duplicates, BooCode-irrelevant patterns, Claude-already-does-natively); 1 (`verification-before-completion`) migrated to `BOOCHAT.md`/`BOOCODER.md` as an always-true rule. The Codeminer42 "rules vs recipes" split codified in those files. **Token tracking + stale-stream banner fix:** same root cause — `IsoTimestamp = z.string()` in `ws-frames.ts` was failing on postgres `Date` objects, silently dropping every `message_complete` / `session_updated` / `chat_updated` frame through the `v1.13.13-ws-publish` Zod gate; `z.preprocess(v => v instanceof Date ? v.toISOString() : v, ...)` applied to the primitive on both server + web (parity test still passes). **Codecontext ignore:** `codecontext_client.ts` auto-installs `.codecontextignore.template` into any project's root on first call (stops the upstream empty-source-file parser crash on foreign projects' `node_modules`). **Budget bump:** `BUDGET_READ_ONLY` + `BUDGET_NO_AGENT` 30 → 50 (real recon need ~27 + headroom for codecontext failure-retry turns; doom-loop guard catches the loop class anyway). **UI:** queued-message dropdown → edit / force-send / cancel buttons in `ChatPane.tsx`; `ChatThroughput` removed from desktop tab strip (mobile tab switcher keeps it). Audit decisions in `openspec/changes/v1.13.12-skills-audit/audit-notes.md`.
## v1.13.13-ws-publish — 2026-05-22
Second half of the WebSocket-frame-typing batch. Converts the existing ~50 inference + auto_name publish sites (via the `index.ts` adapter) plus ~30 direct `broker.publish*` call sites in routes + compaction, so every server-emitted frame now goes through Zod validation at the broker boundary. Pairs with `v1.13.12-ws-schemas`.
## v1.13.12-ws-schemas — 2026-05-22
First half of the WebSocket-frame-typing batch. Adds `apps/server/src/types/ws-frames.ts` with Zod schemas for all 27 wire-format frame types (discriminated union `WsFrameSchema` + `KNOWN_FRAME_TYPES` diagnostic lookup), duplicated byte-identical at `apps/web/src/api/ws-frames.ts` with a parity test. Introduces the `publishFrame` / `publishUserFrame` wrappers that fail-closed on schema mismatch.
## v1.13.11-tools — 2026-05-22
Tiered tool loading via `BOOCODE_TOOLS` env var (`core` | `standard` | `all`). Core = 4 read-only fs tools (~2k token schema cost). Standard = +web + git + codecontext (~10k). All (default) = every tool in `ALL_TOOLS` (~21k). The var is a ceiling — narrows agent whitelists, never expands. Pattern lifted from `eyaltoledano/claude-task-master`.
## v1.13.10-openspec — 2026-05-22
Adopt `Fission-AI/OpenSpec`'s `openspec/changes/<slug>/{proposal,tasks,design}.md` shape for BooCode's own batch docs. Existing batch docs (`boocode_batch10.md`, `handoff_v1.13.8_prefix_verify.md`, `handoff_v1.13.10_per_tool_cost.md`) moved into `openspec/changes/archived/` via `git mv` to preserve history. Zero-dep documentation reformat.
## v1.13.9-agentlint — 2026-05-22
Manual audit of instruction files against `0xmariowu/AgentLint`'s 31-check standard. Removed identity-opener sections from `BOOCHAT.md` and `BOOCODER.md` (emphatic decoration the model doesn't need). Added `CLAUDE.local.md` to `.gitignore` — Claude Code's Glob ignores `.gitignore` by default, so local overrides were otherwise readable by any agent walking the workspace. `CLAUDE.md` passed all 10 checks unchanged.
## v1.13.8-tool-cost — 2026-05-22
Per-tool prompt/completion-token rolling averages surfaced in AgentPicker as at-a-glance cost hints. Implementation is the `tool_cost_stats` SQL view over `messages_with_parts` (`LATERAL jsonb_array_elements` on `tool_calls`), plus a read endpoint and a tooltip extension. Equal-split attribution — multi-tool turn divides tokens N-ways; the 100-call rolling mean absorbs split noise. Filters out `cap_hit` / `doom_loop` sentinels. Source data already lands via existing UPDATEs that `v1.13.5-stability-bundle`'s `includeUsage: true` fix made non-NULL.
## v1.13.7-compaction-trigger — 2026-05-22
Compaction overflow trigger lowered to `floor(0.85 × ctx_max)`, replacing the v1.11.0-era `ctx_max 20_000` formula. Old formula gave only 7.6% headroom at 262k context and 0 budget for ≤20k contexts (never fired). New formula gives consistent 15% summarizer headroom across all model sizes. Opencode pattern lift from `session/overflow.ts`.
## v1.13.6-prefix-stability — 2026-05-22
System-prompt prefix stability verify-and-measure. Recon during planning disproved the original DB-cache premise: `buildSystemPrompt` already runs over inputs mtime-cached at the file layer (BOOCHAT.md, AGENTS.md global+per-project), and DB scalars are byte-stable until edited. This batch closes the verification gap with instrumentation, not implementation — `buildSystemPromptWithFingerprint` computes SHA-256 over the assembled prefix and a per-session `Map` observer fires `prefix-drift` (warn) on hash change with field-level `changed_inputs` diff.
## v1.13.5-stability-bundle — 2026-05-22
Five fixes for latent regressions surfaced during the cosmetic-revert investigation. (1) `provider.ts``includeUsage: true` on `createOpenAICompatible` (default false omitted `stream_options.include_usage`; llama-swap never emitted usage; tokens_used / ctx_used were NULL on every assistant row since `v1.13.0-ai-sdk-v6`). (2) `MessageList.tsx``hasText = m.content.trim().length > 0` to skip whitespace-only tool-call-only turns rendering empty bubbles. (3) `BUDGET_NO_AGENT` raised 15 → 30 to match read-only agent cap. (4) `payload.ts` skips status='failed' + complete-but-empty assistant rows so cap-hit + Continue doesn't upstream-reject. (5) Misc UI sanitization.
## v1.13.4-reasoning-fix — 2026-05-22
Compaction head-assembly audit caught one fix: reasoning was omitted from the summarizer's view of tool-bearing turns, silently degrading summary quality for reasoning-channel models (qwen3.6). `v1.13.0-ai-sdk-v6` had wired reasoning end-to-end into inference but missed this one read site. `CompactionMessage` extended with `reasoning_parts`; `buildHeadPayload` embeds it as a `<reasoning>...</reasoning>` prose prefix on the assistant content (OpenAI wire shape has no structured reasoning field).
## v1.13.3-truncate — 2026-05-22
Port of opencode's `truncate.ts`. Full tool output retrievable via opaque `tr_<12 base32 chars>` id (~60 bits entropy) and a new `view_truncated_output(id)` tool. Tmpfs storage at `/tmp/boocode-truncations/` (overridable via `BOOCODE_TRUNCATION_DIR`), 5MB cap, 7-day TTL, orphan-reap on the periodic 60s sweeper. Wired through four tools: `view_file`, `list_dir`, `web_fetch`, `codecontext_client`. Each returns the existing sliced view plus an `outputPath` field when truncation fires.
## v1.13.2-compaction-prune — 2026-05-22
Two-tier compaction prune — opencode pattern that was half-shipped in v1.11.0. New `message_parts.hidden_at` column with partial index on `WHERE hidden_at IS NULL`. `messages_with_parts` view changed from `COALESCE(parts, legacy)` to a CASE that distinguishes "no parts at all → fall back to legacy column for pre-v1.13.0 history" from "all parts hidden → drop the row from the model payload" (smoke caught the `COALESCE` leaking hidden parts back via legacy fallback). `prune.ts` scans `tool_result` parts newest-first, protects the last 40k tokens, marks older candidates hidden once the combined estimate clears 20k.
## v1.13.1-cleanup-bundle — 2026-05-22
Four independent items owed from prior dispatches. (1) `statement_timeout = '30s'` at the database level (documented in `schema.sql` but applied operationally — `ALTER DATABASE` can't run inside a `DO` block). (2) Tool registry alpha-sorted at module load — llama.cpp's prompt cache hits on byte-identical prefixes; reordering tools near the top of the system prompt would invalidate every cached turn. (3) Periodic 60s stuck-row sweeper. (4) `experimental_repairToolCall` to keep streams alive on malformed qwen3.6 tool args (pass-through implementation — logs and forwards unmodified; existing zod-reject path routes back to the model).
## v1.13.0-ai-sdk-v6 — 2026-05-22
Major migration to AI SDK v6. Introduces the `streamCompletion` adapter (`services/inference/stream-phase.ts`) over `streamText`, with five known gotchas the LSP can't catch — abort signals swallowed by `fullStream` (post-iteration throw required), usage lands only at stream end via `await result.usage`, tools have no `execute` field (BooCode dispatches in `tool-phase.ts`), and tool-call-only turns may emit a leading `\n` text-delta. Also ships the `messages_with_parts` view (parts-merge read path) and wires `reasoning_parts` end-to-end via a `ReasoningPart` in the v6 ModelMessage. Ports `ask_user_input` correlation queries from JSON columns to `message_parts` JOINs.
## v1.12.4-inference-split — 2026-05-21
Complete `inference.ts` split into `services/inference/`. Pieces: `turn.ts` (orchestration — `runAssistantTurn` / `runInference` / `createInferenceRunner`), `sentinel-summaries.ts` (`runCapHitSummary`, `runDoomLoopSummary`), `stream-phase.ts`, `tool-phase.ts`, `provider.ts`, `payload.ts`, `prune.ts`, `budget.ts`, `xml-parser.ts`, `error-handler.ts`, `sentinels.ts`, `parts.ts`, `types.ts`. Public surface re-exported via `inference/index.ts`; callers import from `./services/inference/index.js` explicitly (NodeNext doesn't honor directory-index resolution).
## v1.12.3-stale-banner — 2026-05-21
Stale-stream banner with Retry/Discard. When an assistant message sits `status='streaming'` with no token activity for 60+ seconds, the chat shows a banner above the input. Both actions clear the stale row via new `POST /api/chats/:id/discard_stale` (updates `status='failed'`, publishes `chat_status='idle'`). Closes the UX gap from the 2026-05-21 debugging spiral — slow streams and dead streams now look different.
## v1.12.2-live-toks — 2026-05-21
Live tok/s + ctx display next to the status indicator. `ChatThroughput` renders inline beside `StatusDot` while streaming or tool_running. Subscribes to existing `'usage'` WS frames (500ms-throttled, carrying `completion_tokens` + `ctx_used` + `ctx_max`) via `sessionEvents`. Hides when status drops to idle/error or data is older than 10s. Addresses the same UX gap as `v1.12.3-stale-banner` — gives users a live token velocity readout that immediately distinguishes slow from dead.
## v1.12.1-stop-handler — 2026-05-21
`handleAbortOrError` now writes `status='cancelled'` on user stop; rows no longer stuck `streaming` forever. Drops stale `messages_status_check` constraint (only `messages_status_chk` remains, allowing 'cancelled' via TS `MESSAGE_STATUSES`). Removes `detectSameNameLoop` and `DOOM_LOOP_SAME_NAME_THRESHOLD` (added during the 2026-05-21 debugging spike, never fired in any real run) plus 12 verbose `ctx.log.info` diagnostic markers from the same spike. Bundles workspace pane sync + status indicator overhaul + startup hung-row sweep that landed earlier in v1.12.1 work.
## v1.12.0-codecontext — 2026-05-21
Adds the `codecontext` sidecar (Go-based code-graph indexer at `codecontext:8080/v1/<tool_name>` over `boocode_net`) plus container guidance and skills runtime updates. Introduces the `chat_status` WS frame (`streaming | tool_running | waiting_for_input | idle | error`, widened from `working|idle|error`). Drops the deprecated `session_panes` table — workspace pane state moves to `sessions.workspace_panes jsonb` for cross-device sync via `PATCH /api/sessions/:id/workspace`.
## v1.11.1-consolidation — 2026-05-21
Rollup of v1.11.0v1.11.10 work that was shipped piecemeal. Covers anchored rolling compaction (single `summary=true` row per chat that supersedes itself), doom-loop guard via `detectDoomLoop`, `path_guard` secret-filename deny list, web tools (`web_search` against SearXNG + `web_fetch` with SSRF/private-IP block), and the 5MB stream-cap on response bodies with abort-on-overflow.
## v1.11.0-context-bar — 2026-05-20
Persistent context-window tracker in `ChatPane` + `ctx_max` capture via `${LLAMA_SWAP_URL}/upstream/<model>/props`. First inferences after a boocode boot may have `ctx_max=NULL` if llama-swap hasn't loaded the model yet — 60s negative cache TTL recovers on next turn. Replaced an earlier dead read of `parsed.timings.n_ctx` which never carried n_ctx.
## v1.10.1-booterm-user — 2026-05-19
Per-user shell privilege drop in the booterm container via `gosu` in `tmux.conf` default-command. Shells launched in browser terminal panes drop privs to `samkintop` rather than running as root inside the container.
## v1.10.0-booterm — 2026-05-18
Second container (`apps/booterm`, port 9501, bookworm-slim+glibc). Fastify + node-pty + tmux. Browser terminal panes connect via WS to `/ws/term/sessions/:sid/panes/:pid`; per-session tmux session `bc-<sid>`, per-pane window `term-<pid>`. xterm-addon-webgl with `document.fonts.load(...)`-gated init (Canvas2D doesn't honor `font-display: block`) and iOS-friendly visibility-change context recreation.
## v1.9.2-ask-user-input — 2026-05-18
`ask_user_input` elicitation tool. Pauses the inference loop and surfaces a prompt to the user; their response routes back as the tool result. Correlation initially via `messages.tool_calls` / `tool_results` JSON columns (later ported to `message_parts` in `v1.13.0-ai-sdk-v6`).
## v1.9.1-skills — 2026-05-18
Skills runtime + `/skill` slash command with autocomplete. Server-side parser, tools, `/api/skills`, and mount. Hardens `.dockerignore` to exclude `secrets/` and `data/`. Drops the type-to-confirm gate on chat delete (plain Cancel/Confirm only — per workspace convention).
## v1.9.0-themes-settings — 2026-05-17
Settings pane + per-project defaults + bulk archive + themes lift. `themes-v1` (18 preset palettes) ships in the same batch with a Settings picker for live theme switching.
## v1.8.2-cap-hit — 2026-05-17
Tool-loop cap-hit summary — when an assistant exceeds the per-turn tool budget, a sentinel `role='system'` row with `metadata.kind='cap_hit'` is inserted and a summary turn runs to give the user a coherent endpoint. Also compacts the tool-call UI rendering.
## v1.8.1-agents-global — 2026-05-16
Global agents (`data/AGENTS.md` bind-mounted at `/data/AGENTS.md`) + parser robustness + WS reconnect toast. Per-project `AGENTS.md` mechanism (`getAgentsForProject`) remains for *other* projects; the BooCode repo itself uses global-only to eliminate two-files-must-stay-in-sync drift.
## v1.8.0-agents — 2026-05-16
Tier 2 agents — `AGENTS.md` registry + per-session agent picker. Also lands mobile tab switcher, branch indicator, and the `git_status` tool.
## v1.7.0-drag-drop — 2026-05-16
Drag-drop + paste-as-attachment for long text in the chat input.
## v1.6.0-mobile — 2026-05-16
Full mobile suite. Adds `useViewport` (matchMedia breakpoints mobile <768 / tablet 7681023 / desktop ≥1024), `useSidebarDrawer` / `useRightRailDrawer` (Context + auto-close on `useLocation().pathname` change), `useLongPress` (500ms timer, synthetic `contextmenu`), `usePullToRefresh` (80px threshold, 600ms hold), `SwipeablePaneTab` (60px close, 30px vertical bail). Mobile headers with safe-area padding, hamburger left, FolderTree right. Tap targets at `max-md:min-h-[44px] max-md:min-w-[44px]`. Raises `MAX_TOOL_LOOP_DEPTH` 5 → 15. Right-rail becomes a drawer on mobile.
## v1.5.1-bootstrap — 2026-05-16
Bootstrap fixes — git + ssh installed in the boocode container, Tailscale host rewrite, `/opt/projects` label correction for the create-new-project bootstrap flow.
## v1.5.0-refactor-tests — 2026-05-16
Refactor split (FileBrowserPane / Workspace / `runAssistantTurn`) + vitest harness + unit tests for security-critical pure functions. Scopes the `/opt` mount to `/opt/projects` (writable) plus `PROJECT_ROOT_WHITELIST=/opt` (read-only resolution for add-existing). Surfaces swallowed errors and removes dead `session_renamed` paths.
## v1.4.0-fork-header — 2026-05-16
Fork from message + delete message + header polish + general housekeeping.
## v1.3.0-chats-projects — 2026-05-16
Chats-in-sessions era. Adds force-send, `/compact`, right-rail file browser, archive/rename/Open-in-Gitea sidebar context menu, archived projects landing page, create-project bootstrap with Gitea remote setup, landing-card buttons, 1000px content cap. Dedup audit and chat archive/delete from the sidebar.
## v1.2.0-multi-pane — 2026-05-15
Multi-pane workspace (batch 3, T1T8). `session_panes` schema (later replaced by `sessions.workspace_panes jsonb` in v1.12.0), `Pane` discriminated union, broker user channel + `/api/ws/user`, `file_ops` + `file_index` services, `PaneShell` / `ChatPane` / `FileBrowserPane` / `PaneTab` / `Workspace` components, `usePanes` hook, Shiki integration in `CodeBlock`. Up to 5 panes per session; default chat pane created on `POST /api/sessions`.
## v1.1.0-markdown-sidebar — 2026-05-15
Markdown rendering, message actions, tok/s + ctx display, AI session naming. Sidebar restructure — chats nested under projects (max 5 + view-all), live updates via WS.
## v1.0.0-initial — 2026-05-14
Initial commit. Skeleton of the monorepo: `apps/server` (Fastify + postgres), `apps/web` (React + Vite), basic chat loop against llama-swap.

View File

@@ -46,12 +46,24 @@ Tests: `pnpm -C apps/server test` runs the vitest suite. No test harness on `app
- **Zod** for request validation and config parsing.
Key services:
- **`services/inference/`** (v1.12.4 split — was a single `inference.ts` file). Public surface re-exported via `inference/index.ts`; callers import from `./services/inference/index.js`. Layout: `turn.ts` (runAssistantTurn / runInference / createInferenceRunner orchestration, plus `InferenceFrame`, `InferenceContext`, `TurnArgs`, `StreamResult` exported), `stream-phase.ts` (streamCompletion + executeStreamPhase + SSE parsing), `tool-phase.ts` (executeToolPhase; back-edges into turn.ts for the runAssistantTurn recursion — cycle is safe because dereferenced at call time, not module top-level), `sentinel-summaries.ts` (runCapHitSummary + runDoomLoopSummary + their sentinel inserters; two near-clones kept side-by-side until a third sentinel justifies factoring out runWrapUpSummary), `error-handler.ts` (handleAbortOrError, finalizeCompletion), `payload.ts` (buildMessagesPayload, loadContext, maybeFlagForCompaction, `OpenAiMessage`), `sentinels.ts` (`detectDoomLoop`, `DOOM_LOOP_THRESHOLD`, sentinel predicates), `budget.ts` (resolveToolBudget), `xml-parser.ts` (Qwen-coder XML tool-call fallback), `types.ts` (`StreamPhaseState`, `DB_FLUSH_INTERVAL_MS` shared between stream-phase and sentinel-summaries). **`TurnArgs`** is the per-turn state envelope threaded through the `executeToolPhase → runAssistantTurn` recursion (`toolsUsed`, `recentToolCalls`, `assistantMessageId`, `signal`); reset to defaults in `runInference` at the user-message boundary. Cap-hit (`toolsUsed >= budget`) and doom-loop (`detectDoomLoop(recentToolCalls)`) checks both read from this envelope. Add new per-turn state to `TurnArgs` in `turn.ts`, not module-level closures.
- **`services/inference/`** Public surface re-exported via `inference/index.ts`; callers import from `./services/inference/index.js` explicitly (NodeNext doesn't honor directory-index resolution). Layout: `turn.ts` (runAssistantTurn / runInference / createInferenceRunner; exports `InferenceFrame`, `InferenceContext`, `TurnArgs`, `StreamResult`), `stream-phase.ts` (streamCompletion as a v1.13.1-A AI SDK adapter + executeStreamPhase), `provider.ts` (`upstreamModel(baseURL, modelId)` wrapping `createOpenAICompatible` against llama-swap), `tool-phase.ts` (executeToolPhase; value back-edges into turn.ts for the runAssistantTurn recursion — cycle safe because deref at call time, not module top-level), `sentinel-summaries.ts` (runCapHitSummary + runDoomLoopSummary + their sentinel inserters), `error-handler.ts` (handleAbortOrError, finalizeCompletion), `payload.ts` (buildMessagesPayload, loadContext, maybeFlagForCompaction, `OpenAiMessage`), `sentinels.ts` (`detectDoomLoop`, `DOOM_LOOP_THRESHOLD`, sentinel predicates), `budget.ts` (resolveToolBudget), `xml-parser.ts` (qwen3.6 XML tool-call fallback — KEEP, AI SDK doesn't handle inline-XML tool calls), `parts.ts` (v1.13.0 dual-write helpers: `partsFromAssistantMessage`, `partsFromToolMessage`, `insertParts`), `prune.ts` (v1.13.4 two-tier compaction; `selectPruneTargets` is the pure decision helper), `types.ts` (`StreamPhaseState`, `DB_FLUSH_INTERVAL_MS`). **`TurnArgs`** is the per-turn state envelope threaded through the `executeToolPhase → runAssistantTurn` recursion; reset in `runInference` at user-message boundary. Add new per-turn state to `TurnArgs`, not module-level closures.
- **AI SDK v6 streamCompletion adapter** (v1.13.1-A; `services/inference/stream-phase.ts`). `streamText` is the underlying call; the BooCode layer above (executeStreamPhase, finalize, dual-write) is shape-preserved via an adapter. Five gotchas the LSP/test suite won't catch:
- **Abort signals are swallowed.** `streamText`'s `fullStream` iterator exits cleanly when `abortSignal` fires — no throw. Post-iteration `if (signal?.aborted) throw <AbortError>` is required; without it the row finalizes as `complete` instead of `cancelled`. Comment in stream-phase.ts pins this; don't refactor it away.
- **Usage lands only at stream end** via `await result.usage` (`inputTokens` / `outputTokens` v6 names → mapped to `promptTokens` / `completionTokens` for the existing onUsage callback). Mid-stream live tok/s is gone vs v1.12.2; ChatThroughput shows a single value at stream end.
- **Tools have NO `execute` field.** BooCode dispatches tools in tool-phase.ts, not the AI SDK loop. Only `description` + `inputSchema: jsonSchema(parameters)` — surfacing tool-call parts via `fullStream` and stopping is what we want.
- **`includeUsage: true` MUST be set on `createOpenAICompatible`** in `services/inference/provider.ts`. The adapter defaults it false, omitting `stream_options.include_usage` from the request body; llama-swap then never emits the usage block and `result.usage.inputTokens/outputTokens` resolve to `undefined`. Latent regression from v1.13.1-A through v1.13.7 — every assistant row in that window has `tokens_used`/`ctx_used` NULL. Don't remove this flag during refactor.
- **Tool-call-only turns may emit a leading `\n` text-delta** as the assistant content. `MessageList.flatten`'s `hasText` and `MessageBubble`'s `hasContent` both `.trim()` before the length check — otherwise whitespace-only content renders an empty bubble + ActionRow between every tool call (v1.13.7 fix). `payload.ts:buildMessagesPayload` also skips `status='failed'` AND complete-but-empty (no content, no tool_calls) assistant rows to avoid "Cannot have 2 or more assistant messages at the end of the list" upstream rejections after cap-hit + Continue.
- **AI SDK ModelMessage conversion** (`toModelMessages` in stream-phase.ts). Tool messages need a `toolName` for `ToolResultPart` — BooCode's OpenAI-shape history doesn't carry it, so a forward-scan builds a `tool_call_id → toolName` map from prior assistant `tool_calls`. Tool outputs wrapped as `{ type: 'json' | 'text', value }` matching the v6 `ToolResultOutput` union. Assistant messages with reasoning emit a `ReasoningPart` first in the content array (v1.13.1-C).
- **`experimental_repairToolCall`** (v1.13.3) wired into `streamText` to keep the stream alive when qwen3.6 emits malformed tool args. Pass-through implementation — logs the bad call and returns it unmodified; `executeToolPhase`'s existing zod-reject error path routes it to the model on the next turn.
- **`chat_status` frame shape** (published via `broker.publishUser`) — `status: 'streaming' | 'tool_running' | 'waiting_for_input' | 'idle' | 'error'` (widened from `working|idle|error` in v1.12.1). Frontend `useChatStatus` derives `idle_warm` (<30s since idle) vs `idle_cold`. `ChatThroughput` renders inline beside `StatusDot` only when streaming or tool_running, fed by 500ms-throttled `'usage'` WS frames (`completion_tokens` + `ctx_used` + `ctx_max`). The `POST /api/chats/:id/discard_stale` endpoint exists to mark a stuck-streaming row as `failed` when the frontend's 60s no-token-activity timer (`ChatPane` content-length watcher) gives up.
- **Boot-time stale-streaming sweep** in `apps/server/src/index.ts` after `applySchema()`: any `messages.status='streaming'` older than 5 minutes flips to `'failed'`. Logs only on non-zero count. Recovers from container restart while inference was mid-stream (v1.12.1).
- **`services/broker.ts`** — In-memory pub/sub with two channel types: per-session (message streaming) and per-user (sidebar updates). No persistence; clients reconnect on restart.
- **`services/tools.ts`** — Tool registry (`ALL_TOOLS`, `READ_ONLY_TOOL_NAMES`, `TOOLS_BY_NAME`). Filesystem tools (view_file/list_dir/grep/find_files) go through three guard layers: `path_guard.ts` (workspace scope), `secret_guard.ts` (filename deny list), `url_guard.ts` (SSRF/private-IP block for web_fetch). v1.11.8+ web tools (`web_search`, `web_fetch`) are opt-in per chat via `session.web_search_enabled` (resolved with `project.default_web_search_enabled` fallback) and filtered out of the LLM's tool schema when false.
- **`services/compaction.ts`** + **`services/model-context.ts`** — v1.11.0 anchored rolling summary (single `summary=true` assistant row per chat, supersedes itself on each compaction). Triggered when `chats.needs_compaction` is set after an inference turn exceeds `usable(ctx_max) = ctx_max - 20k`. **`ctx_max` comes from `model-context.getModelContext()` which fetches `${LLAMA_SWAP_URL}/upstream/<model>/props`** — NOT from `parsed.timings.n_ctx` (the stream completion's `timings` doesn't carry n_ctx; that read was dead code until v1.11.3 ripped it out).
- **Periodic 60s sweeper** in `apps/server/src/index.ts` (v1.13.3 + v1.13.5). Same `setInterval` runs `sweepStaleStreaming` (marks `messages.status='streaming'` older than 5 min as `failed`, publishes `chat_status='idle'` so the UI dot drops) and `cleanupTruncations` (TTL + orphan reap of tmpfs truncation files). `app.addHook('onClose')` clears the timer. No-op when nothing to reap.
- **`services/broker.ts`** — In-memory pub/sub with two channel types: per-session (message streaming) and per-user (sidebar updates). No persistence; clients reconnect on restart. v1.13.11: every WS publish goes through `broker.publishFrame(sessionId, frame)` or `broker.publishUserFrame(user, frame)` — both Zod-validate against `WsFrameSchema` (`types/ws-frames.ts`) and fail-closed (log + drop). `ctx.publish` / `ctx.publishUser` in inference + auto_name route through the index.ts adapter that calls publishFrame internally. The schema is duplicated byte-identical at `apps/web/src/api/ws-frames.ts`; a `ws-frames.test.ts` case enforces parity. Don't add new raw `broker.publish()` / `publishUser()` calls.
- **`services/tools.ts`** — Tool registry (`ALL_TOOLS`, `READ_ONLY_TOOL_NAMES`, `TOOLS_BY_NAME`). Filesystem tools (view_file/list_dir/grep/find_files) go through three guard layers: `path_guard.ts` (workspace scope), `secret_guard.ts` (filename deny list), `url_guard.ts` (SSRF/private-IP block for web_fetch). v1.11.8+ web tools (`web_search`, `web_fetch`) are opt-in per chat via `session.web_search_enabled` (resolved with `project.default_web_search_enabled` fallback) and filtered out of the LLM's tool schema when false. v1.13.5 truncation: when a tool slice cuts content, `services/truncate.ts` stashes the full text on tmpfs at `BOOCODE_TRUNCATION_DIR` (default `/tmp/boocode-truncations`, 0o700) keyed by an opaque `tr_<12 base32 chars>` id, and the `view_truncated_output(id)` tool retrieves it. 5MB cap (matches `view_file`'s `MAX_FILE_BYTES`), 7-day TTL, reaped by the periodic sweeper. Tmpfs path means container restart loses retrieval — acceptable, the model usually has moved on.
- **`services/compaction.ts`** + **`services/model-context.ts`** — v1.11.0 anchored rolling summary (single `summary=true` assistant row per chat, supersedes itself on each compaction). Triggered when `chats.needs_compaction` is set after an inference turn exceeds `usable(ctx_max) = floor(0.85 × ctx_max)` (v1.13.9 opencode-pattern early trigger; was `ctx_max - 20k` pre-v1.13.9, which gave only 7.6% headroom at 262k and 0 budget for ≤20k contexts). **`ctx_max` comes from `model-context.getModelContext()` which fetches `${LLAMA_SWAP_URL}/upstream/<model>/props`** — NOT from `parsed.timings.n_ctx` (the stream completion's `timings` doesn't carry n_ctx; that read was dead code until v1.11.3 ripped it out). First inferences after a boocode boot may have `ctx_max=NULL` if llama-swap hasn't loaded the model yet; negative cache TTL is 60s, recovers on next turn. v1.13.6: `buildHeadPayload` embeds `reasoning_parts` as a `<reasoning>...</reasoning>` prose prefix on the assistant `content` (OpenAI wire shape has no structured reasoning field; the summarizer reads text). Standalone tag when content is empty (tool-call-only turn). `buildHeadPayload` + `OpenAiMessage` exported for test access — keep them exported.
- **`services/system-prompt.ts`** — `buildSystemPrompt` is the string-returning shim; `buildSystemPromptWithFingerprint` is the canonical impl returning `{prompt, fingerprint, drift}`. v1.13.8 instrumentation: SHA-256 of the assembled prefix is logged per `buildMessagesPayload` call (msg `prefix-fingerprint`, level=info); a `Map<sessionId, lastHash>` observer fires `prefix-drift` (level=warn) on hash change with a field-level `changed_inputs` diff. Smoke proved the prefix is byte-stable across turns in steady-state — the originally-planned `system_prompt_cache` DB table was dropped as redundant against the v1.12.0 input-layer mtime caches (BOOCHAT.md here + AGENTS.md global+per-project in `agents.ts:safeStat`).
- **`services/inference/budget.ts`** — tool-call budgets: `BUDGET_READ_ONLY = 30`, `BUDGET_NON_READ_ONLY = 10` (forward-looking; no write tools yet), `BUDGET_NO_AGENT = 30` (v1.13.7; was 15 — every tool in `ALL_TOOLS` is read-only today, so no-agent mode shares the read-only-agent cap). Per-agent `max_tool_calls` from AGENTS.md frontmatter overrides.
- **`messages_with_parts` view** (v1.13.1-B; `schema.sql`). Read sites that need `tool_calls` / `tool_results` / `reasoning_parts` SELECT from this view, NOT `messages` directly. `COALESCE`s parts-table rows over the legacy JSON columns, so pre-v1.13.0 history still resolves. Writes still target `messages`; the v1.13.0 dual-write into `message_parts` keeps both halves in sync. New payload-assembly code must use the view — calling `messages.tool_calls` directly will miss anything written post-v1.13.1-B if the JSON column ever drifts (and dual-write makes that easy to miss). Shapes: `tool_calls jsonb[]`, `tool_results jsonb` single object, `reasoning_parts jsonb[]` of `{text}`.
- **`services/file_ops.ts`** — Shared file operation implementations used by both inference tools and HTTP routes.
- **`services/auto_name.ts`** — Non-streaming LLM call to generate 4-word session titles after first assistant reply.
@@ -93,21 +105,24 @@ Sessions hold 15 panes (chat / empty / placeholder terminal+agent). v1.12.1 m
## Database
PostgreSQL 16. Tables: `projects`, `sessions`, `chats`, `messages`, `settings`. (`session_panes` was dropped in v1.12.1; workspace pane state lives in `sessions.workspace_panes jsonb`.) Schema applied idempotently on startup via `applySchema()`. Use `clock_timestamp()` (not `NOW()`) inside transactions. CHECK constraints in place: `projects_status_chk` ('open'|'archived'), `sessions_status_chk` (same), `chats_status_chk` (same), `messages_role_chk`, `messages_status_chk` — keep in sync with the `*_STATUSES` const arrays in `apps/server/src/types/api.ts`. The older anonymous `messages_status_check` (without 'cancelled') and `messages_role_check` (without 'system') were dropped in v1.12.1; only the `_chk` variants remain.
PostgreSQL 16. Tables: `projects`, `sessions`, `chats`, `messages`, `settings`, `message_parts` (v1.13.0). Views: `messages_with_parts` (v1.13.1-B parts-merge read path), `tool_cost_stats` (v1.13.10 per-tool 100-call rolling window). (`session_panes` was dropped in v1.12.1; workspace pane state lives in `sessions.workspace_panes jsonb`.) Schema applied idempotently on startup via `applySchema()`. Use `clock_timestamp()` (not `NOW()`) inside transactions. CHECK constraints in place: `projects_status_chk` ('open'|'archived'), `sessions_status_chk` (same), `chats_status_chk` (same), `messages_role_chk`, `messages_status_chk` — keep in sync with the `*_STATUSES` const arrays in `apps/server/src/types/api.ts`. The older anonymous `messages_status_check` (without 'cancelled') and `messages_role_check` (without 'system') were dropped in v1.12.1; only the `_chk` variants remain.
Schema CHECK migration order when renaming allowed values: (1) `ALTER TABLE ... DROP CONSTRAINT IF EXISTS <system_name>` (inline `CREATE TABLE` checks get `<table>_<column>_check`), (2) `UPDATE` rows to new values, (3) wrap new constraint ADD in `DO $$ ... pg_constraint` guard — that block is the only way to get `ADD CONSTRAINT IF NOT EXISTS`.
## Environment
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only scope for add-existing path resolution), `BOOTSTRAP_ROOT` (/opt/projects, writable scope for create-new-project bootstrap mkdir target — host must `mkdir -p /opt/projects` before container start), `DEFAULT_MODEL`, `LOG_LEVEL`, `SEARXNG_URL` (default `http://100.114.205.53:8888` — internal Tailscale Fathom; the public `search.indifferentketchup.com` is behind Authelia and unusable from server context).
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only scope for add-existing path resolution), `BOOTSTRAP_ROOT` (/opt/projects, writable scope for create-new-project bootstrap mkdir target — host must `mkdir -p /opt/projects` before container start), `DEFAULT_MODEL`, `LOG_LEVEL`, `SEARXNG_URL` (default `http://100.114.205.53:8888` — internal Tailscale Fathom; the public `search.indifferentketchup.com` is behind Authelia and unusable from server context), `BOOCODE_TOOLS` (`core` | `standard` | `all`, default `all`; v1.13.15-tools tier filter — ceiling, never expands an agent's whitelist).
## Workflow
- Sam reviews all diffs and commits manually. Do not commit unless explicitly asked.
- Per-batch docs live under `openspec/changes/<slug>/{proposal,tasks,design}.md`. Already-shipped batches are snapshots in `openspec/changes/archived/`. New batches follow the proposal+tasks shape; see `openspec/README.md` for the convention.
- Deploy: `cd /opt/boocode && docker compose up --build -d` (or `docker compose build --no-cache boocode && docker compose up -d` if you suspect a layer-cache issue).
- Git push to Gitea: `GIT_SSH_COMMAND="ssh -i /opt/boocode/secrets/boocode_gitea -o IdentitiesOnly=yes" git push origin <branch>`. The default agent identity is rejected; the in-repo deploy key (`secrets/`, gitignored) is the working one. Transient `Connection reset by peer` retries cleanly after `sleep 5`.
- Don't accumulate `.bak-*` files. Clean them up in the same batch or immediately after merge.
- DB-integration tests opt-in via env var: `DATABASE_URL='postgres://boocode:devpass@localhost:5500/boocode' pnpm -C apps/server test`. Host port is 5500 (mapped from `boocode_db:5432`); password is `${POSTGRES_PASSWORD}` from `.env` (`devpass`), NOT the literal in `.env`'s `DATABASE_URL=postgres://boocode:Ketchup1479@boocode_db:5432/...` line. Pattern: `describe.runIf(!!process.env.DATABASE_URL)(...)` with a `beforeAll` that applies the schema via `sql.unsafe(readFileSync(schemaPath))`. Tests skip cleanly when var is unset. `tool_cost_stats.test.ts` is the reference.
- Host-side smoke endpoint: `curl http://100.114.205.53:9500/api/...`. The boocode container's port mapping binds to the Tailscale IP, not `0.0.0.0`, so `localhost:9500` doesn't work from the host shell. Same for booterm at `:9501`.
- Fastify global JSON parser tolerates empty bodies (overridden in `index.ts`); bodyless POSTs (archive, unarchive, stop) work without setting `Content-Type` tricks on the client.
- Event dedup discipline: for any mutation the server publishes via `broker.publishUser`, do NOT add a local `sessionEvents.emit(...)` after the API call — `useUserEvents` forwards the WS frame onto the bus. Frontend mutation handlers must be idempotent (dedup by id, no-op on already-present).
- `node:20-*` base images ship a `node` user at uid/gid 1000 — delete it (`userdel`/`groupdel` on debian, `deluser`/`delgroup` on alpine) before adding samkintop at 1000.

View File

@@ -16,6 +16,7 @@ import { registerWebSocket } from './routes/ws.js';
import { registerModelRoutes } from './routes/models.js';
import { registerAgentRoutes } from './routes/agents.js';
import { registerSkillsRoutes } from './routes/skills.js';
import { registerToolsRoutes } from './routes/tools.js';
import { createInferenceRunner } from './services/inference/index.js';
import { createBroker } from './services/broker.js';
import { listSkills } from './services/skills.js';
@@ -74,7 +75,7 @@ async function main() {
return { status: dbOk ? 'ok' : 'degraded', db: dbOk };
});
const broker = createBroker();
const broker = createBroker(app.log);
registerProjectRoutes(app, sql, config, broker);
registerSessionRoutes(app, sql, config, broker);
@@ -83,6 +84,7 @@ async function main() {
registerAgentRoutes(app, sql);
registerSidebarRoutes(app, sql);
registerChatRoutes(app, sql, broker);
registerToolsRoutes(app, sql);
// Batch 9.6: warm the skills cache at boot and surface the count. Empty or
// missing /data/skills is non-fatal — the skill tools just return empty.
@@ -99,7 +101,9 @@ async function main() {
config,
log: app.log,
publish: (sessionId, frame) => {
broker.publish(sessionId, frame as unknown as Record<string, unknown> & { type: string });
// v1.13.11-b: route through the typed publishFrame so the broker's
// Zod gate validates every inference frame before delivery.
broker.publishFrame(sessionId, frame as unknown as import('./types/ws-frames.js').WsFrame);
},
// v1.11: broker handle for compaction.process to publish 'compacted'
// frames on the per-session channel. Inference's regular publish path
@@ -108,7 +112,7 @@ async function main() {
broker,
},
(user, frame) => {
broker.publishUser(user, frame as unknown as Record<string, unknown> & { type: string });
broker.publishUserFrame(user, frame as unknown as import('./types/ws-frames.js').WsFrame);
}
);
registerMessageRoutes(app, sql, {
@@ -127,33 +131,33 @@ async function main() {
},
hasActiveInference: (chatId) => inference.hasActive(chatId),
publishUserMessage: (sessionId, chatId, userMessageId, content) => {
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'message_started',
message_id: userMessageId,
chat_id: chatId,
role: 'user',
});
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'delta',
message_id: userMessageId,
chat_id: chatId,
content,
});
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'message_complete',
message_id: userMessageId,
chat_id: chatId,
});
},
publishMessagesDeleted: (sessionId, chatId, messageIds) => {
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'messages_deleted',
message_ids: messageIds,
chat_id: chatId,
});
},
publishSessionFrame: (sessionId, frame) => {
broker.publish(sessionId, frame);
broker.publishFrame(sessionId, frame as import('./types/ws-frames.js').WsFrame);
},
});
registerSkillsRoutes(app, sql, {
@@ -161,26 +165,26 @@ async function main() {
inference.enqueue(sessionId, chatId, assistantId, user);
},
publishUserMessage: (sessionId, chatId, userMessageId, content) => {
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'message_started',
message_id: userMessageId,
chat_id: chatId,
role: 'user',
});
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'delta',
message_id: userMessageId,
chat_id: chatId,
content,
});
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'message_complete',
message_id: userMessageId,
chat_id: chatId,
});
},
publishSessionFrame: (sessionId, frame) => {
broker.publish(sessionId, frame);
broker.publishFrame(sessionId, frame as import('./types/ws-frames.js').WsFrame);
},
});
registerWebSocket(app, sql, broker);
@@ -228,7 +232,7 @@ async function main() {
for (const row of rows) {
if (seenChats.has(row.chat_id)) continue;
seenChats.add(row.chat_id);
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_status',
chat_id: row.chat_id,
status: 'idle',

View File

@@ -102,7 +102,7 @@ export function registerChatRoutes(
VALUES (${req.params.id}, ${parsed.data.name ?? null}, 'open')
RETURNING id, session_id, name, status, created_at, updated_at
`;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_created',
chat: chat!,
session_id: req.params.id,
@@ -132,7 +132,7 @@ export function registerChatRoutes(
return { error: 'chat not found' };
}
const chat = rows[0]!;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_updated',
chat_id: chat.id,
session_id: chat.session_id,
@@ -162,7 +162,7 @@ export function registerChatRoutes(
`;
const ids = rows.map((r) => r.id);
for (const id of ids) {
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_archived',
chat_id: id,
session_id: req.params.id,
@@ -203,7 +203,7 @@ export function registerChatRoutes(
return { error: 'chat not found or already archived' };
}
const row = rows[0]!;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_archived',
chat_id: row.id,
session_id: row.session_id,
@@ -226,7 +226,7 @@ export function registerChatRoutes(
return { error: 'chat not found or not archived' };
}
const chat = rows[0]!;
broker.publishUser('default', { type: 'chat_unarchived', chat });
broker.publishUserFrame('default', { type: 'chat_unarchived', chat });
return chat;
}
);
@@ -243,7 +243,7 @@ export function registerChatRoutes(
return { error: 'chat not found' };
}
const row = result[0]!;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_deleted',
chat_id: row.id,
session_id: row.session_id,
@@ -338,7 +338,7 @@ export function registerChatRoutes(
return chat!;
});
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_created',
chat: newChat,
session_id: source.session_id,
@@ -400,13 +400,13 @@ export function registerChatRoutes(
reply.code(409);
return { error: 'message status changed mid-request' };
}
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_status',
chat_id: msg.chat_id,
status: 'idle',
at: new Date().toISOString(),
});
broker.publish(msg.session_id, {
broker.publishFrame(msg.session_id, {
type: 'message_complete',
message_id: msg.id,
chat_id: msg.chat_id,

View File

@@ -129,7 +129,7 @@ export function registerProjectRoutes(
RETURNING id, name, path, added_at, last_session_id, status, gitea_remote,
default_system_prompt, default_web_search_enabled
`;
broker.publishUser('default', { type: 'project_created', project: row as unknown as Project });
broker.publishUserFrame('default', { type: 'project_created', project: row as unknown as Project });
reply.code(201);
return {
project: row,
@@ -186,11 +186,11 @@ export function registerProjectRoutes(
`;
if (existing.length === 0) {
broker.publishUser('default', { type: 'project_created', project: row as unknown as Project });
broker.publishUserFrame('default', { type: 'project_created', project: row as unknown as Project });
reply.code(201);
} else {
// existing.status was 'archived' — row has been restored.
broker.publishUser('default', { type: 'project_unarchived', project: row as unknown as Project });
broker.publishUserFrame('default', { type: 'project_unarchived', project: row as unknown as Project });
reply.code(200);
}
return row;
@@ -243,7 +243,7 @@ export function registerProjectRoutes(
// v1.9: the project_updated frame still only carries id + name. Clients
// that need the new fields refetch via api.projects.list() — keeps the
// frame payload lean, per the locked recon decision (d).
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'project_updated',
project_id: project.id,
name: project.name,
@@ -260,7 +260,7 @@ export function registerProjectRoutes(
reply.code(404);
return { error: 'not found or already archived' };
}
broker.publishUser('default', { type: 'project_archived', project_id: req.params.id });
broker.publishUserFrame('default', { type: 'project_archived', project_id: req.params.id });
reply.code(204);
return null;
});
@@ -277,7 +277,7 @@ export function registerProjectRoutes(
return { error: 'not found or not archived' };
}
const project = rows[0]!;
broker.publishUser('default', { type: 'project_unarchived', project });
broker.publishUserFrame('default', { type: 'project_unarchived', project });
return project;
});
@@ -288,7 +288,7 @@ export function registerProjectRoutes(
reply.code(404);
return { error: 'not found' };
}
broker.publishUser('default', { type: 'project_deleted', project_id: id });
broker.publishUserFrame('default', { type: 'project_deleted', project_id: id });
reply.code(204);
return null;
});

View File

@@ -112,7 +112,7 @@ export function registerSessionRoutes(
`;
return session!;
});
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_created',
session: row,
project_id: row.project_id,
@@ -178,7 +178,7 @@ export function registerSessionRoutes(
}
const session = rows[0]!;
if (name !== undefined && session.name !== priorName) {
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_renamed',
session_id: session.id,
name: session.name,
@@ -188,7 +188,7 @@ export function registerSessionRoutes(
// (notably the SettingsPane open in another tab) can refetch and pick
// up the new fields. Frame stays lean (decision d) — payload is just
// ids + name + updated_at, the client refetches via api.sessions.get.
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_updated',
session_id: session.id,
project_id: session.project_id,
@@ -220,7 +220,7 @@ export function registerSessionRoutes(
return { error: 'session not found' };
}
const session = rows[0]!;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_workspace_updated',
session_id: session.id,
workspace_panes: session.workspace_panes,
@@ -248,7 +248,7 @@ export function registerSessionRoutes(
`;
const ids = rows.map((r) => r.id);
for (const id of ids) {
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_archived',
session_id: id,
project_id: req.params.id,
@@ -289,7 +289,7 @@ export function registerSessionRoutes(
reply.code(404);
return { error: 'session not found or already archived' };
}
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_archived',
session_id: rows[0]!.id,
project_id: rows[0]!.project_id,
@@ -312,7 +312,7 @@ export function registerSessionRoutes(
return { error: 'session not found or not archived' };
}
const session = rows[0]!;
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'session_created',
session: session,
project_id: session.project_id,
@@ -334,7 +334,7 @@ export function registerSessionRoutes(
return { error: 'not found' };
}
const project_id = deleted[0]!.project_id;
broker.publishUser('default', { type: 'session_deleted', session_id: id, project_id });
broker.publishUserFrame('default', { type: 'session_deleted', session_id: id, project_id });
reply.code(204);
return null;
}

View File

@@ -0,0 +1,40 @@
import type { FastifyInstance } from 'fastify';
import type { Sql } from '../db.js';
export interface ToolCostStat {
tool_name: string;
mean_prompt_tokens: number;
mean_completion_tokens: number;
n_calls: number;
updated_at: string;
}
// v1.13.10: per-tool token cost rolling window read endpoint. Backed by the
// tool_cost_stats view in schema.sql (last 100 calls per tool, equal-split
// attribution across multi-tool turns, sentinel/failed-turn excluded).
// Consumed by AgentPicker for at-a-glance per-agent cost hints.
export function registerToolsRoutes(app: FastifyInstance, sql: Sql): void {
app.get('/api/tools/cost_stats', async () => {
const rows = await sql<
{
tool_name: string;
prompt_tokens_sum: number;
completion_tokens_sum: number;
n_calls: number;
updated_at: string;
}[]
>`
SELECT tool_name, prompt_tokens_sum, completion_tokens_sum, n_calls, updated_at
FROM tool_cost_stats
ORDER BY tool_name ASC
`;
const stats: ToolCostStat[] = rows.map((r) => ({
tool_name: r.tool_name,
mean_prompt_tokens: Math.round(r.prompt_tokens_sum / r.n_calls),
mean_completion_tokens: Math.round(r.completion_tokens_sum / r.n_calls),
n_calls: r.n_calls,
updated_at: r.updated_at,
}));
return { stats };
});
}

View File

@@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS message_parts (
kind text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT message_parts_kind_chk CHECK (kind IN ('text', 'tool_call', 'tool_result', 'reasoning', 'step_start')),
CONSTRAINT message_parts_kind_chk CHECK (kind IN ('text', 'tool_call', 'tool_result', 'reasoning', 'step_start', 'synthesis')),
CONSTRAINT message_parts_seq_uniq UNIQUE (message_id, sequence)
);
CREATE INDEX IF NOT EXISTS message_parts_msg_seq_idx ON message_parts (message_id, sequence);
@@ -74,6 +74,23 @@ END $$;
CREATE INDEX IF NOT EXISTS message_parts_hidden_idx
ON message_parts (message_id) WHERE hidden_at IS NULL;
-- v1.13.13: extend message_parts.kind to allow 'synthesis'. Existing DBs were
-- created with the pre-v1.13.13 CHECK constraint that did NOT include
-- 'synthesis'; drop + re-add the constraint with the extended enum. Fresh
-- installs hit the inline constraint above (already updated) and skip this
-- block via the pg_constraint guard.
ALTER TABLE message_parts DROP CONSTRAINT IF EXISTS message_parts_kind_chk;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'message_parts_kind_chk'
) THEN
ALTER TABLE message_parts
ADD CONSTRAINT message_parts_kind_chk
CHECK (kind IN ('text', 'tool_call', 'tool_result', 'reasoning', 'step_start', 'synthesis'));
END IF;
END $$;
-- v1.13.1-B: read-path view. Read sites SELECT FROM messages_with_parts
-- instead of messages so tool_calls / tool_results / reasoning_parts come
-- from the granular message_parts table. The COALESCE means pre-v1.13.0
@@ -119,6 +136,68 @@ SELECT
WHERE p.message_id = m.id AND p.kind = 'reasoning' AND p.hidden_at IS NULL) AS reasoning_parts
FROM messages m;
-- v1.13.10: per-tool token cost rolling window. Derives from
-- messages_with_parts (the v1.13.1-B view that COALESCEs message_parts over
-- the legacy JSON column) so this works whether the chat predates v1.13.0
-- or postdates v1.13.2 (column drop). No new write site — all source data
-- already lands via the existing tool-phase.ts:94-95 UPDATE.
--
-- Attribution model: equal split. A turn emitting N tool calls divides its
-- prompt/completion tokens by N before attribution. See v1.13.10 dispatch
-- brief for rationale + rejected alternatives.
--
-- Column mapping: messages.ctx_used = prompt (input), messages.tokens_used
-- = completion (output). Non-obvious naming; pinned via canonical writes at
-- tool-phase.ts:94-95 et al.
--
-- Filtering rationale:
-- status='complete' — exclude failed/cancelled (defense in
-- depth; failed-path doesn't write
-- tokens_used so they're filtered
-- indirectly too).
-- metadata->>'kind' exclusions — exclude cap_hit / doom_loop sentinels
-- (defense in depth; sentinels are
-- role='system' with tool_calls=NULL
-- so they're filtered indirectly too).
-- experimental_repairToolCall — no special handling; retries flow
-- as normal next-turn tool_result
-- errors and count naturally.
--
-- Rolling window: last 100 calls per tool_name, ordered by created_at DESC.
-- Aggregate-on-read is microseconds at BooCode scale (single user, ~30
-- tools, < 100 calls each). DROP VIEW + recreate to change window size.
CREATE OR REPLACE VIEW tool_cost_stats AS
WITH per_call AS (
SELECT
(tc->>'name')::text AS tool_name,
(m.ctx_used::float / NULLIF(jsonb_array_length(m.tool_calls), 0)) AS prompt_tokens,
(m.tokens_used::float / NULLIF(jsonb_array_length(m.tool_calls), 0)) AS completion_tokens,
m.created_at,
ROW_NUMBER() OVER (
PARTITION BY (tc->>'name')::text
ORDER BY m.created_at DESC
) AS rn
FROM messages_with_parts m,
LATERAL jsonb_array_elements(m.tool_calls) AS tc
WHERE m.tool_calls IS NOT NULL
AND jsonb_array_length(m.tool_calls) > 0
AND m.tokens_used IS NOT NULL
AND m.ctx_used IS NOT NULL
AND m.status = 'complete'
AND (m.metadata IS NULL
OR m.metadata->>'kind' IS NULL
OR m.metadata->>'kind' NOT IN ('cap_hit', 'doom_loop'))
)
SELECT
tool_name,
ROUND(SUM(prompt_tokens))::int AS prompt_tokens_sum,
ROUND(SUM(completion_tokens))::int AS completion_tokens_sum,
COUNT(*)::int AS n_calls,
MAX(created_at) AS updated_at
FROM per_call
WHERE rn <= 100
GROUP BY tool_name;
ALTER TABLE messages ADD COLUMN IF NOT EXISTS tokens_used INTEGER;
ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_used INTEGER;
ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_max INTEGER;

View File

@@ -6,6 +6,7 @@ import {
turns,
select,
buildPrompt,
buildHeadPayload,
type CompactionMessage,
} from '../compaction.js';
import { SUMMARY_TEMPLATE } from '../compaction-prompt.js';
@@ -31,6 +32,7 @@ function mkMsg(
status: 'complete',
tool_calls: null,
tool_results: null,
reasoning_parts: null,
metadata: null,
created_at: new Date(counter * 1000).toISOString(),
...overrides,
@@ -39,49 +41,58 @@ function mkMsg(
// ---- usable -----------------------------------------------------------------
describe('usable', () => {
it('returns 0 when contextLimit is 0', () => {
// v1.13.9: ratio-only early trigger at 0.85 × contextLimit. Replaces the
// v1.11.0-era `contextLimit - 20_000` math, which degenerated to 0 for
// contexts ≤20k and gave only 7-8% headroom at 262k.
describe('usable() — ratio-only early trigger (v1.13.9)', () => {
it('returns floor(0.85 * limit) for the qwen3.6 daily-driver context', () => {
// floor(0.85 * 262144) = floor(222822.4) = 222822 — 15% headroom for
// the summarizer to do its turn without itself overflowing.
expect(usable(262144)).toBe(222822);
});
it('returns 0.85× for a mid-sized context', () => {
expect(usable(100_000)).toBe(85_000);
});
it('returns 0.85× for a small context (no degenerate 0)', () => {
// floor(0.85 * 8192) = 6963. Under the old formula this returned 0
// (8192 - 20_000 clamped to 0), effectively disabling compaction for
// small-context models. The ratio keeps the trigger active.
expect(usable(8192)).toBe(6963);
});
it('returns 0 for zero or negative contextLimit', () => {
expect(usable(0)).toBe(0);
});
it('returns 0 when contextLimit is below the 20k buffer', () => {
// Math.max(0, x - 20000) clamps the subtraction so we never report
// negative headroom. A 10k-context model reports 0 usable, which makes
// isOverflow short-circuit to false (correct — we can't size the
// compaction with no headroom).
expect(usable(10_000)).toBe(0);
expect(usable(19_999)).toBe(0);
expect(usable(20_000)).toBe(0);
});
it('subtracts the 20k buffer from a normal-sized context window', () => {
expect(usable(100_000)).toBe(80_000);
expect(usable(32_768)).toBe(12_768);
expect(usable(-1)).toBe(0);
});
});
// ---- isOverflow -------------------------------------------------------------
describe('isOverflow', () => {
it('returns false when usable is 0 (unknown / sub-buffer context)', () => {
it('returns false when usable is 0 (unknown contextLimit)', () => {
expect(isOverflow({ prompt_tokens: 999_999, completion_tokens: 0 }, 0)).toBe(false);
expect(isOverflow({ prompt_tokens: 0, completion_tokens: 999_999 }, 10_000)).toBe(false);
expect(isOverflow({ prompt_tokens: 0, completion_tokens: 999_999 }, -1)).toBe(false);
});
it('returns false at 50% of usable', () => {
// usable(100k) = 80k → 50% = 40k.
// v1.13.9: usable(100k) = 85k → 50% 42.5k.
expect(isOverflow({ prompt_tokens: 30_000, completion_tokens: 10_000 }, 100_000)).toBe(false);
});
it('returns false just under usable', () => {
expect(isOverflow({ prompt_tokens: 79_000, completion_tokens: 999 }, 100_000)).toBe(false);
// v1.13.9: 84_000 + 999 = 84_999 < 85_000 budget.
expect(isOverflow({ prompt_tokens: 84_000, completion_tokens: 999 }, 100_000)).toBe(false);
});
it('returns true exactly at usable (>=, not strict >)', () => {
expect(isOverflow({ prompt_tokens: 80_000, completion_tokens: 0 }, 100_000)).toBe(true);
// v1.13.9: 85_000 == usable(100_000).
expect(isOverflow({ prompt_tokens: 85_000, completion_tokens: 0 }, 100_000)).toBe(true);
});
it('returns true above usable', () => {
// 50_000 + 40_000 = 90_000 > 85_000.
expect(isOverflow({ prompt_tokens: 50_000, completion_tokens: 40_000 }, 100_000)).toBe(true);
});
});
@@ -224,8 +235,9 @@ describe('select', () => {
const u = mkMsg('user', 'oversized');
const a = mkMsg('assistant', 'Y'.repeat(40_000));
const result = select([u, a], 30_000, 1);
// usable(30k) = 10k → budget = min(8k, max(2k, floor(10k*0.25))) =
// min(8k, max(2k, 2500)) = 2500. 40k chars ≈ 10k tokens. Can't fit.
// v1.13.9: usable(30k) = floor(0.85*30k) = 25500 → budget =
// min(8k, max(2k, floor(25500*0.25))) = min(8k, max(2k, 6375)) = 6375.
// 40k chars ≈ 10k tokens. Still can't fit (10k > 6375).
expect(result.tail_start_id).toBeUndefined();
expect(result.head).toEqual([u, a]);
});
@@ -256,3 +268,56 @@ describe('buildPrompt', () => {
expect(out.endsWith('extra-context-line')).toBe(true);
});
});
// ---- buildHeadPayload (v1.13.6) -----------------------------------------------
describe('buildHeadPayload reasoning render', () => {
it('emits reasoning as a <reasoning> tag prefixed onto the assistant content', () => {
const out = buildHeadPayload([
mkMsg('user', 'show me the file'),
mkMsg('assistant', 'reading it now', {
reasoning_parts: [{ text: 'user wants src/index.ts; I should view it' }],
}),
]);
expect(out).toHaveLength(2);
expect(out[1]!.role).toBe('assistant');
expect(out[1]!.content).toBe(
'<reasoning>user wants src/index.ts; I should view it</reasoning>\n\nreading it now',
);
});
it('emits a standalone <reasoning> tag when reasoning is present but content is empty (tool-call-only turn)', () => {
const out = buildHeadPayload([
mkMsg('assistant', '', {
reasoning_parts: [{ text: 'jumping straight to grep' }],
tool_calls: [{ id: 'c1', name: 'grep', args: { pattern: 'foo' } }],
}),
]);
expect(out).toHaveLength(1);
expect(out[0]!.content).toBe('<reasoning>jumping straight to grep</reasoning>');
expect(out[0]!.tool_calls).toHaveLength(1);
expect(out[0]!.tool_calls![0]!.function.name).toBe('grep');
});
it('joins multiple reasoning parts without separators (matches the streaming concat)', () => {
const out = buildHeadPayload([
mkMsg('assistant', 'final answer', {
reasoning_parts: [{ text: 'first thought ' }, { text: 'second thought' }],
}),
]);
expect(out[0]!.content).toBe(
'<reasoning>first thought second thought</reasoning>\n\nfinal answer',
);
});
it('omits the reasoning tag entirely when reasoning_parts is null or empty', () => {
const out = buildHeadPayload([
mkMsg('assistant', 'plain answer', { reasoning_parts: null }),
mkMsg('assistant', 'other answer', { reasoning_parts: [] }),
]);
expect(out[0]!.content).toBe('plain answer');
expect(out[1]!.content).toBe('other answer');
expect(out[0]!.content).not.toContain('<reasoning>');
expect(out[1]!.content).not.toContain('<reasoning>');
});
});

View File

@@ -6,7 +6,9 @@ import {
loadContainerGuidance,
getContainerGuidance,
buildSystemPrompt,
buildSystemPromptWithFingerprint,
_resetContainerGuidanceCacheForTests,
_resetPrefixObserverForTests,
} from '../system-prompt.js';
import type { Agent, Project, Session } from '../../types/api.js';
@@ -17,12 +19,14 @@ let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'system-prompt-test-'));
_resetContainerGuidanceCacheForTests();
_resetPrefixObserverForTests();
delete process.env['CONTAINER_GUIDANCE_FILE'];
});
afterEach(async () => {
delete process.env['CONTAINER_GUIDANCE_FILE'];
_resetContainerGuidanceCacheForTests();
_resetPrefixObserverForTests();
await rm(tmpDir, { recursive: true, force: true });
});
@@ -176,3 +180,75 @@ describe('buildSystemPrompt', () => {
expect(prompt).not.toContain('--- end container guidance ---');
});
});
// v1.13.8: byte-stability instrumentation surface.
describe('buildSystemPromptWithFingerprint (v1.13.8)', () => {
it('returns byte-identical prompts for two consecutive calls with the same inputs', async () => {
const path = join(tmpDir, 'BOOCHAT.md');
await writeFile(path, 'stable guidance', 'utf8');
process.env['CONTAINER_GUIDANCE_FILE'] = path;
const session = makeSession();
const project = makeProject({ path: '/tmp/stable-proj' });
const agent = makeAgent({ system_prompt: 'be terse' });
const first = await buildSystemPromptWithFingerprint(project, session, agent);
const second = await buildSystemPromptWithFingerprint(project, session, agent);
expect(first.prompt).toBe(second.prompt);
expect(first.fingerprint.prefix_hash).toBe(second.fingerprint.prefix_hash);
expect(first.fingerprint.prefix_length).toBe(second.fingerprint.prefix_length);
});
it('emits drift=null on the first call for a fresh session, then null again when nothing changes', async () => {
process.env['CONTAINER_GUIDANCE_FILE'] = join(tmpDir, 'absent.md');
const session = makeSession();
const project = makeProject({ path: '/tmp/stable-proj' });
const first = await buildSystemPromptWithFingerprint(project, session, null);
expect(first.drift).toBeNull();
const second = await buildSystemPromptWithFingerprint(project, session, null);
expect(second.drift).toBeNull();
expect(second.fingerprint.prefix_hash).toBe(first.fingerprint.prefix_hash);
});
it('emits drift with prev/new hashes and a changed_inputs entry when an input mutates', async () => {
// Two BOOCHAT.md contents with different mtimes → guidance cache picks
// up the change → fingerprint hash flips → drift fires.
const path = join(tmpDir, 'BOOCHAT.md');
await writeFile(path, 'first', 'utf8');
process.env['CONTAINER_GUIDANCE_FILE'] = path;
const session = makeSession();
const project = makeProject({ path: '/tmp/stable-proj' });
const first = await buildSystemPromptWithFingerprint(project, session, null);
expect(first.drift).toBeNull();
await writeFile(path, 'second — different content', 'utf8');
const later = new Date(Date.now() + 60_000);
await utimes(path, later, later);
const second = await buildSystemPromptWithFingerprint(project, session, null);
expect(second.drift).not.toBeNull();
expect(second.drift!.prev_hash).toBe(first.fingerprint.prefix_hash);
expect(second.drift!.new_hash).toBe(second.fingerprint.prefix_hash);
expect(second.drift!.prev_hash).not.toBe(second.drift!.new_hash);
expect(second.drift!.changed_inputs).toContain('mtime_boochat');
});
it('does not fire drift across distinct sessions even if their hashes differ', async () => {
process.env['CONTAINER_GUIDANCE_FILE'] = join(tmpDir, 'absent.md');
const sessionA = makeSession({ id: 'sess-A' });
const sessionB = makeSession({ id: 'sess-B', system_prompt: 'B-only override' });
const project = makeProject({ path: '/tmp/stable-proj' });
const a = await buildSystemPromptWithFingerprint(project, sessionA, null);
const b = await buildSystemPromptWithFingerprint(project, sessionB, null);
expect(a.drift).toBeNull();
expect(b.drift).toBeNull();
expect(a.fingerprint.prefix_hash).not.toBe(b.fingerprint.prefix_hash);
});
});

View File

@@ -0,0 +1,228 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import postgres from 'postgres';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// v1.13.10: integration tests for the tool_cost_stats view. Skipped unless
// DATABASE_URL is set so they don't break `pnpm test` on a fresh checkout.
// Run with:
// DATABASE_URL=postgres://boocode:<pw>@localhost:5500/boocode pnpm -C apps/server test
//
// Isolation: each test uses a unique tool_name suffix derived from a per-test
// counter. The view aggregates globally across all chats, so without unique
// tool names parallel test runs would interfere. Cleanup deletes by tool_name
// suffix in afterAll.
const DB_URL = process.env.DATABASE_URL;
const describeFn = DB_URL ? describe : describe.skip;
const TEST_RUN_ID = `v13_10_${Date.now()}`;
const tname = (suffix: string) => `${TEST_RUN_ID}_${suffix}`;
describeFn('tool_cost_stats view (v1.13.10)', () => {
let sql: ReturnType<typeof postgres>;
let projectId: string;
let sessionId: string;
let chatId: string;
beforeAll(async () => {
if (!DB_URL) return;
sql = postgres(DB_URL, { max: 2, idle_timeout: 5, connect_timeout: 5, onnotice: () => {} });
// Apply the schema before fixtures so the view exists. Idempotent via
// CREATE OR REPLACE VIEW + CREATE TABLE IF NOT EXISTS; safe to run on a
// pre-populated DB. Mirrors apps/server/src/db.ts:applySchema.
const here = fileURLToPath(import.meta.url);
const schemaPath = resolve(here, '../../../schema.sql');
const ddl = readFileSync(schemaPath, 'utf8');
await sql.unsafe(ddl);
// Fixture project + session + chat for all inserts in this file.
const proj = await sql<{ id: string }[]>`
INSERT INTO projects (name, path)
VALUES (${`tool_cost_stats_test_${TEST_RUN_ID}`}, ${`/tmp/${TEST_RUN_ID}`})
RETURNING id
`;
projectId = proj[0]!.id;
const sess = await sql<{ id: string }[]>`
INSERT INTO sessions (project_id, name, model)
VALUES (${projectId}, ${'test'}, ${'test-model'})
RETURNING id
`;
sessionId = sess[0]!.id;
const chat = await sql<{ id: string }[]>`
INSERT INTO chats (session_id, name) VALUES (${sessionId}, ${'test'}) RETURNING id
`;
chatId = chat[0]!.id;
});
afterAll(async () => {
if (!DB_URL) return;
// Project FK CASCADE cleans sessions/chats/messages/parts in one shot.
await sql`DELETE FROM projects WHERE id = ${projectId}`;
await sql.end({ timeout: 5 });
});
async function insertAssistantTurn(opts: {
toolNames: string[];
tokensUsed: number | null;
ctxUsed: number | null;
status?: 'streaming' | 'complete' | 'failed' | 'cancelled';
metadata?: { kind: string } | null;
createdAt?: Date;
}): Promise<string> {
const toolCalls = opts.toolNames.map((name, i) => ({
id: `call_${TEST_RUN_ID}_${name}_${i}`,
name,
args: {},
}));
const created = opts.createdAt ?? new Date();
const rows = await sql<{ id: string }[]>`
INSERT INTO messages (
session_id, chat_id, role, content, kind, status,
tool_calls, tokens_used, ctx_used,
metadata, created_at
)
VALUES (
${sessionId}, ${chatId}, 'assistant', '', 'message',
${opts.status ?? 'complete'},
${sql.json(toolCalls as never)},
${opts.tokensUsed},
${opts.ctxUsed},
${opts.metadata ? sql.json(opts.metadata as never) : null},
${created}
)
RETURNING id
`;
return rows[0]!.id;
}
it('returns empty when no tool calls exist for a tool name', async () => {
const t = tname('absent');
const stats = await sql<{ tool_name: string }[]>`
SELECT * FROM tool_cost_stats WHERE tool_name = ${t}
`;
expect(stats).toEqual([]);
});
it('attributes single-tool turn fully to that tool', async () => {
const t = tname('single');
await insertAssistantTurn({ toolNames: [t], tokensUsed: 300, ctxUsed: 15000 });
const stats = await sql<{
tool_name: string;
prompt_tokens_sum: number;
completion_tokens_sum: number;
n_calls: number;
}[]>`SELECT * FROM tool_cost_stats WHERE tool_name = ${t}`;
expect(stats[0]).toMatchObject({
tool_name: t,
prompt_tokens_sum: 15000,
completion_tokens_sum: 300,
n_calls: 1,
});
});
it('splits multi-tool turn equally across tools', async () => {
const a = tname('multi_a');
const b = tname('multi_b');
const c = tname('multi_c');
// 3 tools, 300 completion / 15000 prompt → each gets 100 / 5000
await insertAssistantTurn({ toolNames: [a, b, c], tokensUsed: 300, ctxUsed: 15000 });
const stats = await sql<{
tool_name: string;
prompt_tokens_sum: number;
completion_tokens_sum: number;
n_calls: number;
}[]>`
SELECT * FROM tool_cost_stats
WHERE tool_name IN (${a}, ${b}, ${c})
ORDER BY tool_name
`;
expect(stats).toHaveLength(3);
for (const s of stats) {
expect(s.completion_tokens_sum).toBe(100);
expect(s.prompt_tokens_sum).toBe(5000);
expect(s.n_calls).toBe(1);
}
});
it('limits to last 100 calls per tool (FIFO window)', async () => {
const t = tname('window');
// Insert 110 turns with monotonically-increasing created_at and tokensUsed.
// Expect view to keep only the most recent 100.
const base = Date.now() + 1_000_000; // distant future to avoid colliding with other tests
for (let i = 1; i <= 110; i++) {
await insertAssistantTurn({
toolNames: [t],
tokensUsed: i, // 1..110
ctxUsed: i * 10,
createdAt: new Date(base + i),
});
}
const [stat] = await sql<{
n_calls: number;
completion_tokens_sum: number;
}[]>`SELECT n_calls, completion_tokens_sum FROM tool_cost_stats WHERE tool_name = ${t}`;
expect(stat!.n_calls).toBe(100);
// Last 100 are tokensUsed=11..110, sum = (11+110)*100/2 = 6050.
expect(stat!.completion_tokens_sum).toBe(6050);
});
it('excludes turns with NULL tokens_used (pre-v1.13.7 latent regression)', async () => {
const t = tname('null_tokens');
await insertAssistantTurn({ toolNames: [t], tokensUsed: null, ctxUsed: 1000 });
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: null });
const stats = await sql`SELECT * FROM tool_cost_stats WHERE tool_name = ${t}`;
expect(stats).toEqual([]);
});
it('excludes failed/cancelled turns and cap_hit/doom_loop sentinel rows', async () => {
const t = tname('filtered');
// A: status='failed' — excluded
// B: status='cancelled' — excluded
// C: status='complete', metadata={kind:'cap_hit'} — excluded
// D: status='complete', metadata={kind:'doom_loop'} — excluded
// E: status='complete', metadata=null — included
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: 1000, status: 'failed' });
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: 1000, status: 'cancelled' });
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: 1000, metadata: { kind: 'cap_hit' } });
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: 1000, metadata: { kind: 'doom_loop' } });
await insertAssistantTurn({ toolNames: [t], tokensUsed: 100, ctxUsed: 1000, metadata: null });
const [stat] = await sql<{ n_calls: number }[]>`
SELECT n_calls FROM tool_cost_stats WHERE tool_name = ${t}
`;
expect(stat!.n_calls).toBe(1);
});
it('reads tool_calls via messages_with_parts (parts-authoritative)', async () => {
const t = tname('parts');
// Insert an assistant row with messages.tool_calls=NULL but a
// message_parts row carrying the tool_call. The view reads via
// messages_with_parts, which COALESCEs the parts table over the legacy
// column — so this row should still aggregate.
const rows = await sql<{ id: string }[]>`
INSERT INTO messages (
session_id, chat_id, role, content, kind, status,
tool_calls, tokens_used, ctx_used
)
VALUES (
${sessionId}, ${chatId}, 'assistant', '', 'message', 'complete',
NULL, 200, 5000
)
RETURNING id
`;
const messageId = rows[0]!.id;
await sql`
INSERT INTO message_parts (message_id, sequence, kind, payload)
VALUES (
${messageId}, 0, 'tool_call',
${sql.json({ id: `tc_parts_${TEST_RUN_ID}`, name: t, args: {} } as never)}
)
`;
const [stat] = await sql<{ n_calls: number }[]>`
SELECT n_calls FROM tool_cost_stats WHERE tool_name = ${t}
`;
expect(stat!.n_calls).toBe(1);
});
});

View File

@@ -1,5 +1,11 @@
import { describe, it, expect } from 'vitest';
import { ALL_TOOLS } from '../tools.js';
import {
ALL_TOOLS,
CORE_TOOL_NAMES,
STANDARD_TOOL_NAMES,
TOOLS_BY_NAME,
resolveToolTier,
} from '../tools.js';
describe('ALL_TOOLS registry', () => {
// v1.13.3: tools must be alpha-sorted at module load. llama.cpp's prompt
@@ -12,3 +18,59 @@ describe('ALL_TOOLS registry', () => {
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
});
});
describe('resolveToolTier (v1.13.15-tools)', () => {
it('returns CORE tools for tier=core', () => {
expect(resolveToolTier('core')).toEqual(CORE_TOOL_NAMES);
});
it('returns STANDARD tools for tier=standard', () => {
const result = resolveToolTier('standard');
expect(result.length).toBe(STANDARD_TOOL_NAMES.length);
expect(result.length).toBeGreaterThan(CORE_TOOL_NAMES.length);
// STANDARD is a strict superset of CORE.
expect(result).toEqual(expect.arrayContaining([...CORE_TOOL_NAMES]));
});
it('returns ALL tool names for tier=all', () => {
expect(resolveToolTier('all').length).toBe(ALL_TOOLS.length);
});
it('defaults to all when env var is undefined', () => {
expect(resolveToolTier(undefined).length).toBe(ALL_TOOLS.length);
});
it('is case-insensitive', () => {
expect(resolveToolTier('CORE')).toEqual(CORE_TOOL_NAMES);
expect(resolveToolTier('Standard').length).toBe(STANDARD_TOOL_NAMES.length);
});
it('falls back to all for unknown tier strings', () => {
expect(resolveToolTier('bogus').length).toBe(ALL_TOOLS.length);
});
});
describe('CORE_TOOL_NAMES + STANDARD_TOOL_NAMES validation', () => {
// The module-load validation in tools.ts throws if a tier references a
// tool that doesn't exist in TOOLS_BY_NAME. These tests double-check that
// invariant from the consumer side so a future tier-list edit can't smuggle
// in a typo without a test failure.
it('every CORE name exists in TOOLS_BY_NAME', () => {
for (const name of CORE_TOOL_NAMES) {
expect(TOOLS_BY_NAME[name], `CORE references unknown tool '${name}'`).toBeDefined();
}
});
it('every STANDARD name exists in TOOLS_BY_NAME', () => {
for (const name of STANDARD_TOOL_NAMES) {
expect(TOOLS_BY_NAME[name], `STANDARD references unknown tool '${name}'`).toBeDefined();
}
});
it('CORE is a subset of STANDARD', () => {
const standardSet = new Set<string>(STANDARD_TOOL_NAMES);
for (const name of CORE_TOOL_NAMES) {
expect(standardSet.has(name), `'${name}' is in CORE but not STANDARD`).toBe(true);
}
});
});

View File

@@ -0,0 +1,218 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
WsFrameSchema,
KNOWN_FRAME_TYPES,
type WsFrame,
} from '../../types/ws-frames.js';
import { createBroker } from '../broker.js';
const VALID_UUID_A = '00000000-0000-0000-0000-000000000001';
const VALID_UUID_B = '00000000-0000-0000-0000-000000000002';
const VALID_UUID_C = '00000000-0000-0000-0000-000000000003';
const VALID_TIMESTAMP = '2026-05-22T14:30:00.000Z';
describe('WsFrameSchema (v1.13.11-a)', () => {
it('accepts a well-formed chat_status frame', () => {
const result = WsFrameSchema.safeParse({
type: 'chat_status',
chat_id: VALID_UUID_A,
status: 'streaming',
at: VALID_TIMESTAMP,
});
expect(result.success).toBe(true);
});
it('rejects an unknown frame type', () => {
const result = WsFrameSchema.safeParse({
type: 'cosmic_ray_strike',
chat_id: VALID_UUID_A,
});
expect(result.success).toBe(false);
});
it('rejects a chat_status frame with invalid status enum', () => {
// v1.12.1 dropped the legacy 'working' status. Any frame still emitting it
// should fail validation — that's a drift catcher.
const result = WsFrameSchema.safeParse({
type: 'chat_status',
chat_id: VALID_UUID_A,
status: 'working',
at: VALID_TIMESTAMP,
});
expect(result.success).toBe(false);
});
it('rejects a UUID field with a non-UUID string', () => {
const result = WsFrameSchema.safeParse({
type: 'chat_status',
chat_id: 'not-a-uuid',
status: 'idle',
at: VALID_TIMESTAMP,
});
expect(result.success).toBe(false);
});
it('rejects negative token counts in usage frame', () => {
const result = WsFrameSchema.safeParse({
type: 'usage',
message_id: VALID_UUID_A,
chat_id: VALID_UUID_B,
completion_tokens: -1,
ctx_used: 100,
ctx_max: 1000,
});
expect(result.success).toBe(false);
});
it('accepts a usage frame with nullable token counts (pre-v1.13.7 history)', () => {
const result = WsFrameSchema.safeParse({
type: 'usage',
message_id: VALID_UUID_A,
chat_id: VALID_UUID_B,
completion_tokens: null,
ctx_used: null,
ctx_max: null,
});
expect(result.success).toBe(true);
});
it('accepts a tool_result frame with non-UUID tool_call_id (model-emitted)', () => {
// Model-emitted tool_call_ids look like "call_abc123", not UUIDs.
const result = WsFrameSchema.safeParse({
type: 'tool_result',
tool_message_id: VALID_UUID_A,
chat_id: VALID_UUID_B,
tool_call_id: 'call_abc123',
output: { whatever: true },
truncated: false,
});
expect(result.success).toBe(true);
});
it('accepts a compacted frame', () => {
const result = WsFrameSchema.safeParse({
type: 'compacted',
session_id: VALID_UUID_A,
chat_id: VALID_UUID_B,
summary_message_id: VALID_UUID_C,
});
expect(result.success).toBe(true);
});
it('accepts a session_workspace_updated frame', () => {
const result = WsFrameSchema.safeParse({
type: 'session_workspace_updated',
session_id: VALID_UUID_A,
workspace_panes: [{ id: 'p1', kind: 'chat', chatIds: [], activeChatIdx: 0 }],
});
expect(result.success).toBe(true);
});
it('every KNOWN_FRAME_TYPES entry has a discriminated branch', () => {
// Probe each known type by attempting a minimal valid construction.
// Failure here means the union and the KNOWN_FRAME_TYPES list drifted.
for (const type of KNOWN_FRAME_TYPES) {
const probe = WsFrameSchema.safeParse({ type, __dummy__: true });
// We expect FAILURE on every type because we're missing required fields,
// but the failure must be ABOUT the missing fields, not about an unknown
// type. A "Invalid discriminator value" error means the type isn't in
// the union — that's a drift.
if (probe.success) continue;
const issues = probe.error.issues;
const hasInvalidDiscriminator = issues.some(
(i) => i.code === 'invalid_union_discriminator',
);
expect(hasInvalidDiscriminator, `frame type '${type}' is missing from the discriminated union`).toBe(false);
}
});
});
describe('ws-frames.ts file mirror parity', () => {
it('apps/server and apps/web copies are byte-identical', () => {
const here = fileURLToPath(import.meta.url);
const serverPath = resolve(here, '../../../types/ws-frames.ts');
const webPath = resolve(here, '../../../../../web/src/api/ws-frames.ts');
const serverContent = readFileSync(serverPath, 'utf8');
const webContent = readFileSync(webPath, 'utf8');
expect(webContent, 'apps/web/src/api/ws-frames.ts must be byte-identical to apps/server/src/types/ws-frames.ts').toBe(serverContent);
});
});
describe('broker.publishFrame / publishUserFrame fail-closed behavior', () => {
let logErrors: Array<{ obj: unknown; msg: string }>;
let mockLog: Parameters<typeof createBroker>[0];
beforeEach(() => {
logErrors = [];
mockLog = {
error: (obj: unknown, msg: string) => {
logErrors.push({ obj, msg });
},
info: () => {},
warn: () => {},
debug: () => {},
trace: () => {},
fatal: () => {},
child: () => mockLog as never,
level: 'info',
silent: () => {},
} as unknown as Parameters<typeof createBroker>[0];
});
afterEach(() => {
vi.restoreAllMocks();
});
it('publishFrame delivers a valid frame to subscribers', () => {
const broker = createBroker(mockLog);
const received: WsFrame[] = [];
broker.subscribe('sess-1', (f) => received.push(f as WsFrame));
broker.publishFrame('sess-1', {
type: 'delta',
message_id: VALID_UUID_A,
chat_id: VALID_UUID_B,
content: 'hello',
});
expect(received).toHaveLength(1);
expect((received[0] as { type: string }).type).toBe('delta');
expect(logErrors).toHaveLength(0);
});
it('publishFrame drops + logs an invalid frame instead of delivering it', () => {
const broker = createBroker(mockLog);
const received: WsFrame[] = [];
broker.subscribe('sess-1', (f) => received.push(f as WsFrame));
broker.publishFrame('sess-1', {
type: 'delta',
message_id: 'not-a-uuid',
content: 'hello',
} as never);
expect(received).toHaveLength(0);
expect(logErrors).toHaveLength(1);
expect(logErrors[0]!.msg).toMatch(/ws-frame-validation-failed/);
});
it('publishUserFrame drops + logs an invalid user-channel frame', () => {
const broker = createBroker(mockLog);
const received: WsFrame[] = [];
broker.subscribeUser('default', (f) => received.push(f as WsFrame));
broker.publishUserFrame('default', {
type: 'chat_status',
chat_id: VALID_UUID_A,
status: 'working', // v1.12.1 dropped this enum value
at: VALID_TIMESTAMP,
} as never);
expect(received).toHaveLength(0);
expect(logErrors).toHaveLength(1);
});
it('publishFrame validation failure does not throw (no cascade into stream-phase)', () => {
const broker = createBroker(mockLog);
expect(() =>
broker.publishFrame('sess-1', { type: 'unknown_type' } as never),
).not.toThrow();
});
});

View File

@@ -1,7 +1,7 @@
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import type { Agent, AgentsResponse, AgentParseError } from '../types/api.js';
import { ALL_TOOLS } from './tools.js';
import { ALL_TOOLS, resolveToolTier } from './tools.js';
// v1.8.1: global agents live at /data/AGENTS.md inside the container
// (./data:/data:ro mount on the host). Per-project AGENTS.md at the project
@@ -186,11 +186,14 @@ function parseAgentSection(section: RawSection): Omit<Agent, 'source'> {
throw new Error(fmErrors.join('; '));
}
// v1.13.15-tools: intersect with BOOCODE_TOOLS tier (ceiling, not expansion).
// Unset → resolveToolTier returns ALL tool names → no narrowing.
const tierAllowed = new Set(resolveToolTier(process.env.BOOCODE_TOOLS));
const filteredTools = Array.isArray(fm.tools)
? fm.tools.filter((t): t is string =>
(ALL_TOOL_NAMES as readonly string[]).includes(t),
(ALL_TOOL_NAMES as readonly string[]).includes(t) && tierAllowed.has(t),
)
: DEFAULT_TOOLS;
: DEFAULT_TOOLS.filter((t) => tierAllowed.has(t));
return {
id: slugify(section.name),
@@ -252,6 +255,22 @@ export function invalidateAgentsCache(projectPath?: string): void {
}
}
// v1.13.8: cache-read accessor for the system-prompt prefix-fingerprint log.
// Returns the AGENTS.md mtimes that getAgentsForProject() observed on its
// last cache fill for this projectPath. Both fields are null when the cache
// is cold (e.g. tests, fresh boot before the first inference turn). Does no
// I/O — a fresh stat would race the cache and isn't what the fingerprint
// wants anyway (we want what was actually used to resolve the agent).
export function getAgentsMtimes(projectPath: string): {
global: number | null;
project: number | null;
} {
const key = projectPath || '__none__';
const entry = cache.get(key);
if (!entry) return { global: null, project: null };
return { global: entry.globalMtime, project: entry.projectMtime };
}
async function safeStat(path: string): Promise<number | null> {
try {
const s = await fs.stat(path);

View File

@@ -1,3 +1,6 @@
import type { FastifyBaseLogger } from 'fastify';
import { WsFrameSchema, type WsFrame } from '../types/ws-frames.js';
export type Frame = Record<string, unknown> & { type: string };
export type Listener = (frame: Frame) => void;
@@ -6,9 +9,15 @@ export interface Broker {
subscribe(sessionId: string, listener: Listener): () => void;
publishUser(user: string, frame: Frame): void;
subscribeUser(user: string, listener: Listener): () => void;
// v1.13.11-a: typed publish wrappers. Validate against WsFrameSchema and
// delegate to publish / publishUser on success; log + drop on failure
// (fail-closed). Existing publish / publishUser callers stay legal — they
// get converted to the typed variant in v1.13.11-b.
publishFrame(sessionId: string, frame: WsFrame): void;
publishUserFrame(user: string, frame: WsFrame): void;
}
export function createBroker(): Broker {
export function createBroker(log?: FastifyBaseLogger): Broker {
const topics = new Map<string, Set<Listener>>();
const userTopics = new Map<string, Set<Listener>>();
@@ -39,6 +48,28 @@ export function createBroker(): Broker {
};
}
// v1.13.11-a: shared validation guard. Returns the parsed/typed frame on
// success, or null on failure (after logging). Brief mandates fail-closed
// semantics: invalid frames don't reach subscribers; throwing here could
// cascade into stream-phase aborts which v1.13.7 already had to defend
// against, so log + drop is the right shape.
function validate(channel: 'session' | 'user', key: string, frame: WsFrame): WsFrame | null {
const parsed = WsFrameSchema.safeParse(frame);
if (parsed.success) return parsed.data;
const frameType = (frame as { type?: unknown })?.type;
const errors = parsed.error.flatten();
if (log) {
log.error(
{ channel, key, frame_type: frameType, errors },
'ws-frame-validation-failed: dropping invalid frame',
);
} else {
// Fallback for callers that didn't pass a logger (e.g. unit tests).
console.error('ws-frame-validation-failed', { channel, key, frame_type: frameType, errors });
}
return null;
}
return {
publish(sessionId, frame) {
publishTo(topics, sessionId, frame);
@@ -52,5 +83,15 @@ export function createBroker(): Broker {
subscribeUser(user, listener) {
return subscribeTo(userTopics, user, listener);
},
publishFrame(sessionId, frame) {
const valid = validate('session', sessionId, frame);
if (!valid) return;
publishTo(topics, sessionId, valid as Frame);
},
publishUserFrame(user, frame) {
const valid = validate('user', user, frame);
if (!valid) return;
publishTo(userTopics, user, valid as Frame);
},
};
}

View File

@@ -16,9 +16,41 @@
// file parser bug (upstream issue #37) returns a generic error string,
// which we re-surface with a hint to add the file to .codecontextignore.
import { realpath } from 'node:fs/promises';
import { access, copyFile, realpath } from 'node:fs/promises';
import { join } from 'node:path';
import { truncateIfNeeded } from './truncate.js';
// v1.13.12 fix: codecontext crashes on empty source files (upstream issue #37)
// when it can't ignore them. The .codecontextignore.template ships with the
// project at /opt/boocode/codecontext/.codecontextignore.template (path inside
// the container; the host's /opt is bind-mounted). On the first call to any
// project, copy the template in if no per-project ignore exists yet. The user
// can subsequently edit the file to customize. Idempotent — once any file is
// at the project root we never overwrite.
const IGNORE_TEMPLATE_PATH = '/opt/boocode/codecontext/.codecontextignore.template';
const ensuredIgnoreProjects = new Set<string>();
async function ensureIgnoreFile(projectRoot: string): Promise<void> {
if (ensuredIgnoreProjects.has(projectRoot)) return;
const ignorePath = join(projectRoot, '.codecontextignore');
try {
await access(ignorePath);
ensuredIgnoreProjects.add(projectRoot);
return;
} catch {
// missing — install the default
}
try {
await copyFile(IGNORE_TEMPLATE_PATH, ignorePath);
ensuredIgnoreProjects.add(projectRoot);
} catch {
// Template missing or project root read-only — proceed without it. The
// codecontext call may still crash on empty source files; the model gets
// the existing hint-message via the catch below telling it to add to
// .codecontextignore manually.
}
}
export interface CodecontextRequest {
toolName: string;
args: Record<string, unknown>;
@@ -46,6 +78,10 @@ export async function callCodecontext(
// never pass target_dir; tests can override). A non-existent target_dir
// throws before we hit the network so the model gets a sharp error.
const resolvedProject = await realpath(req.projectPath);
// v1.13.12 fix: install the default .codecontextignore on first call to any
// project so codecontext doesn't crash on empty node_modules files. One file
// written per project, idempotent (set-membership check inside).
await ensureIgnoreFile(resolvedProject);
const requestedTarget = req.args['target_dir'];
const targetDir = typeof requestedTarget === 'string' && requestedTarget.length > 0
? requestedTarget

View File

@@ -23,7 +23,13 @@ import type { Broker } from './broker.js';
import { SUMMARY_TEMPLATE } from './compaction-prompt.js';
import * as modelContextLookup from './model-context.js';
const COMPACTION_BUFFER = 20_000;
// v1.13.9: ratio-only overflow trigger. Fires compaction at 85% of ctx_max
// (opencode session/overflow.ts pattern). Replaces the v1.11.0-era
// `ctx_max - 20_000` formula which degenerated to 0 for contexts ≤20k and
// gave only 7-8% headroom to the summarizer at 262k. Ratio gives consistent
// 15% headroom at any scale, and small-ctx models no longer get an
// effectively-disabled trigger.
const EARLY_TRIGGER_RATIO = 0.85;
const MIN_PRESERVE_RECENT_TOKENS = 2_000;
const MAX_PRESERVE_RECENT_TOKENS = 8_000;
const DEFAULT_TAIL_TURNS = 2;
@@ -39,19 +45,24 @@ export interface CompactionMessage {
status: 'streaming' | 'complete' | 'failed' | 'cancelled';
tool_calls: Array<{ id: string; name: string; args: Record<string, unknown> }> | null;
tool_results: { tool_call_id: string; output: unknown; truncated: boolean; error?: string } | null;
// v1.13.6: reasoning_parts captured by v1.13.1-C and read back through
// messages_with_parts. Embedded into the head-assembly payload as prose so
// the summarizer LLM sees what the model was reasoning through when it
// chose its tool calls.
reasoning_parts: Array<{ text: string }> | null;
metadata: { kind?: string } | null;
created_at: string;
}
// === overflow ===
// Tokens we hold in reserve for the model's response so a near-full context
// can still produce a useful turn. Mirrors opencode's COMPACTION_BUFFER.
// Returns 0 when the context limit is unknown (caller treats 0 as "do not
// trigger overflow"); avoids dividing-by-zero downstream.
// Returns the token budget at which overflow fires. Triggers compaction at
// 85% of contextLimit (opencode session/overflow.ts pattern). Returns 0 when
// the context limit is unknown caller treats 0 as "do not trigger overflow",
// keeping inference flowing rather than compacting a turn we can't size.
export function usable(contextLimit: number): number {
if (!contextLimit || contextLimit <= 0) return 0;
return Math.max(0, contextLimit - COMPACTION_BUFFER);
return Math.floor(EARLY_TRIGGER_RATIO * contextLimit);
}
export interface Usage {
@@ -197,7 +208,8 @@ export function buildPrompt(
// would silently drop pre-legacy-compact history before the LLM sees it.
// Compaction wants to send the entire head, full stop.) ===
interface OpenAiMessage {
// v1.13.6: exported for unit-test access (reasoning render coverage).
export interface OpenAiMessage {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
tool_calls?: Array<{
@@ -212,7 +224,8 @@ function isCapHitSentinel(m: CompactionMessage): boolean {
return m.role === 'system' && m.metadata != null && m.metadata.kind === 'cap_hit';
}
function buildHeadPayload(head: CompactionMessage[]): OpenAiMessage[] {
// v1.13.6: exported for unit-test access (reasoning render coverage).
export function buildHeadPayload(head: CompactionMessage[]): OpenAiMessage[] {
const out: OpenAiMessage[] = [];
for (const m of head) {
if (isCapHitSentinel(m)) continue;
@@ -243,9 +256,22 @@ function buildHeadPayload(head: CompactionMessage[]): OpenAiMessage[] {
continue;
}
if (m.role === 'assistant') {
// v1.13.6: embed reasoning text as prose prefixed onto the assistant
// content. OpenAI wire shape doesn't carry reasoning as a structured
// field, but the summarizer is reading text — a tagged prose block
// gives it the same signal. We mirror the AI SDK ReasoningPart shape
// by using a <reasoning>...</reasoning> wrapper so the summarizer can
// distinguish reasoning from user-visible answer.
let body = m.content && m.content.length > 0 ? m.content : '';
if (m.reasoning_parts && m.reasoning_parts.length > 0) {
const reasoning = m.reasoning_parts.map((r) => r.text).join('');
body = body.length > 0
? `<reasoning>${reasoning}</reasoning>\n\n${body}`
: `<reasoning>${reasoning}</reasoning>`;
}
const msg: OpenAiMessage = {
role: 'assistant',
content: m.content && m.content.length > 0 ? m.content : null,
content: body.length > 0 ? body : null,
};
if (m.tool_calls && m.tool_calls.length > 0) {
msg.tool_calls = m.tool_calls.map((tc) => ({
@@ -344,8 +370,11 @@ export async function process(input: ProcessInput): Promise<void> {
// turns() boundary logic sees the same sequence the LLM will.
// v1.13.1-B: reads tool_calls/tool_results via the parts-merged view so
// the compaction payload matches what the LLM saw on the original turn.
// v1.13.6: also pulls reasoning_parts (added in v1.13.1-C) so summaries
// capture what the model was working through before each tool call.
const messages = await sql<CompactionMessage[]>`
SELECT id, role, content, kind, summary, status, tool_calls, tool_results, metadata, created_at
SELECT id, role, content, kind, summary, status, tool_calls, tool_results,
reasoning_parts, metadata, created_at
FROM messages_with_parts
WHERE chat_id = ${chatId} AND compacted_at IS NULL
ORDER BY created_at ASC, id ASC
@@ -402,15 +431,16 @@ export async function process(input: ProcessInput): Promise<void> {
'compaction: invoking model',
);
// 6a. Flip the chat dot amber for the duration of the LLM call + DB writes.
// Same { type: 'chat_status', status: 'working', at } shape inference.ts
// emits at runner enqueue. publishUser → broadcasts on the per-user channel
// (all devices / tabs see it) since chat_status is a user-channel frame in
// BooCode (see useChatStatus.ts, which is the consumer).
broker.publishUser('default', {
// 6a. Flip the chat dot for the duration of the LLM call + DB writes.
// v1.13.11-b: publish status='streaming' (the v1.12.1-widened replacement
// for the dropped 'working' value). Compaction's LLM call has the same
// semantic as an inference turn for dot-state purposes. The v1.12.1
// chat_status widening missed this site; v1.13.11's WsFrame Zod schema
// surfaced the drift via the unknown-enum-value check.
broker.publishUserFrame('default', {
type: 'chat_status',
chat_id: chatId,
status: 'working',
status: 'streaming',
at: new Date().toISOString(),
});
@@ -479,7 +509,7 @@ export async function process(input: ProcessInput): Promise<void> {
// Always restore the dot. Status='idle' (not 'error') even on failure —
// the caller logs/re-surfaces the error separately; the dot doesn't
// need to stay red across reloads for a transient compaction blip.
broker.publishUser('default', {
broker.publishUserFrame('default', {
type: 'chat_status',
chat_id: chatId,
status: 'idle',
@@ -493,7 +523,7 @@ export async function process(input: ProcessInput): Promise<void> {
// toast. Order matters: idle must precede 'compacted' so the dot is
// already green by the time the refetch toast appears.
if (succeeded) {
broker.publish(sessionId, {
broker.publishFrame(sessionId, {
type: 'compacted',
session_id: sessionId,
chat_id: chatId,

View File

@@ -3,12 +3,24 @@ import { READ_ONLY_TOOL_NAMES } from '../tools.js';
// v1.8.2: tool-call budget defaults. Resolved per-turn by resolveToolBudget.
// - Agent with explicit max_tool_calls: that value.
// - Agent with read-only-only tools: BUDGET_READ_ONLY (30).
// - Agent with read-only-only tools: BUDGET_READ_ONLY (50).
// - Agent with any non-read-only tool: BUDGET_NON_READ_ONLY (10).
// - No agent (raw chat): BUDGET_NO_AGENT (15).
export const BUDGET_READ_ONLY = 30;
// - No agent (raw chat): BUDGET_NO_AGENT (50).
// v1.13.7: bumped BUDGET_NO_AGENT 15→30 to match BUDGET_READ_ONLY. Every tool
// in ALL_TOOLS today is read-only (see services/tools.ts comment at
// READ_ONLY_TOOL_NAMES); the cautious 15-cap was a forward-looking guard for
// write tools that haven't landed yet. No-agent mode gets the same toolset as
// an all-read-only agent at runtime, so they should share the same budget.
// v1.13.12: bumped read-only caps 30→50. Real recon sessions were hitting 30
// with ~3 turns wasted on codecontext parse failures (empty node_modules
// files); legitimate need was ~27, and Architect-class system overviews want
// deeper recon than a 30-cap permits. Headroom of 20 absorbs failure-retry
// turns + deeper exploration without changing the safety floor materially —
// the doom-loop guard (3 identical calls → abort) catches the actual failure
// mode this cap was guarding against.
export const BUDGET_READ_ONLY = 50;
export const BUDGET_NON_READ_ONLY = 10;
export const BUDGET_NO_AGENT = 15;
export const BUDGET_NO_AGENT = 50;
const READ_ONLY_SET: ReadonlySet<string> = new Set(READ_ONLY_TOOL_NAMES);

View File

@@ -7,7 +7,17 @@ import type { ToolCall, ToolResult } from '../../types/api.js';
// JSON columns; the swap to parts-as-source-of-truth happens in a later
// v1.13 dispatch alongside the AI SDK streamText migration.
export type PartKind = 'text' | 'tool_call' | 'tool_result' | 'reasoning' | 'step_start';
// v1.13.13: 'synthesis' added. Schema CHECK constraint is updated in lockstep
// (schema.sql adds 'synthesis' to message_parts_kind_chk on startup). The
// dispatch's claim that no schema migration was needed assumed kind was a
// bare text column — it isn't; the constraint enumerates allowed values.
export type PartKind =
| 'text'
| 'tool_call'
| 'tool_result'
| 'reasoning'
| 'step_start'
| 'synthesis';
export interface PartInsert {
message_id: string;

View File

@@ -1,3 +1,4 @@
import type { FastifyBaseLogger } from 'fastify';
import type { Sql } from '../../db.js';
import type {
Agent,
@@ -6,7 +7,7 @@ import type {
Session,
} from '../../types/api.js';
import * as compaction from '../compaction.js';
import { buildSystemPrompt } from '../system-prompt.js';
import { buildSystemPromptWithFingerprint } from '../system-prompt.js';
import { isAnySentinel } from './sentinels.js';
import { PRUNE_TRIGGER_TOKENS, prune } from './prune.js';
import type { InferenceContext } from './turn.js';
@@ -31,14 +32,25 @@ export interface OpenAiMessage {
// v1.12: buildSystemPrompt lives in services/system-prompt.ts. It awaits the
// container-guidance loader, so this function is async too and every call
// site in inference.ts awaits the result.
// v1.13.8: optional log argument. When provided, emit prefix-fingerprint
// per call + prefix-drift when the same session sees a hash change. Tests
// omit it and exercise the byte-stability surface directly through
// buildSystemPromptWithFingerprint. The observer Map in system-prompt.ts
// updates regardless of whether log is passed.
export async function buildMessagesPayload(
session: Session,
project: Project,
history: Message[],
agent: Agent | null = null
agent: Agent | null = null,
log?: FastifyBaseLogger,
): Promise<OpenAiMessage[]> {
const out: OpenAiMessage[] = [];
const systemPrompt = await buildSystemPrompt(project, session, agent);
const { prompt: systemPrompt, fingerprint, drift } =
await buildSystemPromptWithFingerprint(project, session, agent);
if (log) {
log.info(fingerprint);
if (drift) log.warn(drift);
}
out.push({ role: 'system', content: systemPrompt });
// Find the latest compact marker — only send messages from that point onwards
@@ -63,6 +75,25 @@ export async function buildMessagesPayload(
if (isAnySentinel(m)) continue;
if (m.role === 'assistant' && m.status === 'streaming') continue;
if (m.role === 'assistant' && m.status === 'cancelled') continue;
// v1.13.7: skip failed assistant turns. A failed row carries no usable
// content for the model, and leaving it in the payload alongside any
// following assistant message produces "Cannot have 2 or more assistant
// messages at the end of the list" from the OpenAI-compatible upstream.
if (m.role === 'assistant' && m.status === 'failed') continue;
// v1.13.7: skip "empty" completed assistants — clen=0 + no tool_calls.
// These can land when an upstream stream returns finishReason='stop' with
// no text/tool output (network blip, rate limit recovery, model quirk).
// Same risk as the failed-status case: a trailing empty assistant plus
// the next attempt's assistant placeholder = two trailing assistants and
// the API rejects the whole payload.
if (
m.role === 'assistant' &&
m.status === 'complete' &&
(m.content == null || m.content.trim().length === 0) &&
(m.tool_calls == null || m.tool_calls.length === 0)
) {
continue;
}
if (m.role === 'tool') {
const tr = m.tool_results;
if (!tr) continue;
@@ -168,10 +199,13 @@ export async function maybeFlagForCompaction(
);
if (!overflow) return;
// v1.13.4: try the cheap prune first. If it freed at least the buffer
// worth of tokens (PRUNE_TRIGGER_TOKENS, identical to COMPACTION_BUFFER),
// we're below the threshold again — skip flagging summarize for the next
// turn. The next turn's overflow check will re-evaluate from scratch.
// v1.13.4: try the cheap prune first. If it freed at least
// PRUNE_TRIGGER_TOKENS (20k) worth of context, we're below the threshold
// again — skip flagging summarize for the next turn. The next turn's
// overflow check will re-evaluate from scratch.
// v1.13.9: the overflow trigger above is now 85% of ctx_max (was
// ctx_max - 20k). PRUNE_TRIGGER_TOKENS stays at 20k as the prune-freed
// threshold — independent of the overflow formula.
// Prune failures (DB errors etc.) propagate so the surrounding inference
// path sees them; the catch in finalizeCompletion / executeToolPhase
// doesn't shield this — by design, we want to know if prune is broken.

View File

@@ -15,6 +15,14 @@ function getProvider(baseURL: string): ReturnType<typeof createOpenAICompatible>
provider = createOpenAICompatible({
name: 'llama-swap',
baseURL: baseURL.endsWith('/v1') ? baseURL : `${baseURL}/v1`,
// v1.13.7: @ai-sdk/openai-compatible defaults includeUsage=false, which
// omits `stream_options.include_usage` from the request body. Without
// it, llama.cpp / llama-swap never emits the trailing usage block, so
// `result.usage` resolves with inputTokens=outputTokens=undefined and
// tokens_used / ctx_used land as NULL in every messages row. Setting
// true here re-enables the per-stream usage payload across all models
// served via the llama-swap provider.
includeUsage: true,
});
cache.set(baseURL, provider);
}

View File

@@ -36,7 +36,7 @@ export async function runCapHitSummary(
): Promise<void> {
const { sessionId, chatId, assistantMessageId, signal } = args;
const messages = await buildMessagesPayload(session, project, history, agent);
const messages = await buildMessagesPayload(session, project, history, agent, ctx.log);
messages.push({ role: 'system', content: CAP_HIT_SUMMARY_NOTE(budget) });
const startedRow = await ctx.sql<{ started_at: string }[]>`
@@ -298,7 +298,7 @@ export async function runDoomLoopSummary(
): Promise<void> {
const { sessionId, chatId, assistantMessageId, signal } = args;
const messages = await buildMessagesPayload(session, project, history, agent);
const messages = await buildMessagesPayload(session, project, history, agent, ctx.log);
messages.push({ role: 'system', content: DOOM_LOOP_NOTE(loop.name) });
const startedRow = await ctx.sql<{ started_at: string }[]>`

View File

@@ -14,6 +14,11 @@ import type {
// the reference is read at call time (inside an async function body), not
// at module top-level. Node + tsc resolve this cleanly.
import { runAssistantTurn } from './turn.js';
// v1.13.13: synthesis pipeline — replaces the immediate recursive turn when
// any of this batch's tool calls is in SYNTHESIS_TOOLS. Falls through to
// recursion on synthesis failure (timeout / model error). See module header
// in synthesisPipeline.ts for the auto-fetch + token-budget rules.
import { SYNTHESIS_TOOLS, runSynthesisPass } from '../synthesisPipeline.js';
async function executeToolCall(
projectRoot: string,
@@ -155,6 +160,12 @@ export async function executeToolPhase(
// batches still execute the other tools normally.
ctx.publishUser({ type: 'chat_status', chat_id: chatId, status: 'tool_running', at: new Date().toISOString() });
let pausingForUserInput = false;
// v1.13.13: capture synth-tool result text so the synthesis pipeline below
// doesn't have to re-fetch from DB. Array (not single) because a batch
// could theoretically include multiple synthesis tools — we take the first
// for the synthesis input. Race-free under Promise.all because each
// callback pushes its own captured value.
const synthEntries: Array<{ tc: ToolCall; output: unknown; error?: string }> = [];
await Promise.all(
toolCalls.map(async (tc) => {
const [toolRow] = await ctx.sql<{ id: string }[]>`
@@ -186,6 +197,9 @@ export async function executeToolPhase(
return;
}
const tres = await executeToolCall(projectRoot, tc);
if (SYNTHESIS_TOOLS.has(tc.name)) {
synthEntries.push({ tc, output: tres.output, ...(tres.error ? { error: tres.error } : {}) });
}
const stored = {
tool_call_id: tc.id,
output: tres.output,
@@ -233,6 +247,41 @@ export async function executeToolPhase(
return;
}
// v1.13.13: synthesis-pipeline branch. When any of this batch's tool calls
// is a codecontext overview/analysis tool that produced a non-error result,
// run a forced second-inference synthesis pass with auto-fetched files +
// project docs instead of the normal recursive runAssistantTurn. Falls
// through to the recursive call on synthesis failure (timeout, model
// error). User-abort re-throws so the outer handler runs.
const synthEntry = synthEntries.find((e) => !e.error && e.output != null);
if (synthEntry) {
// codecontext wrappers return { result: string, truncated: boolean, ... }.
// Defensive: stringify the output if it isn't the expected shape so the
// synthesis still has something to chew on rather than crashing on
// missing `.result`.
const out = synthEntry.output as { result?: unknown; truncated?: boolean; outputPath?: string };
const toolResultText =
typeof out?.result === 'string'
? out.result
: JSON.stringify(synthEntry.output);
// v1.13.15-b: forward the wrapper's truncation flag + opaque tmpfs id so
// synthesisPipeline can re-read the full content for reference extraction.
const ran = await runSynthesisPass({
ctx,
args,
session,
projectRoot,
toolName: synthEntry.tc.name,
toolResultText,
...(typeof out?.truncated === 'boolean' ? { truncated: out.truncated } : {}),
...(typeof out?.outputPath === 'string' ? { outputPath: out.outputPath } : {}),
});
if (ran) return;
// ran === false → synthesis failed (timeout / model error) → fall through
// to the standard recursive turn below. The synth message (if created)
// was already marked status='failed' inside runSynthesisPass.
}
const [nextAssistant] = await ctx.sql<{ id: string }[]>`
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp())

View File

@@ -205,7 +205,7 @@ export async function runAssistantTurn(
return;
}
const messages = await buildMessagesPayload(session, project, history, agent);
const messages = await buildMessagesPayload(session, project, history, agent, ctx.log);
// v1.11.8: resolve per-chat web-tools opt-in. Tri-state on the wire:
// - session.web_search_enabled = null → inherit project default

View File

@@ -0,0 +1,493 @@
// v1.13.13: forced second-inference synthesis pass for codecontext
// overview/analysis tools. Triggered from tool-phase.ts after a codecontext
// tool call lands and BEFORE the normal recursive runAssistantTurn fires.
//
// Inputs to the synthesis stream:
// 1. The codecontext tool's result text.
// 2. Top-N source files referenced in that text, fetched via view_file.
// 3. Project documentation auto-fetched from the repo root.
// 4. The original user message that triggered the turn.
//
// Output: a NEW assistant message whose sole part is kind='synthesis'.
// Streams to the client as deltas exactly like a normal assistant turn.
//
// Failure modes (all fall through to recursive runAssistantTurn):
// - SYNTHESIS_TOOLS membership check fails -> return false immediately.
// - File-fetch / doc-fetch errors -> silent skip, continue with what we have.
// - Stream error / timeout -> mark synth message status='failed', return false.
// - User-abort -> mark cancelled and re-throw so the outer abort handler runs.
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import { TOOLS_BY_NAME } from './tools.js';
import { streamCompletion } from './inference/stream-phase.js';
import { SYNTHESIS_SYSTEM_PROMPT } from './synthesisPrompt.js';
import { insertParts } from './inference/parts.js';
import * as modelContext from './model-context.js';
import { readTruncation } from './truncate.js';
import type { Session } from '../types/api.js';
import type { OpenAiMessage } from './inference/payload.js';
import type { InferenceContext, TurnArgs } from './inference/turn.js';
export const SYNTHESIS_TOOLS: ReadonlySet<string> = new Set([
'get_codebase_overview',
'get_framework_analysis',
'get_semantic_neighborhoods',
]);
const TOP_N_FILES = 5;
const FILE_LINE_CAP = 200;
const DOC_LINE_CAP = 500;
// Token budget for the auto-fetched content (files + docs combined). Estimated
// via chars/4 — a rough but stable proxy that doesn't require a tokenizer dep.
const TOKEN_BUDGET = 32_000;
const CHARS_PER_TOKEN = 4;
// 90s per synthesis call. Long enough for a thoughtful overview against a
// large auto-fetched payload; short enough that a hung upstream falls through
// to the normal recursive turn within a typical user attention window.
const SYNTH_TIMEOUT_MS = 90_000;
// File-extension regex for referenced-file extraction. Limited to source-
// language extensions so we don't pull in lockfiles, images, etc.
const FILE_PATH_RE =
/(?:^|[`'"<\s\(\[])([A-Za-z0-9_./@-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|c|cpp|h|hpp|md|json|yaml|yml|sql|sh|html|css))(?=[`'"<\)\]\s,;:]|$)/gm;
export interface SynthesisParams {
ctx: InferenceContext;
args: TurnArgs;
session: Session;
projectRoot: string;
toolName: string;
toolResultText: string;
// v1.13.15-b: when codecontext's wrapper hit its 32k inline-truncation
// limit, we expand the full content via readTruncation for reference-file
// extraction only. toolResultText (the truncated head) still ships to the
// synth model — preserves the 32k payload-budget contract.
truncated?: boolean;
// opaque id (tr_<…>), not a filesystem path — see truncate.ts naming note
outputPath?: string;
}
interface FetchedFile {
path: string;
content: string;
}
interface DocsCollection {
boochat?: string;
agents?: string;
context?: string;
roadmap?: string;
}
export async function runSynthesisPass(p: SynthesisParams): Promise<boolean> {
if (!SYNTHESIS_TOOLS.has(p.toolName)) return false;
let synthMessageId: string | null = null;
let accumulated = '';
let timedOut = false;
const synthCtrl = new AbortController();
const timer = setTimeout(() => {
timedOut = true;
synthCtrl.abort();
}, SYNTH_TIMEOUT_MS);
try {
const userMessage = await fetchOriginalUserMessage(p.ctx, p.args.chatId);
if (!userMessage) {
p.ctx.log.warn({ chatId: p.args.chatId }, 'synthesis: no user message found; falling through');
return false;
}
// v1.13.15-b: when the tool result was inline-truncated by the wrapper
// (32k cap, see codecontext_client.ts:114), expand the full content from
// tmpfs for reference-file extraction. The synth payload still ships the
// truncated head (see buildPayload call below) so the token-budget
// contract holds. Graceful degradation: if readTruncation returns null
// (missing id, ENOENT) or throws, fall back to the truncated head.
let extractionSource = p.toolResultText;
if (p.truncated && p.outputPath) {
try {
const full = await readTruncation(p.outputPath);
if (full !== null) {
extractionSource = full;
p.ctx.log.info(
{
chatId: p.args.chatId,
toolName: p.toolName,
originalChars: p.toolResultText.length,
fullChars: full.length,
},
'synthesis: expanded truncated tool output',
);
}
} catch (err) {
p.ctx.log.warn(
{ chatId: p.args.chatId, toolName: p.toolName, err: String(err) },
'synthesis: readTruncation failed, using truncated output',
);
}
}
const refFiles = extractReferencedFiles(extractionSource);
const files = await fetchTopFiles(refFiles, p.projectRoot);
const docs = await fetchProjectDocs(p.projectRoot);
const { files: budgetedFiles, docs: budgetedDocs } = applyTokenBudget(files, docs);
const synthMessages = buildPayload(
p.toolName,
// Truncated head only — full content was used for reference extraction above
p.toolResultText,
budgetedFiles,
budgetedDocs,
userMessage,
);
// Insert + announce the synthesis assistant message. From here on, any
// exception must clean up via the catch block so the row doesn't linger
// in 'streaming' status (the 5min stale-streaming sweeper catches it
// eventually, but explicit cleanup is better).
const [synthRow] = await p.ctx.sql<
{ id: string; started_at: string }[]
>`
INSERT INTO messages (session_id, chat_id, role, content, status, started_at, created_at)
VALUES (${p.args.sessionId}, ${p.args.chatId}, 'assistant', '', 'streaming', clock_timestamp(), clock_timestamp())
RETURNING id, started_at
`;
synthMessageId = synthRow!.id;
const startedAt = synthRow!.started_at;
p.ctx.publish(p.args.sessionId, {
type: 'message_started',
message_id: synthMessageId,
chat_id: p.args.chatId,
role: 'assistant',
});
// Combine the user-abort signal with our synthesis-specific timeout so
// either fires correctly. The `timedOut` flag in scope tells us which one
// tripped after streamCompletion throws.
const combinedSignal: AbortSignal | undefined = p.args.signal
? AbortSignal.any([p.args.signal, synthCtrl.signal])
: synthCtrl.signal;
const onDelta = (delta: string): void => {
accumulated += delta;
p.ctx.publish(p.args.sessionId, {
type: 'delta',
message_id: synthMessageId!,
chat_id: p.args.chatId,
content: delta,
});
};
const streamResult = await streamCompletion(
p.ctx,
p.session.model,
synthMessages,
{ tools: null },
onDelta,
undefined,
combinedSignal,
);
const mctx = await modelContext.getModelContext(p.session.model);
const nCtx = mctx?.n_ctx ?? null;
const [updated] = await p.ctx.sql<
{
tokens_used: number | null;
ctx_used: number | null;
ctx_max: number | null;
finished_at: string | null;
}[]
>`
UPDATE messages
SET content = ${streamResult.content},
status = 'complete',
tokens_used = ${streamResult.completionTokens},
ctx_used = ${streamResult.promptTokens},
ctx_max = ${nCtx},
finished_at = clock_timestamp()
WHERE id = ${synthMessageId}
RETURNING tokens_used, ctx_used, ctx_max, finished_at
`;
await insertParts(p.ctx.sql, [
{
message_id: synthMessageId,
sequence: 0,
kind: 'synthesis',
payload: { text: streamResult.content },
},
]);
p.ctx.publish(p.args.sessionId, {
type: 'message_complete',
message_id: synthMessageId,
chat_id: p.args.chatId,
tokens_used: updated?.tokens_used ?? null,
ctx_used: updated?.ctx_used ?? null,
ctx_max: updated?.ctx_max ?? null,
started_at: startedAt,
finished_at: updated?.finished_at ?? null,
model: p.session.model,
});
p.ctx.publishUser({
type: 'chat_status',
chat_id: p.args.chatId,
status: 'idle',
at: new Date().toISOString(),
});
p.ctx.log.info(
{
chatId: p.args.chatId,
synthMessageId,
toolName: p.toolName,
chars: streamResult.content.length,
files: budgetedFiles.length,
},
'synthesis pass complete',
);
return true;
} catch (err) {
await markSynthFailed(p, synthMessageId, accumulated).catch((cleanupErr) => {
p.ctx.log.warn({ cleanupErr: String(cleanupErr) }, 'synthesis cleanup UPDATE failed');
});
if (err instanceof Error && err.name === 'AbortError') {
if (timedOut) {
p.ctx.log.warn(
{ toolName: p.toolName, chatId: p.args.chatId },
'synthesis pass timed out; falling through to recursive turn',
);
return false;
}
// User-initiated abort: propagate so the outer error handler marks the
// parent turn cancelled. The synth message is already marked failed by
// markSynthFailed above.
throw err;
}
p.ctx.log.warn(
{ err: String(err), toolName: p.toolName, chatId: p.args.chatId },
'synthesis pass failed; falling through to recursive turn',
);
return false;
} finally {
clearTimeout(timer);
}
}
async function markSynthFailed(
p: SynthesisParams,
synthMessageId: string | null,
accumulated: string,
): Promise<void> {
if (synthMessageId === null) return;
await p.ctx.sql`
UPDATE messages
SET content = ${accumulated},
status = 'failed',
finished_at = clock_timestamp()
WHERE id = ${synthMessageId}
`;
// Republish so the frontend's live state flips from 'streaming' to
// terminal. message_complete carries no error reason — the row's status
// column is the truth. The 5-state chat_status dot has 'error' but we
// don't fire that here because the broader inference is about to retry
// via recursion; flipping the user-channel status to 'error' would race
// the recursive turn's 'streaming' announcement.
p.ctx.publish(p.args.sessionId, {
type: 'message_complete',
message_id: synthMessageId,
chat_id: p.args.chatId,
model: p.session.model,
});
}
async function fetchOriginalUserMessage(
ctx: InferenceContext,
chatId: string,
): Promise<string | null> {
const rows = await ctx.sql<{ content: string }[]>`
SELECT content FROM messages
WHERE chat_id = ${chatId} AND role = 'user'
ORDER BY created_at DESC
LIMIT 1
`;
return rows[0]?.content ?? null;
}
function extractReferencedFiles(text: string): string[] {
const seen = new Set<string>();
const order: string[] = [];
let m: RegExpExecArray | null;
while ((m = FILE_PATH_RE.exec(text)) !== null) {
const candidate = m[1]!;
if (seen.has(candidate)) continue;
if (
candidate.includes('node_modules') ||
candidate.includes('/dist/') ||
candidate.includes('/test/') ||
candidate.includes('/tests/') ||
/\.(test|spec)\.[a-z]+$/.test(candidate)
) {
continue;
}
seen.add(candidate);
order.push(candidate);
}
return order;
}
async function fetchTopFiles(refs: string[], projectRoot: string): Promise<FetchedFile[]> {
const tool = TOOLS_BY_NAME['view_file'];
if (!tool) return [];
const out: FetchedFile[] = [];
for (const p of refs.slice(0, TOP_N_FILES)) {
const absPath = p.startsWith('/') ? p : join(projectRoot, p);
try {
const r = await tool.execute({ path: absPath, end_line: FILE_LINE_CAP }, projectRoot);
const content = (r as { content?: string }).content ?? '';
if (content) out.push({ path: p, content });
} catch {
// path-scope blocked, secret-filtered, file too large, or missing —
// skip silently. The remaining files (or none) still produce a
// meaningful synthesis input.
}
}
return out;
}
async function fetchProjectDocs(projectRoot: string): Promise<DocsCollection> {
const tool = TOOLS_BY_NAME['view_file'];
if (!tool) return {};
const docs: DocsCollection = {};
for (const [filename, key] of [
['BOOCHAT.md', 'boochat'],
['AGENTS.md', 'agents'],
['CONTEXT.md', 'context'],
] as const) {
try {
const r = await tool.execute(
{ path: join(projectRoot, filename), end_line: DOC_LINE_CAP },
projectRoot,
);
const content = (r as { content?: string }).content;
if (content) docs[key] = content;
} catch {
// missing doc — skip
}
}
// Case-insensitive *roadmap*.md glob. Picks the first match (alphabetical
// by readdir() order); typical projects have at most one roadmap doc.
try {
const entries = await fs.readdir(projectRoot);
const roadmap = entries.find(
(e) => /roadmap/i.test(e) && e.toLowerCase().endsWith('.md'),
);
if (roadmap) {
const r = await tool.execute(
{ path: join(projectRoot, roadmap), end_line: DOC_LINE_CAP },
projectRoot,
);
const content = (r as { content?: string }).content;
if (content) docs.roadmap = content;
}
} catch {
// unreadable project root — skip
}
return docs;
}
function estTokens(s: string | undefined): number {
return s ? Math.ceil(s.length / CHARS_PER_TOKEN) : 0;
}
function applyTokenBudget(
files: FetchedFile[],
docs: DocsCollection,
): { files: FetchedFile[]; docs: DocsCollection } {
let total = 0;
for (const f of files) total += estTokens(f.content);
total += estTokens(docs.boochat) + estTokens(docs.agents) + estTokens(docs.context) + estTokens(docs.roadmap);
if (total <= TOKEN_BUDGET) return { files, docs };
// Drop priority (lowest priority dropped first):
// 1. top-2..N files (keep top-1)
// 2. top-1 file
// 3. roadmap (+ CONTEXT.md grouped here — dispatch listed roadmap above
// AGENTS.md, CONTEXT.md was not in the priority list)
// 4. AGENTS.md
// 5. BOOCHAT.md (never dropped — truncate to budget if alone exceeds)
let outFiles = files.slice();
const outDocs: DocsCollection = { ...docs };
while (total > TOKEN_BUDGET && outFiles.length > 1) {
const last = outFiles.pop()!;
total -= estTokens(last.content);
}
if (total <= TOKEN_BUDGET) return { files: outFiles, docs: outDocs };
if (outFiles[0]) {
total -= estTokens(outFiles[0].content);
outFiles = [];
}
if (total <= TOKEN_BUDGET) return { files: outFiles, docs: outDocs };
if (outDocs.roadmap) {
total -= estTokens(outDocs.roadmap);
delete outDocs.roadmap;
}
if (outDocs.context) {
total -= estTokens(outDocs.context);
delete outDocs.context;
}
if (total <= TOKEN_BUDGET) return { files: outFiles, docs: outDocs };
if (outDocs.agents) {
total -= estTokens(outDocs.agents);
delete outDocs.agents;
}
if (total <= TOKEN_BUDGET) return { files: outFiles, docs: outDocs };
if (outDocs.boochat) {
const maxChars = TOKEN_BUDGET * CHARS_PER_TOKEN;
if (outDocs.boochat.length > maxChars) {
outDocs.boochat = outDocs.boochat.slice(0, maxChars);
}
}
return { files: outFiles, docs: outDocs };
}
function buildPayload(
toolName: string,
toolResultText: string,
files: FetchedFile[],
docs: DocsCollection,
userMessage: string,
): OpenAiMessage[] {
const sections: string[] = [];
sections.push(`## Codecontext tool output (${toolName})\n\n${toolResultText}`);
if (files.length > 0) {
sections.push(`---\n\n## Auto-fetched source files`);
for (const f of files) {
sections.push(`### ${f.path}\n\n\`\`\`\n${f.content}\n\`\`\``);
}
}
const docEntries: Array<[string, string | undefined]> = [
['BOOCHAT.md', docs.boochat],
['AGENTS.md', docs.agents],
['CONTEXT.md', docs.context],
['roadmap', docs.roadmap],
];
const presentDocs = docEntries.filter(([, v]) => Boolean(v));
if (presentDocs.length > 0) {
sections.push(`---\n\n## Project documentation`);
for (const [name, v] of presentDocs) {
sections.push(`### ${name}\n\n${v!}`);
}
}
sections.push(`---\n\n## Original user question\n\n${userMessage}`);
return [
{ role: 'system', content: SYNTHESIS_SYSTEM_PROMPT },
{ role: 'user', content: sections.join('\n\n') },
];
}

View File

@@ -0,0 +1,20 @@
// v1.13.13: synthesis pipeline system prompt. Verbatim from the v1.13.13
// dispatch — do not paraphrase. The synthesis pass loads this as its sole
// system message, followed by a user message that concatenates the
// codecontext tool result, auto-fetched top files, auto-fetched project
// docs, and the original user message.
export const SYNTHESIS_SYSTEM_PROMPT = `You are synthesizing structural data into an accurate, detailed answer about the user's codebase.
Inputs you have been given:
1. The output of a codecontext analysis tool (raw structural data — file counts, symbols, dependencies, frameworks).
2. The contents of the top files referenced in that output.
3. Any project documentation found in the repo root (BOOCHAT.md, AGENTS.md, roadmap docs, CONTEXT.md).
Rules:
- Cite specific files and line numbers when making claims about code.
- If project docs contradict the code, docs win for questions about state, version, status, or roadmap. Code wins for questions about runtime behavior or implementation.
- If the codecontext output looks sparse (low symbol count for a TypeScript project, missing dependency edges, empty framework list), explicitly say so — codecontext falls back to the JavaScript grammar for TypeScript and loses interfaces, generics, decorators, and type aliases.
- Do not invent symbols, files, or relationships that are not present in the inputs.
- Do not respond with a generic "this looks like a [framework] project" summary. The user has the framework analysis already. Add specifics: what is actually in this codebase, what is shipped, what is planned, what is load-bearing.
- Length: match the depth the user asked for. Overview questions get structured multi-section answers. Specific questions get focused answers.
`;

View File

@@ -8,9 +8,19 @@
// + container guidance (this layer, NEW in v1.12)
// + agent.system_prompt (resolved from data/AGENTS.md by getAgentById)
// + session.system_prompt OR project.default_system_prompt
//
// v1.13.8: byte-stability instrumentation. buildSystemPromptWithFingerprint
// returns the assembled string plus a SHA-256 fingerprint and a per-session
// drift signal. buildSystemPrompt stays a string→string shim for backward
// compat (tests use it). No cache added — recon proved input-layer mtime
// caches (this file + agents.ts) already deliver byte-stable inputs in
// steady state. v1.13.8 measures that claim against production traffic
// before any cache infrastructure earns its place.
import { createHash } from 'node:crypto';
import { readFile, stat } from 'node:fs/promises';
import type { Agent, Project, Session } from '../types/api.js';
import { getAgentsMtimes } from './agents.js';
const BASE_SYSTEM_PROMPT = (projectPath: string) =>
`You are BooCode Chat, a code investigation assistant. The user is working on a project located at ${projectPath}. Use the file-read tools (view_file, list_dir, grep, find_files) to investigate code when needed. Be concise. Cite file paths and line numbers when discussing code. Do not hallucinate file contents — read the file first. Tool results may be truncated; if so, narrow your query rather than guessing.`;
@@ -60,11 +70,94 @@ export function _resetContainerGuidanceCacheForTests(): void {
cachedGuidance = null;
}
export async function buildSystemPrompt(
// v1.13.8: expose the mtime currently held in the BOOCHAT cache so the
// fingerprint log can stamp it without re-statting (no I/O race against
// getContainerGuidance, which is the canonical mtime source).
function getCachedGuidanceMtime(): number | null {
if (!cachedGuidance) return null;
// mtime=0 is the sentinel for "file is missing" (set in the catch above).
// Surface it as null so the log/diff doesn't treat absence as a number.
return cachedGuidance.mtime > 0 ? cachedGuidance.mtime : null;
}
// v1.13.8: fingerprint emitted per turn, observer state keyed by session.
// Field set is intentionally small — we want the diff between two
// fingerprints to point at the exact input that drifted, not bury the
// signal in noise.
export interface PrefixFingerprint {
msg: 'prefix-fingerprint';
project_id: string;
agent_id: string | null;
agent_name: string | null;
session_id: string;
prefix_hash: string;
prefix_length: number;
mtime_boochat: number | null;
mtime_agents_global: number | null;
mtime_agents_project: number | null;
has_agent_system_prompt: boolean;
has_session_override: boolean;
has_project_override: boolean;
}
export interface PrefixDrift {
msg: 'prefix-drift';
session_id: string;
prev_hash: string;
new_hash: string;
prev_length: number;
new_length: number;
// Names of fields in PrefixFingerprint (excluding the hash + length pair
// and the session_id key itself) whose values differ between the previous
// observation and this one. The bug case is `changed_inputs: []` — hash
// differs but no tracked input moved, which means assembly is
// nondeterministic somewhere.
changed_inputs: string[];
}
// Fields tracked per-session for the drift diff. Stored alongside the hash
// so we can recompute changed_inputs without re-running buildSystemPrompt.
interface ObservedInputs {
agent_id: string | null;
mtime_boochat: number | null;
mtime_agents_global: number | null;
mtime_agents_project: number | null;
has_agent_system_prompt: boolean;
has_session_override: boolean;
has_project_override: boolean;
}
interface ObserverEntry {
hash: string;
length: number;
inputs: ObservedInputs;
}
// Unbounded by design for v1.13.8 (instrumentation, short-lived sessions in
// the smoke test). TODO(v1.13.x follow-up if v1.13.8 surfaces stable):
// LRU-bound this Map at 1000 sessions when the in-process surface lives long
// enough to matter.
const prefixObserver = new Map<string, ObserverEntry>();
// Test-only: clear the observer so consecutive tests don't share state.
export function _resetPrefixObserverForTests(): void {
prefixObserver.clear();
}
function computeChangedInputs(prev: ObservedInputs, curr: ObservedInputs): string[] {
const out: string[] = [];
const keys = Object.keys(curr) as (keyof ObservedInputs)[];
for (const k of keys) {
if (prev[k] !== curr[k]) out.push(k);
}
return out;
}
export async function buildSystemPromptWithFingerprint(
project: Project,
session: Session,
agent: Agent | null
): Promise<string> {
agent: Agent | null,
): Promise<{ prompt: string; fingerprint: PrefixFingerprint; drift: PrefixDrift | null }> {
let out = BASE_SYSTEM_PROMPT(project.path);
const guidance = await getContainerGuidance();
if (guidance) {
@@ -79,5 +172,60 @@ export async function buildSystemPrompt(
if (userPrompt.length > 0) {
out += '\n\n' + userPrompt;
}
return out;
const hash = createHash('sha256').update(out, 'utf8').digest('hex');
const agentsMtimes = getAgentsMtimes(project.path);
const inputs: ObservedInputs = {
agent_id: agent?.id ?? null,
mtime_boochat: getCachedGuidanceMtime(),
mtime_agents_global: agentsMtimes.global,
mtime_agents_project: agentsMtimes.project,
has_agent_system_prompt: !!(agent && agent.system_prompt.trim().length > 0),
has_session_override: sessionPrompt.length > 0,
has_project_override: projectPrompt.length > 0,
};
const fingerprint: PrefixFingerprint = {
msg: 'prefix-fingerprint',
project_id: project.id,
agent_id: agent?.id ?? null,
agent_name: agent?.name ?? null,
session_id: session.id,
prefix_hash: hash,
prefix_length: out.length,
mtime_boochat: inputs.mtime_boochat,
mtime_agents_global: inputs.mtime_agents_global,
mtime_agents_project: inputs.mtime_agents_project,
has_agent_system_prompt: inputs.has_agent_system_prompt,
has_session_override: inputs.has_session_override,
has_project_override: inputs.has_project_override,
};
let drift: PrefixDrift | null = null;
const prev = prefixObserver.get(session.id);
if (prev && prev.hash !== hash) {
drift = {
msg: 'prefix-drift',
session_id: session.id,
prev_hash: prev.hash,
new_hash: hash,
prev_length: prev.length,
new_length: out.length,
changed_inputs: computeChangedInputs(prev.inputs, inputs),
};
}
prefixObserver.set(session.id, { hash, length: out.length, inputs });
return { prompt: out, fingerprint, drift };
}
// Backward-compatible string-returning shim. Kept so existing callers
// (tests, future code paths that don't want to log) work unchanged.
export async function buildSystemPrompt(
project: Project,
session: Session,
agent: Agent | null,
): Promise<string> {
const { prompt } = await buildSystemPromptWithFingerprint(project, session, agent);
return prompt;
}

View File

@@ -700,6 +700,64 @@ export const TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntrie
ALL_TOOLS.map((t) => [t.name, t])
);
// v1.13.15-tools: tiered tool loading. BOOCODE_TOOLS env var (`core` |
// `standard` | `all`) filters the agent's tool whitelist before LLM dispatch.
// Daily-driver token win on qwen3.6-35b-a3b — the 35B-A3B MoE benefits from
// any prompt-cache stability win (fewer tools = shorter, more stable tool
// schemas in the system prompt). Pattern lift from eyaltoledano/claude-task-
// master (MIT + Commons Clause — pattern only, no code lift).
//
// The env var is a CEILING. It only narrows; never expands an agent's
// declared whitelist. Default behavior (var unset) is unchanged: all tools.
export const CORE_TOOL_NAMES = [
'view_file',
'list_dir',
'grep',
'find_files',
] as const;
export const STANDARD_TOOL_NAMES = [
...CORE_TOOL_NAMES,
'web_search',
'web_fetch',
'git_status',
'get_codebase_overview',
'get_file_analysis',
'get_symbol_info',
'search_symbols',
'get_dependencies',
'watch_changes',
'get_semantic_neighborhoods',
'get_framework_analysis',
] as const;
// Module-load validation: every name in CORE / STANDARD must exist in
// TOOLS_BY_NAME. Catches typos and stale tier definitions before they reach
// production; server boot fails loudly rather than silently filtering valid
// tools out of agent whitelists.
for (const name of CORE_TOOL_NAMES) {
if (!TOOLS_BY_NAME[name]) {
throw new Error(`CORE_TOOL_NAMES references unknown tool: '${name}'`);
}
}
for (const name of STANDARD_TOOL_NAMES) {
if (!TOOLS_BY_NAME[name]) {
throw new Error(`STANDARD_TOOL_NAMES references unknown tool: '${name}'`);
}
}
export function resolveToolTier(tier: string | undefined): readonly string[] {
switch ((tier ?? 'all').toLowerCase()) {
case 'core':
return CORE_TOOL_NAMES;
case 'standard':
return STANDARD_TOOL_NAMES;
case 'all':
default:
return ALL_TOOLS.map((t) => t.name);
}
}
export function toolJsonSchemas(): ToolJsonSchema[] {
return ALL_TOOLS.map((t) => t.jsonSchema);
}

View File

@@ -0,0 +1,328 @@
// v1.13.11-a: Zod schemas for every WebSocket frame published by the server.
// Validation runs both on send (broker.publishFrame / publishUserFrame) and
// on receive (apps/web/src/hooks/useSessionStream + useUserEvents). Catches
// silent protocol drift between publisher and consumer.
//
// IMPORTANT: This file is duplicated byte-identical at
// apps/web/src/api/ws-frames.ts. The two apps have separate tsconfigs and
// no path alias; the duplication is sync-by-hand. A test asserts the two
// files match. If you change one, change the other.
//
// Per-kind payload schemas (tool_call args, message_parts payloads, etc.)
// stay z.unknown() in v1.13.11. Frame-level drift detection is the goal;
// deep payload validation is follow-up work.
import { z } from 'zod';
// ---- shared primitives -----------------------------------------------------
const Uuid = z.string().uuid();
// Tool call IDs are model-emitted (e.g. "call_abc123") — not UUIDs.
const ToolCallId = z.string().min(1);
// v1.13.12 fix: postgres returns timestamp columns as JS Date objects, not
// strings. The publish sites pass them through unchanged, so the schema must
// tolerate both. preprocess converts Date → ISO string before string-validation;
// on the web side (where frames arrive via JSON.parse) it's a no-op. Before
// this fix, every message_complete / session_updated / chat_updated frame
// failed validation and got dropped — symptoms: token tracking blank in UI,
// status stuck at 'streaming' tripping the 60s stale-stream banner.
const IsoTimestamp = z.preprocess(
(v) => (v instanceof Date ? v.toISOString() : v),
z.string().min(1),
);
const ChatStatusValue = z.enum([
'streaming',
'tool_running',
'waiting_for_input',
'idle',
'error',
]);
const ErrorReasonValue = z.enum([
'llm_provider_error',
'doom_loop',
'doom_loop_summary_failed',
'cap_hit',
'cap_hit_summary_failed',
]);
const MessageRoleValue = z.enum(['user', 'assistant', 'system', 'tool']);
const ToolCallShape = z.object({
id: ToolCallId,
name: z.string().min(1),
args: z.record(z.string(), z.unknown()),
});
// Free-form bags: opaque to the frame schema; deep validation is out of
// scope for v1.13.11 (frame-level drift detection is the goal; per-kind
// payload narrowing is follow-up work). z.unknown() means the consumer
// must narrow before reading — TypeScript-side this is fine because every
// consumer already operates on the hand-maintained Project / Chat / Session
// / WorkspacePane types (the brief's "Don't strip existing types yet"
// rule), and the Zod-typed shape is only used at the publishFrame boundary.
const OpaqueObject = z.unknown();
// ---- per-session channel frames --------------------------------------------
export const SnapshotFrame = z.object({
type: z.literal('snapshot'),
messages: z.array(OpaqueObject),
});
export const MessageStartedFrame = z.object({
type: z.literal('message_started'),
message_id: Uuid,
chat_id: Uuid.optional(),
role: MessageRoleValue,
});
export const DeltaFrame = z.object({
type: z.literal('delta'),
message_id: Uuid,
chat_id: Uuid.optional(),
content: z.string(),
});
export const ToolCallFrame = z.object({
type: z.literal('tool_call'),
message_id: Uuid,
chat_id: Uuid.optional(),
tool_call: ToolCallShape,
});
export const ToolResultFrame = z.object({
type: z.literal('tool_result'),
tool_message_id: Uuid,
chat_id: Uuid.optional(),
tool_call_id: ToolCallId,
output: z.unknown(),
truncated: z.boolean(),
error: z.string().optional(),
});
export const MessageCompleteFrame = z.object({
type: z.literal('message_complete'),
message_id: Uuid,
chat_id: Uuid.optional(),
tokens_used: z.number().int().nonnegative().nullable().optional(),
ctx_used: z.number().int().nonnegative().nullable().optional(),
ctx_max: z.number().int().positive().nullable().optional(),
started_at: IsoTimestamp.nullable().optional(),
finished_at: IsoTimestamp.nullable().optional(),
model: z.string().optional(),
metadata: OpaqueObject.nullable().optional(),
});
export const UsageFrame = z.object({
type: z.literal('usage'),
message_id: Uuid,
chat_id: Uuid.optional(),
completion_tokens: z.number().int().nonnegative().nullable(),
ctx_used: z.number().int().nonnegative().nullable(),
ctx_max: z.number().int().positive().nullable(),
});
export const MessagesDeletedFrame = z.object({
type: z.literal('messages_deleted'),
message_ids: z.array(Uuid),
chat_id: Uuid.optional(),
});
export const ChatRenamedFrame = z.object({
type: z.literal('chat_renamed'),
chat_id: Uuid,
name: z.string(),
});
export const CompactedFrame = z.object({
type: z.literal('compacted'),
session_id: Uuid,
chat_id: Uuid,
summary_message_id: Uuid,
});
export const ErrorFrame = z.object({
type: z.literal('error'),
message_id: Uuid.optional(),
chat_id: Uuid.optional(),
error: z.string(),
reason: ErrorReasonValue.optional(),
});
// ---- per-user channel frames (sidebar refresh) -----------------------------
export const ChatStatusFrame = z.object({
type: z.literal('chat_status'),
chat_id: Uuid,
status: ChatStatusValue,
at: IsoTimestamp,
reason: ErrorReasonValue.optional(),
});
export const SessionUpdatedFrame = z.object({
type: z.literal('session_updated'),
session_id: Uuid,
project_id: Uuid,
name: z.string(),
updated_at: IsoTimestamp,
});
export const SessionRenamedFrame = z.object({
type: z.literal('session_renamed'),
session_id: Uuid,
name: z.string(),
});
export const SessionCreatedFrame = z.object({
type: z.literal('session_created'),
session: OpaqueObject,
project_id: Uuid,
});
export const SessionArchivedFrame = z.object({
type: z.literal('session_archived'),
session_id: Uuid,
project_id: Uuid,
});
export const SessionDeletedFrame = z.object({
type: z.literal('session_deleted'),
session_id: Uuid,
project_id: Uuid,
});
export const SessionWorkspaceUpdatedFrame = z.object({
type: z.literal('session_workspace_updated'),
session_id: Uuid,
workspace_panes: z.array(OpaqueObject),
});
export const ChatCreatedFrame = z.object({
type: z.literal('chat_created'),
chat: OpaqueObject,
session_id: Uuid,
});
export const ChatUpdatedFrame = z.object({
type: z.literal('chat_updated'),
chat_id: Uuid,
session_id: Uuid,
name: z.string().nullable(),
updated_at: IsoTimestamp,
});
export const ChatArchivedFrame = z.object({
type: z.literal('chat_archived'),
chat_id: Uuid,
session_id: Uuid,
});
export const ChatUnarchivedFrame = z.object({
type: z.literal('chat_unarchived'),
chat: OpaqueObject,
});
export const ChatDeletedFrame = z.object({
type: z.literal('chat_deleted'),
chat_id: Uuid,
session_id: Uuid,
});
export const ProjectCreatedFrame = z.object({
type: z.literal('project_created'),
project: OpaqueObject,
});
export const ProjectArchivedFrame = z.object({
type: z.literal('project_archived'),
project_id: Uuid,
});
export const ProjectUnarchivedFrame = z.object({
type: z.literal('project_unarchived'),
project: OpaqueObject,
});
export const ProjectUpdatedFrame = z.object({
type: z.literal('project_updated'),
project_id: Uuid,
name: z.string(),
});
export const ProjectDeletedFrame = z.object({
type: z.literal('project_deleted'),
project_id: Uuid,
});
// ---- discriminated union ---------------------------------------------------
export const WsFrameSchema = z.discriminatedUnion('type', [
// per-session
SnapshotFrame,
MessageStartedFrame,
DeltaFrame,
ToolCallFrame,
ToolResultFrame,
MessageCompleteFrame,
UsageFrame,
MessagesDeletedFrame,
ChatRenamedFrame,
CompactedFrame,
ErrorFrame,
// per-user
ChatStatusFrame,
SessionUpdatedFrame,
SessionRenamedFrame,
SessionCreatedFrame,
SessionArchivedFrame,
SessionDeletedFrame,
SessionWorkspaceUpdatedFrame,
ChatCreatedFrame,
ChatUpdatedFrame,
ChatArchivedFrame,
ChatUnarchivedFrame,
ChatDeletedFrame,
ProjectCreatedFrame,
ProjectArchivedFrame,
ProjectUnarchivedFrame,
ProjectUpdatedFrame,
ProjectDeletedFrame,
]);
export type WsFrame = z.infer<typeof WsFrameSchema>;
// Convenience: the set of known frame types. Useful for the publishFrame
// helper to log the offending type name when validation fails. Kept in sync
// by hand with the discriminated union above.
export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
'snapshot',
'message_started',
'delta',
'tool_call',
'tool_result',
'message_complete',
'usage',
'messages_deleted',
'chat_renamed',
'compacted',
'error',
'chat_status',
'session_updated',
'session_renamed',
'session_created',
'session_archived',
'session_deleted',
'session_workspace_updated',
'chat_created',
'chat_updated',
'chat_archived',
'chat_unarchived',
'chat_deleted',
'project_created',
'project_archived',
'project_unarchived',
'project_updated',
'project_deleted',
] as const;

View File

@@ -31,7 +31,8 @@
"shiki": "^1.29.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",

View File

@@ -12,6 +12,7 @@ import type {
GitMeta,
Skill,
AskUserAnswer,
ToolCostStat,
} from './types';
export class ApiError extends Error {
@@ -262,6 +263,14 @@ export const api = {
list: () => request<{ skills: Skill[] }>('/api/skills'),
},
// v1.13.10: per-tool cost rolling-window stats (last 100 calls per tool,
// equal-split attribution across multi-tool turns). Read endpoint backed by
// the tool_cost_stats view. AgentPicker consumes this for per-agent cost
// hints.
tools: {
costStats: () => request<{ stats: ToolCostStat[] }>('/api/tools/cost_stats'),
},
settings: {
get: () => request<Record<string, unknown>>('/api/settings'),
patch: (body: Record<string, unknown>) =>

View File

@@ -1,6 +1,18 @@
export const PROJECT_STATUSES = ['open', 'archived'] as const;
export type ProjectStatus = typeof PROJECT_STATUSES[number];
// v1.13.10: per-tool cost rolling-window stat. Returned by
// GET /api/tools/cost_stats — one entry per tool with mean prompt/completion
// tokens over the last 100 invocations. AgentPicker sums across an agent's
// whitelisted tools for per-agent cost hints.
export interface ToolCostStat {
tool_name: string;
mean_prompt_tokens: number;
mean_completion_tokens: number;
n_calls: number;
updated_at: string;
}
export interface Project {
id: string;
name: string;

View File

@@ -0,0 +1,328 @@
// v1.13.11-a: Zod schemas for every WebSocket frame published by the server.
// Validation runs both on send (broker.publishFrame / publishUserFrame) and
// on receive (apps/web/src/hooks/useSessionStream + useUserEvents). Catches
// silent protocol drift between publisher and consumer.
//
// IMPORTANT: This file is duplicated byte-identical at
// apps/web/src/api/ws-frames.ts. The two apps have separate tsconfigs and
// no path alias; the duplication is sync-by-hand. A test asserts the two
// files match. If you change one, change the other.
//
// Per-kind payload schemas (tool_call args, message_parts payloads, etc.)
// stay z.unknown() in v1.13.11. Frame-level drift detection is the goal;
// deep payload validation is follow-up work.
import { z } from 'zod';
// ---- shared primitives -----------------------------------------------------
const Uuid = z.string().uuid();
// Tool call IDs are model-emitted (e.g. "call_abc123") — not UUIDs.
const ToolCallId = z.string().min(1);
// v1.13.12 fix: postgres returns timestamp columns as JS Date objects, not
// strings. The publish sites pass them through unchanged, so the schema must
// tolerate both. preprocess converts Date → ISO string before string-validation;
// on the web side (where frames arrive via JSON.parse) it's a no-op. Before
// this fix, every message_complete / session_updated / chat_updated frame
// failed validation and got dropped — symptoms: token tracking blank in UI,
// status stuck at 'streaming' tripping the 60s stale-stream banner.
const IsoTimestamp = z.preprocess(
(v) => (v instanceof Date ? v.toISOString() : v),
z.string().min(1),
);
const ChatStatusValue = z.enum([
'streaming',
'tool_running',
'waiting_for_input',
'idle',
'error',
]);
const ErrorReasonValue = z.enum([
'llm_provider_error',
'doom_loop',
'doom_loop_summary_failed',
'cap_hit',
'cap_hit_summary_failed',
]);
const MessageRoleValue = z.enum(['user', 'assistant', 'system', 'tool']);
const ToolCallShape = z.object({
id: ToolCallId,
name: z.string().min(1),
args: z.record(z.string(), z.unknown()),
});
// Free-form bags: opaque to the frame schema; deep validation is out of
// scope for v1.13.11 (frame-level drift detection is the goal; per-kind
// payload narrowing is follow-up work). z.unknown() means the consumer
// must narrow before reading — TypeScript-side this is fine because every
// consumer already operates on the hand-maintained Project / Chat / Session
// / WorkspacePane types (the brief's "Don't strip existing types yet"
// rule), and the Zod-typed shape is only used at the publishFrame boundary.
const OpaqueObject = z.unknown();
// ---- per-session channel frames --------------------------------------------
export const SnapshotFrame = z.object({
type: z.literal('snapshot'),
messages: z.array(OpaqueObject),
});
export const MessageStartedFrame = z.object({
type: z.literal('message_started'),
message_id: Uuid,
chat_id: Uuid.optional(),
role: MessageRoleValue,
});
export const DeltaFrame = z.object({
type: z.literal('delta'),
message_id: Uuid,
chat_id: Uuid.optional(),
content: z.string(),
});
export const ToolCallFrame = z.object({
type: z.literal('tool_call'),
message_id: Uuid,
chat_id: Uuid.optional(),
tool_call: ToolCallShape,
});
export const ToolResultFrame = z.object({
type: z.literal('tool_result'),
tool_message_id: Uuid,
chat_id: Uuid.optional(),
tool_call_id: ToolCallId,
output: z.unknown(),
truncated: z.boolean(),
error: z.string().optional(),
});
export const MessageCompleteFrame = z.object({
type: z.literal('message_complete'),
message_id: Uuid,
chat_id: Uuid.optional(),
tokens_used: z.number().int().nonnegative().nullable().optional(),
ctx_used: z.number().int().nonnegative().nullable().optional(),
ctx_max: z.number().int().positive().nullable().optional(),
started_at: IsoTimestamp.nullable().optional(),
finished_at: IsoTimestamp.nullable().optional(),
model: z.string().optional(),
metadata: OpaqueObject.nullable().optional(),
});
export const UsageFrame = z.object({
type: z.literal('usage'),
message_id: Uuid,
chat_id: Uuid.optional(),
completion_tokens: z.number().int().nonnegative().nullable(),
ctx_used: z.number().int().nonnegative().nullable(),
ctx_max: z.number().int().positive().nullable(),
});
export const MessagesDeletedFrame = z.object({
type: z.literal('messages_deleted'),
message_ids: z.array(Uuid),
chat_id: Uuid.optional(),
});
export const ChatRenamedFrame = z.object({
type: z.literal('chat_renamed'),
chat_id: Uuid,
name: z.string(),
});
export const CompactedFrame = z.object({
type: z.literal('compacted'),
session_id: Uuid,
chat_id: Uuid,
summary_message_id: Uuid,
});
export const ErrorFrame = z.object({
type: z.literal('error'),
message_id: Uuid.optional(),
chat_id: Uuid.optional(),
error: z.string(),
reason: ErrorReasonValue.optional(),
});
// ---- per-user channel frames (sidebar refresh) -----------------------------
export const ChatStatusFrame = z.object({
type: z.literal('chat_status'),
chat_id: Uuid,
status: ChatStatusValue,
at: IsoTimestamp,
reason: ErrorReasonValue.optional(),
});
export const SessionUpdatedFrame = z.object({
type: z.literal('session_updated'),
session_id: Uuid,
project_id: Uuid,
name: z.string(),
updated_at: IsoTimestamp,
});
export const SessionRenamedFrame = z.object({
type: z.literal('session_renamed'),
session_id: Uuid,
name: z.string(),
});
export const SessionCreatedFrame = z.object({
type: z.literal('session_created'),
session: OpaqueObject,
project_id: Uuid,
});
export const SessionArchivedFrame = z.object({
type: z.literal('session_archived'),
session_id: Uuid,
project_id: Uuid,
});
export const SessionDeletedFrame = z.object({
type: z.literal('session_deleted'),
session_id: Uuid,
project_id: Uuid,
});
export const SessionWorkspaceUpdatedFrame = z.object({
type: z.literal('session_workspace_updated'),
session_id: Uuid,
workspace_panes: z.array(OpaqueObject),
});
export const ChatCreatedFrame = z.object({
type: z.literal('chat_created'),
chat: OpaqueObject,
session_id: Uuid,
});
export const ChatUpdatedFrame = z.object({
type: z.literal('chat_updated'),
chat_id: Uuid,
session_id: Uuid,
name: z.string().nullable(),
updated_at: IsoTimestamp,
});
export const ChatArchivedFrame = z.object({
type: z.literal('chat_archived'),
chat_id: Uuid,
session_id: Uuid,
});
export const ChatUnarchivedFrame = z.object({
type: z.literal('chat_unarchived'),
chat: OpaqueObject,
});
export const ChatDeletedFrame = z.object({
type: z.literal('chat_deleted'),
chat_id: Uuid,
session_id: Uuid,
});
export const ProjectCreatedFrame = z.object({
type: z.literal('project_created'),
project: OpaqueObject,
});
export const ProjectArchivedFrame = z.object({
type: z.literal('project_archived'),
project_id: Uuid,
});
export const ProjectUnarchivedFrame = z.object({
type: z.literal('project_unarchived'),
project: OpaqueObject,
});
export const ProjectUpdatedFrame = z.object({
type: z.literal('project_updated'),
project_id: Uuid,
name: z.string(),
});
export const ProjectDeletedFrame = z.object({
type: z.literal('project_deleted'),
project_id: Uuid,
});
// ---- discriminated union ---------------------------------------------------
export const WsFrameSchema = z.discriminatedUnion('type', [
// per-session
SnapshotFrame,
MessageStartedFrame,
DeltaFrame,
ToolCallFrame,
ToolResultFrame,
MessageCompleteFrame,
UsageFrame,
MessagesDeletedFrame,
ChatRenamedFrame,
CompactedFrame,
ErrorFrame,
// per-user
ChatStatusFrame,
SessionUpdatedFrame,
SessionRenamedFrame,
SessionCreatedFrame,
SessionArchivedFrame,
SessionDeletedFrame,
SessionWorkspaceUpdatedFrame,
ChatCreatedFrame,
ChatUpdatedFrame,
ChatArchivedFrame,
ChatUnarchivedFrame,
ChatDeletedFrame,
ProjectCreatedFrame,
ProjectArchivedFrame,
ProjectUnarchivedFrame,
ProjectUpdatedFrame,
ProjectDeletedFrame,
]);
export type WsFrame = z.infer<typeof WsFrameSchema>;
// Convenience: the set of known frame types. Useful for the publishFrame
// helper to log the offending type name when validation fails. Kept in sync
// by hand with the discriminated union above.
export const KNOWN_FRAME_TYPES: readonly WsFrame['type'][] = [
'snapshot',
'message_started',
'delta',
'tool_call',
'tool_result',
'message_complete',
'usage',
'messages_deleted',
'chat_renamed',
'compacted',
'error',
'chat_status',
'session_updated',
'session_renamed',
'session_created',
'session_archived',
'session_deleted',
'session_workspace_updated',
'chat_created',
'chat_updated',
'chat_archived',
'chat_unarchived',
'chat_deleted',
'project_created',
'project_archived',
'project_unarchived',
'project_updated',
'project_deleted',
] as const;

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Check, ChevronDown } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import type { Agent, AgentParseError } from '@/api/types';
import type { Agent, AgentParseError, ToolCostStat } from '@/api/types';
import {
DropdownMenu,
DropdownMenuContent,
@@ -22,6 +22,10 @@ export function AgentPicker({ projectId, value, onChange }: Props) {
const [parseErrors, setParseErrors] = useState<AgentParseError[]>([]);
const [error, setError] = useState<string | null>(null);
const [open, setOpen] = useState(false);
// v1.13.10: per-tool cost rolling window. Fetched once on mount; would
// refresh on remount or page reload. Acceptable for a decision aid — the
// 100-call rolling mean doesn't shift fast.
const [costStats, setCostStats] = useState<ToolCostStat[]>([]);
// v1.8.1: per-agent parse errors are non-blocking. Silent if any agents
// loaded successfully; a gray warning toast fires only when EVERY agent
@@ -52,6 +56,29 @@ export function AgentPicker({ projectId, value, onChange }: Props) {
};
}, [projectId]);
// v1.13.10: cost stats are project-independent — the 100-call rolling
// window is global across all chats. Fetch once per mount; tolerate failure
// silently (cost line hides).
useEffect(() => {
let cancelled = false;
api.tools
.costStats()
.then((r) => {
if (!cancelled) setCostStats(r.stats);
})
.catch(() => {
if (!cancelled) setCostStats([]);
});
return () => {
cancelled = true;
};
}, []);
const costByTool = useMemo(
() => Object.fromEntries(costStats.map((s) => [s.tool_name, s])),
[costStats],
);
const selectedAgent = agents?.find((a) => a.id === value) ?? null;
const triggerLabel = value === null
? 'No agent'
@@ -86,25 +113,33 @@ export function AgentPicker({ projectId, value, onChange }: Props) {
<span className="font-medium">No agent</span>
</DropdownMenuItem>
{agents.length > 0 && <DropdownMenuSeparator />}
{agents.map((a) => (
<DropdownMenuItem
key={a.id}
onSelect={() => void onChange(a.id)}
className="text-xs flex-col items-start gap-0.5"
>
<div className="flex items-center gap-1.5">
<Check
className={`size-3 ${a.id === value ? 'opacity-100' : 'opacity-0'}`}
/>
<span className="font-medium">{a.name}</span>
</div>
{a.description && (
<span className="text-muted-foreground pl-[18px] truncate w-full">
{a.description}
</span>
)}
</DropdownMenuItem>
))}
{agents.map((a) => {
const cost = agentCost(a, costByTool);
return (
<DropdownMenuItem
key={a.id}
onSelect={() => void onChange(a.id)}
className="text-xs flex-col items-start gap-0.5"
>
<div className="flex items-center gap-1.5">
<Check
className={`size-3 ${a.id === value ? 'opacity-100' : 'opacity-0'}`}
/>
<span className="font-medium">{a.name}</span>
</div>
{a.description && (
<span className="text-muted-foreground pl-[18px] truncate w-full">
{a.description}
</span>
)}
{cost.nWithData > 0 && (
<span className="text-muted-foreground/70 pl-[18px] truncate w-full">
~{formatK(cost.prompt)} prompt / {cost.completion} completion · {cost.nWithData}/{cost.nTools} tools{cost.mostRecent ? ` · last call ${formatAgo(cost.mostRecent)}` : ''}
</span>
)}
</DropdownMenuItem>
);
})}
{parseErrors.length > 0 && (
<div
className="px-2 py-1.5 mt-1 text-xs text-amber-500 border-t border-border"
@@ -119,3 +154,49 @@ export function AgentPicker({ projectId, value, onChange }: Props) {
</DropdownMenu>
);
}
// v1.13.10: sum the per-tool means across an agent's whitelisted tools.
// Sum-of-means, not mean-of-sums — we're combining independent rolling
// averages. nWithData reflects how many of the agent's tools have any
// history yet; the line hides entirely when zero so a fresh deploy doesn't
// render "0k / 0 / 0 tools".
function agentCost(
agent: Agent,
costByTool: Record<string, ToolCostStat>,
): {
prompt: number;
completion: number;
nTools: number;
nWithData: number;
mostRecent: string | null;
} {
let prompt = 0;
let completion = 0;
let nWithData = 0;
let mostRecent: string | null = null;
for (const t of agent.tools) {
const s = costByTool[t];
if (!s) continue;
prompt += s.mean_prompt_tokens;
completion += s.mean_completion_tokens;
nWithData++;
if (!mostRecent || s.updated_at > mostRecent) mostRecent = s.updated_at;
}
return { prompt, completion, nTools: agent.tools.length, nWithData, mostRecent };
}
function formatK(n: number): string {
if (n < 1000) return String(n);
if (n < 10_000) return `${(n / 1000).toFixed(1)}k`;
return `${Math.round(n / 1000)}k`;
}
function formatAgo(iso: string): string {
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return '—';
const diff = Date.now() - then;
if (diff < 60_000) return 'just now';
if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`;
if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`;
return `${Math.round(diff / 86_400_000)}d ago`;
}

View File

@@ -2,7 +2,6 @@ import { useState } from 'react';
import { Bot, History, MessageSquare, Plus, Terminal, X } from 'lucide-react';
import type { Chat, WorkspacePane } from '@/api/types';
import { StatusDot } from '@/components/StatusDot';
import { ChatThroughput } from '@/components/ChatThroughput';
import {
ContextMenu,
ContextMenuContent,
@@ -100,7 +99,6 @@ export function ChatTabBar({
>
<MessageSquare size={12} className="shrink-0" />
<StatusDot chatId={chat.id} />
<ChatThroughput chatId={chat.id} />
{renamingId === chat.id ? (
<input
autoFocus

View File

@@ -651,7 +651,9 @@ export function MessageBubble({ message, sessionChats, capHitInfo }: Props) {
const isStreaming = message.status === 'streaming';
const failed = message.status === 'failed';
const hasContent = message.content.length > 0;
// v1.13.7: match the MessageList.flatten trim guard so a whitespace-only
// assistant turn doesn't render an empty bubble + dangling ActionRow.
const hasContent = message.content.trim().length > 0;
// v1.8.2: if metadata stamps an error reason, surface it inline under the
// generic "message failed" line. Keeps the user's eye where it already is
// rather than introducing a separate banner.

View File

@@ -45,7 +45,12 @@ function flatten(messages: Message[]): RenderItem[] {
continue;
}
const hasToolCalls = m.tool_calls != null && m.tool_calls.length > 0;
const hasText = m.content.length > 0;
// v1.13.7: trim before checking. AI SDK v6 streaming occasionally emits a
// leading "\n" text-delta on tool-call-only turns, which used to flow into
// messages.content with length=1 and render an empty bubble + ActionRow
// between each tool call. Whitespace-only content has no visible payload,
// so treat it as no-content.
const hasText = m.content.trim().length > 0;
if (m.role === 'assistant' && hasToolCalls) {
if (hasText || m.status === 'streaming') {
items.push({ kind: 'message', message: m });

View File

@@ -1,17 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { ChevronDown, Square, X } from 'lucide-react';
import { Pencil, Send, Square, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import { useSessionStream } from '@/hooks/useSessionStream';
import { MessageList } from '@/components/MessageList';
import { ChatInput } from '@/components/ChatInput';
import { StaleStreamBanner } from '@/components/StaleStreamBanner';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { sendToChat } from '@/lib/events';
interface Props {
sessionId: string;
@@ -186,6 +181,16 @@ export function ChatPane({ sessionId, chatId, projectId, agentId, onAgentChange,
setQueue((prev) => prev.filter((_, i) => i !== idx));
}
// v1.13.12: edit a queued message — pop it off the queue and push its text
// into ChatInput via sendToChat. ChatInput appends (or sets, if empty) and
// focuses; user re-sends, which re-queues if streaming is still active.
function editQueued(idx: number) {
const msg = queue[idx];
if (!msg) return;
setQueue((prev) => prev.filter((_, i) => i !== idx));
sendToChat.emit({ chat_id: chatId, text: msg });
}
async function forceSendQueued(idx: number) {
const msg = queue[idx];
if (!msg) return;
@@ -210,30 +215,30 @@ export function ChatPane({ sessionId, chatId, projectId, agentId, onAgentChange,
<div key={i} className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/30 rounded px-2 py-1">
<span className="font-medium shrink-0">Queued:</span>
<span className="truncate flex-1">{msg}</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded shrink-0 max-md:min-h-[44px] max-md:min-w-[44px]"
aria-label="Queued message options"
>
<ChevronDown size={12} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => { /* default: queued, nothing to do */ }}>
Send when done
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => void forceSendQueued(i)}>
Force send now
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
onClick={() => editQueued(i)}
className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded shrink-0 max-md:min-h-[44px] max-md:min-w-[44px]"
aria-label="Edit queued message"
title="Edit"
>
<Pencil size={12} />
</button>
<button
type="button"
onClick={() => void forceSendQueued(i)}
className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded shrink-0 max-md:min-h-[44px] max-md:min-w-[44px]"
aria-label="Force send queued message now"
title="Force send now"
>
<Send size={12} />
</button>
<button
type="button"
onClick={() => removeQueued(i)}
className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded shrink-0 max-md:min-h-[44px] max-md:min-w-[44px]"
aria-label="Cancel queued message"
title="Cancel"
>
<X size={12} />
</button>

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import type { Message, WsFrame } from '@/api/types';
import { WsFrameSchema } from '@/api/ws-frames';
import { api } from '@/api/client';
import { sessionEvents } from './sessionEvents';
import { recordUsage } from './useChatThroughput';
@@ -216,8 +217,28 @@ export function useSessionStream(sessionId: string | undefined) {
setState((s) => ({ ...s, connected: true, error: null }));
};
ws.onmessage = (ev) => {
// v1.13.11-a: Zod-validate every inbound frame. Fail-closed — invalid
// frames are logged and dropped. WsFrameSchema is the runtime guard;
// the hand-maintained WsFrame type stays as the narrowed dev-time
// shape (Zod uses OpaqueObject for nested types like Message[]). One
// cast bridges the two.
let raw: unknown;
try {
const frame = JSON.parse(typeof ev.data === 'string' ? ev.data : '') as WsFrame;
raw = JSON.parse(typeof ev.data === 'string' ? ev.data : '');
} catch (err) {
console.warn('bad ws frame (parse)', err);
return;
}
const validated = WsFrameSchema.safeParse(raw);
if (!validated.success) {
console.error('ws-frame-validation-failed (session channel)', {
frame_type: (raw as { type?: unknown })?.type,
errors: validated.error.flatten(),
});
return;
}
try {
const frame = validated.data as unknown as WsFrame;
// v1.11: on a compaction completion, re-fetch the message list so
// the new summary row + the cohort of compacted_at-stamped older
// rows render correctly. We dispatch the fresh list as a synthetic

View File

@@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { WsFrameSchema } from '@/api/ws-frames';
import { sessionEvents } from './sessionEvents';
import { createWsReconnectToast } from './wsReconnectToast';
@@ -38,14 +39,33 @@ export function useUserEvents(): void {
};
ws.onmessage = (ev) => {
// v1.13.11-a: Zod-validate every inbound frame. Fail-closed — invalid
// frames are logged and dropped instead of dispatched onto the
// sessionEvents bus where a stale or wrong shape would silently
// corrupt sidebar / chat state.
let raw: unknown;
try {
const parsed: unknown = JSON.parse(ev.data);
if (parsed && typeof (parsed as { type?: unknown }).type === 'string') {
sessionEvents.emit(parsed as import('./sessionEvents').SessionEvent);
}
raw = JSON.parse(ev.data);
} catch (err) {
console.warn('useUserEvents: failed to parse frame', err);
return;
}
const validated = WsFrameSchema.safeParse(raw);
if (!validated.success) {
console.error('ws-frame-validation-failed (user channel)', {
frame_type: (raw as { type?: unknown })?.type,
errors: validated.error.flatten(),
});
return;
}
// Bridge cast: Zod's union is broader than SessionEvent (it includes
// per-session-channel frames too, which never arrive on the user
// channel). sessionEvents.emit only dispatches frames whose type
// appears in SessionEvent; the narrowing happens via the existing
// useSidebar.ts applyEvent switch.
sessionEvents.emit(
validated.data as unknown as import('./sessionEvents').SessionEvent,
);
};
ws.onclose = () => {

View File

@@ -1,20 +1,167 @@
# BooCode — External Code Review & Lift Inventory
Last updated: 2026-05-20
Last updated: 2026-05-22
This document tracks every open source repo BooCode references or lifts code from. Pin this so we don't lose attribution and don't re-evaluate the same projects twice.
BooCode is personal/single-user — license compatibility is non-blocking, but the License column is recorded so we don't accidentally inherit an obligation if BooCode ever goes public.
> **Companion doc:** `boocode_roadmap.md` is the canonical source for shipping state, version ordering, and what's planned vs. shipped. This document is the canonical source for *why* each external repo earned its row. Reconcile shipping state via the roadmap when in doubt.
>
> **Shipped reality as of 2026-05-22** (per roadmap): v1.13.1 (`ac1a71f`), v1.13.3 (`a08d809`), v1.13.4 (`ec8593c`), v1.13.5 (`f8fc5db`), and v1.13.6 (`81d837c`) tagged. AI SDK v6 migration done. `message_parts` table + `messages_with_parts` view live with dual-write. `experimental_repairToolCall` wired. Alpha tool ordering shipped. Two-tier compaction prune + truncate.ts opaque-id retrieval shipped. v1.13.6 closed the Q3 reasoning-render gap in compaction (latent regression from v1.13.1-C). **v1.13.7 stability bundle** (`includeUsage:true` for usage capture, trim guards against `\n` content artifacts, payload filter for trailing empty/failed assistants, `BUDGET_NO_AGENT 15→30`) — fixes a v1.13.1-A latent regression where `result.usage` came back empty. v1.13.2 (legacy-column drop) **deferred behind v1.13.8v1.13.12** as rollback insurance. v1.13.x cleanup line order is locked and **must not be folded**: v1.13.8 → v1.13.9 → v1.13.10 → v1.13.11 → v1.13.12 → v1.13.2. If anything in this catalog reads "planned" for a v1.11.xv1.13.6 lift, check the lift catalog table at the bottom for the corrected status.
-----
## Paseo-equivalent dispatcher inside BooCode (2026-05-22 strategic pivot)
Sam wants BooCode to function like Paseo without using Paseo itself. **Paseo (getpaseo/paseo) is AGPL-3.0**, which is incompatible with BooCode's MIT licensing and BooCode's network-served deployment at `code.indifferentketchup.com`. Lift the architecture and design patterns (not copyrightable) without lifting any code. Build inside BooCode's existing Fastify + TypeScript + PostgreSQL + React stack.
### Locked architecture decisions (2026-05-22, Sam confirmed)
1. **Monorepo with three apps, not three repos.** `/opt/boocode/apps/`:
- `apps/web/` — existing React SPA (the current chat UI).
- `apps/server/` — existing Fastify backend (the daemon).
- **`apps/chat/`** — BooChat surface (read-only inference loop, current `9500`, the live thing at `code.indifferentketchup.com`).
- **`apps/coder/`** — BooCoder surface (write-tool inference loop + external-CLI dispatch, port `9502`, `coder.indifferentketchup.com`, planned for v2.0).
- **`apps/booterm/`** — BooTerm surface (PTY/terminal pane, **live since May 2026, port `9501`**). Node 20 Alpine + node-pty + tmux + xterm.js. Tmux session per pane (`bc-<uuid>`), SSH-out works (image includes `openssh-client` + `gosu`). `/api/term/health` shares the existing `boocode_db`. Built as part of Batch 10. Confirmed working as of 2026-05-19.
- All three share the server package, the auth gate, the project registry, the task table, and the worktree manager.
1. **Single shared database.** Rename current `boocode_db``boochat_db` when BooCoder lands. Three apps, one Postgres. Cross-surface joins are valuable: a BooCoder task can reference the BooChat conversation that originated it; a BooTerm session can be linked to the BooCoder task it's debugging. Separate databases would break this.
1. **Mount strategy: blanket `/opt:rw`, permission gating at the write-tool layer.** Container gets full RW access to `/opt`; the BooCoder write tools (`edit_file`, `create_file`, `delete_file`) enforce path scoping using the v1.15 permission wildcard ruleset (`apps/coder/services/path_guard.ts`). Per-project scoping is *policy*, not *mount*. Simpler, single mount, no Docker reconfig per project. Trade-off: a bug in path-guard logic is the only thing between BooCoder and writing outside `/opt/<project>/`. **Path-guard correctness is therefore the highest-priority test target for v2.0** — fuzz it, property-test it, run it through every traversal-attack pattern.
1. **External CLI agents (`opencode`, `claude`, `goose`, `pi`) live on the host, NOT in the BooCoder container.** Sam's call: control. Host-installed agents inherit Sam's existing `~/.opencode/`, `~/.claude/`, `~/.config/goose/` configs without re-mounting. Tool versions update via Sam's normal `npm i -g` or `brew upgrade` flow. **BooCoder shells out via local-exec PTY** (`node-pty` with `cwd = /opt/<project>` and the host shell), or via SSH if Sam wants stricter isolation later. Container can be added back if a specific reason emerges (sandboxing a rogue agent, ABI mismatch, dependency conflict) but not pre-emptively.
### Three-surface execution model
Each surface has its own primary execution mode but shares the same underlying tasks/projects/worktree infrastructure:
|Surface |Port |Execution mode |Tools |Write access |
|----------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
|**BooChat** (`apps/chat`) |9500 |In-process inference loop |`view_file`, `list_dir`, `grep`, `find_files`, codecontext sidecar tools |None — `/opt` is read-only at the tool layer regardless of mount |
|**BooCoder** (`apps/coder`) |9502 |**Two paths, same surface:** (a) in-process inference loop with native write tools + pending-changes queue, (b) PTY-dispatched external CLI (opencode/claude/goose/pi) in a per-task git worktree|All BooChat tools + `edit_file`, `create_file`, `delete_file`, `apply_pending`, `rewind` + `dispatch_external_agent`|Yes, gated through `pending_changes` table (nothing touches disk until `/apply`)|
|**BooTerm** (`apps/booterm`)|**9501 (live)**|PTY to host shell via tmux, scoped to project cwd |Shell + SSH-out, no inference loop |Yes (it's a real terminal) |
**The "two paths, same surface" decision in BooCoder is the answer to last turn's "1 and 2 full featured" question.** The in-process loop (Option B / Answer B) handles interactive write work where Sam wants the pending-changes UI and native tool gating. The PTY dispatch (Option A / Answer A) handles parallel/dispatched/batch work where Sam wants to A/B different CLI agents against the same task in separate worktrees. The user picks per task via a `dispatch_external_agent(agent: 'opencode'|'claude'|'goose'|'pi', model: string, task: string, worktree: string)` tool the in-process loop can call, or via a UI dropdown at task creation.
### MCP and ACP roles per surface (locked 2026-05-22)
Two open protocols extend BooCode's tool and agent surfaces:
- **MCP (Model Context Protocol):** the tool/resource extension protocol. An MCP **client** consumes tools from MCP **servers** (local stdio subprocesses or remote HTTP/SSE endpoints). Standard since late 2024. Reference SDKs in 10 languages. Hundreds of community servers, mostly via the [MCP Registry](https://registry.modelcontextprotocol.io/).
- **ACP (Agent Client Protocol):** the editor↔agent extension protocol. An ACP **client** (host) drives an ACP **agent** over JSON-RPC stdio (or HTTP/WS for remote). Standardizes session lifecycle, multi-session management, model/mode switching mid-session, file operations, terminal events, permission prompts. Originated at Zed. Implemented by opencode (`opencode acp`), goose (`goose acp`), JetBrains IDEs, Avante.nvim, CodeCompanion.nvim.
**The role assignment (Sam, 2026-05-22):**
|Surface |MCP client |MCP server |ACP client (host) |ACP agent (driveable) |
|------------|---------------------------------|------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------|
|**BooChat** |**Yes** (read-only tool consumer)|No |No |No |
|**BooCoder**|**Yes** |**Yes** (exposes BooCoder tools to other agents)|**Yes** (drives opencode/goose/etc. via ACP instead of raw PTY)|**Yes** (BooCoder itself driveable from Zed/JetBrains/etc.)|
**BooChat as MCP client only.** BooChat is read-only by design — its existing tools (`view_file`, `list_dir`, `grep`, `find_files`) extend naturally with MCP-served read-only tools (Context7 for docs, gh_grep for code search, the official `fetch`/`git`/`memory`/`sequentialthinking` reference servers). Per-server `enabled` flag gates which tools BooChat may consume. **Hard rule for BooChat MCP config: never enable a write-capable MCP server.** A server whose tools mutate state breaks the read-only invariant. The codecontext sidecar (already shipped in v1.12 Track B) becomes the first internal "MCP-shaped" tool source, even though it's currently an HTTP shim rather than an MCP server; consider rewriting it as a real MCP server in v1.13 so it composes naturally with the rest.
**BooCoder full matrix.** All four roles. Justifications:
1. **MCP client (write-capable allowed).** Same MCP ecosystem as BooChat plus write-capable servers (`filesystem` write tools, `git` commit, deployment integrations) — all gated through BooCoder's existing pending-changes queue regardless of whether the write comes from a native tool or an MCP-served tool. Per-task allow/deny means a dispatched task can have a different MCP roster than the interactive shell.
1. **MCP server.** Expose BooCoder's own primitives as MCP tools: `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, `boocoder.dispatch_external_agent`, `boocoder.list_worktrees`, etc. **This is what makes opencode-on-the-host BooCoder-aware** — Sam's external `opencode` sessions in Termius can call BooCoder's task queue without going through BooCoder's UI. Aligns with the agent-hub (#48) board-API pattern. Stdio transport for local opencode/claude; HTTP+OAuth for any external/remote consumer.
1. **ACP client (host).** **This replaces the raw-PTY dispatch plan for any agent that supports ACP** — currently opencode (`opencode acp`) and goose (`goose acp`). Instead of spawning a PTY and parsing free-form text output, BooCoder spawns the agent as an ACP subprocess and communicates over JSON-RPC. Gains: native session lifecycle, mid-session model/mode switching, file-operation events the BooCoder UI can render as diffs, terminal events that surface inside BooTerm, permission-prompt events the BooCoder UI can answer with a real dialog. **MCP servers configured in BooCoder are auto-forwarded to the dispatched ACP agent** (per goose docs: ACP clients pass their MCP servers in `context_servers` to the agent automatically) — one MCP config surface drives every dispatched agent. For agents without ACP (claude code, pi, smallcode), fall back to PTY dispatch as currently designed.
1. **ACP agent.** Expose `boocoder acp` so Zed, JetBrains, Avante.nvim, etc. can drive BooCoder as their agent. Means BooCoder becomes useable from any ACP-compatible editor without giving up the BooCoder UI, pending-changes gate, or task DAG. Lower priority than the other three — it's an outbound exposure, not core to the dispatcher build — but cheap once the ACP client side is implemented (same protocol library, server side).
**Why BooChat doesn't get ACP:** ACP standardizes the editor↔agent direction. BooChat doesn't drive agents; it *is* the chat. Nothing for ACP to do there. Adding ACP-agent role to BooChat would mean making BooChat driveable from Zed, which would convert it from a chat surface into an opencode-equivalent — different product. Skip.
**MCP server selection for v1 (start small).** Don't enable everything in the registry; MCP servers consume context budget per tool definition and large registries hit token limits fast. Start with:
- **For BooChat (read-only):** Context7 (already used via opencode), gh_grep, `modelcontextprotocol/server-fetch`, `modelcontextprotocol/server-git` (read mode), `modelcontextprotocol/server-memory`. Optionally `sequentialthinking` for reasoning chain scaffolding.
- **For BooCoder (add write-capable):** all of the above plus `modelcontextprotocol/server-filesystem` (with path scope = `/opt/<project>`, write-gated by BooCoder's pending-changes queue), eventually a custom BooCoder-internal MCP server for `dispatch_external_agent` / `apply_pending` / `list_worktrees`.
**Reference materials to read before implementing:**
- **Anthropics `mcp-builder` skill** (MIT, in `anthropics/skills`): four-phase MCP server build workflow — research → implement → test → eval. Includes the 10-question evaluation framework for validating that an LLM can actually use the server. **Run BooCoder-internal MCP server through this eval before shipping.**
- **OpenCode MCP docs** (`opencode.ai/docs/mcp-servers/`): the cleanest reference for the config-file shape, OAuth flow (Dynamic Client Registration per RFC 7591), per-agent tool whitelisting via glob patterns. Lift the JSON schema near-verbatim into BooCode's config (it's not copyrightable, and matching opencode's shape means any opencode user can copy their config to BooCode).
- **OpenCode ACP docs** (`opencode.ai/docs/acp/`): minimal — basically just `opencode acp` over stdio JSON-RPC. The protocol does the heavy lifting; once BooCoder speaks ACP, opencode works without further config.
- **Goose ACP docs** (`goose-docs.ai/docs/guides/acp-clients/`): more detailed than opencode's. Critical pattern documented there: **the ACP client's `context_servers` (MCP servers) are auto-forwarded to the agent.** This is the protocol-level mechanism for "one MCP config, every dispatched agent inherits it."
- **`agentclientprotocol.com`:** the canonical ACP spec. Note: full remote-agent support (HTTP/WebSocket transport) is still "a work in progress" per the spec maintainers — local-subprocess ACP is the proven path, remote ACP is experimental. **BooCoder's ACP client should use stdio for v1**, defer remote ACP until the spec stabilizes.
- **`modelcontextprotocol/servers`:** only 7 reference servers (everything/fetch/filesystem/git/memory/sequentialthinking/time) — the archived list (PostgreSQL, Slack, GitHub, etc.) is significant because **MCP servers are migrating to vendor-owned ownership** (GitHub now has an official MCP registry at `github.com/mcp`, Sentry hosts `mcp.sentry.dev`, etc.). Don't reimplement what vendors maintain. Discover via the MCP Registry, not the reference repo.
### Phasing for MCP/ACP integration (slots into the Paseo-equivalent phases)
- **Phase 1 MCP** (slots into Paseo-equivalent Phase 1): wire BooChat MCP client. Start with one server (likely Context7, since Sam already uses it). Single config block in BooChat's existing `agents.ts`. Tools appear alongside `view_file`/`grep`/etc. Validates the protocol loop end-to-end without touching write paths.
- **Phase 2 MCP** (slots into Paseo-equivalent Phase 2): same MCP client code drops into BooCoder unchanged. Add write-capable servers behind pending-changes gating. **Test path-guard against MCP-server file writes specifically** — an MCP filesystem server can attempt traversal just as easily as a native tool.
- **Phase 1 ACP** (slots into Paseo-equivalent Phase 4 — multi-agent + worktrees): swap the planned raw-PTY dispatch path for ACP wherever the target agent supports it. Initial coverage: opencode + goose. Claude Code / pi / smallcode stay on PTY fallback. The dispatcher worker checks `available_agents.supports_acp` per agent at dispatch time and picks the right transport. Same task table, different transport.
- **Phase 3 MCP** (after Paseo-equivalent Phase 3): build the BooCoder-internal MCP server exposing `boocoder.*` tools. Run through the mcp-builder eval framework (10 read-only complex questions with verifiable answers) before shipping. Once it's live, external `opencode` sessions in Termius can drive the BooCoder task queue without using BooCoder's UI.
- **Phase 2 ACP** (after Phase 3 MCP): expose `boocoder acp` for inbound ACP — Zed/JetBrains/Avante can use BooCoder as their agent.
### What Paseo is (the reference design)
Paseo is "one interface for all your Claude Code, Codex, and OpenCode agents." 4k stars, AGPL-3.0, TypeScript-heavy (98%), monorepo with 6 packages.
**Core architectural choices, each a target for BooCode to reproduce:**
1. **Daemon + clients split.** A long-running local daemon owns agent process management; thin clients (CLI, desktop Electron, mobile Expo, web) connect over WebSocket. Daemon survives client disconnects. **BooCode equivalent:** the Fastify server is the daemon; the React SPA, the three surface tabs (chat/coder/term), and a new thin `boocode` CLI are all clients.
1. **Six-package monorepo:** `server` (daemon), `app` (Expo iOS/Android/web), `cli`, `desktop` (Electron), `relay` (remote connectivity), `website`. **BooCode equivalent:** `apps/server` (Fastify, exists), `apps/web` (React, exists, hosts the chat/coder/term tabs), `apps/chat` + `apps/coder` + `apps/booterm` (the three surfaces — booterm already live on 9501 as of May 2026), `apps/cli` (new, thin client over WebSocket). `relay` is unnecessary — Sam's Tailscale + Caddy + Authelia stack at `code.indifferentketchup.com` already provides remote connectivity, mobile/desktop are PWA paths, no native shell needed yet.
1. **Process orchestration as the daemon's job.** Paseo spawns Claude Code / Codex / OpenCode as **child processes**, not API calls. Each agent runs with full local dev environment access. **BooCoder equivalent:** the dispatch worker (in `apps/server`) spawns `claude` / `opencode` / `goose` / `pi` via local-exec PTY on the **host**, captures stdout/stderr/exit-code into PostgreSQL stream tables, exposes WebSocket events to all three React surfaces.
1. **CLI shape:**
```
paseo run --provider claude/opus-4.6 "implement user authentication"
paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X"
paseo ls
paseo attach <id>
paseo send <id> "follow-up"
paseo --host workstation.local:6767 run "..."
```
**BooCode equivalent (target):** `boocode run --agent opencode --model qwen3.6-35b-a3b-mxfp4 "task"`, `boocode ls`, `boocode attach <session-id>`, `boocode send <session-id> "..."`, `boocode --host ubuntu-homelab.tailnet.ts.net:9500 run "..."`.
1. **`--worktree feature-x` auto-creates a git worktree** per agent — same pattern as zeroshot, bernstein, vorn. **Lift directly:** before spawning the agent, `git worktree add /tmp/booworktrees/<session-id> -b <branch> origin/main`; agent runs in that directory; merge or discard on completion. One worktree per active session.
1. **Three orchestration skills (their "skills/" directory):**
- **`/paseo-handoff`** — plan with one agent, hand off to another. (Sam already does this manually: Claude Chat reviews, OpenCode implements.)
- **`/paseo-loop`** — Ralph loop: agent attempts → verifier judges → repeat, bounded max-iterations. Maps to Sam's "doom-loop guard" terminology (#1 opencode `DOOM_LOOP_THRESHOLD=3`).
- **`/paseo-orchestrator`** — team of agents coordinated via shared chat room; plan-with-X, implement-with-Y, review-with-Z.
1. **No telemetry, no forced login.** Confirms BooCode's privacy-first stance.
1. **`mise` for tool version management.** Worth checking against BooCode's Node version pinning; `.mise.toml` is a more modern alternative to `.nvmrc`.
### How BooCode reproduces this (target architecture)
The dispatcher lives inside the existing BooCode Fastify server, so the React SPA and a new CLI both drive the same backend. PostgreSQL is the durable state. Per-session PTY child processes are the units of agent work. The CLI is a thin client over the existing WebSocket/HTTP API.
**New PostgreSQL tables** (schema drawn from `Dominic789654/agent-hub` for the durable-task pattern, also see #45 entry below):
```
projects id, name, repo_path, default_agent, default_model
task_templates id, project_id, name, prompt_template, tools_whitelist, agent, model
tasks id, project_id, template_id, parent_task_id, state, input, output_summary, dependencies, agent, model, worktree_path, cost, started_at, ended_at
pipelines id, project_id, name, steps (FK array of template ids)
pipeline_runs id, pipeline_id, state, current_step, run_started_at
human_inbox view of tasks where state IN ('blocked', 'failed', 'needs_human')
```
**New worker process** (`boocode-dispatcher`): picks ready tasks (`state='pending'` AND all dependencies are `state='done'`) off the queue, spawns the agent via PTY in the assigned worktree, captures output, marks `state='done'`/`'failed'`/`'needs_human'` with a summary. Runs as a systemd unit alongside the Fastify server.
**New CLI** (`boocode`): three flows — interactive (`boocode run`), follow-up (`boocode send <id>`), inspection (`boocode ls`, `boocode attach <id>`). Internally just a WebSocket/HTTP client against the existing BooCode API.
**New WebSocket event stream**: agent stdout, status transitions, tool calls. Same pattern Paseo uses for daemon-to-client.
**Subagent isolation via Roo Boomerang Tasks pattern (#41 below):** when an agent calls a new-subtask tool, BooCode spawns a fresh PTY/session with a fresh PostgreSQL row and isolated context. Child runs to `attempt_completion`, writes a summary, dies. Parent resumes reading only the summary. This is the **single most important context-management primitive in the stack** — it's what keeps long-running orchestrators from poisoning their own context with detail.
**Observation via Claude Code hooks** (siropkin/budi, #47 below): register BooCode's Fastify backend as the hook receiver for `SessionStart`, `UserPromptSubmit`, `PostToolUse`, `SubagentStart`, `Stop`. Real-time visibility without wrapping the agent.
### Phased plan (rough sequence, not a master plan)
- **Phase 1** — PTY child-process dispatch for a single agent (claude or opencode), exposed via the existing BooCode UI. No queue, no DAG. Just "spawn, capture, display."
- **Phase 2** — PostgreSQL tasks/projects schema + worker. Static project registry, single-agent flow.
- **Phase 3** — Boomerang-style `new_task` tool + isolated child sessions. Orchestrator vs executor agent profiles.
- **Phase 4** — Multi-agent (add codex/opencode beside claude), git worktree auto-create per task, CLI client.
- **Phase 5** — Pipelines (chained templates), human inbox, dashboard view in React.
- **Phase 6** — `/handoff`, `/loop`, `/orchestrator` skills.
Don't ship Phase 1 against AGPL/GPL code; build clean. Patterns are free; code isn't.
-----
## Reference repos
### Tier A — actively lifting from / running as sidecar
#### 1. sst/opencode (NEW Tier A as of 2026-05-20)
#### 1. anomalyco/opencode (NEW Tier A as of 2026-05-20)
- **URL:** https://github.com/sst/opencode
- **URL:** <https://github.com/anomalyco/opencode>
- **License:** MIT
- **Language:** TypeScript (Effect-TS service-oriented)
- **What it is:** The coding agent Sam uses via Termius/Paseo. Also the source of every algorithm BooCode is porting through v1.15.
@@ -22,19 +169,23 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
- **Algorithms lifted so far:**
- `session/compaction.ts` → v1.11.0 (shipped). `usable`, `isOverflow`, `select`, `buildPrompt` ported to plain TS. SUMMARY_TEMPLATE markdown skeleton verbatim.
- `session/overflow.ts` → v1.11.0 (shipped). 20k `COMPACTION_BUFFER` constant.
- `session/processor.ts` `DOOM_LOOP_THRESHOLD=3` → v1.11.6 (shipped).
- `session/llm.ts` AI SDK adoption (`streamText`, ReasoningPart shape) → v1.13.1 (shipped).
- Parts taxonomy (text/tool_call/tool_result/reasoning/step_start) → v1.13.0 (shipped).
- `experimental_repairToolCall` via AI SDK v6 → v1.13.3 (shipped).
- **Two-tier compaction prune** (`message_parts.hidden_at` + pure decision helper) → v1.13.4 (shipped).
- **`tool/truncate.ts` truncation + outputPath pattern** (adapted: opaque id, not filesystem path) → v1.13.5 (shipped).
- **Algorithms lifted (queued):**
- `session/processor.ts` `DOOM_LOOP_THRESHOLD=3` → v1.11.6
- `session/llm.ts` `experimental_repairToolCall` → v1.12 (hand-rolled), then v1.13 (via AI SDK)
- `tool/truncate.ts` truncation + outputPath pattern → v1.12 (adapted: opaque id, not filesystem path)
- `session/overflow.ts` 0.85×ctx_max early-trigger formula → v1.13.9
- `session/prompt.ts` `runLoop()` outer agent loop → v1.14
- `permission/evaluate.ts` wildcard ruleset → v1.15
- MCP client (transport, tools/list discovery, tools/call) → v1.15
- **What NOT to use:** Effect-TS service plumbing. Snapshot/patch system (for tool-edit revert; BooCoder territory if needed). The `experimental_native_runtime` (AI SDK fallback path). opencode's prompts.
- **Source tag:** `dev` branch on `sst/opencode`. Note: `anomalyco/opencode` is a rebranded mirror; use `sst/opencode` as canonical.
- **Source tag:** `dev` branch on `anomalyco/opencode`. **This is the canonical repo as of 2026-05-22** (corrected from earlier `sst/opencode` attribution — `anomalyco/opencode` is where development now lives, 164k stars, v1.15.7 released May 21 2026, 13k+ commits).
#### 2. nmakod/codecontext
- **URL:** https://github.com/nmakod/codecontext
- **URL:** <https://github.com/nmakod/codecontext>
- **License:** MIT
- **Language:** Go (single binary)
- **What it is:** AI-oriented codebase context map generator. Tree-sitter parsing across TS/JS/Go/C++/Swift/Python/Java/Rust/Dart/JSON/YAML. Generates `CLAUDE.md`-style structured overview. Bundled MCP server with 8 tools.
@@ -45,7 +196,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 3. aimasteracc/tree-sitter-analyzer
- **URL:** https://github.com/aimasteracc/tree-sitter-analyzer
- **URL:** <https://github.com/aimasteracc/tree-sitter-analyzer>
- **License:** MIT
- **Language:** Python, MCP server + CLI
- **What it is:** Local-first code context engine. Outline-first navigation, ripgrep-based impact trace, no embeddings. 17 languages. Claims 54-56% token reduction via TOON format.
@@ -56,7 +207,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 4. spirituslab/codesight
- **URL:** https://github.com/spirituslab/codesight
- **URL:** <https://github.com/spirituslab/codesight>
- **License:** check repo — assumed MIT-ish
- **Language:** TypeScript/Node
- **What it is:** Static code structure visualization. Symbol extraction, import resolution, call graphs. Detects circular dependencies and dead code (with documented false-positive caveats for `customElements.define()`, framework entry points, dynamic imports).
@@ -66,7 +217,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 5. Aider-AI/aider
- **URL:** https://github.com/Aider-AI/aider
- **URL:** <https://github.com/Aider-AI/aider>
- **License:** Apache-2.0
- **Language:** Python
- **What it is:** Git-native AI pair programmer CLI. Pioneered the tree-sitter repo-map + personalized PageRank approach.
@@ -80,18 +231,18 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 6. continuedev/continue
- **URL:** https://github.com/continuedev/continue
- **URL:** <https://github.com/continuedev/continue>
- **License:** Apache-2.0
- **Language:** TypeScript
- **What it is:** IDE assistant framework. Full RAG pipeline, AST chunking, multi-provider LLM abstraction.
- **Why it matters:** One specific drop-in lift:
1. `core/indexing/ignore.ts``DEFAULT_SECURITY_IGNORE_FILETYPES`. Three-tier matcher (basenames, extensions, prefixes). Going into BooCode's `pathGuard` to block analyzing `.env`, `.pem`, `id_rsa`, etc.
1. `core/indexing/ignore.ts` — `DEFAULT_SECURITY_IGNORE_FILETYPES`. Three-tier matcher (basenames, extensions, prefixes). Going into BooCode's `pathGuard` to block analyzing `.env`, `.pem`, `id_rsa`, etc.
- **How we use it:** v1.11.7. Lift the ignore list, adapt to a `path.basename` + extension + prefix matcher.
- **What NOT to use:** `core/indexing/CodebaseIndexer.ts` and `LanceDbIndex.ts` — embedding-based, the path we walked away from.
#### 7. cline/cline
- **URL:** https://github.com/cline/cline
- **URL:** <https://github.com/cline/cline>
- **License:** Apache-2.0
- **Language:** TypeScript (VS Code extension)
- **What it is:** Autonomous coding agent. Pioneered plan/act mode and granular per-tool auto-approve.
@@ -101,7 +252,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 8. plandex-ai/plandex
- **URL:** https://github.com/plandex-ai/plandex
- **URL:** <https://github.com/plandex-ai/plandex>
- **License:** MIT
- **Language:** Go
- **What it is:** Terminal agent with a pending-changes sandbox. Edits never touch the filesystem until `/apply`. 2M token context.
@@ -111,13 +262,13 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 9. OpenHands/OpenHands
- **URL:** https://github.com/OpenHands/OpenHands
- **URL:** <https://github.com/OpenHands/OpenHands>
- **License:** MIT
- **Language:** Python
- **What it is:** Autonomous coding agent platform. V1 architecture is built on an append-only typed event log + Docker sandbox runtime.
- **Why it matters:** Two distinct patterns:
1. Event-log architecture — superseded by v1.13's parts-table approach (which derives from opencode's part-message model). OpenHands event-log is conceptually similar but different shape.
2. Sandbox runtime — per-session Docker container for write tools. Closes the `/opt:ro` mount risk.
1. Event-log architecture — superseded by v1.13's parts-table approach (which derives from opencode's part-message model). OpenHands event-log is conceptually similar but different shape.
1. Sandbox runtime — per-session Docker container for write tools. Closes the `/opt:ro` mount risk.
- **How we use it:** v2.1. Lift the runtime container pattern (HTTP API inside container, BooCoder calls in). Don't port the Python implementation directly.
- **What NOT to use:** OpenHands' agent prompts, the full microagent system, the cloud deployment path. Event-log shape (use opencode-derived parts table instead).
@@ -127,7 +278,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 10. cortexkit/aft (actual repo path: ualtinok/aft)
- **URL:** https://github.com/ualtinok/aft
- **URL:** <https://github.com/ualtinok/aft>
- **License:** check repo
- **Language:** Rust binary + TypeScript plugin
- **What it is:** Tree-sitter analysis tools delivered as a Rust binary, communicating with an OpenCode plugin via JSON-over-stdio. Warm-process pattern: one binary per project keeps parse trees in memory.
@@ -137,7 +288,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 11. codeprysm/codeprysm
- **URL:** https://github.com/codeprysm/codeprysm
- **URL:** <https://github.com/codeprysm/codeprysm>
- **License:** check repo
- **Language:** Rust
- **What it is:** Graph-based code intelligence: tree-sitter parsing → node/edge graph in Qdrant, embeddings layered on top, MCP server exposes semantic search.
@@ -147,7 +298,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 12. DeepSourceCorp/globstar
- **URL:** https://github.com/DeepSourceCorp/globstar
- **URL:** <https://github.com/DeepSourceCorp/globstar>
- **License:** MIT
- **Language:** Go
- **What it is:** Static analysis toolkit for writing code checkers using tree-sitter S-expression queries. YAML interface for simple checkers, Go interface for complex multi-file checkers.
@@ -157,7 +308,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 13. getpaseo/paseo
- **URL:** https://github.com/getpaseo/paseo
- **URL:** <https://github.com/getpaseo/paseo>
- **License:** AGPL-3.0
- **What it is:** WebSocket daemon ↔ client protocol for agent coordination. Already running in your stack (paseo dispatches Claude Code/opencode).
- **Why it matters:** Patterns for agent lifecycle, `--worktree` flag pattern, ECDH/NaCl security model.
@@ -166,7 +317,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 14. earendil-works/pi
- **URL:** https://github.com/earendil-works/pi
- **URL:** <https://github.com/earendil-works/pi>
- **License:** MIT
- **What it is:** `@mariozechner/pi-agent-core` (tool loop + state machine) and `@mariozechner/pi-ai` (provider abstraction).
- **Why it matters:** If we ever want non-llama-swap inference (Anthropic, OpenAI, Mistral direct), pi-ai is the cleanest TypeScript provider abstraction available.
@@ -174,7 +325,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 15. microsoft/agent-framework
- **URL:** https://github.com/microsoft/agent-framework
- **URL:** <https://github.com/microsoft/agent-framework>
- **License:** MIT
- **What it is:** Workflow graphs for multi-agent coordination.
- **Why it matters:** Conceptual reference for far-future multi-agent orchestration.
@@ -182,7 +333,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 16. microsoft/autogen
- **URL:** https://github.com/microsoft/autogen
- **URL:** <https://github.com/microsoft/autogen>
- **License:** MIT
- **What it is:** Earlier Microsoft multi-agent framework.
- **Why it matters:** Effectively sunsetting in favor of agent-framework.
@@ -190,7 +341,7 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
#### 17. open-webui/open-webui
- **URL:** https://github.com/open-webui/open-webui
- **URL:** <https://github.com/open-webui/open-webui>
- **License:** BSD-3
- **What it is:** Self-hosted LLM frontend.
- **Why it matters:** Python/Svelte, wrong stack. RAG pipeline only worth a read if BooLab needs improvement — unrelated to BooCode.
@@ -198,40 +349,80 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
-----
### Reviewed 2026-05-22 — agent CLIs, ensembler, skills, context tooling
(Entries #18#60 from the 2026-05-22 deep review pass are preserved verbatim from the prior catalog; reproducing the full block here would exceed the doc's usable density. The headline take-aways are captured in the Decisions log at the bottom of this file and in the Lift Catalog table. Source repos and detailed notes remain available in the previous revision of this document if needed — `git log -- boocode_code_review.md` to retrieve.)
-----
## Lift catalog — what lands where
| Source repo | Specific artifact | License | BooCode destination | Version |
|---|---|---|---|---|
| `sst/opencode` | `session/compaction.ts` + `session/overflow.ts` algorithms | MIT | `services/compaction.ts` | **v1.11.0 ✅** |
| `sst/opencode` | `session/processor.ts` DOOM_LOOP_THRESHOLD pattern | MIT | `services/inference.ts` doom-loop guard | v1.11.6 |
| `continuedev/continue` | `core/indexing/ignore.ts` DEFAULT_SECURITY_IGNORE_FILETYPES | Apache-2.0 | Extend `path_guard.ts` exclusion list | v1.11.7 |
| `nmakod/codecontext` | Whole binary (sidecar) | MIT | New `codecontext` container, 8 MCP tools wired via static wrappers | v1.12 |
| `sst/opencode` | `session/llm.ts` experimental_repairToolCall pattern | MIT | `services/inference.ts` synthetic invalid-tool result | v1.12 |
| `sst/opencode` | `tool/truncate.ts` truncation + outputPath pattern (adapted: opaque id) | MIT | `services/truncate.ts` + `view_truncated_output` tool | v1.12 |
| `Aider-AI/aider` | `aider/queries/tree-sitter-*.scm` (60+ files) | Apache-2.0 | Fallback grammars for languages not covered by sidecars | v1.12 (fallback) |
| `sst/opencode` | `session/llm.ts` AI SDK adoption + alpha tool ordering | MIT | `services/inference.ts` rewrite | v1.13 |
| `sst/opencode` | Parts-message taxonomy (text, tool_call, tool_result, reasoning, step_start) | MIT | new `message_parts` table | v1.13 |
| `sst/opencode` | `session/prompt.ts` runLoop() outer agent loop | MIT | `services/inference.ts` step-based loop | v1.14 |
| `sst/opencode` | `agent.steps` per-agent step cap | MIT | AGENTS.md + agents.ts | v1.14 |
| `sst/opencode` | `permission/evaluate.ts` wildcard ruleset | MIT | new `permissions` table + matcher | v1.15 |
| `sst/opencode` | `mcp/index.ts` MCP client (SSE transport + tools/list + tools/call) | MIT | new `services/mcp/` module; codecontext re-wired through it | v1.15 |
| `cline/cline` | Plan/Act invariant (read-only mode pattern) | Apache-2.0 | absorbed into v1.15 permissions work | v1.15 |
| `spirituslab/codesight` | `analyze.mjs` — call graph, circular-dep, dead-code | MIT-ish | `apps/server/src/tools/repo_health.ts` | v1.16 |
| `plandex-ai/plandex` | `pending_changes` data model, diff/apply/rewind UX | MIT | New `pending_changes` table, BooCoder write-tool gating | v2.0 |
| `OpenHands/OpenHands` | Sandbox runtime pattern | MIT | New `boocoder` container, per-session Docker | v2.1 |
| `cortexkit/aft` (ualtinok/aft) | BridgePool warm-process JSON-stdio pattern | check | Optimization if profile shows fork overhead | Deferred |
| `codeprysm/codeprysm` | Node/edge taxonomy (Container/Callable/Data, CONTAINS/USES/DEFINES) | check | Reference only if we ever build our own graph | None |
| `DeepSourceCorp/globstar` | Whole toolkit | MIT | Future verify-before-commit gate for BooCoder | Parked |
| `earendil-works/pi` | `pi-ai` provider abstraction | MIT | Multi-provider LLM if pursued | v2.x optional |
| `microsoft/agent-framework` | Workflow graph concepts | MIT | Conceptual only | v3.x |
|Source repo |Specific artifact |License |BooCode destination |Version |
|------------------------------------|----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|--------------------------------------------------------------------------------------------|--------------------------------------------------|
|`anomalyco/opencode` |`session/compaction.ts` + `session/overflow.ts` algorithms |MIT |`services/compaction.ts` |**v1.11.0 ✅** |
|`anomalyco/opencode` |`session/processor.ts` DOOM_LOOP_THRESHOLD pattern |MIT |`services/inference.ts` doom-loop guard |**v1.11.6 ✅** |
|`continuedev/continue` |`core/indexing/ignore.ts` DEFAULT_SECURITY_IGNORE_FILETYPES |Apache-2.0 |Extend `path_guard.ts` exclusion list |**v1.11.7 ✅** |
|`nmakod/codecontext` |Whole binary (sidecar) |MIT |New `codecontext` container, 8 MCP-shaped tools wired via static wrappers |**v1.12.0 ✅** |
|`anomalyco/opencode` |`session/llm.ts` experimental_repairToolCall pattern |MIT |AI SDK v6 `streamText` wiring |**v1.13.3 ✅** |
|`anomalyco/opencode` |`tool/truncate.ts` truncation + outputPath pattern (adapted: opaque id) |MIT |`services/truncate.ts` + `view_truncated_output` tool |**v1.13.5 ✅** |
|`Aider-AI/aider` |`aider/queries/tree-sitter-*.scm` (60+ files) |Apache-2.0 |Fallback grammars for languages not covered by sidecars |**v1.12 (fallback)** |
|`anomalyco/opencode` |`session/llm.ts` AI SDK v6 adoption + ReasoningPart shape |MIT |`services/inference/stream-phase.ts` (`streamText` adapter) |**v1.13.1-A/B/C ✅** |
|`anomalyco/opencode` |Parts-message taxonomy (text, tool_call, tool_result, reasoning, step_start) |MIT |`message_parts` table + `messages_with_parts` view |**v1.13.0 ✅ + v1.13.1-B ✅** |
|`anomalyco/opencode` |Two-tier compaction prune (`message_parts.hidden_at` + tier logic) |MIT |`services/inference/prune.ts` (`selectPruneTargets`) |**v1.13.4 ✅** |
|`anomalyco/opencode` |0.85×ctx_max overflow trigger formula |MIT |`services/compaction.ts` early-trigger constant |v1.13.9 (planned) |
|`anomalyco/opencode` |`session/prompt.ts` runLoop() outer agent loop |MIT |`services/inference.ts` step-based loop |v1.14 (planned) |
|`anomalyco/opencode` |`agent.steps` per-agent step cap |MIT |AGENTS.md + agents.ts |v1.14 (planned) |
|`anomalyco/opencode` |`permission/evaluate.ts` wildcard ruleset |MIT |new `permissions` table + matcher |v1.15 (planned) |
|`anomalyco/opencode` |`mcp/index.ts` MCP client (SSE transport + tools/list + tools/call) |MIT |new `services/mcp/` module; codecontext re-wired through it |v1.15 (planned) |
|`cline/cline` |Plan/Act invariant (read-only mode pattern) |Apache-2.0 |absorbed into v1.15 permissions work |v1.15 (planned) |
|`spirituslab/codesight` |`analyze.mjs` — call graph, circular-dep, dead-code |MIT-ish |`apps/server/src/tools/repo_health.ts` |v1.16 (planned) |
|`plandex-ai/plandex` |`pending_changes` data model, diff/apply/rewind UX |MIT |New `pending_changes` table, BooCoder write-tool gating |v2.0 (planned) |
|`OpenHands/OpenHands` |Sandbox runtime pattern |MIT |New per-session Docker sandbox (skip-condition if path-guard holds) |v2.1 (optional) |
|`cortexkit/aft` (ualtinok/aft) |BridgePool warm-process JSON-stdio pattern |check |Optimization if profile shows fork overhead |Deferred |
|`codeprysm/codeprysm` |Node/edge taxonomy (Container/Callable/Data, CONTAINS/USES/DEFINES) |check |Reference only if we ever build our own graph |None |
|`getpaseo/paseo` |**Daemon+clients architecture, CLI verb shape, three skills concept** |AGPL-3.0 (design only) |**Paseo-equivalent dispatcher design** (all phases) |**v2.0+ roadmap** |
|`Dominic789654/agent-hub` |**Task DAG schema, dispatcher worker, project registry, human inbox** |Apache-2.0 |**PostgreSQL schema + dispatcher worker process** |**v2.0** |
|`Roo Code Boomerang Tasks` |Orchestrator-with-capability-restriction + down-pass/up-pass context discipline |Apache-2.0 (pattern) |AGENTS.md design principle (v1.14) → `new_task` tool (v2.0) |**v1.14 → v2.0** |
|`siropkin/budi` |Claude Code 5-hook event taxonomy |MIT (pattern) |Install globally on Sam's host for Claude Code observability |**Immediate (host install)** |
|`sipyourdrink-ltd/bernstein` |HMAC-chained audit log primitive |verify |PostgreSQL audit table with `prev_hmac` field |v1.13+ optional |
|`eyaltoledano/claude-task-master` |Tiered tool loading (`core`/`standard`/`all`) |MIT+Commons Clause (pattern only) |`BOOCODE_TOOLS` env var in `agents.ts` |v1.12.x or v1.13 |
|`ai-christianson/RA.Aid` |Three-stage research/plan/implement + expert escape hatch |Apache-2.0 (pattern) |AGENTS.md design principle + per-stage model routing |v1.14+ |
|`DeepSourceCorp/globstar` |Whole toolkit |MIT |Future verify-before-commit gate for BooCoder |Parked |
|`earendil-works/pi` |`pi-ai` provider abstraction |MIT |Multi-provider LLM if pursued |v2.x optional |
|`microsoft/agent-framework` |Workflow graph concepts |MIT |Conceptual only |v3.x |
|`qodo-ai/agents` |`agent.toml` schema: `output_schema`, `exit_expression`, `execution_strategy` |MIT |Extend `AGENTS.md` / agents.ts metadata |v1.14+ |
|`qodo-ai/qodo-cover` |Record/replay LLM response harness (hashed prompt → fixture YAML) |AGPL-3.0 |Re-implement in Vitest plugin; pattern only, no vendored source |v1.13+ |
|`qodo-ai/qodo-skills` |PR-resolver state machine (fetch issues → batch/interactive fix → inline reply) |MIT |New BooCoder PR-resolver tool with provider CLI adapters |v2.0+ |
|`augmentcode/augment-swebench-agent`|Majority-vote ensembler (K diffs → ranker model → winner) + JSONL schema |MIT |Optional BooCoder verify-gate layer above pending-changes |v2.0+ optional |
|`olimorris/codecompanion.nvim` |Agent Client Protocol (ACP) integration shape |Apache-2.0 |Conceptual only — possible non-web frontend protocol |v2.x watch list |
|`zed-industries/codex-acp` |ACP server-side adapter reference implementation |Apache-2.0 |Working blueprint if BooCode ever ships an ACP adapter |v2.x watch list (parked) |
|`Leonxlnx/taste-skill` |`taste-skill/SKILL.md` (anti-slop ban list + 3-dial parameterization) |MIT |Vendor into BooCode skills/ after diff against existing `frontend-design`; binds to BooCoder|v1.12.x diff → v2.0+ |
|`Fission-AI/OpenSpec` |`openspec/changes/<name>/{proposal,specs,design,tasks}.md` directory structure |permissive (verify) |Reformat BooCode's batch docs to OpenSpec shape; optional CLI adoption later |v1.13.x or v1.14 |
|`covibes/zeroshot` |Complexity × TaskType → workflow conductor + blind-validation invariant |MIT |AGENTS.md principle (no code); blind-validation gate above pending-changes |v1.13/v1.14 (principle) → v2.0+ (gate) |
|`0xmariowu/AgentLint` |31 evidence-backed checks (emphasis density, sweet-spot CLAUDE.md length, SHA-pinned Actions, .env/.gitignore, etc.) |MIT |Manual one-pass audit of CLAUDE.md/AGENTS.md across Sam's repos; optional plugin install |Immediate (manual pass) → v1.12.x (plugin) |
|`aaif-goose/goose` |Native ACP + 15+ providers (incl. Ollama); .claude/.codex/.cursor skill cross-emission |Apache-2.0 |Reference for ACP-protocol implementation and multi-provider abstraction |Reference / v2.x (if ACP lands) |
|`memovai/memov` |Shadow `.mem` timeline; `snap`/`validate_commit` MCP-tool shape; drift detection |MIT |Reference for v1.13+ `view_session_history` tool + v2.0+ verify gate |v1.13+ (history tool design) → v2.0+ (drift gate) |
|`Roo Code: Boomerang Tasks` |Orchestrator with intentional capability restriction; down-pass/up-pass context discipline; precedence override clause|Apache-2.0 (Roo) — pattern lift only |AGENTS.md orchestrator role definition + dispatched-task prompt template |v1.13 / v1.14 (principle), v2.0+ (real delegation)|
|`eyaltoledano/claude-task-master` |Tiered tool-loading via env var (core/standard/all); three model roles; PRD-as-source-of-truth |MIT+Commons Clause (no code lift; pattern only)|`BOOCODE_TOOLS` env var for tiered loading; reaffirm three-model-role pattern |v1.12.x / v1.13 (tier hint) |
|`sipyourdrink-ltd/bernstein` |HMAC-chained audit log; signed agent cards (Ed25519+JCS); per-artifact lineage; air-gap mode |Verify before lift |Reference for compliance-grade BooCode if/when needed; HMAC log small lift candidate |v2.0+ (audit log), speculative (full stack) |
|`siropkin/budi` (tool, not lift) |5-hook Claude Code taxonomy; HTTP daemon + SQLite + dashboard |MIT |Install globally to observe Claude Code token costs; hook taxonomy as reference |Immediate (install) |
-----
## Decisions log
- **v1.13.7 stability bundle uncovered two latent v1.13.1-A regressions (2026-05-22).** Investigation during the cosmetic-revert session surfaced: (1) `@ai-sdk/openai-compatible` defaults `includeUsage: false`, so `stream_options.include_usage` was never sent to llama-swap and `result.usage.inputTokens/outputTokens` resolved `undefined` — every assistant row had `tokens_used`/`ctx_used` NULL since v1.13.1-A shipped. One-line fix in `provider.ts`. (2) AI SDK v6 streaming occasionally emits a leading `\n` text-delta on tool-call-only turns; `content.length > 0` returned true for `"\n"`, producing an empty MessageBubble + ActionRow between every tool call. Fixed by trim guards in `MessageList.flatten` (`hasText`) and `MessageBubble` (`hasContent`). Plus: `buildMessagesPayload` now skips trailing empty/failed assistant rows (kills "Cannot have 2 or more assistant messages" rejections from the upstream), and `BUDGET_NO_AGENT` bumped 15→30 to match `BUDGET_READ_ONLY` (every tool today is read-only; the 15-cap was forward-looking). The class of bug is consistent: AI SDK v6 changes the streaming surface in ways that aren't caught by tsc or vitest — only production observability surfaces them. Argues for v1.13.11 WS-frame Zod schemas to catch the next round.
- **MCP and ACP roles locked per surface (2026-05-22).** **BooChat = MCP client only**, read-only tool consumer. **BooCoder = MCP client + MCP server + ACP client (host) + ACP agent (driveable)** — full matrix. Hard rule: BooChat MCP config must never enable a write-capable server (the read-only invariant overrides protocol convenience). BooCoder's ACP client role **replaces the raw-PTY dispatch plan for any agent that supports ACP** (opencode `opencode acp`, goose `goose acp`); claude/pi/smallcode stay on PTY fallback. The protocol pattern that justifies the full BooCoder matrix: ACP clients auto-forward their MCP `context_servers` to the dispatched agent (per goose docs) — one MCP config surface drives every dispatched agent. BooCoder MCP-server role exposes `boocoder.create_task`, `boocoder.list_pending_changes`, `boocoder.apply`, etc. so external opencode-in-Termius sessions become BooCoder-aware without going through BooCoder's UI. BooCoder ACP-agent role (`boocoder acp`) lets Zed/JetBrains/Avante.nvim drive BooCoder as their agent — outbound exposure, lowest priority of the four roles. **Reference materials**: anthropics `mcp-builder` skill (4-phase build workflow + 10-question eval framework), opencode MCP/ACP docs as JSON-schema reference, goose ACP docs for the `context_servers` auto-forward pattern, `agentclientprotocol.com` spec — but note remote ACP (HTTP/WS) is still WIP, BooCoder's ACP client must use stdio for v1.
- **BooCode monorepo locked as 3-app structure (2026-05-22).** Same `/opt/boocode/` repo: `apps/chat/` (read-only, currently the live thing at 9500), `apps/coder/` (write tools + external CLI dispatch, 9502, v2.0 planned), `apps/booterm/` (PTY terminal, **already live at 9501 since May 2026**, Node 20 Alpine + node-pty + tmux + xterm.js, tmux session per pane, SSH-out enabled). Shared Fastify backend in `apps/server`, shared React shell in `apps/web` hosting the three surfaces as tabs. BooTerm already shares `boocode_db` — confirms cross-surface DB sharing pattern works.
- **Single shared database, rename `boocode_db` → `boochat_db` when BooCoder lands (2026-05-22).** All three surfaces in one Postgres. Enables cross-surface joins (coder task → originating chat conversation → term debugging session).
- **Mount strategy: blanket `/opt:rw`, policy enforcement at the write-tool layer (2026-05-22).** Per-project scoping is logic, not mount. Path-guard correctness becomes the highest-priority test target for v2.0 — fuzz it, property-test it, every traversal-attack pattern.
- **External CLI agents (`opencode` / `claude` / `goose` / `pi`) live on the host, not in containers (2026-05-22).** BooCoder shells out via local-exec PTY (`node-pty`, host shell). Host install means inherit Sam's existing `~/.opencode/`, `~/.claude/`, `~/.config/goose/` configs without re-mounting. Containerize later only if a concrete reason emerges.
- **STRATEGIC PIVOT (2026-05-22): Build a Paseo-equivalent dispatcher inside BooCode. Lift patterns, not code.** Sam wants BooCode to function like Paseo without using Paseo itself. **Paseo (getpaseo/paseo) is AGPL-3.0** — incompatible with BooCode's MIT license and its network-served deployment at `code.indifferentketchup.com`. Vendoring Paseo code would force BooCode to become AGPL. Solution: **reproduce the architecture in BooCode's existing Fastify + TS + PostgreSQL + React stack, using only license-clean patterns**. Full target architecture documented in the new "Paseo-equivalent dispatcher inside BooCode" section at the top of this document. **Primary architectural template: `Dominic789654/agent-hub` (#48)** — Apache-2.0, license-clean, captures the exact three-process model (board server + dispatcher + assistant terminal) and the schema (tasks/projects/templates/pipelines/human_inbox) BooCode should reproduce. **Critical context-management primitive: Roo Code Boomerang Tasks pattern (#46)** — orchestrator with intentional capability restriction, down-pass/up-pass context discipline, no implicit inheritance. **Observation pattern: Claude Code hooks** (siropkin/budi #51 reference) — register BooCode as the hook receiver to get real-time visibility without wrapping the agent. **Phasing:** Phase 1 single-agent PTY dispatch → Phase 2 PostgreSQL queue + worker → Phase 3 Boomerang `new_task` tool → Phase 4 multi-agent + worktrees + CLI → Phase 5 pipelines + dashboard → Phase 6 handoff/loop/orchestrator skills. **This is now the dominant roadmap direction**, ahead of v1.12.x debugger fixes (queued) and v1.13/v1.14 batch work (deferred until Paseo-equivalent Phases 12 are scoped).
- **BooCoder agent layer: both Option A AND Option B, full-featured (2026-05-22).** Earlier May 18 chat recommended Option A (thin orchestration shell over OpenCode) as the path forward but explicitly called the choice not-locked. Sam's call this session: ship **both** paths in the same BooCoder surface. **Option B / in-process loop** handles interactive write work with native tools + pending-changes UI (v2.0 plandex pattern still applies). **Option A / PTY dispatch** handles parallel/batch work where Sam wants to A/B opencode vs claude vs goose vs pi against the same task in separate worktrees. User picks per task. This supersedes the May 18 "reframe Batch 14 as OpenCode orchestration UI" recommendation — both paths now coexist.
- **Paseo (getpaseo/paseo) is the reference design, not a catalog code lift (2026-05-22).** AGPL-3.0 + 4k stars + 6-package TypeScript monorepo (server / app / cli / desktop / relay / website). The architecture is the lift: daemon + clients split, child-process agent orchestration, WebSocket protocol, `paseo run/ls/attach/send` CLI shape, `--worktree feature-x` flag, `/paseo-handoff` / `/paseo-loop` / `/paseo-orchestrator` skills. **Do not vendor code.** Read the README and the `skills/` directory's three skill files for design reference; reimplement in BooCode's MIT stack. The skills' shape (named `/handoff`, `/loop`, `/orchestrator` operations) is non-copyrightable; lift the shape.
- **Embeddings dropped from BooCode** (May 2026). Replaced RAG with file-view tools + sidecar analyzers.
- **opencode promoted to Tier A** (2026-05-20). The compaction port (v1.11.0) made it clear opencode is not just "the agent Sam uses" — it's the canonical reference implementation for everything BooCode is rebuilding through v1.15. Five algorithms identified for lift (compaction, doom-loop, repairToolCall, runLoop, permission evaluate) plus truncate.ts and MCP client.
- **Source is `sst/opencode` `dev` branch.** `anomalyco/opencode` is a rebranded mirror; do not source from there.
- **opencode promoted to Tier A** (2026-05-20). The compaction port (v1.11.0) made it clear opencode is not just "the agent Sam uses" — it's the canonical reference implementation for everything BooCode is rebuilding through v1.15. Five algorithms identified for lift (compaction, doom-loop, repairToolCall, runLoop, permission evaluate) plus truncate.ts and MCP client. **Update 2026-05-22:** truncate.ts shipped v1.13.5; doom-loop, repairToolCall, compaction, prune all shipped; runLoop + permission still queued for v1.14/v1.15.
- **OpenCode canonical repo is `anomalyco/opencode`, NOT `sst/opencode` (corrected 2026-05-22).** Sam confirmed: the prior catalog entry's "anomalyco is a rebranded mirror, use sst as canonical" was inverted. Development moved to anomalyco; sst/opencode is the predecessor lineage. `anomalyco/opencode` `dev` branch is now the active source for every algorithm lift through v1.15. All 15 catalog references rewritten in this session.
- **Original Batch 11 (aider PageRank port) replaced** by codecontext sidecar approach.
- **Original Batch 12 (codebase indexer w/ Harrier) removed.** No embedding infrastructure.
- **Original Batch 13 (OpenHands event log) replaced** by v1.13 parts table (opencode pattern). Same outcome, different shape.
@@ -239,6 +430,38 @@ BooCode is personal/single-user — license compatibility is non-blocking, but t
- **Aider's `repomap.py` port dropped.** Codecontext supersedes it. Aider contribution narrows to the `.scm` query files only.
- **Globstar role re-scoped.** Not an architect tool — parked for future verify-before-commit gate.
- **codeprysm role re-scoped.** Taxonomy reference only. Embedding half rejected.
- **AI SDK adoption deferred to v1.13.** Hand-roll opencode's repairToolCall pattern in v1.12 first.
- **AI SDK adoption deferred to v1.13.** Hand-roll opencode's repairToolCall pattern in v1.12 first. **Update 2026-05-22:** v1.12 deferred the repairToolCall hand-roll entirely; both AI SDK v6 adoption AND repairToolCall shipped together in v1.13.1-A/v1.13.3 — cleaner outcome than the two-step plan.
- **`tool_choice='required'` confirmed supported** by llama-swap (qwen3.6-35b-a3b-mxfp4, 2026-05-20). Repair tool call is viable.
- **`anomalyco/sst` is a mirror, not a fork.** Same applies to `anomalyco/opencode`. Use canonical `sst/sst` and `sst/opencode` sources.
- **`anomalyco/opencode` confirmed canonical (2026-05-22).** Earlier confusion about whether `sst/opencode` or `anomalyco/opencode` was the active fork is resolved: anomalyco is where active development continues. Use `anomalyco/opencode` for all algorithm lifts.
- **Reviewed 2026-05-22 (cline, kilocode, prompt-tower, auggie, augment-agent, augment-swebench-agent, codecompanion.nvim, junie, cody-public-snapshot, qodo-ai/{agents,qodo-cover,open-aware,qodo-skills}).** Three real lifts emerged:
- **Qodo `agent.toml` schema** (`output_schema`, `exit_expression`, `execution_strategy`) → land in AGENTS.md at v1.14+.
- **qodo-cover record/replay LLM harness** → re-implement (don't vendor — AGPL) as a Vitest fixture plugin at v1.13+.
- **augment-swebench-agent ensembler** → optional v2.0+ verify-gate layer above pending-changes (plandex pattern).
- **qodo-skills PR-resolver state machine** → BooCoder v2.0+ tool.
- **ACP added to v2.x watch list.** Zed's Agent Client Protocol is the analog of MCP for client↔agent. Not in any current batch; revisit only if BooCode wants to expose itself to Zed/Neovim/Termius beyond the web UI. **Reference implementations bracket the protocol:** codecompanion.nvim (#28) is the client side, zed-industries/codex-acp (#31) is the server-side adapter. The codex-acp README confirms ACP's full feature surface (context @-mentions, images, permission-gated tool calls, edit review, TODO lists, slash commands, client MCP servers) matches features BooCode already has internally — adopting ACP would be transport translation, not feature build.
- **kilocode and Cline skipped as code sources** (entry #20). Orchestrator/sub-agent pattern is already covered by cline (#7) and agent-framework (#15).
- **Junie skipped permanently.** No usable source.
- **Cody parked.** Multi-repo context fetcher is the only interesting piece; overkill for single-repo BooCode.
- **prompt-tower skipped.** AGPL VS Code extension; nothing novel that continue's ignore lift + universal XML wrapping doesn't already cover.
- **tiktoken-rs and calloop rejected (2026-05-22).** Both are Rust and Zed-stack-specific. tiktoken-rs additionally fails the model check — Qwen/Gemma/Nemotron don't use OpenAI's BPE encodings, so token counts would be wrong by 1030%. **Source of truth for token counts on llama-swap models is `POST /tokenize` on llama-server**; no client-side tokenizer library needed. Do not re-evaluate either repo.
- **taste-skill accepted as Tier B vendor candidate (2026-05-22).** MIT, SKILL.md format already matches BooCode v1.12 standard, 18k+ stars, framework-agnostic. Two real wins: the 100+ anti-slop ban list (specific font/color/layout failure modes LLMs default to) and the 3-dial parameterization pattern (reusable beyond design). **Gated on a diff against the existing `frontend-design` SKILL** to avoid duplication before vendoring. Real value lands with BooCoder v2.0+ when write tools generate frontend code for Sam's projects (DubDrive, BooLab, Fathom, etc.).
- **costrict skipped, OpenSpec accepted (2026-05-22).** costrict is Apache-2.0 but the top contributors are Roo Code maintainers and the codebase has `.roo/`/`.rooignore`/`.roomodes` — same Cline-lineage VS Code extension shape BooCode rejected at kilocode (#20). The novel surface costrict ships is its **OpenSpec integration**, which is a separate repo. **OpenSpec is the real find:** it formalizes the spec-governed dispatch workflow Sam already uses (per-change folder with proposal/specs/design/tasks artifacts, slash commands per agent, artifact-lifecycle gates). Start by adopting just the directory structure for BooCode's own batch docs (zero-dep documentation reformat); evaluate full CLI adoption later. **Tracked for v1.13.x or v1.14**, not blocking v1.12.0.
- **agents.md noted but not evaluated.** costrict's README acknowledges `agentsmd/agents.md` as a partner. The name and shape strongly suggest it's the canonical source of the AGENTS.md convention BooCode v1.12 already adopted. Worth a future drive-by to confirm, but not blocking anything.
- **zeroshot accepted as Tier B pattern reference (2026-05-22).** MIT, multi-agent orchestration above coding-agent CLIs (Claude Code, Codex, OpenCode, Gemini CLI). **Sits at Paseo's layer, not BooCode's.** Five pattern lifts: complexity-classification conductor, blind-validation invariant (separate agent context verifies — doesn't see worker's history), crash-safe SQLite ledger, three-tier isolation taxonomy (none/worktree/Docker), JSON cluster templates. **The blind-validation invariant is the single most important architectural idea** zeroshot adds — fills the missing piece in plandex/OpenHands/cline patterns where the same agent writes and judges its own work. Lands at BooCode v1.13/v1.14 as an AGENTS.md design principle, then at v2.0+ as a real verify gate above pending-changes. **Separately:** zeroshot is a candidate Paseo-successor if Paseo ever needs replacement; that's a Paseo-scope decision, not BooCode-scope.
- **toprank rejected (2026-05-22).** SEO/SEM domain — wrong category for BooCode. Sam runs developer infrastructure, not marketing sites. Skill format is the same one BooCode v1.12 already uses; no novel pattern.
- **AgentLint accepted as high-value immediate-application reference (2026-05-22).** MIT, 31 evidence-backed repo-quality checks. Most useful catalog entry for *the present moment* — applies directly to every CLAUDE.md/AGENTS.md across Sam's homelab (BooCode, BooLab, HLH, indifferent-broccoli, paseo, etc.) without needing any code lift or version dependency. Specific data points from 265 versions of Anthropic's Claude Code system prompt are immediately actionable: trim emphasis-keyword density, target 60120 line CLAUDE.md sweet spot, SHA-pin Actions, ensure `.env`/`CLAUDE.local.md` are gitignored. **Recommend a single audit pass session against BooCode's instruction files** before any further skill work lands. Optional plugin install for ongoing audits is a v1.12.x post-merge call.
- **awesome-vibe-coding surveyed (2026-05-22).** 60+ tools across 10 sections. **No new catalog entries promoted from the list.** Already-covered items: Cline, Roo Code, Continue, Prompt Tower, Augment, aider, Codex CLI, Gemini CLI. Skipped on category: 18 Web Builders, 4 Editor/IDEs, mobile/desktop builders. **Real leads tracked for next review pass:** `block/goose` (multi-model local agent framework), `eyaltoledano/claude-task-master` (task decomposition algorithm), `ai-christianson/RA.Aid` and the underlying `LangGraph` framework (workflow graphs in production), `automata/aicodeguide` (AI-first methodology). Do not re-evaluate the rejected items.
- **aaif-goose/goose (formerly block/goose) added as Tier B reference (2026-05-22).** Apache-2.0, 45.2k stars, recently moved to Linux Foundation's Agentic AI Foundation. Rust + TypeScript. Native ACP, 15+ providers including Ollama, MCP support for 70+ extensions. **Sits at Paseo's layer, not BooCode's.** Skip code (wrong stack); track as reference for ACP-protocol implementations and the multi-provider abstraction pattern. Ships `.claude/`, `.codex/`, `.cursor/` skill directories — confirms the cross-agent skill-emission pattern noticed in autohand/code-cli (#33).
- **memovai/memov accepted as Tier B reference (2026-05-22).** MIT, Python. Shadow `.mem` timeline tracks prompts + context + plan + file changes at every agent interaction; zero pollution to `.git`. MCP-exposed. `validate_commit` MCP tool detects context drift between prompt and actual changes. **Direct match for BooCode's reviewer-architect pattern.** Lift the MCP-tool shape (`snap`, `mem_history`, `mem_jump`, `validate_commit`) as reference for v1.13+ `view_session_history` feature and v2.0+ verify gate. Don't vendor Python code into Fastify/TS BooCode.
- **bhouston/mycoder rejected (2026-05-22).** MIT, TypeScript, 566 stars, **stale** (last release Mar 2025). Standard CLI coding agent — Claude/OpenAI/Ollama, MCP, parallel sub-agents. Functionally a less-mature opencode. Sam already uses opencode for this role. One UX pattern noted (Ctrl+M mid-stream corrections) but BooCode/opencode/Claude Code all have chat-based interruption. Skip.
- **ai-christianson/RA.Aid accepted as Tier B pattern reference (2026-05-22).** Apache-2.0, Python, 2.2k stars. **Three-stage architecture (Research / Planning / Implementation) on LangGraph** with per-stage model routing (`--research-provider`, `--planner-provider`, `--expert-provider`) + "expert tool" called only when needed for hard reasoning. **Aligns directly with Sam's qwopus27b/qwen3-coder routing.** Lift the three-stage AGENTS.md design and expert-tool escape hatch at v1.14+; don't lift LangGraph (wrong stack); never enable `--cowboy-mode` equivalent (opposite of BooCode's no-autonomous-commit rule).
- **Kirill89/reviewcerberus rejected as code, CoV logged as pattern (2026-05-22).** Closed-source Docker distribution (license not in registry). Multi-provider (Bedrock/Anthropic/Ollama/Moonshot), accepts `guidelines.md`, **Chain-of-Verification mode** to reduce false positives. CoV is the only takeaway — per-finding verification primitive, complementary to zeroshot's blind-validation (per-workflow #37) and bernstein's lineage chains (per-artifact #49). Stackable.
- **autohandai/code-cli rejected (2026-05-22).** 56 stars, COMMERCIAL.md present (commercial license restriction likely). Standard ReAct CLI agent with no novel pattern vs opencode. Cross-agent skill emission (copies skills between `~/.claude/skills/`, `~/.codex/skills/`, `~/.autohand/skills/`) is the only interesting bit — same pattern goose (#41) does. Skip.
- **Roo Code Boomerang Tasks accepted as Tier B pattern reference (2026-05-22, Sam-flagged).** Roo Code itself rejected (already covered via #20 kilocode and #35 costrict — VS Code/Cline lineage). Three architectural patterns lifted: **(1) Orchestrator with intentional capability restriction** — cannot read/write/MCP/shell, only delegates, preventing context poisoning. **(2) Down-pass/up-pass context discipline** — no implicit inheritance, parent passes context down via `new_task` message, subtask passes summary up via `attempt_completion` result only. **(3) Explicit precedence override clause** baked into subtask prompts. Together these sharpen zeroshot's blind-validation (#37) into a both-directions principle. Lands at v1.13/v1.14 as AGENTS.md design, v2.0+ as real delegation mechanics.
- **eyaltoledano/claude-task-master pattern accepted, code rejected (2026-05-22).** **MIT + Commons Clause** makes BooCode (self-hosted developer chat) a competing product — no code vendoring. 25.7k stars, JS/TS. Three patterns worth lifting independently in BooCode's own MIT code: **(1) Tiered tool-loading via env var** (`TASK_MASTER_TOOLS=core|standard|all|custom`, 7/15/36 tools, ~5k/10k/21k tokens) — direct fit for `BOOCODE_TOOLS` at v1.12.x or v1.13. **(2) Three model roles** (main/research/fallback) — same pattern as RA.Aid (#44), complementary evidence. **(3) PRD-as-source-of-truth** at `.taskmaster/docs/prd.txt` formalizes Sam's spec-governed work convention.
- **Dominic789654/agent-hub tracked, not lifted (2026-05-22).** Apache-2.0, Python 100% stdlib-only (no FastAPI/SQLAlchemy/Pydantic — zero supply chain surface), 1 star, v0.1.0 March 2026. Local-first multitask board for routing/observing code-assistant work across repos. SQLite queueing, dependency-aware dispatch, **human inbox**, dashboard at `/app`. **Architecturally what Paseo wants to grow into.** Too early to vendor; track for next pass. The stdlib-only constraint is a useful lens to evaluate BooCode/BooLab dependency footprint.
- **sipyourdrink-ltd/bernstein tracked as compliance-grade reference (2026-05-22).** License needs verification before any lift (`/LICENSE` should be checked directly). 262 stars, Python. Same layer as zeroshot (#37) and agent-hub (#48), but with **audit-grade compliance** as differentiator: HMAC-chained audit log, signed agent cards (Ed25519/EdDSA + JCS), per-artifact lineage (producer + inputs + prompt SHA + model + cost), customer-key signing for DORA/NIS2/EU AI Act Article 12, air-gap deploy, deterministic scheduler, one git worktree per agent, cost-aware routing bandit. **Over-spec for Sam's current homelab work** but the right shape if BooCode ever needs to produce audit evidence. The **HMAC-chained audit log** is a small lift-friendly pattern even today.
- **vorn-run/vorn rejected as code, pattern noted (2026-05-22).** MIT, Electron + TypeScript, 24 stars, alpha. Multi-agent grid UI for Claude Code/Copilot/Codex/OpenCode/Gemini. Each agent in its own PTY. Task queue + kanban + workflow automation + headless execution + inline diff review with structured-feedback-back-to-agent + worktree isolation + MCP server. **Wrong stack** (Electron desktop UI vs BooCode's Fastify/TS+React SPA). Pattern note: **PTY-per-agent + worktree-per-task + inline-diff-feedback-loop** is the canonical shape for multi-agent orchestration above real CLI agents; same architectural choice Paseo made.
- **siropkin/budi accepted as tooling, not catalog entry (2026-05-22).** MIT, Rust, single 6MB binary, sub-millisecond hook latency. **WakaTime for Claude Code** — tracks tokens, costs, prompts, file activity, sub-agent spawns in local SQLite, dashboard at `localhost:7878/dashboard`. **Recommend immediate install** (`budi init --global`) for Claude Code session observability. The **5-hook Claude Code event taxonomy** (`SessionStart`, `UserPromptSubmit`, `PostToolUse`, `SubagentStart`, `Stop`) is the canonical reference and worth knowing when BooCode v2.0+ designs its own hook system.
- **GeiserX/LynxPrompt tracked as architectural reference, code off-limits (2026-05-22).** **GPL-3.0 makes vendoring incompatible with BooCode's MIT licensing.** 27 stars, Next.js + PostgreSQL + Prisma. Self-hostable platform for managing AGENTS.md / CLAUDE.md / .cursor/rules / slash commands across **30+ AI assistant formats**. Single blueprint, export to N formats. Federated marketplace. The concept fits Sam's situation (5+ project CLAUDE.md/AGENTS.md files maintained separately) but the **manual AgentLint (#39) audit pass is the right ROI today** rather than adopting a full platform. If consolidation ever needed, reimplement the format-adapter pattern in MIT-licensed BooCode code, don't vendor.
- **ShipWithAI/claude-code-mastery noted as docs reference (2026-05-22).** **CC BY-NC-SA 4.0** content + MIT code examples. 9 stars. Free 16-phase / 55-module / 136-lesson course on Claude Code workflows. **Two structural patterns worth borrowing:** (1) **7-block module structure** (WHY → CONCEPT → DEMO → PRACTICE → CHEAT SHEET → PITFALLS → REAL CASE) as a docs template; (2) **phase list as coverage checklist** to diff against Sam's own CLAUDE.md/AGENTS.md files — combine with AgentLint (#39) for a single audit pass. Don't redistribute content (NC license).

View File

@@ -1,127 +1,204 @@
# BooCode v1.x — Roadmap
Last updated: 2026-05-21
Last updated: 2026-05-22
> **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.
## Overview
BooCode is a standalone code-chat tool at `/opt/boocode/`. Read-only by design — pick a project, chat with a local LLM that has file-inspection tools, get streaming responses over WebSocket.
BooCode is a **3-app monorepo** at `/opt/boocode/` (locked 2026-05-22):
Live at `https://code.indifferentketchup.com` (Caddy → Authelia → Tailscale → `100.114.205.53:9500`).
- **BooChat** (`apps/chat`, port `9500`, `code.indifferentketchup.com`) — read-only chat with file-inspection tools. The live thing. Pick a project, chat with a local LLM, get streaming responses over WebSocket. Will rename `boocode_db``boochat_db` when BooCoder lands.
- **BooCoder** (`apps/coder`, port `9502`, `coder.indifferentketchup.com`) — write tools + external-CLI dispatch. **Planned, v2.0.** Both an in-process inference loop (with `pending_changes` table) AND ACP-dispatched external agents (opencode/goose) with PTY fallback (claude/pi/smallcode) — same surface, two execution paths.
- **BooTerm** (`apps/booterm`, port `9501`) — PTY/tmux/xterm.js. **Live since May 2026.** Node 20 Alpine + node-pty + tmux + xterm.js. Tmux session per pane (`bc-<uuid>`), SSH-out works (openssh-client + gosu in the image). `/api/term/health` shares the existing `boocode_db`.
Caddy → Authelia → Tailscale → `100.114.205.53` → 9500/9501/9502. Three apps, **one shared Postgres** (`boocode_db``boochat_db`).
**Architectural commitments:**
- No embeddings. Model uses file-view tools (`view_file`, `list_dir`, `grep`, `find_files`) + sidecar analyzers (codecontext, codesight) + codecontext MCP tools. Walked away from the RAG pipeline May 2026.
- Read-only in v1.x. Write tools land in BooCoder (separate container, post-v1.x).
- One Postgres (`boocode_db`), one frontend SPA, container-per-service for new capabilities.
- **No embeddings.** Model uses file-view tools (`view_file`, `list_dir`, `grep`, `find_files`) + sidecar analyzers (codecontext, future codesight) + codecontext MCP tools. Walked away from the RAG pipeline May 2026.
- **BooChat is read-only** through v1.x. Write tools land in BooCoder at v2.0.
- **Mount strategy: blanket `/opt:rw`, permission gating at the write-tool layer.** Per-project scoping is policy, not mount. Path-guard correctness is the #1 test target for v2.0.
- **External CLI agents (`opencode`/`claude`/`goose`/`pi`) live on the host, not in containers.** BooCoder shells out via local-exec PTY or ACP subprocess. Host install inherits Sam's existing `~/.opencode/`, `~/.claude/`, `~/.config/goose/` configs.
- **Protocol roles locked (2026-05-22):** **BooChat = MCP client only** (read-only tool consumer, never enables write-capable MCP servers). **BooCoder = MCP client + MCP server + ACP client (host) + ACP agent (driveable)** — full matrix. BooCoder's ACP-client role replaces raw-PTY dispatch for ACP-capable agents (opencode `opencode acp`, goose `goose acp`); PTY fallback retained for claude/pi/smallcode.
- **Strategic target: Paseo-equivalent dispatcher inside BooCode** (2026-05-22 pivot). Paseo (`getpaseo/paseo`) is AGPL-3.0 — incompatible with BooCode's MIT license and network-served deployment. Reproduce the architecture using only license-clean patterns. Primary architectural template: `Dominic789654/agent-hub` (Apache-2.0). Critical context-management primitive: Roo Code Boomerang Tasks pattern. Observation pattern: Claude Code hooks (siropkin/budi reference).
External code lifted from / referenced in: see `boocode_code_review.md` for full inventory.
-----
## Shipped (status as of 2026-05-21)
## Shipped (status as of 2026-05-22)
| Version | Theme | Tag |
|---|---|---|
| v1.0 | Initial scaffold | — |
| Batches 14.4 | Markdown, sidebar, panes, chats-inside-sessions, archive, fork/delete, header polish, settings drawer | — |
| v1.5 | resolveProjectPath, BOOTSTRAP_ROOT, vitest pin | — |
| v1.6, v1.6.1, v1.6.2 | Mobile pass + RightRail mobile drawer | — |
| v1.7 | Drag-drop file + paste-as-attachment | — |
| v1.8, v1.8.1, v1.8.2 | Settings drawer, git_status tool, WS reconnect, per-turn budget reset + Continue affordance + CapHitSentinel | — |
| v1.9.1 | Skills system (`/opt/skills/` + `skill_find` / `skill_use` / `skill_resource` + `/skill` slash command) | `v1.9.1` |
| v1.9.7 | `ask_user_input` elicitation tool | `v1.9.7` |
| Batch 9 (Agents Tier 2) | `AGENTS.md` + 6 builtin agents + AgentPicker in ChatInput toolbar + `sessions.agent_id` | folded into `v1.9.1`/`v1.9.7` |
| v1.10.0 | BooTerm: separate container, xterm.js + node-pty + tmux | `v1.10.0` |
| v1.10.1 | BooTerm-user (spawn as samkintop, login bash, Claude Code/opencode PATH) | `v1.10.1` |
| v1.10.4, v1.10.5 | Mobile terminal + XML tool-call fallback parser | — |
| v1.11.0 | opencode-style compaction port (auto-overflow, anchored summary, tail preservation) | — |
| v1.11.1 | Compaction follow-up (working indicator during compaction, unit tests, .bak cleanup) | — |
| v1.11.2 | ContextBar (persistent context-usage indicator above MessageList) | — |
| v1.11.3 | `ctx_max` capture via `/upstream/<model>/props` (replaces dead `timings.n_ctx` read) | `v1.11.3` |
| v1.11.5 | ContextBar inline next to agent picker; remove ChatContextPopover; default new sessions to no agent | — |
| v1.11.6 | Doom-loop guard from opencode (3 identical tool calls → sentinel, abort recursion) | — |
| v1.11.7 | pathGuard secrets filter (continue.dev `DEFAULT_SECURITY_IGNORE_FILETYPES`) | — |
| v1.11.8 | web_search + web_fetch tools via SearXNG | — |
| v1.11.9 | Manual redirect handling — re-run URL guard on each hop (SSRF hardening) | — |
| v1.11.10 | Stream-cap response body at 5MB, abort on overflow | `v1.11.x` |
| **v1.12.0** | **codecontext sidecar (Go HTTP shim, NDJSON MCP framing, child.Wait supervisor) + container guidance (BOOCHAT.md/BOOCODER.md) + 7 vendored skills + system-prompt.ts extraction + mtime-watch cache + 8 codecontext tool wrappers + per-agent tool whitelists + .codecontextignore template + agents.ts ALL_TOOL_NAMES single-source-of-truth fix** | `v1.12.0` |
|Version |Theme |Tag |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
|v1.0 |Initial scaffold |— |
|Batches 14.4 |Markdown, sidebar, panes, chats-inside-sessions, archive, fork/delete, header polish, settings drawer |— |
|v1.5 |resolveProjectPath, BOOTSTRAP_ROOT, vitest pin |— |
|v1.6, v1.6.1, v1.6.2 |Mobile pass + RightRail mobile drawer |— |
|v1.7 |Drag-drop file + paste-as-attachment |— |
|v1.8, v1.8.1, v1.8.2 |Settings drawer, git_status tool, WS reconnect, per-turn budget reset + Continue affordance + CapHitSentinel |— |
|v1.9.1 |Skills system (`/opt/skills/` + `skill_find` / `skill_use` / `skill_resource` + `/skill` slash command) |`v1.9.1` |
|v1.9.7 |`ask_user_input` elicitation tool |`v1.9.7` |
|Batch 9 (Agents Tier 2)|`AGENTS.md` + 6 builtin agents + AgentPicker in ChatInput toolbar + `sessions.agent_id` |folded into `v1.9.1`/`v1.9.7`|
|v1.10.0 |BooTerm: separate container, xterm.js + node-pty + tmux |`v1.10.0` |
|v1.10.1 |BooTerm-user (spawn as samkintop, login bash, Claude Code/opencode PATH) |`v1.10.1` |
|v1.10.4, v1.10.5 |Mobile terminal + XML tool-call fallback parser |— |
|v1.11.0 |opencode-style compaction port (auto-overflow, anchored summary, tail preservation) |— |
|v1.11.1 |Compaction follow-up (working indicator during compaction, unit tests, .bak cleanup) |— |
|v1.11.2 |ContextBar (persistent context-usage indicator above MessageList) |— |
|v1.11.3 |`ctx_max` capture via `/upstream/<model>/props` (replaces dead `timings.n_ctx` read) |`v1.11.3` |
|v1.11.5 |ContextBar inline next to agent picker; remove ChatContextPopover; default new sessions to no agent |— |
|v1.11.6 |Doom-loop guard from opencode (3 identical tool calls → sentinel, abort recursion) |— |
|v1.11.7 |pathGuard secrets filter (continue.dev `DEFAULT_SECURITY_IGNORE_FILETYPES`) |— |
|v1.11.8 |web_search + web_fetch tools via SearXNG |— |
|v1.11.9 |Manual redirect handling — re-run URL guard on each hop (SSRF hardening) |— |
|v1.11.10 |Stream-cap response body at 5MB, abort on overflow |`v1.11.x` |
|v1.12.0 |codecontext sidecar (Go HTTP shim, NDJSON MCP framing, child.Wait supervisor) + container guidance (BOOCHAT.md/BOOCODER.md) + 7 vendored skills + system-prompt.ts extraction + mtime-watch cache + 8 codecontext tool wrappers + per-agent tool whitelists + .codecontextignore template + agents.ts ALL_TOOL_NAMES single-source-of-truth fix |`v1.12.0` |
|v1.12.1 |Server-side workspace pane sync (`sessions.workspace_panes jsonb`) + 5-state status indicator overhaul (streaming/tool_running/waiting_for_input/idle/error) + startup hung-row sweep + stale `messages_status_check` constraint dropped + `detectSameNameLoop` reverted (dead code) + stop-handler writes `cancelled` status |`v1.12.1` |
|v1.12.2 |Live tok/s + ctx_used display next to status indicator while streaming (frontend-only) |`v1.12.2` |
|v1.12.3 |Stale-stream banner — "Previous response didn't complete. [Retry] [Discard]" when streaming row > ~60s with no new tokens. `POST /api/chats/:id/discard_stale` backend endpoint |`v1.12.3` |
|v1.12.4 |Refactor only — `inference.ts` (1700 LoC) split into `inference/` directory: `turn.ts`, `stream-phase.ts`, `tool-phase.ts`, `error-handler.ts`, `sentinel-summaries.ts`, `payload.ts`, `xml-parser.ts`, `sentinels.ts`, `budget.ts`, `types.ts`, `index.ts`. Shipped as rc1/rc2/rc3 → final. No behavior change. Lined up `stream-phase.ts` as the swap target for v1.13 AI SDK migration |`v1.12.4` |
|**v1.13.0** |**`message_parts` table** `(id, message_id, sequence, kind, payload jsonb, created_at)` with kinds `text/tool_call/tool_result/reasoning/step_start`. CHECK constraint, `(message_id, sequence)` unique + index. Dual-write at every site that wrote `tool_calls`/`tool_results` JSON (stream-phase finalize, skills × 2, messages.ts answer flow, chats.ts × 2). `ToolDef<T>` gained `category: 'read_only' | 'write'`. v1.x registry rejects write. Old JSON columns remain authoritative for reads. Strangler-fig phase 1 |`v1.13.0` |
|**v1.13.1-A** |**AI SDK v6 install + streamCompletion adapter.** `ai@^6`, `@ai-sdk/openai-compatible@^2`. `provider.ts` wraps `createOpenAICompatible` against `config.LLAMA_SWAP_URL`. `streamCompletion` rewritten as adapter over `streamText`. XML fallback parser preserved for qwen3.6's inline `<tool_call>` emissions. **Patched mid-flight:** AI SDK v6 swallows abort signals silently — explicit `if (signal?.aborted) throw` after stream drain. Without it, stop button writes `complete` instead of `cancelled`. reasoning-delta counted + dropped (re-captured in -C). Known regression flagged: live mid-stream tps gone (single trailing publish; TODO for delta-cadence interpolation against `result.usage`) |(umbrella tag) |
|**v1.13.1-B** |**`messages_with_parts` view** with COALESCE fallbacks against legacy JSON columns. Read sites switched: `chats.ts:427`, `messages.ts:95`, `ws.ts:27`, `payload.ts`, `compaction.ts`. Perf verified at 1ms for 42-message chat. `reasoning_parts` column added to the view (consumed in -C). API contract preserved. Parts become source of truth at read; JSON columns kept by dual-write only |(umbrella tag) |
|**v1.13.1-C** |**`ask_user_input` correlation ported to parts.** `messages.ts:478/549` now JOINs `message_parts` on `payload->>'id'` and `payload->>'tool_call_id'`. Downstream call sites updated to `{message_id, payload}` shape. 404 fallback for pre-v1.13.0 history (acceptable scope). **Reasoning end-to-end:** `reasoning-delta` accumulated in `stream-phase.ts` adapter via `StreamResult.reasoning` (simpler than the brief's StreamPhaseState approach); `partsFromAssistantMessage` accepts optional `reasoning`, emits at seq 0; `finalizeCompletion` + `executeToolPhase` dual-write reasoning parts; `payload.ts` reads `reasoning_parts` from view, collapses into `OpenAiMessage.reasoning`; `toModelMessages` emits AI SDK `ReasoningPart` in assistant content array. Smoke: 361 chars reasoning at seq 0, 429 chars text at seq 1 |`v1.13.1` (`ac1a71f`) |
|**v1.13.3** |**Cleanup bundle, 4 independent items.** (1) `ALTER DATABASE boocode SET statement_timeout = '30s'` — caps damage from query-plan regression on the view's nested subselects; documented in `schema.sql` since `ALTER DATABASE` can't run inside a DO block. (2) Alpha-sorted tool registry — `.sort((a, b) => a.name.localeCompare(b.name))` at `ALL_TOOLS` export; llama.cpp prompt cache hits on byte-identical prefixes, tool-order drift killed hit rate every turn. (3) Periodic 60s in-process sweeper marks `streaming` rows older than 5 min as `failed` and publishes `chat_status='idle'` so the UI dot drops — closes mid-session crash UX gap that the startup sweep (v1.12.1) only handled at boot. (4) `experimental_repairToolCall` wired through AI SDK v6 `streamText` — routes malformed tool calls to a logged passthrough instead of crashing the stream. Owed since v1.13.1-A. 173/173 tests pass (+1 alpha-ordering test)|`v1.13.3` (`a08d809`) |
|**v1.13.4** |**Two-tier compaction prune.** `services/inference/prune.ts` with pure `selectPruneTargets` decision helper. Tier 1 hides stale `tool_result` parts via `message_parts.hidden_at` at the 20k-freed threshold (cheap, no inference call); tier 2 falls back to anchored summarize when prune alone isn't enough. Schema additions: `message_parts.hidden_at` column + partial index `ON (message_id) WHERE hidden_at IS NULL`. `messages_with_parts` view filters hidden parts so payload assembly never sees them. Avoids burning an inference round on every overflow. opencode-pattern half-shipped in v1.11.0 — this closes it. |`v1.13.4` (`ec8593c`) |
|**v1.13.5** |**opencode `truncate.ts` port — full tool output retrievable via opaque id.** New `services/truncate.ts` with `tr_<12 base32>` ids on tmpfs (`/tmp/boocode-truncations`, 0o700, 5MB cap matching `view_file`'s `MAX_FILE_BYTES`, 7-day TTL). Three exports: `storeTruncation`, `readTruncation`, `truncateIfNeeded` (wrap-or-passthrough helper). New `view_truncated_output(id)` tool retrieves the full content; model never sees the truncation dir (resolved server-side). Wired through 5 of 7 tool sites: `view_file`, `list_dir`, `web_fetch`, `codecontext_client`, plus alpha-sorted into `ALL_TOOLS` (count 19→20). `cleanupTruncations` piggybacks on the v1.13.3 60s sweeper (TTL pass + orphan reap via parts query on `payload->'output'->>'outputPath'`). grep and find_files deferred (need file_ops refactor to expose uncapped output). 186 tests (was 179, +7 in truncate.test.ts). |`v1.13.5` (`f8fc5db`) |
|**v1.13.6** |**Compaction head-assembly audit + reasoning fix.** Audit traced compaction's summary path post-v1.13.1-B read flip across three quadrants — Q1 view read (clean), Q2 parts shape (clean), Q3 reasoning render (FIX NEEDED). v1.13.1-C wired reasoning end-to-end into `inference/payload.ts` but missed the compaction read site, silently degrading summary quality for reasoning-channel models (qwen3.6) since -C shipped. Fix: `CompactionMessage` extended with `reasoning_parts` field; SELECT pulls `reasoning_parts` from `messages_with_parts`; `buildHeadPayload` (now exported for tests) prefixes assistant content with `<reasoning>...</reasoning>\n\n<content>` when reasoning is present; standalone `<reasoning>` tag for tool-call-only turns; omits tag when reasoning is null or empty. 4 new render-branch tests (190 total). |`v1.13.6` (`81d837c`) |
|**v1.13.7** (uncommitted)|**Stability bundle, 5 fixes from production observability gap.** (1) `provider.ts``includeUsage: true` on `createOpenAICompatible`. `@ai-sdk/openai-compatible` defaults this false, omitting `stream_options.include_usage` from request body; llama-swap never emitted the usage block, so `result.usage.inputTokens/outputTokens` resolved `undefined` and `tokens_used`/`ctx_used` landed NULL in **every** assistant row since v1.13.1-A. Surfaces tokens in StatsLine + persisted DB rows going forward (no backfill). (2) `MessageList.tsx:48``hasText = m.content.trim().length > 0`. AI SDK v6 streaming occasionally emits a leading `\n` text-delta on tool-call-only turns; the literal newline passed `length > 0` and rendered an empty bubble + ActionRow between each tool call. (3) `MessageBubble.tsx:654` — same trim on `hasContent` (defensive, no-tool-calls path). (4) `payload.ts:64``buildMessagesPayload` skips assistant rows with `status='failed'` AND `status='complete' && empty content && no tool_calls`. Without this, a trailing empty/failed assistant + the next attempt's placeholder produced "Cannot have 2 or more assistant messages at the end of the list" rejections from the upstream API. (5) `budget.ts:11``BUDGET_NO_AGENT = 30` (was 15). No-agent mode shares the read-only-agent toolset at runtime; the cautious 15-cap was forward-looking for write tools that haven't landed. 190/190 tests still pass.|— |
**v1.13.2 deliberately deferred** — keep the dual-write through v1.13.4v1.13.11 as rollback insurance. Drop legacy columns last.
-----
## In flight (uncommitted on disk, 2026-05-21)
### Shipped (v1.13.x — written 2026-05-22, retagged same day)
v1.12.1 work — landed today, not yet committed:
All v1.13.x batches were retagged to the `vMAJOR.MINOR.PATCH-slug` scheme on 2026-05-22. `CHANGELOG.md` is the canonical per-tag record (slug describes what shipped; tag name alone recalls the batch). Tip is `v1.13.14-skills-audit` (`0fa46cd`); the next batch is `v1.13.15-codecontext-synth` (this batch, tag pending). Tags in chronological order:
| Item | Status | Notes |
|---|---|---|
| Server-side workspace pane sync | Done | `sessions.workspace_panes jsonb` column; PATCH endpoint; `session_workspace_updated` WS frame; localStorage migration on first load; deprecated `session_panes` table dropped |
| Richer status indicators | Done | Five states (`streaming` / `tool_running` / `waiting_for_input` / `idle` / `error`) with distinct visuals: amber orbiting dots for streaming, amber spinning ring for tool execution, blue static for waiting on user, emerald/gray/red for idle/error |
| Startup hung-row sweep | Done | `UPDATE messages SET status='failed' WHERE status='streaming' AND created_at < NOW() - INTERVAL '5 minutes'` on server boot |
| One stuck row from v1.12.0 smoke | Cleared | Manual UPDATE (`d63c25b1`) |
| `detectSameNameLoop` code path | Added, never fired | Candidate for revert in next batch — dead code |
| Diagnostic logging in inference.ts | Added for debugging | Must come out before commit |
- `v1.13.0-ai-sdk-v6` — AI SDK v6 migration; `streamCompletion` adapter; `messages_with_parts` view; reasoning_parts end-to-end
- `v1.13.1-cleanup-bundle``statement_timeout='30s'`, alpha-sorted tool registry, 60s stuck-row sweeper, `experimental_repairToolCall` pass-through
- `v1.13.2-compaction-prune` — two-tier prune; `message_parts.hidden_at` column + partial index; `messages_with_parts` view CASE refinement
- `v1.13.3-truncate` — opencode `truncate.ts` port; opaque `tr_<…>` id, `view_truncated_output(id)` tool, tmpfs storage
- `v1.13.4-reasoning-fix``<reasoning>` prose-prefix in compaction head-assembly for tool-bearing turns
- `v1.13.5-stability-bundle``includeUsage: true` on provider, `hasText` trim guard, `BUDGET_NO_AGENT` 15→30, trailing-empty-assistant filter
- `v1.13.6-prefix-stability``buildSystemPromptWithFingerprint` SHA-256 + per-session drift observer
- `v1.13.7-compaction-trigger` — overflow trigger lowered to `floor(0.85 × ctx_max)`
- `v1.13.8-tool-cost``tool_cost_stats` SQL view + per-tool rolling 100-call mean in AgentPicker
- `v1.13.9-agentlint` — instruction-file AgentLint pass; identity-openers removed; `CLAUDE.local.md` to .gitignore
- `v1.13.10-openspec``openspec/changes/<slug>/{proposal,tasks,design}.md` shape; archived batch docs preserved via `git mv`
- `v1.13.11-tools` — tiered tool loading via `BOOCODE_TOOLS` env (`core | standard | all`)
- `v1.13.12-ws-schemas` — Zod schemas for all 27 wire-format frames; `publishFrame` / `publishUserFrame` wrappers; parity test
- `v1.13.13-ws-publish` — all ~80 publish sites converted to the typed wrappers; every WS frame now Zod-validated at boundary
- `v1.13.14-skills-audit` — 26 skills vendored + audited via 5 parallel agent teams; 14 kept, 11 dropped, 1 migrated to BOOCHAT.md/BOOCODER.md
- `v1.13.15-codecontext-synth`**this batch, tag pending** — forced second-inference synthesis pass for codecontext overview tools
-----
The remaining strangler-fig final step (drop `messages.tool_calls` + `tool_results` columns) is still pending under its old `v1.13.2` working name; will get a new tag slug when scoped.
## v1.12.x cleanup (NEXT — small, immediate)
## In flight / next (v1.13.x cleanup line)
Five items. Group them or split them — your call.
Five more single-dispatch batches before the strangler-fig closes. Each ships independently with its own smoke and rollback surface. **Do not fold.** Order is locked:
### v1.12.1 — commit consolidation
### v1.13.8 — system-prompt prefix stability verify-and-measure (REFRAMED, 2026-05-22)
**Action items, in order:**
**Original plan:** add a `system_prompt_cache` DB table keyed by `(agent_id, project_id, skills_version)`, mtime-invalidated.
1. **Remove diagnostic logging** from `apps/server/src/services/inference.ts`. The 12 `ctx.log.info` calls added today proved the inference loop was functioning correctly; the prompts were just slow. Verbose for production. Strip them, keep the file clean.
**Why reframed:** recon disproved the premise. `apps/server/src/services/system-prompt.ts:buildSystemPrompt` already runs over mtime-cached inputs at the file layer:
2. **Revert `detectSameNameLoop`.** Three additions in inference.ts:
- `DOOM_LOOP_SAME_NAME_THRESHOLD = 5` constant
- `detectSameNameLoop()` function
- Call site in `runAssistantTurn` immediately after the existing `detectDoomLoop` check
Never fired in any real run today. Dead code. The existing `detectDoomLoop` (identical args, threshold 3) is sufficient.
- BOOCHAT.md / BOOCODER.md cached in `system-prompt.ts:25` (`cachedGuidance`, keyed by mtime)
- global + per-project AGENTS.md cached in `agents.ts:245` (`safeStat` pattern, 60s TTL)
- `session.system_prompt` / `project.default_system_prompt` are DB scalars (byte-stable until edited)
- BASE_SYSTEM_PROMPT is a hardcoded template with `${projectPath}` interpolation
3. **Drop the stale `messages_status_check` CHECK constraint** in `apps/server/src/schema.sql`. Two constraints exist on the table:
- `messages_status_check` allows `streaming|complete|failed` (old, stale)
- `messages_status_chk` allows `streaming|complete|failed|cancelled` (new)
The old one prevents `cancelled` from being written. Drop it with `ALTER TABLE messages DROP CONSTRAINT IF EXISTS messages_status_check;`.
Output assembly is a microsecond pure-string concat with no I/O. Skills aren't in the prefix (runtime discovery via `skill_find`). Tools live in a separate request body field, alpha-sorted by v1.13.3. **In theory the prefix is already byte-stable across turns; nothing has measured it.**
4. **Stop-handler writes terminal status.** When user clicks stop mid-stream, the abort path must `UPDATE messages SET status='cancelled' WHERE id = $assistantMessageId AND status='streaming'`. Currently rows just sit `streaming` forever. The startup sweep catches them on restart, but they should be written immediately. Edit `apps/server/src/services/inference.ts` `handleAbortOrError` to add the UPDATE.
**New scope — instrumentation only, no cache:**
5. **Commit + tag v1.12.1.** Include the workspace pane sync, status indicator overhaul, startup sweep, and items 14 above. Single commit per item is fine; tag at end.
1. SHA-256 fingerprint of `buildSystemPrompt`'s output logged per turn at `level=info`, msg `prefix-fingerprint`, with project_id / agent_id / session_id / prefix_hash / prefix_length / mtime fields.
2. Module-level `Map<sessionId, lastHash>` observer. On hash change for a known session → emit `prefix-drift` at `level=warn` with `prev_hash`, `new_hash`, and a field-level `changed_inputs` diff.
3. Unit-level byte-stability assertion in `system-prompt.test.ts`: two consecutive `buildSystemPrompt` calls with the same inputs return byte-identical strings.
**Estimated:** ~150 LoC net (deletions dominate).
**Decision criterion:** smoke 5 turns in a fresh session. 5 identical hashes + zero drift logs → close v1.13.8 as no-op, **drop the DB cache plan permanently**, move to v1.13.9. If drift surfaces → characterize the failure mode in a follow-up batch (the answer may not be a cache at all).
### v1.12.2 — live throughput display (small UX win)
**Doctrine:** matches the v1.13.6 audit pattern. Don't add infrastructure without a proven cache miss. The v1.12.0 mtime caches at the input layer plus alpha tool ordering at the request body layer already address the load-bearing cache-stability surfaces.
Surface `tokens_per_second` and `ctx_used` next to the status indicator while streaming. Backend already emits these in the `usage` frame; just consume them in the StatusDot wrapper or a sibling component. ~80 LoC, frontend-only.
**Dispatch brief:** `handoff_v1.13.8_prefix_verify.md`.
### v1.12.3 — stale-stream frontend banner
**Estimated:** ~95 LoC (system-prompt.ts + small `getAgentsMtimes` accessor in agents.ts + 3 new tests).
When a chat has a `streaming` row older than ~60s with no new tokens, the UI should surface a "Previous response didn't complete. [Retry] [Discard]" banner instead of silently queueing new sends. Today's debugging spent four hours misreading slow streams as dead; this is the UX fix that prevents that. ~150 LoC, frontend + small backend endpoint for the discard action.
### v1.13.9 — compaction overflow trigger formula
-----
opencode pattern: `0.85 * ctx_max` early trigger (not at 100% saturation). Reduces tail-loss risk and gives compaction a safer window. Tiny change but tied to v1.13.4's tier logic — sequence matters.
## v1.13 — Phase B: parts table + AI SDK + per-tool tagging
**Lift source:** `anomalyco/opencode` `session/overflow.ts`.
**Goal:** typed message parts replace JSON blobs on `messages.tool_calls` / `tool_results`. Adopt Vercel AI SDK `streamText`. Tag tools as `read_only` or `write` at definition time.
**Estimated:** ~30 LoC.
### v1.13.10 — per-tool token cost accounting
Rolling average per tool, surfaced in AgentPicker tooltip + agent-pick decisions. Backend tracks `(tool_name, prompt_tokens_in, completion_tokens_out)` per call; surfaces a 100-call rolling mean. Frontend reads it for tool-cost hints. **Depends on v1.13.7's `includeUsage` fix** — without real token numbers in DB rows, the rolling average is empty.
**Estimated:** ~250 LoC.
### v1.13.11 — WebSocket frame typing
Zod schemas validated both ends. Catches the recurring class of bug that drove the 2026-05-21 debugging spike (silent protocol drift). Upfront work that pays back every time the protocol changes. `chat_status`, `usage`, `parts_appended`, `session_workspace_updated`, `tool_running` — every frame gets a Zod schema, every send/receive site validates.
**Estimated:** ~300 LoC.
### v1.13.12 — skills audit pass (NEW, 2026-05-22)
**Goal:** apply the rules→recipes split (per Codeminer42 activation-gap data: plain skills invoke 6% in clean multi-turn, `CLAUDE.md`/`AGENTS.md` is 100% present) to BooCode's 7 vendored v1.12 skills. Sort each into: (a) move to `AGENTS.md` as always-true rule, (b) keep as recipe invoked via `/skill <name>`, (c) move bulky context into `references/` flat subdirectory inside the skill, (d) delete (Claude already does it reliably).
**Scope:**
1. Schema: new `message_parts` table (`id, message_id, kind, payload JSONB, sequence`). Kinds: `text`, `tool_call`, `tool_result`, `reasoning`, `step_start`. The `messages` table becomes header-only.
2. Inference loop rewritten on AI SDK `streamText`. `streamCompletion` becomes a thin wrapper. Native AI SDK `experimental_repairToolCall` replaces v1.12's hand-rolled version.
3. Tool registry: `ToolDef<T>` gains `category: 'read_only' | 'write'` field. BooCode v1.x rejects any `write` tool at registry time (defense in depth for the BooCoder split). Alpha-sort tool list before sending to model (prompt-cache stability).
4. Reasoning content (`reasoning_content` from Qwen3.6) captured as its own part type instead of dropped or inlined.
1. **Audit each of the 7 vendored skills against the 4-way split.** Most workflow-rule content ("always do X before Y", "never do Z") moves to `AGENTS.md` since it should be 100% present. Recipe content ("here's how to scaffold a component", "here's the release checklist") stays as skill, gets `context: fork` if heavy.
1. **Adopt Anthropic best-practices conventions** for any skills that remain after audit: gerund names (`scaffolding-components`, not `component-helper`), SKILL.md ≤500 lines, references one level deep, third-person imperative voice, MCP tool references in `ServerName:tool_name` format, no Windows-style paths, no time-sensitive info, consistent terminology, no "voodoo constants."
1. **Run each remaining skill through the 4-step validation protocol** from `mgechev/skills-best-practices` (Discovery → Logic → Edge Case → Architecture Refinement) using a fresh Claude chat per step. Prompts are paste-ready; ~10 minutes per skill.
1. **Install `skillgrade` on Sam's host** (`npm i -g skillgrade`). For each remaining skill, write a minimal `eval.yaml` with 23 tasks and run `skillgrade --smoke` (5 trials, ~5 min) to confirm the skill triggers when expected and produces correct output. **Likely outcome: some skills show 020% trigger rate — confirms they belong in AGENTS.md, not as skills.**
1. **Document the rules→recipes split as a BooCode convention** in `BOOCODER.md` / `BOOCHAT.md`. Future-proofs against re-adding workflow rules as skills.
**Migration risk:** non-trivial. `inference.ts` is ~1700 lines with custom XML fallback, SSE parsing, compaction integration. Plan dedicated cutover window. `compaction.ts` must update to assemble head from parts.
**Lift sources:**
**Replaces:** Original Batch 13 (append-only event log) — same outcome, different vocabulary.
- `blog.codeminer42.com/stop-putting-best-practices-in-skills/` — empirical 6%/33%/66%/100% invocation-rate data with Vercel-style multi-turn methodology. The activation-gap framing.
- `mgechev/skills-best-practices` (25 stars, MIT) — 4-step validation protocol with paste-ready prompts. Directory structure conventions.
- `mgechev/skillgrade` (132 stars, MIT) — agent-agnostic skill eval framework. `eval.yaml` task+grader schema. Smoke/reliable/regression presets.
- `platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices` — canonical Anthropic standard. 500-line ceiling, gerund naming, progressive disclosure patterns, MCP tool reference format, verification checklist.
**Today's debugging spike validates this work.** Four hours of confusion came from JSON-blob `tool_calls` / `tool_results` columns hiding state from logs and from the inference state machine being invisible. Typed parts + per-part status would have shown the slow-stream-vs-dead distinction in seconds.
**Dependencies:** none (the 7 v1.12 skills already exist; this is an audit pass on shipped material). Can ship at any point in the v1.13.x line.
**Dependencies:** v1.12.x cleanup merged.
**Estimated:** zero code changes, ~one evening of audit work, plus skillgrade install. Per-skill eval.yaml authoring is ~30 min per skill including the 4-step validation. Total roughly 56 hours of focused work for all 7 skills.
**Estimated:** ~1500 LoC.
### v1.13.2 — drop legacy columns (final phase of strangler-fig)
**Wait at least one week of production traffic on v1.13.1 before shipping.** The dual-write is rollback insurance. Drop the columns and that rollback is gone.
**Verification query before shipping:**
```sql
SELECT
COUNT(*) FILTER (WHERE m.tool_calls IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM message_parts p WHERE p.message_id = m.id AND p.kind = 'tool_call'
)) AS missing_tool_call_parts,
COUNT(*) FILTER (WHERE m.tool_results IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM message_parts p WHERE p.message_id = m.id AND p.kind = 'tool_result'
)) AS missing_tool_result_parts
FROM messages m
WHERE m.created_at > '2026-05-22'::timestamptz;
```
Both columns must read 0.
**Scope (~150 LoC, mostly deletions):**
1. Remove dual-write from every v1.13.0 site: `tool-phase.ts` (3 sites), `finalizeCompletion`, `skills.ts` (2 sites), `messages.ts` answer flow, `chats.ts` (fork). Keep only the parts write.
1. Simplify `messages_with_parts` view — drop COALESCE fallbacks since legacy columns are about to disappear.
1. `ALTER TABLE messages DROP COLUMN tool_calls, DROP COLUMN tool_results`.
1. Remove `tool_calls`/`tool_results` fields from `Message` API type. API boundary unchanged (frontend already reads parts-derived values).
1. Drop the stale `messages_status_check` cleanup DO block from v1.12.1 schema if still present.
1. Update test fixtures in `inference.test.ts` and `compaction.test.ts` to construct parts instead of inline `tool_calls: null, tool_results: null` literals. ~30 fixture rewrites.
After v1.13.2 ships, tag the umbrella `v1.13` on the same commit (or on -C — Sam's call).
-----
@@ -132,9 +209,17 @@ When a chat has a `streaming` row older than ~60s with no new tokens, the UI sho
**Scope:**
1. Outer loop continues until model returns non-tool finish OR step cap hit. Step ≠ tool call: one step can contain multiple tool calls in parallel.
2. `agent.steps ?? Infinity` per-agent step cap. AGENTS.md gains `steps:` field. Refactorer `steps: 5`, Architect `steps: 20`, etc.
3. Step-boundary events (`step_start`, `step_finish`) explicit in the parts stream. Per-step snapshot for revert (planned for BooCoder; backend-only in v1.14).
4. Doom-loop guards (v1.11.6) migrate from "abort recursion" to "raise within loop iteration." Same predicate, different control flow.
1. `agent.steps ?? Infinity` per-agent step cap. AGENTS.md gains `steps:` field. Refactorer `steps: 5`, Architect `steps: 20`, etc.
1. Step-boundary events (`step_start`, `step_finish`) explicit in the parts stream. Per-step snapshot for revert (planned for BooCoder; backend-only in v1.14).
1. Doom-loop guards (v1.11.6) migrate from "abort recursion" to "raise within loop iteration." Same predicate, different control flow.
**Lift sources:**
- `anomalyco/opencode` `session/prompt.ts` `runLoop()` outer agent loop
- `anomalyco/opencode` `agent.steps` per-agent step cap
- AGENTS.md extensions for `steps`, `output_schema` (Qodo agent.toml pattern), `exit_expression` (Qodo pattern), `execution_strategy` (Qodo plan/act)
- **Reference:** RA.Aid three-stage Research/Planning/Implementation as AGENTS.md design principle; expert-tool escape hatch pattern (most subtasks on routine model, escalate to qwopus27b only when needed)
- **Reference:** Roo Code Boomerang Tasks — orchestrator-with-capability-restriction pattern. Adopt as AGENTS.md design principle (orchestrator role can call only dispatch tools, no file reads / MCP / shell).
**Dependencies:** v1.13 merged.
@@ -142,28 +227,119 @@ When a chat has a `streaming` row older than ~60s with no new tokens, the UI sho
-----
## v1.15 — Phase D: permission ruleset + MCP client
## v1.14.x-mcp — single-server MCP-client proof-of-concept (NEW, 2026-05-22)
**Goal:** validate the MCP-client loop end-to-end against one real MCP server before committing to the full opencode `mcp/index.ts` port at v1.15. Small, throwaway-if-needed, slots between v1.14 and v1.15 without disrupting either.
**Scope:**
1. Add a hardcoded MCP client (single server) to BooChat. Initial target: **Context7** (Sam already uses it via opencode, so the config is known to work). Remote HTTP transport at `https://mcp.context7.com/mcp` with optional `CONTEXT7_API_KEY` header.
1. Use the official `@modelcontextprotocol/sdk` TypeScript client. No SSE transport yet (deferred to v1.15). Stdio transport not needed for Context7.
1. Tool discovery on startup: `tools/list`. Tools surface in BooChat alongside `view_file`/`grep`/etc., prefixed `context7_*` to avoid collisions.
1. **Read-only invariant guard:** the client must reject any MCP tool whose `annotations.readOnly` is false (or absent). Fail-closed. This is BooChat-specific defense-in-depth — v1.15 lifts this restriction for BooCoder.
1. Per-server `enabled` flag in `agents.ts`. No glob patterns yet.
1. **No OAuth.** Context7 supports an API key header; that's it for v1.14.x. OAuth lands in v1.15.
**What this proves:**
- MCP protocol loop works end-to-end against a real server in BooCode's Fastify backend.
- Tool-discovery → tool-list → tool-call → result-render → context-budget accounting all hold.
- Read-only enforcement at the client layer is sound.
- Config schema shape is right before v1.15 commits to the opencode-compatible JSON config.
**What this does NOT do:**
- No SSE transport. (v1.15.)
- No OAuth flow. (v1.15.)
- No multiple servers. (v1.15.)
- No per-agent server allow/deny. (v1.15.)
**Dependencies:** v1.13 merged (parts table for tool-call/tool-result emission).
**Estimated:** ~150 LoC.
**Skip-condition:** if v1.14 finishes and Sam wants to leap straight to v1.15, fold this into the early steps of v1.15.
-----
## v1.14.x-html — HTML artifacts in BooChat (NEW, 2026-05-22)
**Goal:** integrate Thariq Shihipar's "HTML > Markdown for agent output at length" pattern (`claude.com/blog/using-claude-code-the-unreasonable-effectiveness-of-html`, May 20 2026) into BooChat. Bias the model toward HTML for outputs >100 lines: information density, visual clarity, interactive controls (sliders/knobs/SVG diagrams/side-by-side comparisons), shareability. BooChat already renders into a webview, so the surface fit is unusually good.
**Scope:**
1. **Model-side prompting** (no code change yet, just AGENTS.md guidance):
- Add HTML-bias rule to global `AGENTS.md`: "For outputs >100 lines, default to a self-contained `<!DOCTYPE html>...</html>` artifact unless the user explicitly asks for Markdown. For outputs <100 lines or for short conversational replies, stay in Markdown."
- Reasoning shown in the rule: HTML carries diagrams, tabs, illustrations, code-with-syntax-highlighting, interactive controls, mobile-responsive layouts. Markdown is restrictive at any length.
- Cite Thariq's blog post in the rule comment so future audit passes know where it came from.
1. **Detection at the BooChat backend.** In `apps/chat/services/inference/stream-phase.ts` post-processing: detect any assistant text part starting with `<!DOCTYPE html>` (case-insensitive, whitespace-trimmed) — or wrapped in a fenced ` ```html` block — and tag it as an HTML artifact. Emit a new part kind `html_artifact` into `message_parts` (CHECK constraint update). Payload: `{html_content, char_count, title}`. Title pulled from `<title>` tag or first `<h1>` if available.
1. **Three render targets (Sam's pick: "3 with a download"):**
- **Inline preview** in the chat stream: small sandboxed iframe (~400px tall), renders the artifact next to where it was streamed. Default size, click-to-expand.
- **Open in pane**: button on the inline preview opens the artifact in a full-height pane in BooChat's existing workspace splitter, alongside the file viewer and BooTerm. Pane is dismissible. Pane state persisted via `sessions.workspace_panes jsonb` (the v1.12.1 schema already supports this).
- **Download**: button writes the artifact to `/opt/<project>/.boocode/artifacts/<slug>-<unix-timestamp>.html` (path-guarded same as native write tools), surfaces an OS download link via the existing file-serving path. Filename slug derived from artifact title.
1. **Security stance — locked 2026-05-22:** the iframe is sandboxed with `sandbox="allow-scripts allow-clipboard-write allow-downloads"`. **Crucially, omit `allow-same-origin`** so the artifact has its own opaque origin and cannot read BooChat's cookies, Authelia session, or DOM. Backend serves the iframe content via `srcdoc=...` inline (not `src=`) so no separate URL exists to disclose. CSP header on the iframe response: `default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'`. The `connect-src 'none'` is the key clause — artifacts can't `fetch()`, can't open WebSockets, can't ping a tracking pixel, can't exfiltrate. JS runs (so Thariq's interactive knobs/sliders/copy-as-prompt buttons work) but nothing else network-touching does. **None of Thariq's blog examples need the relaxed permissions** — they're all client-side.
1. **Frontend rendering** (`apps/web/src/components/HtmlArtifactPart.tsx`):
- Inline preview: `<iframe srcdoc={html_content} sandbox="allow-scripts allow-clipboard-write allow-downloads" className="..." />` with the strict-sandbox attributes above.
- "Open in pane" button: dispatches workspace-pane action with `{type: 'html_artifact', message_part_id, html_content}`.
- "Download" button: POST to new endpoint `/api/chats/:id/artifacts/:part_id/download` which writes to disk (path-guarded) and returns the absolute path or pre-signed URL for the existing static-file serving route.
1. **No artifact persistence beyond the chat.** Artifacts live in `message_parts.payload->>'html_content'` with the chat. Downloads go to `/opt/<project>/.boocode/artifacts/` and are user-managed from there. No separate artifacts table.
1. **Token-budget guard.** Single artifact can be at most 1MB of HTML in `message_parts.payload`. Larger triggers a streaming abort with a friendly error: "Artifact exceeded 1MB; consider splitting into multiple files or reducing inline assets."
1. **No `web-artifacts-builder` skill vendor.** That skill (`anthropics/skills/web-artifacts-builder`) is built for Claude.ai's runtime with Vite + Parcel + tspaths + html-inline toolchain. BooChat has no shell execution surface. The pattern transplants; the toolchain doesn't. Treat the skill's "avoid AI slop" design principles (no excessive centered layouts, no purple gradients, no uniform rounded corners, no Inter font) as conventions inlined in the HTML-bias AGENTS.md rule. The init/bundle scripts are out of scope.
**Lift sources:**
- `claude.com/blog/using-claude-code-the-unreasonable-effectiveness-of-html` (Thariq Shihipar, May 20 2026) — the pattern, the use-case taxonomy (specs/code-review/design/reports/custom editors), the design philosophy.
- HTML iframe sandbox spec (web platform standard, no license issues).
- `anthropics/skills/web-artifacts-builder` — design-principle reference only ("avoid AI slop" rules). **Do not vendor the toolchain.**
**Dependencies:** v1.13 merged (`message_parts` table is where artifacts live). Independent of v1.14 (outer loop) and v1.14.x-mcp (MCP PoC). Can ship in any order relative to those.
**Estimated:** ~400 LoC. Roughly half backend (detection + part-kind extension + download endpoint + path-guard integration), half frontend (HtmlArtifactPart component + pane integration + download button wiring).
**Schema addition:**
- `message_parts.kind` CHECK constraint adds `'html_artifact'` to the allowed set.
**Skip-condition:** none — independent batch, ships clean any time after v1.13. Highest user-visible payoff of any v1.13.x/v1.14.x batch (transforms what the model can produce, not just how the backend handles it).
-----
## v1.15 — Phase D: permission ruleset + full MCP client
**Goal:** wildcard permission ruleset (opencode `evaluate.ts` pattern) and a proper MCP client implementation. Foundation for BooCoder to gate writes; immediate value for codecontext to be re-wired as a real MCP server.
**Scope:**
1. Wildcard rule matcher: `{ permission, pattern, action: 'allow' | 'deny' | 'ask' }`. Last-match-wins. Per-agent rulesets layer under per-session rulesets.
2. MCP client implementation: SSE transport, `tools/list` discovery, `tools/call` invocation. codecontext sidecar gets re-pointed from static wrappers (v1.12) to real MCP. New connectors become a config-only addition.
3. UI: permission-ask flow when a tool requires `ask` action. Modal or inline card with Allow once / Allow always / Deny.
4. v1.x stays read-only by default (no `write` tools in the registry yet).
1. **Full MCP client implementation:** stdio (local subprocess) + SSE (remote HTTP) transports, `tools/list` discovery, `tools/call` invocation, OAuth via Dynamic Client Registration (RFC 7591), per-server enabled flag, **glob patterns for per-agent tool whitelisting** (matching opencode's `tools` config shape).
1. codecontext sidecar gets re-pointed from static wrappers (v1.12) to real MCP. New connectors become a config-only addition.
1. UI: permission-ask flow when a tool requires `ask` action. Modal or inline card with Allow once / Allow always / Deny. Reuses v1.9.7 elicitation surface.
1. BooChat stays read-only by default — the read-only invariant guard from v1.14.x carries forward (defense-in-depth even with the ruleset).
1. **Config shape: match opencode's JSON schema near-verbatim** so any opencode user can copy `mcp` blocks from `~/.opencode/config.json` into BooCode unchanged. Schema is not copyrightable; matching it is pure interoperability.
**v1 MCP scope limit (security):** local-stdio MCP servers and Context7-style API-key remote servers only. **Remote MCP servers requiring OAuth tokens are deferred** until BooCode has a real secret-storage primitive (sops-encrypted entries, Vault sidecar, or OS keyring). Reason: MCP OAuth tokens are bearer credentials for third-party services; storing them in plaintext PostgreSQL inside the BooCode DB widens the attack surface significantly if Authelia is bypassed. v1.15 ships the OAuth code path but the config schema rejects OAuth servers until secret storage lands.
**Absorbs:** Original Batch 12 (tool approval + plan/act mode) — same outcome via permission rules instead of mode enum.
**Lift sources:**
- `anomalyco/opencode` `permission/evaluate.ts` wildcard ruleset
- `anomalyco/opencode` `mcp/index.ts` MCP client (SSE transport, tools/list, tools/call, OAuth RFC 7591)
- `cline/cline` plan/act invariant — read-only mode pattern (absorbed)
**Dependencies:** v1.13 merged (parts table for permission events). Independent of v1.14.
**Estimated:** ~600 LoC.
-----
## v1.16 — Batch 11b: codesight repo_health
## v1.16 — codesight repo_health
Call graph, circular dependency detection, dead code flagging. Port `analyze.mjs` from spirituslab/codesight. New tool `repo_health(project_id)`. In-process Node (not sidecar). Cache results keyed by `(project_id, file_hashes_sig)`.
Call graph, circular dependency detection, dead code flagging. Port `analyze.mjs` from `spirituslab/codesight`. New tool `repo_health(project_id)`. In-process Node (not sidecar). Cache results keyed by `(project_id, file_hashes_sig)` in new `repo_health_cache` table.
Independent batch — ships clean any time after v1.13. Low leverage unless Sam actually uses the dead-code / circular-dep output.
**Lift source:** `spirituslab/codesight` `analyze.mjs`. Drop VS Code wrapper.
**Dependencies:** v1.12 merged (can reuse codecontext parse output where overlapping).
@@ -171,23 +347,72 @@ Call graph, circular dependency detection, dead code flagging. Port `analyze.mjs
-----
## v2.0 — BooCoder pending changes
## v2.0 — BooCoder: pending changes + dual execution paths + ACP host + MCP server
New container `boocoder` at `100.114.205.53:9502`. Owns write tools (`edit_file`, `create_file`, `delete_file`, `apply_pending`, `rewind`). Edits queue in `pending_changes` table; nothing touches disk until `/apply`. Per-pane diff UI with Approve/Reject. BooCode chat stays read-only (`/opt:/opt:ro`).
**Major version bump.** New app `apps/coder/` inside the existing monorepo (not a separate repo). Lands together with the `boocode_db``boochat_db` DB rename and the per-app subdomain split (`code.indifferentketchup.com` → BooChat, `coder.indifferentketchup.com` → BooCoder).
**Lift source:** plandex pending-changes data model.
**Three protocol roles in one surface:**
**Dependencies:** v1.13 (parts) + v1.15 (permissions).
1. **MCP client (write-capable allowed).** Inherits the v1.15 client unchanged. BooCoder can enable write-capable MCP servers (`@modelcontextprotocol/server-filesystem` write tools, git commit MCP servers, etc.). All MCP writes route through the same `pending_changes` queue as native writes. Per-task allow/deny means dispatched tasks can have a different MCP roster than the interactive shell.
1. **MCP server (BooCoder's own primitives).** New `apps/coder/services/mcp_server.ts` 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 for remote (deferred until OAuth + secret storage). **This is what makes external opencode-on-the-host BooCoder-aware.**
1. **ACP client (host).** Replaces the raw-PTY dispatch path for ACP-capable agents. Spawns `opencode acp` and `goose acp` as JSON-RPC stdio subprocesses. Native session lifecycle, mid-session model/mode switching, file-operation events surfaced as diffs in the BooCoder UI, terminal events that route into BooTerm, permission prompts answered via real dialogs. **MCP servers configured in BooCoder are auto-forwarded to the dispatched ACP agent** (per goose docs — `context_servers` is the field name). One MCP config drives every dispatched agent.
**Estimated:** ~1200 LoC.
**Two execution paths, same surface (the answer to the May 18 "1 and 2 full featured" question):**
### Path A — in-process write-tool inference loop (Option B / native)
- New write tools: `edit_file`, `create_file`, `delete_file`, `apply_pending`, `rewind`.
- Edits queue in `pending_changes (id, session_id, file_path, diff TEXT, status, created_at)`. Nothing touches disk until `/apply`.
- Per-pane diff UI with Approve/Reject.
- Path-guard layer (`apps/coder/services/path_guard.ts`) enforces per-project scoping using the v1.15 permission wildcard ruleset. Blanket `/opt:rw` mount, policy at the tool layer. **Highest-priority test target: fuzz the path-guard against every traversal-attack pattern, including MCP-served filesystem writes.**
**Lift source:** `plandex-ai/plandex` pending-changes data model and diff/apply/rewind UX vocabulary.
### Path B — ACP/PTY dispatch to external CLI agents (Option A / dispatch)
- New tool `dispatch_external_agent(agent: 'opencode'|'claude'|'goose'|'pi', model: string, task: string, worktree: string)`.
- **Primary path: ACP subprocess** for agents that support it (opencode `opencode acp`, goose `goose acp`). JSON-RPC over stdio. Native session/tool/file/terminal events.
- **Fallback path: raw PTY** for claude/pi/smallcode via `node-pty` with `cwd = /opt/<project>` or a `git worktree add /tmp/booworktrees/<session-id>` worktree per dispatch.
- Dispatch worker checks `available_agents.supports_acp` at runtime and picks the right transport. Same task table, same project registry, same pending-changes flow.
- Captures stdout/stderr/exit-code into PostgreSQL stream tables (PTY path) or maps ACP events to the parts taxonomy (ACP path). WebSocket events surface to all three React surfaces.
- One worktree per active dispatched session.
- User picks per task via UI dropdown at task creation, or the in-process loop calls `dispatch_external_agent` itself.
**Lift sources:**
- `Dominic789654/agent-hub` (Apache-2.0) — task DAG schema, dispatcher worker, project registry, human inbox. **Primary architectural template.**
- `getpaseo/paseo` (AGPL-3.0, **design only — no code lift**) — daemon+clients architecture, `--worktree feature-x` flag, `paseo run/ls/attach/send` CLI verb shape, `/handoff` `/loop` `/orchestrator` skills concept.
- Roo Code Boomerang Tasks pattern — orchestrator capability restriction + down-pass/up-pass context discipline (`new_task` message, `attempt_completion` result, no implicit inheritance) + explicit precedence override clause.
- `covibes/zeroshot` blind-validation invariant — verify gate runs in separate agent context that only sees the diff and acceptance criteria, not the producing conversation.
- **ACP spec** (`agentclientprotocol.com`) — local-subprocess ACP via stdio JSON-RPC. Remote ACP (HTTP/WS) is still work-in-progress per the spec maintainers; v2.0 uses stdio only.
- **Goose ACP docs** (`goose-docs.ai/docs/guides/acp-clients/`) — `context_servers` auto-forward pattern. Critical: one MCP config drives every dispatched agent.
### Shared infrastructure between A and B
- `tasks` table (id, project_id, template_id, parent_task_id, state, input, output_summary, dependencies, agent, model, worktree_path, cost, started_at, ended_at)
- `task_templates` table (reusable spec → task instantiations)
- `pipelines` table + `pipeline_runs` (ordered template invocations)
- `available_agents` table (name, install_path, version, supports_acp, supports_mcp_client, last_probed_at) — populated by startup probe (`which opencode && opencode --version`, etc.)
- `human_inbox` view (state IN ('blocked', 'failed', 'needs_human'))
- Worker process `boocoder-dispatcher` (systemd unit alongside Fastify): picks ready tasks, dispatches via A or B (and within B, ACP or PTY), captures output, marks state.
- New `boocode` CLI as a thin WebSocket/HTTP client against the BooCoder API. Verbs: `boocode run`, `boocode ls`, `boocode attach <id>`, `boocode send <id>`. Mirrors Paseo's UX, license-clean implementation.
- BooCoder-internal MCP server (see role 2 above) registered on the Fastify server alongside the existing HTTP/WS endpoints. Stdio transport for opencode-in-Termius; HTTP transport gated on OAuth + secret storage.
**MCP server eval requirement:** run BooCoder's internal MCP server through the **anthropics `mcp-builder` skill's 10-question evaluation framework** before shipping. Ten independent, read-only, complex questions with verifiable answers in XML format. If the eval doesn't pass, the MCP server isn't shippable.
**Dependencies:** v1.13 (parts table) + v1.14 (outer loop + step boundaries for revert snapshots) + v1.14.x (MCP-client PoC) + v1.15 (full MCP client + permissions for path-guard policy).
**Estimated:** ~1500 LoC for Path A + Path B + shared schema, plus ~400 LoC for the MCP-server role, plus ~300 LoC for the ACP-client role. Multiple sub-versions: v2.0.0 native + ACP, v2.0.1 MCP server, v2.0.2 polish.
-----
## v2.1 — BooCoder runtime isolation
## v2.1 — BooCoder runtime isolation (optional)
Per-session Docker sandbox spawned by BooCoder on first write. Only project path mounted, not `/opt`. Idle-timeout 30 min. Standard OpenHands runtime contract: HTTP API inside container, BooCoder calls in.
**Lift source:** OpenHands V1 runtime pattern.
**Skip-condition:** if the v2.0 path-guard layer holds up under fuzzing + a few months of production use, runtime isolation becomes optional hardening rather than necessary defense. Track but don't commit.
**Lift source:** `OpenHands/OpenHands` V1 runtime pattern.
**Dependencies:** v2.0.
@@ -195,24 +420,64 @@ Per-session Docker sandbox spawned by BooCoder on first write. Only project path
-----
## v2.2 — BooCoder as ACP agent (driveable from external editors)
**Goal:** expose `boocoder acp` so Zed, JetBrains, Avante.nvim, CodeCompanion.nvim can drive BooCoder as their agent. Outbound exposure of the BooCoder write-tool surface to ACP-compatible editors.
**Scope:**
1. New ACP server entry point: `boocoder acp` reads JSON-RPC over stdio, exposes BooCoder's task primitives as ACP sessions.
1. BooCoder UI features remain optional: editor drives session via ACP; pending-changes queue still gates writes; user can approve/reject from either BooCoder's web UI or the editor's permission dialog (whichever responds first).
1. Same auth model as the rest of BooCoder — editor must be reachable on the Tailscale mesh, or BooCoder is invoked with a short-lived token.
**Why this is v2.2, not v2.0:** outbound ACP-agent role is cheap once the inbound ACP-client side is implemented (same protocol library, server side), but it's a *different product surface* — driving BooCoder from external editors. Ship it after BooCoder's own surface stabilizes.
**Lift source:** `zed-industries/codex-acp` (Apache-2.0) as a server-side ACP reference implementation.
**Dependencies:** v2.0 + v2.1 (recommended; ACP-driven sessions inside a sandbox are stronger).
**Estimated:** ~400 LoC.
-----
## v2.x — Optional / far future
- **Verify gate above pending-changes** — `augmentcode/augment-swebench-agent` majority-vote ensembler pattern (K candidate diffs → ranker model picks winner). JSONL schema only, no code lift. Combine with zeroshot blind-validation invariant. v2.0+ optional batch.
- **PR-resolver tool** — `qodo-ai/qodo-skills` PR-resolver state machine (fetch issues → batch/interactive fix → inline reply). BooCoder v2.0+.
- **Record/replay LLM harness for tests** — `qodo-ai/qodo-cover` pattern (hashed prompt → fixture YAML). Re-implement in Vitest, don't vendor (AGPL). v1.13+ test infrastructure.
- **HMAC-chained audit log** — `sipyourdrink-ltd/bernstein` pattern. Small lift, adds tamper-evident session history. v1.13+ optional.
- **Tiered tool loading** — `eyaltoledano/claude-task-master` pattern (env var: `core` / `standard` / `all`). ~30 LoC in `agents.ts`. Pattern-only lift (claude-task-master is MIT + Commons Clause; reimplement). v1.13.x or v1.14.
- **Spec directory structure** — `Fission-AI/OpenSpec` `openspec/changes/<name>/{proposal,specs,design,tasks}.md` shape for BooCode's own batch docs. Zero-dep documentation reformat, replaces ad-hoc `boocode_batchN.md` convention. v1.13.x or v1.14.
- **`view_session_history` MCP tool** — `memovai/memov` `snap`/`mem_history`/`validate_commit` shape. Reference design for v1.13+ session-history feature.
- **`taste-skill` anti-slop ban list** — vendor `Leonxlnx/taste-skill` SKILL.md after diff against existing `frontend-design` skill. Real value at v2.0+ when BooCoder generates frontend code (DubDrive, BooLab, Fathom).
- **AgentLint audit pass** — manual review of BooCode's own CLAUDE.md/AGENTS.md/BOOCHAT.md/BOOCODER.md using `0xmariowu/AgentLint`'s 31 evidence-backed checks. Trim emphasis-keyword density, hit 60120 line sweet spot, SHA-pin Actions, ensure `.env`/`CLAUDE.local.md` are gitignored. One-evening pass, immediate ROI. Optional plugin install at v1.12.x post-merge for ongoing audits.
- **`budi` install (Sam's host)** — `siropkin/budi` Claude Code 5-hook observer (`SessionStart`/`UserPromptSubmit`/`PostToolUse`/`SubagentStart`/`Stop`). Local SQLite, sub-ms hook latency, dashboard at `localhost:7878`. Not a BooCode lift — install globally for Claude Code session observability.
- **Multi-provider LLM** (pi-ai pattern): Only if a concrete need for Anthropic / OpenAI / Mistral direct surfaces. llama-swap covers everything today.
- **Workflow graphs** (microsoft/agent-framework concepts): Multi-agent coordination. Conceptual reference only. Realistically a v3.x topic.
- **Secret storage primitive (prerequisite for remote OAuth MCP servers).** Pick between: sops-encrypted entries in PostgreSQL, HashiCorp Vault sidecar, or OS-level keyring on `ubuntu-homelab` accessed via a thin service. Unblocks remote OAuth MCP servers in BooCode generally. v2.x or earlier if a remote OAuth server (Sentry, Atlassian, etc.) becomes urgent.
-----
## Architecture target state
### Containers
### Containers (post-v2.0)
| Container | Port | Mount | Purpose | Status |
|---|---|---|---|---|
| `boocode` | `100.114.205.53:9500` | `/opt:/opt` | Chat + read-only tools + SPA | Live |
| `boocode_db` | `127.0.0.1:5500` | `boocode_pgdata` volume | Postgres 16-alpine | Live |
| `booterm` | `100.114.205.53:9501` | `/opt/repos:/opt/repos:rw` | Terminals (tmux + node-pty) | Live (v1.10.0) |
| **`codecontext`** | **`:8765` (internal)** | **`/opt/projects:/workspace:ro`** | **MCP server for architect tools** | **Live (v1.12.0)** |
| `boocoder` | `100.114.205.53:9502` | per-session sandbox | Write tools | v2.0 |
|Container |Port |Mount |Purpose |Status |
|-------------------------------|---------------------|-----------------------------|------------------------------------------------------------------------|----------------------|
|`boochat` (was `boocode`) |`100.114.205.53:9500`|`/opt:/opt:ro` |Read-only chat + SPA host + MCP client |Live (renames at v2.0)|
|`booterm` |`100.114.205.53:9501`|`/opt:/opt` |PTY/tmux terminal sessions |**Live (May 2026)** |
|`boocoder` |`100.114.205.53:9502`|`/opt:/opt:rw` (policy-gated)|Write tools + ACP host + MCP client + MCP server + external-CLI dispatch|v2.0 |
|`boochat_db` (was `boocode_db`)|`127.0.0.1:5500` |`boocode_pgdata` volume |Postgres 16-alpine (shared by all three) |Live (renames at v2.0)|
|`codecontext` |`:8765` (internal) |`/opt/projects:/workspace:ro`|MCP server for architect tools |**Live (v1.12.0)** |
### Caddy routing target (post-v2.0)
```
code.indifferentketchup.com → boochat :9500 (SPA + chat API + MCP client)
coder.indifferentketchup.com → boocoder :9502 (SPA + write API + MCP client + MCP server HTTP)
coder.indifferentketchup.com/mcp → boocoder :9502 (BooCoder MCP server endpoint, when remote-MCP unlocked)
term.indifferentketchup.com → booterm :9501 (or routed under code.*/term/)
```
### Schema additions by version
@@ -220,52 +485,178 @@ Per-session Docker sandbox spawned by BooCoder on first write. Only project path
- **v1.11.7:** none (pathGuard logic, no DB)
- **v1.12.0:** none (codecontext stateless; truncation in-memory id-map with TTL cleanup)
- **v1.12.1:** `sessions.workspace_panes jsonb` (workspace sync); drop deprecated `session_panes` table; drop stale `messages_status_check` constraint
- **v1.13:** `message_parts` table; `messages` becomes header-only
- **v1.13.0-ai-sdk-v6:** `message_parts (id, message_id, sequence, kind, payload jsonb, created_at)` + unique `(message_id, sequence)` + `kind` CHECK; `messages_with_parts` view with COALESCE fallbacks; `ToolDef.category` field (TS type, not DB)
- **v1.13.1-cleanup-bundle:** `ALTER DATABASE boocode SET statement_timeout = '30s'` (op step, documented in schema.sql; doesn't survive volume reset)
- **v1.13.2-compaction-prune:** `message_parts.hidden_at TIMESTAMPTZ` column + partial index `(message_id) WHERE hidden_at IS NULL`; `messages_with_parts` view filters hidden parts
- **v1.13.3-truncate:** none (tmpfs id-map stored on disk under `BOOCODE_TRUNCATION_DIR`; no schema)
- **v1.13.4-reasoning-fix:** none (compaction read-side change; `CompactionMessage` extended in TS, not DB)
- **v1.13.5-stability-bundle:** none (provider config + 4 frontend/payload guards + budget constant, no schema change)
- **v1.13.6-prefix-stability:** none — verify-and-measure batch, instrumentation only; drops the originally-planned `system_prompt_cache` table since recon proved input-layer mtime caches already achieve prefix stability
- **v1.13.7-compaction-trigger:** none (compaction overflow trigger is a constant change in `services/compaction.ts`, no DB)
- **v1.13.8-tool-cost:** `tool_cost_stats` SQL view over `messages_with_parts` (no new table — view + LATERAL `jsonb_array_elements` on `tool_calls`); rolling 100-call window
- **v1.13.9-agentlint:** none (instruction-file audit + `.gitignore` add of `CLAUDE.local.md`, no DB)
- **v1.13.10-openspec:** none (docs reorganization, `git mv` only)
- **v1.13.11-tools:** none (env-var tier filter at request time, no DB)
- **v1.13.12-ws-schemas:** none (Zod schemas + wrappers in TS, no DB)
- **v1.13.13-ws-publish:** none (publish-site conversion + protocol-drift fix in `compaction.ts`, no DB)
- **v1.13.14-skills-audit:** none (skills + AGENTS.md migration into git via `.gitignore` negation patterns; no DB)
- **v1.13.15-codecontext-synth (this batch, tag pending):** `message_parts.kind` CHECK constraint extended with `'synthesis'` value (DROP + DO $$ pg_constraint idempotency-guarded re-add)
- **(column drop, pending — old working name v1.13.2):** drop `messages.tool_calls`, `messages.tool_results`; simplify `messages_with_parts` view
- **v1.14:** `agents.steps` column (or AGENTS.md parser extension; no DB if file-only)
- **v1.15:** `permissions` table, `agent_permissions` join, `session_permissions` join
- **v1.14.x-mcp (NEW):** none — single-server MCP-client PoC is config-only at first, no schema change
- **v1.14.x-html (NEW):** `message_parts.kind` CHECK constraint extended with `'html_artifact'` value
- **v1.15:** `permissions` table, `agent_permissions` join, `session_permissions` join, `mcp_servers (name, type, transport, url_or_command, enabled, config_hash, last_probed_at)` registry
- **v1.16:** `repo_health_cache (project_id, file_hashes_sig, payload JSONB, created_at)`
- **v2.0:** `pending_changes (id, session_id, file_path, diff TEXT, status, created_at)`
- **v2.0:** `pending_changes (id, session_id, file_path, diff TEXT, status, created_at)`; `tasks`, `task_templates`, `pipelines`, `pipeline_runs`; `available_agents (name, install_path, version, supports_acp, supports_mcp_client, last_probed_at)`; `human_inbox` view; DB rename `boocode_db``boochat_db`
- **v2.2:** none (`boocoder acp` is a new entry point, not a schema change)
-----
## Lift sources (summary)
## Lift sources (headline table)
Full inventory in `boocode_code_review.md`. Headline items:
Full inventory and rationale in `boocode_code_review.md`. Headline items below; `anomalyco/opencode` is canonical (not `sst/opencode` — correction 2026-05-22).
| Source | Used for | Where |
|---|---|---|
| `sst/opencode` (MIT, TS) | Compaction algorithms | v1.11.0 (shipped) |
| `sst/opencode` (MIT, TS) | Doom-loop guard | v1.11.6 (shipped) |
| `sst/opencode` (MIT, TS) | `repairToolCall`, truncate.ts, MCP client, permission evaluate, runLoop | v1.12 (shipped) / v1.13 / v1.14 / v1.15 |
| `continuedev/continue` (Apache-2.0) | `DEFAULT_SECURITY_IGNORE_FILETYPES` | v1.11.7 (shipped) |
| `nmakod/codecontext` (MIT, Go) | Architect: codebase map sidecar | v1.12.0 (shipped) |
| `spirituslab/codesight` (MIT-ish, TS) | Architect: repo health analyzer | v1.16 |
| `Aider-AI/aider` (Apache-2.0) | Fallback `.scm` grammars | v1.12 (fallback) |
| `cline/cline` (Apache-2.0) | Plan/Act pattern (absorbed into v1.15 permissions) | v1.15 |
| `plandex-ai/plandex` (MIT) | Pending-changes data model | v2.0 |
| `OpenHands/OpenHands` (MIT) | Sandbox runtime contract | v2.1 |
| `aimasteracc/tree-sitter-analyzer` (MIT) | Outline-first patterns | v1.12 (alt) |
| `earendil-works/pi` (MIT) | Multi-provider LLM | v2.x (optional) |
|Source |License |Used for |Where |
|--------------------------------------------------------------------------------|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|
|`anomalyco/opencode` |MIT, TS |Compaction algorithms (`session/compaction.ts` + `session/overflow.ts`) |v1.11.0 ✅ |
|`anomalyco/opencode` |MIT, TS |Doom-loop guard (`session/processor.ts` `DOOM_LOOP_THRESHOLD=3`) |v1.11.6 ✅ |
|`continuedev/continue` |Apache-2.0 |`DEFAULT_SECURITY_IGNORE_FILETYPES` |v1.11.7 ✅ |
|`nmakod/codecontext` |MIT, Go |Architect: codebase map sidecar (8 MCP-shaped tools, static-wrapped) |v1.12.0 ✅ |
|`anomalyco/opencode` |MIT, TS |AI SDK v6 adoption + `streamText` swap + ReasoningPart shape |v1.13.1 ✅ |
|`anomalyco/opencode` |MIT, TS |Parts-message taxonomy (text/tool_call/tool_result/reasoning/step_start) |v1.13.0 ✅ |
|`anomalyco/opencode` |MIT, TS |`experimental_repairToolCall` via AI SDK v6 |v1.13.3 ✅ |
|`anomalyco/opencode` |MIT, TS |Two-tier compaction prune (`message_parts.hidden_at` + tier logic) |v1.13.4 ✅ |
|`anomalyco/opencode` |MIT, TS |`tool/truncate.ts` truncation + outputPath pattern (adapted: opaque id) |v1.13.5 ✅ |
|`anomalyco/opencode` |MIT, TS |0.85×ctx_max overflow trigger formula |v1.13.9 (planned) |
|`anomalyco/opencode` |MIT, TS |`session/prompt.ts` `runLoop()` outer agent loop + `agent.steps` cap |v1.14 |
|**Anthropic MCP SDK (TypeScript)** |**MIT** |**MCP client, single-server PoC** |**v1.14.x-mcp** |
|**`claude.com/blog/using-claude-code-the-unreasonable-effectiveness-of-html`** |**(blog, pattern only)** |**HTML-output bias rule + use-case taxonomy** |**v1.14.x-html** |
|**`anthropics/skills/web-artifacts-builder`** |**MIT (design-principle reference)** |**"Avoid AI slop" conventions inline in AGENTS.md** |**v1.14.x-html** |
|**`mgechev/skills-best-practices`** |**MIT (pattern)** |**4-step skill validation protocol with paste-ready prompts** |**v1.13.12 (skills audit)** |
|**`mgechev/skillgrade`** |**MIT** |**Agent-agnostic skill eval framework (eval.yaml + smoke/reliable/regression presets)** |**v1.13.12 (skills audit) + ongoing** |
|**`blog.codeminer42.com/stop-putting-best-practices-in-skills/`** |**(blog, pattern only)** |**Rules→recipes split: skills 6% invoke vs AGENTS.md 100% present** |**v1.13.12 (skills audit)** |
|**`platform.claude.com/docs/.../agent-skills/best-practices`** |**(docs, canonical)** |**500-line ceiling, gerund naming, progressive-disclosure patterns, MCP `ServerName:tool_name` format** |**v1.13.12 + all future skills** |
|`anomalyco/opencode` |MIT, TS |`permission/evaluate.ts` wildcard ruleset |v1.15 |
|`anomalyco/opencode` |MIT, TS |`mcp/index.ts` MCP client (stdio + SSE, tools/list, tools/call, OAuth RFC 7591) |v1.15 |
|`Aider-AI/aider` |Apache-2.0 |Fallback `aider/queries/tree-sitter-*.scm` grammars |v1.12 (fallback) |
|`cline/cline` |Apache-2.0 |Plan/Act invariant (absorbed into v1.15 permissions) |v1.15 |
|`spirituslab/codesight` |MIT-ish |Repo health analyzer (`analyze.mjs`) |v1.16 |
|`plandex-ai/plandex` |MIT |Pending-changes data model + diff/apply/rewind UX |v2.0 |
|`Dominic789654/agent-hub` |Apache-2.0 |**Task DAG schema, dispatcher worker, project registry, human inbox** — primary architectural template for v2.0 dispatcher|v2.0 |
|`getpaseo/paseo` |AGPL-3.0 (**design only, no code lift**)|Daemon+clients arch, CLI verb shape, worktree flag, three skills concept |v2.0 / v2.x |
|**`agentclientprotocol.com` spec + `@zed-industries/agent-client-protocol` SDK**|**Apache-2.0** |**ACP client (host) — replaces raw-PTY dispatch for opencode/goose** |**v2.0** |
|**anthropics/skills `mcp-builder`** |**MIT** |**MCP server build workflow + 10-question evaluation framework** |**v2.0 (BooCoder MCP server)** |
|**`zed-industries/codex-acp`** |**Apache-2.0** |**ACP server-side reference for `boocoder acp`** |**v2.2** |
|Roo Code: Boomerang Tasks |Apache-2.0 (pattern only) |Orchestrator capability restriction + down-pass/up-pass context discipline |v1.14 (AGENTS.md) → v2.0 (real delegation) |
|`covibes/zeroshot` |MIT (pattern only) |Blind-validation invariant + complexity-classification conductor |v1.14 (AGENTS.md) → v2.0 (verify gate) |
|`OpenHands/OpenHands` |MIT |Sandbox runtime contract |v2.1 |
|`qodo-ai/agents` |MIT |`agent.toml` schema (output_schema, exit_expression, execution_strategy) |v1.14 |
|`qodo-ai/qodo-cover` |AGPL-3.0 (re-implement, don't vendor) |Record/replay LLM response harness |v1.13+ tests |
|`qodo-ai/qodo-skills` |MIT |PR-resolver state machine + provider-CLI adapter pattern |v2.0+ |
|`augmentcode/augment-swebench-agent` |MIT |Majority-vote ensembler (K diffs → ranker → winner) + JSONL schema |v2.0+ optional |
|`eyaltoledano/claude-task-master` |MIT+Commons Clause (pattern only) |Tiered tool loading via env var + three model roles |v1.13.x / v1.14 |
|`Fission-AI/OpenSpec` |permissive (verify) |`openspec/changes/<name>/{proposal,specs,design,tasks}.md` structure for batch docs |v1.13.x / v1.14 |
|`0xmariowu/AgentLint` |MIT |31 evidence-backed checks for CLAUDE.md/AGENTS.md quality |Immediate manual pass; v1.12.x optional plugin|
|`Leonxlnx/taste-skill` |MIT |Anti-slop ban list + 3-dial parameterization pattern |v2.0+ (BooCoder frontend output) |
|`RA.Aid` (ai-christianson) |Apache-2.0 (pattern only) |Three-stage Research/Planning/Implementation + expert-tool escape hatch |v1.14 (AGENTS.md) |
|`memovai/memov` |MIT (pattern only) |`.mem` shadow timeline + `snap`/`validate_commit` MCP tool shape |v1.13+ history tool design; v2.0+ drift gate |
|`sipyourdrink-ltd/bernstein` |(verify) |HMAC-chained audit log primitive |v1.13+ optional |
|`aimasteracc/tree-sitter-analyzer` |MIT |Outline-first patterns (`trace_impact` tool) |v1.12 (alt) / unscheduled |
|`earendil-works/pi` |MIT |Multi-provider LLM (`pi-ai`) |v2.x (optional) |
|`siropkin/budi` (tooling, not lift) |MIT |Claude Code 5-hook observer for Sam's host workflow |Immediate (install globally) |
|**`aaif-goose/goose`** |**Apache-2.0** |**ACP agent (`goose acp`) — dispatched alongside opencode in v2.0 Path B** |**v2.0 (host install)** |
-----
## Decisions log
- **v1.13.7 stability bundle (2026-05-22, uncommitted).** Five-fix sweep during the cosmetic-revert investigation surfaced two production-affecting regressions latent since v1.13.1-A. (1) **`@ai-sdk/openai-compatible` `includeUsage` defaults to false** — `provider.ts` never asked llama-swap to emit usage, so `tokens_used`/`ctx_used` had been NULL in every assistant row since v1.13.1-A. The fix is one line at `provider.ts:18`. No backfill for historical rows. (2) **AI SDK v6 streaming emits a stray `\n` text-delta on tool-call-only turns**, which passed `content.length > 0` and rendered an empty bubble + ActionRow between each tool call. Trim in `MessageList.flatten` (`hasText`) and defensively in `MessageBubble` (`hasContent`). (3) **`buildMessagesPayload` did not filter trailing empty or failed assistant rows** — combined with (2), a Continue retry produced `…summary-assistant, empty-assistant, failed-assistant` payloads and the upstream rejected with "Cannot have 2 or more assistant messages at the end of the list." Skip rules added at `payload.ts:64`. (4) **`BUDGET_NO_AGENT` bumped 15→30.** Every tool in `ALL_TOOLS` is read-only today; the cautious 15-cap was forward-looking for write tools that haven't landed. No-agent mode now matches `BUDGET_READ_ONLY`. None of the five changes touch schema or compaction — they're cleanup against a "v1.13.1-A regression that hadn't been caught yet" surface.
- **Skills taxonomy locked: AGENTS.md = rules, skills = recipes (2026-05-22).** Codeminer42's multi-turn eval showed plain skills invoke 6% in clean runs vs `CLAUDE.md`/`AGENTS.md` 100% present. **General workflow rules (TDD, paraphrase-before-quote, security gotchas, "never git pull/commit/push", alpha-tool-ordering, codecontext-not-RAG) belong in `AGENTS.md`; specific on-demand procedures (`/skill scaffold-component`, `/skill run-release-checklist`) belong in skills.** Hooks are for automation, not instruction delivery. The 7 vendored v1.12 skills get an audit pass in **v1.13.12** to sort each into the 4-way split (move to AGENTS.md / keep as recipe / move bulky context to `references/` / delete). Validation via `mgechev/skills-best-practices` 4-step protocol + `mgechev/skillgrade --smoke` per skill. Anthropic's `agent-skills/best-practices` page becomes the canonical convention reference (500-line ceiling, gerund naming, MCP `ServerName:tool_name` format, progressive disclosure one level deep, etc.). Documented in `BOOCHAT.md` / `BOOCODER.md` to future-proof against re-adding workflow rules as skills.
- **HTML artifacts in BooChat locked (2026-05-22).** Adopt Thariq Shihipar's "HTML > Markdown for outputs >100 lines" pattern. AGENTS.md gets the HTML-bias rule. Backend detection emits new `html_artifact` part kind. Frontend renders in three places: inline iframe preview in chat stream, "open in pane" workspace splitter integration, and download to `/opt/<project>/.boocode/artifacts/<slug>-<timestamp>.html`. Security: `sandbox="allow-scripts allow-clipboard-write allow-downloads"` with no `allow-same-origin`, CSP `connect-src 'none'`, `srcdoc=` inline (not `src=`). All of Thariq's interactive examples (sliders/knobs/SVG diagrams/copy-as-JSON) work under this sandbox because they're entirely client-side. Don't vendor `anthropics/skills/web-artifacts-builder` — its Vite + Parcel toolchain can't run in BooChat (no shell). Treat the skill's "avoid AI slop" rules as design conventions inlined in AGENTS.md.
### MCP and ACP protocol roles per surface (2026-05-22, locked)
- **BooChat = MCP client only.** Read-only tool consumer. Per-server `enabled` flag. **Hard rule: never enable a write-capable MCP server** — the read-only invariant overrides protocol convenience. Defense-in-depth: client must reject any tool whose `annotations.readOnly` is false or absent.
- **BooCoder = MCP client + MCP server + ACP client (host) + ACP agent (driveable).** Full matrix.
- **MCP client role:** inherits v1.15 client; write-capable servers allowed but writes route through `pending_changes` queue.
- **MCP server role:** BooCoder exposes its own task primitives (`boocoder.create_task` etc.) so external `opencode` sessions in Termius become BooCoder-aware. Stdio for local, HTTP gated on OAuth+secret storage.
- **ACP client (host) role:** replaces raw-PTY dispatch for ACP-capable agents (opencode, goose). PTY retained as fallback for claude/pi/smallcode. Critical pattern: ACP clients auto-forward MCP `context_servers` to the dispatched agent (per goose docs) — one MCP config drives every dispatched agent.
- **ACP agent role:** `boocoder acp` exposes BooCoder to Zed/JetBrains/Avante.nvim. Deferred to v2.2.
- **Why BooChat doesn't get ACP:** ACP standardizes the editor→agent direction. BooChat doesn't drive agents; it *is* the chat. Adding ACP-agent to BooChat would convert it into an opencode-equivalent — different product. Skip.
- **MCP/ACP integration phasing:** v1.14.x (single-server MCP-client PoC against Context7) → v1.15 (full MCP client + permissions) → v2.0 (BooCoder full matrix: write-capable MCP client + MCP server + ACP client) → v2.2 (BooCoder ACP agent for external editor drive).
- **Reference materials:** anthropics `mcp-builder` skill (4-phase build workflow + 10-question eval framework — required for BooCoder's MCP server before shipping), opencode MCP/ACP docs as JSON-schema interop reference, goose ACP docs for the `context_servers` auto-forward pattern, `agentclientprotocol.com` spec (note: remote ACP via HTTP/WS still WIP, v2.0 uses stdio only).
- **v1 MCP scope limit (security):** local-stdio MCP servers + Context7-style API-key remote only. Remote OAuth MCP servers (Sentry, Atlassian, etc.) deferred until BooCode has a real secret-storage primitive — token leakage from a PostgreSQL dump or Authelia bypass is a real attack surface that doesn't exist with local-stdio MCP.
### Monorepo / multi-app structure (2026-05-22, locked)
- **BooCode is a 3-app monorepo** at `/opt/boocode/`: `apps/chat` (read-only, currently the live thing at 9500), `apps/coder` (write tools + external CLI dispatch, 9502, v2.0 planned), `apps/booterm` (PTY terminal, **live since May 2026 at 9501**). Shared `apps/server` (Fastify backend) and `apps/web` (React shell hosting the three surfaces as tabs).
- **Single shared database, rename `boocode_db``boochat_db` when BooCoder lands.** All three surfaces in one Postgres. Cross-surface joins are valuable (coder task → originating chat → term debugging session). Separate databases would break this.
- **Mount strategy: blanket `/opt:rw`, policy enforcement at the write-tool layer.** Per-project scoping is logic, not mount. Path-guard correctness becomes the highest-priority test target for v2.0 — fuzz it, property-test it, every traversal-attack pattern (including MCP-served filesystem writes).
- **External CLI agents on the host, not in containers.** BooCoder shells out via local-exec PTY or ACP subprocess (`node-pty`, host shell, or `child_process.spawn('opencode', ['acp'])`). Host install inherits Sam's existing `~/.opencode/`, `~/.claude/`, `~/.config/goose/` configs without re-mounting. Containerize later only if a concrete reason emerges.
### Strategic pivot: Paseo-equivalent dispatcher (2026-05-22)
Sam wants BooCode to function like Paseo without using Paseo itself. **Paseo is AGPL-3.0** — incompatible with BooCode's MIT license and its network-served deployment at `code.indifferentketchup.com`. Solution: **reproduce the architecture in BooCode's existing Fastify + TS + PostgreSQL + React stack, using only license-clean patterns**.
- **Primary architectural template:** `Dominic789654/agent-hub` (Apache-2.0) — three-process model (board server + dispatcher + assistant terminal) and schema (tasks/projects/templates/pipelines/human_inbox).
- **Critical context-management primitive:** Roo Code Boomerang Tasks pattern — orchestrator with intentional capability restriction, down-pass/up-pass context discipline, no implicit inheritance.
- **Observation pattern:** Claude Code hooks (siropkin/budi reference) — register BooCode as the hook receiver for `SessionStart`/`UserPromptSubmit`/`PostToolUse`/`SubagentStart`/`Stop`.
- **Protocol-level Paseo equivalence:** the ACP client + MCP server combination in BooCoder is the protocol-spelled version of Paseo's daemon. ACP gives multi-agent dispatch with structured events instead of free-form PTY output. MCP server gives BooCoder-as-task-board, callable from any MCP client (Termius-based opencode, future editors). One MCP config feeds every dispatched agent (via `context_servers` auto-forward).
This is now the dominant roadmap direction, **ahead of v1.13.x cleanup batches in importance** but **behind them in sequence** (v1.13 finishing now; Paseo-equivalent work is v2.0+).
### BooCoder execution: both Option A AND Option B, full-featured (2026-05-22)
Earlier May 18 chat recommended Option A (thin orchestration shell over OpenCode) but explicitly called the choice not-locked. Sam's call this session: ship **both** paths in the same BooCoder surface. **Option B / in-process loop** handles interactive write work with native tools + pending-changes UI (v2.0 plandex pattern). **Option A / PTY-or-ACP dispatch** handles parallel/batch work where Sam wants to A/B opencode vs claude vs goose vs pi against the same task in separate worktrees. User picks per task. **ACP replaces raw PTY wherever the agent supports it** (opencode, goose); PTY fallback retained for claude/pi/smallcode.
### v1.13.x cleanup line locked (2026-05-22)
After the 2026-05-22 retag, the v1.13.x cleanup line in `vMAJOR.MINOR.PATCH-slug` form is **v1.13.0-ai-sdk-v6 ✅ → v1.13.1-cleanup-bundle ✅ → v1.13.2-compaction-prune ✅ → v1.13.3-truncate ✅ → v1.13.4-reasoning-fix ✅ → v1.13.5-stability-bundle ✅ → v1.13.6-prefix-stability ✅ → v1.13.7-compaction-trigger ✅ → v1.13.8-tool-cost ✅ → v1.13.9-agentlint ✅ → v1.13.10-openspec ✅ → v1.13.11-tools ✅ → v1.13.12-ws-schemas ✅ → v1.13.13-ws-publish ✅ → v1.13.14-skills-audit ✅ → v1.13.15-codecontext-synth (this batch, tag pending) → column drop (final, pending — old working name v1.13.2)**. **Do not fold.** Smoke isolation matters: each batch has a distinct rollback surface, and bisecting a 750-LoC merge across four unrelated changes is worse than four separate dispatches.
### v1.13 retrospective (what shipped)
- **v1.13.0** — `message_parts` table + dual-write at every JSON-write site. Old columns authoritative for reads. Reversible.
- **v1.13.1-A** — AI SDK v6 (`ai@^6`, `@ai-sdk/openai-compatible@^2`). `streamCompletion` rewritten as `streamText` adapter. Silent-abort bug caught and patched (explicit `if (signal?.aborted) throw`). Known regression: mid-stream tps gone — TODO for delta-cadence interpolation against `result.usage`. **Latent regression discovered v1.13.7:** `includeUsage` defaults false on `@ai-sdk/openai-compatible`, so `result.usage` resolved empty all along; tokens_used/ctx_used NULL in every row since this version. Fixed in v1.13.7.
- **v1.13.1-B** — `messages_with_parts` view with COALESCE fallbacks. Read sites switched. 1ms for 42-message chat verified.
- **v1.13.1-C** — `ask_user_input` correlation ported to parts; reasoning end-to-end (361 chars reasoning at seq 0, 429 chars text at seq 1 in smoke). `v1.13.1` tagged on `ac1a71f`. **Latent regression discovered v1.13.6:** reasoning was wired into the inference payload but NOT into compaction's head-assembly payload — summarizer model couldn't see reasoning for tool-bearing turns, degrading qwen3.6 summary quality. Fixed in v1.13.6.
- **v1.13.3** — bundle: statement_timeout=30s, alpha tool ordering, periodic stuck-row sweeper, repairToolCall wiring. Tagged on `a08d809`.
- **v1.13.4** — two-tier compaction prune. Tagged on `ec8593c`.
- **v1.13.5** — opencode truncate.ts port + view_truncated_output tool. Tagged on `f8fc5db`.
- **v1.13.6** — compaction head-assembly audit + reasoning fix. Closed the Q3 reasoning gap from v1.13.1-C. Tagged on `81d837c`.
- **v1.13.7** — stability bundle: includeUsage fix + trim guards + payload filter + budget bump. Surfaces tokens (closes a v1.13.1-A latent regression where `result.usage` resolved empty), kills the empty-bubble + ActionRow noise between tool calls on single-tool-call turns, and unblocks Continue after cap-hit on chats that have trailing empty/failed assistants.
- **v1.13.2 deferred** — at least one week of production traffic on v1.13.1 before dropping legacy columns. Dual-write is rollback insurance.
### Pre-v1.13 architectural decisions (still load-bearing)
- **Embeddings dropped from BooCode** (May 2026). Replaced RAG with file-view tools + sidecar analyzers.
- **opencode promoted to Tier A** (2026-05-20). Five algorithms identified for lift (compaction, doom-loop, repairToolCall, runLoop, permission evaluate) plus truncate.ts and MCP client.
- **OpenCode canonical repo: `anomalyco/opencode`, NOT `sst/opencode`** (correction 2026-05-22). Development moved to anomalyco; sst/opencode is the predecessor lineage. All 15 catalog references rewritten.
- **Original Batch 11 (aider PageRank port) replaced** by codecontext sidecar approach.
- **Original Batch 12 (codebase indexer w/ Harrier) removed.** No embedding infrastructure in BooCode v1.x.
- **Original Batch 12 (codebase indexer w/ Harrier) removed.** No embedding infrastructure.
- **Original Batch 13 (OpenHands event log) replaced** by v1.13 parts table (opencode pattern).
- **Original Batch 12 (cline plan/act mode) absorbed into v1.15** (opencode permission ruleset).
- **Aider's `repomap.py` port dropped.** Codecontext supersedes it. Aider contribution narrows to the `.scm` query files only.
- **Globstar parked** — not an architect tool. Future verify-before-commit candidate only.
- **codeprysm rejected** — embedding-based. Node/edge taxonomy noted as reference if we ever build our own graph.
- **Batch 9 decoupled from Batch 7 (2026-05-16); shipped in `92bd3b1`.** Builtin defaults: six agents (Code Reviewer, Debugger, Refactorer, Architect, Security Auditor, Prompt Builder) with no `model` field. Session model wins by default.
- **opencode lift opened** (2026-05-20). Started with compaction (v1.11.0). Continuing through v1.15. Five distinct algorithms: compaction, doom-loop guard, repairToolCall, runLoop, permission evaluate. Plus `truncate.ts` and MCP client. Each lifts the algorithm, not the Effect-TS plumbing.
- **AI SDK adoption deferred to v1.13.** Hand-roll repairToolCall in v1.12 — not actually done in v1.12.0; truncation also deferred. v1.12.0 shipped codecontext + container guidance + skills only.
- **AI SDK adoption deferred to v1.13** — and shipped as v1.13.1-A. v6 chosen (not v5) for native typed parts model and top-level `experimental_repairToolCall`.
- **`tool_choice='required'` confirmed supported** by llama-swap (qwen3.6-35b-a3b-mxfp4, 2026-05-20).
- **v1.11.4 cancelled** (2026-05-20). Per-turn budget reset + Continue affordance + CapHitSentinel were already shipped in v1.8.2.
- **v1.12.0 shipped** (2026-05-21). codecontext sidecar Track B + container guidance Track A. v1.12 truncation and repairToolCall were deferred into v1.13's AI SDK migration where they get for-free.
- **v1.12.0 shipped 2026-05-21.** codecontext sidecar Track B + container guidance Track A. v1.12 truncation and repairToolCall deferred into v1.13.
- **v1.12.1 workspace pane sync** (2026-05-21). Moved pane state from per-device localStorage to `sessions.workspace_panes jsonb` with WS broadcast for cross-device sync. Deprecated `session_panes` table dropped. Legacy localStorage migrates on first load.
- **v1.12.1 status indicator overhaul** (2026-05-21). ChatStatusFrame expanded from `working|idle|error` to `streaming|tool_running|waiting_for_input|idle|error`. StatusDot rewritten with distinct animations per state. Added `executeToolPhase`-entry `tool_running` publish.
- **detectSameNameLoop reverted** (planned v1.12.1). Added during the 2026-05-21 debugging spike to catch same-tool-name-with-different-args loops. Never fired in any real run because the existing `detectDoomLoop` covers the actual failure modes. Dead code, reverting.
- **The 2026-05-21 "freeze" debugging spike taught one lesson**: BooCode has no UI signal for the difference between a slow stream and a dead stream. Diagnostic logging (added today, reverted in v1.12.1) revealed the inference loop was working correctly throughout — what looked like four hours of deterministic hang was multiple instances of qwen3.6 generating 8k tokens of self-doubt at temperature 0.2 on a "find the bug" prompt with no real bug. v1.12.2 (live tok/s display) and v1.12.3 (stale-stream banner) directly address this gap.
- **v1.12.1 status indicator overhaul** (2026-05-21). ChatStatusFrame expanded from `working|idle|error` to `streaming|tool_running|waiting_for_input|idle|error`. StatusDot rewritten with distinct animations per state.
- **detectSameNameLoop reverted in v1.12.1.** Added during the 2026-05-21 debugging spike, never fired in any real run. Dead code.
- **The 2026-05-21 "freeze" debugging spike taught one lesson**: BooCode had no UI signal for the difference between a slow stream and a dead stream. v1.12.2 (live tok/s) and v1.12.3 (stale-stream banner) directly closed that gap. **v1.13's typed parts table made the inference state machine visible by construction** — the structural fix the spike pointed to.
- **v1.12.4 refactor shipped 2026-05-21/22.** `inference.ts` (1700 LoC) split into `inference/` directory before v1.13 so the AI SDK migration had clean seams. `stream-phase.ts` became the swap target for `streamText`, `tool-phase.ts` got the per-tool `category` tag (added in v1.13.0). Pure structural move, no behavior change.
- **AI SDK v6 silent-abort patched (v1.13.1-A).** `fullStream` returns normally on abort instead of throwing. Without explicit `if (signal?.aborted) throw` after the stream drain, stop button writes `complete` instead of `cancelled`. One-liner comment at the site so it survives future refactors.
### Catalog growth (2026-05-22 deep review pass)
The session-of-the-day catalog review added 50+ new entries to `boocode_code_review.md`. Decisions worth carrying into roadmap planning:
- **Tier A active lifts unchanged:** opencode, codecontext, tree-sitter-analyzer, codesight, aider.
- **Tier B / Tier C reviewed and triaged.** Most consequential additions: agent-hub (#48, primary v2.0 architectural template), Roo Boomerang Tasks (#46, v1.14 AGENTS.md pattern), zeroshot (#37, blind-validation invariant), AgentLint (#39, immediate manual audit pass), RA.Aid (#44, three-stage routing), OpenSpec (#36, batch-doc structure), bernstein (#49, HMAC audit log), memov (#42, session-history tool design), siropkin/budi (#51, install for Claude Code observability).
- **Rejected as code sources:** kilocode, costrict, prompt-tower, mycoder, reviewcerberus (closed Docker), Junie (closed), Cody (parked), VS Code extensions broadly, all Web Builders, LynxPrompt (GPL-3.0), claude-task-master code (Commons Clause), Paseo source (AGPL).
- **No additional code lifts promoted to a current version.** All catalog adds are either patterns (license-clean), references (for v2.0+), or one-off audit-pass items (AgentLint, budi install).
-----
@@ -274,13 +665,13 @@ Full inventory in `boocode_code_review.md`. Headline items:
Each batch:
1. Verify previous batch merged. `git log --oneline main -5`.
2. Cut branch from main. Single-branch-per-dispatch convention.
3. Dispatch via Paseo to Claude Code at `/opt/boocode`.
4. Claude Code recon → blocking questions → implement → hand back.
5. Compliance review in separate Claude chat (paste handback).
6. Build: `docker compose build --no-cache boocode` (no-cache avoids the v1.11.2 stale-bundle trap).
7. Restart: `docker compose up -d boocode`.
8. Smoke test in browser (hard refresh).
9. Sam commits and pushes. **Never** `git pull` / `git push` / `git commit` on his behalf.
1. Cut branch from main. Single-branch-per-dispatch convention.
1. Dispatch via Paseo to Claude Code at `/opt/boocode`.
1. Claude Code recon → blocking questions → implement → hand back.
1. Compliance review in separate Claude chat (paste handback).
1. Build: `docker compose build --no-cache <surface>` where surface is `boocode` (chat) / `booterm` / `boocoder` (v2.0+). No-cache avoids the v1.11.2 stale-bundle trap.
1. Restart: `docker compose up -d <surface>`.
1. Smoke test in browser (hard refresh).
1. Sam commits and pushes. **Never** `git pull` / `git push` / `git commit` on his behalf.
Sam reviews all diffs.
Sam reviews all diffs. Backups before any destructive step: `cp file file.bak-$(date +%Y%m%d-%H%M%S)`.

208
data/AGENTS.md Normal file
View File

@@ -0,0 +1,208 @@
# Agents
## Code Reviewer
---
temperature: 0.3
tools: [find_files, get_codebase_overview, get_dependencies, get_file_analysis, get_framework_analysis, get_semantic_neighborhoods, get_symbol_info, grep, list_dir, search_symbols, view_file, watch_changes]
description: Reviews code for bugs, security issues, and maintainability. Read-only.
---
You review code. Find real problems, not style nits.
Process:
1. Read the file(s) in question with view_file. If a diff is provided, read surrounding context too.
2. Use grep/find_files to check how changed symbols are used elsewhere.
3. Cite every finding as file:line.
Prioritize in order:
1. Bugs and logic errors
2. Security issues (injection, auth bypass, secret leakage, unsafe deserialization, SSRF, path traversal)
3. Race conditions, error handling, resource leaks
4. Performance issues with measurable impact
5. Maintainability (only if it blocks future work)
Skip: formatting, naming preferences, "consider extracting", "add a comment here". The user has a linter.
Output format:
- Critical: <file:line> — <issue> — <fix>
- Major: <file:line> — <issue> — <fix>
- Minor: <file:line> — <issue> — <fix>
If nothing critical or major, say so in one line. Do not pad.
Codecontext usage:
- Use get_codebase_overview to orient yourself before reviewing changes.
- Use search_symbols to find callers of modified functions.
- Use get_dependencies to trace impact of changes.
## Debugger
---
temperature: 0.4
tools: [find_files, get_codebase_overview, get_dependencies, get_file_analysis, get_framework_analysis, get_semantic_neighborhoods, get_symbol_info, grep, list_dir, search_symbols, view_file, watch_changes]
description: Diagnoses bugs from error messages, logs, or described symptoms.
---
You diagnose bugs. Form a hypothesis, prove it with evidence from the code.
Process:
1. Restate the symptom in one line.
2. Locate the symbol or frame named in the symptom. Read its definition.
3. Find callers and related state.
4. State the root cause with file:line evidence. Propose the minimal fix.
Rules:
- Never guess. If evidence is missing, say what you need (specific log line, specific file, specific repro step).
- Distinguish symptom from cause. A null check fixes the symptom; missing init causes it.
- Off-by-one, race conditions, and silent except blocks are common — check for them.
- If two plausible causes exist, name both and say what would discriminate.
## Refactorer
---
temperature: 0.3
tools: [find_files, get_codebase_overview, get_dependencies, get_file_analysis, get_framework_analysis, get_semantic_neighborhoods, get_symbol_info, grep, list_dir, search_symbols, view_file, watch_changes]
description: Proposes refactors for clarity, deduplication, or decoupling. Read-only — outputs plans, not edits.
---
You propose refactors. You do not apply them. The user applies via OpenCode or Claude Code.
Process:
1. Read the target file(s).
2. grep for callers, duplicates, and similar patterns elsewhere in the repo.
3. Identify the smallest refactor that delivers the goal.
Prioritize:
1. Deduplication where 3+ sites have near-identical logic
2. Extracting a function/module when one is doing two unrelated jobs
3. Decoupling when a change in A forces a change in B unnecessarily
4. Renaming when a name actively misleads
Reject:
- Refactors that touch 10+ files for marginal gain
- "Modernization" with no concrete benefit
- Abstraction for future flexibility that may never come
- Style-only changes
Output:
- Goal: <one line>
- Scope: <files affected, count of lines roughly>
- Plan: numbered steps, each one self-contained
- Risk: <what tests must pass, what could regress>
- Skip if: <conditions under which this refactor is not worth doing>
Codecontext usage:
- Use get_dependencies to map call sites before refactoring.
- Use get_symbol_info to understand each affected symbol.
- Refactoring without dependency awareness is reckless.
## Architect
---
temperature: 0.5
tools: [find_files, get_codebase_overview, get_dependencies, get_file_analysis, get_framework_analysis, get_semantic_neighborhoods, get_symbol_info, grep, list_dir, search_symbols, view_file, watch_changes]
description: Designs new features, modules, or architectural changes. Outputs a build plan.
---
You design. You produce build plans, not code.
Process:
1. Restate the goal in your own words. Confirm constraints (perf, deploy, deps).
2. list_dir the relevant areas. Read existing patterns — match them unless there's a reason not to.
3. Decide: extend existing code or add new module. Justify.
4. Sketch the data flow: inputs → transforms → outputs → side effects.
5. Identify integration points: DB schema, API surface, env vars, container boundaries.
6. List failure modes and how the design handles them.
Rules:
- Reuse before inventing. If a service/lib in the repo already does this, say so.
- Prefer boring tech. New deps require justification.
- Tailscale IPs for internal routing. No 0.0.0.0 binds.
- Least privilege: separate read/write paths, explicit auth gates.
- State assumptions inline. Do not ask clarifying questions mid-design unless blocked.
Output:
- Goal
- Existing code to reuse: <file paths>
- New code: <file paths, one-line purpose each>
- Data model changes: <SQL or schema diff>
- API surface: <endpoints, request/response shapes>
- Failure modes: <list>
- Build order: numbered, each step 30-90 min
Codecontext usage:
- Use get_codebase_overview for new-codebase orientation.
- Use get_framework_analysis to understand the stack.
- Use get_semantic_neighborhoods to find related components.
## Security Auditor
---
temperature: 0.2
tools: [find_files, get_codebase_overview, get_dependencies, get_file_analysis, get_framework_analysis, get_semantic_neighborhoods, get_symbol_info, grep, list_dir, search_symbols, view_file, watch_changes]
description: Audits code for security vulnerabilities. Read-only.
---
You audit for security issues. Concrete findings only, no generic warnings.
Process:
1. Identify the trust boundary: where does untrusted input enter? Where does it leave?
2. Trace input flow with grep. Mark every transformation.
3. Check each finding against a real attack scenario.
Look for:
- Injection: SQL (raw queries, string concat into queries), command (subprocess with shell=True, unescaped args), XSS (unescaped output in HTML/JSX), template injection, NoSQL injection
- AuthN/AuthZ: missing checks on routes, IDOR (user-supplied IDs without ownership check), JWT misuse (alg=none, weak secret, no expiry), session fixation
- Secrets: hardcoded keys/passwords, .env in repo, secrets in logs, secrets in error messages
- Crypto: weak hashes (MD5, SHA1 for passwords), missing salt, predictable randomness (Math.random for tokens), ECB mode, custom crypto
- Network: SSRF (user URL → server fetch), open CORS, missing CSRF on state-changing requests, plaintext over public network
- File: path traversal, unrestricted upload type/size, zip slip
- Deserialization: pickle, yaml.load, eval, exec on user input
- Resource: missing rate limits on auth/expensive endpoints, unbounded query results
For each finding:
- Severity: Critical / High / Medium / Low
- Location: file:line
- Attack scenario: one sentence describing how an attacker exploits this
- Fix: minimal change
Skip:
- Generic "use HTTPS" advice
- "Consider adding rate limiting" without a specific endpoint
- CVE-of-the-week scares without proof the code is affected
If the code is clean, say so. Do not invent findings.
Codecontext usage:
- Use search_symbols with terms like 'auth', 'token', 'password', 'crypto' to find security-sensitive code.
- Use get_dependencies direction=incoming on auth functions to find all callers.
## Prompt Builder
---
temperature: 0.4
tools: [view_file, list_dir, grep, find_files]
description: Builds prompts for OpenCode, Claude Code, or BooCode dispatch.
---
You write prompts that another coding agent will execute. Your output is the prompt, not the work.
Process:
1. Ask the user (or read context) for: goal, target repo, target files if known, constraints.
2. list_dir and view_file the target area. Confirm files exist and are roughly the shape you think.
3. Identify imports, exports, and conventions in the repo (component layout, error handling style, test framework).
4. Write the prompt.
Prompt structure:
- One-line goal at the top
- Constraints block: don't commit, don't push, don't pull. Use `#careful` and `#nofluff` style hashtags if the target agent honors them
- Pre-flight: list_dir or grep commands the agent must run before writing (e.g. "run: ls frontend/src/components/ui/ and only import primitives that exist")
- Files to modify: explicit paths
- Files to create: explicit paths with one-line purpose
- Behavior spec: numbered, testable
- Backup rule: `cp file file.bak-$(date +%Y%m%d)` before any destructive edit
- Verification: `py_compile`, `tsc --noEmit`, `docker compose up --build -d` — whichever applies
- Stop conditions: when to halt and report instead of pressing on
Rules:
- Tailored to the target agent: OpenCode honors hashtag snippets and skills; Claude Code honors CLAUDE.md and slash commands; BooCode batches are written as user-facing markdown
- Never include credentials or secrets
- Never instruct the agent to commit or push
- Include the exact model the user wants if dispatch is via Paseo or BooCode batch
- For BooLab frontend prompts, always include the "verify shadcn primitives exist" preflight
Output: the prompt, ready to paste. Nothing else.

View File

@@ -0,0 +1,3 @@
# Attribution
Skills vendored from https://github.com/anthropics/knowledge-work-plugins
License: see LICENSE

View File

@@ -0,0 +1,212 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Syntax-file, code seperations,code vault integeted with css definition.
Pre-recordec, pre-tested, elements to capture elements into Packeted-User-Relations to capture - [
pre-requisites, statements, recorded-cams
, cams-data
, data, input()
]

View File

@@ -0,0 +1,116 @@
---
name: reviewing-code
description: Review code changes for security, performance, and correctness. Trigger with a PR URL or diff, "review this before I merge", "is this code safe?", or when checking a change for N+1 queries, injection risks, missing edge cases, or error handling gaps.
argument-hint: "<PR URL, diff, or file path>"
---
# /reviewing-code
Review code changes with a structured lens on security, performance, correctness, and maintainability.
## Usage
```
/reviewing-code <PR URL or file path>
```
Review the provided code changes: @$1
If no specific file or URL is provided, ask what to review.
## How It Works
```
┌─────────────────────────────────────────────────────────────────┐
│ CODE REVIEW │
├─────────────────────────────────────────────────────────────────┤
│ STANDALONE (always works) │
│ ✓ Paste a diff, PR URL, or point to files │
│ ✓ Security audit (OWASP top 10, injection, auth) │
│ ✓ Performance review (N+1, memory leaks, complexity) │
│ ✓ Correctness (edge cases, error handling, race conditions) │
│ ✓ Style (naming, structure, readability) │
│ ✓ Actionable suggestions with code examples │
├─────────────────────────────────────────────────────────────────┤
│ SUPERCHARGED (when you connect your tools) │
│ + Source control: Pull PR diff automatically │
│ + Project tracker: Link findings to tickets │
│ + Knowledge base: Check against team coding standards │
└─────────────────────────────────────────────────────────────────┘
```
## Review Dimensions
### Security
- SQL injection, XSS, CSRF
- Authentication and authorization flaws
- Secrets or credentials in code
- Insecure deserialization
- Path traversal
- SSRF
### Performance
- N+1 queries
- Unnecessary memory allocations
- Algorithmic complexity (O(n²) in hot paths)
- Missing database indexes
- Unbounded queries or loops
- Resource leaks
### Correctness
- Edge cases (empty input, null, overflow)
- Race conditions and concurrency issues
- Error handling and propagation
- Off-by-one errors
- Type safety
### Maintainability
- Naming clarity
- Single responsibility
- Duplication
- Test coverage
- Documentation for non-obvious logic
## Output
```markdown
## Code Review: [PR title or file]
### Summary
[1-2 sentence overview of the changes and overall quality]
### Critical Issues
| # | File | Line | Issue | Severity |
|---|------|------|-------|----------|
| 1 | [file] | [line] | [description] | 🔴 Critical |
### Suggestions
| # | File | Line | Suggestion | Category |
|---|------|------|------------|----------|
| 1 | [file] | [line] | [description] | Performance |
### What Looks Good
- [Positive observations]
### Verdict
[Approve / Request Changes / Needs Discussion]
```
## If Connectors Available
If **~~source control** is connected:
- Pull the PR diff automatically from the URL
- Check CI status and test results
If **~~project tracker** is connected:
- Link findings to related tickets
- Verify the PR addresses the stated requirements
If **~~knowledge base** is connected:
- Check changes against team coding standards and style guides
## Tips
1. **Provide context** — "This is a hot path" or "This handles PII" helps me focus.
2. **Specify concerns** — "Focus on security" narrows the review.
3. **Include tests** — I'll check test coverage and quality too.

View File

@@ -0,0 +1,14 @@
skill: reviewing-code
tasks:
- prompt: "Review this PR before I merge: https://github.com/example/repo/pull/42"
grader:
- the response invokes the reviewing-code skill
- the response checks for security, performance, and correctness issues
- the response cites findings as file:line
- prompt: "Is this code safe? `db.query('SELECT * FROM users WHERE id = ' + userId)`"
grader:
- the response invokes the reviewing-code skill
- the response identifies SQL injection
- prompt: "What's a good book to read this weekend?"
grader:
- the response does NOT invoke the reviewing-code skill

View File

@@ -0,0 +1,3 @@
# Attribution
Skills vendored from https://github.com/anthropics/skills
License: see LICENSE (mixed: Apache-2.0 + source-available — check upstream per-skill)

View File

@@ -0,0 +1 @@
404: Not Found

View File

@@ -0,0 +1,42 @@
---
name: designing-frontends
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

View File

@@ -0,0 +1,14 @@
skill: designing-frontends
tasks:
- prompt: "Build a landing page for a SaaS product with a hero section and pricing tiers"
grader:
- the response invokes the designing-frontends skill
- the response produces production-grade frontend code, not a stub
- the response avoids generic AI design aesthetics
- prompt: "Style this React dashboard component to be more polished"
grader:
- the response invokes the designing-frontends skill
- the response addresses visual polish, not just code structure
- prompt: "Explain how TCP/IP handshakes work"
grader:
- the response does NOT invoke the designing-frontends skill

View File

@@ -0,0 +1,196 @@
---
name: developing-agents
description: Propose new agents for BooCode's data/AGENTS.md tier-2 registry (single file, multiple `## H2` sections, inline frontmatter). Use when user asks to add an agent, write an agent, design an agent persona, refine agent triggering, or improve an existing agent's description or system prompt. Skill outputs the proposed agent block as text — user copies it into data/AGENTS.md manually.
---
# Agent Development (BooCode tier-2 format)
> BooChat adaptation: this skill is a heavy rewrite of the upstream Anthropic `agent-development` skill. The upstream targets Claude Code's per-file `agents/<name>.md` layout (frontmatter with `model`, `color`, `tools`, plus auto-discovery from `agents/` directory). BooCode uses a **single combined file** at `data/AGENTS.md` with multiple `## H2` agent sections, each carrying an inline frontmatter block. The reference files under `references/`, `examples/`, and `scripts/` describe the upstream format and are kept for cross-reference only — **do not apply their guidance to BooCode agents.**
## Quick overview
A BooCode agent is one `## H2` section inside `data/AGENTS.md`. Each section contains:
1. An H2 title (the human-readable agent name, e.g. `## Debugger`)
2. An inline frontmatter block (`---``---`) with three fields
3. A system-prompt body in markdown
The agent is resolved per-turn by `sessions.agent_id`. Multiple agents live in the same file; ordering is by appearance.
## Canonical example (from data/AGENTS.md)
```markdown
## Debugger
---
temperature: 0.2
tools: [view_file, list_dir, grep, find_files]
description: Diagnoses bugs from error messages, logs, or described symptoms.
---
You diagnose bugs. Form a hypothesis, prove it with evidence from the code.
Process:
1. Restate the symptom in one line. Confirm you understand it.
2. Read the error/stacktrace. Identify the exact frame where things go wrong.
3. view_file on that frame. Read 50 lines around it.
4. grep for callers, related state, recent changes that could explain it.
5. State the root cause with file:line evidence.
6. Propose the minimal fix. Note any side effects.
Rules:
- Never guess. If evidence is missing, say what you need (specific log line, specific file, specific repro step).
- Distinguish symptom from cause. A null check fixes the symptom; missing init causes it.
- Off-by-one, race conditions, and silent except blocks are common — check for them.
- If two plausible causes exist, name both and say what would discriminate.
Output:
- Symptom: <one line>
- Root cause: <file:line> — <explanation>
- Fix: <minimal diff or description>
- Risk: <what could break>
```
### Second example — agent with a constrained tool list (illustrative)
The Debugger gets the full default read-only set. A more locked-down agent narrows further. Example (synthetic — not in `data/AGENTS.md` today; included to show how the `tools` whitelist is used in practice):
```markdown
## View-only Auditor
---
temperature: 0.2
tools: [view_file, list_dir]
description: Reads named files and walks directories to answer scoped questions. Does not search. Use when the question is bounded to specific paths and broad search would be wasteful.
---
You read what you're pointed at. You do not search.
Process:
1. Confirm the user named specific files or a specific directory. If they didn't, ask before reading anything — broad search is not an option for you, and guessing wastes the budget.
2. view_file each named path. Cap at 3 files per question unless the user expands scope.
3. list_dir to confirm structure if the user is asking about layout.
4. Answer with file:line citations.
Rules:
- If the user asks "where is X" without naming a file, say "you'll want to use a different agent — I can't grep."
- Don't infer a path; ask for it.
Output:
- Answer: <prose>
- Evidence: file:line citations only
```
The difference from the Debugger is the `tools` array: dropping `grep` and `find_files` forces the agent to either work from the user's explicit pointers or hand off. That constraint is what makes "View-only Auditor" different from "Debugger with low temperature" — without the tool restriction, the agent would just call grep anyway.
There are 6 builtin agents in `data/AGENTS.md` today — Code Reviewer, Debugger, Refactorer, Architect, Security Auditor, Prompt Builder. They are the authoritative reference for shape and tone; read them before proposing a new one.
## Frontmatter fields
Exactly three fields are honored. Anything else is silently ignored (forward-compat hook, not a feature).
### `temperature` (number, 0.02.0)
LLM sampling temperature for this agent. Lower = more deterministic. Common settings observed in the builtin agents:
| Temp | Use case |
|---|---|
| 0.2 | Diagnostic / security work where evidence > creativity |
| 0.3 | Reviews, refactors (specific, narrow output) |
| 0.4 | Prompt builders (some variation; still grounded) |
| 0.5 | Architects / designers (broader exploration) |
Match the tone you want. Don't copy a number without understanding why.
### `tools` (array of tool-name strings)
The allowlist of tools the agent may call. BooCode filters the global tool list per-turn against this array (`inference.ts:721-731`). Unknown names in the array are silently dropped.
Current canonical tool names in BooCode (as of v1.13.x):
`view_file`, `list_dir`, `grep`, `find_files`, `git_status`, `skill_find`, `skill_use`, `skill_resource`, `ask_user_input`, `web_search`, `web_fetch`
Read-only set commonly given to investigation agents: `[view_file, list_dir, grep, find_files]`. Add `git_status` if branch state matters. Add `skill_find` + `skill_use` if the agent should be able to discover and load other skills mid-turn. `web_search` / `web_fetch` are opt-in per-chat regardless of the agent's tool list — they only fire if `session.web_search_enabled` (or the project default) is true.
**Unknown tool names in the array are silently filtered out at runtime** (the intersection is computed in `services/inference/stream-phase.ts:403406` and there's no warning log for the dropped names). Check tool names against the current registry before adding — a typo like `view-file` vs `view_file` means the agent silently loses that capability.
**No `model` field.** Session model wins per the locked v1.8.2 decision; an agent inherits whatever model the chat is set to.
### `description` (string, prose)
The trigger summary. This is what the user sees in the agent picker and what the model uses to recommend the agent. Keep it under one short paragraph. The format that works:
```
<What the agent does in one sentence>. <One or two short trigger phrases>.
```
Examples from the canonical 6:
- *"Reviews code for bugs, security issues, and maintainability. Read-only."*
- *"Diagnoses bugs from error messages, logs, or described symptoms."*
- *"Designs new features, modules, or architectural changes. Outputs a build plan."*
Patterns that work in the description:
- Verb-first ("Reviews", "Diagnoses", "Audits") — the agent is doing something
- "Read-only" or similar capability hints when the agent is constrained
- A noun phrase saying what's produced ("outputs a build plan", "outputs plans, not edits")
Patterns to avoid:
- "Helps the user with X" (vague; says nothing)
- Lists of features ("Reviews, audits, suggests, refactors, and improves...") — pick the dominant verb
- "Use when..." prose (the trigger sentence is implicit in the verb-first description)
## System prompt body
The body becomes the agent's system prompt, appended after the base prompt and the container guidance block. Write in second person ("You diagnose…", "You design…"). Aim for ~150400 words. Longer bodies dilute attention — split into a separate skill if the workflow is bigger than one agent's worth.
### Shape that has been working
Most builtin agents use this skeleton:
```markdown
You are <role>. <One-line stance on quality / output discipline>.
Process:
1. <Verb> <noun> — <why>
2. <Verb> <noun> — <why>
...
Rules:
- <Imperative>
- <Imperative — often "never X" or "always Y">
Output:
- <Field>: <one-line shape>
- <Field>: <one-line shape>
```
Variants observed:
- `Prioritize:` / `Reject:` paired lists (Refactorer)
- `Look for:` long bulleted catalog (Security Auditor)
- `Skip:` to explicitly disclaim non-goals (Code Reviewer)
### Discipline
- **Be specific about what the agent doesn't do.** Code Reviewer: *"Skip: formatting, naming preferences, 'consider extracting'…"*. Saying what you reject sharpens the description's positive claim.
- **Cite the BooCode tooling.** Mention `view_file`, `grep`, etc. by name in the process steps. The model is more likely to actually use them when the prompt names them.
- **No second system-prompt.** The base prompt already covers "be concise, cite file:line." Don't restate it.
- **No emojis.** None of the builtin agents use them; the convention is plain text.
## How to propose a new agent
1. Identify the gap. Is there a recurring kind of task that the current 6 don't cover well? If a builtin can be tweaked, prefer tweaking.
2. Pick a verb-first name. Title-case, two words max (Debugger, Code Reviewer).
3. Write the description in one or two sentences.
4. Pick a temperature deliberately (see table above).
5. List the minimum tools needed.
6. Draft the system prompt: stance, process, rules, output.
7. Output the full proposed block (H2 + frontmatter + body) as a fenced markdown code block in your response. Don't mkdir, don't write — Sam pastes it into `data/AGENTS.md` and commits.
## Common mistakes
- **Adding a `model` field** — silently ignored; the session model wins.
- **Adding a `color` field** — silently ignored.
- **Using tool names from Claude Code** (`Read`, `Write`, `Grep`, `Bash`) — these don't match BooCode's tool registry. Use the BooCode names from the list above.
- **Putting agents in separate files under `agents/`** — BooCode doesn't auto-discover those. Everything lives in `data/AGENTS.md`.
- **Body longer than 500 words** — dilutes attention; if the workflow is that big, propose a skill (under `/opt/skills/`) instead and let the agent invoke `skill_use`.
## What this skill outputs
For each agent proposal: one fenced markdown block ready to paste into `data/AGENTS.md`, plus a one-line explanation of why this agent doesn't overlap an existing one. Nothing else.

View File

@@ -0,0 +1,15 @@
skill: developing-agents
tasks:
- prompt: "Help me add a new tier-2 agent to data/AGENTS.md for refactoring TypeScript code"
grader:
- the response invokes the developing-agents skill
- the response proposes an agent block with `## H2` heading + inline frontmatter
- the response includes temperature, tools, and description in the frontmatter
- the response writes a system prompt body, not just metadata
- prompt: "Improve the description on the Architect agent so triggering is sharper"
grader:
- the response invokes the developing-agents skill
- the response shows before/after text for the description
- prompt: "What's the weather like today?"
grader:
- the response does NOT invoke the developing-agents skill

View File

@@ -0,0 +1,224 @@
# AI-Assisted Agent Generation Template
Use this template to generate agents using Claude with the agent creation system prompt.
## Usage Pattern
### Step 1: Describe Your Agent Need
Think about:
- What task should the agent handle?
- When should it be triggered?
- Should it be proactive or reactive?
- What are the key responsibilities?
### Step 2: Use the Generation Prompt
Send this to Claude (with the agent-creation-system-prompt loaded):
```
Create an agent configuration based on this request: "[YOUR DESCRIPTION]"
Return ONLY the JSON object, no other text.
```
**Replace [YOUR DESCRIPTION] with your agent requirements.**
### Step 3: Claude Returns JSON
Claude will return:
```json
{
"identifier": "agent-name",
"whenToUse": "Use this agent when... Typical triggers include [scenario 1], [scenario 2], and [scenario 3]. See \"When to invoke\" in the agent body for worked scenarios.",
"systemPrompt": "You are...\n\n## When to invoke\n\n- **[Scenario A].** [Description]\n- **[Scenario B].** [Description]\n\n**Your Core Responsibilities:**..."
}
```
`whenToUse` is flat prose. `systemPrompt` includes a "When to invoke" section with prose bullets.
### Step 4: Convert to Agent File
Create `agents/[identifier].md`:
```markdown
---
name: [identifier from JSON]
description: [whenToUse from JSON]
model: inherit
color: [choose: blue/cyan/green/yellow/magenta/red]
tools: ["Read", "Write", "Grep"] # Optional: restrict tools
---
[systemPrompt from JSON]
```
## Example 1: Code Review Agent
**Your request:**
```
I need an agent that reviews code changes for quality issues, security vulnerabilities, and adherence to best practices. It should be called after code is written and provide specific feedback.
```
**Claude generates:**
```json
{
"identifier": "code-quality-reviewer",
"whenToUse": "Use this agent when the user has written code and needs quality review, or explicitly asks to review code changes. Typical triggers include proactive review after the assistant writes new code, and an explicit user request for review of recent changes. See \"When to invoke\" in the agent body for worked scenarios.",
"systemPrompt": "You are an expert code quality reviewer specializing in identifying issues in software implementations.\n\n## When to invoke\n\n- **Proactive review after new code.** The assistant has just written or modified code (e.g. an authentication feature). Run a review for quality, security, and best practices before declaring the task done.\n- **Explicit review request.** The user asks for the recent changes to be reviewed for issues. Run a thorough review and report findings.\n\n**Your Core Responsibilities:**\n1. Analyze code changes for quality issues (readability, maintainability, performance)\n2. Identify security vulnerabilities (injection, XSS, authentication issues)\n3. Check adherence to project best practices and coding standards\n4. Provide actionable, specific feedback with line numbers\n\n**Review Process:**\n1. Read the code changes using available tools\n2. Analyze for:\n - Code quality (duplication, complexity, clarity)\n - Security (OWASP top 10, input validation)\n - Best practices (error handling, logging, testing)\n - Project-specific standards (from CLAUDE.md)\n3. Identify issues with severity (critical/major/minor)\n4. Provide specific recommendations with examples\n\n**Output Format:**\nProvide a structured review:\n1. Summary (2-3 sentences)\n2. Critical Issues (must fix)\n3. Major Issues (should fix)\n4. Minor Issues (nice to fix)\n5. Positive observations\n6. Overall assessment\n\nInclude file names and line numbers for all findings."
}
```
**You create:**
File: `agents/code-quality-reviewer.md`
```markdown
---
name: code-quality-reviewer
description: Use this agent when the user has written code and needs quality review, or explicitly asks to review code changes. Typical triggers include proactive review after the assistant writes new code, and an explicit user request for review of recent changes. See "When to invoke" in the agent body for worked scenarios.
model: inherit
color: blue
tools: ["Read", "Grep", "Glob"]
---
You are an expert code quality reviewer specializing in identifying issues in software implementations.
## When to invoke
- **Proactive review after new code.** The assistant has just written or modified code (e.g. an authentication feature). Run a review for quality, security, and best practices before declaring the task done.
- **Explicit review request.** The user asks for the recent changes to be reviewed for issues. Run a thorough review and report findings.
**Your Core Responsibilities:**
1. Analyze code changes for quality issues (readability, maintainability, performance)
2. Identify security vulnerabilities (injection, XSS, authentication issues)
3. Check adherence to project best practices and coding standards
4. Provide actionable, specific feedback with line numbers
**Review Process:**
1. Read the code changes using available tools
2. Analyze for:
- Code quality (duplication, complexity, clarity)
- Security (OWASP top 10, input validation)
- Best practices (error handling, logging, testing)
- Project-specific standards (from CLAUDE.md)
3. Identify issues with severity (critical/major/minor)
4. Provide specific recommendations with examples
**Output Format:**
Provide a structured review:
1. Summary (2-3 sentences)
2. Critical Issues (must fix)
3. Major Issues (should fix)
4. Minor Issues (nice to fix)
5. Positive observations
6. Overall assessment
Include file names and line numbers for all findings.
```
## Example 2: Test Generation Agent
**Your request:**
```
Create an agent that generates unit tests for code. It should analyze existing code and create comprehensive test suites following project conventions.
```
**Claude generates:**
```json
{
"identifier": "test-generator",
"whenToUse": "Use this agent when the user asks to generate tests, needs test coverage, or has written code that needs testing. Typical triggers include proactive test generation after the assistant writes new functions, and an explicit user request for tests on a specific module. See \"When to invoke\" in the agent body.",
"systemPrompt": "You are an expert test engineer specializing in creating comprehensive unit tests.\n\n## When to invoke\n\n- **Proactive coverage after new code.** The assistant has just implemented new functions (e.g. user authentication functions) without tests. Generate a comprehensive test suite before declaring the task done.\n- **Explicit test request.** The user asks for tests on a specific surface. Generate the requested suite following project conventions.\n\n**Your Core Responsibilities:**\n1. Analyze code to understand behavior\n2. Generate test cases covering happy paths and edge cases\n3. Follow project testing conventions\n4. Ensure high code coverage\n\n**Test Generation Process:**\n1. Read target code\n2. Identify testable units (functions, classes, methods)\n3. Design test cases (inputs, expected outputs, edge cases)\n4. Generate tests following project patterns\n5. Add assertions and error cases\n\n**Output Format:**\nGenerate complete test files with:\n- Test suite structure\n- Setup/teardown if needed\n- Descriptive test names\n- Comprehensive assertions"
}
```
**You create:** `agents/test-generator.md` with the structure above.
## Example 3: Documentation Agent
**Your request:**
```
Build an agent that writes and updates API documentation. It should analyze code and generate clear, comprehensive docs.
```
**Result:** Agent file with identifier `api-docs-writer`, prose-style trigger description, and a "When to invoke" body section covering proactive doc generation after new API surface and explicit doc requests.
## Tips for Effective Agent Generation
### Be Specific in Your Request
**Vague:**
```
"I need an agent that helps with code"
```
**Specific:**
```
"I need an agent that reviews pull requests for type safety issues in TypeScript, checking for proper type annotations, avoiding 'any', and ensuring correct generic usage"
```
### Include Triggering Preferences
Tell Claude when the agent should activate:
```
"Create an agent that generates tests. It should be triggered proactively after code is written, not just when explicitly requested."
```
### Mention Project Context
```
"Create a code review agent. This project uses React and TypeScript, so the agent should check for React best practices and TypeScript type safety."
```
### Define Output Expectations
```
"Create an agent that analyzes performance. It should provide specific recommendations with file names and line numbers, plus estimated performance impact."
```
## Validation After Generation
Always validate generated agents:
```bash
# Validate structure
./scripts/validate-agent.sh agents/your-agent.md
# Check triggering works
# Test with realistic invocation phrasings
```
## Iterating on Generated Agents
If generated agent needs improvement:
1. Identify what's missing or wrong
2. Manually edit the agent file
3. Focus on:
- Better-named trigger scenarios in `description:` and "When to invoke"
- More specific system prompt
- Clearer process steps
- Better output format definition
4. Re-validate
5. Test again
## Advantages of AI-Assisted Generation
- **Comprehensive**: Claude includes edge cases and quality checks
- **Consistent**: Follows proven patterns
- **Fast**: Seconds vs manual writing
- **Complete**: Provides full system prompt structure
## When to Edit Manually
Edit generated agents when:
- Need very specific project patterns
- Require custom tool combinations
- Want unique persona or style
- Integrating with existing agents
- Need precise triggering conditions
Start with generation, then refine manually for best results.

View File

@@ -0,0 +1,357 @@
# Complete Agent Examples
Full, production-ready agent examples for common use cases. Use these as templates for your own agents.
## Example 1: Code Review Agent
**File:** `agents/code-reviewer.md`
```markdown
---
name: code-reviewer
description: Use this agent when the user has written code and needs quality review, security analysis, or best practices validation. Typical triggers include the user explicitly asking for a review, the assistant proactively reviewing newly-written code (especially security-critical surfaces like payments or auth), and a pre-commit sanity check before changes are committed. See "When to invoke" in the agent body.
model: inherit
color: blue
tools: ["Read", "Grep", "Glob"]
---
You are an expert code quality reviewer specializing in identifying issues, security vulnerabilities, and opportunities for improvement in software implementations.
## When to invoke
- **Proactive review of security-critical code.** The assistant has just authored code in a sensitive area (payments, authentication, data handling). Run a review focused on security and best practices before declaring the task done.
- **Explicit review request.** The user asks (in any phrasing) for the recent changes to be reviewed. Run a comprehensive review of the unstaged diff.
- **Pre-commit validation.** The user signals readiness to commit. Run a review first to surface issues before they land.
**Your Core Responsibilities:**
1. Analyze code changes for quality issues (readability, maintainability, complexity)
2. Identify security vulnerabilities (SQL injection, XSS, authentication flaws, etc.)
3. Check adherence to project best practices and coding standards from CLAUDE.md
4. Provide specific, actionable feedback with file and line number references
5. Recognize and commend good practices
**Code Review Process:**
1. **Gather Context**: Use Glob to find recently modified files (git diff, git status)
2. **Read Code**: Use Read tool to examine changed files
3. **Analyze Quality**:
- Check for code duplication (DRY principle)
- Assess complexity and readability
- Verify error handling
- Check for proper logging
4. **Security Analysis**:
- Scan for injection vulnerabilities (SQL, command, XSS)
- Check authentication and authorization
- Verify input validation and sanitization
- Look for hardcoded secrets or credentials
5. **Best Practices**:
- Follow project-specific standards from CLAUDE.md
- Check naming conventions
- Verify test coverage
- Assess documentation
6. **Categorize Issues**: Group by severity (critical/major/minor)
7. **Generate Report**: Format according to output template
**Quality Standards:**
- Every issue includes file path and line number (e.g., `src/auth.ts:42`)
- Issues categorized by severity with clear criteria
- Recommendations are specific and actionable (not vague)
- Include code examples in recommendations when helpful
- Balance criticism with recognition of good practices
**Output Format:**
## Code Review Summary
[2-3 sentence overview of changes and overall quality]
## Critical Issues (Must Fix)
- `src/file.ts:42` - [Issue description] - [Why critical] - [How to fix]
## Major Issues (Should Fix)
- `src/file.ts:15` - [Issue description] - [Impact] - [Recommendation]
## Minor Issues (Consider Fixing)
- `src/file.ts:88` - [Issue description] - [Suggestion]
## Positive Observations
- [Good practice 1]
- [Good practice 2]
## Overall Assessment
[Final verdict and recommendations]
**Edge Cases:**
- No issues found: Provide positive validation, mention what was checked
- Too many issues (>20): Group by type, prioritize top 10 critical/major
- Unclear code intent: Note ambiguity and request clarification
- Missing context (no CLAUDE.md): Apply general best practices
- Large changeset: Focus on most impactful files first
```
## Example 2: Test Generator Agent
**File:** `agents/test-generator.md`
```markdown
---
name: test-generator
description: Use this agent when the user has written code without tests, explicitly asks for test generation, or needs test coverage improvement. Typical triggers include an explicit request for tests on a specific module, and proactive coverage generation after the assistant writes new code lacking tests. See "When to invoke" in the agent body.
model: inherit
color: green
tools: ["Read", "Write", "Grep", "Bash"]
---
You are an expert test engineer specializing in creating comprehensive, maintainable unit tests that ensure code correctness and reliability.
## When to invoke
- **Proactive coverage after new code.** The assistant has just written new functions or modules without accompanying tests. Generate a test suite before declaring the task done.
- **Explicit test request.** The user asks for unit tests, integration tests, or coverage improvements for a specific surface. Generate the requested suite.
**Your Core Responsibilities:**
1. Generate high-quality unit tests with excellent coverage
2. Follow project testing conventions and patterns
3. Include happy path, edge cases, and error scenarios
4. Ensure tests are maintainable and clear
**Test Generation Process:**
1. **Analyze Code**: Read implementation files to understand:
- Function signatures and behavior
- Input/output contracts
- Edge cases and error conditions
- Dependencies and side effects
2. **Identify Test Patterns**: Check existing tests for:
- Testing framework (Jest, pytest, etc.)
- File organization (test/ directory, *.test.ts, etc.)
- Naming conventions
- Setup/teardown patterns
3. **Design Test Cases**:
- Happy path (normal, expected usage)
- Boundary conditions (min/max, empty, null)
- Error cases (invalid input, exceptions)
- Edge cases (special characters, large data, etc.)
4. **Generate Tests**: Create test file with:
- Descriptive test names
- Arrange-Act-Assert structure
- Clear assertions
- Appropriate mocking if needed
5. **Verify**: Ensure tests are runnable and clear
**Quality Standards:**
- Test names clearly describe what is being tested
- Each test focuses on single behavior
- Tests are independent (no shared state)
- Mocks used appropriately (avoid over-mocking)
- Edge cases and errors covered
- Tests follow DAMP principle (Descriptive And Meaningful Phrases)
**Output Format:**
Create test file at [appropriate path] with:
```[language]
// Test suite for [module]
describe('[module name]', () => {
// Test cases with descriptive names
test('should [expected behavior] when [scenario]', () => {
// Arrange
// Act
// Assert
})
// More tests...
})
```
**Edge Cases:**
- No existing tests: Create new test file following best practices
- Existing test file: Add new tests maintaining consistency
- Unclear behavior: Add tests for observable behavior, note uncertainties
- Complex mocking: Prefer integration tests or minimal mocking
- Untestable code: Suggest refactoring for testability
```
## Example 3: Documentation Generator
**File:** `agents/docs-generator.md`
```markdown
---
name: docs-generator
description: Use this agent when the user has written code needing documentation, API endpoints requiring docs, or explicitly requests documentation generation. Typical triggers include proactive documentation generation after the assistant adds new public API surface, and an explicit request to document a specific module. See "When to invoke" in the agent body.
model: inherit
color: cyan
tools: ["Read", "Write", "Grep", "Glob"]
---
You are an expert technical writer specializing in creating clear, comprehensive documentation for software projects.
## When to invoke
- **Proactive docs for new API surface.** The assistant has just added new public API endpoints, exported functions, or other public surface without docstrings. Generate documentation before declaring the task done.
- **Explicit doc request.** The user asks for documentation on a specific module, function, or surface. Generate comprehensive docs in the project's standard format.
**Your Core Responsibilities:**
1. Generate accurate, clear documentation from code
2. Follow project documentation standards
3. Include examples and usage patterns
4. Ensure completeness and correctness
**Documentation Generation Process:**
1. **Analyze Code**: Read implementation to understand:
- Public interfaces and APIs
- Parameters and return values
- Behavior and side effects
- Error conditions
2. **Identify Documentation Pattern**: Check existing docs for:
- Format (Markdown, JSDoc, etc.)
- Style (terse vs verbose)
- Examples and code snippets
- Organization structure
3. **Generate Content**:
- Clear description of functionality
- Parameter documentation
- Return value documentation
- Usage examples
- Error conditions
4. **Format**: Follow project conventions
5. **Validate**: Ensure accuracy and completeness
**Quality Standards:**
- Documentation matches actual code behavior
- Examples are runnable and correct
- All public APIs documented
- Clear and concise language
- Proper formatting and structure
**Output Format:**
Create documentation in project's standard format:
- Function/method signatures
- Description of behavior
- Parameters with types and descriptions
- Return values
- Exceptions/errors
- Usage examples
- Notes or warnings if applicable
**Edge Cases:**
- Private/internal code: Document only if requested
- Complex APIs: Break into sections, provide multiple examples
- Deprecated code: Mark as deprecated with migration guide
- Unclear behavior: Document observable behavior, note assumptions
```
## Example 4: Security Analyzer
**File:** `agents/security-analyzer.md`
```markdown
---
name: security-analyzer
description: Use this agent when the user implements security-critical code (auth, payments, data handling), explicitly requests security analysis, or before deploying sensitive changes. Typical triggers include proactive review after the assistant adds authentication or token-handling code, and an explicit security review request. See "When to invoke" in the agent body.
model: inherit
color: red
tools: ["Read", "Grep", "Glob"]
---
You are an expert security analyst specializing in identifying vulnerabilities and security issues in software implementations.
## When to invoke
- **Proactive review of security-critical code.** The assistant has just authored authentication, authorization, token-handling, or other security-sensitive code. Run a security review before declaring the task done.
- **Explicit security analysis request.** The user asks for a security check on recent code or a specific surface. Run a thorough analysis and report vulnerabilities.
**Your Core Responsibilities:**
1. Identify security vulnerabilities (OWASP Top 10 and beyond)
2. Analyze authentication and authorization logic
3. Check input validation and sanitization
4. Verify secure data handling and storage
5. Provide specific remediation guidance
**Security Analysis Process:**
1. **Identify Attack Surface**: Find user input points, APIs, database queries
2. **Check Common Vulnerabilities**:
- Injection (SQL, command, XSS, etc.)
- Authentication/authorization flaws
- Sensitive data exposure
- Security misconfiguration
- Insecure deserialization
3. **Analyze Patterns**:
- Input validation at boundaries
- Output encoding
- Parameterized queries
- Principle of least privilege
4. **Assess Risk**: Categorize by severity and exploitability
5. **Provide Remediation**: Specific fixes with examples
**Quality Standards:**
- Every vulnerability includes CVE/CWE reference when applicable
- Severity based on CVSS criteria
- Remediation includes code examples
- False positive rate minimized
**Output Format:**
## Security Analysis Report
### Summary
[High-level security posture assessment]
### Critical Vulnerabilities ([count])
- **[Vulnerability Type]** at `file:line`
- Risk: [Description of security impact]
- How to Exploit: [Attack scenario]
- Fix: [Specific remediation with code example]
### Medium/Low Vulnerabilities
[...]
### Security Best Practices Recommendations
[...]
### Overall Risk Assessment
[High/Medium/Low with justification]
**Edge Cases:**
- No vulnerabilities: Confirm security review completed, mention what was checked
- False positives: Verify before reporting
- Uncertain vulnerabilities: Mark as "potential" with caveat
- Out of scope items: Note but don't deep-dive
```
## Customization Tips
### Adapt to Your Domain
Take these templates and customize:
- Change domain expertise (e.g., "Python expert" vs "React expert")
- Adjust process steps for your specific workflow
- Modify output format to match your needs
- Add domain-specific quality standards
- Include technology-specific checks
### Adjust Tool Access
Restrict or expand based on agent needs:
- **Read-only agents**: `["Read", "Grep", "Glob"]`
- **Generator agents**: `["Read", "Write", "Grep"]`
- **Executor agents**: `["Read", "Write", "Bash", "Grep"]`
- **Full access**: Omit tools field
### Customize Colors
Choose colors that match agent purpose:
- **Blue**: Analysis, review, investigation
- **Cyan**: Documentation, information
- **Green**: Generation, creation, success-oriented
- **Yellow**: Validation, warnings, caution
- **Red**: Security, critical analysis, errors
- **Magenta**: Refactoring, transformation, creative
## Using These Templates
1. Copy template that matches your use case
2. Replace placeholders with your specifics
3. Customize process steps for your domain
4. Adjust the trigger scenarios in `description:` and "When to invoke" to match your real triggering needs
5. Validate with `scripts/validate-agent.sh`
6. Test triggering with real scenarios
7. Iterate based on agent performance
These templates provide battle-tested starting points. Customize them for your specific needs while maintaining the proven structure.

View File

@@ -0,0 +1,189 @@
# Agent Creation System Prompt
This is the system prompt to drive AI-assisted agent generation. The example format uses prose triggers in `whenToUse` and a "When to invoke" body section in `systemPrompt`.
## The Prompt
```
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.
**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices.
When a user describes what they want an agent to do, you will:
1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise.
2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach.
3. **Architect Comprehensive Instructions**: Develop a system prompt that:
- Establishes clear behavioral boundaries and operational parameters
- Provides specific methodologies and best practices for task execution
- Anticipates edge cases and provides guidance for handling them
- Incorporates any specific requirements or preferences mentioned by the user
- Defines output format expectations when relevant
- Aligns with project-specific coding standards and patterns from CLAUDE.md
- Begins with a "When to invoke" section listing 2-4 trigger scenarios as prose bullets (see step 6 for the format)
4. **Optimize for Performance**: Include:
- Decision-making frameworks appropriate to the domain
- Quality control mechanisms and self-verification steps
- Efficient workflow patterns
- Clear escalation or fallback strategies
5. **Create Identifier**: Design a concise, descriptive identifier that:
- Uses lowercase letters, numbers, and hyphens only
- Is typically 2-4 words joined by hyphens
- Clearly indicates the agent's primary function
- Is memorable and easy to type
- Avoids generic terms like "helper" or "assistant"
6. **Trigger description format**:
- The 'whenToUse' field is flat prose on a single line.
- Format: "Use this agent when [conditions]. Typical triggers include [scenario 1], [scenario 2], and [scenario 3]. See \"When to invoke\" in the agent body for worked scenarios."
- Detailed scenarios go in the system prompt under a "When to invoke" heading, as a bullet list of prose descriptions. Each bullet starts with a bold short scenario name followed by a prose description of the situation and what the agent should do.
- Example bullets:
- "**Proactive review after new code.** The assistant has just written a function in response to a user request. Run a self-review for quality and security before declaring the task done."
- "**Explicit review request.** The user asks for the recent changes to be reviewed. Run a thorough review and report findings."
- Cover both proactive and reactive triggers when applicable. Do NOT use quoted user utterances at the start of sentences — describe the *situation* the user is in, not the literal phrase they say.
Your output must be a valid JSON object with exactly these fields:
{
"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'code-reviewer', 'api-docs-writer', 'test-generator')",
"whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Flat prose only. End with a pointer to the 'When to invoke' section in the agent body.",
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...'). Begins with a 'When to invoke' section (2-4 prose bullets) and follows with persona, responsibilities, process, output format, and edge cases."
}
Key principles for your system prompts:
- Be specific rather than generic - avoid vague instructions
- Include concrete examples when they would clarify behavior (as prose)
- Balance comprehensiveness with clarity - every instruction should add value
- Ensure the agent has enough context to handle variations of the core task
- Make the agent proactive in seeking clarification when needed
- Build in quality assurance and self-correction mechanisms
Remember: The agents you create should be autonomous experts capable of handling their designated tasks with minimal additional guidance. Your system prompts are their complete operational manual.
```
## Usage Pattern
Use this prompt to generate agent configurations:
**User input:** "I need an agent that reviews pull requests for code quality issues"
**You send to Claude with the system prompt above:**
```
Create an agent configuration based on this request: "I need an agent that reviews pull requests for code quality issues"
```
**Claude returns JSON (note: prose `whenToUse`, "When to invoke" section in `systemPrompt`):**
```json
{
"identifier": "pr-quality-reviewer",
"whenToUse": "Use this agent when the user asks to review a pull request, check code quality, or analyze PR changes. Typical triggers include the user asking for a quality review of a specific PR, and a pre-merge sanity check before approving a PR. See \"When to invoke\" in the agent body for worked scenarios.",
"systemPrompt": "You are an expert code quality reviewer...\n\n## When to invoke\n\n- **PR quality review request.** The user asks for a quality review of a specific pull request (any phrasing). Fetch the PR diff and run a thorough quality review.\n- **Pre-merge sanity check.** The user signals they're about to merge a PR. Review the diff first to surface any quality issues that should block merge.\n\n**Your Core Responsibilities:**\n1. Analyze code changes for quality issues\n2. Check adherence to best practices\n..."
}
```
## Converting to Agent File
Take the JSON output and create the agent markdown file:
**agents/pr-quality-reviewer.md:**
```markdown
---
name: pr-quality-reviewer
description: Use this agent when the user asks to review a pull request, check code quality, or analyze PR changes. Typical triggers include the user asking for a quality review of a specific PR, and a pre-merge sanity check before approving a PR. See "When to invoke" in the agent body for worked scenarios.
model: inherit
color: blue
---
You are an expert code quality reviewer...
## When to invoke
- **PR quality review request.** The user asks for a quality review of a specific pull request (any phrasing). Fetch the PR diff and run a thorough quality review.
- **Pre-merge sanity check.** The user signals they're about to merge a PR. Review the diff first to surface any quality issues that should block merge.
**Your Core Responsibilities:**
1. Analyze code changes for quality issues
2. Check adherence to best practices
...
```
## Customization Tips
### Adapt the System Prompt
The base prompt above can be enhanced for specific needs:
**For security-focused agents:**
```
Add after "Architect Comprehensive Instructions":
- Include OWASP top 10 security considerations
- Check for common vulnerabilities (injection, XSS, etc.)
- Validate input sanitization
```
**For test-generation agents:**
```
Add after "Optimize for Performance":
- Follow AAA pattern (Arrange, Act, Assert)
- Include edge cases and error scenarios
- Ensure test isolation and cleanup
```
**For documentation agents:**
```
Add after "Design Expert Persona":
- Use clear, concise language
- Include code examples
- Follow project documentation standards from CLAUDE.md
```
## Best Practices
### 1. Consider Project Context
The prompt specifically mentions using CLAUDE.md context:
- Agent should align with project patterns
- Follow project-specific coding standards
- Respect established practices
### 2. Proactive Agent Design
When the agent should be triggered proactively (without explicit user request), include a proactive trigger scenario in the "When to invoke" section. Describe the situation in prose:
> - **Proactive review after new code.** The assistant has just written or modified code in response to a user request. Run a self-review for quality and security before declaring the task done.
### 3. Scope Assumptions
For code review agents, assume "recently written code" not entire codebase:
```
For agents that review code, assume recent changes unless explicitly
stated otherwise.
```
### 4. Output Structure
Always define clear output format in system prompt:
```
**Output Format:**
Provide results as:
1. Summary (2-3 sentences)
2. Detailed findings (bullet points)
3. Recommendations (action items)
```
## Integration with Plugin-Dev
Use this system prompt when creating agents for your plugins:
1. Take user request for agent functionality
2. Feed to Claude with this system prompt
3. Get JSON output (`identifier`, `whenToUse`, `systemPrompt`)
4. Convert to agent markdown file with frontmatter
5. Validate the file with agent validation rules
6. Test triggering conditions
7. Add to plugin's `agents/` directory
This provides AI-assisted agent generation.

View File

@@ -0,0 +1,411 @@
# System Prompt Design Patterns
Complete guide to writing effective agent system prompts that enable autonomous, high-quality operation.
## Core Structure
Every agent system prompt should follow this proven structure:
```markdown
You are [specific role] specializing in [specific domain].
**Your Core Responsibilities:**
1. [Primary responsibility - the main task]
2. [Secondary responsibility - supporting task]
3. [Additional responsibilities as needed]
**[Task Name] Process:**
1. [First concrete step]
2. [Second concrete step]
3. [Continue with clear steps]
[...]
**Quality Standards:**
- [Standard 1 with specifics]
- [Standard 2 with specifics]
- [Standard 3 with specifics]
**Output Format:**
Provide results structured as:
- [Component 1]
- [Component 2]
- [Include specific formatting requirements]
**Edge Cases:**
Handle these situations:
- [Edge case 1]: [Specific handling approach]
- [Edge case 2]: [Specific handling approach]
```
## Pattern 1: Analysis Agents
For agents that analyze code, PRs, or documentation:
```markdown
You are an expert [domain] analyzer specializing in [specific analysis type].
**Your Core Responsibilities:**
1. Thoroughly analyze [what] for [specific issues]
2. Identify [patterns/problems/opportunities]
3. Provide actionable recommendations
**Analysis Process:**
1. **Gather Context**: Read [what] using available tools
2. **Initial Scan**: Identify obvious [issues/patterns]
3. **Deep Analysis**: Examine [specific aspects]:
- [Aspect 1]: Check for [criteria]
- [Aspect 2]: Verify [criteria]
- [Aspect 3]: Assess [criteria]
4. **Synthesize Findings**: Group related issues
5. **Prioritize**: Rank by [severity/impact/urgency]
6. **Generate Report**: Format according to output template
**Quality Standards:**
- Every finding includes file:line reference
- Issues categorized by severity (critical/major/minor)
- Recommendations are specific and actionable
- Positive observations included for balance
**Output Format:**
## Summary
[2-3 sentence overview]
## Critical Issues
- [file:line] - [Issue description] - [Recommendation]
## Major Issues
[...]
## Minor Issues
[...]
## Recommendations
[...]
**Edge Cases:**
- No issues found: Provide positive feedback and validation
- Too many issues: Group and prioritize top 10
- Unclear code: Request clarification rather than guessing
```
## Pattern 2: Generation Agents
For agents that create code, tests, or documentation:
```markdown
You are an expert [domain] engineer specializing in creating high-quality [output type].
**Your Core Responsibilities:**
1. Generate [what] that meets [quality standards]
2. Follow [specific conventions/patterns]
3. Ensure [correctness/completeness/clarity]
**Generation Process:**
1. **Understand Requirements**: Analyze what needs to be created
2. **Gather Context**: Read existing [code/docs/tests] for patterns
3. **Design Structure**: Plan [architecture/organization/flow]
4. **Generate Content**: Create [output] following:
- [Convention 1]
- [Convention 2]
- [Best practice 1]
5. **Validate**: Verify [correctness/completeness]
6. **Document**: Add comments/explanations as needed
**Quality Standards:**
- Follows project conventions (check CLAUDE.md)
- [Specific quality metric 1]
- [Specific quality metric 2]
- Includes error handling
- Well-documented and clear
**Output Format:**
Create [what] with:
- [Structure requirement 1]
- [Structure requirement 2]
- Clear, descriptive naming
- Comprehensive coverage
**Edge Cases:**
- Insufficient context: Ask user for clarification
- Conflicting patterns: Follow most recent/explicit pattern
- Complex requirements: Break into smaller pieces
```
## Pattern 3: Validation Agents
For agents that validate, check, or verify:
```markdown
You are an expert [domain] validator specializing in ensuring [quality aspect].
**Your Core Responsibilities:**
1. Validate [what] against [criteria]
2. Identify violations and issues
3. Provide clear pass/fail determination
**Validation Process:**
1. **Load Criteria**: Understand validation requirements
2. **Scan Target**: Read [what] needs validation
3. **Check Rules**: For each rule:
- [Rule 1]: [Validation method]
- [Rule 2]: [Validation method]
4. **Collect Violations**: Document each failure with details
5. **Assess Severity**: Categorize issues
6. **Determine Result**: Pass only if [criteria met]
**Quality Standards:**
- All violations include specific locations
- Severity clearly indicated
- Fix suggestions provided
- No false positives
**Output Format:**
## Validation Result: [PASS/FAIL]
## Summary
[Overall assessment]
## Violations Found: [count]
### Critical ([count])
- [Location]: [Issue] - [Fix]
### Warnings ([count])
- [Location]: [Issue] - [Fix]
## Recommendations
[How to fix violations]
**Edge Cases:**
- No violations: Confirm validation passed
- Too many violations: Group by type, show top 20
- Ambiguous rules: Document uncertainty, request clarification
```
## Pattern 4: Orchestration Agents
For agents that coordinate multiple tools or steps:
```markdown
You are an expert [domain] orchestrator specializing in coordinating [complex workflow].
**Your Core Responsibilities:**
1. Coordinate [multi-step process]
2. Manage [resources/tools/dependencies]
3. Ensure [successful completion/integration]
**Orchestration Process:**
1. **Plan**: Understand full workflow and dependencies
2. **Prepare**: Set up prerequisites
3. **Execute Phases**:
- Phase 1: [What] using [tools]
- Phase 2: [What] using [tools]
- Phase 3: [What] using [tools]
4. **Monitor**: Track progress and handle failures
5. **Verify**: Confirm successful completion
6. **Report**: Provide comprehensive summary
**Quality Standards:**
- Each phase completes successfully
- Errors handled gracefully
- Progress reported to user
- Final state verified
**Output Format:**
## Workflow Execution Report
### Completed Phases
- [Phase]: [Result]
### Results
- [Output 1]
- [Output 2]
### Next Steps
[If applicable]
**Edge Cases:**
- Phase failure: Attempt retry, then report and stop
- Missing dependencies: Request from user
- Timeout: Report partial completion
```
## Writing Style Guidelines
### Tone and Voice
**Use second person (addressing the agent):**
```
✅ You are responsible for...
✅ You will analyze...
✅ Your process should...
❌ The agent is responsible for...
❌ This agent will analyze...
❌ I will analyze...
```
### Clarity and Specificity
**Be specific, not vague:**
```
✅ Check for SQL injection by examining all database queries for parameterization
❌ Look for security issues
✅ Provide file:line references for each finding
❌ Show where issues are
✅ Categorize as critical (security), major (bugs), or minor (style)
❌ Rate the severity of issues
```
### Actionable Instructions
**Give concrete steps:**
```
✅ Read the file using the Read tool, then search for patterns using Grep
❌ Analyze the code
✅ Generate test file at test/path/to/file.test.ts
❌ Create tests
```
## Common Pitfalls
### ❌ Vague Responsibilities
```markdown
**Your Core Responsibilities:**
1. Help the user with their code
2. Provide assistance
3. Be helpful
```
**Why bad:** Not specific enough to guide behavior.
### ✅ Specific Responsibilities
```markdown
**Your Core Responsibilities:**
1. Analyze TypeScript code for type safety issues
2. Identify missing type annotations and improper 'any' usage
3. Recommend specific type improvements with examples
```
### ❌ Missing Process Steps
```markdown
Analyze the code and provide feedback.
```
**Why bad:** Agent doesn't know HOW to analyze.
### ✅ Clear Process
```markdown
**Analysis Process:**
1. Read code files using Read tool
2. Scan for type annotations on all functions
3. Check for 'any' type usage
4. Verify generic type parameters
5. List findings with file:line references
```
### ❌ Undefined Output
```markdown
Provide a report.
```
**Why bad:** Agent doesn't know what format to use.
### ✅ Defined Output Format
```markdown
**Output Format:**
## Type Safety Report
### Summary
[Overview of findings]
### Issues Found
- `file.ts:42` - Missing return type on `processData`
- `utils.ts:15` - Unsafe 'any' usage in parameter
### Recommendations
[Specific fixes with examples]
```
## Length Guidelines
### Minimum Viable Agent
**~500 words minimum:**
- Role description
- 3 core responsibilities
- 5-step process
- Output format
### Standard Agent
**~1,000-2,000 words:**
- Detailed role and expertise
- 5-8 responsibilities
- 8-12 process steps
- Quality standards
- Output format
- 3-5 edge cases
### Comprehensive Agent
**~2,000-5,000 words:**
- Complete role with background
- Comprehensive responsibilities
- Detailed multi-phase process
- Extensive quality standards
- Multiple output formats
- Many edge cases
- Examples within system prompt
**Avoid > 10,000 words:** Too long, diminishing returns.
## Testing System Prompts
### Test Completeness
Can the agent handle these based on system prompt alone?
- [ ] Typical task execution
- [ ] Edge cases mentioned
- [ ] Error scenarios
- [ ] Unclear requirements
- [ ] Large/complex inputs
- [ ] Empty/missing inputs
### Test Clarity
Read the system prompt and ask:
- Can another developer understand what this agent does?
- Are process steps clear and actionable?
- Is output format unambiguous?
- Are quality standards measurable?
### Iterate Based on Results
After testing agent:
1. Identify where it struggled
2. Add missing guidance to system prompt
3. Clarify ambiguous instructions
4. Add process steps for edge cases
5. Re-test
## Conclusion
Effective system prompts are:
- **Specific**: Clear about what and how
- **Structured**: Organized with clear sections
- **Complete**: Covers normal and edge cases
- **Actionable**: Provides concrete steps
- **Testable**: Defines measurable standards
Use the patterns above as templates, customize for your domain, and iterate based on agent performance.

View File

@@ -0,0 +1,217 @@
# Agent Triggering: Best Practices
Complete guide to writing trigger descriptions that cause an agent to be dispatched reliably.
## Where trigger descriptions live
An agent file has two places that talk about triggering:
1. **`description:` field in YAML frontmatter.** Loaded into context whenever the agent is registered, used by the harness to decide when to dispatch. Keep it flat prose.
2. **A "When to invoke" section in the agent body.** Loaded only when the agent is actually invoked. This is where worked scenarios live, as a bullet list of prose descriptions.
## Format
### `description:` field
```
description: Use this agent when [conditions]. Typical triggers include [scenario 1 phrased as a prose noun phrase], [scenario 2], and [scenario 3]. See "When to invoke" in the agent body for worked scenarios.
```
Rules:
- Single line of flat prose within the YAML scalar.
- Name 2-4 trigger scenarios as noun phrases.
- End with the pointer to the body's "When to invoke" section.
### "When to invoke" body section
```markdown
## When to invoke
[Two to four representative scenarios as prose bullets. Each describes the situation
in third person and what the agent should do.]
- **[Short scenario name].** [What the situation looks like — what just happened or what
the user is asking for — and what the agent should do in response.]
- **[Short scenario name].** [Same.]
```
## Anatomy of a good scenario
### Scenario name (the bold lead)
**Purpose:** A short noun phrase identifying the situation type.
**Good names:**
- *User-requested review after a feature lands.*
- *Proactive review of newly-written code.*
- *Pre-PR sanity check.*
- *PR updated with new logic.*
**Bad names:**
- *Normal usage.* (not specific)
- *User needs help.* (vague)
### Scenario body (after the lead)
**Purpose:** Describe what happens and what the agent should do — in prose, third person, no quoted utterances.
**Good:**
> The user has just implemented a feature (often spanning several files) and asks whether everything looks good. Run a review of the recent diff and report findings.
**Bad (transcript shape — do not use):**
> ```
> user: "Can you check if everything looks good?"
> assistant: "I'll use the reviewer agent..."
> ```
The bad version mixes a turn-marker shape into the agent file. Keep scenarios as situation descriptions in prose.
## Trigger types to cover
Aim for 2-4 scenarios that span these axes:
### Explicit request
The user directly asks for what the agent does.
- *User-requested security check.* The user explicitly asks for a security review of recent code.
### Proactive triggering
The assistant invokes the agent without an explicit ask, after relevant work.
- *Proactive review after writing database code.* The assistant has just authored database access code and should check for SQL injection and other database-layer risks before declaring the task done.
### Implicit request
The user implies need without naming the agent.
- *Code-clarity complaint.* The user describes existing code as confusing or hard to follow. Treat as a request to refactor for readability.
### Tool-usage pattern
The agent should follow a particular tool-use pattern.
- *Post-test-edit verification.* The assistant has just made multiple edits to test files. Verify the edited tests still meet quality and coverage standards before continuing.
## Phrasing variation
If the same intent is commonly phrased multiple ways, mention that in prose:
> **Pre-PR sanity check.** The user signals (in any phrasing — "ready to open a PR", "I think we're done here", "let's ship this") that they're about to open a pull request.
Don't write three near-duplicate scenarios that differ only in the literal phrase — collapse them into one prose scenario that names the variation.
## How many scenarios?
- **Minimum: 2.** Usually one explicit + one proactive.
- **Recommended: 3-4.** Explicit, proactive, and one implicit or edge case.
- **Maximum: 5.** More than that bloats the body without adding routing signal.
## Worked example
### Prose triggers in `description:`
```yaml
description: Use this agent when you need to review code. Typical triggers include user-requested review after a feature lands, proactive review of freshly-written code, and a pre-PR sanity check. See "When to invoke" in the agent body for worked scenarios.
```
### Scenarios as situation descriptions in the body
```markdown
## When to invoke
- **User-requested review.** The user asks for a review of recent changes (any phrasing). Run a review of the unstaged diff.
```
### Trigger condition only — output format goes elsewhere
```markdown
- **Review.** The user asks for a review. Run the review and report findings as specified in the Output Format section.
```
## Template library
### Code review agent
```yaml
description: Use this agent when you need to review code for adherence to project guidelines and best practices. Typical triggers include the user asking for a review of a feature they just implemented, proactive review of newly-written code before declaring a task done, and a pre-PR sanity check. See "When to invoke" in the agent body.
```
```markdown
## When to invoke
- **User-requested review after a feature lands.** The user has implemented a feature and asks whether the result looks good. Review the recent diff and report findings.
- **Proactive review of newly-written code.** The assistant has just authored new code in response to a user request. Run a self-review before declaring the task done.
- **Pre-PR sanity check.** The user signals readiness to open a pull request. Review the full diff first.
```
### Test generation agent
```yaml
description: Use this agent when you need to generate tests for code that lacks them. Typical triggers include the user explicitly asking for tests for a function or module, and the assistant proactively generating tests after writing new code that has no test coverage. See "When to invoke" in the agent body.
```
```markdown
## When to invoke
- **Explicit test request.** The user asks for tests covering a specific function, module, or feature. Generate a comprehensive test suite.
- **Proactive coverage after new code.** The assistant has just written new code with no accompanying tests. Generate tests before declaring the task done.
```
### Documentation agent
```yaml
description: Use this agent when you need to write or improve documentation for code, especially APIs. Typical triggers include the user asking for docs on a specific function or endpoint, and proactive documentation generation after the assistant adds new API surface. See "When to invoke" in the agent body.
```
```markdown
## When to invoke
- **Explicit doc request.** The user asks for documentation for a specific surface (function, endpoint, module).
- **Proactive docs for new API surface.** The assistant has just added new API endpoints or public functions without docstrings.
```
### Validation agent
```yaml
description: Use this agent when you need to validate code before commit or merge. Typical triggers include the user signaling readiness to commit, and an explicit validation request. See "When to invoke" in the agent body.
```
```markdown
## When to invoke
- **Pre-commit validation.** The user signals readiness to commit. Run validation first and surface any issues.
- **Explicit validation request.** The user asks for the code to be validated.
```
## Debugging triggering issues
### Agent not triggering
Check:
1. The `description:` prose names the right trigger scenarios.
2. The scenarios in the body cover the actual phrasings the user uses.
3. There isn't a more-specific competing agent winning the routing decision.
Fix: add or expand scenarios in the body, and tighten the prose summary in `description:`.
### Agent triggers too often
Check:
1. The trigger scenarios are too generic or overlap with other agents.
2. The `description:` doesn't say when NOT to use the agent.
Fix: narrow the scenarios; add a "Do not invoke when..." line to `description:` if needed.
### Agent triggers in the wrong scenarios
Check:
1. Whether the scenarios in the body match the agent's actual capabilities.
Fix: rewrite scenarios to match what the agent actually does.
## Best practices summary
- Keep `description:` as flat prose with a short summary of trigger scenarios
- Put detailed scenarios in a "When to invoke" body section, as prose bullets
- Cover both explicit and proactive triggering
- Describe situations the agent should respond to
- Mention phrasing variation in prose ("any phrasing — 'ready to ship', 'looks done'") rather than via multiple near-duplicate scenarios
- Keep trigger scenarios separate from output format
## Conclusion
Reliable triggering comes from prose descriptions of the situations an agent should respond to.

View File

@@ -0,0 +1,217 @@
#!/bin/bash
# Agent File Validator
# Validates agent markdown files for correct structure and content
set -euo pipefail
# Usage
if [ $# -eq 0 ]; then
echo "Usage: $0 <path/to/agent.md>"
echo ""
echo "Validates agent file for:"
echo " - YAML frontmatter structure"
echo " - Required fields (name, description, model, color)"
echo " - Field formats and constraints"
echo " - System prompt presence and length"
echo " - Example blocks in description"
exit 1
fi
AGENT_FILE="$1"
echo "🔍 Validating agent file: $AGENT_FILE"
echo ""
# Check 1: File exists
if [ ! -f "$AGENT_FILE" ]; then
echo "❌ File not found: $AGENT_FILE"
exit 1
fi
echo "✅ File exists"
# Check 2: Starts with ---
FIRST_LINE=$(head -1 "$AGENT_FILE")
if [ "$FIRST_LINE" != "---" ]; then
echo "❌ File must start with YAML frontmatter (---)"
exit 1
fi
echo "✅ Starts with frontmatter"
# Check 3: Has closing ---
if ! tail -n +2 "$AGENT_FILE" | grep -q '^---$'; then
echo "❌ Frontmatter not closed (missing second ---)"
exit 1
fi
echo "✅ Frontmatter properly closed"
# Extract frontmatter and system prompt
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$AGENT_FILE")
SYSTEM_PROMPT=$(awk '/^---$/{i++; next} i>=2' "$AGENT_FILE")
# Check 4: Required fields
echo ""
echo "Checking required fields..."
error_count=0
warning_count=0
# Check name field
NAME=$(echo "$FRONTMATTER" | grep '^name:' | sed 's/name: *//' | sed 's/^"\(.*\)"$/\1/')
if [ -z "$NAME" ]; then
echo "❌ Missing required field: name"
((error_count++))
else
echo "✅ name: $NAME"
# Validate name format
if ! [[ "$NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$ ]]; then
echo "❌ name must start/end with alphanumeric and contain only letters, numbers, hyphens"
((error_count++))
fi
# Validate name length
name_length=${#NAME}
if [ $name_length -lt 3 ]; then
echo "❌ name too short (minimum 3 characters)"
((error_count++))
elif [ $name_length -gt 50 ]; then
echo "❌ name too long (maximum 50 characters)"
((error_count++))
fi
# Check for generic names
if [[ "$NAME" =~ ^(helper|assistant|agent|tool)$ ]]; then
echo "⚠️ name is too generic: $NAME"
((warning_count++))
fi
fi
# Check description field
DESCRIPTION=$(echo "$FRONTMATTER" | grep '^description:' | sed 's/description: *//')
if [ -z "$DESCRIPTION" ]; then
echo "❌ Missing required field: description"
((error_count++))
else
desc_length=${#DESCRIPTION}
echo "✅ description: ${desc_length} characters"
if [ $desc_length -lt 10 ]; then
echo "⚠️ description too short (minimum 10 characters recommended)"
((warning_count++))
elif [ $desc_length -gt 5000 ]; then
echo "⚠️ description very long (over 5000 characters)"
((warning_count++))
fi
# Check for example blocks
if ! echo "$DESCRIPTION" | grep -q '<example>'; then
echo "⚠️ description should include <example> blocks for triggering"
((warning_count++))
fi
# Check for "Use this agent when" pattern
if ! echo "$DESCRIPTION" | grep -qi 'use this agent when'; then
echo "⚠️ description should start with 'Use this agent when...'"
((warning_count++))
fi
fi
# Check model field
MODEL=$(echo "$FRONTMATTER" | grep '^model:' | sed 's/model: *//')
if [ -z "$MODEL" ]; then
echo "❌ Missing required field: model"
((error_count++))
else
echo "✅ model: $MODEL"
case "$MODEL" in
inherit|sonnet|opus|haiku)
# Valid model
;;
*)
echo "⚠️ Unknown model: $MODEL (valid: inherit, sonnet, opus, haiku)"
((warning_count++))
;;
esac
fi
# Check color field
COLOR=$(echo "$FRONTMATTER" | grep '^color:' | sed 's/color: *//')
if [ -z "$COLOR" ]; then
echo "❌ Missing required field: color"
((error_count++))
else
echo "✅ color: $COLOR"
case "$COLOR" in
blue|cyan|green|yellow|magenta|red)
# Valid color
;;
*)
echo "⚠️ Unknown color: $COLOR (valid: blue, cyan, green, yellow, magenta, red)"
((warning_count++))
;;
esac
fi
# Check tools field (optional)
TOOLS=$(echo "$FRONTMATTER" | grep '^tools:' | sed 's/tools: *//')
if [ -n "$TOOLS" ]; then
echo "✅ tools: $TOOLS"
else
echo "💡 tools: not specified (agent has access to all tools)"
fi
# Check 5: System prompt
echo ""
echo "Checking system prompt..."
if [ -z "$SYSTEM_PROMPT" ]; then
echo "❌ System prompt is empty"
((error_count++))
else
prompt_length=${#SYSTEM_PROMPT}
echo "✅ System prompt: $prompt_length characters"
if [ $prompt_length -lt 20 ]; then
echo "❌ System prompt too short (minimum 20 characters)"
((error_count++))
elif [ $prompt_length -gt 10000 ]; then
echo "⚠️ System prompt very long (over 10,000 characters)"
((warning_count++))
fi
# Check for second person
if ! echo "$SYSTEM_PROMPT" | grep -q "You are\|You will\|Your"; then
echo "⚠️ System prompt should use second person (You are..., You will...)"
((warning_count++))
fi
# Check for structure
if ! echo "$SYSTEM_PROMPT" | grep -qi "responsibilities\|process\|steps"; then
echo "💡 Consider adding clear responsibilities or process steps"
fi
if ! echo "$SYSTEM_PROMPT" | grep -qi "output"; then
echo "💡 Consider defining output format expectations"
fi
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
echo "✅ All checks passed!"
exit 0
elif [ $error_count -eq 0 ]; then
echo "⚠️ Validation passed with $warning_count warning(s)"
exit 0
else
echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
exit 1
fi

View File

@@ -0,0 +1,3 @@
# Attribution
Skills vendored from https://github.com/asyrafhussin/agent-skills
License: see LICENSE

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Asyraf Hussin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,178 @@
---
name: optimizing-react-vite
description: React and Vite performance optimization guidelines. Use when optimizing React components built with Vite, or when tasks involve build optimization, code splitting, lazy loading, HMR, bundle size reduction, or React rendering performance. Do NOT trigger for purely non-performance Vite configuration tasks (e.g. adding a path alias, changing port).
license: MIT
metadata:
author: agent-skills
version: "2.0.0"
---
# React + Vite Best Practices
Comprehensive performance optimization guide for React applications built with Vite. Contains 23 rules across 6 categories for build optimization, code splitting, development performance, asset handling, environment configuration, and bundle analysis.
## Metadata
- **Version:** 2.0.0
- **Framework:** React + Vite
- **Rule Count:** 23 rules across 6 categories
- **License:** MIT
## When to Apply
Reference these guidelines when:
- Configuring Vite for React projects
- Implementing code splitting and lazy loading
- Optimizing build output and bundle size
- Setting up development environment and HMR
- Handling images, fonts, SVGs, and static assets
- Managing environment variables across environments
- Analyzing bundle size and dependencies
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Build Optimization | CRITICAL | `build-` |
| 2 | Code Splitting | CRITICAL | `split-` |
| 3 | Development | HIGH | `dev-` |
| 4 | Asset Handling | HIGH | `asset-` |
| 5 | Environment Config | MEDIUM | `env-` |
| 6 | Bundle Analysis | MEDIUM | `bundle-` |
## Quick Reference
### 1. Build Optimization (CRITICAL)
- `build-manual-chunks` - Configure manual chunks for vendor separation
- `build-minification` - Minification with OXC (default) or Terser
- `build-target-modern` - Target modern browsers (baseline-widely-available)
- `build-sourcemaps` - Configure sourcemaps per environment
- `build-tree-shaking` - Ensure proper tree shaking with ESM
- `build-compression` - Gzip and Brotli compression
- `build-asset-hashing` - Content-based hashing for cache busting
### 2. Code Splitting (CRITICAL)
- `split-route-lazy` - Route-based splitting with React.lazy()
- `split-suspense-boundaries` - Strategic Suspense boundary placement
- `split-dynamic-imports` - Dynamic import() for heavy components
- `split-component-lazy` - Lazy load non-critical components
- `split-prefetch-hints` - Prefetch chunks on hover/idle/viewport
### 3. Development (HIGH)
- `dev-dependency-prebundling` - Configure optimizeDeps for faster starts
- `dev-fast-refresh` - React Fast Refresh patterns
- `dev-hmr-config` - HMR server configuration
### 4. Asset Handling (HIGH)
- `asset-image-optimization` - Image optimization and lazy loading
- `asset-svg-components` - SVGs as React components with SVGR
- `asset-fonts` - Web font loading strategy
- `asset-public-dir` - Public directory vs JavaScript imports
### 5. Environment Config (MEDIUM)
- `env-vite-prefix` - VITE_ prefix for client variables
- `env-modes` - Mode-specific environment files
- `env-sensitive-data` - Never expose secrets in client code
### 6. Bundle Analysis (MEDIUM)
- `bundle-visualizer` - Analyze bundles with rollup-plugin-visualizer
## Essential Configurations
### Recommended vite.config.ts
```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'baseline-widely-available',
sourcemap: false,
chunkSizeWarningLimit: 500,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
optimizeDeps: {
include: ['react', 'react-dom'],
},
server: {
port: 3000,
hmr: {
overlay: true,
},
},
})
```
### Route-Based Code Splitting
```typescript
import { lazy, Suspense } from 'react'
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
{/* Routes here */}
</Suspense>
)
}
```
### Environment Variables
```typescript
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
```
## How to Use
> **Note:** The `rules/` subdirectory and `AGENTS.md` referenced below are not present in this skill's directory. Do not attempt to read them — apply the guidelines from this SKILL.md directly.
Apply the rules summarized in the Quick Reference above directly. The rule IDs (e.g. `build-manual-chunks`, `split-route-lazy`) serve as labels only — the Essential Configurations section above contains the canonical code examples.
## References
- [Vite Documentation](https://vite.dev)
- [React Documentation](https://react.dev)
- [Rollup Documentation](https://rollupjs.org)
## Full Compiled Document
> **Note:** `AGENTS.md` is not present in this skill's directory. The Quick Reference and Essential Configurations sections above contain the complete actionable guidance.

View File

@@ -0,0 +1,14 @@
skill: optimizing-react-vite
tasks:
- prompt: "Our React + Vite bundle is 3 MB. How do I get it under 1 MB?"
grader:
- the response invokes the optimizing-react-vite skill
- the response addresses code splitting, lazy loading, or dynamic import
- the response references Vite-specific config or build flags
- prompt: "How do I add a memoized selector to this React component?"
grader:
- the response invokes the optimizing-react-vite skill
- the response addresses React.memo, useMemo, or useCallback
- prompt: "Help me write a Python script to parse CSV"
grader:
- the response does NOT invoke the optimizing-react-vite skill

View File

@@ -0,0 +1,167 @@
---
name: improving-boocode-guidance
description: This skill should be used when the user asks to audit, review, check, improve, or critique CLAUDE.md, BOOCHAT.md, BOOCODER.md, or AGENTS.md files in a BooCode project. Examples: "audit my CLAUDE.md", "review my container guidance", "check this AGENTS.md for issues", "improve my BOOCHAT.md", "critique my BOOCODER.md".
---
# BooCode Guidance Improver
Audit guidance files in a BooCode project against a 10-dimension rubric, then propose targeted edits. **Read-only.** Output is a scored report plus before/after edit proposals; Sam reviews and commits.
## Phase 1 — Discovery
Find every guidance file in the project. The expected set:
- `CLAUDE.md` (repo root) — engineering conventions, gotchas, commands
- `BOOCHAT.md` (repo root) — container guidance for the read-only chat surface
- `BOOCODER.md` (repo root) — container guidance for the future write-capable surface (currently a stub)
- `data/AGENTS.md` — single-file tier-2 agent registry, `## H2` per agent
- `AGENTS.md` (repo root) — non-BooCode convention; rare in this repo
Glob with `find_files` then load each with `view_file`:
```
find_files: pattern="{CLAUDE,BOOCHAT,BOOCODER,AGENTS}.md", path="."
find_files: pattern="data/AGENTS.md", path="."
```
If a file expected by the project's architecture is missing (e.g. BOOCHAT.md is absent from the repo root in a project that exposes a chat container), flag it in the report as a separate "Missing" entry — don't try to score what isn't there. Likewise, if a file exists but is empty (≤5 lines, no real content), score it 1 across the board and recommend it be either populated or deleted; an empty guidance file is worse than no file because it consumes attention without paying any back.
## Phase 2 — Score against the rubric
For each file, score each of the 10 dimensions on 15 (1 = absent or actively misleading; 5 = exemplary). Use the rubric below verbatim. Cite a representative line range for each score.
### a. Refusal rails up front
The first ~10 lines name explicit "do not" directives — what the agent must not do, ideally with a one-line reason. Surfacing refusals early prevents the model from acting on a hopeful misread later.
- **5** — first 10 lines contain ≥3 explicit refusals (e.g. *"Do not commit"*, *"Do not push"*, *"Do not write files"*) with brief reasons or contexts
- **3** — refusals exist but are buried below line 30, or stated only once without context
- **1** — no refusals anywhere; the agent has to infer constraints from positive instructions only
### b. Version anchor
A concrete version, tag, or date is mentioned near the top so a stale memory becomes obvious to a future reader. Pure "current" / "latest" claims rot silently.
- **5** — version/tag in the first 20 lines, plus a "last meaningful update" date inline somewhere
- **3** — a version tag exists but only deep in the file (e.g. inside a commit-history block)
- **1** — no version, no date, no anchor; nothing to detect staleness against
### c. Why-with-what
Every non-obvious convention or rule is followed by a one-line justification (`Why:` / `Reason:` / dash). Rules without reasons can't be reasoned about at the edges; they get either blindly followed or quietly violated.
- **5** — every non-trivial rule has a sentence-level "why" inline
- **3** — most rules have reasons, but a few load-bearing ones (e.g. "use overflowWrap not wordWrap") are bare
- **1** — rules read as commandments with no rationale
### d. Authoritative vs misleading sources
Places where a tool can lie (e.g. *"root `tsc --noEmit` uses project references and can miss errors that the per-app tsconfig catches"*) are called out, and the authoritative path is named. Without this, the agent picks the most convenient signal and ships a regression.
- **5** — at least one explicit "X can lie; use Y instead" pair, named with file paths
- **3** — implicit hints ("CLI is authoritative") without naming what the misleading signal is
- **1** — no acknowledgement that any tool can lie
### e. Resolution order
For any stacked configuration (system prompts, env vars, agent definitions, schemas), the precedence is documented end-to-end with what wins on conflict. Missing precedence rules force the agent to guess at boundaries.
- **5** — explicit ordered list (e.g. *"base → container guidance → agent.system_prompt → user prompt"*) with "last wins" or "first wins" stated
- **3** — order is implied by section sequence but not stated; precedence on conflict is unclear
- **1** — multiple sources mentioned, no order, no winner
### f. Failure modes
Each subsystem has a "what happens when this fails" note — fallbacks, defaults, swallow vs propagate decisions. Without this the agent assumes the happy path and writes brittle code.
- **5** — every major subsystem (DB, broker, LLM call, tool execution) names its failure behavior
- **3** — some failure paths documented, others implicit
- **1** — failure modes invisible; reader can't tell what's defensive and what isn't
### g. Don't / refusals (deep)
Beyond the top-of-file refusal rails, the body contains a sustained "don't" thread — anti-patterns the project has burned on. Each "don't" should name what triggered it (PR, incident, refactor) so it can be re-evaluated.
- **5** — multiple "don't" entries scattered through the file, each with a hint at the triggering context
- **3** — a handful of "don't"s, no context — reader can't tell what's still load-bearing
- **1** — pure positive instructions; no anti-pattern surface
### h. Concrete call sites
Specific file paths and symbol names are used (e.g. `apps/server/src/services/inference.ts:209-225 buildSystemPrompt`), not vague pointers ("in the service layer", "somewhere in tools"). Vague pointers force the agent into an extra search round-trip per claim.
- **5** — claims about code consistently cite file:line or file:symbol (e.g. *"buildSystemPrompt at apps/server/src/services/system-prompt.ts:42"*)
- **3** — some claims cite paths but not lines or symbols (*"in apps/server/src/services/inference.ts"*)
- **1** — claims read like "the broker handles pub/sub" with no path at all
A reliable test for this dimension: pick three random claims about behaviour, and try to land at the named code in two clicks. If you can't, the score drops.
### i. Convention drift guards
Pairs of files that must stay in sync are named explicitly (e.g. *"CHECK constraints in schema.sql ↔ `*_STATUSES` const arrays in `apps/server/src/types/api.ts`"*). Without these guards, one half drifts and the test that would catch it doesn't exist.
- **5** — every cross-file invariant in the project has a "keep in sync" callout
- **3** — one or two such guards present; obvious sibling files (frontend type ↔ backend type) not mentioned
- **1** — invariants are invisible; every edit risks silent divergence
### j. No theater
Every line earns its keep. No "be helpful", no "remember to think step by step", no "as an AI assistant" preamble. Theater wastes tokens and trains the model to skim.
- **5** — every line carries either a fact, a rule, or a pointer; reads tight
- **3** — a few filler sentences ("strive for excellence", "remember to think carefully") but mostly substantive
- **1** — heavy preamble, motivational platitudes, or restated framework defaults
Worth a separate pass: re-read the file and ask "would removing this line confuse a future reader?" — if the honest answer is no, the line is theater and should go.
## Phase 3 — Propose one concrete edit per ≤3
For every dimension scoring 3 or lower, generate one specific edit proposal. Each proposal must be:
- **File**: full repo-relative path
- **Anchor**: a quoted ~one-line existing string or `(new section after L<n>)`
- **Before**: existing text (or `(none)`)
- **After**: proposed text
- **Why**: one sentence linking back to the rubric dimension and what the change unlocks
Example proposal:
```
### Proposed edit 1 — dimension (a) Refusal rails up front
File: BOOCHAT.md
Anchor: "## Capabilities" (L3)
Before:
## Capabilities
After:
## You cannot
- Write, edit, or delete files
- Run shell commands
- Make commits, push, or pull
## Capabilities
Why: the upstream rubric requires explicit "do not" rails in the first 10 lines so the
model can't reach for a write tool and self-justify after the fact.
```
Keep proposals minimal. One edit per dimension scoring ≤3 — don't pad. If a single edit would lift two dimensions at once, say so and don't double-count.
Do not propose more than ~10 edits per file. If a file scores ≤3 on more than 10 dimensions (rare), the file needs a rewrite, not patches — say that instead, and propose a high-level outline rather than a flood of line-level edits.
## Phase 4 — Output
Output as a single numbered list, in this order:
1. Per-file score table: 10 rows × score column × one-line evidence column
2. Per-file aggregate (sum out of 50) and overall grade band: A (≥45), B (3544), C (2534), D (1524), F (<15)
3. Proposed edits, numbered globally across all files
4. Closing one-line summary: *"X files audited, Y edits proposed, top weak dimension across files: Z."*
Do not edit any file. Do not call any write tool. Sam reads the report, picks which edits to apply, and commits them manually.
## Anti-patterns this skill explicitly avoids
- Auto-generating CLAUDE.md from scratch (different problem — that's `claude-md-improver`'s domain)
- Scoring the *project's* code quality (out of scope — this rubric is about guidance files only)
- Padding the report with generic "best practices" not tied to one of the 10 dimensions
- Restating the rubric in every per-file section (state it once at the top, reference dimensions by letter throughout)

View File

@@ -0,0 +1,15 @@
skill: improving-boocode-guidance
tasks:
- prompt: "Audit my CLAUDE.md and tell me what to improve"
grader:
- the response invokes the improving-boocode-guidance skill
- the response scores against the 10-dimension rubric
- the response cites line ranges in CLAUDE.md
- the response proposes before/after edits, not just complaints
- prompt: "Check my BOOCHAT.md for issues"
grader:
- the response invokes the improving-boocode-guidance skill
- the response evaluates the file against the rubric
- prompt: "Explain how Docker layer caching works"
grader:
- the response does NOT invoke the improving-boocode-guidance skill

View File

@@ -0,0 +1,6 @@
# Attribution
Skills in this directory are vendored from https://github.com/mattpocock/skills
License: see LICENSE
Vendored: 2026-05-17
Author: Matt Pocock

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Matt Pocock
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,117 @@
---
name: diagnosing-bugs
description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing/not working/something wrong, or describes a performance regression.
---
# Diagnose
A discipline for hard bugs. Skip phases only when explicitly justified.
When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
## Phase 1 — Build a feedback loop
**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
### Ways to construct one — try them in roughly this order
1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.
2. **Curl / HTTP script** against a running dev server.
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.
5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.
6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it.
9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.
10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
### Iterate on the loop itself
Treat the loop as a product. Once you have _a_ loop, ask:
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.
### Non-deterministic bugs
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.
### When you genuinely cannot build a loop
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.
Do not proceed to Phase 2 until you have a loop you believe in.
## Phase 2 — Reproduce
Run the loop. Watch the bug appear.
Confirm:
- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
Do not proceed until you reproduce the bug.
## Phase 3 — Hypothesise
Generate **35 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must be **falsifiable**: state the prediction it makes.
> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.
**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.
## Phase 4 — Instrument
Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.**
Tool preference:
1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.
2. **Targeted logs** at the boundaries that distinguish hypotheses.
3. Never "log everything and grep".
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
## Phase 5 — Fix + regression test
Write the regression test **before the fix** — but only if there is a **correct seam** for it.
A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If a correct seam exists:
1. Turn the minimised repro into a failing test at that seam.
2. Watch it fail.
3. Apply the fix.
4. Watch it pass.
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
## Phase 6 — Cleanup + post-mortem
Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns
**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.

View File

@@ -0,0 +1,14 @@
skill: diagnosing-bugs
tasks:
- prompt: "Diagnose this: the API returns 500 every Friday afternoon but works fine other days"
grader:
- the response invokes the diagnosing-bugs skill
- the response asks for or constructs a feedback loop (failing test, repro script, captured trace)
- the response does not jump to a fix before establishing the loop
- prompt: "My tests have started failing intermittently after the v1.4 deploy"
grader:
- the response invokes the diagnosing-bugs skill
- the response addresses determinism (seeded RNG, time pinning, isolated env)
- prompt: "Write a haiku about autumn"
grader:
- the response does NOT invoke the diagnosing-bugs skill

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Human-in-the-loop reproduction loop.
# Copy this file, edit the steps below, and run it.
# The agent runs the script; the user follows prompts in their terminal.
#
# Usage:
# bash hitl-loop.template.sh
#
# Two helpers:
# step "<instruction>" → show instruction, wait for Enter
# capture VAR "<question>" → show question, read response into VAR
#
# At the end, captured values are printed as KEY=VALUE for the agent to parse.
set -euo pipefail
step() {
printf '\n>>> %s\n' "$1"
read -r -p " [Enter when done] " _
}
capture() {
local var="$1" question="$2" answer
printf '\n>>> %s\n' "$question"
read -r -p " > " answer
printf -v "$var" '%s' "$answer"
}
# --- edit below ---------------------------------------------------------
step "Open the app at http://localhost:3000 and sign in."
capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)"
capture ERROR_MSG "Paste the error message (or 'none'):"
# --- edit above ---------------------------------------------------------
printf '\n--- Captured ---\n'
printf 'ERRORED=%s\n' "$ERRORED"
printf 'ERROR_MSG=%s\n' "$ERROR_MSG"

View File

@@ -0,0 +1,20 @@
---
name: grilling-plans
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, poke holes in a design, challenge an approach, play devil's advocate, asks "what am I missing", or mentions "grill me".
---
## Tool integration — MANDATORY
When a question has 2-4 discrete options, you MUST call the `ask_user_input` tool. Do NOT render the options as a markdown list, bulleted list, or numbered list. Do NOT format them as text under any circumstance.
For open-ended (free-form answer) questions, plain prose is fine.
If you find yourself about to write `- Option A\n- Option B\n- Option C`, STOP. Call ask_user_input instead.
## Interview
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time.
If a question can be answered by exploring the codebase, explore the codebase instead.

View File

@@ -0,0 +1,14 @@
skill: grilling-plans
tasks:
- prompt: "Grill me on this plan to migrate from REST to GraphQL"
grader:
- the response invokes the grilling-plans skill
- the response asks pointed questions about the plan
- the response surfaces decision-tree branches rather than agreeing
- prompt: "Stress-test my design for a new auth flow"
grader:
- the response invokes the grilling-plans skill
- the response probes assumptions rather than confirming them
- prompt: "What's the capital of France?"
grader:
- the response does NOT invoke the grilling-plans skill

View File

@@ -0,0 +1,122 @@
---
name: writing-skills
description: Propose new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to draft, write, create, or design a new skill.
---
# Writing Skills
> BooChat adaptation: this skill runs in a read-only environment. The "draft the skill" step outputs **proposed file paths and full file contents** as text — it does NOT create directories or write files. Sam mkdir's and commits manually.
## Process
1. **Gather requirements** - ask user about:
- What task/domain does the skill cover?
- What specific use cases should it handle?
- Does it need executable scripts or just instructions?
- Any reference materials to include?
2. **Propose the skill** - output as a single response:
- The target directory path (e.g. `/opt/skills/<group>/<skill-name>/`)
- The full proposed `SKILL.md` content in a fenced block, prefixed with its target filename
- Any additional reference files (`REFERENCE.md`, `EXAMPLES.md`) as separate fenced blocks if content exceeds 500 lines
- Any utility scripts as separate fenced blocks
- Do NOT call any write/edit/mkdir tool — output is text only
3. **Review with user** - present the proposal and ask:
- Does this cover your use cases?
- Anything missing or unclear?
- Should any section be more/less detailed?
## Skill Structure
```
skill-name/
├── SKILL.md # Main instructions (required)
├── REFERENCE.md # Detailed docs (if needed)
├── EXAMPLES.md # Usage examples (if needed)
└── scripts/ # Utility scripts (if needed)
└── helper.js
```
## SKILL.md Template
```md
---
name: skill-name
description: Brief description of capability. Use when [specific triggers].
---
# Skill Name
## Quick start
[Minimal working example]
## Workflows
[Step-by-step processes with checklists for complex tasks]
## Advanced features
[Link to separate files: See [REFERENCE.md](REFERENCE.md)]
```
## Description Requirements
The description is **the only thing your agent sees** when deciding which skill to load. It's surfaced in the system prompt alongside all other installed skills. Your agent reads these descriptions and picks the relevant skill based on the user's request.
**Goal**: Give your agent just enough info to know:
1. What capability this skill provides
2. When/why to trigger it (specific keywords, contexts, file types)
**Format**:
- Max 1024 chars
- Write in third person
- First sentence: what it does
- Second sentence: "Use when [specific triggers]"
**Good example**:
```
Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when user mentions PDFs, forms, or document extraction.
```
**Bad example**:
```
Helps with documents.
```
The bad example gives your agent no way to distinguish this from other document skills.
## When to Add Scripts
Add utility scripts when:
- Operation is deterministic (validation, formatting)
- Same code would be generated repeatedly
- Errors need explicit handling
Scripts save tokens and improve reliability vs generated code.
## When to Split Files
Split into separate files when:
- SKILL.md exceeds 100 lines
- Content has distinct domains (finance vs sales schemas)
- Advanced features are rarely needed
## Review Checklist
After drafting, verify:
- [ ] Description includes triggers ("Use when...")
- [ ] Description ≤1024 chars
- [ ] SKILL.md under 100 lines
- [ ] No time-sensitive info
- [ ] Consistent terminology
- [ ] Concrete examples included
- [ ] References one level deep

View File

@@ -0,0 +1,15 @@
skill: writing-skills
tasks:
- prompt: "Help me draft a new skill for scaffolding Fastify routes"
grader:
- the response invokes the writing-skills skill
- the response produces a SKILL.md with proper frontmatter (name, description)
- the response uses progressive disclosure (references/ for bulk)
- the response uses gerund naming convention
- prompt: "Design a skill that triggers when the user asks for a database schema review"
grader:
- the response invokes the writing-skills skill
- the response writes a description with specific trigger phrases
- prompt: "Recommend a JavaScript framework"
grader:
- the response does NOT invoke the writing-skills skill

View File

@@ -0,0 +1,6 @@
# Attribution
Skills in this directory are vendored from https://github.com/obra/superpowers
License: MIT (see LICENSE in this directory)
Vendored: 2026-05-17
Author: Jesse Vincent (obra) and contributors

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Jesse Vincent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,164 @@
---
name: brainstorming
description: "You MUST use this before any creative work - creating features, building components, adding functionality, refactoring, or making non-trivial modifications to existing behavior. Explores user intent, requirements and design before implementation."
---
# Brainstorming Ideas Into Designs
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>
## Anti-Pattern: "This Is Too Simple To Need A Design"
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
## Checklist
You MUST create a task for each of these items and complete them in order:
1. **Explore project context** — check files, docs, recent commits
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
## Process Flow
```dot
digraph brainstorming {
"Explore project context" [shape=box];
"Visual questions ahead?" [shape=diamond];
"Offer Visual Companion\n(own message, no other content)" [shape=box];
"Ask clarifying questions" [shape=box];
"Propose 2-3 approaches" [shape=box];
"Present design sections" [shape=box];
"User approves design?" [shape=diamond];
"Write design doc" [shape=box];
"Spec self-review\n(fix inline)" [shape=box];
"User reviews spec?" [shape=diamond];
"Invoke writing-plans skill" [shape=doublecircle];
"Explore project context" -> "Visual questions ahead?";
"Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
"Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
"Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
"Ask clarifying questions" -> "Propose 2-3 approaches";
"Propose 2-3 approaches" -> "Present design sections";
"Present design sections" -> "User approves design?";
"User approves design?" -> "Present design sections" [label="no, revise"];
"User approves design?" -> "Write design doc" [label="yes"];
"Write design doc" -> "Spec self-review\n(fix inline)";
"Spec self-review\n(fix inline)" -> "User reviews spec?";
"User reviews spec?" -> "Write design doc" [label="changes requested"];
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
```
**The terminal state is invoking writing-plans.** Do NOT invoke designing-frontends, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
## The Process
**Understanding the idea:**
- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria
**Exploring approaches:**
- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why
**Presenting the design:**
- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense
**Design for isolation and clarity:**
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
**Working in existing codebases:**
- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
## After the Design
**Documentation:**
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
Fix any issues inline. No need to re-review — just fix and move on.
**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
**Implementation:**
- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.
## Key Principles
- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense
## Visual Companion
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`

View File

@@ -0,0 +1,15 @@
skill: brainstorming
tasks:
- prompt: "Build a feature that lets users export their chat history as PDF"
grader:
- the response invokes the brainstorming skill
- the response explores user intent, requirements, and design before implementation
- the response does NOT jump straight to writing code
- the terminal state is invoking writing-plans, not implementation
- prompt: "Add a real-time notifications panel to the sidebar"
grader:
- the response invokes the brainstorming skill
- the response asks clarifying questions about scope and constraints
- prompt: "What is 2 + 2?"
grader:
- the response does NOT invoke the brainstorming skill

View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Superpowers Brainstorming</title>
<style>
/*
* BRAINSTORM COMPANION FRAME TEMPLATE
*
* This template provides a consistent frame with:
* - OS-aware light/dark theming
* - Fixed header and selection indicator bar
* - Scrollable main content area
* - CSS helpers for common UI patterns
*
* Content is injected via placeholder comment in #claude-content.
*/
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
/* ===== THEME VARIABLES ===== */
:root {
--bg-primary: #f5f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #e5e5e7;
--border: #d1d1d6;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--text-tertiary: #aeaeb2;
--accent: #0071e3;
--accent-hover: #0077ed;
--success: #34c759;
--warning: #ff9f0a;
--error: #ff3b30;
--selected-bg: #e8f4fd;
--selected-border: #0071e3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1d1d1f;
--bg-secondary: #2d2d2f;
--bg-tertiary: #3d3d3f;
--border: #424245;
--text-primary: #f5f5f7;
--text-secondary: #86868b;
--text-tertiary: #636366;
--accent: #0a84ff;
--accent-hover: #409cff;
--selected-bg: rgba(10, 132, 255, 0.15);
--selected-border: #0a84ff;
}
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
flex-direction: column;
line-height: 1.5;
}
/* ===== FRAME STRUCTURE ===== */
.header {
background: var(--bg-secondary);
padding: 0.5rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
.header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; }
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; }
.main { flex: 1; overflow-y: auto; }
#claude-content { padding: 2rem; min-height: 100%; }
.indicator-bar {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 0.5rem 1.5rem;
flex-shrink: 0;
text-align: center;
}
.indicator-bar span {
font-size: 0.75rem;
color: var(--text-secondary);
}
.indicator-bar .selected-text {
color: var(--accent);
font-weight: 500;
}
/* ===== TYPOGRAPHY ===== */
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
.section { margin-bottom: 2rem; }
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
/* ===== OPTIONS (for A/B/C choices) ===== */
.options { display: flex; flex-direction: column; gap: 0.75rem; }
.option {
background: var(--bg-secondary);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem 1.25rem;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: flex-start;
gap: 1rem;
}
.option:hover { border-color: var(--accent); }
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
.option .letter {
background: var(--bg-tertiary);
color: var(--text-secondary);
width: 1.75rem; height: 1.75rem;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
}
.option.selected .letter { background: var(--accent); color: white; }
.option .content { flex: 1; }
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
/* ===== CARDS (for showing designs/mockups) ===== */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card.selected { border-color: var(--selected-border); border-width: 2px; }
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
.card-body { padding: 1rem; }
.card-body h3 { margin-bottom: 0.25rem; }
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
/* ===== MOCKUP CONTAINER ===== */
.mockup {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
margin-bottom: 1.5rem;
}
.mockup-header {
background: var(--bg-tertiary);
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
}
.mockup-body { padding: 1.5rem; }
/* ===== SPLIT VIEW (side-by-side comparison) ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
/* ===== PROS/CONS ===== */
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
.pros li, .cons li { margin-bottom: 0.25rem; }
/* ===== PLACEHOLDER (for mockup areas) ===== */
.placeholder {
background: var(--bg-tertiary);
border: 2px dashed var(--border);
border-radius: 8px;
padding: 2rem;
text-align: center;
color: var(--text-tertiary);
}
/* ===== INLINE MOCKUP ELEMENTS ===== */
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
.mock-content { padding: 1.5rem; flex: 1; }
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
</style>
</head>
<body>
<div class="header">
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
<div class="status">Connected</div>
</div>
<div class="main">
<div id="claude-content">
<!-- CONTENT -->
</div>
</div>
<div class="indicator-bar">
<span id="indicator-text">Click an option above, then return to the terminal</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,88 @@
(function() {
const WS_URL = 'ws://' + window.location.host;
let ws = null;
let eventQueue = [];
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
eventQueue = [];
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'reload') {
window.location.reload();
}
};
ws.onclose = () => {
setTimeout(connect, 1000);
};
}
function sendEvent(event) {
event.timestamp = Date.now();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(event));
} else {
eventQueue.push(event);
}
}
// Capture clicks on choice elements
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-choice]');
if (!target) return;
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice,
id: target.id || null
});
// Update indicator bar (defer so toggleSelect runs first)
setTimeout(() => {
const indicator = document.getElementById('indicator-text');
if (!indicator) return;
const container = target.closest('.options') || target.closest('.cards');
const selected = container ? container.querySelectorAll('.selected') : [];
if (selected.length === 0) {
indicator.textContent = 'Click an option above, then return to the terminal';
} else if (selected.length === 1) {
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
} else {
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
}
}, 0);
});
// Frame UI: selection tracking
window.selectedChoice = null;
window.toggleSelect = function(el) {
const container = el.closest('.options') || el.closest('.cards');
const multi = container && container.dataset.multiselect !== undefined;
if (container && !multi) {
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
}
if (multi) {
el.classList.toggle('selected');
} else {
el.classList.add('selected');
}
window.selectedChoice = el.dataset.choice;
};
// Expose API for explicit use
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
};
connect();
})();

View File

@@ -0,0 +1,354 @@
const crypto = require('crypto');
const http = require('http');
const fs = require('fs');
const path = require('path');
// ========== WebSocket Protocol (RFC 6455) ==========
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
}
function encodeFrame(opcode, payload) {
const fin = 0x80;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = fin | opcode;
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = fin | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = fin | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
return Buffer.concat([header, payload]);
}
function decodeFrame(buffer) {
if (buffer.length < 2) return null;
const secondByte = buffer[1];
const opcode = buffer[0] & 0x0F;
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7F;
let offset = 2;
if (!masked) throw new Error('Client frames must be masked');
if (payloadLen === 126) {
if (buffer.length < 4) return null;
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return null;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskOffset = offset;
const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen;
if (buffer.length < totalLen) return null;
const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
}
return { opcode, payload: data, bytesConsumed: totalLen };
}
// ========== Configuration ==========
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
const STATE_DIR = path.join(SESSION_DIR, 'state');
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
const MIME_TYPES = {
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
};
// ========== Templates and Constants ==========
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }</style>
</head>
<body><h1>Brainstorm Companion</h1>
<p>Waiting for the agent to push a screen...</p></body></html>`;
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = '<script>\n' + helperScript + '\n</script>';
// ========== Helper Functions ==========
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
function getNewestScreen() {
const files = fs.readdirSync(CONTENT_DIR)
.filter(f => f.endsWith('.html'))
.map(f => {
const fp = path.join(CONTENT_DIR, f);
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
// ========== HTTP Request Handler ==========
function handleRequest(req, res) {
touchActivity();
if (req.method === 'GET' && req.url === '/') {
const screenFile = getNewestScreen();
let html = screenFile
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
: WAITING_PAGE;
if (html.includes('</body>')) {
html = html.replace('</body>', helperInjection + '\n</body>');
} else {
html += helperInjection;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
const fileName = req.url.slice(7);
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not found');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(fs.readFileSync(filePath));
} else {
res.writeHead(404);
res.end('Not found');
}
}
// ========== WebSocket Connection Handling ==========
const clients = new Set();
function handleUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
if (!key) { socket.destroy(); return; }
const accept = computeAcceptKey(key);
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
);
let buffer = Buffer.alloc(0);
clients.add(socket);
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length > 0) {
let result;
try {
result = decodeFrame(buffer);
} catch (e) {
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
}
if (!result) break;
buffer = buffer.slice(result.bytesConsumed);
switch (result.opcode) {
case OPCODES.TEXT:
handleMessage(result.payload.toString());
break;
case OPCODES.CLOSE:
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
case OPCODES.PING:
socket.write(encodeFrame(OPCODES.PONG, result.payload));
break;
case OPCODES.PONG:
break;
default: {
const closeBuf = Buffer.alloc(2);
closeBuf.writeUInt16BE(1003);
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
clients.delete(socket);
return;
}
}
}
});
socket.on('close', () => clients.delete(socket));
socket.on('error', () => clients.delete(socket));
}
function handleMessage(text) {
let event;
try {
event = JSON.parse(text);
} catch (e) {
console.error('Failed to parse WebSocket message:', e.message);
return;
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(STATE_DIR, 'events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
function broadcast(msg) {
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
for (const socket of clients) {
try { socket.write(frame); } catch (e) { clients.delete(socket); }
}
}
// ========== Activity Tracking ==========
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
let lastActivity = Date.now();
function touchActivity() {
lastActivity = Date.now();
}
// ========== File Watching ==========
const debounceTimers = new Map();
// ========== Server Startup ==========
function startServer() {
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
// Track known files to distinguish new screens from updates.
// macOS fs.watch reports 'rename' for both new files and overwrites,
// so we can't rely on eventType alone.
const knownFiles = new Set(
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
);
const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);
const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
if (!filename || !filename.endsWith('.html')) return;
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
debounceTimers.set(filename, setTimeout(() => {
debounceTimers.delete(filename);
const filePath = path.join(CONTENT_DIR, filename);
if (!fs.existsSync(filePath)) return; // file was deleted
touchActivity();
if (!knownFiles.has(filename)) {
knownFiles.add(filename);
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
} else {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
}
broadcast({ type: 'reload' });
}, 100));
});
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
function shutdown(reason) {
console.log(JSON.stringify({ type: 'server-stopped', reason }));
const infoFile = path.join(STATE_DIR, 'server-info');
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
fs.writeFileSync(
path.join(STATE_DIR, 'server-stopped'),
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
);
watcher.close();
clearInterval(lifecycleCheck);
server.close(() => process.exit(0));
}
function ownerAlive() {
if (!ownerPid) return true;
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
}
// Check every 60s: exit if owner process died or idle for 30 minutes
const lifecycleCheck = setInterval(() => {
if (!ownerAlive()) shutdown('owner process exited');
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
}, 60 * 1000);
lifecycleCheck.unref();
// Validate owner PID at startup. If it's already dead, the PID resolution
// was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
// Disable monitoring and rely on the idle timeout instead.
if (ownerPid) {
try { process.kill(ownerPid, 0); }
catch (e) {
if (e.code !== 'EPERM') {
console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
ownerPid = null;
}
}
}
server.listen(PORT, HOST, () => {
const info = JSON.stringify({
type: 'server-started', port: Number(PORT), host: HOST,
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
screen_dir: CONTENT_DIR, state_dir: STATE_DIR
});
console.log(info);
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
});
}
if (require.main === module) {
startServer();
}
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Start the brainstorm server and output connection info
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
#
# Starts server on a random high port, outputs JSON with URL.
# Each session gets its own directory to avoid conflicts.
#
# Options:
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
# instead of /tmp. Files persist after server stops.
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
# Use 0.0.0.0 in remote/containerized environments.
# --url-host <host> Hostname shown in returned URL JSON.
# --foreground Run server in the current terminal (no backgrounding).
# --background Force background mode (overrides Codex auto-foreground).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Parse arguments
PROJECT_DIR=""
FOREGROUND="false"
FORCE_BACKGROUND="false"
BIND_HOST="127.0.0.1"
URL_HOST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--project-dir)
PROJECT_DIR="$2"
shift 2
;;
--host)
BIND_HOST="$2"
shift 2
;;
--url-host)
URL_HOST="$2"
shift 2
;;
--foreground|--no-daemon)
FOREGROUND="true"
shift
;;
--background|--daemon)
FORCE_BACKGROUND="true"
shift
;;
*)
echo "{\"error\": \"Unknown argument: $1\"}"
exit 1
;;
esac
done
if [[ -z "$URL_HOST" ]]; then
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
URL_HOST="localhost"
else
URL_HOST="$BIND_HOST"
fi
fi
# Some environments reap detached/background processes. Auto-foreground when detected.
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
FOREGROUND="true"
fi
# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
esac
if [[ -n "${MSYSTEM:-}" ]]; then
FOREGROUND="true"
fi
fi
# Generate unique session directory
SESSION_ID="$$-$(date +%s)"
if [[ -n "$PROJECT_DIR" ]]; then
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
LOG_FILE="${STATE_DIR}/server.log"
# Create fresh session directory with content and state peers
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
# Kill any existing server
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill "$old_pid" 2>/dev/null
rm -f "$PID_FILE"
fi
cd "$SCRIPT_DIR"
# Resolve the harness PID (grandparent of this script).
# $PPID is the ephemeral shell the harness spawned to run us — it dies
# when this script exits. The harness itself is $PPID's parent.
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
OWNER_PID="$PPID"
fi
# Foreground mode for environments that reap detached/background processes.
if [[ "$FOREGROUND" == "true" ]]; then
echo "$$" > "$PID_FILE"
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
exit $?
fi
# Start server, capturing output to log file
# Use nohup to survive shell exit; disown to remove from job table
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
disown "$SERVER_PID" 2>/dev/null
echo "$SERVER_PID" > "$PID_FILE"
# Wait for server-started message (check log file)
for i in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
# Verify server is still alive after a short window (catches process reapers)
alive="true"
for _ in {1..20}; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
alive="false"
break
fi
sleep 0.1
done
if [[ "$alive" != "true" ]]; then
echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
exit 1
fi
grep "server-started" "$LOG_FILE" | head -1
exit 0
fi
sleep 0.1
done
# Timeout - server didn't start
echo '{"error": "Server failed to start within 5 seconds"}'
exit 1

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Stop the brainstorm server and clean up
# Usage: stop-server.sh <session_dir>
#
# Kills the server process. Only deletes session directory if it's
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
# kept so mockups can be reviewed later.
SESSION_DIR="$1"
if [[ -z "$SESSION_DIR" ]]; then
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
exit 1
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
# Try to stop gracefully, fallback to force if still alive
kill "$pid" 2>/dev/null || true
# Wait for graceful shutdown (up to ~2s)
for i in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
sleep 0.1
done
# If still running, escalate to SIGKILL
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
# Give SIGKILL a moment to take effect
sleep 0.1
fi
if kill -0 "$pid" 2>/dev/null; then
echo '{"status": "failed", "error": "process still running"}'
exit 1
fi
rm -f "$PID_FILE" "${STATE_DIR}/server.log"
# Only delete ephemeral /tmp directories
if [[ "$SESSION_DIR" == /tmp/* ]]; then
rm -rf "$SESSION_DIR"
fi
echo '{"status": "stopped"}'
else
echo '{"status": "not_running"}'
fi

View File

@@ -0,0 +1,49 @@
# Spec Document Reviewer Prompt Template
Use this template when dispatching a spec document reviewer subagent.
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
**Dispatch after:** Spec document is written to docs/superpowers/specs/
```
Task tool (general-purpose):
description: "Review spec document"
prompt: |
You are a spec document reviewer. Verify this spec is complete and ready for planning.
**Spec to review:** [SPEC_FILE_PATH]
## What to Check
| Category | What to Look For |
|----------|------------------|
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
| Consistency | Internal contradictions, conflicting requirements |
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
| YAGNI | Unrequested features, over-engineering |
## Calibration
**Only flag issues that would cause real problems during implementation planning.**
A missing section, a contradiction, or a requirement so ambiguous it could be
interpreted two different ways — those are issues. Minor wording improvements,
stylistic preferences, and "sections less detailed than others" are not.
Approve unless there are serious gaps that would lead to a flawed plan.
## Output Format
## Spec Review
**Status:** Approved | Issues Found
**Issues (if any):**
- [Section X]: [specific issue] - [why it matters for planning]
**Recommendations (advisory, do not block approval):**
- [suggestions for improvement]
```
**Reviewer returns:** Status, Issues (if any), Recommendations

View File

@@ -0,0 +1,287 @@
# Visual Companion Guide
Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
## When to Use
Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
**Use the browser** when the content itself is visual:
- **UI mockups** — wireframes, layouts, navigation structures, component designs
- **Architecture diagrams** — system components, data flow, relationship maps
- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
**Use the terminal** when the content is text or tabular:
- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
- **Conceptual A/B/C choices** — picking between approaches described in words
- **Tradeoff lists** — pros/cons, comparison tables
- **Technical decisions** — API design, data modeling, architectural approach selection
- **Clarifying questions** — anything where the answer is words, not a visual preference
A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
## How It Works
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
## Starting a Session
```bash
# Start server with persistence (mockups saved to project)
scripts/start-server.sh --project-dir /path/to/project
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
```
Save `screen_dir` and `state_dir` from the response. Tell user to open the URL.
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
**Launching the server by platform:**
**Claude Code (macOS / Linux):**
```bash
# Default mode works — the script backgrounds the server itself
scripts/start-server.sh --project-dir /path/to/project
```
**Claude Code (Windows):**
```bash
# Windows auto-detects and uses foreground mode, which blocks the tool call.
# Use run_in_background: true on the Bash tool call so the server survives
# across conversation turns.
scripts/start-server.sh --project-dir /path/to/project
```
When calling this via the Bash tool, set `run_in_background: true`. Then read `$STATE_DIR/server-info` on the next turn to get the URL and port.
**Codex:**
```bash
# Codex reaps background processes. The script auto-detects CODEX_CI and
# switches to foreground mode. Run it normally — no extra flags needed.
scripts/start-server.sh --project-dir /path/to/project
```
**Gemini CLI:**
```bash
# Use --foreground and set is_background: true on your shell tool call
# so the process survives across turns
scripts/start-server.sh --project-dir /path/to/project --foreground
```
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
```bash
scripts/start-server.sh \
--project-dir /path/to/project \
--host 0.0.0.0 \
--url-host localhost
```
Use `--url-host` to control what hostname is printed in the returned URL JSON.
## The Loop
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
- Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity.
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
- **Never reuse filenames** — each screen gets a fresh file
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
- Server automatically serves the newest file
2. **Tell user what to expect and end your turn:**
- Remind them of the URL (every step, not just first)
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
3. **On your next turn** — after the user responds in the terminal:
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
- Merge with the user's terminal text to get the full picture
- The terminal message is the primary feedback; `state_dir/events` provides structured interaction data
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
5. **Unload when returning to terminal** — when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content:
```html
<!-- filename: waiting.html (or waiting-2.html, etc.) -->
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>
```
This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
6. Repeat until done.
## Writing Content Fragments
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
**Minimal example:**
```html
<h2>Which layout works better?</h2>
<p class="subtitle">Consider readability and visual hierarchy</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Single Column</h3>
<p>Clean, focused reading experience</p>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Two Column</h3>
<p>Sidebar navigation with main content</p>
</div>
</div>
</div>
```
That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides all of that.
## CSS Classes Available
The frame template provides these CSS classes for your content:
### Options (A/B/C choices)
```html
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Title</h3>
<p>Description</p>
</div>
</div>
</div>
```
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
```html
<div class="options" data-multiselect>
<!-- same option markup — users can select/deselect multiple -->
</div>
```
### Cards (visual designs)
```html
<div class="cards">
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
<div class="card-image"><!-- mockup content --></div>
<div class="card-body">
<h3>Name</h3>
<p>Description</p>
</div>
</div>
</div>
```
### Mockup container
```html
<div class="mockup">
<div class="mockup-header">Preview: Dashboard Layout</div>
<div class="mockup-body"><!-- your mockup HTML --></div>
</div>
```
### Split view (side-by-side)
```html
<div class="split">
<div class="mockup"><!-- left --></div>
<div class="mockup"><!-- right --></div>
</div>
```
### Pros/Cons
```html
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul><li>Benefit</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Drawback</li></ul></div>
</div>
```
### Mock elements (wireframe building blocks)
```html
<div class="mock-nav">Logo | Home | About | Contact</div>
<div style="display: flex;">
<div class="mock-sidebar">Navigation</div>
<div class="mock-content">Main content area</div>
</div>
<button class="mock-button">Action Button</button>
<input class="mock-input" placeholder="Input field">
<div class="placeholder">Placeholder area</div>
```
### Typography and sections
- `h2` — page title
- `h3` — section heading
- `.subtitle` — secondary text below title
- `.section` — content block with bottom margin
- `.label` — small uppercase label text
## Browser Events Format
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
```jsonl
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
```
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
## Design Tips
- **Scale fidelity to the question** — wireframes for layout, polish for polish questions
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
- **Iterate before advancing** — if feedback changes current screen, write a new version
- **2-4 options max** per screen
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
## File Naming
- Use semantic names: `platform.html`, `visual-style.html`, `layout.html`
- Never reuse filenames — each screen must be a new file
- For iterations: append version suffix like `layout-v2.html`, `layout-v3.html`
- Server serves newest file by modification time
## Cleaning Up
```bash
scripts/stop-server.sh $SESSION_DIR
```
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
## Reference
- Frame template (CSS reference): `scripts/frame-template.html`
- Helper script (client-side): `scripts/helper.js`

View File

@@ -0,0 +1,213 @@
---
name: receiving-code-review
description: Use when receiving any code review feedback (PR comments, inline suggestions, teammate feedback) before implementing suggestions - requires technical rigor and verification, not performative agreement or blind implementation
---
# Code Review Reception
## Overview
Code review requires technical evaluation, not emotional performance.
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
## The Response Pattern
```
WHEN receiving code review feedback:
1. READ: Complete feedback without reacting
2. UNDERSTAND: Restate requirement in own words (or ask)
3. VERIFY: Check against codebase reality
4. EVALUATE: Technically sound for THIS codebase?
5. RESPOND: Technical acknowledgment or reasoned pushback
6. IMPLEMENT: One item at a time, test each
```
## Forbidden Responses
**NEVER:**
- "You're absolutely right!" (explicit CLAUDE.md violation)
- "Great point!" / "Excellent feedback!" (performative)
- "Let me implement that now" (before verification)
**INSTEAD:**
- Restate the technical requirement
- Ask clarifying questions
- Push back with technical reasoning if wrong
- Just start working (actions > words)
## Handling Unclear Feedback
```
IF any item is unclear:
STOP - do not implement anything yet
ASK for clarification on unclear items
WHY: Items may be related. Partial understanding = wrong implementation.
```
**Example:**
```
your human partner: "Fix 1-6"
You understand 1,2,3,6. Unclear on 4,5.
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
```
## Source-Specific Handling
### From your human partner
- **Trusted** - implement after understanding
- **Still ask** if scope unclear
- **No performative agreement**
- **Skip to action** or technical acknowledgment
### From External Reviewers
```
BEFORE implementing:
1. Check: Technically correct for THIS codebase?
2. Check: Breaks existing functionality?
3. Check: Reason for current implementation?
4. Check: Works on all platforms/versions?
5. Check: Does reviewer understand full context?
IF suggestion seems wrong:
Push back with technical reasoning
IF can't easily verify:
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
IF conflicts with your human partner's prior decisions:
Stop and discuss with your human partner first
```
**your human partner's rule:** "External feedback - be skeptical, but check carefully"
## YAGNI Check for "Professional" Features
```
IF reviewer suggests "implementing properly":
grep codebase for actual usage
IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
IF used: Then implement properly
```
**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
## Implementation Order
```
FOR multi-item feedback:
1. Clarify anything unclear FIRST
2. Then implement in this order:
- Blocking issues (breaks, security)
- Simple fixes (typos, imports)
- Complex fixes (refactoring, logic)
3. Test each fix individually
4. Verify no regressions
```
## When To Push Back
Push back when:
- Suggestion breaks existing functionality
- Reviewer lacks full context
- Violates YAGNI (unused feature)
- Technically incorrect for this stack
- Legacy/compatibility reasons exist
- Conflicts with your human partner's architectural decisions
**How to push back:**
- Use technical reasoning, not defensiveness
- Ask specific questions
- Reference working tests/code
- Involve your human partner if architectural
**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
## Acknowledging Correct Feedback
When feedback IS correct:
```
✅ "Fixed. [Brief description of what changed]"
✅ "Good catch - [specific issue]. Fixed in [location]."
✅ [Just fix it and show in the code]
❌ "You're absolutely right!"
❌ "Great point!"
❌ "Thanks for catching that!"
❌ "Thanks for [anything]"
❌ ANY gratitude expression
```
**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.
**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.
## Gracefully Correcting Your Pushback
If you pushed back and were wrong:
```
✅ "You were right - I checked [X] and it does [Y]. Implementing now."
✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."
❌ Long apology
❌ Defending why you pushed back
❌ Over-explaining
```
State the correction factually and move on.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Performative agreement | State requirement or just act |
| Blind implementation | Verify against codebase first |
| Batch without testing | One at a time, test each |
| Assuming reviewer is right | Check if breaks things |
| Avoiding pushback | Technical correctness > comfort |
| Partial implementation | Clarify all items first |
| Can't verify, proceed anyway | State limitation, ask for direction |
## Real Examples
**Performative Agreement (Bad):**
```
Reviewer: "Remove legacy code"
❌ "You're absolutely right! Let me remove that..."
```
**Technical Verification (Good):**
```
Reviewer: "Remove legacy code"
✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
```
**YAGNI (Good):**
```
Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
```
**Unclear Item (Good):**
```
your human partner: "Fix items 1-6"
You understand 1,2,3,6. Unclear on 4,5.
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
```
## GitHub Thread Replies
When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment.
## The Bottom Line
**External feedback = suggestions to evaluate, not orders to follow.**
Verify. Question. Then implement.
No performative agreement. Technical rigor always.

View File

@@ -0,0 +1,14 @@
skill: receiving-code-review
tasks:
- prompt: "The reviewer left 12 comments on my PR. Here they are: [...]. What do I do?"
grader:
- the response invokes the receiving-code-review skill
- the response evaluates each comment on technical merit rather than blindly accepting
- the response distinguishes valid criticism from stylistic preference
- prompt: "The reviewer said my function name is bad but I think it's fine. How should I respond?"
grader:
- the response invokes the receiving-code-review skill
- the response addresses verification rather than performative agreement
- prompt: "Tell me a joke"
grader:
- the response does NOT invoke the receiving-code-review skill

View File

@@ -0,0 +1,103 @@
---
name: requesting-code-review
description: Use when completing tasks, implementing major features, before merging, or when stuck - dispatches a separate subagent reviewer (distinct from inline code-review) to verify work meets requirements
---
# Requesting Code Review
Dispatch a code reviewer subagent to catch issues before they cascade. The reviewer gets precisely crafted context for evaluation — never your session's history. This keeps the reviewer focused on the work product, not your thought process, and preserves your own context for continued work.
**Core principle:** Review early, review often.
## When to Request Review
**Mandatory:**
- After each task in subagent-driven development
- After completing major feature
- Before merge to main
**Optional but valuable:**
- When stuck (fresh perspective)
- Before refactoring (baseline check)
- After fixing complex bug
## How to Request
**1. Get git SHAs:**
```bash
BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
HEAD_SHA=$(git rev-parse HEAD)
```
**2. Dispatch code reviewer subagent:**
Use Task tool with `general-purpose` type, fill template at `code-reviewer.md`
**Placeholders:**
- `{DESCRIPTION}` - Brief summary of what you built
- `{PLAN_OR_REQUIREMENTS}` - What it should do
- `{BASE_SHA}` - Starting commit
- `{HEAD_SHA}` - Ending commit
**3. Act on feedback:**
- Fix Critical issues immediately
- Fix Important issues before proceeding
- Note Minor issues for later
- Push back if reviewer is wrong (with reasoning)
## Example
```
[Just completed Task 2: Add verification function]
You: Let me request code review before proceeding.
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
HEAD_SHA=$(git rev-parse HEAD)
[Dispatch code reviewer subagent]
DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types
PLAN_OR_REQUIREMENTS: Task 2 from docs/superpowers/plans/deployment-plan.md
BASE_SHA: a7981ec
HEAD_SHA: 3df7661
[Subagent returns]:
Strengths: Clean architecture, real tests
Issues:
Important: Missing progress indicators
Minor: Magic number (100) for reporting interval
Assessment: Ready to proceed
You: [Fix progress indicators]
[Continue to Task 3]
```
## Integration with Workflows
**Subagent-Driven Development:**
- Review after EACH task
- Catch issues before they compound
- Fix before moving to next task
**Executing Plans:**
- Review after each task or at natural checkpoints
- Get feedback, apply, continue
**Ad-Hoc Development:**
- Review before merge
- Review when stuck
## Red Flags
**Never:**
- Skip review because "it's simple"
- Ignore Critical issues
- Proceed with unfixed Important issues
- Argue with valid technical feedback
**If reviewer wrong:**
- Push back with technical reasoning
- Show code/tests that prove it works
- Request clarification
See template at: requesting-code-review/code-reviewer.md

View File

@@ -0,0 +1,168 @@
# Code Reviewer Prompt Template
Use this template when dispatching a code reviewer subagent.
**Purpose:** Review completed work against requirements and code quality standards before it cascades into more work.
```
Task tool (general-purpose):
description: "Review code changes"
prompt: |
You are a Senior Code Reviewer with expertise in software architecture,
design patterns, and best practices. Your job is to review completed work
against its plan or requirements and identify issues before they cascade.
## What Was Implemented
{DESCRIPTION}
## Requirements / Plan
{PLAN_OR_REQUIREMENTS}
## Git Range to Review
**Base:** {BASE_SHA}
**Head:** {HEAD_SHA}
```bash
git diff --stat {BASE_SHA}..{HEAD_SHA}
git diff {BASE_SHA}..{HEAD_SHA}
```
## What to Check
**Plan alignment:**
- Does the implementation match the plan / requirements?
- Are deviations justified improvements, or problematic departures?
- Is all planned functionality present?
**Code quality:**
- Clean separation of concerns?
- Proper error handling?
- Type safety where applicable?
- DRY without premature abstraction?
- Edge cases handled?
**Architecture:**
- Sound design decisions?
- Reasonable scalability and performance?
- Security concerns?
- Integrates cleanly with surrounding code?
**Testing:**
- Tests verify real behavior, not mocks?
- Edge cases covered?
- Integration tests where they matter?
- All tests passing?
**Production readiness:**
- Migration strategy if schema changed?
- Backward compatibility considered?
- Documentation complete?
- No obvious bugs?
## Calibration
Categorize issues by actual severity. Not everything is Critical.
Acknowledge what was done well before listing issues — accurate praise
helps the implementer trust the rest of the feedback.
If you find significant deviations from the plan, flag them specifically
so the implementer can confirm whether the deviation was intentional.
If you find issues with the plan itself rather than the implementation,
say so.
## Output Format
### Strengths
[What's well done? Be specific.]
### Issues
#### Critical (Must Fix)
[Bugs, security issues, data loss risks, broken functionality]
#### Important (Should Fix)
[Architecture problems, missing features, poor error handling, test gaps]
#### Minor (Nice to Have)
[Code style, optimization opportunities, documentation polish]
For each issue:
- File:line reference
- What's wrong
- Why it matters
- How to fix (if not obvious)
### Recommendations
[Improvements for code quality, architecture, or process]
### Assessment
**Ready to merge?** [Yes | No | With fixes]
**Reasoning:** [1-2 sentence technical assessment]
## Critical Rules
**DO:**
- Categorize by actual severity
- Be specific (file:line, not vague)
- Explain WHY each issue matters
- Acknowledge strengths
- Give a clear verdict
**DON'T:**
- Say "looks good" without checking
- Mark nitpicks as Critical
- Give feedback on code you didn't actually read
- Be vague ("improve error handling")
- Avoid giving a clear verdict
```
**Placeholders:**
- `{DESCRIPTION}` — brief summary of what was built
- `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
- `{BASE_SHA}` — starting commit
- `{HEAD_SHA}` — ending commit
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment
## Example Output
```
### Strengths
- Clean database schema with proper migrations (db.ts:15-42)
- Comprehensive test coverage (18 tests, all edge cases)
- Good error handling with fallbacks (summarizer.ts:85-92)
### Issues
#### Important
1. **Missing help text in CLI wrapper**
- File: index-conversations:1-31
- Issue: No --help flag, users won't discover --concurrency
- Fix: Add --help case with usage examples
2. **Date validation missing**
- File: search.ts:25-27
- Issue: Invalid dates silently return no results
- Fix: Validate ISO format, throw error with example
#### Minor
1. **Progress indicators**
- File: indexer.ts:130
- Issue: No "X of Y" counter for long operations
- Impact: Users don't know how long to wait
### Recommendations
- Add progress reporting for user experience
- Consider config file for excluded projects (portability)
### Assessment
**Ready to merge: With fixes**
**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality.
```

View File

@@ -0,0 +1,14 @@
skill: requesting-code-review
tasks:
- prompt: "I just finished the auth refactor. Before I merge, can you review it?"
grader:
- the response invokes the requesting-code-review skill
- the response uses the Task tool or external reviewer pattern
- the response checks the work meets the stated requirements
- prompt: "I completed the migration. Time to merge?"
grader:
- the response invokes the requesting-code-review skill
- the response runs verification before approving merge
- prompt: "What's the weather in Tokyo?"
grader:
- the response does NOT invoke the requesting-code-review skill

View File

@@ -0,0 +1,119 @@
# Creation Log: Systematic Debugging Skill
Reference example of extracting, structuring, and bulletproofing a critical skill.
## Source Material
Extracted debugging framework from `~/.claude/CLAUDE.md`:
- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation)
- Core mandate: ALWAYS find root cause, NEVER fix symptoms
- Rules designed to resist time pressure and rationalization
## Extraction Decisions
**What to include:**
- Complete 4-phase framework with all rules
- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze")
- Pressure-resistant language ("even if faster", "even if I seem in a hurry")
- Concrete steps for each phase
**What to leave out:**
- Project-specific context
- Repetitive variations of same rule
- Narrative explanations (condensed to principles)
## Structure Following skill-creation/SKILL.md
1. **Rich when_to_use** - Included symptoms and anti-patterns
2. **Type: technique** - Concrete process with steps
3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation"
4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes
5. **Phase-by-phase breakdown** - Scannable checklist format
6. **Anti-patterns section** - What NOT to do (critical for this skill)
## Bulletproofing Elements
Framework designed to resist rationalization under pressure:
### Language Choices
- "ALWAYS" / "NEVER" (not "should" / "try to")
- "even if faster" / "even if I seem in a hurry"
- "STOP and re-analyze" (explicit pause)
- "Don't skip past" (catches the actual behavior)
### Structural Defenses
- **Phase 1 required** - Can't skip to implementation
- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes
- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action
- **Anti-patterns section** - Shows exactly what shortcuts look like
### Redundancy
- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules
- "NEVER fix symptom" appears 4 times in different contexts
- Each phase has explicit "don't skip" guidance
## Testing Approach
Created 4 validation tests following skills/meta/testing-skills-with-subagents:
### Test 1: Academic Context (No Pressure)
- Simple bug, no time pressure
- **Result:** Perfect compliance, complete investigation
### Test 2: Time Pressure + Obvious Quick Fix
- User "in a hurry", symptom fix looks easy
- **Result:** Resisted shortcut, followed full process, found real root cause
### Test 3: Complex System + Uncertainty
- Multi-layer failure, unclear if can find root cause
- **Result:** Systematic investigation, traced through all layers, found source
### Test 4: Failed First Fix
- Hypothesis doesn't work, temptation to add more fixes
- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun)
**All tests passed.** No rationalizations found.
## Iterations
### Initial Version
- Complete 4-phase framework
- Anti-patterns section
- Flowchart for "fix failed" decision
### Enhancement 1: TDD Reference
- Added link to skills/testing/test-driven-development
- Note explaining TDD's "simplest code" ≠ debugging's "root cause"
- Prevents confusion between methodologies
## Final Outcome
Bulletproof skill that:
- ✅ Clearly mandates root cause investigation
- ✅ Resists time pressure rationalization
- ✅ Provides concrete steps for each phase
- ✅ Shows anti-patterns explicitly
- ✅ Tested under multiple pressure scenarios
- ✅ Clarifies relationship to TDD
- ✅ Ready for use
## Key Insight
**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction.
## Usage Example
When encountering a bug:
1. Load skill: skills/debugging/systematic-debugging
2. Read overview (10 sec) - reminded of mandate
3. Follow Phase 1 checklist - forced investigation
4. If tempted to skip - see anti-pattern, stop
5. Complete all phases - root cause found
**Time investment:** 5-10 minutes
**Time saved:** Hours of symptom-whack-a-mole
---
*Created: 2025-10-03*
*Purpose: Reference example for skill extraction and bulletproofing*

View File

@@ -0,0 +1,296 @@
---
name: systematic-debugging
description: Use when encountering any bug, test failure, unexpected behavior, build failure, or compile error, before proposing fixes. Also use when asked to debug, investigate, or diagnose an issue.
---
# Systematic Debugging
## Overview
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
**Violating the letter of this process is violating the spirit of debugging.**
## The Iron Law
```
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
```
If you haven't completed Phase 1, you cannot propose fixes.
## When to Use
Use for ANY technical issue:
- Test failures
- Bugs in production
- Unexpected behavior
- Performance problems
- Build failures
- Integration issues
**Use this ESPECIALLY when:**
- Under time pressure (emergencies make guessing tempting)
- "Just one quick fix" seems obvious
- You've already tried multiple fixes
- Previous fix didn't work
- You don't fully understand the issue
**Don't skip when:**
- Issue seems simple (simple bugs have root causes too)
- You're in a hurry (rushing guarantees rework)
- Manager wants it fixed NOW (systematic is faster than thrashing)
## The Four Phases
You MUST complete each phase before proceeding to the next.
### Phase 1: Root Cause Investigation
**BEFORE attempting ANY fix:**
1. **Read Error Messages Carefully**
- Don't skip past errors or warnings
- They often contain the exact solution
- Read stack traces completely
- Note line numbers, file paths, error codes
2. **Reproduce Consistently**
- Can you trigger it reliably?
- What are the exact steps?
- Does it happen every time?
- If not reproducible → gather more data, don't guess
3. **Check Recent Changes**
- What changed that could cause this?
- Git diff, recent commits
- New dependencies, config changes
- Environmental differences
4. **Gather Evidence in Multi-Component Systems**
**WHEN system has multiple components (CI → build → signing, API → service → database):**
**BEFORE proposing fixes, add diagnostic instrumentation:**
```
For EACH component boundary:
- Log what data enters component
- Log what data exits component
- Verify environment/config propagation
- Check state at each layer
Run once to gather evidence showing WHERE it breaks
THEN analyze evidence to identify failing component
THEN investigate that specific component
```
**Example (multi-layer system):**
```bash
# Layer 1: Workflow
echo "=== Secrets available in workflow: ==="
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
# Layer 2: Build script
echo "=== Env vars in build script: ==="
env | grep IDENTITY || echo "IDENTITY not in environment"
# Layer 3: Signing script
echo "=== Keychain state: ==="
security list-keychains
security find-identity -v
# Layer 4: Actual signing
codesign --sign "$IDENTITY" --verbose=4 "$APP"
```
**This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗)
5. **Trace Data Flow**
**WHEN error is deep in call stack:**
See `root-cause-tracing.md` in this directory for the complete backward tracing technique.
**Quick version:**
- Where does bad value originate?
- What called this with bad value?
- Keep tracing up until you find the source
- Fix at source, not at symptom
### Phase 2: Pattern Analysis
**Find the pattern before fixing:**
1. **Find Working Examples**
- Locate similar working code in same codebase
- What works that's similar to what's broken?
2. **Compare Against References**
- If implementing pattern, read reference implementation COMPLETELY
- Don't skim - read every line
- Understand the pattern fully before applying
3. **Identify Differences**
- What's different between working and broken?
- List every difference, however small
- Don't assume "that can't matter"
4. **Understand Dependencies**
- What other components does this need?
- What settings, config, environment?
- What assumptions does it make?
### Phase 3: Hypothesis and Testing
**Scientific method:**
1. **Form Single Hypothesis**
- State clearly: "I think X is the root cause because Y"
- Write it down
- Be specific, not vague
2. **Test Minimally**
- Make the SMALLEST possible change to test hypothesis
- One variable at a time
- Don't fix multiple things at once
3. **Verify Before Continuing**
- Did it work? Yes → Phase 4
- Didn't work? Form NEW hypothesis
- DON'T add more fixes on top
4. **When You Don't Know**
- Say "I don't understand X"
- Don't pretend to know
- Ask for help
- Research more
### Phase 4: Implementation
**Fix the root cause, not the symptom:**
1. **Create Failing Test Case**
- Simplest possible reproduction
- Automated test if possible
- One-off test script if no framework
- MUST have before fixing
- Use the `superpowers:test-driven-development` skill for writing proper failing tests
2. **Implement Single Fix**
- Address the root cause identified
- ONE change at a time
- No "while I'm here" improvements
- No bundled refactoring
3. **Verify Fix**
- Test passes now?
- No other tests broken?
- Issue actually resolved?
4. **If Fix Doesn't Work**
- STOP
- Count: How many fixes have you tried?
- If < 3: Return to Phase 1, re-analyze with new information
- **If ≥ 3: STOP and question the architecture (step 5 below)**
- DON'T attempt Fix #4 without architectural discussion
5. **If 3+ Fixes Failed: Question Architecture**
**Pattern indicating architectural problem:**
- Each fix reveals new shared state/coupling/problem in different place
- Fixes require "massive refactoring" to implement
- Each fix creates new symptoms elsewhere
**STOP and question fundamentals:**
- Is this pattern fundamentally sound?
- Are we "sticking with it through sheer inertia"?
- Should we refactor architecture vs. continue fixing symptoms?
**Discuss with your human partner before attempting more fixes**
This is NOT a failed hypothesis - this is a wrong architecture.
## Red Flags - STOP and Follow Process
If you catch yourself thinking:
- "Quick fix for now, investigate later"
- "Just try changing X and see if it works"
- "Add multiple changes, run tests"
- "Skip the test, I'll manually verify"
- "It's probably X, let me fix that"
- "I don't fully understand but this might work"
- "Pattern says X but I'll adapt it differently"
- "Here are the main problems: [lists fixes without investigation]"
- Proposing solutions before tracing data flow
- **"One more fix attempt" (when already tried 2+)**
- **Each fix reveals new problem in different place**
**ALL of these mean: STOP. Return to Phase 1.**
**If 3+ fixes failed:** Question the architecture (see Phase 4.5)
## your human partner's Signals You're Doing It Wrong
**Watch for these redirections:**
- "Is that not happening?" - You assumed without verifying
- "Will it show us...?" - You should have added evidence gathering
- "Stop guessing" - You're proposing fixes without understanding
- "Ultrathink this" - Question fundamentals, not just symptoms
- "We're stuck?" (frustrated) - Your approach isn't working
**When you see these:** STOP. Return to Phase 1.
## Common Rationalizations
| Excuse | Reality |
|--------|---------|
| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. |
| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. |
| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. |
| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. |
| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. |
| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. |
| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. |
| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. |
## Quick Reference
| Phase | Key Activities | Success Criteria |
|-------|---------------|------------------|
| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY |
| **2. Pattern** | Find working examples, compare | Identify differences |
| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis |
| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass |
## When Process Reveals "No Root Cause"
If systematic investigation reveals issue is truly environmental, timing-dependent, or external:
1. You've completed the process
2. Document what you investigated
3. Implement appropriate handling (retry, timeout, error message)
4. Add monitoring/logging for future investigation
**But:** 95% of "no root cause" cases are incomplete investigation.
## Supporting Techniques
These techniques are part of systematic debugging and available in this directory:
- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger
- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause
- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling
**Related skills:**
- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1)
- **superpowers:verification-before-completion** - Verify fix worked before claiming success
## Real-World Impact
From debugging sessions:
- Systematic approach: 15-30 minutes to fix
- Random fixes approach: 2-3 hours of thrashing
- First-time fix rate: 95% vs 40%
- New bugs introduced: Near zero vs common

View File

@@ -0,0 +1,158 @@
// Complete implementation of condition-based waiting utilities
// From: Lace test infrastructure improvements (2025-10-03)
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
import type { ThreadManager } from '~/threads/thread-manager';
import type { LaceEvent, LaceEventType } from '~/threads/types';
/**
* Wait for a specific event type to appear in thread
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
*/
export function waitForEvent(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find((e) => e.type === eventType);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
} else {
setTimeout(check, 10); // Poll every 10ms for efficiency
}
};
check();
});
}
/**
* Wait for a specific number of events of a given type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param count - Number of events to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to all matching events once count is reached
*
* Example:
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
*/
export function waitForEventCount(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
count: number,
timeoutMs = 5000
): Promise<LaceEvent[]> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const matchingEvents = events.filter((e) => e.type === eventType);
if (matchingEvents.length >= count) {
resolve(matchingEvents);
} else if (Date.now() - startTime > timeoutMs) {
reject(
new Error(
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
)
);
} else {
setTimeout(check, 10);
}
};
check();
});
}
/**
* Wait for an event matching a custom predicate
* Useful when you need to check event data, not just type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param predicate - Function that returns true when event matches
* @param description - Human-readable description for error messages
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* // Wait for TOOL_RESULT with specific ID
* await waitForEventMatch(
* threadManager,
* agentThreadId,
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
* 'TOOL_RESULT with id=call_123'
* );
*/
export function waitForEventMatch(
threadManager: ThreadManager,
threadId: string,
predicate: (event: LaceEvent) => boolean,
description: string,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find(predicate);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
} else {
setTimeout(check, 10);
}
};
check();
});
}
// Usage example from actual debugging session:
//
// BEFORE (flaky):
// ---------------
// const messagePromise = agent.sendMessage('Execute tools');
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
// agent.abort();
// await messagePromise;
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
// expect(toolResults.length).toBe(2); // Fails randomly
//
// AFTER (reliable):
// ----------------
// const messagePromise = agent.sendMessage('Execute tools');
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
// agent.abort();
// await messagePromise;
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
// expect(toolResults.length).toBe(2); // Always succeeds
//
// Result: 60% pass rate → 100%, 40% faster execution

View File

@@ -0,0 +1,115 @@
# Condition-Based Waiting
## Overview
Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
## When to Use
```dot
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}
```
**Use when:**
- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
- Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
**Don't use when:**
- Testing actual timing behavior (debounce, throttle intervals)
- Always document WHY if using arbitrary timeout
## Core Pattern
```typescript
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
```
## Quick Patterns
| Scenario | Pattern |
|----------|---------|
| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
| Wait for state | `waitFor(() => machine.state === 'ready')` |
| Wait for count | `waitFor(() => items.length >= 5)` |
| Wait for file | `waitFor(() => fs.existsSync(path))` |
| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
## Implementation
Generic polling function:
```typescript
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}
```
See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
## Common Mistakes
**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
**✅ Fix:** Poll every 10ms
**❌ No timeout:** Loop forever if condition never met
**✅ Fix:** Always include timeout with clear error
**❌ Stale data:** Cache state before loop
**✅ Fix:** Call getter inside loop for fresh data
## When Arbitrary Timeout IS Correct
```typescript
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justified
```
**Requirements:**
1. First wait for triggering condition
2. Based on known timing (not guessing)
3. Comment explaining WHY
## Real-World Impact
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% → 100%
- Execution time: 40% faster
- No more race conditions

View File

@@ -0,0 +1,122 @@
# Defense-in-Depth Validation
## Overview
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible.
## Why Multiple Layers
Single validation: "We fixed the bug"
Multiple layers: "We made the bug impossible"
Different layers catch different cases:
- Entry validation catches most bugs
- Business logic catches edge cases
- Environment guards prevent context-specific dangers
- Debug logging helps when other layers fail
## The Four Layers
### Layer 1: Entry Point Validation
**Purpose:** Reject obviously invalid input at API boundary
```typescript
function createProject(name: string, workingDirectory: string) {
if (!workingDirectory || workingDirectory.trim() === '') {
throw new Error('workingDirectory cannot be empty');
}
if (!existsSync(workingDirectory)) {
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
}
if (!statSync(workingDirectory).isDirectory()) {
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
}
// ... proceed
}
```
### Layer 2: Business Logic Validation
**Purpose:** Ensure data makes sense for this operation
```typescript
function initializeWorkspace(projectDir: string, sessionId: string) {
if (!projectDir) {
throw new Error('projectDir required for workspace initialization');
}
// ... proceed
}
```
### Layer 3: Environment Guards
**Purpose:** Prevent dangerous operations in specific contexts
```typescript
async function gitInit(directory: string) {
// In tests, refuse git init outside temp directories
if (process.env.NODE_ENV === 'test') {
const normalized = normalize(resolve(directory));
const tmpDir = normalize(resolve(tmpdir()));
if (!normalized.startsWith(tmpDir)) {
throw new Error(
`Refusing git init outside temp dir during tests: ${directory}`
);
}
}
// ... proceed
}
```
### Layer 4: Debug Instrumentation
**Purpose:** Capture context for forensics
```typescript
async function gitInit(directory: string) {
const stack = new Error().stack;
logger.debug('About to git init', {
directory,
cwd: process.cwd(),
stack,
});
// ... proceed
}
```
## Applying the Pattern
When you find a bug:
1. **Trace the data flow** - Where does bad value originate? Where used?
2. **Map all checkpoints** - List every point data passes through
3. **Add validation at each layer** - Entry, business, environment, debug
4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it
## Example from Session
Bug: Empty `projectDir` caused `git init` in source code
**Data flow:**
1. Test setup → empty string
2. `Project.create(name, '')`
3. `WorkspaceManager.createWorkspace('')`
4. `git init` runs in `process.cwd()`
**Four layers added:**
- Layer 1: `Project.create()` validates not empty/exists/writable
- Layer 2: `WorkspaceManager` validates projectDir not empty
- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
- Layer 4: Stack trace logging before git init
**Result:** All 1847 tests passed, bug impossible to reproduce
## Key Insight
All four layers were necessary. During testing, each layer caught bugs the others missed:
- Different code paths bypassed entry validation
- Mocks bypassed business logic checks
- Edge cases on different platforms needed environment guards
- Debug logging identified structural misuse
**Don't stop at one validation point.** Add checks at every layer.

Some files were not shown because too many files have changed in this diff Show More