@lawaty/lacode 0.4.1 → 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/CHANGELOG.md +15 -0
- package/README.md +119 -551
- package/dist/agents.js +48 -3
- package/dist/commands.js +49 -15
- package/dist/index.js +226 -57
- package/dist/lacode.js +253 -11
- package/dist/pool.js +15 -4
- package/docs/CONFIGURATION.md +111 -0
- package/docs/INSTALL.md +209 -0
- package/docs/OMNIROUTE.md +232 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,9 @@ is also on disk at `node_modules/@lawaty/lacode/CHANGELOG.md`.
|
|
|
27
27
|
| SSH notification tunnel | Work on a remote host raises its notification on that host, which has no desktop. |
|
|
28
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. |
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Getting started
|
|
31
|
+
|
|
32
|
+
### 1. Install
|
|
31
33
|
|
|
32
34
|
Add one line to your opencode config — `~/.config/opencode/opencode.json`, or
|
|
33
35
|
the `.jsonc` beside it:
|
|
@@ -38,228 +40,60 @@ the `.jsonc` beside it:
|
|
|
38
40
|
|
|
39
41
|
**That is the entire install.** opencode resolves an npm plugin specifier itself
|
|
40
42
|
and installs it at startup, so there is no `npm install` to run first and no
|
|
41
|
-
directory to create. Do not run `npm i -g @lawaty/lacode`: a global npm install
|
|
42
|
-
|
|
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
|
|
43
45
|
loads.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
[procedure below](#if-the-agents-are-missing) says when that is, and
|
|
48
|
-
[which versions this is known on](#which-opencode-versions-this-is-known-on)
|
|
49
|
-
says what is actually verified.
|
|
50
|
-
|
|
51
|
-
To install it explicitly instead — or to install it once for every project on
|
|
52
|
-
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:
|
|
53
49
|
|
|
54
50
|
```bash
|
|
55
|
-
opencode plugin @lawaty/lacode
|
|
56
|
-
opencode plugin @lawaty/lacode -g # global, for every project on the machine
|
|
51
|
+
opencode plugin @lawaty/lacode -g
|
|
57
52
|
```
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
read at startup, so without a restart the change does not take effect and the
|
|
61
|
-
agent set will look stale for no visible reason.
|
|
54
|
+
### 2. Restart opencode
|
|
62
55
|
|
|
63
|
-
|
|
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.
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
cache — not into your project's `node_modules`, and not from anywhere a global
|
|
67
|
-
npm install put them. Ask opencode where that cache is instead of assuming,
|
|
68
|
-
because where it lives differs by platform:
|
|
60
|
+
### 3. Check it loaded
|
|
69
61
|
|
|
70
62
|
```bash
|
|
71
|
-
opencode
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
It prints the directories opencode is using on this machine — config, data, log
|
|
75
|
-
and the rest. Find the **cache** line and append
|
|
76
|
-
`/packages/<specifier>/node_modules/` to the path it reports. On Linux with no
|
|
77
|
-
`XDG_CACHE_HOME` set, that comes out as, for example:
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
~/.cache/opencode/packages/@lawaty/lacode@latest/node_modules/
|
|
63
|
+
opencode agent list | grep -E '^(explore|implement|context)'
|
|
81
64
|
```
|
|
82
65
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
something this document should guess at — read it from `opencode debug paths`
|
|
86
|
-
and use what it prints.
|
|
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.
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
whole reason for reading it.
|
|
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.
|
|
95
75
|
|
|
96
|
-
###
|
|
76
|
+
### 4. Use it
|
|
97
77
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
below is written to hold whichever opencode version you are running — none of
|
|
101
|
-
them depends on knowing the answer in advance.
|
|
78
|
+
Nothing to turn on. Ask for something that delegates, and the pool takes it from
|
|
79
|
+
there:
|
|
102
80
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{ "plugin": ["@lawaty/lacode"] }
|
|
81
|
+
```
|
|
82
|
+
> use the explore agent to find where auth tokens are validated
|
|
107
83
|
```
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
**2 — Check whether the plugin loaded.**
|
|
85
|
+
To see what the pool is doing underneath:
|
|
112
86
|
|
|
113
87
|
```bash
|
|
114
|
-
|
|
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
|
|
115
90
|
```
|
|
116
91
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
did not load. Run it in the same shell environment opencode runs in — an
|
|
121
|
-
`agent list` under a different `HOME` or `XDG_CONFIG_HOME` tests a different
|
|
122
|
-
machine. **If the agents are listed, stop here: the one line above is the whole
|
|
123
|
-
install**, and nothing further in this section applies to you.
|
|
124
|
-
|
|
125
|
-
**3 — Decide which failure you are looking at.** Inspect the package directory
|
|
126
|
-
under the cache root from
|
|
127
|
-
[Where it is installed](#where-it-is-installed), and also check that `plugin`
|
|
128
|
-
really is in the config file opencode reads.
|
|
129
|
-
|
|
130
|
-
- **Empty** (no `node_modules` inside): the install failed — check the package
|
|
131
|
-
name, and that it is published. There is nothing to load yet.
|
|
132
|
-
- **Populated**: the install worked and the loader dropped the plugin. Go to
|
|
133
|
-
step 4. These two look identical from the outside, which is why this step
|
|
134
|
-
exists.
|
|
135
|
-
|
|
136
|
-
**4 — If the install worked and the agents are still missing, point the loader
|
|
137
|
-
at the built file.** Add a second entry; leave the bare name in place:
|
|
138
|
-
|
|
139
|
-
```json
|
|
140
|
-
{
|
|
141
|
-
"plugin": [
|
|
142
|
-
"@lawaty/lacode",
|
|
143
|
-
"file:///ABSOLUTE-PATH-FROM-debug-paths/packages/@lawaty/lacode@latest/node_modules/@lawaty/lacode/dist/index.js"
|
|
144
|
-
]
|
|
145
|
-
}
|
|
146
|
-
```
|
|
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.
|
|
147
95
|
|
|
148
|
-
|
|
149
|
-
directory: take the cache path `opencode debug paths` reports for **your**
|
|
150
|
-
machine and put it in front of `/packages/…`. It must be an absolute path with
|
|
151
|
-
`$HOME` already expanded and no `~` left in it. Re-derive it after a cache wipe
|
|
152
|
-
or a reinstall — the directory is keyed by specifier, so it moves when the
|
|
153
|
-
version does.
|
|
154
|
-
|
|
155
|
-
Why keep both entries instead of swapping the bare name out:
|
|
156
|
-
|
|
157
|
-
- The bare name is what makes opencode install the package into its cache, and
|
|
158
|
-
therefore what creates the path the `file://` entry points at.
|
|
159
|
-
- The `file://` entry is what actually loads the plugin.
|
|
160
|
-
- They **cannot double-inject** on opencode 1.18.35, where the bare name
|
|
161
|
-
contributes nothing at all: the pair loads the plugin exactly once. On any
|
|
162
|
-
other version this is untested, which is precisely why step 4 is something you
|
|
163
|
-
take *because* step 2 showed the agents missing, and never on the expectation
|
|
164
|
-
that it would help.
|
|
165
|
-
|
|
166
|
-
**5 — Restart, and expect two restarts after a cache wipe.** The first start
|
|
167
|
-
installs the package and creates the path the `file://` entry points at, but a
|
|
168
|
-
path that does not exist at startup is a path that cannot be loaded in that same
|
|
169
|
-
run. The second start finds it and loads the plugin. After that it is steady
|
|
170
|
-
state.
|
|
171
|
-
|
|
172
|
-
This package also publishes `main` and an `exports["./server"]` entry pointing at
|
|
173
|
-
the same built file, because those are two of the shapes a plugin loader
|
|
174
|
-
resolving an entry point consults. They are forward-compatibility metadata and
|
|
175
|
-
they cost nothing — but they do **not** fix this on 1.18.35: adding either one to
|
|
176
|
-
the *installed* cache copy was tested there and the bare specifier still loaded
|
|
177
|
-
nothing. No other version has been observed where they help either. Use the
|
|
178
|
-
`file://` entry.
|
|
179
|
-
|
|
180
|
-
### Which opencode versions this is known on
|
|
181
|
-
|
|
182
|
-
Exactly one version has been measured, and the procedure above is deliberately
|
|
183
|
-
not built around that fact. Treat this table as the evidence it is, which is one
|
|
184
|
-
row wide.
|
|
185
|
-
|
|
186
|
-
| opencode version | bare spec loads? | `file://` needed? | evidence |
|
|
187
|
-
| --- | --- | --- | --- |
|
|
188
|
-
| 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. |
|
|
189
|
-
| Any other version | **Unknown** | **Unknown** | **Untested.** No other opencode version has been run against this package in either direction. |
|
|
190
|
-
|
|
191
|
-
Every command quoted on this page was run against 1.18.35. **If your version is
|
|
192
|
-
not the one measured here, follow the procedure above rather than reading this
|
|
193
|
-
table as coverage of your build**: each step stands on its own, and step 2 tells
|
|
194
|
-
you which situation you are in before any version-specific advice is relevant.
|
|
195
|
-
|
|
196
|
-
### macOS
|
|
197
|
-
|
|
198
|
-
The plugin runs on macOS. It is plain JavaScript with no native dependency, and
|
|
199
|
-
what the package publishes is the built JavaScript rather than TypeScript
|
|
200
|
-
sources, so nothing about loading it is Linux-only. Use the procedure above
|
|
201
|
-
unchanged.
|
|
202
|
-
|
|
203
|
-
Three things differ, and all three are worth reading rather than assuming:
|
|
204
|
-
|
|
205
|
-
- **Paths.** Do not copy a cache path out of this document — the example under
|
|
206
|
-
[Where it is installed](#where-it-is-installed) is one platform's layout. Run
|
|
207
|
-
`opencode debug paths` and build the `file://` entry from what it reports.
|
|
208
|
-
This page deliberately does not name a macOS cache root: where opencode keeps
|
|
209
|
-
it on a Mac has not been established here, and a plausible-looking guess is
|
|
210
|
-
worse than an instruction to look.
|
|
211
|
-
- **Notifications** are delivered through `osascript`, or through
|
|
212
|
-
`terminal-notifier` when `osascript` is not installed, rather than through the
|
|
213
|
-
Linux `notify-send` (US-26). `lacode doctor` reports which channel your machine
|
|
214
|
-
would actually use.
|
|
215
|
-
- **The repository's shell scripts are Linux-only by design**, and are not in the
|
|
216
|
-
published package. They drive one person's SSH topology, so each is on the side
|
|
217
|
-
it belongs to:
|
|
218
|
-
|
|
219
|
-
- **Remote hosts are plain npm clients.** A remote host installs the plugin the
|
|
220
|
-
same way (`opencode plugin @lawaty/lacode -g`); there is nothing to rsync. The
|
|
221
|
-
notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
|
|
222
|
-
once with `lacode tunnel setup` — no reverse forward, no listening port, no
|
|
223
|
-
systemd unit. See [Remote hosts](#remote-hosts) and docs/TUNNEL.md.
|
|
224
|
-
- The repo still carries the author's personal, Linux-only tooling from the older
|
|
225
|
-
reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
|
|
226
|
-
`bin/oc-sync`). None of it is in the published package, and none of it is needed
|
|
227
|
-
for the npm-client path above.
|
|
228
|
-
|
|
229
|
-
**The test suite runs on macOS.** It is written to pass on a Mac as well as on
|
|
230
|
-
Linux, so "it works on a Mac" is a supported claim rather than an intention. The
|
|
231
|
-
few checks that exercise the Linux-only shell scripts above skip themselves on a
|
|
232
|
-
machine that cannot run them, naming the reason — a bash 3.2 with no `mapfile`,
|
|
233
|
-
or no GNU `timeout` — rather than passing silently or failing confusingly.
|
|
234
|
-
|
|
235
|
-
### The `lacode` CLI
|
|
236
|
-
|
|
237
|
-
The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
|
|
238
|
-
[Remote hosts](#remote-hosts). The plugin install does **not** put it on your
|
|
239
|
-
`PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
|
|
240
|
-
globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
|
|
241
|
-
global install is for the command only — it is still never what makes the plugin
|
|
242
|
-
load.)
|
|
243
|
-
|
|
244
|
-
### What the install gives you
|
|
245
|
-
|
|
246
|
-
Two things are worth knowing about what npm actually hands you:
|
|
247
|
-
|
|
248
|
-
- **`@opencode-ai/plugin` is a peer dependency.** It is not vendored into this
|
|
249
|
-
package and never will be: a second copy of the SDK inside the tarball would
|
|
250
|
-
mean opencode and the plugin holding two different module instances, and
|
|
251
|
-
instance identity is what plugin state hangs on. npm installs the peer for you;
|
|
252
|
-
if you install with a tool that does not, install `@opencode-ai/plugin` at the
|
|
253
|
-
same version yourself.
|
|
254
|
-
- **You get compiled JavaScript, not TypeScript sources.** The tarball ships
|
|
255
|
-
`dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
|
|
256
|
-
`dist/` cannot be published). This is not a preference: node strips types
|
|
257
|
-
natively from 22.18 but **refuses to for files under `node_modules`**, so a
|
|
258
|
-
package that shipped `.ts` worked fine in a checkout and failed on install —
|
|
259
|
-
`lacode --help` died with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A
|
|
260
|
-
published package cannot depend on its consumer's loader being lenient.
|
|
261
|
-
|
|
262
|
-
## Configuration
|
|
96
|
+
## Configure
|
|
263
97
|
|
|
264
98
|
Plugin options are a tuple — the package name, then an options object. Every
|
|
265
99
|
option is optional:
|
|
@@ -284,11 +118,11 @@ option is optional:
|
|
|
284
118
|
| --- | --- | --- |
|
|
285
119
|
| `enabled` | `true` | The single off switch. `false` injects nothing at all and does no writes. |
|
|
286
120
|
| `models` | — | An inline slot list. Highest-priority source. |
|
|
287
|
-
| `modelsFile` | — | An explicit slot file; short-circuits the chain
|
|
121
|
+
| `modelsFile` | — | An explicit slot file; short-circuits the [resolution chain](docs/CONFIGURATION.md#pool-preset-resolution). |
|
|
288
122
|
| `reapMs` | `30000` | Hang-reaper interval. |
|
|
289
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. |
|
|
290
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. |
|
|
291
|
-
| `omniroute` | — | Optional [OmniRoute](
|
|
125
|
+
| `omniroute` | — | Optional [OmniRoute](docs/OMNIROUTE.md) gateway integration. Absent or `{ enabled: false }` changes nothing. |
|
|
292
126
|
|
|
293
127
|
### Staying up to date
|
|
294
128
|
|
|
@@ -302,338 +136,55 @@ binary — no npm or bun required. The running session keeps its loaded code; th
|
|
|
302
136
|
new version takes effect on the next start, and the log says so. Run
|
|
303
137
|
`lacode update` to do it on demand, or `lacode update --check` to just report.
|
|
304
138
|
|
|
305
|
-
###
|
|
306
|
-
|
|
307
|
-
The slot list is the first of these that yields usable slots:
|
|
308
|
-
|
|
309
|
-
1. plugin options — `models` in the tuple above
|
|
310
|
-
2. `~/.config/lacode/pool.json` (its parent directory is created for you)
|
|
311
|
-
3. the bundled `presets/free-tier.json`
|
|
312
|
-
4. a built-in fallback, so routing always works
|
|
313
|
-
|
|
314
|
-
A missing, unreadable or structurally broken source falls through to the next and
|
|
315
|
-
logs one warning; nothing here throws into opencode. Nothing hot-reloads —
|
|
316
|
-
change a preset, then restart every opencode process.
|
|
317
|
-
|
|
318
|
-
**Any model list is accepted, including paid ones** (US-7). There is no free-tier
|
|
319
|
-
allowlist anywhere in the package, and no cost check of any kind: only the
|
|
320
|
-
structure is validated. The bundled preset is built from free models because
|
|
321
|
-
that is a sensible default, not because the pool requires it.
|
|
322
|
-
|
|
323
|
-
### Configuring OmniRoute
|
|
324
|
-
|
|
325
|
-
[OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local
|
|
326
|
-
OpenAI-compatible AI gateway (default `http://localhost:20128/v1`) that routes a
|
|
327
|
-
request across many providers with fallback and "combos". LaCode can register it
|
|
328
|
-
as an opencode provider for you. It is **off by default**: if you do not set
|
|
329
|
-
`omniroute.enabled: true`, nothing below happens and LaCode makes no request.
|
|
330
|
-
|
|
331
|
-
**Setting it up on a client machine.** Four steps, in this order:
|
|
332
|
-
|
|
333
|
-
1. Install OmniRoute, then make it stay up: run `omniroute autostart enable`
|
|
334
|
-
once. That writes a systemd user unit in a headless session, or an XDG
|
|
335
|
-
autostart entry in a graphical one, and OmniRoute supervises itself from
|
|
336
|
-
there on. Its gateway listens on `http://localhost:20128/v1`. The gateway's
|
|
337
|
-
lifecycle is **the client's responsibility**: LaCode never starts, supervises
|
|
338
|
-
or stops it, it only probes it. The gateway must therefore already be
|
|
339
|
-
running **before** you start opencode — one that is down at startup is
|
|
340
|
-
disabled for that run, and the integration is per-run.
|
|
341
|
-
2. Add the plugin block to your opencode config:
|
|
342
|
-
`["@lawaty/lacode", { "omniroute": { "enabled": true, "apiKey": "<your omniroute key>", "combo": "auto", "models": [] } }]`
|
|
343
|
-
`auto` is the zero-config default, but on a free tier it often resolves to a
|
|
344
|
-
slow chain. [A tested fast combo](#a-tested-fast-combo-free-mix) below is
|
|
345
|
-
ordered for latency and is worth applying on a new client.
|
|
346
|
-
3. Optionally, to route pooled work through it, make `omniroute/auto 3` the
|
|
347
|
-
**first** line of `~/.config/lacode/pool.txt`. `pool.txt` is optional user
|
|
348
|
-
config; the bundled default preset does **not** include OmniRoute.
|
|
349
|
-
4. **Restart opencode.** Nothing here hot-reloads.
|
|
350
|
-
|
|
351
|
-
With `models: []` and `combo: "auto"`, the plugin registers exactly one model,
|
|
352
|
-
`omniroute/auto`, which then appears in the model picker for any agent — no
|
|
353
|
-
hand-maintained model list.
|
|
354
|
-
|
|
355
|
-
The full option set, for reference:
|
|
356
|
-
|
|
357
|
-
```jsonc
|
|
358
|
-
{
|
|
359
|
-
"plugin": [
|
|
360
|
-
["@lawaty/lacode", {
|
|
361
|
-
"omniroute": {
|
|
362
|
-
"enabled": true,
|
|
363
|
-
"baseURL": "http://localhost:20128/v1",
|
|
364
|
-
"apiKey": "sk_omniroute", // optional; OMNIROUTE_API_KEY overrides it
|
|
365
|
-
"combo": "static-best-free", // optional: let OmniRoute do the routing
|
|
366
|
-
"models": ["cc/claude-sonnet-4-6", "gemini-3-flash"], // hard allowlist
|
|
367
|
-
"tiers": {
|
|
368
|
-
"primary": ["cc/claude-sonnet-4-6"],
|
|
369
|
-
"backup": ["gemini-3-flash"]
|
|
370
|
-
},
|
|
371
|
-
"stickyPerSession": true,
|
|
372
|
-
"healthCheck": true,
|
|
373
|
-
"timeoutMs": 5000
|
|
374
|
-
}
|
|
375
|
-
}]
|
|
376
|
-
]
|
|
377
|
-
}
|
|
378
|
-
```
|
|
139
|
+
### Which LaCode is running
|
|
379
140
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
| `enabled` | `false` | Turns the integration on. |
|
|
383
|
-
| `baseURL` | `http://localhost:20128/v1` | Gateway base URL, normalised to exactly one `/v1`. |
|
|
384
|
-
| `apiKey` | — | Bearer key. **`OMNIROUTE_API_KEY` overrides this value.** The key is never logged. |
|
|
385
|
-
| `combo` | — | An OmniRoute combo id (or `auto`) to expose as a selectable model even though the gateway does not list it. Exposed first. |
|
|
386
|
-
| `models` | — | A **hard allowlist**: only these ids are ever exposed, even if the gateway lists more. |
|
|
387
|
-
| `tiers` | — | `{ "primary": [...], "backup": [...] }` preference order over models the allowlist already permits. |
|
|
388
|
-
| `stickyPerSession` | `true` | Keep a session on one model while that model is healthy. |
|
|
389
|
-
| `healthCheck` | `true` | Probe `GET /models` at startup; a failed check disables the integration for that run only. |
|
|
390
|
-
| `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. |
|
|
391
|
-
|
|
392
|
-
An enabled but invalid block (bad `baseURL`, a non-array `models`, …) is rejected
|
|
393
|
-
as a whole: LaCode logs one warning naming the problem and continues exactly as
|
|
394
|
-
if the integration were off. It never crashes opencode.
|
|
395
|
-
|
|
396
|
-
**What it registers.** On the first `config` pass LaCode health-checks the
|
|
397
|
-
gateway, reads `GET /models`, filters it through your `models` allowlist (or
|
|
398
|
-
exposes everything the gateway lists when no allowlist is set), and writes a
|
|
399
|
-
`provider.omniroute` entry — `npm: "@ai-sdk/openai-compatible"` with your
|
|
400
|
-
`baseURL` / `apiKey` — so `omniroute/<model-id>` becomes selectable. If you
|
|
401
|
-
already have a `provider.omniroute` block, LaCode leaves it completely untouched
|
|
402
|
-
and warns; it also never adds a `whitelist`. Because opencode loads
|
|
403
|
-
`@ai-sdk/openai-compatible` itself, no extra dependency is installed.
|
|
404
|
-
|
|
405
|
-
**Combo vs. allowlist.** Set `combo` and OmniRoute does the fallback for you
|
|
406
|
-
(let it route): the combo leads the exposed models, so opencode can hand a failed
|
|
407
|
-
request to the gateway's own fallback chain. Without a combo, LaCode exposes the
|
|
408
|
-
allowlist ordered by `tiers` (primary first). Candidate order is `filterModels`
|
|
409
|
-
(combo first, then the hard allowlist) followed by `orderByTier`, and that is
|
|
410
|
-
all — LaCode picks which model is exposed and in what order; it does not measure
|
|
411
|
-
latency or error rate at runtime, and it does not skip a model because it
|
|
412
|
-
failed.
|
|
413
|
-
|
|
414
|
-
**Using OmniRoute in the pool.** The integration only *registers* the provider —
|
|
415
|
-
it never touches your pool, and it does not place a slot for you. To route pooled
|
|
416
|
-
work (exploration, implementation, cartography) through the gateway, list it in
|
|
417
|
-
your pool like any other model:
|
|
418
|
-
|
|
419
|
-
```
|
|
420
|
-
# ~/.config/lacode/pool.txt
|
|
421
|
-
omniroute/auto 3
|
|
422
|
-
opencode/space-bunny-free 2
|
|
423
|
-
opencode-go/step-5-preview-free 1
|
|
424
|
-
opencode/big-pickle 1
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
`omniroute/auto` then takes slot 1 (weight 3) and drains pooled mechanical work
|
|
428
|
-
before it reaches the bundled opencode/opencode-go ("zen") models. Any
|
|
429
|
-
`omniroute/<id>` works, provided the id is exposed — by `combo`/`models`, or the
|
|
430
|
-
whole catalog when neither is set. Because the plugin writes the pool nothing,
|
|
431
|
-
the only thing that changes when the gateway is down is that `omniroute/<id>`
|
|
432
|
-
fails; every other slot keeps working. See
|
|
433
|
-
[docs/USER-STORIES.md](docs/USER-STORIES.md) US-28.
|
|
434
|
-
|
|
435
|
-
### A tested fast combo: `free-mix`
|
|
436
|
-
|
|
437
|
-
`combo: "auto"` is the zero-configuration default, but `auto` inherits whatever
|
|
438
|
-
the gateway chooses, and on a busy free tier that is frequently the slowest
|
|
439
|
-
chain available. The combo below was **measured** against a live free-tier
|
|
440
|
-
gateway and is ordered for latency rather than for breadth. Apply it during the
|
|
441
|
-
"Setting it up on a client machine" steps above and pooled work is usable
|
|
442
|
-
straight away, instead of starting from a gateway that queues for half a minute
|
|
443
|
-
per call.
|
|
444
|
-
|
|
445
|
-
```jsonc
|
|
446
|
-
{
|
|
447
|
-
"name": "free-mix",
|
|
448
|
-
// Order decides. Do NOT use "weighted" here — weights let the gateway
|
|
449
|
-
// re-shuffle a model that is returning 429 into first position again.
|
|
450
|
-
"strategy": "priority",
|
|
451
|
-
"models": [
|
|
452
|
-
{ "model": "gemini/gemini-3.1-flash-lite", "providerId": "gemini" },
|
|
453
|
-
{ "model": "gemini/gemini-3.8-flash", "providerId": "gemini" },
|
|
454
|
-
{ "model": "gemini/gemini-3.7-flash", "providerId": "gemini" },
|
|
455
|
-
{ "model": "gemini/gemini-3-flash-preview", "providerId": "gemini" },
|
|
456
|
-
{ "model": "openrouter/nvidia/nemotron-3-super-120b-a12b:free", "providerId": "openrouter" },
|
|
457
|
-
{ "model": "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", "providerId": "openrouter" },
|
|
458
|
-
{ "model": "openrouter/nvidia/nemotron-3.5-lightning:free", "providerId": "openrouter" },
|
|
459
|
-
{ "model": "openrouter/cohere/north-mini-code:free", "providerId": "openrouter" },
|
|
460
|
-
{ "model": "openrouter/poolside/laguna-s-2.1:free", "providerId": "openrouter" },
|
|
461
|
-
{ "model": "openrouter/dots-studio/dots-3-note-preview:free", "providerId": "openrouter" }
|
|
462
|
-
]
|
|
463
|
-
}
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Why this order, from 859 calls over a single day against that gateway:
|
|
467
|
-
|
|
468
|
-
| Model | Observed behaviour | Position |
|
|
469
|
-
| --- | --- | --- |
|
|
470
|
-
| `gemini/gemini-3.1-flash-lite` | **165/172 succeeded (96%)**, ~4.5s median | **first** |
|
|
471
|
-
| `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 |
|
|
472
|
-
| `openrouter/*:free` | **~90% `429`**, but fails *fast* (~1.8s) and occasionally succeeds | last, as a cheap final fallback |
|
|
473
|
-
| `gemini-2.5-flash`, `gemini-2.5-flash-lite` | **`404` on every attempt** — dead ids | **omitted entirely** |
|
|
474
|
-
|
|
475
|
-
The ordering matters because a combo pays for *every* step it walks past. Put the
|
|
476
|
-
96%-success model first and the common case is a single hop; the 30s models sit
|
|
477
|
-
behind it where they cost nothing, and the 429s sit last where they give up in
|
|
478
|
-
under two seconds.
|
|
479
|
-
|
|
480
|
-
Measured on the same gateway, same day, `max_tokens: 10`:
|
|
481
|
-
|
|
482
|
-
| | Before | After |
|
|
483
|
-
| --- | --- | --- |
|
|
484
|
-
| `free-mix` response | 42.7s / 41.8s / 34.7s | 4.1s / 3.1s / 3.2s / 4.0s / 3.2s |
|
|
485
|
-
| Median | **~41.8s** | **~3.2s** |
|
|
486
|
-
| Provider time burned on calls >25s | **56.9 of 102.4 min (56%)** | — |
|
|
487
|
-
|
|
488
|
-
Before the change, the real call sequence was a `429` after 2.7s, then a `503`
|
|
489
|
-
after 30.0s, then the actual work in 5.7s — roughly **33 seconds of dead time in
|
|
490
|
-
front of every successful request**, and a p90 of 100s when three 30s steps
|
|
491
|
-
chained.
|
|
492
|
-
|
|
493
|
-
These free-tier endpoints change availability constantly, so treat the ordering
|
|
494
|
-
as a **starting point to re-measure, not a permanent truth**. Re-check it with
|
|
495
|
-
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:
|
|
496
143
|
|
|
497
144
|
```bash
|
|
498
|
-
|
|
499
|
-
curl -o /dev/null -w '%{http_code} %{time_starttransfer}s\n' \
|
|
500
|
-
-X POST http://localhost:20128/v1/chat/completions \
|
|
501
|
-
-H "Authorization: Bearer $OMNIROUTE_API_KEY" -H 'Content-Type: application/json' \
|
|
502
|
-
-d '{"model":"free-mix","messages":[{"role":"user","content":"Say OK"}],"max_tokens":10}'
|
|
503
|
-
|
|
504
|
-
# per-model status + duration, straight from OmniRoute's logs
|
|
505
|
-
python3 - <<'PY'
|
|
506
|
-
import json, glob, collections
|
|
507
|
-
d = "/home/<user>/.omniroute/call_logs/<YYYY-MM-DD>"
|
|
508
|
-
g = collections.defaultdict(collections.Counter)
|
|
509
|
-
for f in glob.glob(d + "/*.json"):
|
|
510
|
-
try: s = json.load(open(f))["summary"]
|
|
511
|
-
except Exception: continue
|
|
512
|
-
g[s.get("comboStepId")][s.get("status")] += 1
|
|
513
|
-
for k, v in sorted(g.items(), key=lambda x: str(x[0])):
|
|
514
|
-
print(f"{str(k)[:44]:44} {dict(v)}")
|
|
515
|
-
PY
|
|
145
|
+
lacode version
|
|
516
146
|
```
|
|
517
147
|
|
|
518
|
-
Apply it with the bundled CLI (`PUT /api/combos/<id>` updates in place, so the
|
|
519
|
-
combo is never briefly missing):
|
|
520
|
-
|
|
521
|
-
```bash
|
|
522
|
-
omniroute api combos get-api-combos-id- --id <combo-id> --json > free-mix.json
|
|
523
|
-
# edit name/strategy/models in free-mix.json, then:
|
|
524
|
-
omniroute api combos put-api-combos-id- --id <combo-id> --body @free-mix.json
|
|
525
148
|
```
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
copy a key into `OMNIROUTE_API_KEY` (recommended) or `omniroute.apiKey`. On a
|
|
535
|
-
local instance with `REQUIRE_API_KEY=false`, the literal placeholder
|
|
536
|
-
`sk_omniroute` is accepted.
|
|
537
|
-
|
|
538
|
-
**Troubleshooting**
|
|
539
|
-
|
|
540
|
-
| Symptom | Cause | Fix |
|
|
541
|
-
| --- | --- | --- |
|
|
542
|
-
| `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. |
|
|
543
|
-
| `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. |
|
|
544
|
-
| `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. |
|
|
545
|
-
| `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. |
|
|
546
|
-
| `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`. |
|
|
547
|
-
| `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. |
|
|
548
|
-
| 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. |
|
|
549
|
-
| 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. |
|
|
550
|
-
|
|
551
|
-
### Which models run which agent
|
|
552
|
-
|
|
553
|
-
Only the **pool** writes a model onto a pooled agent, from whichever source wins
|
|
554
|
-
above. The pooled bases (`explore`, `implement`, `context`)
|
|
555
|
-
and their hidden `-1`..`-4` variants therefore carry no model of their own: a
|
|
556
|
-
model written next to an agent would be a second source of truth that silently
|
|
557
|
-
wins whenever routing falls through. Spawn a base and let the pool route it.
|
|
558
|
-
|
|
559
|
-
### ⚠️ Per-agent tool grants must be in your own config
|
|
560
|
-
|
|
561
|
-
**LaCode cannot grant tools to its agents, and this is not an oversight you can
|
|
562
|
-
work around by configuring it.** If you keep a global `"tools"` block in your
|
|
563
|
-
config — for example `"tools": { "playwright_*": false }` — then the pooled
|
|
564
|
-
exploration and implementation bases (`explore` and `implement`, plus their
|
|
565
|
-
hidden `-1`..`-4` pool variants) lose browser access, and **nothing tells them why**: the tool call is simply
|
|
566
|
-
refused mid-task. `context` gets no grant either, which is correct — it
|
|
567
|
-
writes the context map and has no business in a browser.
|
|
568
|
-
|
|
569
|
-
opencode folds an agent's `tools` into its permission list **at config-load
|
|
570
|
-
time**, while decoding the config — before any plugin runs. A plugin's `config`
|
|
571
|
-
hook mutates the already-decoded config, so the fold never re-runs and anything
|
|
572
|
-
it puts in `agent.tools` is inert. (Verified on opencode 1.18.34/1.18.35 with
|
|
573
|
-
`opencode debug agent explore`: a grant written in your own config shows two
|
|
574
|
-
`playwright_*` entries — `deny` then `allow`, last one winning — while the same
|
|
575
|
-
grant injected by the plugin shows only the `deny`.) LaCode's other injected
|
|
576
|
-
fields are not affected; `permission`, `prompt`, `model`, `description`,
|
|
577
|
-
`temperature`, `mode` and `hidden` are all read after the plugin hook runs.
|
|
578
|
-
|
|
579
|
-
**So if you use a global `"tools"` block, declare the grants yourself:**
|
|
580
|
-
|
|
581
|
-
```jsonc
|
|
582
|
-
{
|
|
583
|
-
"tools": { "playwright_*": false },
|
|
584
|
-
"agent": {
|
|
585
|
-
"explore": { "tools": { "playwright_*": true } },
|
|
586
|
-
"implement": { "tools": { "playwright_*": true } }
|
|
587
|
-
}
|
|
588
|
-
}
|
|
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
|
|
589
157
|
```
|
|
590
158
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
`
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
a default a package should impose on a stranger.
|
|
621
|
-
|
|
622
|
-
One **field** is deliberately not injected either: `tools`, for the
|
|
623
|
-
load-time-folding reason explained above. Nothing else the plugin writes into
|
|
624
|
-
an agent is subject to it.
|
|
625
|
-
|
|
626
|
-
**Installation merges, it does not clobber** (US-16). An agent, command or
|
|
627
|
-
instruction is added only when that key is absent from your config. A
|
|
628
|
-
user-defined agent of the same name is left byte-identical — not patched, not
|
|
629
|
-
partially merged. Skips are counted and logged once.
|
|
630
|
-
|
|
631
|
-
**`enabled: false` turns all of it off** (US-17). No pool model rewriting, no
|
|
632
|
-
agent injection, no command injection, no reaper timer armed, no
|
|
633
|
-
`~/.config/lacode` directory created.
|
|
634
|
-
|
|
635
|
-
These three are specified in [docs/USER-STORIES.md](docs/USER-STORIES.md) as
|
|
636
|
-
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).
|
|
637
188
|
|
|
638
189
|
## Context map
|
|
639
190
|
|
|
@@ -673,25 +224,32 @@ in [docs/CONTEXT.md](docs/CONTEXT.md).
|
|
|
673
224
|
|
|
674
225
|
## Managerial directions
|
|
675
226
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
- `##
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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".
|
|
695
253
|
|
|
696
254
|
## Cost
|
|
697
255
|
|
|
@@ -767,12 +325,22 @@ status.
|
|
|
767
325
|
Configuration, the payload format, the receiver contract and holding the forward
|
|
768
326
|
open are in [docs/TUNNEL.md](docs/TUNNEL.md).
|
|
769
327
|
|
|
770
|
-
##
|
|
328
|
+
## Further reading
|
|
771
329
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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. |
|
|
776
344
|
|
|
777
345
|
## Development
|
|
778
346
|
|
|
@@ -807,4 +375,4 @@ exist.
|
|
|
807
375
|
|
|
808
376
|
## License
|
|
809
377
|
|
|
810
|
-
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).
|