@lawaty/lacode 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -56
- package/dist/agents.js +47 -240
- package/dist/commands.js +6 -6
- package/dist/index.js +145 -274
- package/dist/lacode.js +115 -27
- package/dist/pool.js +25 -2
- package/dist/tunnel.js +76 -25
- package/docs/CONTEXT.md +4 -4
- package/docs/TUNNEL.md +50 -8
- package/docs/USER-STORIES.md +41 -15
- package/package.json +4 -2
- package/presets/free-tier.json +1 -1
package/docs/USER-STORIES.md
CHANGED
|
@@ -16,7 +16,7 @@ document; everything here is testable offline with a stub client and a
|
|
|
16
16
|
`mkdtempSync` state directory.
|
|
17
17
|
|
|
18
18
|
**Terminology.** A *slot* is one entry in `presets/free-tier.json`, carrying a
|
|
19
|
-
model and a `weight`. A *base* is a pooled agent type (`explore
|
|
19
|
+
model and a `weight`. A *base* is a pooled agent type (`explore`), and its slot
|
|
20
20
|
holders are `<base>-<slot>`, hidden in `opencode.jsonc`. A *claim* is a running
|
|
21
21
|
spawn recorded in `claims.<pid>.json`. A *context map* is the five-file
|
|
22
22
|
`.opencode/context/` codemap described in Epic G.
|
|
@@ -45,7 +45,7 @@ spawn recorded in `claims.<pid>.json`. A *context map* is the five-file
|
|
|
45
45
|
**Story ID(s) for tests:** US-1
|
|
46
46
|
**Acceptance criteria:**
|
|
47
47
|
- [ ] Given the root agent `build` with read/glob/grep denied, When a read-family tool is invoked, Then the call is denied and the agent is pointed at an exploration base rather than executing inline.
|
|
48
|
-
- [ ] Given a pooled base such as `explore
|
|
48
|
+
- [ ] Given a pooled base such as `explore` is spawned, When the plugin rewrites the spawn, Then the concrete model selected is a slot declared in `presets/free-tier.json`.
|
|
49
49
|
- [ ] Given a pool whose slots are all zero-cost, When N exploration spawns run, Then none of them is dispatched to a paid model.
|
|
50
50
|
|
|
51
51
|
### US-2: One knob selects the exploration/implementation model
|
|
@@ -125,9 +125,9 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
|
|
|
125
125
|
- [ ] Given a slot names a paid model, When the pool loads, Then the slot is accepted and routed to without a free-tier filter rejecting it.
|
|
126
126
|
- [ ] Given a model id contains a vendor prefix with slashes (e.g. `openrouter/nvidia/model:free`), When it is split into provider and model id, Then the split is on the first slash only and the id tail is preserved.
|
|
127
127
|
- [ ] Given more slots are declared than the pool allows, When the pool loads, Then the surplus entries are dropped with a warning naming the cap.
|
|
128
|
-
- [ ] Given a provider configured with **no** `whitelist` key, When the config hook binds a slot
|
|
129
|
-
- [ ] Given a provider the pool never names a slot
|
|
130
|
-
- [ ] Given a provider that already carries a `whitelist`, When the config hook binds a slot model
|
|
128
|
+
- [ ] Given a provider configured with **no** `whitelist` key, When the config hook binds a slot on that provider, Then no `whitelist` is written for it, because an absent whitelist already allows every model the provider offers.
|
|
129
|
+
- [ ] Given a provider the pool never names a slot on, When the config hook runs, Then that provider entry is left exactly as it was, with no `whitelist` key added.
|
|
130
|
+
- [ ] Given a provider that already carries a `whitelist`, When the config hook binds a slot model on it, Then the model id is appended and every entry the user wrote is preserved in order.
|
|
131
131
|
- [ ] Given any provider after the config hook has run, When its whitelist is inspected, Then it is either absent or non-empty, because an empty allowlist names no model at all and opencode deletes the provider outright.
|
|
132
132
|
|
|
133
133
|
`provider.<name>.whitelist` is an **allowlist**, not a to-do list: opencode
|
|
@@ -183,7 +183,8 @@ does not.
|
|
|
183
183
|
**Story ID(s) for tests:** US-11
|
|
184
184
|
**Acceptance criteria:**
|
|
185
185
|
- [ ] Given a session went busy and is now idle, When the idle event fires, Then a `done` notification is delivered.
|
|
186
|
-
- [ ] Given a session
|
|
186
|
+
- [ ] Given a session ends with an unrecognized error, When the error event fires, Then an `error` notification is delivered, it bypasses the duplicate-suppression cooldown, and it carries the error's own message.
|
|
187
|
+
- [ ] Given a recognized rate-limit or quota error, or a user abort, When a session error event fires, Then no failure notification is raised — the pool already handles the former and the user already knows the latter.
|
|
187
188
|
- [ ] Given a session that never went busy, When it goes idle, Then no notification is sent.
|
|
188
189
|
- [ ] Given a subagent session or a cartographer session, When it goes idle, Then it is suppressed.
|
|
189
190
|
- [ ] Given the notify helper exits non-zero or writes to stderr, When the result is evaluated, Then the delivery is reported as failed rather than sent.
|
|
@@ -246,6 +247,24 @@ does not.
|
|
|
246
247
|
- [ ] Given a payload containing shell metacharacters (`$(…)`, backticks, `;`, `&&`) or extra fields, When the receiver handles it, Then it is parsed as data only: nothing is executed and the argument list passed to `notify-send` is unchanged.
|
|
247
248
|
- [ ] Given a payload that cannot be understood at all, When it arrives, Then the receiver still exits 0 and pops up what it received, rather than failing the hop.
|
|
248
249
|
|
|
250
|
+
### US-26: A local notification is delivered on macOS, not only on Linux
|
|
251
|
+
**As a** user whose desktop is a Mac, **I want** a notification to actually appear when a session finishes, **so that** installing the package on macOS is not the same as getting no notifications at all.
|
|
252
|
+
|
|
253
|
+
**Story ID(s) for tests:** US-26
|
|
254
|
+
**Acceptance criteria:**
|
|
255
|
+
- [ ] 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.
|
|
256
|
+
- [ ] 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.
|
|
257
|
+
- [ ] 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.
|
|
258
|
+
- [ ] 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.
|
|
259
|
+
- [ ] 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.
|
|
260
|
+
- [ ] Given the CLI runs on macOS, When the send does not complete, Then it is still bounded by a timeout, exactly as on Linux.
|
|
261
|
+
- [ ] Given the repository's own shell scripts, When each is classified, Then the doc says which side of the hop it runs on — `bin/oc` and `bin/oc-notify` on the remote host that runs opencode, `bin/oc-notify-receiver` and `bin/oc-sync` on the desktop — and says plainly why the desktop pair is Linux-only (a `notify-send` channel behind a systemd user unit), so a Mac user is not sent to debug an ssh script that was never meant to run on their machine.
|
|
262
|
+
- [ ] 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`.
|
|
263
|
+
- [ ] 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.
|
|
264
|
+
- [ ] 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.
|
|
265
|
+
- [ ] 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.
|
|
266
|
+
- [ ] 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.
|
|
267
|
+
|
|
249
268
|
---
|
|
250
269
|
|
|
251
270
|
## Epic F — Adoption and safety
|
|
@@ -258,6 +277,12 @@ does not.
|
|
|
258
277
|
- [ ] 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.
|
|
259
278
|
- [ ] Given no configuration is present at all, When LaCode boots, Then it falls back to shipped defaults and still functions.
|
|
260
279
|
- [ ] Given the plugin package is installed, When its peer dependency is unmet, Then the requirement is declared rather than silently duplicated inside the package.
|
|
280
|
+
- [ ] 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.
|
|
281
|
+
- [ ] 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.
|
|
282
|
+
- [ ] 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.
|
|
283
|
+
- [ ] 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.
|
|
284
|
+
- [ ] Given a macOS user, When they look for what works on their machine, Then the README documents macOS: the plugin runs there, notifications are delivered through `osascript` or `terminal-notifier`, paths still come from `opencode debug paths` with no asserted macOS cache root, and the remote-host and tunnel scripts are marked Linux-only rather than implied to run there.
|
|
285
|
+
- [ ] Given a user whose cache holds an installed package, When the plugin's agents are nevertheless absent, Then the README distinguishes that from a failed install (an empty cache directory) and points at the loader workaround, so "installed" and "loaded" are never reported as the same fact.
|
|
261
286
|
|
|
262
287
|
### US-16: Merges, never clobbers
|
|
263
288
|
**As a** user with an existing opencode config, **I want** LaCode to add its agents/commands only where absent and never overwrite my own definitions, **so that** installing it cannot break my setup.
|
|
@@ -305,17 +330,17 @@ makes agents read it before they explore, then keeps it current on its own.
|
|
|
305
330
|
|
|
306
331
|
**Story ID(s) for tests:** US-19
|
|
307
332
|
**Acceptance criteria:**
|
|
308
|
-
- [ ] Given a config that defines none of LaCode's agents, When the config hook runs, Then a `context
|
|
309
|
-
- [ ] Given the injected agent set, When the slot variants are built, Then a hidden `context
|
|
333
|
+
- [ ] Given a config that defines none of LaCode's agents, When the config hook runs, Then a `context` subagent is injected, and it carries no model of its own, because the pool is the only thing that decides which model runs it.
|
|
334
|
+
- [ ] Given the injected agent set, When the slot variants are built, Then a hidden `context-<slot>` variant exists for every slot the pool can hand out, so a cartography run borrows shared capacity instead of a dedicated model.
|
|
310
335
|
- [ ] Given a user config with no `instructions` array, When the config hook runs, Then the standing context-map rule is injected, naming all five map files, telling the agent to check with `ls` and never the `glob` tool, to read only the file relevant to the task, and to start from the canonical entry points the map lists.
|
|
311
|
-
- [ ] Given the injected `context
|
|
336
|
+
- [ ] Given the injected `context`, When its prompt is inspected, Then it names the five files it owns, requires a short "Start here" list of canonical files per significant subsystem, and forbids file inventories — the map answers "what should I look at first?", never "tell me everything".
|
|
312
337
|
|
|
313
338
|
### US-20: The map stays current without being asked
|
|
314
339
|
**As a** user, **I want** the map refreshed automatically after a session that edited code, **so that** agents are never misled by stale documentation.
|
|
315
340
|
|
|
316
341
|
**Story ID(s) for tests:** US-20
|
|
317
342
|
**Acceptance criteria:**
|
|
318
|
-
- [ ] Given a file inside the project was edited, When the root session next goes idle, Then a `context
|
|
343
|
+
- [ ] Given a file inside the project was edited, When the root session next goes idle, Then a `context` session is created and prompted with the edited paths listed, rather than nothing happening.
|
|
319
344
|
- [ ] Given `.opencode/context/` does not exist, When the automatic run fires, Then it is told to bootstrap the map; given that the directory exists, Then it is told to make an incremental update.
|
|
320
345
|
- [ ] Given an idle event with no pending edit, or one from a subagent session, or one following an edit inside the map itself or inside a build directory, Then no cartographer is spawned.
|
|
321
346
|
- [ ] Given an automatic run borrowed a pool slot, When it completes, Then the slot is released and the cartographer session going idle does not spawn a second run.
|
|
@@ -327,15 +352,15 @@ makes agents read it before they explore, then keeps it current on its own.
|
|
|
327
352
|
|
|
328
353
|
**Story ID(s) for tests:** US-21
|
|
329
354
|
|
|
330
|
-
Enforcement is split deliberately, and the split matters: **what the permission table enforces** is that `context
|
|
355
|
+
Enforcement is split deliberately, and the split matters: **what the permission table enforces** is that `context` can write the map and that *no other agent can*; **what the prompt enforces** is that every other agent knows to report map-relevant findings instead of making them. The permission half is the load-bearing one — it holds regardless of what the model decides to do — but it cannot tell an agent what it *should* have done, so the instruction is not redundant and is not going away.
|
|
331
356
|
|
|
332
357
|
**Acceptance criteria:**
|
|
333
|
-
- [ ] Given the injected agent set, When every agent's `edit` permission is evaluated against a path inside `.opencode/context/`, Then `context
|
|
358
|
+
- [ ] Given the injected agent set, When every agent's `edit` permission is evaluated against a path inside `.opencode/context/`, Then `context` is the only agent whose winning rule for that path is `allow`, and every agent that may otherwise write files carries a `deny` on both spellings of the map path — so the map is closed to non-cartographers by permission, not only by instruction.
|
|
334
359
|
- [ ] Given an agent's `edit` rules are evaluated the way opencode evaluates them, When the narrow map `deny` is declared before a broad `"*": "allow"`, Then the broad rule wins and the agent is reported as a second writer; so the deny is required to be the LAST entry, and the shipped table must be declared in that order.
|
|
335
|
-
- [ ] Given the `context
|
|
360
|
+
- [ ] Given the `context` permission rule, When it is evaluated for a normally-rooted project and for a project rooted at `/`, Then both spellings of the map path are allowed, because opencode evaluates file permissions relative to the project root.
|
|
336
361
|
- [ ] Given a config whose one-writer rule allows neither spelling, When the writer rule is verified, Then it reports which rules it saw, that the map is not writable, and that both forms are required — instead of leaving a map that silently never updates.
|
|
337
362
|
- [ ] Given a config in which a non-cartographer agent would be granted the map, When the writer rule is verified, Then it reports that agent by name together with the rule that wins for it, and the report is silent once the deny is narrowed and placed last.
|
|
338
|
-
- [ ] Given an agent other than `context
|
|
363
|
+
- [ ] Given an agent other than `context` finishes work, When the standing rule reaches it, Then it is told never to edit the map itself and to report what belongs in it instead, so that a denied agent knows what to do rather than only that it was refused.
|
|
339
364
|
- [ ] Given `edit` is denied for the map, When the same agents read the map, Then reading is unaffected: the rule narrows the `edit` tool only, so the map stays readable by exactly the agents that are told to check it.
|
|
340
365
|
|
|
341
366
|
### US-22: On-demand map operations
|
|
@@ -343,7 +368,7 @@ Enforcement is split deliberately, and the split matters: **what the permission
|
|
|
343
368
|
|
|
344
369
|
**Story ID(s) for tests:** US-22
|
|
345
370
|
**Acceptance criteria:**
|
|
346
|
-
- [ ] Given a config that defines none of LaCode's commands, When the config hook runs, Then `/context-init`, `/context-update` and `/context-review` are injected, each targeting `context
|
|
371
|
+
- [ ] Given a config that defines none of LaCode's commands, When the config hook runs, Then `/context-init`, `/context-update` and `/context-review` are injected, each targeting `context` as a subtask.
|
|
347
372
|
- [ ] Given an injected command template, When opencode substitutes `$ARGUMENTS`, Then `/context-update` carries the user's focus text in the placeholder's place and the procedure around it is unchanged.
|
|
348
373
|
- [ ] Given the three commands have been injected, When their templates are inspected, Then each names the procedure it delegates to (bootstrap, incremental update, or review) and none carries an unsubstituted placeholder other than `/context-update`'s single `$ARGUMENTS`.
|
|
349
374
|
- [ ] Given the user's config already defines one of the three commands, When the config hook runs, Then that command is left byte-identical while the other two are still added.
|
|
@@ -378,6 +403,7 @@ table was filled in.
|
|
|
378
403
|
| US-13 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/notify-test.mjs`, `tests/oc-test.mjs` |
|
|
379
404
|
| US-14 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/oc-test.mjs` |
|
|
380
405
|
| US-18 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/oc-test.mjs` |
|
|
406
|
+
| US-26 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/oc-test.mjs` |
|
|
381
407
|
| US-15 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
|
382
408
|
| US-16 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
|
383
409
|
| US-17 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=22.18"
|
|
29
29
|
},
|
|
30
|
-
"
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"exports": {
|
|
31
32
|
".": "./dist/index.js",
|
|
33
|
+
"./server": "./dist/index.js",
|
|
32
34
|
"./pool": "./dist/pool.js",
|
|
33
35
|
"./agents": "./dist/agents.js",
|
|
34
36
|
"./commands": "./dist/commands.js",
|
package/presets/free-tier.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"plugin options -> ~/.config/lacode/pool.json -> this file -> a built-in fallback.",
|
|
5
5
|
"",
|
|
6
6
|
"src/lib/pool.ts reads it at config time and rewrites every pooled variant agent",
|
|
7
|
-
"(explore-
|
|
7
|
+
"(explore-N, implement-N, context-N) plus the provider whitelists",
|
|
8
8
|
"to match, so no model is written down next to an agent anywhere.",
|
|
9
9
|
"Slot N is the same model for every base agent, which is what makes load shared.",
|
|
10
10
|
"",
|