Skip to content

feat: give opencode sessions the tools to drive the others - #12

Merged
omartelo merged 1 commit into
mainfrom
feat/opencode-tools
Aug 11, 2026
Merged

omartelo merged 1 commit into
mainfrom
feat/opencode-tools

Conversation

@omartelo

@omartelo omartelo commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

An opencode session could report what it was doing and nothing else. This adds
the other direction.

Claude Code and Codex get it as MCP tools lich registers at spawn — it can
pass a server on their command line. opencode cannot be told about one, and
a plugin there cannot register an MCP server
either. What it can do is define tools, so the same seven are defined in the
module, under the same names the MCP server uses: an agent that learns one
surface should find the other where it expects it.

Tool
list_sessions the live sessions, as JSON
send_to_session hand one a task, wait for its answer
wait_for_answer wait again on a ticket
reply_to_session answer a task you were given
open_session open one, optionally on a fresh worktree
close_session close one, and settle its checkout
list_worktrees the checkouts, what is uncommitted, who is in them

They shell out to lich

Each tool builds an argv and runs the binary. Posting to the same loopback
endpoint the reports use would have been fewer moving parts — and the third
implementation of one contract (lich's CLI, lich's MCP server, this), in a
repository lich cannot see. An argument that moved in docs/cli.md would have
gone red in neither.

So what lives here is the argv. Which arguments are required, what a worktree's
last session decides, when --force is needed, and every wording written for an
agent to act on stay in lich; a command that fails answers with its own stderr,
unedited.

The binary is $LICH_BIN, never lich off PATH — a machine running an
installed lich beside a task dev build has two, and only one belongs to the
session.

Two absences, on purpose

The reports keep working in both, which is the point: they need neither.

  • Outside lich nothing is registered. Seven tools that could only answer "no
    lich is running" would be seven tools in the prompt of every unrelated
    opencode session on the machine.
  • Without opencode's plugin package nothing is registered either: the tool
    helper is imported at run time inside a try, because it resolves from
    opencode's own plugin dependencies and a module dropped where those do not
    reach would otherwise fail at import — taking the reports down with it.

Test plan

  • node --test tests/ — 121 pass. The new suite pins the translation and nothing else: the binary that runs, optional arguments left out rather than sent empty, the wait capped at 90, --force passed only for a literal true (a model sending "false" must not discard someone's work), a refusal returned as lich worded it, and both absences.
  • The suite is written to survive being run from inside a lich session, where the coordinates are already in the environment and "outside lich" would otherwise quietly test nothing.
  • Against a running opencode server (opencode serve, isolated config, module in its plugin dir): all seven appear in /experimental/tool/ids beside the native ones.
  • Against Bun's real shell: an array interpolates as separate arguments, with "my project" surviving as one.
  • Against a live lich: list_sessions and list_worktrees returned real JSON, close_session came back with lich's own refusal about the checkout's fate, and open_session opened one.

Needs no change in lich to work. A follow-up there is worth considering: the
relayed message decides whether to name the reply_to_session tool or the
command from providers.AcceptsMCPServer, which will now be wrong for opencode.

An opencode session could report what it was doing and nothing else.
Claude Code and Codex get the other direction — list the sessions, hand
one a task, answer one, open a session with a worktree under it, close
one, read the checkouts — as MCP tools lich registers when it spawns
them. opencode has no flag to be told about a server, and a plugin there
cannot register one either. It can define tools, so the same seven are
defined here, under the same names: an agent that learns one surface
finds the other where it expects it.

Each tool builds an argv and runs the lich binary. Posting to the
endpoint directly would have been fewer moving parts and the third
implementation of one contract, in a repository lich cannot see — an
argument that moved in its docs/cli.md would have gone red in neither.
This way what lives here is the argv, and every rule, refusal and wording
stays where it is written; a failure answers with lich's own stderr,
unedited.

Two absences on purpose, and the reports survive both. Outside lich
nothing is registered: seven tools that could only answer "no lich is
running" would sit in the prompt of every unrelated opencode session.
And the tool helper is imported inside a try, because it resolves from
opencode's own plugin dependencies and a module dropped where they do not
reach must still report.

Verified against a running opencode server: the seven appear in its tool
list beside the native ones, and against a live lich they list, refuse
and open for real.
@omartelo
omartelo merged commit 3cbfcfc into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant