@lawaty/lacode 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -13,6 +13,9 @@ desktop.
13
13
  Install it with one config line. It adds agents and commands it finds missing,
14
14
  and it never overwrites one you already defined.
15
15
 
16
+ Upgrading? See [CHANGELOG.md](CHANGELOG.md) — it ships with the package, so it
17
+ is also on disk at `node_modules/@lawaty/lacode/CHANGELOG.md`.
18
+
16
19
  ## Why
17
20
 
18
21
  | Capability | The problem it solves |
@@ -24,7 +27,9 @@ and it never overwrites one you already defined.
24
27
  | SSH notification tunnel | Work on a remote host raises its notification on that host, which has no desktop. |
25
28
  | 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
29
 
27
- ## Install
30
+ ## Getting started
31
+
32
+ ### 1. Install
28
33
 
29
34
  Add one line to your opencode config — `~/.config/opencode/opencode.json`, or
30
35
  the `.jsonc` beside it:
@@ -35,228 +40,60 @@ the `.jsonc` beside it:
35
40
 
36
41
  **That is the entire install.** opencode resolves an npm plugin specifier itself
37
42
  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
43
+ directory to create. Do not run `npm i -g @lawaty/lacode`: a global npm install is
44
+ never consulted by opencode, so it changes nothing about whether the plugin
40
45
  loads.
41
46
 
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:
47
+ To install it for every project on the machine rather than per config, let the
48
+ CLI do the same thing:
50
49
 
51
50
  ```bash
52
- opencode plugin @lawaty/lacode # this project
53
- opencode plugin @lawaty/lacode -g # global, for every project on the machine
51
+ opencode plugin @lawaty/lacode -g
54
52
  ```
55
53
 
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.
54
+ ### 2. Restart opencode
59
55
 
60
- ### Where it is installed
56
+ **Nothing here hot-reloads.** The `plugin` array is read at startup, so without a
57
+ restart the change does not take effect and the agent set will look stale for no
58
+ visible reason.
61
59
 
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:
60
+ ### 3. Check it loaded
66
61
 
67
62
  ```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/
63
+ opencode agent list | grep -E '^(explore|implement|context)'
78
64
  ```
79
65
 
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.
66
+ You should see `explore`, `implement` and `context`, each with hidden `-1`..`-4`
67
+ pool variants alongside. That is the whole install, and you are done.
92
68
 
93
- ### If the agents are missing
69
+ **If that prints nothing, the plugin did not load** — and opencode will not tell
70
+ you: it starts normally, logs nothing, and the agents are simply absent. A
71
+ plugin that fails to load is silent by design of the host, so this check is the
72
+ only way to know. The
73
+ [troubleshooting guide](docs/INSTALL.md#if-the-agents-are-missing) walks the
74
+ procedure.
94
75
 
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.
76
+ ### 4. Use it
99
77
 
100
- **1 — Write the bare specifier and restart.**
78
+ Nothing to turn on. Ask for something that delegates, and the pool takes it from
79
+ there:
101
80
 
102
- ```json
103
- { "plugin": ["@lawaty/lacode"] }
81
+ ```
82
+ > use the explore agent to find where auth tokens are validated
104
83
  ```
105
84
 
106
- Nothing here hot-reloads, so the restart is part of the step, not a footnote.
107
-
108
- **2 — Check whether the plugin loaded.**
85
+ To see what the pool is doing underneath:
109
86
 
110
87
  ```bash
111
- opencode agent list | grep -E '^(explore|implement|context)'
88
+ /lacode-doctor # slots, model resolution, OmniRoute — report only, changes nothing
89
+ lacode version # which LaCode is actually running, and whether opencode loads that one
112
90
  ```
113
91
 
114
- A working install prints `explore`, `implement` and `context`, each with the
115
- 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.
92
+ In a repository with no `.opencode/context/` yet, the first session builds it in
93
+ the background; `/context-init` forces it, and [Context map](#context-map)
94
+ explains what it is for.
132
95
 
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
- - **Remote hosts are plain npm clients.** A remote host installs the plugin the
217
- same way (`opencode plugin @lawaty/lacode -g`); there is nothing to rsync. The
218
- notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
219
- once with `lacode tunnel setup` — no reverse forward, no listening port, no
220
- systemd unit. See [Remote hosts](#remote-hosts) and docs/TUNNEL.md.
221
- - The repo still carries the author's personal, Linux-only tooling from the older
222
- reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
223
- `bin/oc-sync`). None of it is in the published package, and none of it is needed
224
- for the npm-client path above.
225
-
226
- **The test suite runs on macOS.** It is written to pass on a Mac as well as on
227
- Linux, so "it works on a Mac" is a supported claim rather than an intention. The
228
- few checks that exercise the Linux-only shell scripts above skip themselves on a
229
- machine that cannot run them, naming the reason — a bash 3.2 with no `mapfile`,
230
- or no GNU `timeout` — rather than passing silently or failing confusingly.
231
-
232
- ### The `lacode` CLI
233
-
234
- The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
235
- [Remote hosts](#remote-hosts). The plugin install does **not** put it on your
236
- `PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
237
- globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
238
- global install is for the command only — it is still never what makes the plugin
239
- load.)
240
-
241
- ### What the install gives you
242
-
243
- Two things are worth knowing about what npm actually hands you:
244
-
245
- - **`@opencode-ai/plugin` is a peer dependency.** It is not vendored into this
246
- package and never will be: a second copy of the SDK inside the tarball would
247
- mean opencode and the plugin holding two different module instances, and
248
- instance identity is what plugin state hangs on. npm installs the peer for you;
249
- if you install with a tool that does not, install `@opencode-ai/plugin` at the
250
- same version yourself.
251
- - **You get compiled JavaScript, not TypeScript sources.** The tarball ships
252
- `dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
253
- `dist/` cannot be published). This is not a preference: node strips types
254
- natively from 22.18 but **refuses to for files under `node_modules`**, so a
255
- package that shipped `.ts` worked fine in a checkout and failed on install —
256
- `lacode --help` died with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A
257
- published package cannot depend on its consumer's loader being lenient.
258
-
259
- ## Configuration
96
+ ## Configure
260
97
 
261
98
  Plugin options are a tuple — the package name, then an options object. Every
262
99
  option is optional:
@@ -281,11 +118,11 @@ option is optional:
281
118
  | --- | --- | --- |
282
119
  | `enabled` | `true` | The single off switch. `false` injects nothing at all and does no writes. |
283
120
  | `models` | — | An inline slot list. Highest-priority source. |
284
- | `modelsFile` | — | An explicit slot file; short-circuits the chain below. |
121
+ | `modelsFile` | — | An explicit slot file; short-circuits the [resolution chain](docs/CONFIGURATION.md#pool-preset-resolution). |
285
122
  | `reapMs` | `30000` | Hang-reaper interval. |
286
123
  | `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. |
287
124
  | `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. |
288
- | `omniroute` | — | Optional [OmniRoute](#configuring-omniroute) gateway integration. Absent or `{ enabled: false }` changes nothing. |
125
+ | `omniroute` | — | Optional [OmniRoute](docs/OMNIROUTE.md) gateway integration. Absent or `{ enabled: false }` changes nothing. |
289
126
 
290
127
  ### Staying up to date
291
128
 
@@ -299,335 +136,55 @@ binary — no npm or bun required. The running session keeps its loaded code; th
299
136
  new version takes effect on the next start, and the log says so. Run
300
137
  `lacode update` to do it on demand, or `lacode update --check` to just report.
301
138
 
302
- ### Pool preset resolution
303
-
304
- The slot list is the first of these that yields usable slots:
305
-
306
- 1. plugin options — `models` in the tuple above
307
- 2. `~/.config/lacode/pool.json` (its parent directory is created for you)
308
- 3. the bundled `presets/free-tier.json`
309
- 4. a built-in fallback, so routing always works
310
-
311
- A missing, unreadable or structurally broken source falls through to the next and
312
- logs one warning; nothing here throws into opencode. Nothing hot-reloads —
313
- change a preset, then restart every opencode process.
314
-
315
- **Any model list is accepted, including paid ones** (US-7). There is no free-tier
316
- allowlist anywhere in the package, and no cost check of any kind: only the
317
- structure is validated. The bundled preset is built from free models because
318
- that is a sensible default, not because the pool requires it.
319
-
320
- ### Configuring OmniRoute
321
-
322
- [OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local
323
- OpenAI-compatible AI gateway (default `http://localhost:20128/v1`) that routes a
324
- request across many providers with fallback and "combos". LaCode can register it
325
- as an opencode provider for you. It is **off by default**: if you do not set
326
- `omniroute.enabled: true`, nothing below happens and LaCode makes no request.
327
-
328
- **Setting it up on a client machine.** Four steps, in this order:
329
-
330
- 1. Install OmniRoute, then make it stay up: run `omniroute autostart enable`
331
- once. That writes a systemd user unit in a headless session, or an XDG
332
- autostart entry in a graphical one, and OmniRoute supervises itself from
333
- there on. Its gateway listens on `http://localhost:20128/v1`. The gateway's
334
- lifecycle is **the client's responsibility**: LaCode never starts, supervises
335
- or stops it, it only probes it. The gateway must therefore already be
336
- running **before** you start opencode — one that is down at startup is
337
- disabled for that run, and the integration is per-run.
338
- 2. Add the plugin block to your opencode config:
339
- `["@lawaty/lacode", { "omniroute": { "enabled": true, "apiKey": "<your omniroute key>", "combo": "auto", "models": [] } }]`
340
- `auto` is the zero-config default, but on a free tier it often resolves to a
341
- slow chain. [A tested fast combo](#a-tested-fast-combo-free-mix) below is
342
- ordered for latency and is worth applying on a new client.
343
- 3. Optionally, to route pooled work through it, make `omniroute/auto 3` the
344
- **first** line of `~/.config/lacode/pool.txt`. `pool.txt` is optional user
345
- config; the bundled default preset does **not** include OmniRoute.
346
- 4. **Restart opencode.** Nothing here hot-reloads.
347
-
348
- With `models: []` and `combo: "auto"`, the plugin registers exactly one model,
349
- `omniroute/auto`, which then appears in the model picker for any agent — no
350
- hand-maintained model list.
351
-
352
- The full option set, for reference:
353
-
354
- ```jsonc
355
- {
356
- "plugin": [
357
- ["@lawaty/lacode", {
358
- "omniroute": {
359
- "enabled": true,
360
- "baseURL": "http://localhost:20128/v1",
361
- "apiKey": "sk_omniroute", // optional; OMNIROUTE_API_KEY overrides it
362
- "combo": "static-best-free", // optional: let OmniRoute do the routing
363
- "models": ["cc/claude-sonnet-4-6", "gemini-3-flash"], // hard allowlist
364
- "tiers": {
365
- "primary": ["cc/claude-sonnet-4-6"],
366
- "backup": ["gemini-3-flash"]
367
- },
368
- "stickyPerSession": true,
369
- "healthCheck": true,
370
- "timeoutMs": 5000
371
- }
372
- }]
373
- ]
374
- }
375
- ```
376
-
377
- | Field | Default | Meaning |
378
- | --- | --- | --- |
379
- | `enabled` | `false` | Turns the integration on. |
380
- | `baseURL` | `http://localhost:20128/v1` | Gateway base URL, normalised to exactly one `/v1`. |
381
- | `apiKey` | — | Bearer key. **`OMNIROUTE_API_KEY` overrides this value.** The key is never logged. |
382
- | `combo` | — | An OmniRoute combo id (or `auto`) to expose as a selectable model even though the gateway does not list it. Exposed first. |
383
- | `models` | — | A **hard allowlist**: only these ids are ever exposed, even if the gateway lists more. |
384
- | `tiers` | — | `{ "primary": [...], "backup": [...] }` preference order over models the allowlist already permits. |
385
- | `stickyPerSession` | `true` | Keep a session on one model while that model is healthy. |
386
- | `healthCheck` | `true` | Probe `GET /models` at startup; a failed check disables the integration for that run only. |
387
- | `timeoutMs` | `5000` | Budget for LaCode's own probe and model-cache calls to the gateway. It is not written into the provider options, so it does not bound real traffic — opencode's own default applies there. |
388
-
389
- An enabled but invalid block (bad `baseURL`, a non-array `models`, …) is rejected
390
- as a whole: LaCode logs one warning naming the problem and continues exactly as
391
- if the integration were off. It never crashes opencode.
392
-
393
- **What it registers.** On the first `config` pass LaCode health-checks the
394
- gateway, reads `GET /models`, filters it through your `models` allowlist (or
395
- exposes everything the gateway lists when no allowlist is set), and writes a
396
- `provider.omniroute` entry — `npm: "@ai-sdk/openai-compatible"` with your
397
- `baseURL` / `apiKey` — so `omniroute/<model-id>` becomes selectable. If you
398
- already have a `provider.omniroute` block, LaCode leaves it completely untouched
399
- and warns; it also never adds a `whitelist`. Because opencode loads
400
- `@ai-sdk/openai-compatible` itself, no extra dependency is installed.
401
-
402
- **Combo vs. allowlist.** Set `combo` and OmniRoute does the fallback for you
403
- (let it route): the combo leads the exposed models, so opencode can hand a failed
404
- request to the gateway's own fallback chain. Without a combo, LaCode exposes the
405
- allowlist ordered by `tiers` (primary first). Candidate order is `filterModels`
406
- (combo first, then the hard allowlist) followed by `orderByTier`, and that is
407
- all — LaCode picks which model is exposed and in what order; it does not measure
408
- latency or error rate at runtime, and it does not skip a model because it
409
- failed.
410
-
411
- **Using OmniRoute in the pool.** The integration only *registers* the provider —
412
- it never touches your pool, and it does not place a slot for you. To route pooled
413
- work (exploration, implementation, cartography) through the gateway, list it in
414
- your pool like any other model:
415
-
416
- ```
417
- # ~/.config/lacode/pool.txt
418
- omniroute/auto 3
419
- opencode/space-bunny-free 2
420
- opencode-go/step-5-preview-free 1
421
- opencode/big-pickle 1
422
- ```
423
-
424
- `omniroute/auto` then takes slot 1 (weight 3) and drains pooled mechanical work
425
- before it reaches the bundled opencode/opencode-go ("zen") models. Any
426
- `omniroute/<id>` works, provided the id is exposed — by `combo`/`models`, or the
427
- whole catalog when neither is set. Because the plugin writes the pool nothing,
428
- the only thing that changes when the gateway is down is that `omniroute/<id>`
429
- fails; every other slot keeps working. See
430
- [docs/USER-STORIES.md](docs/USER-STORIES.md) US-28.
431
-
432
- ### A tested fast combo: `free-mix`
139
+ ### Which LaCode is running
433
140
 
434
- `combo: "auto"` is the zero-configuration default, but `auto` inherits whatever
435
- the gateway chooses, and on a busy free tier that is frequently the slowest
436
- chain available. The combo below was **measured** against a live free-tier
437
- gateway and is ordered for latency rather than for breadth. Apply it during the
438
- "Setting it up on a client machine" steps above and pooled work is usable
439
- straight away, instead of starting from a gateway that queues for half a minute
440
- per call.
441
-
442
- ```jsonc
443
- {
444
- "name": "free-mix",
445
- // Order decides. Do NOT use "weighted" here — weights let the gateway
446
- // re-shuffle a model that is returning 429 into first position again.
447
- "strategy": "priority",
448
- "models": [
449
- { "model": "gemini/gemini-3.1-flash-lite", "providerId": "gemini" },
450
- { "model": "gemini/gemini-3.8-flash", "providerId": "gemini" },
451
- { "model": "gemini/gemini-3.7-flash", "providerId": "gemini" },
452
- { "model": "gemini/gemini-3-flash-preview", "providerId": "gemini" },
453
- { "model": "openrouter/nvidia/nemotron-3-super-120b-a12b:free", "providerId": "openrouter" },
454
- { "model": "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", "providerId": "openrouter" },
455
- { "model": "openrouter/nvidia/nemotron-3.5-lightning:free", "providerId": "openrouter" },
456
- { "model": "openrouter/cohere/north-mini-code:free", "providerId": "openrouter" },
457
- { "model": "openrouter/poolside/laguna-s-2.1:free", "providerId": "openrouter" },
458
- { "model": "openrouter/dots-studio/dots-3-note-preview:free", "providerId": "openrouter" }
459
- ]
460
- }
461
- ```
462
-
463
- Why this order, from 859 calls over a single day against that gateway:
464
-
465
- | Model | Observed behaviour | Position |
466
- | --- | --- | --- |
467
- | `gemini/gemini-3.1-flash-lite` | **165/172 succeeded (96%)**, ~4.5s median | **first** |
468
- | `gemini-3.8-flash` / `3.7-flash` / `3-flash-preview` | succeed intermittently, but **burn exactly 30.0s** on a `503` queue-budget timeout when they do not | next |
469
- | `openrouter/*:free` | **~90% `429`**, but fails *fast* (~1.8s) and occasionally succeeds | last, as a cheap final fallback |
470
- | `gemini-2.5-flash`, `gemini-2.5-flash-lite` | **`404` on every attempt** — dead ids | **omitted entirely** |
471
-
472
- The ordering matters because a combo pays for *every* step it walks past. Put the
473
- 96%-success model first and the common case is a single hop; the 30s models sit
474
- behind it where they cost nothing, and the 429s sit last where they give up in
475
- under two seconds.
476
-
477
- Measured on the same gateway, same day, `max_tokens: 10`:
478
-
479
- | | Before | After |
480
- | --- | --- | --- |
481
- | `free-mix` response | 42.7s / 41.8s / 34.7s | 4.1s / 3.1s / 3.2s / 4.0s / 3.2s |
482
- | Median | **~41.8s** | **~3.2s** |
483
- | Provider time burned on calls >25s | **56.9 of 102.4 min (56%)** | — |
484
-
485
- Before the change, the real call sequence was a `429` after 2.7s, then a `503`
486
- after 30.0s, then the actual work in 5.7s — roughly **33 seconds of dead time in
487
- front of every successful request**, and a p90 of 100s when three 30s steps
488
- chained.
489
-
490
- These free-tier endpoints change availability constantly, so treat the ordering
491
- as a **starting point to re-measure, not a permanent truth**. Re-check it with
492
- OmniRoute's own call logs before assuming it still holds:
141
+ `lacode version` answers it — and the answer is often not the version number you
142
+ expected, because the plugin entry in your config decides which copy is loaded:
493
143
 
494
144
  ```bash
495
- # one timed request through the gateway
496
- curl -o /dev/null -w '%{http_code} %{time_starttransfer}s\n' \
497
- -X POST http://localhost:20128/v1/chat/completions \
498
- -H "Authorization: Bearer $OMNIROUTE_API_KEY" -H 'Content-Type: application/json' \
499
- -d '{"model":"free-mix","messages":[{"role":"user","content":"Say OK"}],"max_tokens":10}'
500
-
501
- # per-model status + duration, straight from OmniRoute's logs
502
- python3 - <<'PY'
503
- import json, glob, collections
504
- d = "/home/<user>/.omniroute/call_logs/<YYYY-MM-DD>"
505
- g = collections.defaultdict(collections.Counter)
506
- for f in glob.glob(d + "/*.json"):
507
- try: s = json.load(open(f))["summary"]
508
- except Exception: continue
509
- g[s.get("comboStepId")][s.get("status")] += 1
510
- for k, v in sorted(g.items(), key=lambda x: str(x[0])):
511
- print(f"{str(k)[:44]:44} {dict(v)}")
512
- PY
145
+ lacode version
513
146
  ```
514
147
 
515
- Apply it with the bundled CLI (`PUT /api/combos/<id>` updates in place, so the
516
- combo is never briefly missing):
517
-
518
- ```bash
519
- omniroute api combos get-api-combos-id- --id <combo-id> --json > free-mix.json
520
- # edit name/strategy/models in free-mix.json, then:
521
- omniroute api combos put-api-combos-id- --id <combo-id> --body @free-mix.json
522
148
  ```
523
-
524
- **A note on mid-session switching.** opencode 1.18.34 exposes no hook to change
525
- an agent's main model mid-request, so LaCode cannot swap models inside a running
526
- session; use an OmniRoute `combo` (or `auto`) if you want per-request fallback.
527
- LaCode's selection engine decides which models are exposed and in what order, and
528
- its cooldown/backoff rules are unit-tested.
529
-
530
- **Getting a key.** Open the OmniRoute dashboard, go to API Manager / API Keys and
531
- copy a key into `OMNIROUTE_API_KEY` (recommended) or `omniroute.apiKey`. On a
532
- local instance with `REQUIRE_API_KEY=false`, the literal placeholder
533
- `sk_omniroute` is accepted.
534
-
535
- **Troubleshooting**
536
-
537
- | Symptom | Cause | Fix |
538
- | --- | --- | --- |
539
- | `omniroute health check failed` at startup | Gateway not running, wrong `baseURL`, or a startup race | Start OmniRoute on `http://localhost:20128` and restart opencode; `omniroute autostart enable` makes that durable across restarts. The integration is disabled for that run only, not permanently. |
540
- | `omniroute health check failed` at startup, then a repeating `cooling omniroute/<id> (hang)` and `Model not found` churn on one slot | OmniRoute was started *after* opencode, and `pool.txt` names an `omniroute/<id>` slot | The integration is off for that run, but the pool still routes to the slot: the request hangs, the reaper cools it for 600s, and the cycle repeats. Start OmniRoute, then restart opencode. |
541
- | `ProviderModelNotFoundError: Model not found` on an id the gateway serves | The id is a valid OmniRoute route but is not in the plugin's `models` allowlist, so the plugin never exposed it | Add the id to `models`, or use `combo: "auto"` with `omniroute/auto`. A valid gateway route is not automatically an exposed model. |
542
- | `no usable models` | The gateway returned an empty list, or every id was filtered out by `models` | `curl -H "Authorization: Bearer $OMNIROUTE_API_KEY" http://localhost:20128/v1/models` and check `models`/`combo` against the returned ids. |
543
- | `401` / `Invalid API key` | `REQUIRE_API_KEY=true` and the key is missing or wrong | Create a key in the dashboard, export `OMNIROUTE_API_KEY`, or set `REQUIRE_API_KEY=false` locally and use `sk_omniroute`. |
544
- | `404` on a URL containing `/v1/v1/` | A `baseURL` that already carried `/v1` was given twice | LaCode normalises this, so it should not come from LaCode; check a hand-written `provider.omniroute` block, which LaCode leaves untouched. |
545
- | Provider registered but models are missing | You already had `provider.omniroute` | Remove it and let LaCode write it, or add the models yourself — LaCode never modifies an existing provider. |
546
- | Pooled work still runs on the bundled models | The gateway was unhealthy/disabled at startup, or you have no `omniroute/…` slot in your pool | Check the startup log for `omniroute health check failed` / `no usable models`, then add `omniroute/<model>` to `pool.txt` (or `pool.json`) to route pooled work through the gateway. |
547
-
548
- ### Which models run which agent
549
-
550
- Only the **pool** writes a model onto a pooled agent, from whichever source wins
551
- above. The pooled bases (`explore`, `implement`, `context`)
552
- and their hidden `-1`..`-4` variants therefore carry no model of their own: a
553
- model written next to an agent would be a second source of truth that silently
554
- wins whenever routing falls through. Spawn a base and let the pool route it.
555
-
556
- ### ⚠️ Per-agent tool grants must be in your own config
557
-
558
- **LaCode cannot grant tools to its agents, and this is not an oversight you can
559
- work around by configuring it.** If you keep a global `"tools"` block in your
560
- config — for example `"tools": { "playwright_*": false }` — then the pooled
561
- exploration and implementation bases (`explore` and `implement`, plus their
562
- hidden `-1`..`-4` pool variants) lose browser access, and **nothing tells them why**: the tool call is simply
563
- refused mid-task. `context` gets no grant either, which is correct — it
564
- writes the context map and has no business in a browser.
565
-
566
- opencode folds an agent's `tools` into its permission list **at config-load
567
- time**, while decoding the config — before any plugin runs. A plugin's `config`
568
- hook mutates the already-decoded config, so the fold never re-runs and anything
569
- it puts in `agent.tools` is inert. (Verified on opencode 1.18.34/1.18.35 with
570
- `opencode debug agent explore`: a grant written in your own config shows two
571
- `playwright_*` entries — `deny` then `allow`, last one winning — while the same
572
- grant injected by the plugin shows only the `deny`.) LaCode's other injected
573
- fields are not affected; `permission`, `prompt`, `model`, `description`,
574
- `temperature`, `mode` and `hidden` are all read after the plugin hook runs.
575
-
576
- **So if you use a global `"tools"` block, declare the grants yourself:**
577
-
578
- ```jsonc
579
- {
580
- "tools": { "playwright_*": false },
581
- "agent": {
582
- "explore": { "tools": { "playwright_*": true } },
583
- "implement": { "tools": { "playwright_*": true } }
584
- }
585
- }
149
+ version 0.4.1
150
+ root /home/you/lacode
151
+ source checkout
152
+ commit 9446c88 (dirty)
153
+ dist current
154
+ config /home/you/.config/opencode/opencode.jsonc
155
+ plugin file:///home/you/lacode/dist/index.js
156
+ this copy yes — opencode loads this copy
586
157
  ```
587
158
 
588
- The hidden pool variants (`explore-1`, `implement-2`, …) inherit
589
- nothing here — add a line per variant you actually spawn, or drop the global
590
- deny and scope it to the agents that should not have browser access instead.
591
-
592
- Nothing hot-reloads: after editing your config, restart every running opencode
593
- process.
594
-
595
- ### What gets injected
596
-
597
- **17 agent keys**: the 5 bases `build`, `plan`, `explore`, `implement`,
598
- `context`, plus one hidden
599
- `<base>-1`..`-4` pool variant for each of the 3 pooled bases.
600
-
601
- `build` and `plan` exist in opencode as built-ins, so on a stock config those
602
- two keys are already taken and merge-without-clobber keeps the built-in — the
603
- LaCode definitions of `build` and `plan` are what you get if you remove the
604
- built-ins of those names first.
605
-
606
- **Five keys are deliberately not injected.** `basic` is a byte-for-byte
607
- duplicate of `plan` apart from a pinned model, and `plan` is injected instead.
608
- `expert` pins a specific model and carries a personal benchmark note in its
609
- description — a package cannot ship someone else's provider, quota or benchmark
610
- results — though its escalation ladder still resolves, because the two deep
611
- tiers it escalates to *are* injected. And `explore`, `general` and `implement`
612
- are only `{ "disable": true }` stubs in the author's own config: turning off
613
- opencode's built-in agents is a decision about one person's configuration, not
614
- a default a package should impose on a stranger.
615
-
616
- One **field** is deliberately not injected either: `tools`, for the
617
- load-time-folding reason explained above. Nothing else the plugin writes into
618
- an agent is subject to it.
619
-
620
- **Installation merges, it does not clobber** (US-16). An agent, command or
621
- instruction is added only when that key is absent from your config. A
622
- user-defined agent of the same name is left byte-identical — not patched, not
623
- partially merged. Skips are counted and logged once.
624
-
625
- **`enabled: false` turns all of it off** (US-17). No pool model rewriting, no
626
- agent injection, no command injection, no reaper timer armed, no
627
- `~/.config/lacode` directory created.
628
-
629
- These three are specified in [docs/USER-STORIES.md](docs/USER-STORIES.md) as
630
- US-7, US-16 and US-17, and each has tests.
159
+ `npm ls -g @lawaty/lacode` answers a different question. It reports the package
160
+ opencode resolves *by name*, which is not necessarily the one running: a config
161
+ may load the plugin by absolute path, in which case a stale global install can
162
+ sit there reporting a version nothing uses. So this command reads your actual
163
+ `plugin` array and says whether it resolves to the copy speaking.
164
+
165
+ `source` distinguishes a git checkout from an npm install, `commit` and the
166
+ dirty flag name what the code was built from, and `dist` is the trap worth
167
+ knowing about — opencode loads `dist/index.js` while you edit and pull `src/`,
168
+ so a `git pull` can leave you running yesterday's build with nothing failing.
169
+ When it reports `stale`, rebuild with `npm run build`. No network is touched;
170
+ `lacode update --check` is what asks npm whether something newer exists.
171
+
172
+ ## What you get
173
+
174
+ Three agents, pooled and rotated: `explore`, `implement` and `context`. You spawn
175
+ the base name and the pool picks a cheap model slot for it. Behind each base sits
176
+ up to four hidden variants — one per slot — and a spawn of the bare name is the
177
+ only thing that works: naming a variant lets the caller choose the provider
178
+ directly, which is exactly what the rotation and the per-slot ceiling exist to
179
+ prevent, so it is refused.
180
+
181
+ Four commands: `/context-init`, `/context-update`, `/context-review` and
182
+ `/lacode-doctor`. Everything is added **only when your config does not already
183
+ define it** — an agent or command you wrote yourself is left byte-identical,
184
+ never patched or partially merged. `{"enabled": false}` turns the whole thing off
185
+ without leaving a trace. The exact injection list, and the three keys that are
186
+ deliberately *not* injected, are in
187
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md#what-gets-injected).
631
188
 
632
189
  ## Context map
633
190
 
@@ -667,25 +224,32 @@ in [docs/CONTEXT.md](docs/CONTEXT.md).
667
224
 
668
225
  ## Managerial directions
669
226
 
670
- You can optionally add `manager.md` to `.opencode/context/`. This file is owned
671
- by you alone and is never written, edited, pruned, or promoted by any agent. It
672
- outranks the five generated files on focus and settled decisions, but it is
673
- secondary to the source code: if the code disagrees, the code wins and the
674
- conflict is reported back to you.
675
-
676
- The file is per-clone, not committed, and entirely optional: if it is missing,
677
- agents behave exactly as before. When present, it should contain these three
678
- sections:
679
-
680
- - `## Stage`: A snapshot of your current work-in-progress. Rewrite this section
681
- rather than appending to it to keep it concise. If this file passes roughly 150
682
- lines, run `/context-review`.
683
- - `## Confirmed`: Facts and decisions you have personally ratified. Treat these
684
- as settled, but not as permission to skip reading the code.
685
- - `## Instructions`: Standing managerial directions for all agents.
686
-
687
- No agent may write this file at all: the context agent's edit permission denies
688
- `manager.md` explicitly, so ratification can only ever come from your hand.
227
+ The plugin creates `manager.md` in `.opencode/context/` for you, and never
228
+ overwrites it once it exists. It is your file: it records what you want done
229
+ and how you want it approached — what is being worked on, what comes next,
230
+ what you have settled, and what every agent should keep in mind. It says
231
+ nothing about where code lives, so it can never contradict the code, and
232
+ within that domain it outranks the five generated files and any agent's
233
+ inference about what matters right now.
234
+
235
+ The file is per-clone and not committed, so it is yours rather than shared
236
+ team state. It has no fixed schema — the headings below are a convenience.
237
+ Add your own; every heading in it is treated as direction.
238
+
239
+ - `## Current`: what is being worked on right now. Rewrite it rather than
240
+ appending, so it does not become a running history. If the file passes
241
+ roughly 150 lines, run `/context-review`.
242
+ - `## Next`: what comes next, in order.
243
+ - `## Confirmed`: what you have personally ratified. Treat these as settled
244
+ decisions, not as permission to skip reading the code.
245
+ - `## Instructions`: standing direction beyond the current task — priorities,
246
+ things to care about, constraints that are not obvious from the code.
247
+
248
+ **Who may write it.** The context agent is explicitly permission-denied from
249
+ touching this path, so the automatic post-session run can never alter it. An
250
+ agent working in your live session may write it only when you explicitly ask
251
+ it to, and only what you asked for — never on its own initiative, and never
252
+ to "keep it up to date".
689
253
 
690
254
  ## Cost
691
255
 
@@ -761,12 +325,22 @@ status.
761
325
  Configuration, the payload format, the receiver contract and holding the forward
762
326
  open are in [docs/TUNNEL.md](docs/TUNNEL.md).
763
327
 
764
- ## User stories
328
+ ## Further reading
765
329
 
766
- Twenty-nine stories across seven epics, each written as a behavioural contract
767
- with acceptance criteria a test can assert mechanically — the pains behind them,
768
- the Given/When/Then criteria, and the story-to-test mapping are in
769
- [docs/USER-STORIES.md](docs/USER-STORIES.md).
330
+ Optional. Nothing above needs any of it — this is where the detail lives once you
331
+ want it.
332
+
333
+ | | |
334
+ | --- | --- |
335
+ | [Install and troubleshooting](docs/INSTALL.md) | Where opencode puts the package, what to do when the agents are missing, the version matrix, macOS differences, and the `lacode` CLI. |
336
+ | [Configuration reference](docs/CONFIGURATION.md) | Every plugin option, how the pool resolves its slots, per-agent tool grants, and exactly what gets written into your config. |
337
+ | [OmniRoute gateway](docs/OMNIROUTE.md) | The optional local gateway integration: setup, full option set, a latency-measured free combo, and its troubleshooting table. |
338
+ | [Context map](docs/CONTEXT.md) | The mechanism behind `.opencode/context/` — the one-writer permission rule, the five files, and what the log lines mean. |
339
+ | [Remote hosts and notifications](docs/TUNNEL.md) | The SSH notification hop, payload format, receiver contract, and holding the forward open. |
340
+ | [Benchmarks](docs/BENCHMARKS.md) | How the cost harness works, and why it deliberately publishes no savings number. |
341
+ | [User stories](docs/USER-STORIES.md) | The behavioural contracts behind all of the above, written so a test can assert them. |
342
+ | [Operations runbook](OPERATIONS.md) | Maintainer-facing: what runs where, what needs a restart, how the pieces are distributed across machines. |
343
+ | [Changelog](CHANGELOG.md) | What changed in each release. |
770
344
 
771
345
  ## Development
772
346
 
@@ -801,4 +375,4 @@ exist.
801
375
 
802
376
  ## License
803
377
 
804
- MIT licensed. Based on work by [lawaty](https://github.com/lawaty/lacode).
378
+ MIT licensed. Based on work by [lawaty](https://github.com/lawaty/lacode).