Files
bench/manager/core/adapters/opencode/wire
T
istosandClaude Fable 5 f8d1bf252d Launch headless agents with permissions that allow their own contract (task 05)
Work agents were launched under acceptEdits with no Bash allowlist, so
headless runs could edit files but never run tests or commit — the exact
contradiction card 05 documents. Each launch intent (work, act-pr,
review) now carries a permissions.allow list granting exactly what its
prompt demands, delivered through the same generated settings JSON as
the event hooks. Project test/check commands arrive as neutral prefixes
via BOARD_AGENT_COMMANDS; a new opencode adapter renders the same three
stances in its config language as the portability proof. A clean agent
exit with an empty branch no longer advances the card to review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:46:23 +02:00

74 lines
2.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""opencode adapter: wire live-session visibility into the host project.
Contract (same for every adapter): called by install.py with the project
root as argv[1] (and optionally --dry-run). Idempotently ensures the
project's own sessions report events to the board; prints a report; exit
0 on ok/fixed, 1 on a project that cannot be wired.
For opencode that means a plugin. opencode loads project plugins from
.opencode/plugin/, so this installs .opencode/plugin/bench-board.js — a
one-line re-export of this adapter's plugin.js, which stays under
manager/core/ where updates replace it wholesale. The shim only has to
exist; committed to the repo, it also rides along into every work
agent's worktree, so headless jobs report events too.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
SHIM_NAME = "bench-board.js"
def shim_text(target: Path) -> str:
rel = os.path.relpath(HERE / "plugin.js", target.parent).replace(os.sep, "/")
return (
"// Bench board bridge — installed by the opencode adapter's `wire`.\n"
"// The implementation lives with the task manager core, so updates\n"
"// replace it in place; this shim only has to exist.\n"
f'export {{ BenchBoard }} from "{rel}"\n'
)
def main() -> int:
args = [a for a in sys.argv[1:] if a != "--dry-run"]
dry_run = "--dry-run" in sys.argv[1:]
project = Path(args[0]).resolve() if args else Path.cwd()
if not project.is_dir():
print(f"{project} is not a directory — nothing to wire.")
return 1
if not (HERE / "plugin.js").is_file():
print(f"error: {HERE / 'plugin.js'} is missing — the adapter looks broken.")
return 1
target = project / ".opencode" / "plugin" / SHIM_NAME
wanted = shim_text(target)
current = target.read_text(encoding="utf-8") if target.is_file() else None
status = "ok" if current == wanted else ("added" if current is None else "repaired")
print(f"adapter: opencode\nplugin: {target}\n")
print(f" plugin {SHIM_NAME:<20} {status}")
if status == "ok":
print("\nEverything already in place — nothing to do.")
return 0
if dry_run:
print("\nDry run — no changes written.")
return 0
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(wanted, encoding="utf-8")
print("\nWrote the plugin shim. Note: running opencode sessions load "
"plugins at startup — restart them to pick this up. Commit "
".opencode/ so agent worktrees carry it too.")
return 0
if __name__ == "__main__":
sys.exit(main())