@lawaty/lacode 0.6.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/CHANGELOG.md +23 -1
- package/README.md +47 -54
- package/dist/agents.js +69 -78
- package/dist/commands.js +11 -63
- package/dist/index.js +88 -194
- package/docs/BENCHMARKS.md +7 -9
- package/docs/CONFIGURATION.md +12 -12
- package/docs/CONTEXT.md +39 -26
- package/docs/INSTALL.md +14 -15
- package/docs/OMNIROUTE.md +12 -25
- package/docs/TUNNEL.md +16 -14
- package/docs/USER-STORIES.md +25 -26
- package/package.json +1 -1
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
|
|
48
|
+
## The files in the map
|
|
49
49
|
|
|
50
|
-
`.opencode/context/` holds
|
|
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,
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
122
|
-
|
|
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
|
|
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 |
|
|
135
|
-
| Any other version | **Unknown** | **Unknown** |
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
172
|
-
|
|
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
|
|
|
@@ -205,6 +204,6 @@ Two things are worth knowing about what npm actually hands you:
|
|
|
205
204
|
`dist/`, built from `src/` at pack time (`prepack` runs the build, so a stale
|
|
206
205
|
`dist/` cannot be published). This is not a preference: node strips types
|
|
207
206
|
natively from 22.18 but **refuses to for files under `node_modules`**, so a
|
|
208
|
-
package that
|
|
209
|
-
`
|
|
210
|
-
|
|
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
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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 |
|
|
151
|
+
| Model | Behaviour on a busy free tier | Position |
|
|
152
152
|
| --- | --- | --- |
|
|
153
|
-
| `gemini/gemini-3.1-flash-lite` |
|
|
154
|
-
| `gemini-3.8-flash` / `3.7-flash` / `3-flash-preview` | succeed intermittently, but **
|
|
155
|
-
| `openrouter/*:free` |
|
|
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
|
-
|
|
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
|
|
42
|
-
`question`, `permission` or `error`. A sender that stops at `icon`
|
|
43
|
-
leaves it empty, and an empty or unrecognised name is `none`, so a
|
|
44
|
-
payload is
|
|
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
|
|
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
|
|
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` |
|
|
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
|
-
##
|
|
155
|
+
## The reverse forward
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -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.
|
|
8
|
-
|
|
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)
|
|
31
|
-
| **B — Provider resilience** | Provider limits. Free-tier quotas are per-provider and per-minute, so a long session
|
|
32
|
-
| **C — Hang recovery** | Hangs. A wedged or silently-stalled subagent
|
|
33
|
-
| **D — Notifications** | Parallel sessions. With several sessions open, work finishing — or a question blocking —
|
|
34
|
-
| **E — Remote hosts** | Remote SSH notification. Work
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
462
|
-
|
|
463
|
-
|
|
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.**
|
|
466
|
-
|
|
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