@lawaty/lacode 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +32 -0
- package/README.md +562 -2
- package/dist/agents.js +819 -0
- package/dist/commands.js +109 -0
- package/dist/index.js +2250 -0
- package/dist/lacode.js +254 -0
- package/dist/pool.js +487 -0
- package/dist/tunnel.js +180 -0
- package/docs/BENCHMARKS.md +127 -0
- package/docs/CONTEXT.md +211 -0
- package/docs/TUNNEL.md +174 -0
- package/docs/USER-STORIES.md +421 -0
- package/package.json +60 -4
- package/presets/README.md +121 -0
- package/presets/free-tier.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lawaty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Attribution
|
|
26
|
+
|
|
27
|
+
This project is MIT licensed. The MIT terms above require only that the
|
|
28
|
+
copyright notice travel with the software — nothing more. As a courtesy,
|
|
29
|
+
though, please keep lawaty credited on derivative work and credit the
|
|
30
|
+
original project when forking:
|
|
31
|
+
|
|
32
|
+
Based on work by lawaty — https://github.com/lawaty/lacode
|
package/README.md
CHANGED
|
@@ -1,3 +1,563 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LaCode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Ultimate opencode setup for Go users
|
|
4
|
+
|
|
5
|
+
LaCode is an opencode plugin. It routes delegated work onto a weighted pool of
|
|
6
|
+
cheap models instead of the expensive one, reaps subagents that hang without
|
|
7
|
+
telling anyone, keeps a maintained map of your repository so agents stop
|
|
8
|
+
exploring it cold on every task, and tells you when a parallel session finishes
|
|
9
|
+
or is waiting on you — including sessions running on remote hosts over SSH,
|
|
10
|
+
where the desktop notification would otherwise die on a machine that has no
|
|
11
|
+
desktop.
|
|
12
|
+
|
|
13
|
+
Install it with one config line. It adds agents and commands it finds missing,
|
|
14
|
+
and it never overwrites one you already defined.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
| Capability | The problem it solves |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Weighted model pool | An expensive main model burns through its quota doing file listing. |
|
|
21
|
+
| Per-model cooldowns and failover | A provider rate-limits mid-session and the whole thing hard-fails, while other providers sit idle. |
|
|
22
|
+
| Hang reaper | A wedged subagent holds its slot forever, so the session stalls with no error and no timeout. |
|
|
23
|
+
| Desktop notifications | With several sessions open you cannot see which one finished, or which is blocked on a question. |
|
|
24
|
+
| SSH notification tunnel | Work on a remote host raises its notification on that host, which has no desktop. |
|
|
25
|
+
| 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. |
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Add one line to your opencode config — `~/.config/opencode/opencode.json`, or
|
|
30
|
+
the `.jsonc` beside it:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "plugin": ["@lawaty/lacode"] }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**That is the entire install.** opencode resolves an npm plugin specifier itself
|
|
37
|
+
and installs it at startup, so there is no `npm install` to run first and no
|
|
38
|
+
directory to create. Do not run `npm i -g @lawaty/lacode`: a global npm install
|
|
39
|
+
is never consulted by opencode, so it changes nothing about whether the plugin
|
|
40
|
+
loads.
|
|
41
|
+
|
|
42
|
+
It is also the form to keep as your default: if your opencode does not load the
|
|
43
|
+
bare specifier, add to this line rather than replacing it. The
|
|
44
|
+
[procedure below](#if-the-agents-are-missing) says when that is, and
|
|
45
|
+
[which versions this is known on](#which-opencode-versions-this-is-known-on)
|
|
46
|
+
says what is actually verified.
|
|
47
|
+
|
|
48
|
+
To install it explicitly instead — or to install it once for every project on
|
|
49
|
+
the machine rather than per config — use the CLI, which does the same thing:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
opencode plugin @lawaty/lacode # this project
|
|
53
|
+
opencode plugin @lawaty/lacode -g # global, for every project on the machine
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Restart opencode afterwards.** Nothing here hot-reloads: the `plugin` array is
|
|
57
|
+
read at startup, so without a restart the change does not take effect and the
|
|
58
|
+
agent set will look stale for no visible reason.
|
|
59
|
+
|
|
60
|
+
### Where it is installed
|
|
61
|
+
|
|
62
|
+
opencode installs npm plugins itself, into one directory per specifier under its
|
|
63
|
+
cache — not into your project's `node_modules`, and not from anywhere a global
|
|
64
|
+
npm install put them. Ask opencode where that cache is instead of assuming,
|
|
65
|
+
because where it lives differs by platform:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
opencode debug paths
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It prints the directories opencode is using on this machine — config, data, log
|
|
72
|
+
and the rest. Find the **cache** line and append
|
|
73
|
+
`/packages/<specifier>/node_modules/` to the path it reports. On Linux with no
|
|
74
|
+
`XDG_CACHE_HOME` set, that comes out as, for example:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
~/.cache/opencode/packages/@lawaty/lacode@latest/node_modules/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**That last line is an illustration of one platform's layout, not a path to
|
|
81
|
+
copy.** Where the same cache root sits on another operating system is not
|
|
82
|
+
something this document should guess at — read it from `opencode debug paths`
|
|
83
|
+
and use what it prints.
|
|
84
|
+
|
|
85
|
+
To inspect what a specifier actually resolved to, list its directory under that
|
|
86
|
+
cache root. An **empty** directory with no `node_modules` inside it means the
|
|
87
|
+
install failed — almost always an unpublished or misspelled package — and
|
|
88
|
+
opencode starts normally **without** the plugin. A **populated** directory there
|
|
89
|
+
means the install succeeded and tells you nothing at all about whether the
|
|
90
|
+
plugin was loaded; that is the next section, and telling those two apart is the
|
|
91
|
+
whole reason for reading it.
|
|
92
|
+
|
|
93
|
+
### If the agents are missing
|
|
94
|
+
|
|
95
|
+
A plugin that fails to load is silent: opencode starts normally, prints nothing,
|
|
96
|
+
logs nothing, and the agents are simply not there. Work down this. Every step
|
|
97
|
+
below is written to hold whichever opencode version you are running — none of
|
|
98
|
+
them depends on knowing the answer in advance.
|
|
99
|
+
|
|
100
|
+
**1 — Write the bare specifier and restart.**
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "plugin": ["@lawaty/lacode"] }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Nothing here hot-reloads, so the restart is part of the step, not a footnote.
|
|
107
|
+
|
|
108
|
+
**2 — Check whether the plugin loaded.**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
opencode agent list | grep -E '^(explore-fast|implement-fast|explore-deep|implement-deep)'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A working install prints `explore-fast`, `implement-fast`, `explore-deep` and
|
|
115
|
+
`implement-deep`, each with the hidden `-1`..`-4` pool variants alongside. If the
|
|
116
|
+
grep returns nothing and the list holds only opencode's own built-ins, the plugin
|
|
117
|
+
did not load. Run it in the same shell environment opencode runs in — an
|
|
118
|
+
`agent list` under a different `HOME` or `XDG_CONFIG_HOME` tests a different
|
|
119
|
+
machine. **If the agents are listed, stop here: the one line above is the whole
|
|
120
|
+
install**, and nothing further in this section applies to you.
|
|
121
|
+
|
|
122
|
+
**3 — Decide which failure you are looking at.** Inspect the package directory
|
|
123
|
+
under the cache root from
|
|
124
|
+
[Where it is installed](#where-it-is-installed), and also check that `plugin`
|
|
125
|
+
really is in the config file opencode reads.
|
|
126
|
+
|
|
127
|
+
- **Empty** (no `node_modules` inside): the install failed — check the package
|
|
128
|
+
name, and that it is published. There is nothing to load yet.
|
|
129
|
+
- **Populated**: the install worked and the loader dropped the plugin. Go to
|
|
130
|
+
step 4. These two look identical from the outside, which is why this step
|
|
131
|
+
exists.
|
|
132
|
+
|
|
133
|
+
**4 — If the install worked and the agents are still missing, point the loader
|
|
134
|
+
at the built file.** Add a second entry; leave the bare name in place:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"plugin": [
|
|
139
|
+
"@lawaty/lacode",
|
|
140
|
+
"file:///ABSOLUTE-PATH-FROM-debug-paths/packages/@lawaty/lacode@latest/node_modules/@lawaty/lacode/dist/index.js"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`ABSOLUTE-PATH-FROM-debug-paths` is a placeholder to be replaced, not a
|
|
146
|
+
directory: take the cache path `opencode debug paths` reports for **your**
|
|
147
|
+
machine and put it in front of `/packages/…`. It must be an absolute path with
|
|
148
|
+
`$HOME` already expanded and no `~` left in it. Re-derive it after a cache wipe
|
|
149
|
+
or a reinstall — the directory is keyed by specifier, so it moves when the
|
|
150
|
+
version does.
|
|
151
|
+
|
|
152
|
+
Why keep both entries instead of swapping the bare name out:
|
|
153
|
+
|
|
154
|
+
- The bare name is what makes opencode install the package into its cache, and
|
|
155
|
+
therefore what creates the path the `file://` entry points at.
|
|
156
|
+
- The `file://` entry is what actually loads the plugin.
|
|
157
|
+
- They **cannot double-inject** on opencode 1.18.35, where the bare name
|
|
158
|
+
contributes nothing at all: the pair loads the plugin exactly once. On any
|
|
159
|
+
other version this is untested, which is precisely why step 4 is something you
|
|
160
|
+
take *because* step 2 showed the agents missing, and never on the expectation
|
|
161
|
+
that it would help.
|
|
162
|
+
|
|
163
|
+
**5 — Restart, and expect two restarts after a cache wipe.** The first start
|
|
164
|
+
installs the package and creates the path the `file://` entry points at, but a
|
|
165
|
+
path that does not exist at startup is a path that cannot be loaded in that same
|
|
166
|
+
run. The second start finds it and loads the plugin. After that it is steady
|
|
167
|
+
state.
|
|
168
|
+
|
|
169
|
+
This package also publishes `main` and an `exports["./server"]` entry pointing at
|
|
170
|
+
the same built file, because those are two of the shapes a plugin loader
|
|
171
|
+
resolving an entry point consults. They are forward-compatibility metadata and
|
|
172
|
+
they cost nothing — but they do **not** fix this on 1.18.35: adding either one to
|
|
173
|
+
the *installed* cache copy was tested there and the bare specifier still loaded
|
|
174
|
+
nothing. No other version has been observed where they help either. Use the
|
|
175
|
+
`file://` entry.
|
|
176
|
+
|
|
177
|
+
### Which opencode versions this is known on
|
|
178
|
+
|
|
179
|
+
Exactly one version has been measured, and the procedure above is deliberately
|
|
180
|
+
not built around that fact. Treat this table as the evidence it is, which is one
|
|
181
|
+
row wide.
|
|
182
|
+
|
|
183
|
+
| opencode version | bare spec loads? | `file://` needed? | evidence |
|
|
184
|
+
| --- | --- | --- | --- |
|
|
185
|
+
| 1.18.35 | No — installs the package, then silently drops it | Yes | **Verified** on 1.18.35: a populated cache directory, zero agents injected, no error and no log line; the `file://` entry loads the full agent set. |
|
|
186
|
+
| Any other version | **Unknown** | **Unknown** | **Untested.** No other opencode version has been run against this package in either direction. |
|
|
187
|
+
|
|
188
|
+
Every command quoted on this page was run against 1.18.35. **If your version is
|
|
189
|
+
not the one measured here, follow the procedure above rather than reading this
|
|
190
|
+
table as coverage of your build**: each step stands on its own, and step 2 tells
|
|
191
|
+
you which situation you are in before any version-specific advice is relevant.
|
|
192
|
+
|
|
193
|
+
### macOS
|
|
194
|
+
|
|
195
|
+
The plugin runs on macOS. It is plain JavaScript with no native dependency, and
|
|
196
|
+
what the package publishes is the built JavaScript rather than TypeScript
|
|
197
|
+
sources, so nothing about loading it is Linux-only. Use the procedure above
|
|
198
|
+
unchanged.
|
|
199
|
+
|
|
200
|
+
Three things differ, and all three are worth reading rather than assuming:
|
|
201
|
+
|
|
202
|
+
- **Paths.** Do not copy a cache path out of this document — the example under
|
|
203
|
+
[Where it is installed](#where-it-is-installed) is one platform's layout. Run
|
|
204
|
+
`opencode debug paths` and build the `file://` entry from what it reports.
|
|
205
|
+
This page deliberately does not name a macOS cache root: where opencode keeps
|
|
206
|
+
it on a Mac has not been established here, and a plausible-looking guess is
|
|
207
|
+
worse than an instruction to look.
|
|
208
|
+
- **Notifications** are delivered through `osascript`, or through
|
|
209
|
+
`terminal-notifier` when `osascript` is not installed, rather than through the
|
|
210
|
+
Linux `notify-send` (US-26). `lacode doctor` reports which channel your machine
|
|
211
|
+
would actually use.
|
|
212
|
+
- **The repository's shell scripts are Linux-only by design**, and are not in the
|
|
213
|
+
published package. They drive one person's SSH topology, so each is on the side
|
|
214
|
+
it belongs to:
|
|
215
|
+
|
|
216
|
+
| Script | Runs on | Why it is Linux-only |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `bin/oc`, `bin/oc-notify` | the **remote host** that runs opencode | Server-side. They are pushed to `~/bin/` on each host by `bin/oc-sync` and notify *your desktop* over the tunnel. |
|
|
219
|
+
| `bin/oc-notify-receiver` | your **desktop** | Delivers through `notify-send` (libnotify), and the persistent forward that reaches it is a systemd user unit. |
|
|
220
|
+
| `bin/oc-sync` | your **desktop** | Installs those systemd user units and runs `loginctl enable-linger`. |
|
|
221
|
+
|
|
222
|
+
None of this is a limitation of the package: a macOS client installs the plugin
|
|
223
|
+
and uses the `lacode` CLI, and both are portable. The scripts above are the
|
|
224
|
+
author's personal remote-tunnel tooling. `bin/oc-sync` refuses to run where it
|
|
225
|
+
cannot work and says which `lacode` subcommand to use instead, rather than
|
|
226
|
+
failing somewhere less obvious.
|
|
227
|
+
|
|
228
|
+
**The test suite runs on macOS.** It is written to pass on a Mac as well as on
|
|
229
|
+
Linux, so "it works on a Mac" is a supported claim rather than an intention. The
|
|
230
|
+
few checks that exercise the Linux-only shell scripts above skip themselves on a
|
|
231
|
+
machine that cannot run them, naming the reason — a bash 3.2 with no `mapfile`,
|
|
232
|
+
or no GNU `timeout` — rather than passing silently or failing confusingly.
|
|
233
|
+
|
|
234
|
+
### The `lacode` CLI
|
|
235
|
+
|
|
236
|
+
The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`) — see
|
|
237
|
+
[Remote hosts](#remote-hosts). The plugin install does **not** put it on your
|
|
238
|
+
`PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
|
|
239
|
+
globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
|
|
240
|
+
global install is for the command only — it is still never what makes the plugin
|
|
241
|
+
load.)
|
|
242
|
+
|
|
243
|
+
### What the install gives you
|
|
244
|
+
|
|
245
|
+
Two things are worth knowing about what npm actually hands you:
|
|
246
|
+
|
|
247
|
+
- **`@opencode-ai/plugin` is a peer dependency.** It is not vendored into this
|
|
248
|
+
package and never will be: a second copy of the SDK inside the tarball would
|
|
249
|
+
mean opencode and the plugin holding two different module instances, and
|
|
250
|
+
instance identity is what plugin state hangs on. npm installs the peer for you;
|
|
251
|
+
if you install with a tool that does not, install `@opencode-ai/plugin` at the
|
|
252
|
+
same version yourself.
|
|
253
|
+
- **You get compiled JavaScript, not TypeScript sources.** The tarball ships
|
|
254
|
+
`dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
|
|
255
|
+
`dist/` cannot be published). This is not a preference: node strips types
|
|
256
|
+
natively from 22.18 but **refuses to for files under `node_modules`**, so a
|
|
257
|
+
package that shipped `.ts` worked fine in a checkout and failed on install —
|
|
258
|
+
`lacode --help` died with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A
|
|
259
|
+
published package cannot depend on its consumer's loader being lenient.
|
|
260
|
+
|
|
261
|
+
## Configuration
|
|
262
|
+
|
|
263
|
+
Plugin options are a tuple — the package name, then an options object. Every
|
|
264
|
+
option is optional:
|
|
265
|
+
|
|
266
|
+
```jsonc
|
|
267
|
+
{
|
|
268
|
+
"plugin": [
|
|
269
|
+
["@lawaty/lacode", {
|
|
270
|
+
"enabled": true,
|
|
271
|
+
"models": {
|
|
272
|
+
"slots": [
|
|
273
|
+
{ "model": "anthropic/claude-sonnet-4-5", "weight": 2 },
|
|
274
|
+
{ "model": "opencode/big-pickle", "weight": 1 }
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
}]
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| Option | Default | Meaning |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| `enabled` | `true` | The single off switch. `false` injects nothing at all and does no writes. |
|
|
285
|
+
| `models` | — | An inline slot list. Highest-priority source. |
|
|
286
|
+
| `modelsFile` | — | An explicit slot file; short-circuits the chain below. |
|
|
287
|
+
| `deepModel` | — | A `provider/model-id` pinned onto `explore-deep` and `implement-deep`. |
|
|
288
|
+
| `reapMs` | `30000` | Hang-reaper interval. |
|
|
289
|
+
|
|
290
|
+
### Pool preset resolution
|
|
291
|
+
|
|
292
|
+
The slot list is the first of these that yields usable slots:
|
|
293
|
+
|
|
294
|
+
1. plugin options — `models` in the tuple above
|
|
295
|
+
2. `~/.config/lacode/pool.json` (its parent directory is created for you)
|
|
296
|
+
3. the bundled `presets/free-tier.json`
|
|
297
|
+
4. a built-in fallback, so routing always works
|
|
298
|
+
|
|
299
|
+
A missing, unreadable or structurally broken source falls through to the next and
|
|
300
|
+
logs one warning; nothing here throws into opencode. Nothing hot-reloads —
|
|
301
|
+
change a preset, then restart every opencode process.
|
|
302
|
+
|
|
303
|
+
**Any model list is accepted, including paid ones** (US-7). There is no free-tier
|
|
304
|
+
allowlist anywhere in the package, and no cost check of any kind: only the
|
|
305
|
+
structure is validated. The bundled preset is built from free models because
|
|
306
|
+
that is a sensible default, not because the pool requires it.
|
|
307
|
+
|
|
308
|
+
### Which models run which agent
|
|
309
|
+
|
|
310
|
+
Only the **pool** writes a model onto a pooled agent, from whichever source wins
|
|
311
|
+
above. The pooled bases (`explore-fast`, `implement-fast`, `context-manager`)
|
|
312
|
+
and their hidden `-1`..`-4` variants therefore carry no model of their own: a
|
|
313
|
+
model written next to an agent would be a second source of truth that silently
|
|
314
|
+
wins whenever routing falls through. Spawn a base and let the pool route it.
|
|
315
|
+
|
|
316
|
+
`explore-deep` and `implement-deep` are **not** pooled — they are the deliberate
|
|
317
|
+
escalation above a pooled tier — so nothing routes them and nothing writes their
|
|
318
|
+
model. Left alone they fall through to your default model, which turns "I am
|
|
319
|
+
stuck, escalate" into an unannounced jump to the most expensive model in your
|
|
320
|
+
setup. Set `deepModel` to pin them:
|
|
321
|
+
|
|
322
|
+
```jsonc
|
|
323
|
+
"plugin": [["@lawaty/lacode", { "deepModel": "your-provider/your-deep-model" }]]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
It is an option and not a literal in the package because which model to escalate
|
|
327
|
+
onto is a property of your providers, not of LaCode. If you keep a `whitelist`
|
|
328
|
+
on that provider, LaCode appends the pinned model to it for you: the whitelist is
|
|
329
|
+
an allowlist, so opencode drops any model a provider offers that it does not
|
|
330
|
+
name, and an unlisted pin would resolve to nothing at all. If you have no
|
|
331
|
+
whitelist there, none is written — an absent one already allows every model. With
|
|
332
|
+
`deepModel` unset, no model is written and the deep tiers use your
|
|
333
|
+
default model — the behaviour of earlier versions.
|
|
334
|
+
|
|
335
|
+
### ⚠️ Per-agent tool grants must be in your own config
|
|
336
|
+
|
|
337
|
+
**LaCode cannot grant tools to its agents, and this is not an oversight you can
|
|
338
|
+
work around by configuring it.** If you keep a global `"tools"` block in your
|
|
339
|
+
config — for example `"tools": { "playwright_*": false }` — then the four
|
|
340
|
+
exploration and implementation tiers (`explore-fast`, `explore-deep`,
|
|
341
|
+
`implement-fast`, `implement-deep`, plus their hidden `-1`..`-4` pool variants)
|
|
342
|
+
lose browser access, and **nothing tells them why**: the tool call is simply
|
|
343
|
+
refused mid-task. `context-manager` gets no grant either, which is correct — it
|
|
344
|
+
writes the context map and has no business in a browser.
|
|
345
|
+
|
|
346
|
+
opencode folds an agent's `tools` into its permission list **at config-load
|
|
347
|
+
time**, while decoding the config — before any plugin runs. A plugin's `config`
|
|
348
|
+
hook mutates the already-decoded config, so the fold never re-runs and anything
|
|
349
|
+
it puts in `agent.tools` is inert. (Verified on opencode 1.18.34/1.18.35 with
|
|
350
|
+
`opencode debug agent explore-fast`: a grant written in your own config shows two
|
|
351
|
+
`playwright_*` entries — `deny` then `allow`, last one winning — while the same
|
|
352
|
+
grant injected by the plugin shows only the `deny`.) LaCode's other injected
|
|
353
|
+
fields are not affected; `permission`, `prompt`, `model`, `description`,
|
|
354
|
+
`temperature`, `mode` and `hidden` are all read after the plugin hook runs.
|
|
355
|
+
|
|
356
|
+
**So if you use a global `"tools"` block, declare the grants yourself:**
|
|
357
|
+
|
|
358
|
+
```jsonc
|
|
359
|
+
{
|
|
360
|
+
"tools": { "playwright_*": false },
|
|
361
|
+
"agent": {
|
|
362
|
+
"explore-fast": { "tools": { "playwright_*": true } },
|
|
363
|
+
"explore-deep": { "tools": { "playwright_*": true } },
|
|
364
|
+
"implement-fast": { "tools": { "playwright_*": true } },
|
|
365
|
+
"implement-deep": { "tools": { "playwright_*": true } }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The hidden pool variants (`explore-fast-1`, `implement-fast-2`, …) inherit
|
|
371
|
+
nothing here — add a line per variant you actually spawn, or drop the global
|
|
372
|
+
deny and scope it to the agents that should not have browser access instead.
|
|
373
|
+
|
|
374
|
+
Nothing hot-reloads: after editing your config, restart every running opencode
|
|
375
|
+
process.
|
|
376
|
+
|
|
377
|
+
### What gets injected
|
|
378
|
+
|
|
379
|
+
**19 agent keys**: the 7 bases `build`, `plan`, `explore-fast`, `explore-deep`,
|
|
380
|
+
`implement-fast`, `implement-deep`, `context-manager`, plus one hidden
|
|
381
|
+
`<base>-1`..`-4` pool variant for each of the 3 pooled bases.
|
|
382
|
+
|
|
383
|
+
`build` and `plan` exist in opencode as built-ins, so on a stock config those
|
|
384
|
+
two keys are already taken and merge-without-clobber keeps the built-in — the
|
|
385
|
+
LaCode definitions of `build` and `plan` are what you get if you remove the
|
|
386
|
+
built-ins of those names first.
|
|
387
|
+
|
|
388
|
+
**Five keys are deliberately not injected.** `basic` is a byte-for-byte
|
|
389
|
+
duplicate of `plan` apart from a pinned model, and `plan` is injected instead.
|
|
390
|
+
`expert` pins a specific model and carries a personal benchmark note in its
|
|
391
|
+
description — a package cannot ship someone else's provider, quota or benchmark
|
|
392
|
+
results — though its escalation ladder still resolves, because the two deep
|
|
393
|
+
tiers it escalates to *are* injected. And `explore`, `general` and `implement`
|
|
394
|
+
are only `{ "disable": true }` stubs in the author's own config: turning off
|
|
395
|
+
opencode's built-in agents is a decision about one person's configuration, not
|
|
396
|
+
a default a package should impose on a stranger.
|
|
397
|
+
|
|
398
|
+
One **field** is deliberately not injected either: `tools`, for the
|
|
399
|
+
load-time-folding reason explained above. Nothing else the plugin writes into
|
|
400
|
+
an agent is subject to it.
|
|
401
|
+
|
|
402
|
+
**Installation merges, it does not clobber** (US-16). An agent, command or
|
|
403
|
+
instruction is added only when that key is absent from your config. A
|
|
404
|
+
user-defined agent of the same name is left byte-identical — not patched, not
|
|
405
|
+
partially merged. Skips are counted and logged once.
|
|
406
|
+
|
|
407
|
+
**`enabled: false` turns all of it off** (US-17). No pool model rewriting, no
|
|
408
|
+
agent injection, no command injection, no reaper timer armed, no
|
|
409
|
+
`~/.config/lacode` directory created.
|
|
410
|
+
|
|
411
|
+
These three are specified in [docs/USER-STORIES.md](docs/USER-STORIES.md) as
|
|
412
|
+
US-7, US-16 and US-17, and each has tests.
|
|
413
|
+
|
|
414
|
+
## Context map
|
|
415
|
+
|
|
416
|
+
Agents are stateless between tasks, and exploring your repository is the most
|
|
417
|
+
expensive part of any task — so every agent pays for it again, re-deriving the
|
|
418
|
+
same layout, entry points and boundaries from scratch. LaCode keeps a short,
|
|
419
|
+
maintained map of the repository in `.opencode/context/` (five files:
|
|
420
|
+
`architecture`, `contexts`, `conventions`, `workflows`, `decisions`) and injects
|
|
421
|
+
a standing rule that every agent reads the relevant file before exploring and
|
|
422
|
+
starts from the canonical entry points it lists. The map holds conclusions, not
|
|
423
|
+
inventories: it answers "what should I look at first?" and nothing else, which
|
|
424
|
+
is what makes reading it cheaper than the exploration it replaces.
|
|
425
|
+
|
|
426
|
+
It keeps itself current. When a session that edited code goes idle, LaCode
|
|
427
|
+
starts a `context-manager` run that borrows a slot from the same pool every
|
|
428
|
+
other subagent uses and updates the map from the changed paths and their
|
|
429
|
+
architectural neighbourhood — defaulting to *no edit*, since a changed file is
|
|
430
|
+
not a changed fact. Only that one agent ever writes the map; everything else
|
|
431
|
+
reports what belongs in it, so concurrent sessions cannot contradict each other.
|
|
432
|
+
You can also drive it by hand, which is the move when you do not trust what you
|
|
433
|
+
are reading:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
/context-init # build the map, or verify and refresh an existing one
|
|
437
|
+
/context-update # incremental refresh; accepts a focus, e.g. /context-update src/payments
|
|
438
|
+
/context-review # audit it for staleness, contradictions, duplication and bloat
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The map is local and untracked — it describes your checkout on your machine, so
|
|
442
|
+
it is not committed and not synced. If it is missing, agents check, find
|
|
443
|
+
nothing and carry on as before. What this buys is tokens not spent
|
|
444
|
+
re-discovering structure; as with everything else here, no savings percentage is
|
|
445
|
+
published, because none has been independently measured. The mechanism, the
|
|
446
|
+
permission rule that makes one writer possible, and what the log lines mean are
|
|
447
|
+
in [docs/CONTEXT.md](docs/CONTEXT.md).
|
|
448
|
+
|
|
449
|
+
## Cost
|
|
450
|
+
|
|
451
|
+
The mechanism is specific and small: bulk exploration, implementation and
|
|
452
|
+
repository cartography run on pooled cheap or free subagents instead of the main
|
|
453
|
+
model. There are **two separate effects** here, and they are worth telling apart
|
|
454
|
+
because only one of them is about money per token:
|
|
455
|
+
|
|
456
|
+
1. **The tokens are spent on a cheaper model.** A read, a grep, or a mechanical
|
|
457
|
+
edit performed by a near-free subagent costs per-token approximately nothing,
|
|
458
|
+
where the same call on your main model does not. This is the effect people
|
|
459
|
+
usually mean when they say the pool saves money.
|
|
460
|
+
2. **The main model's context is never filled with file contents it does not
|
|
461
|
+
need.** A delegated explorer returns a conclusion and a `file:line`, not
|
|
462
|
+
forty tool calls' worth of source. The expensive model spends its attention
|
|
463
|
+
on design work instead of on reconstructing a directory tree, and its context
|
|
464
|
+
window — a hard, non-renewable budget — is spent on reasoning rather than on
|
|
465
|
+
raw file text.
|
|
466
|
+
|
|
467
|
+
The second is the larger of the two, and it is not a cost-per-token effect at
|
|
468
|
+
all: it is a capacity effect. It holds even when the pooled model is not free,
|
|
469
|
+
and it is why the pool is scoped to *mechanical* work. Work that needs real
|
|
470
|
+
judgement belongs to the main model regardless of what the pool is running,
|
|
471
|
+
because pulling a difficult conclusion back through a subagent costs more
|
|
472
|
+
context on the far side than reading the file directly would have.
|
|
473
|
+
|
|
474
|
+
That scoping is why the bundled preset is free: pooled work is work you would
|
|
475
|
+
not have wanted the main model to do anyway. **Free is the shipped default, not
|
|
476
|
+
an enforced restriction.** The pool is open — it accepts any model you give it,
|
|
477
|
+
free, cheap or expensive, and never inspects, prices, warns about, filters or
|
|
478
|
+
rewrites one. The only per-model rule is structural: a slot must be written
|
|
479
|
+
`provider/model-id` so it is addressable at all. That default lives in exactly one
|
|
480
|
+
place, `presets/free-tier.json`, where you can read it and change it (US-7, US-23).
|
|
481
|
+
|
|
482
|
+
Agents start from the maintained context map above rather than exploring cold, so
|
|
483
|
+
the structure of your codebase is written down once instead of re-derived on
|
|
484
|
+
every task. Load is counted per model across every agent type and every process,
|
|
485
|
+
so a slot cannot hide behind its own dedicated model while another idles, and a
|
|
486
|
+
model that just rate-limited one project is skipped by all of them.
|
|
487
|
+
|
|
488
|
+
**No savings percentage is published here, because none has been independently
|
|
489
|
+
measured.** What the mechanism is worth in money depends entirely on the token
|
|
490
|
+
mix of your tasks and on the models you compare the pooled run against. A figure
|
|
491
|
+
from someone else's workload is not a claim about yours.
|
|
492
|
+
|
|
493
|
+
The harness exists so you can produce your own number:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
npm run benchmark # offline simulation from the pinned price snapshot
|
|
497
|
+
npm run benchmark -- --real # runs both sides for real and reads back the tokens
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Both print tokens and cost for the same scenario run two ways, and both refuse
|
|
501
|
+
to emit a number they cannot derive — an unpriced model reports `unknown`, never
|
|
502
|
+
`0`. How the two modes differ is in [docs/BENCHMARKS.md](docs/BENCHMARKS.md),
|
|
503
|
+
which explains the harness and deliberately contains no results.
|
|
504
|
+
|
|
505
|
+
## Remote hosts
|
|
506
|
+
|
|
507
|
+
Notifications raised on a remote host need a way back to your desktop. The
|
|
508
|
+
reverse SSH forward is what carries them:
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
lacode notify "build finished" "4 files changed" # local desktop, or forwarded if remote
|
|
512
|
+
lacode doctor # what the environment resolves to
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`lacode notify` is fire-and-forget and always exits 0 — an unreachable desktop
|
|
516
|
+
must never be able to fail the process that tried to notify you. `lacode forward
|
|
517
|
+
HOST -- COMMAND` runs a command on another host over ssh and exits with its
|
|
518
|
+
status.
|
|
519
|
+
|
|
520
|
+
Configuration, the payload format, the receiver contract and holding the forward
|
|
521
|
+
open are in [docs/TUNNEL.md](docs/TUNNEL.md).
|
|
522
|
+
|
|
523
|
+
## User stories
|
|
524
|
+
|
|
525
|
+
Twenty-three stories across seven epics, each written as a behavioural contract
|
|
526
|
+
with acceptance criteria a test can assert mechanically — the pains behind them,
|
|
527
|
+
the Given/When/Then criteria, and the story-to-test mapping are in
|
|
528
|
+
[docs/USER-STORIES.md](docs/USER-STORIES.md).
|
|
529
|
+
|
|
530
|
+
## Development
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
npm run build # esbuild -> dist/ (what the tarball ships)
|
|
534
|
+
npm test # node --test tests/ — offline, no network, no running server
|
|
535
|
+
npm run benchmark # the cost harness; see docs/BENCHMARKS.md
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Node ≥ 22.18.
|
|
539
|
+
|
|
540
|
+
`npm run build` compiles each entry point in `package.json` (`exports` and
|
|
541
|
+
`bin`) into `dist/` with esbuild, one bundle per entry point, so nothing in
|
|
542
|
+
`src/` has to be renamed to satisfy node's ESM resolver. `@opencode-ai/plugin`
|
|
543
|
+
and the node builtins are external — see the header of `scripts/build.mjs` for
|
|
544
|
+
why a peer dependency must never be inlined. `dist/` is generated and
|
|
545
|
+
gitignored; `prepack` builds it, so `npm pack` cannot produce a stale tarball.
|
|
546
|
+
|
|
547
|
+
The sources are TypeScript and the tests import `src/` directly (node strips the
|
|
548
|
+
types outside `node_modules`, which is exactly where the tests live), so the
|
|
549
|
+
loop stays: edit `src/`, run `npm test`. Only the packaging fixtures build, and
|
|
550
|
+
they call the same `scripts/build.mjs` the tarball does.
|
|
551
|
+
|
|
552
|
+
`tests/stories/` holds the story suites and `tests/stories/coverage.test.mjs`
|
|
553
|
+
is the guard: it reads the `### US-N:` headings out of `docs/USER-STORIES.md`
|
|
554
|
+
and the `[US-N]` markers out of the test names and fails if they disagree, in
|
|
555
|
+
either direction. **A test that covers a story must have the id at the start of
|
|
556
|
+
its name** — `test("[US-7] a paid model is accepted")` — or the coverage guard
|
|
557
|
+
fails it.
|
|
558
|
+
A story with no test is as much a failure as a test naming a story that does not
|
|
559
|
+
exist.
|
|
560
|
+
|
|
561
|
+
## License
|
|
562
|
+
|
|
563
|
+
MIT licensed. Based on work by [lawaty](https://github.com/lawaty/lacode).
|