Lacode
Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.
0
1,632
近 7 天 1.5k
39.7
生态多维模型
12 小时前
2026-10-20
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@lawaty/lacode@0.4.2"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@lawaty/lacode@0.4.2"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D @lawaty/lacodeOpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
Ultimate opencode setup for Go users
Beta — experimental, changing fast. LaCode ships on the npm
betadist-tag rather thanlatest, so nothing pulls it by accident: install@lawaty/lacode@beta, or pin an exact version. Interfaces here can change between releases without a major bump.
LaCode is an opencode plugin. It routes delegated work onto a weighted pool of cheap models instead of the expensive one, reaps subagents that hang without telling anyone, keeps a maintained map of your repository so agents stop exploring it cold on every task, and tells you when a parallel session finishes or is waiting on you — including sessions running on remote hosts over SSH, where the desktop notification would otherwise die on a machine that has no desktop.
Install it with one config line. It adds agents and commands it finds missing, and it never overwrites one you already defined.
Upgrading? See CHANGELOG.md — it ships with the package, so it
is also on disk at node_modules/@lawaty/lacode/CHANGELOG.md.
Why
| Capability | The problem it solves |
|---|---|
| Weighted model pool | An expensive main model burns through its quota doing file listing. |
| Per-model cooldowns and failover | A provider rate-limits mid-session and the whole thing hard-fails, while other providers sit idle. |
| Hang reaper | A wedged subagent holds its slot forever, so the session stalls with no error and no timeout. |
| Desktop notifications | With several sessions open you cannot see which one finished, or which is blocked on a question. |
| SSH notification tunnel | Work on a remote host raises its notification on that host, which has no desktop. |
| Repository context map | Every task starts by exploring your codebase cold, and every agent re-discovers the same layout, the same entry points and the same boundaries — paid for again on every run. |
Getting started
1. Install
Add one line to your opencode config — ~/.config/opencode/opencode.json, or
the .jsonc beside it:
{ "plugin": ["@lawaty/lacode"] }
That is the entire install. opencode resolves an npm plugin specifier itself
and installs it at startup, so there is no npm install to run first and no
directory to create. Do not run npm i -g @lawaty/lacode: a global npm install is
never consulted by opencode, so it changes nothing about whether the plugin
loads.
To install it for every project on the machine rather than per config, let the CLI do the same thing:
opencode plugin @lawaty/lacode -g
2. Restart opencode
Nothing here hot-reloads. The plugin array is read at startup, so without a
restart the change does not take effect and the agent set will look stale for no
visible reason.
3. Check it loaded
opencode agent list | grep -E '^(explore|implement|context)'
You should see explore, implement and context, each with hidden -1..-4
pool variants alongside. That is the whole install, and you are done.
If that prints nothing, the plugin did not load — and opencode will not tell you: it starts normally, logs nothing, and the agents are simply absent. A plugin that fails to load is silent by design of the host, so this check is the only way to know. The troubleshooting guide walks the procedure.
4. Use it
Nothing to turn on. Ask for something that delegates, and the pool takes it from there:
> use the explore agent to find where auth tokens are validated
To see what the pool is doing underneath:
/lacode-doctor # slots, model resolution, OmniRoute — report only, changes nothing
lacode version # which LaCode is actually running, and whether opencode loads that one
In a repository with no .opencode/context/ yet, the first session builds it in
the background; /context-init forces it, and Context map
explains what it is for.
Configure
Plugin options are a tuple — the package name, then an options object. Every option is optional:
{
"plugin": [
["@lawaty/lacode", {
"enabled": true,
"models": {
"slots": [
{ "model": "anthropic/claude-sonnet-4-5", "weight": 2 },
{ "model": "opencode/big-pickle", "weight": 1 }
]
}
}]
]
}
| Option | Default | Meaning |
|---|---|---|
enabled |
true |
The single off switch. false injects nothing at all and does no writes. |
models |
— | An inline slot list. Highest-priority source. |
modelsFile |
— | An explicit slot file; short-circuits the resolution chain. |
reapMs |
30000 |
Hang-reaper interval. |
probeMs |
900000 |
How often benched models are checked by actually using them. 0 turns the periodic check off and leaves the pool_recheck tool as the only way to ask. |
selfUpdate |
true |
Check for a newer release at most once per ~6h and install it in the background; it applies on the next opencode start. false — or LACODE_NO_SELFUPDATE=1 — turns the check off. |
sound |
true |
Play a short tone with each notification, one per outcome, so you can tell a finished session from a question or a permission prompt without reading the popup. It is opt-out: false — or LACODE_SOUND=0 (false, off, no work too, and beat the option) — turns the sound off, and anything else, a typo included, leaves it on. |
omniroute |
— | Optional OmniRoute gateway integration. Absent or { enabled: false } changes nothing. |
Staying up to date
opencode's plugin cache is write-once: it resolves @lawaty/lacode to an
exact version once and never re-resolves — not on restart, not with
opencode plugin -g -f, not with a range pin, not with a tagged config spec.
Nothing in the host re-resolves it, so the plugin owns its own currency
(US-27): at startup LaCode compares its own version to npm's latest (at most
once per ~6h, silent when offline) and, when behind, reinstalls its own cache
entry in the background using the opencode binary — no npm or bun required.
The running session keeps its loaded code; the new version takes effect on the
next start, and the log says so. Run lacode update to do it on demand, or
lacode update --check to just report.
Which LaCode is running
lacode version answers it — and the answer is often not the version number you
expected, because the plugin entry in your config decides which copy is loaded:
lacode version
version 0.4.1
root /home/you/lacode
source checkout
commit <hash> (dirty)
dist current
config /home/you/.config/opencode/opencode.jsonc
plugin file:///home/you/lacode/dist/index.js
this copy yes — opencode loads this copy
npm ls -g @lawaty/lacode answers a different question. It reports the package
opencode resolves by name, which is not necessarily the one running: a config
may load the plugin by absolute path, and a global install can sit there
reporting a version nothing uses. So this command reads your actual plugin
array and says whether it resolves to the copy speaking.
source distinguishes a git checkout from an npm install, commit and the
dirty flag name what the code was built from, and dist is the trap worth
knowing about — opencode loads dist/index.js while you edit and pull src/,
so a git pull can leave you running a build that differs from the installed
package, with nothing failing.
When it reports stale, rebuild with npm run build. No network is touched;
lacode update --check is what asks npm whether something newer exists.
What you get
Three agents, pooled and rotated: explore, implement and context. You spawn
the base name and the pool picks a cheap model slot for it. Behind each base sits
up to four hidden variants — one per slot — and a spawn of the bare name is the
only thing that works: naming a variant lets the caller choose the provider
directly, which is exactly what the rotation and the per-slot ceiling exist to
prevent, so it is refused.
A model that rate-limits or runs out of quota is benched and taken out of the
rotation until its window ends, so one sick provider does not take the pool with
it. Those windows are computed backoff rather than a promise from the provider,
and a quota-shaped one can run for hours — long enough for a provider that has
since recovered, or whose quota you reset in its own console, to stay excluded
while every delegated spawn queues behind the survivors. So LaCode asks instead
of guessing: every fifteen minutes, and on demand via the pool_recheck tool, it
probes each benched model with one real request on that exact model. A model that
answers is re-admitted immediately — nothing restarts, nothing reloads — and a
model that is still limited keeps exactly the window it had, because a check must
never be the thing that extends the wait. A change is reported as a toast and a
log line; a pass that finds nothing changed says nothing at all.
Six commands: /context-init, /context-update, /context-review,
/lacode-doctor, /agent-instructions and /pool. Everything is added only
when your config does not already
define it — an agent or command you wrote yourself is left byte-identical,
never patched or partially merged. {"enabled": false} turns the whole thing off
without leaving a trace. The exact injection list, and the five keys that are
deliberately not injected, are in
docs/CONFIGURATION.md.
Context map
Agents are stateless between tasks, and exploring your repository is the most
expensive part of any task — so every agent pays for it again, re-deriving the
same layout, entry points and boundaries from scratch. LaCode keeps a short,
maintained map of the repository in .opencode/context/ (five navigation files:
architecture, contexts, conventions, workflows, decisions, plus
active.md — the active backlog) and injects
a standing rule that every agent reads the relevant file before exploring and
starts from the canonical entry points it lists. The map holds conclusions, not
inventories: it answers "what should I look at first?" and nothing else, which
is what makes reading it cheaper than the exploration it replaces.
It keeps itself current. When a session that edited code goes idle, LaCode
starts a context run that borrows a slot from the same pool every
other subagent uses and updates the map from the changed paths and their
architectural neighbourhood — defaulting to no edit, since a changed file is
not a changed fact. Only that one agent ever writes the map; everything else
reports what belongs in it, so concurrent sessions cannot contradict each other.
You can also drive it by hand, which is the move when you do not trust what you
are reading:
/context-init # build the map, or verify and refresh an existing one
/context-update # incremental refresh; accepts a focus, e.g. /context-update src/payments
/context-review # audit it for staleness, contradictions, duplication and bloat
/lacode-doctor # check the pool, model resolution and the OmniRoute gateway. Report only
/agent-instructions # apply a client instruction to the project's AGENTS.md — client-only, see below
/pool # rewrite the pool slot table in ~/.config/lacode/pool.txt; restart to apply
The map is local and untracked — it describes your checkout on your machine, so it is not committed and not synced. If it is missing, agents check, find nothing and carry on without it. What this buys is tokens not spent re-discovering structure; as with everything else here, no savings percentage is published, because none has been independently measured. The mechanism, the permission rule that makes one writer possible, and what the log lines mean are in docs/CONTEXT.md.
Standing instructions
The project's root AGENTS.md is your own statement of intent: it records
what you want worked on and how you want it approached. It says nothing about
where code lives, so it can never contradict the code, and within that domain
it outranks the generated context files and any agent's inference about what
matters right now.
The file is per-clone and not committed, so it is yours rather than shared team state. There is no fixed schema and no prescribed headings — you write whatever you like, and every heading you add is treated as direction.
Anything you have recorded there is read before work starts, and what you have recorded is treated as settled by the agents — not as permission to skip reading the code.
Who may write it. Every agent is explicitly permission-denied from
touching this path, so no automatic run can ever alter it. The one exception
is the hidden instructions agent, reachable solely through the
/agent-instructions command.
/agent-instructions — the one sanctioned write path. Type
/agent-instructions <text> and the instructions agent applies whatever
instruction you have given to AGENTS.md, then reports what it changed. It is
a normal agent turn, not a dead append: tell it what you want and review the
result. The instructions agent is the one agent permitted to edit that file,
its write grant is scoped to AGENTS.md alone, and no other agent can spawn it —
the command is the only route in. It may also read .opencode/context/, so a
directive does not restate a fact the map has already settled or contradict one it
has not read; the map stays read-only to it, and the context agent remains its
only writer.
Cost
The mechanism is specific and small: bulk exploration, implementation and repository cartography run on pooled cheap or free subagents instead of the main model. There are two separate effects here, and they are worth telling apart because only one of them is about money per token:
- The tokens are spent on a cheaper model. A read, a grep, or a mechanical edit performed by a near-free subagent costs per-token approximately nothing, where the same call on your main model does not. This is the effect people usually mean when they say the pool saves money.
- The main model's context is never filled with file contents it does not
need. A delegated explorer returns a conclusion and a
file:line, not forty tool calls' worth of source. The expensive model spends its attention on design work instead of on reconstructing a directory tree, and its context window — a hard, non-renewable budget — is spent on reasoning rather than on raw file text.
The second is the larger of the two, and it is not a cost-per-token effect at all: it is a capacity effect. It holds even when the pooled model is not free, and it is why the pool is scoped to mechanical work. Work that needs real judgement belongs to the main model regardless of what the pool is running, because pulling a difficult conclusion back through a subagent costs more context on the far side than reading the file directly would have.
That scoping is why the bundled preset is free: pooled work is work you would
not have wanted the main model to do anyway. Free is the shipped default, not
an enforced restriction. The pool is open — it accepts any model you give it,
free, cheap or expensive, and never inspects, prices, warns about, filters or
rewrites one. The only per-model rule is structural: a slot must be written
provider/model-id so it is addressable at all. That default lives in exactly one
place, presets/free-tier.json, where you can read it and change it (US-7, US-23).
Agents start from the maintained context map above rather than exploring cold, so the structure of your codebase is written down once instead of re-derived on every task. Load is counted per model across every agent type and every process, so a slot cannot hide behind its own dedicated model while another idles, and a model that just rate-limited one project is skipped by all of them.
No savings percentage is published here, because none has been independently measured. What the mechanism is worth in money depends entirely on the token mix of your tasks and on the models you compare the pooled run against. A figure from someone else's workload is not a claim about yours.
The harness exists so you can produce your own number:
npm run benchmark # offline simulation from the pinned price snapshot
npm run benchmark -- --real # runs both sides for real and reads back the tokens
Both print tokens and cost for the same scenario run two ways, and both refuse
to emit a number they cannot derive — an unpriced model reports unknown, never
0. How the two modes differ is in docs/BENCHMARKS.md,
which explains the harness and deliberately contains no results.
Remote hosts
Notifications raised on a remote host need a way back to your desktop. The reverse SSH forward is what carries them:
lacode notify "build finished" "4 files changed" # local desktop, or forwarded if remote
lacode doctor # what the environment resolves to
lacode notify is fire-and-forget and always exits 0 — an unreachable desktop
must never be able to fail the process that tried to notify you. lacode forward HOST -- COMMAND runs a command on another host over ssh and exits with its
status.
The full CLI is notify, forward, doctor, version, update, receive
and tunnel setup. version reports which copy of LaCode is running — and
whether opencode is loading that same copy — and update [--check] refreshes an
installed one. receive is the desktop end of the tunnel (the forced command a
remote key is restricted to) and tunnel setup provisions the key and the
forward.
Configuration, the payload format, the receiver contract and holding the forward open are in docs/TUNNEL.md.
Further reading
Optional. Nothing above needs any of it — this is where the detail lives once you want it.
| Install and troubleshooting | Where opencode puts the package, what to do when the agents are missing, the version matrix, macOS differences, and the lacode CLI. |
| Configuration reference | Every plugin option, how the pool resolves its slots, per-agent tool grants, and exactly what gets written into your config. |
| OmniRoute gateway | The optional local gateway integration: setup, full option set, a latency-measured free combo, and its troubleshooting table. |
| Context map | The mechanism behind .opencode/context/ — the one-writer permission rule, the files, and what the log lines mean. |
| Remote hosts and notifications | The SSH notification hop, payload format, receiver contract, and holding the forward open. |
| Benchmarks | How the cost harness works, and why it deliberately publishes no savings number. |
| User stories | The behavioural contracts behind all of the above, written so a test can assert them. |
| Operations runbook | Maintainer-facing: what runs where, what needs a restart, how the pieces are distributed across machines. |
| Changelog | What changed in each release. |
Development
npm run build # esbuild -> dist/ (what the tarball ships)
npm test # node --test tests/ — offline, no network, no running server
npm run benchmark # the cost harness; see docs/BENCHMARKS.md
Node ≥ 22.18.
npm run build compiles each entry point in package.json (exports and
bin) into dist/ with esbuild, one bundle per entry point, so nothing in
src/ has to be renamed to satisfy node's ESM resolver. @opencode-ai/plugin
and the node builtins are external — see the header of scripts/build.mjs for
why a peer dependency must never be inlined. dist/ is generated and
gitignored; prepack builds it, so npm pack cannot produce a stale tarball.
The sources are TypeScript and the tests import src/ directly (node strips the
types outside node_modules, which is exactly where the tests live), so the
loop is: edit src/, run npm test. Only the packaging fixtures build, and
they call the same scripts/build.mjs the tarball does.
tests/stories/ holds the story suites and tests/stories/coverage.test.mjs
is the guard: it reads the ### US-N: headings out of docs/USER-STORIES.md
and the [US-N] markers out of the test names and fails if they disagree, in
either direction. A test that covers a story must have the id at the start of
its name — test("[US-7] a paid model is accepted") — or the coverage guard
fails it.
A story with no test is as much a failure as a test naming a story that does not
exist.
License
MIT licensed. Based on work by lawaty.
同类生态推荐
Matrixx
opencode-matrixx
The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools
Commandcode
@brainervirus/opencode-commandcode
Command Code API provider for opencode — use Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax, and Step models via Command Code
Commandcode
@herouucn/opencode-commandcode
Inject Command Code API provider into opencode — access Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax & Step via unified Command Code API. / 将 Command Code API provider 注入 opencode —— 通过统一的 Command Code API 使用 Claude、GPT、Gemini、DeepSeek、Qwen、Kim