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