@lawaty/lacode 0.5.0 → 0.7.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/docs/CONTEXT.md CHANGED
@@ -45,9 +45,9 @@ independently measured.** A number derived from somebody else's repository and
45
45
  token mix is not a claim about yours; see [BENCHMARKS.md](BENCHMARKS.md) for the
46
46
  harness that produces a number for your own workload.
47
47
 
48
- ## The five files
48
+ ## The files in the map
49
49
 
50
- `.opencode/context/` holds exactly these, and nothing else:
50
+ `.opencode/context/` holds the five generated files below:
51
51
 
52
52
  | File | What belongs in it |
53
53
  | --- | --- |
@@ -57,6 +57,12 @@ harness that produces a number for your own workload.
57
57
  | `workflows.md` | The few runtime flows worth tracing end to end — how a request becomes a mutation, how a plugin hook reaches the thing it hooks. Read this when you need to know what actually happens at runtime. |
58
58
  | `decisions.md` | Architectural decisions that the repository's own decision log does not already record. Concise, variable length. |
59
59
 
60
+ The project's root `AGENTS.md` is the standing-instructions home: the client's
61
+ own statement of intent, what they want worked on and how they want it
62
+ approached. It sits outside the map, is per-clone and not committed, and has
63
+ no prescribed structure. It has exactly one writer — the hidden `instructions`
64
+ agent, reachable only from the `/agent-instructions` command.
65
+
60
66
  Files the user or the plan explicitly names always win over the map. The map is
61
67
  a shortcut to the right starting point, not an authority: when it disagrees with
62
68
  the source, the source is right and the map is wrong.
@@ -98,12 +104,20 @@ formatting, local renames, added tests and dependency bumps rarely alter what
98
104
  the map should say. The cartographer is told to edit only when the work
99
105
  introduced or revealed something that will help a future agent — a new boundary,
100
106
  a new or moved entry point, a changed workflow, a new convention, an
101
- architectural decision, a previously undocumented invariant — and to report
107
+ architectural decision, an undocumented invariant — and to report
102
108
  "no update needed" otherwise.
103
109
 
104
- If the map is missing entirely, nothing breaks: agents check, find nothing, and
105
- explore as they always did. The standing rule that tells them to check is
106
- injected into your config only when you have no `instructions` array of your own.
110
+ If the map is missing entirely, nothing breaks: agents run the check, find no
111
+ `.opencode/context/`, and explore from scratch. What tells them to run the check
112
+ is an entry appended to your config's `instructions` array: your own entries are
113
+ kept as they are, and a marked entry goes after them unless one of your entries
114
+ already carries its marker or its opt-out. LaCode appends two such entries, each
115
+ governed by its own pair — the context-map rule
116
+ (`<!-- lacode:context-map-instruction:v1 -->`, opt out with
117
+ `<!-- lacode:context-map-instruction:off -->`) and the rule that documentation
118
+ states the current state (`<!-- lacode:current-state-rule:v1 -->`, opt out with
119
+ `<!-- lacode:current-state-rule:off -->`). One opt-out suppresses only the rule
120
+ it names.
107
121
 
108
122
  ### On demand
109
123
 
@@ -132,10 +146,10 @@ outside the map — not by convention, but by an `edit` rule that allows
132
146
  The rule runs in both directions. The agents that *could* write anywhere else in
133
147
  the repository — the root `build` agent and both implement tiers — carry the
134
148
  mirror image of the cartographer's grant: `"*": "allow"` followed by a `deny` on
135
- `.opencode/context/**`, so they edit everything except the map. The instruction
136
- telling them not to touch it is still there, and still matters — a permission
137
- denial tells an agent nothing about what it should have done instead — but it is
138
- no longer the only thing standing between them and the map.
149
+ `.opencode/context/**`, so they edit everything except the map. Enforcement is
150
+ two mechanisms at once, and both are load-bearing: the injected instruction
151
+ naming the one writer, and the permission rule — a permission denial tells an
152
+ agent nothing about what it should have done instead.
139
153
 
140
154
  This exists because two writers produce a map that is neither current nor
141
155
  trustworthy. Concurrent sessions rewriting the same five files produce
@@ -150,14 +164,14 @@ writes every narrow rule last, in both files (`src/agents.ts` and your own
150
164
  grepping for the word `allow` — which is what stops a correctly-placed deny from
151
165
  being reported as a hole.
152
166
 
153
- The rule has a sharp edge worth knowing about, because it has already bitten
154
- this project: **opencode evaluates a file permission against the path relative
155
- to the project root, not the absolute path the tool was given.** A rule written
156
- as `*/.opencode/context/**` matches an absolute path and therefore matches
157
- nothing at all. Both spellings are therefore required — `.opencode/context/**`
158
- for a normally-rooted project and `*/.opencode/context/**` for one rooted at
159
- `/` — and LaCode ships both. At startup the plugin checks your config for that,
160
- and logs an error naming the rules it saw if neither form would ever fire.
167
+ The rule has a sharp edge worth knowing about: **opencode evaluates a file
168
+ permission against the path relative to the project root, not the absolute path
169
+ the tool was given.** A rule written as `*/.opencode/context/**` matches an
170
+ absolute path and therefore matches nothing at all. Both spellings are therefore
171
+ required — `.opencode/context/**` for a normally-rooted project and
172
+ `*/.opencode/context/**` for one rooted at `/` — and LaCode ships both. At
173
+ startup the plugin checks your config for that, and logs an error naming the
174
+ rules it saw if neither form would ever fire.
161
175
 
162
176
  ### The residual risk: a subdirectory worktree
163
177
 
@@ -181,14 +195,13 @@ How to detect it: open opencode from the repository root, not from a
181
195
  subdirectory of it. If `ls .opencode/context` from your shell's current
182
196
  directory does not list the five files, your shell is not at the root and
183
197
  neither is opencode's worktree. The cost of getting it wrong is not a crash —
184
- it is a silent return to a map maintained only by prompt, which is the exact
185
- state US-21 exists to eliminate.
186
-
187
- The symptom of getting this wrong is not a crash. The cartographer's writes are
188
- simply denied, its run still "succeeds", and the map quietly stops changing. So
189
- a run that changed nothing on disk is reported as a warning, not as a success:
190
- if you see `auto update finished without changing the map` repeatedly, the first
191
- thing to check is that permission rule, not the map's content.
198
+ it is a map maintained only by prompt, with no mechanism enforcing who writes it.
199
+
200
+ The symptom is silence: the cartographer's writes are denied without an error,
201
+ its run reports success, and the map quietly stops changing. So a run that
202
+ changed nothing on disk is reported as a warning, not as a success: if you see
203
+ `auto update finished without changing the map` repeatedly, the first thing
204
+ to check is that permission rule, not the map's content.
192
205
 
193
206
  ## What you will see in the log
194
207
 
package/docs/INSTALL.md CHANGED
@@ -118,26 +118,25 @@ This package also publishes `main` and an `exports["./server"]` entry pointing a
118
118
  the same built file, because those are two of the shapes a plugin loader
119
119
  resolving an entry point consults. They are forward-compatibility metadata and
120
120
  they cost nothing — but they do **not** fix this on 1.18.35: adding either one to
121
- the *installed* cache copy was tested there and the bare specifier still loaded
122
- nothing. No other version has been observed where they help either. Use the
123
- `file://` entry.
121
+ the *installed* cache copy leaves the bare specifier loading nothing. Whether
122
+ they help on any other version is unknown. Use the `file://` entry.
124
123
 
125
124
 
126
125
  ## Which opencode versions this is known on
127
126
 
128
- Exactly one version has been measured, and the procedure above is deliberately
127
+ Exactly one version is measured, and the procedure above is deliberately
129
128
  not built around that fact. Treat this table as the evidence it is, which is one
130
129
  row wide.
131
130
 
132
131
  | opencode version | bare spec loads? | `file://` needed? | evidence |
133
132
  | --- | --- | --- | --- |
134
- | 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. |
135
- | Any other version | **Unknown** | **Unknown** | **Untested.** No other opencode version has been run against this package in either direction. |
133
+ | 1.18.35 | No — installs the package, then silently drops it | Yes | Measured 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. |
134
+ | Any other version | **Unknown** | **Unknown** | No other opencode version is covered by this table, in either direction. |
136
135
 
137
- Every command quoted on this page was run against 1.18.35. **If your version is
138
- not the one measured here, follow the procedure above rather than reading this
139
- table as coverage of your build**: each step stands on its own, and step 2 tells
140
- you which situation you are in before any version-specific advice is relevant.
136
+ **If your version is not the one measured here, follow the procedure above
137
+ rather than reading this table as coverage of your build**: each step stands on
138
+ its own, and step 2 tells you which situation you are in before any
139
+ version-specific advice is relevant.
141
140
 
142
141
 
143
142
  ## macOS
@@ -168,8 +167,8 @@ Three things differ, and all three are worth reading rather than assuming:
168
167
  notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
169
168
  once with `lacode tunnel setup` — no reverse forward, no listening port, no
170
169
  systemd unit. See [Remote hosts](../README.md#remote-hosts) and docs/TUNNEL.md.
171
- - The repo still carries the author's personal, Linux-only tooling from the older
172
- reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
170
+ - The repo carries the author's personal, Linux-only reverse-forward tooling
171
+ (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
173
172
  `bin/oc-sync`). None of it is in the published package, and none of it is needed
174
173
  for the npm-client path above.
175
174
 
@@ -182,7 +181,8 @@ or no GNU `timeout` — rather than passing silently or failing confusingly.
182
181
 
183
182
  ## The `lacode` CLI
184
183
 
185
- The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
184
+ The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `version`,
185
+ `update`, `receive`, `tunnel setup`) — see
186
186
  [Remote hosts](../README.md#remote-hosts). The plugin install does **not** put it on your
187
187
  `PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
188
188
  globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
@@ -204,6 +204,6 @@ Two things are worth knowing about what npm actually hands you:
204
204
  `dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
205
205
  `dist/` cannot be published). This is not a preference: node strips types
206
206
  natively from 22.18 but **refuses to for files under `node_modules`**, so a
207
- package that shipped `.ts` worked fine in a checkout and failed on install —
208
- `lacode --help` died with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A
209
- published package cannot depend on its consumer's loader being lenient.
207
+ package that ships `.ts` fails on install — `lacode --help` exits with
208
+ `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. A published package cannot
209
+ depend on its consumer's loader being lenient.
package/docs/OMNIROUTE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OmniRoute gateway integration
2
2
 
3
- [OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local OpenAI-compatible gateway that routes a request across many providers with fallback and "combos". LaCode can register it as an opencode provider. It is **off by default**. The summary is in the [README](../README.md#configure); this page is the full reference, including a latency-measured combo that is worth applying on a new client.
3
+ [OmniRoute](https://github.com/diegosouzapw/OmniRoute) is a local OpenAI-compatible gateway that routes a request across many providers with fallback and "combos". LaCode can register it as an opencode provider. It is **off by default**. The summary is in the [README](../README.md#configure); this page is the full reference, including a combo ordered for latency that is worth applying on a new client.
4
4
 
5
5
  ## Configuring OmniRoute
6
6
 
@@ -119,11 +119,10 @@ fails; every other slot keeps working. See
119
119
 
120
120
  `combo: "auto"` is the zero-configuration default, but `auto` inherits whatever
121
121
  the gateway chooses, and on a busy free tier that is frequently the slowest
122
- chain available. The combo below was **measured** against a live free-tier
123
- gateway and is ordered for latency rather than for breadth. Apply it during the
124
- "Setting it up on a client machine" steps above and pooled work is usable
125
- straight away, instead of starting from a gateway that queues for half a minute
126
- per call.
122
+ chain available. The combo below is ordered for latency rather than for
123
+ breadth. Apply it during the "Setting it up on a client machine" steps above
124
+ and pooled work is usable straight away, instead of starting from a gateway that
125
+ queues for half a minute per call.
127
126
 
128
127
  ```jsonc
129
128
  {
@@ -146,33 +145,21 @@ per call.
146
145
  }
147
146
  ```
148
147
 
149
- Why this order, from 859 calls over a single day against that gateway:
148
+ Why this order — it is the two free-tier failure modes, a hard reject and a
149
+ slow queue-budget timeout:
150
150
 
151
- | Model | Observed behaviour | Position |
151
+ | Model | Behaviour on a busy free tier | Position |
152
152
  | --- | --- | --- |
153
- | `gemini/gemini-3.1-flash-lite` | **165/172 succeeded (96%)**, ~4.5s median | **first** |
154
- | `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 |
155
- | `openrouter/*:free` | **~90% `429`**, but fails *fast* (~1.8s) and occasionally succeeds | last, as a cheap final fallback |
153
+ | `gemini/gemini-3.1-flash-lite` | succeeds on the large majority of calls, ~4.5s median | **first** |
154
+ | `gemini-3.8-flash` / `3.7-flash` / `3-flash-preview` | succeed intermittently, but **stall ~30s** on a `503` queue-budget timeout when they do not | next |
155
+ | `openrouter/*:free` | **mostly `429`**, but fail *fast* (~1.8s) and occasionally succeed | last, as a cheap final fallback |
156
156
  | `gemini-2.5-flash`, `gemini-2.5-flash-lite` | **`404` on every attempt** — dead ids | **omitted entirely** |
157
157
 
158
158
  The ordering matters because a combo pays for *every* step it walks past. Put the
159
- 96%-success model first and the common case is a single hop; the 30s models sit
159
+ reliable model first and the common case is a single hop; the 30s models sit
160
160
  behind it where they cost nothing, and the 429s sit last where they give up in
161
161
  under two seconds.
162
162
 
163
- Measured on the same gateway, same day, `max_tokens: 10`:
164
-
165
- | | Before | After |
166
- | --- | --- | --- |
167
- | `free-mix` response | 42.7s / 41.8s / 34.7s | 4.1s / 3.1s / 3.2s / 4.0s / 3.2s |
168
- | Median | **~41.8s** | **~3.2s** |
169
- | Provider time burned on calls >25s | **56.9 of 102.4 min (56%)** | — |
170
-
171
- Before the change, the real call sequence was a `429` after 2.7s, then a `503`
172
- after 30.0s, then the actual work in 5.7s — roughly **33 seconds of dead time in
173
- front of every successful request**, and a p90 of 100s when three 30s steps
174
- chained.
175
-
176
163
  These free-tier endpoints change availability constantly, so treat the ordering
177
164
  as a **starting point to re-measure, not a permanent truth**. Re-check it with
178
165
  OmniRoute's own call logs before assuming it still holds:
package/docs/TUNNEL.md CHANGED
@@ -38,10 +38,10 @@ as one newline-delimited argument:
38
38
  title\nmessage\nurgency\nicon\nsound
39
39
  ```
40
40
 
41
- Five fields, and only the last one is new: `sound` is `none`, `done`,
42
- `question`, `permission` or `error`. A sender that stops at `icon` simply
43
- leaves it empty, and an empty or unrecognised name is `none`, so a four-field
44
- payload is still a valid one.
41
+ Five fields — `title`, `message`, `urgency`, `icon`, `sound`. `sound` is `none`,
42
+ `done`, `question`, `permission` or `error`. A sender that stops at `icon`
43
+ simply leaves it empty, and an empty or unrecognised name is `none`, so a
44
+ four-field payload is a valid one.
45
45
 
46
46
  Newline is safe as the delimiter because both ends strip control characters from
47
47
  the free-text fields before assembling or reading it, and `urgency`, `icon` and
@@ -93,7 +93,7 @@ lacode doctor
93
93
 
94
94
  SOUND is optional and last: `none`, `done`, `question`, `permission` or `error`,
95
95
  one bundled tone per outcome. Leave it off and nothing plays; the line above
96
- without a fifth word is still correct.
96
+ without a fifth word is correct.
97
97
 
98
98
  `notify` is fire-and-forget by design. An unreachable desktop, a missing key, no
99
99
  notifier installed at all — all of them print a warning on stderr and exit 0. A
@@ -124,7 +124,8 @@ runs the session and has no speakers. The name is validated against the sound
124
124
  whitelist at the same trust boundary as `urgency` and `icon`, and the receiver
125
125
  looks the file up in the package's own `sounds/` directory — the sender's word
126
126
  never becomes a path. `afplay` plays it on macOS, `paplay`, `aplay` or
127
- `pw-play` on Linux, and a machine with none of them still gets the notification.
127
+ `pw-play` on Linux, and a machine with none of them gets the notification
128
+ regardless.
128
129
 
129
130
  ## Configuration
130
131
 
@@ -135,7 +136,7 @@ never becomes a path. `afplay` plays it on macOS, `paplay`, `aplay` or
135
136
  | `OC_NOTIFY_KEY` | `~/.ssh/lacode_notify` | The dedicated key. Never a personal key. |
136
137
  | `OC_CONNECT_TIMEOUT_MS` | `5000` | Ceiling on the hop. |
137
138
  | `OC_SSH_TIMEOUT_MS` | `30000` | Ceiling on `forward`. |
138
- | `OC_TUNNEL_PORT` | `10022` | **Legacy** reverse-forward listener port. Used only when `OC_LOCAL_HOST` is unset. |
139
+ | `OC_TUNNEL_PORT` | `10022` | Reverse-forward listener port. Used when `OC_LOCAL_HOST` is unset. |
139
140
 
140
141
  `OC_LOCAL_USER` is read rather than probed on purpose. `id -un` on a remote
141
142
  account frequently returns `root`, and a dbus path built from that reaches
@@ -151,11 +152,12 @@ documented with the other plugin options in the README. Set it to `0`, `false`,
151
152
  `off` or `no` to silence notifications; anything else — a typo included — leaves
152
153
  sound on, because a misspelt variable must not be how you turn a feature off.
153
154
 
154
- ## Legacy: the reverse forward
155
+ ## The reverse forward
155
156
 
156
- The original design opened a reverse forward (`ssh -R`) held by a `systemd --user`
157
- unit on the desktop, and delivered to a `notify-send`-only bash receiver
158
- (`bin/oc-notify-receiver`). It is kept only for hosts that still set
159
- `OC_TUNNEL_PORT` and leave `OC_LOCAL_HOST` unset: `sendRemote` falls back to an
160
- ssh call to `localhost:<port>`. New setups should use the direct ssh above. See
161
- the git history for the old systemd unit shape.
157
+ A host that sets `OC_TUNNEL_PORT` and leaves `OC_LOCAL_HOST` unset reaches the
158
+ desktop through a reverse forward: `sendRemote` falls back to an ssh call to
159
+ `localhost:<port>`, where a `systemd --user` unit on the desktop holds an
160
+ `ssh -R` forward open and delivers to a `notify-send`-only bash receiver
161
+ (`bin/oc-notify-receiver`). That receiver is the Linux end of the hop: it
162
+ delivers through `notify-send`, and libnotify is a Linux thing, so a Mac cannot
163
+ be this end of the tunnel.
@@ -4,8 +4,8 @@
4
4
 
5
5
  Every story below is a behavioural contract, not a feature wish. Each one is
6
6
  traceable to a pain point the author actually hit, and each carries acceptance
7
- criteria written so that a test can assert them mechanically. Phase 3 fills in
8
- the test file for each row of the Coverage table.
7
+ criteria written so that a test can assert them mechanically. Every row of the
8
+ Coverage table has a test file.
9
9
 
10
10
  **Reading the criteria.** Every criterion is written as *Given … When … Then …
11
11
  * so that it maps to one test case. "Given" is a state or fixture, "When" is a
@@ -27,13 +27,13 @@ spawn recorded in `claims.<pid>.json`. A *context map* is the five-file
27
27
 
28
28
  | Epic | Pain point it solves |
29
29
  | --- | --- |
30
- | **A — Cost control** | Quota exhaustion. Routine work (grep, reading, cartography) was consuming the same expensive budget as design work, and the main model was being spent on file listing. |
31
- | **B — Provider resilience** | Provider limits. Free-tier quotas are per-provider and per-minute, so a long session would hard-fail the moment one provider's window closed, even while three other providers sat idle. |
32
- | **C — Hang recovery** | Hangs. A wedged or silently-stalled subagent held its slot forever; one bad task stalled the session with no error and no timeout. |
33
- | **D — Notifications** | Parallel sessions. With several sessions open, work finishing — or a question blocking — was invisible unless you happened to be looking at that terminal. |
34
- | **E — Remote hosts** | Remote SSH notification. Work ran on remote hosts, so notifications raised there died on that host's nonexistent desktop instead of reaching the local one. |
30
+ | **A — Cost control** | Quota exhaustion. Routine work (grep, reading, cartography) consumes the same expensive budget as design work, and the main model is spent on file listing. |
31
+ | **B — Provider resilience** | Provider limits. Free-tier quotas are per-provider and per-minute, so a long session hard-fails the moment one provider's window closes, even while three other providers sit idle. |
32
+ | **C — Hang recovery** | Hangs. A wedged or silently-stalled subagent holds its slot forever; one bad task stalls the session with no error and no timeout. |
33
+ | **D — Notifications** | Parallel sessions. With several sessions open, work finishing — or a question blocking — is invisible unless you happen to be looking at that terminal. |
34
+ | **E — Remote hosts** | Remote SSH notification. Work runs on remote hosts, so notifications raised there die on that host's nonexistent desktop instead of reaching the local one. |
35
35
  | **F — Adoption and safety** | Installability. A personal config cannot be adopted safely: it either clobbers what you already have, or needs manual surgery before it runs. |
36
- | **G — Context and cost** | Cold exploration. Every task started by re-discovering the structure of the codebase from scratch, so the same layout was re-read — and re-paid for — on every single run. |
36
+ | **G — Context and cost** | Cold exploration. Every task starts by re-discovering the structure of the codebase from scratch, so the same layout is re-read — and re-paid for — on every single run. |
37
37
 
38
38
  ---
39
39
 
@@ -275,16 +275,16 @@ does not.
275
275
 
276
276
  **Story ID(s) for tests:** US-26
277
277
  **Acceptance criteria:**
278
- - [ ] Given the notification is raised on macOS, When it is delivered locally, Then it goes to `osascript` — or to `terminal-notifier` if `osascript` is not installed — and not to `notify-send`, which does not exist there. Before this, the send exited 0, printed nothing, and popped up nothing.
278
+ - [ ] Given the notification is raised on macOS, When it is delivered locally, Then it goes to `osascript` — or to `terminal-notifier` if `osascript` is not installed — and not to `notify-send`, which does not exist there.
279
279
  - [ ] Given the channel is chosen, When it is picked, Then it is decided by `process.platform` and by which binary is actually installed, never by guessing, and every platform other than macOS keeps the `notify-send` call it has always made.
280
280
  - [ ] Given the title and the body reach `osascript`, When the script runs, Then they are passed as arguments to an `on run argv` handler and never concatenated into the script source, because both fields are attacker-influenced text — a session title, a file path, a command.
281
281
  - [ ] Given a body containing `"`, `'`, `;`, `$(…)` or backticks, When the macOS send runs, Then it reaches `display notification` byte-for-byte as data and nothing is executed.
282
282
  - [ ] Given a machine has no notifier for its platform, When a notification is sent, Then the result is a failed delivery with a reason and no exception, and `lacode doctor` names the channel that would run — or reports that there is none — instead of reporting a binary that is not used there.
283
283
  - [ ] Given the CLI runs on macOS, When the send does not complete, Then it is still bounded by a timeout, exactly as on Linux.
284
- - [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the older reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's legacy tooling, so a Mac user is not sent to debug a script that was never meant to run on their machine.
285
- - [ ] Given a machine without GNU `timeout` or with bash older than 4.0, When `bin/oc-sync` runs, Then it refuses **before** touching anything, names each missing prerequisite, and names the `lacode` subcommand to use instead. It must never report a misleading cause: the host list is read with `mapfile`, so without it the run used to die at "no hosts configured" — a confident wrong answer that sends the reader to edit `.env`.
284
+ - [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's legacy tooling, so a Mac user is not sent to debug a script that was never meant to run on their machine.
285
+ - [ ] Given a machine without GNU `timeout` or with bash older than 4.0, When `bin/oc-sync` runs, Then it refuses **before** touching anything, names each missing prerequisite, and names the `lacode` subcommand to use instead. It must never report a misleading cause: the host list is read with `mapfile`, so without it the run dies at "no hosts configured" — a confident wrong answer that sends the reader to edit `.env`.
286
286
  - [ ] Given `bin/oc-notify-receiver` runs where `notify-send` does not exist, When it is invoked, Then it exits non-zero and names both the missing channel and the portable path, because its stderr reaches whoever opened the ssh connection and nobody else, and a bare "command not found" reads as a desktop that quietly stopped notifying.
287
- - [ ] Given the payload is parsed, When the receiver splits `$SSH_ORIGINAL_COMMAND`, Then it splits on newlines only using constructs that exist in bash 3.2 as well as bash 5, and the suite **proves the split by running the receiver** rather than by finding the word `mapfile` in the file. Under bash 3.2 — the version macOS ships — a bash-4-only builtin left every field at its default, so the popup said "LaCode" with an empty body and no error anywhere: the same silent blank notification the argv bug caused once, reached again through a different door.
287
+ - [ ] Given the payload is parsed, When the receiver splits `$SSH_ORIGINAL_COMMAND`, Then it splits on newlines only using constructs that exist in bash 3.2 as well as bash 5, and the suite **proves the split by running the receiver** rather than by finding the word `mapfile` in the file. Under bash 3.2 — the version macOS ships — a bash-4-only builtin leaves every field at its default, so the popup says "LaCode" with an empty body and no error anywhere: the same silent blank notification the argv bug causes, reached through a different door.
288
288
  - [ ] Given the whole suite, When it runs on macOS, Then it passes there too. Checks that exercise the Linux-only shell scripts **skip** on a machine that cannot run them, each naming the specific missing prerequisite (a bash version below 4.0, an absent GNU `timeout`) rather than passing silently.
289
289
  - [ ] Given the local notification channel differs by platform, When a test asserts on the notifier's argv, Then it reads **every** channel's stub rather than only `notify-send`. A Mac picks `osascript` and never invokes `notify-send` at all, so a suite that stubbed only `notify-send` would pass on Linux, read an empty log on a Mac, and in the process invoke the runner's real `osascript` against a desktop session that is not there.
290
290
 
@@ -300,7 +300,7 @@ does not.
300
300
  - [ ] Given a fresh machine with no LaCode config, When the install runs, Then one config line is sufficient and no further manual step is required.
301
301
  - [ ] Given no configuration is present at all, When LaCode boots, Then it falls back to shipped defaults and still functions.
302
302
  - [ ] Given the plugin package is installed, When its peer dependency is unmet, Then the requirement is declared rather than silently duplicated inside the package.
303
- - [ ] Given the published manifest, When a plugin loader looks for an entry point, Then it finds one in both shapes a resolver may consult — a top-level `main` and `exports["./server"]` — while `exports["."]` keeps pointing at the same built plugin, and the documentation states plainly that these are forward-compatibility metadata and **not** a fix for opencode 1.18.35, where adding them to the installed copy was tested and the bare specifier still loaded nothing.
303
+ - [ ] Given the published manifest, When a plugin loader looks for an entry point, Then it finds one in both shapes a resolver may consult — a top-level `main` and `exports["./server"]` — while `exports["."]` keeps pointing at the same built plugin, and the documentation states plainly that these are forward-compatibility metadata and **not** a fix for opencode 1.18.35, where the bare specifier still loads nothing.
304
304
  - [ ] Given an opencode that does not load the bare specifier, When the reader follows the install documentation, Then the README offers a numbered procedure that holds **whichever version they run** — write the bare specifier and restart, verify with `opencode agent list`, and only if the agents are still missing while the package is installed, add a `file://` entry and restart — rather than instructions scoped to one build. Each step is justified by what the previous step showed, not by what the reader's version is known to do.
305
305
  - [ ] Given a version nobody has tested, When the reader checks the version table, Then **every** row other than the measured one is marked untested or unknown, the README claims no loading behaviour for any version it has not run, and it tells the reader to follow the procedure rather than to read the table as coverage of their build. The one row that does claim behaviour records the evidence behind it, and says that installing the package and then silently dropping it is what was measured.
306
306
  - [ ] Given any operating system, When the reader needs the package directory to build a `file://` entry, Then the path comes from `opencode debug paths` rather than a hardcoded root, and any literal path in the document is marked as an illustration of one platform's layout instead of a value to copy. Where a platform's cache root has not been established, the README says so rather than guessing it.
@@ -329,7 +329,7 @@ does not.
329
329
  ### US-24: Tool grants are documented, not injected, because a plugin cannot set them
330
330
  **As a** user who keeps a global `"tools"` block, **I want** to know that per-agent tool grants must be declared in my own config, **so that** browser automation is not silently denied on the agents that need it.
331
331
 
332
- The pain: LaCode once shipped a `tools: { "playwright_*": true }` grant on its four exploration and implementation tiers, with a comment claiming it rescued anyone who had globally disabled Playwright. It did not. opencode folds an agent's `tools` into its permission list **at config-load time**, inside the schema decode that runs before any plugin is called; a plugin's `config` hook mutates the already-decoded object, so the fold never re-runs and the injected value is read by nothing. The grant was dead code asserting a rescue it never performed. The fix is not to re-inject it — it is to tell the truth about it and document the one-line fix the user can apply themselves.
332
+ A plugin cannot set per-agent tool grants. opencode folds an agent's `tools` into its permission list **at config-load time**, inside the schema decode that runs before any plugin is called; a plugin's `config` hook mutates the already-decoded object, so the fold never re-runs and any injected `tools` value is read by nothing. A `tools: { "playwright_*": true }` grant on the exploration and implementation tiers would therefore be dead code asserting a rescue it never performs. The fix is not to inject it — it is to tell the truth about it and document the one-line fix the user can apply themselves.
333
333
 
334
334
  **Story ID(s) for tests:** US-24
335
335
  **Acceptance criteria:**
@@ -343,7 +343,7 @@ The pain: LaCode once shipped a `tools: { "playwright_*": true }` grant on its f
343
343
  ### US-27: The package keeps itself current, without npm and without being asked
344
344
  **As a** user who installed LaCode once, **I want** each new release to reach my machine automatically, **so that** I never have to learn that opencode freezes the plugin version and hand-clear a cache to upgrade.
345
345
 
346
- The pain: opencode's plugin cache is **write-once**. It resolves `@lawaty/lacode` to an exact version once and never re-resolves — not on restart, not with `opencode plugin -g -f`, not with a range pin, not with a tagged config spec (all four verified against opencode 1.18.34). A published release therefore reaches a client only when something installs it into that cache again, and nothing in opencode ever does. The plugin has to do it itself, and it may assume only one tool: the opencode binary the code already runs inside.
346
+ opencode's plugin cache is **write-once**. It resolves `@lawaty/lacode` to an exact version once and never re-resolves — not on restart, not with `opencode plugin -g -f`, not with a range pin, not with a tagged config spec (all four verified against opencode 1.18.34). A published release therefore reaches a client only when something installs it into that cache again, and nothing in opencode ever does. The plugin has to do it itself, and it may assume only one tool: the opencode binary the code already runs inside.
347
347
 
348
348
  **Story ID(s) for tests:** US-27
349
349
  **Acceptance criteria:**
@@ -361,11 +361,11 @@ The pain: opencode's plugin cache is **write-once**. It resolves `@lawaty/lacode
361
361
 
362
362
  ## Epic G — Context and cost (a maintained map instead of cold exploration)
363
363
 
364
- The pain: every task opened by exploring the repository from scratch. The same
365
- directory layout, the same entry points and the same layering were rediscovered
366
- — and re-paid for — on every run, and nothing written down survived the session
367
- that learned it. LaCode keeps a five-file codemap at `.opencode/context/` and
368
- makes agents read it before they explore, then keeps it current on its own.
364
+ Every task opens by exploring the repository from scratch. The same directory
365
+ layout, the same entry points and the same layering are rediscovered — and
366
+ re-paid for — on every run, and nothing written down survives the session that
367
+ learned it. LaCode keeps a five-file codemap at `.opencode/context/` and makes
368
+ agents read it before they explore, then keeps it current on its own.
369
369
 
370
370
  ### US-19: Curated map replaces cold exploration
371
371
  **As a** user, **I want** agents to start from a maintained map of the codebase, **so that** each task spends fewer tokens on re-discovering structure that is already written down.
@@ -458,10 +458,9 @@ table was filled in.
458
458
  | US-21 | G | `tests/stories/g-context-management.test.mjs`, `tests/permission-test.mjs` |
459
459
  | US-22 | G | `tests/stories/g-context-management.test.mjs` |
460
460
 
461
- The nine suites under `tests/` are unit-style and are kept as they are; the
462
- `tests/stories/` suites are the behavioural layer, written one epic per file with
463
- every test name carrying its story id. Run everything with `npm test`.
461
+ The nine suites under `tests/` are unit-style; the `tests/stories/` suites are
462
+ the behavioural layer, written one epic per file with every test name carrying
463
+ its story id. Run everything with `npm test`.
464
464
 
465
- **No story is uncovered.** The previously uncovered set — US-1, US-2, US-3,
466
- US-6, and all of epic F — now has tests, and the coverage guard fails the build
467
- if any of them loses one.
465
+ **No story is uncovered.** US-1, US-2, US-3, US-6, and all of epic F have tests,
466
+ and the coverage guard fails the build if any of them loses one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,5 +60,8 @@
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@opencode-ai/plugin": "1.18.34"
63
+ },
64
+ "publishConfig": {
65
+ "tag": "beta"
63
66
  }
64
67
  }