Skip to content

Agent integration

Source: packages/cli/src/agent/, packages/cli/src/commands/agent.ts, packages/core/src/agents/, skills/beetl/SKILL.md · Spec: §9, §10

beetl agent install writes into the host project:

  1. .claude/skills/beetl/SKILL.md — the contract: search before debugging, file on encounter (new / file-complete), always --actor agent:<tool>:<model>, move through gates, report BTL- IDs to the user. Canonical source is packages/cli/src/agent/skill.ts; the repo copy at skills/beetl/SKILL.md is kept in sync by a test.

  2. .claude/hooks/beetl-stop-nudge.sh + a Stop hook merged into .claude/settings.json (existing settings preserved, idempotent). The heuristic: source files changed but no .beetl/sessions/ file touched → block once with a reminder to file; stop_hook_active guards re-entry. The script is versioned (# beetl-hook-version: N, M-07) and reconciled on every install: outdated scripts are rewritten, and autoFile: false removes an installed hook (script + settings entry, other hooks kept) rather than merely skipping it.

    Detection is turn-scoped via a per-session_id state file in TMPDIR (v4): it accumulates the source paths already nudged for, so each path nudges once and an unrelated later edit does not re-blame a file the user already dismissed. “Was a session filed this turn?” reads the store by mtime against that file’s own mtime (the previous Stop) — not by git status — so committing the session you were just told to file does not re-arm the nudge; on the first Stop, where no baseline exists yet, a session in HEAD stands in. Porcelain is parsed with -z, since field-splitting the quoted form truncates paths containing spaces and reports a rename as its old path.

  3. --claude-md appends a two-line pointer to CLAUDE.md (offered, not forced).

AgentRunner interface with classify(), synthesize(), and diagnose() (M3). Every outcome is either a result or a well-formed prompt — the none runner emits the prompt so a human or the host agent can execute it.

  • Detection order: claude on PATH → claude-cli; codex → codex-cli; else none. api is explicit-config only (never auto-selected).
  • Execution context (H-05/L-07): every local runner subprocess runs with cwd set to the target store’s root (RunnerExecContext.rootDir) — never the caller’s directory, which --cwd can point elsewhere. Prompts are delivered via stdin (claude -p reads stdin; codex exec -), never argv, so they don’t appear in process listings or hit argument-size limits.
  • ExecRunner (core/src/agents/exec-runner.ts) is the shared prompt→execute→parse base; subclasses provide only run(): claude-cli (claude -p), codex-cli (codex exec, --model honored), api (Anthropic Messages API, key from agent.apiKeyEnv, default model claude-fable-5). Exec/fetch are constructor-injectable for tests.
  • Tolerant JSON parsing (fenced or wrapped); unparseable/empty output maps to exit code 3 (RUNNER_FAILURE) with a truncated output sample.
  • API-mode honesty (M-01, ADR-0007): every diagnose outcome carries grounding: "repository" | "metadata-only". The api runner sees only session metadata plus a file-name listing — no repository investigation — so its result is a candidate hypothesis: beetl diagnose --runner api emits it (kind: "hypothesis", applied: false) without touching the session; persisting requires the explicit --apply flag, and the update event’s note records diagnosed by api (grounding: metadata-only). Repository-grounded runners auto-apply and record the same provenance note. ANTHROPIC_BASE_URL overrides the API endpoint (proxies, tests).
  • beetl new --classify fills missing type/category/surface from the runner, or surfaces the prompt when no runner is available.

Minisearch over title/symptoms/errorOutput/rootCause/mechanism/files/tags/ category/surface, built on demand from the JSON store. beetl search "<query>", --similar-to <ref>, and automatic similar-on-create in beetl new (returned in --json as similar).