@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 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
- # Temporary Holding Version
1
+ # LaCode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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).