The MultiEdit deny rule was already dropped in the hotfix; this is task 10's follow-up half — the guard that catches the next fossil. The claude adapter's review branch spells "cannot edit files" in the vendor's own tool names, and that roster moves: a rule naming a tool the installed CLI does not have is refused at startup, so the launch dies before the agent speaks. Membership assertions could not see that happening, so the stub-binary test now asserts each mode's flag list literally (settings payload elided) — a deny name added or renamed shows up as a diff a reviewer must re-verify against the installed CLI. The run script carries the same warning where the list actually lives. The helper drops a leaked AGENT_MODEL, since --model would otherwise appear in an argv now compared whole; test_agent_model.py owns that flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Agent adapters
An adapter makes the task manager work with a particular coding agent.
Core never speaks any vendor's language — it launches jobs and ingests
normalized events; adapters translate at the edge. Two ship as reference
implementations: claude/ (the default) and opencode/. Select with
BOARD_AGENT_ADAPTER in local/.env; a directory of the same name under
local/adapters/ overrides the core one.
The contract
An adapter is a directory with two executables:
run — execute one headless job to completion
- env in:
AGENT_PROMPT(the full prompt),AGENT_MODE(the launch intent, below),AGENT_COMMANDS(the project's allowed command prefixes, below),AGENT_MODEL(optional, below),AGENT_CWD, and theBOARD_*passthrough (BOARD_AGENT_ID,BOARD_TASK,BOARD_PORT) which your event bridge must forward with every event. - stdout is captured by the board as the job log. The prompts instruct the
agent to end with marker lines (
NOT READY:,RELEVANCE REVIEW:,PR REVIEW:,ADDRESSED:) — the board parses them from this output, so the agent's final text must reach stdout. - exit 0 = completed; anything else = failed.
- The workflow brief the prompts point agents at is
AGENTS.mdat the repo root (CLAUDE.mdbeside it is only a compatibility pointer). Vendors that readAGENTS.mdfrom the working directory's tree natively — opencode does, as does current Claude Code — pick it up in every worktree with no adapter work;runnever needs to inject it.
Launch intents (AGENT_MODE)
Core signals intent; every adapter maps it to its vendor's permission mechanism. Headless runs have no human at a permission prompt, so anything not auto-approved is denied — grant each intent exactly the side effects its prompt demands, and never a blanket allow-everything (the worktree is isolated, the shell is not):
work— implement, test, commit in an isolated worktree. May edit files, run local git bookkeeping (git add/commit/status/diff) and the project'sAGENT_COMMANDS. No push.act-pr— the work stance, plusgit push(the PR must update), reading the PR's reviews and line comments (gh pr view,gh pr diff,gh api), andgit fetch/git mergeso a conflicted PR can be resolved by merging main into the branch. Resolution is additive only — the branch is public — sogit rebaseand the force-push spellings must be denied, not merely unlisted (a plaingit pushallow would otherwise cover them).review— read-only on the working tree: no edit tools, no commits. May read a PR (gh pr view,gh pr diff, read-only git) and post the verdict (gh pr review,gh pr comment).
The project's allowed commands (AGENT_COMMANDS)
The git/gh grants above are universal; which test/check commands a
project's agents run is project knowledge. It arrives as comma-separated
plain command prefixes — BOARD_AGENT_COMMANDS in local/.env, e.g.
python3 -m unittest,npm test — never in any vendor's rule syntax. Each
adapter renders them natively; both shipped rule languages are
prefix-pattern based, so the translation is mechanical:
- claude →
Bash(git commit:*)-style allow-rules in the generated settings JSON (claude/hook_settings.py) - opencode →
"permission": {"bash": {"*": "deny", "git commit *": "allow"}}in a generated config, wildcard rules, last match wins (opencode/permission_config.py)
The model (AGENT_MODEL) — optional
Absent = the vendor's own default: launch without any model argument and
let your CLI resolve it however it normally would. When set, it is an
opaque vendor-native model name — a claude alias, an opencode
provider/model-id — that core never validates or interprets; pass it
through untranslated (claude → --model "$AGENT_MODEL", opencode → the
generated config's model key). Never send your vendor an empty value:
the board only sets the variable when a model is actually configured
(BOARD_AGENT_MODEL and its per-intent overrides in local/.env).
wire — wire live-session visibility into the host project
Called by install.py with the project root as argv[1] (plus --dry-run).
Idempotently make the project's own interactive sessions report events —
however your platform allows (Claude Code: hooks in .claude/settings.json;
opencode: a plugin shim in .opencode/plugin/ subscribing to its event
bus). Print a report; exit 0 on ok/fixed. If the platform has no way to
observe sessions, be a no-op with an honest message: the board still runs
headless jobs via run, you just lose the live play-by-play.
Events — the normalized schema (v1)
POST to http://127.0.0.1:$BOARD_PORT/api/events:
{"v": 1, "session": str, "kind": str, "summary": str,
"file"?: str, "cmd"?: str, "detail"?: str, "ok"?: bool,
"running"?: bool, "agent"?: $BOARD_AGENT_ID, "task"?: $BOARD_TASK}
kinds: session end idle edit read search command test check git plan subagent web other. running: true marks an in-flight action (shown as
the live line, not appended to the timeline); follow it with the completed
event. kind: idle = finished responding; kind: end = session over.
Classification happens in YOUR emitter — core never sees vendor payloads.
Writing one
Read the two shipped adapters side by side — they are small and map the same three intents onto very different vendor mechanisms. The essentials:
run: launch your agent headlessly with permissions generated fromAGENT_MODE+AGENT_COMMANDS; make sure the final output lands on stdout and the exit code passes through.wire: install your platform's observer (hook, plugin) into the project so sessions POST the normalized schema with theBOARD_*env forwarded.- Events beat perfection: start with
session/endplus a genericcommandper tool call, refine kinds later —opencode/plugin.jsstarts exactly that coarse on purpose.