@lawaty/lacode 0.7.1 → 0.9.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 +224 -2
- package/README.md +24 -6
- package/dist/agents.js +100 -14
- package/dist/commands.js +10 -5
- package/dist/index.js +740 -77
- package/dist/pool.js +153 -12
- package/docs/CONTEXT.md +21 -6
- package/docs/OMNIROUTE.md +17 -15
- package/docs/USER-STORIES.md +2 -2
- package/package.json +1 -1
- package/presets/README.md +8 -8
- package/presets/free-tier.json +26 -9
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,228 @@ All notable changes to LaCode are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
Versions come from git release commits; the npm version is bumped in the same
|
|
8
8
|
commit as the changelog entry it describes.
|
|
9
|
-
|
|
10
9
|
## [Unreleased]
|
|
11
10
|
|
|
11
|
+
## [0.9.0] — the cartographer keeps an active backlog, and the model stops naming slot variants
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **The cartographer keeps an active backlog, and the plugin enforces its
|
|
16
|
+
window.** `.opencode/context/active.md` is a sixth file in the map directory:
|
|
17
|
+
the work in flight, grouped by the ISO week each task was opened in. A task
|
|
18
|
+
that is not marked `[x]` is open, and an open task is never removed however
|
|
19
|
+
old it is; a task that is finished and dated is kept for a week after it was
|
|
20
|
+
finished and is then dropped. The file is therefore a rolling window rather
|
|
21
|
+
than a history — the question it answers is "what is open, and what just
|
|
22
|
+
closed", not "what has ever been done". A backlog that only grows is a
|
|
23
|
+
backlog nobody reads.
|
|
24
|
+
|
|
25
|
+
The window is applied by `src/lib/active-backlog.ts` and by the plugin, not
|
|
26
|
+
by the cartographer's prompt. Deciding when a task has aged out is
|
|
27
|
+
arithmetic, and arithmetic belongs where a test can pin the boundary: a rule
|
|
28
|
+
a model applies is a rule the model applies when it feels like it — one run
|
|
29
|
+
prunes and the next does not, and nothing anywhere reports the difference,
|
|
30
|
+
so the failure is silent and the backlog just grows. The plugin normalizes
|
|
31
|
+
the file on every edit of it and once when the session starts, so a hand
|
|
32
|
+
edit is grouped and pruned whether the cartographer ran or not, and the file
|
|
33
|
+
is rewritten only when its canonical form differs from what is on disk, so
|
|
34
|
+
the write cannot loop. The boundary is strict: a task finished exactly seven
|
|
35
|
+
days ago is still that week's news, and is kept.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **A slot variant is no longer in the agent list the model is shown, so the model
|
|
40
|
+
stops naming slots by hand.** opencode builds the system prompt's "Available
|
|
41
|
+
agent types" block by filtering the agent registry on `mode !== "primary"` and on
|
|
42
|
+
the caller's task permission. It never reads `hidden`: that flag is honoured only
|
|
43
|
+
by the TUI, the @-mention pickers, the "agent not found" message and default-agent
|
|
44
|
+
resolution. Every pooled variant was therefore listed to the model under its own
|
|
45
|
+
name — "explore-1: Hidden pool variant explore-1" — and a model that can see
|
|
46
|
+
twelve concrete slot names reaches for them, so the root agent kept spawning
|
|
47
|
+
`explore-1` and `implement-4` instead of the bare base, and the pool refused
|
|
48
|
+
every one. The refusal was right and the leak that provoked it was not: naming a
|
|
49
|
+
variant is how a caller would pick the provider and skip the rotation and the
|
|
50
|
+
slot's ceiling, so it cannot be allowed to succeed. The variants now carry
|
|
51
|
+
`mode: "primary"`, the one field that filter respects, which takes them out of
|
|
52
|
+
the agent list while leaving them spawnable by name — the task tool resolves an
|
|
53
|
+
agent by name and never reads its mode — and out of the TUI, where `hidden`
|
|
54
|
+
already kept them. The bare bases are untouched and stay in the list, so the
|
|
55
|
+
model has nothing but the bases to choose from.
|
|
56
|
+
|
|
57
|
+
- **A refusal no longer names the variant it refused, and the instructions no
|
|
58
|
+
longer list the names they forbid.** The refusal was the strongest remaining
|
|
59
|
+
channel. It is the tool result the model reads at the exact moment it is
|
|
60
|
+
choosing what to spawn next, and it used to repeat the name that had just been
|
|
61
|
+
denied — so the denial was the thing that taught the name, and the model copied
|
|
62
|
+
it back. It now names the base instead, which is what the model should type.
|
|
63
|
+
The same reasoning applies to the rules themselves: "never name a `-N` variant
|
|
64
|
+
(`explore-1`..`explore-4`, `implement-1`..)" put eight concrete agent names in
|
|
65
|
+
front of the model in the sentence that told it what to spawn. A rule that
|
|
66
|
+
lists the names it forbids teaches the names; the rule now states the rule
|
|
67
|
+
without an example, in the shipped agent prompts, the project prompts and the
|
|
68
|
+
global delegation rules.
|
|
69
|
+
|
|
70
|
+
- **The cartographer no longer leaves empty sessions in the session list.** The
|
|
71
|
+
session was created before the run started, and only the turn's own success
|
|
72
|
+
and failure paths retired it. Anything that threw in between — a slot that
|
|
73
|
+
could not be picked, a claim that could not be written, a prompt that failed
|
|
74
|
+
before it returned — stranded a session with no messages in it: an entry in
|
|
75
|
+
the user's list that opens as nothing at all. Retirement now covers the whole
|
|
76
|
+
gap between the create and the prompt, and the delete is *verified* rather
|
|
77
|
+
than assumed — it is read back afterwards, retried once, and a failure is
|
|
78
|
+
logged instead of swallowed. (Archiving is not a way out: opencode's list is
|
|
79
|
+
served from the session rows themselves, so a session with `time_archived` set
|
|
80
|
+
is still listed. Only a delete removes an entry.)
|
|
81
|
+
- **A finished cartographer run retires its session and keeps the transcript**
|
|
82
|
+
was already true, and stays true: the transcript is written first and the
|
|
83
|
+
session deleted second, because the transcript is the only record of what the
|
|
84
|
+
run did. The plugin's own last 20 runs are kept in full.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- **The bundled default pool no longer ships a model its provider has withdrawn.**
|
|
89
|
+
`presets/free-tier.json` named `opencode/space-bunny-free` as its first slot at
|
|
90
|
+
weight 3 — the slot that took the most turns. The opencode provider withdrew that
|
|
91
|
+
model, and because it was the default, every new session then died at step 0 with
|
|
92
|
+
`ProviderModelNotFoundError: Model not found: opencode/space-bunny-free`. Nothing
|
|
93
|
+
in the package could tell: the pool warns about a slot that is missing from the
|
|
94
|
+
config, not about a slot whose model the provider no longer serves, and the root
|
|
95
|
+
agent's own model lives in the user's config rather than here.
|
|
96
|
+
|
|
97
|
+
- **`scripts/retire-cartographer-sessions.mjs` clears the sessions the feature
|
|
98
|
+
did not reach.** Retirement landed after 250 runs had already piled up across
|
|
99
|
+
the machine's projects, and those were never cleaned. The tool talks to the
|
|
100
|
+
opencode server over its HTTP API — the same interface the plugin uses — and
|
|
101
|
+
writes a compact record of each retired session to
|
|
102
|
+
`~/.local/share/opencode/lacode/cartographer-archive/` before deleting it, so
|
|
103
|
+
nothing is lost. `--dry-run` shows what it would do.
|
|
104
|
+
|
|
105
|
+
## [0.8.0] — cooldowns are probed instead of trusted, and the instructions agent can read the map
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- **Benched models are now checked by using them.** A model that rate-limits or runs
|
|
110
|
+
out of quota is benched until its window ends, and that window is computed
|
|
111
|
+
backoff rather than a promise from the provider — a quota-shaped one reaches six
|
|
112
|
+
hours, long enough for a provider that recovered, or whose quota was reset by hand
|
|
113
|
+
in its own console, to stay excluded while every delegated spawn queues behind the
|
|
114
|
+
survivors. `pool_recheck` asks instead of guessing: one real request on that exact
|
|
115
|
+
model, with a one-word prompt and its own system string so the check costs
|
|
116
|
+
handfuls of tokens rather than shipping a subagent's instructions. A model that
|
|
117
|
+
answers is re-admitted at once — no restart, no reload, because `readLimits`
|
|
118
|
+
rebuilds the cooldown state from disk on every routing decision — and one that is
|
|
119
|
+
still limited keeps exactly the window it had. `src/lib/probe.ts` owns the probe;
|
|
120
|
+
it runs every fifteen minutes while anything is cooling (`probeMs`, `0` disables)
|
|
121
|
+
and on demand through the tool, one model at a time, silent when nothing changed
|
|
122
|
+
and reported as a toast plus a log line when something did.
|
|
123
|
+
|
|
124
|
+
Three rules keep a health check from becoming the thing it is checking for. A
|
|
125
|
+
probe's own failure is never counted as a strike, or repeated checks would drive
|
|
126
|
+
the window to the six-hour cap and no probe could ever pass; the only window it
|
|
127
|
+
may change is one the provider itself stated, which is the provider correcting a
|
|
128
|
+
guess. A probe's 429 never benches a whole provider — one request is evidence
|
|
129
|
+
about one request. And a probe is a root session like any other, so it is marked
|
|
130
|
+
in the shared pool state to keep the cartographer's idle trigger from starting a
|
|
131
|
+
context run on its way past.
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
|
|
135
|
+
- **A finished cartographer run leaves the session list.** opencode's list shows
|
|
136
|
+
root sessions, and the cartographer was created with no `parentID` — so every
|
|
137
|
+
automatic run landed in the user's main list and buried real work (112 of them
|
|
138
|
+
on one machine). Nesting it under the triggering session is unsafe:
|
|
139
|
+
`pool.bindChild` adopts a fresh child into any claim the parent holds without a
|
|
140
|
+
target, which is exactly what a root has while its own explore tasks are in
|
|
141
|
+
flight, and the reaper would abort the cartographer as though it were the hung
|
|
142
|
+
task. Archiving is unavailable in 1.18.34 — no archive method, and `session.list`
|
|
143
|
+
takes only `directory`. So the run now exports its transcript to
|
|
144
|
+
`~/.local/share/opencode/lacode/cartographer/` (last 20 kept, override with
|
|
145
|
+
`LACODE_TRANSCRIPT_DIR`) and deletes the session, on the failure path as well as
|
|
146
|
+
the success path, since a retrying cartographer is what fills a list fastest.
|
|
147
|
+
Its notifications were already suppressed.
|
|
148
|
+
|
|
149
|
+
- **A slot's concurrency ceiling is now twice its weight.** Weight still sets how
|
|
150
|
+
many turns a slot takes per weighted round-robin rotation, but the ceiling is
|
|
151
|
+
`2 × weight`, so one full rotation fills only half the pool and the run is walked
|
|
152
|
+
twice before every slot is full. `pool_status` prints the ceiling as `load=n/2w`,
|
|
153
|
+
and a weight-3 slot may now hold 6 concurrent sessions.
|
|
154
|
+
|
|
155
|
+
- **The `instructions` agent can read the context map.** Its `read` grant was
|
|
156
|
+
scoped to `AGENTS.md` alone, so an `/agent-instructions` directive was applied
|
|
157
|
+
without sight of what the project has already settled — the agent could restate
|
|
158
|
+
a fact `conventions.md` records, or contradict one it had never read. The map is
|
|
159
|
+
now readable through both path spellings (`.opencode/context/**` and
|
|
160
|
+
`*/.opencode/context/**`), declared after the broad deny so the grant is not
|
|
161
|
+
inert. The **write** grant is unchanged and still reaches `AGENTS.md` only: the
|
|
162
|
+
map stays read-only to this agent, `context` remains its only writer, and the
|
|
163
|
+
writer guard — which reads `edit` alone — needs no new exemption.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- **The pool no longer grows without bound.** The per-session `activity` and
|
|
168
|
+
`children` maps were written on every message event and every `session.created`
|
|
169
|
+
and never pruned, so a long-running server accumulated an entry per session for
|
|
170
|
+
the life of the process. Both are now swept by the reaper and on every spawn, and
|
|
171
|
+
the notifier's per-session bookkeeping (`active` / `titles`) is capped at 500.
|
|
172
|
+
|
|
173
|
+
- **The routing log no longer prints a slot variant's name.** It read
|
|
174
|
+
`routed explore -> explore-2`, which is read by the very agent being routed and
|
|
175
|
+
hands it a valid-looking agent name in the exact place it decides what to spawn.
|
|
176
|
+
The refusals that followed were the model copying those names back —
|
|
177
|
+
`implement-4` 23 times, `implement-2` 20, `explore-2` 12, roughly one per routed
|
|
178
|
+
spawn. The line now names the slot and the model, which is the same diagnostic
|
|
179
|
+
value with nothing for the model to echo. The refusal itself is unchanged.
|
|
180
|
+
|
|
181
|
+
- **The notification helper spawn is bounded.** Question, permission and error
|
|
182
|
+
outcomes bypass the notification cooldown on purpose — a session blocked on a
|
|
183
|
+
prompt the user was never told about is the silent stall this plugin exists to
|
|
184
|
+
prevent — so a failure storm could start one detached helper per event, each
|
|
185
|
+
`bin/oc-notify` → `bin/oc` → an ssh attempt, with nothing reaping them. The
|
|
186
|
+
bound is on the transport, not on the decision: at most 4 helpers in flight,
|
|
187
|
+
and one that has not answered in 30s is killed. Which notifications fire is
|
|
188
|
+
unchanged.
|
|
189
|
+
|
|
190
|
+
- **A provider-stated reset can no longer bench a model for days.** A stated
|
|
191
|
+
window ("Resets in 35min") is still trusted over the computed backoff, but the
|
|
192
|
+
parse was unbounded, so one malformed upstream number removed a slot from
|
|
193
|
+
rotation indefinitely: an `account_rate_limit` carrying `retry in 329917s`
|
|
194
|
+
(~3.8 days) cooled a model for **91.6 hours**, long past the window it was
|
|
195
|
+
waiting for, and every delegated spawn queued behind the survivors. A stated
|
|
196
|
+
window is now discarded past `STATED_COOLDOWN_MAX_MS` (6h, the quota cap) so
|
|
197
|
+
the caller falls back to the ordinary strike backoff instead of trusting a
|
|
198
|
+
number it cannot believe. A limit already on disk is unaffected — delete its
|
|
199
|
+
`limit.<model>.json` to re-admit the model now.
|
|
200
|
+
|
|
201
|
+
- **`"Resets in 2hr 17min"` is no longer read as 2h00.** The unit alternations
|
|
202
|
+
were ordered `h|hr|hour`, so `2hr 17min` matched the bare `h`, left `r 17min`
|
|
203
|
+
unparsed and silently dropped the minutes — a window too *short* by exactly the
|
|
204
|
+
part it discarded, despite the docstring claiming that spelling was handled.
|
|
205
|
+
Alternations are now longest-first (`hour|hr|h`, `minute|min|m`, `second|sec|s`).
|
|
206
|
+
|
|
207
|
+
- **The OmniRoute sticky-assignment table is bounded.** It is keyed by session and
|
|
208
|
+
only a *failure* deleted an entry, so it grew with every session the process ever
|
|
209
|
+
served rather than with the sessions in flight. It is now capped at 500,
|
|
210
|
+
evicting oldest-first, and a `select()` that returns an existing assignment
|
|
211
|
+
re-inserts it — previously a long-running session kept its original position and
|
|
212
|
+
was evicted as "oldest" while still actively in use.
|
|
213
|
+
|
|
214
|
+
- **A pinned agent whose model is cooling now says so.** `basic` and `expert`
|
|
215
|
+
hard-code a model and are not pooled, so a spawn of either falls through routing
|
|
216
|
+
with nothing logged: when that provider was benched the escalation agent simply
|
|
217
|
+
read as "cancelled". The pool now warns once, naming the model, the reason and
|
|
218
|
+
the fact that only `explore`/`implement`/`context` can fail over.
|
|
219
|
+
|
|
220
|
+
- **The shared state directory now cleans itself without needing traffic.** Dead
|
|
221
|
+
claims files and lapsed cooldowns were reclaimed only as a side effect of
|
|
222
|
+
`snapshot()`, which runs when something spawns — so an idle machine, or one
|
|
223
|
+
whose process was killed before its next spawn, carried a dead process's claims
|
|
224
|
+
file and every lapsed cooldown indefinitely, and those files are read on every
|
|
225
|
+
routing decision. `sweepStateDir` applies the same policy on an hourly clock,
|
|
226
|
+
and once at startup so the previous run's debris goes immediately. It never
|
|
227
|
+
touches this process's own claims file, `cursor.json`, a running cooldown, or a
|
|
228
|
+
lapsed cooldown still inside its strike-decay window (deleting that would reset
|
|
229
|
+
the strike count and restart the backoff at 1).
|
|
230
|
+
|
|
12
231
|
## [0.7.1] — /agent-instructions creates AGENTS.md on first use
|
|
13
232
|
|
|
14
233
|
### Changed
|
|
@@ -245,11 +464,14 @@ experimental; `latest` remains 0.4.2.
|
|
|
245
464
|
|
|
246
465
|
- First published release: pooled subagents over a shared set of models.
|
|
247
466
|
|
|
248
|
-
[Unreleased]: https://github.com/lawaty/lacode/compare/v0.
|
|
467
|
+
[Unreleased]: https://github.com/lawaty/lacode/compare/v0.9.0...HEAD
|
|
468
|
+
[0.9.0]: https://github.com/lawaty/lacode/compare/v0.8.0...v0.9.0
|
|
469
|
+
[0.8.0]: https://github.com/lawaty/lacode/compare/v0.7.1...v0.8.0
|
|
249
470
|
[0.7.1]: https://github.com/lawaty/lacode/compare/v0.7.0...v0.7.1
|
|
250
471
|
[0.7.0]: https://github.com/lawaty/lacode/compare/v0.6.0...v0.7.0
|
|
251
472
|
[0.6.0]: https://github.com/lawaty/lacode/compare/v0.5.0...v0.6.0
|
|
252
473
|
[0.5.0]: https://github.com/lawaty/lacode/compare/v0.4.2...v0.5.0
|
|
474
|
+
[0.4.2]: https://github.com/lawaty/lacode/compare/v0.4.1...v0.4.2
|
|
253
475
|
[0.4.1]: https://github.com/lawaty/lacode/compare/v0.4.0...v0.4.1
|
|
254
476
|
[0.4.0]: https://github.com/lawaty/lacode/compare/v0.3.0...v0.4.0
|
|
255
477
|
[0.3.0]: https://github.com/lawaty/lacode/compare/v0.2.2...v0.3.0
|
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ option is optional:
|
|
|
125
125
|
| `models` | — | An inline slot list. Highest-priority source. |
|
|
126
126
|
| `modelsFile` | — | An explicit slot file; short-circuits the [resolution chain](docs/CONFIGURATION.md#pool-preset-resolution). |
|
|
127
127
|
| `reapMs` | `30000` | Hang-reaper interval. |
|
|
128
|
+
| `probeMs` | `900000` | How often benched models are checked by actually using them. `0` turns the periodic check off and leaves the `pool_recheck` tool as the only way to ask. |
|
|
128
129
|
| `selfUpdate` | `true` | Check for a newer release at most once per ~6h and install it in the background; it applies on the next opencode start. `false` — or `LACODE_NO_SELFUPDATE=1` — turns the check off. |
|
|
129
130
|
| `sound` | `true` | Play a short tone with each notification, one per outcome, so you can tell a finished session from a question or a permission prompt without reading the popup. It is opt-out: `false` — or `LACODE_SOUND=0` (`false`, `off`, `no` work too, and beat the option) — turns the sound off, and anything else, a typo included, leaves it on. |
|
|
130
131
|
| `omniroute` | — | Optional [OmniRoute](docs/OMNIROUTE.md) gateway integration. Absent or `{ enabled: false }` changes nothing. |
|
|
@@ -185,6 +186,19 @@ only thing that works: naming a variant lets the caller choose the provider
|
|
|
185
186
|
directly, which is exactly what the rotation and the per-slot ceiling exist to
|
|
186
187
|
prevent, so it is refused.
|
|
187
188
|
|
|
189
|
+
A model that rate-limits or runs out of quota is benched and taken out of the
|
|
190
|
+
rotation until its window ends, so one sick provider does not take the pool with
|
|
191
|
+
it. Those windows are computed backoff rather than a promise from the provider,
|
|
192
|
+
and a quota-shaped one can run for hours — long enough for a provider that has
|
|
193
|
+
since recovered, or whose quota you reset in its own console, to stay excluded
|
|
194
|
+
while every delegated spawn queues behind the survivors. So LaCode asks instead
|
|
195
|
+
of guessing: every fifteen minutes, and on demand via the `pool_recheck` tool, it
|
|
196
|
+
probes each benched model with one real request on that exact model. A model that
|
|
197
|
+
answers is re-admitted immediately — nothing restarts, nothing reloads — and a
|
|
198
|
+
model that is still limited keeps exactly the window it had, because a check must
|
|
199
|
+
never be the thing that extends the wait. A change is reported as a toast and a
|
|
200
|
+
log line; a pass that finds nothing changed says nothing at all.
|
|
201
|
+
|
|
188
202
|
Six commands: `/context-init`, `/context-update`, `/context-review`,
|
|
189
203
|
`/lacode-doctor`, `/agent-instructions` and `/pool`. Everything is added **only
|
|
190
204
|
when your config does not already
|
|
@@ -199,8 +213,9 @@ deliberately *not* injected, are in
|
|
|
199
213
|
Agents are stateless between tasks, and exploring your repository is the most
|
|
200
214
|
expensive part of any task — so every agent pays for it again, re-deriving the
|
|
201
215
|
same layout, entry points and boundaries from scratch. LaCode keeps a short,
|
|
202
|
-
maintained map of the repository in `.opencode/context/` (five files:
|
|
203
|
-
`architecture`, `contexts`, `conventions`, `workflows`, `decisions
|
|
216
|
+
maintained map of the repository in `.opencode/context/` (five navigation files:
|
|
217
|
+
`architecture`, `contexts`, `conventions`, `workflows`, `decisions`, plus
|
|
218
|
+
`active.md` — the active backlog) and injects
|
|
204
219
|
a standing rule that every agent reads the relevant file before exploring and
|
|
205
220
|
starts from the canonical entry points it lists. The map holds conclusions, not
|
|
206
221
|
inventories: it answers "what should I look at first?" and nothing else, which
|
|
@@ -237,7 +252,7 @@ in [docs/CONTEXT.md](docs/CONTEXT.md).
|
|
|
237
252
|
The project's root `AGENTS.md` is your own statement of intent: it records
|
|
238
253
|
what you want worked on and how you want it approached. It says nothing about
|
|
239
254
|
where code lives, so it can never contradict the code, and within that domain
|
|
240
|
-
it outranks the
|
|
255
|
+
it outranks the generated context files and any agent's inference about what
|
|
241
256
|
matters right now.
|
|
242
257
|
|
|
243
258
|
The file is per-clone and not committed, so it is yours rather than shared
|
|
@@ -258,8 +273,11 @@ is the hidden `instructions` agent, reachable solely through the
|
|
|
258
273
|
instruction you have given to `AGENTS.md`, then reports what it changed. It is
|
|
259
274
|
a normal agent turn, not a dead append: tell it what you want and review the
|
|
260
275
|
result. The `instructions` agent is the one agent permitted to edit that file,
|
|
261
|
-
its grant is scoped to `AGENTS.md` alone, and no other agent can spawn it —
|
|
262
|
-
the command is the only route in.
|
|
276
|
+
its write grant is scoped to `AGENTS.md` alone, and no other agent can spawn it —
|
|
277
|
+
the command is the only route in. It may also **read** `.opencode/context/`, so a
|
|
278
|
+
directive does not restate a fact the map has already settled or contradict one it
|
|
279
|
+
has not read; the map stays read-only to it, and the `context` agent remains its
|
|
280
|
+
only writer.
|
|
263
281
|
|
|
264
282
|
## Cost
|
|
265
283
|
|
|
@@ -352,7 +370,7 @@ want it.
|
|
|
352
370
|
| [Install and troubleshooting](docs/INSTALL.md) | Where opencode puts the package, what to do when the agents are missing, the version matrix, macOS differences, and the `lacode` CLI. |
|
|
353
371
|
| [Configuration reference](docs/CONFIGURATION.md) | Every plugin option, how the pool resolves its slots, per-agent tool grants, and exactly what gets written into your config. |
|
|
354
372
|
| [OmniRoute gateway](docs/OMNIROUTE.md) | The optional local gateway integration: setup, full option set, a latency-measured free combo, and its troubleshooting table. |
|
|
355
|
-
| [Context map](docs/CONTEXT.md) | The mechanism behind `.opencode/context/` — the one-writer permission rule, the
|
|
373
|
+
| [Context map](docs/CONTEXT.md) | The mechanism behind `.opencode/context/` — the one-writer permission rule, the files, and what the log lines mean. |
|
|
356
374
|
| [Remote hosts and notifications](docs/TUNNEL.md) | The SSH notification hop, payload format, receiver contract, and holding the forward open. |
|
|
357
375
|
| [Benchmarks](docs/BENCHMARKS.md) | How the cost harness works, and why it deliberately publishes no savings number. |
|
|
358
376
|
| [User stories](docs/USER-STORIES.md) | The behavioural contracts behind all of the above, written so a test can assert them. |
|
package/dist/agents.js
CHANGED
|
@@ -44,6 +44,7 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
44
44
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
45
45
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
46
46
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
47
|
+
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
47
48
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
48
49
|
|
|
49
50
|
// src/lib/agent-instructions.ts
|
|
@@ -89,8 +90,8 @@ failing, or a runtime message saying no slot is available \u2014 that is not a r
|
|
|
89
90
|
to do the work yourself, and not a reason to stop and ask.
|
|
90
91
|
|
|
91
92
|
**Retry.** A refused or cooling slot is transient and costs you nothing but a
|
|
92
|
-
turn. Spawn the bare base again. Do not escalate a
|
|
93
|
-
|
|
93
|
+
turn. Spawn the bare base again. Do not escalate to a numbered variant after a
|
|
94
|
+
refusal \u2014 the refusal means the pool has a routing problem, not that you
|
|
94
95
|
picked the wrong number.
|
|
95
96
|
|
|
96
97
|
Retry is always available and never wrong, which is why it is the default. But
|
|
@@ -449,7 +450,7 @@ if the blocker is understanding rather than execution, report that instead.`,
|
|
|
449
450
|
}
|
|
450
451
|
},
|
|
451
452
|
"context": {
|
|
452
|
-
"description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
|
|
453
|
+
"description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents, and keeps .opencode/context/active.md \u2014 the active backlog of work in flight, grouped by week. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
|
|
453
454
|
"mode": "subagent",
|
|
454
455
|
"temperature": 0.1,
|
|
455
456
|
prompt: `# Role
|
|
@@ -465,7 +466,7 @@ map is a derived index of it.
|
|
|
465
466
|
|
|
466
467
|
# Owned files
|
|
467
468
|
|
|
468
|
-
You own exactly these
|
|
469
|
+
You own exactly these files and nothing else:
|
|
469
470
|
|
|
470
471
|
- \`.opencode/context/architecture.md\` \u2014 high-level architecture, major layers,
|
|
471
472
|
boundaries, important entry points, dependency rules. Target 50\u2013150 lines.
|
|
@@ -480,6 +481,9 @@ You own exactly these five files and nothing else:
|
|
|
480
481
|
- \`.opencode/context/decisions.md\` \u2014 architectural decisions discovered
|
|
481
482
|
during work that are not already recorded in the project's own decision
|
|
482
483
|
log. Concise; variable length.
|
|
484
|
+
- \`.opencode/context/active.md\` \u2014 the active backlog: work in flight, grouped
|
|
485
|
+
by the week it was opened. Not part of the navigation map; it is described
|
|
486
|
+
under "Task backlog" below.
|
|
483
487
|
|
|
484
488
|
# Current-state only
|
|
485
489
|
|
|
@@ -519,7 +523,8 @@ Explore progressively and stop as soon as you have a usable map:
|
|
|
519
523
|
You are mapping a city, not auditing every house. Do not attempt to
|
|
520
524
|
understand every file.
|
|
521
525
|
|
|
522
|
-
Then write the five files.
|
|
526
|
+
Then write the five files. Also create \`.opencode/context/active.md\` with its
|
|
527
|
+
heading and no tasks. For each significant subsystem, give a short
|
|
523
528
|
"Start here" list \u2014 at most about five canonical files, each with a one-phrase
|
|
524
529
|
reason. This is the single most valuable thing you produce: where an agent
|
|
525
530
|
should open its editor next.
|
|
@@ -543,7 +548,7 @@ Trigger: \`.opencode/context/\` exists and work has just been done here.
|
|
|
543
548
|
- an architectural decision
|
|
544
549
|
- a new dependency between subsystems
|
|
545
550
|
- a renamed or relocated subsystem
|
|
546
|
-
|
|
551
|
+
- a previously undocumented invariant
|
|
547
552
|
|
|
548
553
|
Do not update documentation merely because source files changed. Do not
|
|
549
554
|
rewrite files wholesale. Do not make timestamp-only edits. Do not add a fact
|
|
@@ -551,6 +556,38 @@ that is already stated in another context file \u2014 one fact has one home, and
|
|
|
551
556
|
the other files link to it. If nothing material changed, make no edits and
|
|
552
557
|
report "no update needed".
|
|
553
558
|
|
|
559
|
+
Also keep \`.opencode/context/active.md\` current: record work that is in
|
|
560
|
+
flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.
|
|
561
|
+
|
|
562
|
+
# Task backlog
|
|
563
|
+
|
|
564
|
+
\`.opencode/context/active.md\` holds the work in flight. It is a rolling
|
|
565
|
+
window, not a history: it answers "what is open, and what just finished".
|
|
566
|
+
|
|
567
|
+
Format \u2014 one line per task, under the ISO week the task was opened in:
|
|
568
|
+
|
|
569
|
+
## 2026-W43
|
|
570
|
+
|
|
571
|
+
- [ ] Ship the release notes \u2014 opened 2026-W43
|
|
572
|
+
- [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
|
|
573
|
+
|
|
574
|
+
The rules:
|
|
575
|
+
|
|
576
|
+
- A task that is not marked \`[x]\` is OPEN. An open task is never removed,
|
|
577
|
+
however old it is.
|
|
578
|
+
- A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\`
|
|
579
|
+
date. Record the date it actually finished, as \`YYYY-MM-DD\`.
|
|
580
|
+
- The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
|
|
581
|
+
- A finished task is dropped once it is more than a week old. The plugin
|
|
582
|
+
enforces that: record the finished task with its date and let the window
|
|
583
|
+
close on its own. Do not delete old finished tasks by hand, and do not
|
|
584
|
+
keep a finished task back to preserve it \u2014 the file is meant to stay
|
|
585
|
+
small.
|
|
586
|
+
- Never invent a task. Record work that actually happened, and mark a task
|
|
587
|
+
finished only once it is finished.
|
|
588
|
+
- Keep the file inside the window. If the backlog has grown past a handful
|
|
589
|
+
of weeks, that is a signal to close finished tasks, not to add sections.
|
|
590
|
+
|
|
554
591
|
# Minimal exploration
|
|
555
592
|
|
|
556
593
|
Default shape:
|
|
@@ -637,7 +674,7 @@ the lowest-value ones, and link out to authoritative project docs.
|
|
|
637
674
|
|
|
638
675
|
Trigger: an explicit review (see the \`context-review\` command).
|
|
639
676
|
|
|
640
|
-
Audit the
|
|
677
|
+
Audit the map files and the backlog for:
|
|
641
678
|
|
|
642
679
|
- stale information \u2014 verify claims against the code
|
|
643
680
|
- contradictions between files, or between the map and the code
|
|
@@ -645,10 +682,12 @@ Audit the five files for:
|
|
|
645
682
|
- duplicated information \u2014 one canonical place per fact
|
|
646
683
|
- missing canonical entry points for significant subsystems
|
|
647
684
|
- unsupported assumptions \u2014 anything unmarked that you cannot verify
|
|
685
|
+
- backlog tasks that are finished but carry no date \u2014 the retention window
|
|
686
|
+
cannot age those out, so they stay forever
|
|
648
687
|
|
|
649
|
-
Fix problems only in the
|
|
650
|
-
|
|
651
|
-
|
|
688
|
+
Fix problems only in the files you own. Never modify source code during a
|
|
689
|
+
review. Report what you found, what you fixed, and what you could not
|
|
690
|
+
verify.
|
|
652
691
|
|
|
653
692
|
# Reporting
|
|
654
693
|
|
|
@@ -753,20 +792,46 @@ on that.
|
|
|
753
792
|
write only what the client asked for.
|
|
754
793
|
- Say plainly in your report that you created the file.
|
|
755
794
|
|
|
795
|
+
# Before you write
|
|
796
|
+
|
|
797
|
+
Read \`.opencode/context/\` first when it is there \u2014 \`architecture.md\`,
|
|
798
|
+
\`conventions.md\` or \`contexts.md\`, whichever bears on the directive \u2014 so you do
|
|
799
|
+
not record a fact the project has already settled, and do not contradict one you
|
|
800
|
+
do not know yet. Read only what is relevant, and carry on if it is missing.
|
|
801
|
+
|
|
802
|
+
That map is READ-ONLY to you: the \`context\` agent is its only writer. If the
|
|
803
|
+
directive contradicts it, say so plainly in your report and still write what the
|
|
804
|
+
client asked for. Never edit the map to agree with \`AGENTS.md\` \u2014 the two are
|
|
805
|
+
different files with different jobs, and this one is yours alone.
|
|
806
|
+
|
|
756
807
|
# Reporting
|
|
757
808
|
|
|
758
809
|
Report briefly what you changed and where. Do not edit any other file.`,
|
|
759
810
|
permission: {
|
|
760
|
-
// Read
|
|
761
|
-
//
|
|
811
|
+
// Read reaches AGENTS.md and the context map, and nothing else.
|
|
812
|
+
//
|
|
813
|
+
// The map is here because the directive is about how work is DONE in this
|
|
814
|
+
// project and the map is the maintained record of what the project IS: reading
|
|
815
|
+
// it is how this agent avoids recording a fact the code already settles, and
|
|
816
|
+
// avoids contradicting one it does not know yet. It stays READ-ONLY — `edit`
|
|
817
|
+
// below still denies the map, so this grant cannot turn the one-writer rule
|
|
818
|
+
// into a two-writer rule, and the writer guard does not have to care about it
|
|
819
|
+
// (it reads `edit` alone).
|
|
820
|
+
//
|
|
821
|
+
// Both spellings are required, and every allow must sit AFTER the broad deny:
|
|
822
|
+
// LAST match wins, so an allow written before "*": "deny" is silently inert.
|
|
762
823
|
read: {
|
|
763
824
|
"*": "deny",
|
|
764
825
|
"AGENTS.md": "allow",
|
|
765
|
-
"*/AGENTS.md": "allow"
|
|
826
|
+
"*/AGENTS.md": "allow",
|
|
827
|
+
"*/.opencode/context/**": "allow",
|
|
828
|
+
".opencode/context/**": "allow"
|
|
766
829
|
},
|
|
767
830
|
glob: "deny",
|
|
768
831
|
grep: "deny",
|
|
769
832
|
list: "deny",
|
|
833
|
+
// Still exactly one writable path. The map is readable (see above) and stays
|
|
834
|
+
// unwritable: `context` is its only writer, and this agent is not it.
|
|
770
835
|
edit: {
|
|
771
836
|
"*": "deny",
|
|
772
837
|
"AGENTS.md": "allow",
|
|
@@ -790,7 +855,28 @@ function buildAgentSet() {
|
|
|
790
855
|
const variant = {
|
|
791
856
|
...def,
|
|
792
857
|
description: `Hidden pool variant ${base}-${slot}: slot ${slot}. Its model is NOT set here -- LaCode injects it from the resolved pool preset at config time, so the preset is the only place a pool model is written down. Spawn '${base}' and let the pool route.`,
|
|
793
|
-
hidden: true
|
|
858
|
+
hidden: true,
|
|
859
|
+
// `mode: "primary"` is what keeps a variant OUT of the model's system
|
|
860
|
+
// prompt. `hidden` cannot do it.
|
|
861
|
+
//
|
|
862
|
+
// opencode builds the "Available agent types" block by filtering the
|
|
863
|
+
// agent registry on `mode !== "primary"` and on the caller's task
|
|
864
|
+
// permission. It never looks at `hidden`: that flag is honoured only by
|
|
865
|
+
// the TUI and the @-mention pickers, by the "agent not found" message,
|
|
866
|
+
// and by default-agent resolution. A variant left as `mode: "subagent"`
|
|
867
|
+
// is therefore listed to the model under its own name -- "explore-1:
|
|
868
|
+
// Hidden pool variant explore-1" -- and the model copies that name
|
|
869
|
+
// straight into its next spawn. The pool then refuses it, because
|
|
870
|
+
// naming a variant is how a caller would pick the provider and skip the
|
|
871
|
+
// rotation. The refusal is right; the leak that provokes it is not.
|
|
872
|
+
//
|
|
873
|
+
// `mode` is the only field that filter respects, and setting it here is
|
|
874
|
+
// safe: the task tool never reads an agent's mode -- it resolves the
|
|
875
|
+
// agent by name and runs it -- and every other place that filters on
|
|
876
|
+
// `mode !== "primary"` also filters on `!hidden`, which these already
|
|
877
|
+
// are. opencode uses this same pair for its own compaction, title and
|
|
878
|
+
// summary agents.
|
|
879
|
+
mode: "primary"
|
|
794
880
|
};
|
|
795
881
|
delete variant.model;
|
|
796
882
|
set[`${base}-${slot}`] = variant;
|
package/dist/commands.js
CHANGED
|
@@ -49,6 +49,7 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
49
49
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
50
50
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
51
51
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
52
|
+
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
52
53
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
53
54
|
|
|
54
55
|
// src/commands.ts
|
|
@@ -66,7 +67,8 @@ your system prompt.
|
|
|
66
67
|
Otherwise follow the bootstrap procedure in your system prompt: progressive
|
|
67
68
|
exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level structure \u2192 build
|
|
68
69
|
and framework config \u2192 entry points \u2192 selective source), then write the five
|
|
69
|
-
context files inside their size targets.
|
|
70
|
+
context files inside their size targets. Also create
|
|
71
|
+
\`.opencode/context/active.md\` with its heading and no tasks.
|
|
70
72
|
|
|
71
73
|
Remember: write conclusions, not inventories. Link to the project's existing
|
|
72
74
|
documentation instead of duplicating it.
|
|
@@ -103,10 +105,10 @@ timestamp-only edits. If nothing material changed, make no edits and report
|
|
|
103
105
|
subtask: true,
|
|
104
106
|
template: `Context review.
|
|
105
107
|
|
|
106
|
-
Everything outside
|
|
108
|
+
Everything outside \`.opencode/context/\` is read-only for this run. Do not
|
|
107
109
|
modify source code under any circumstances.
|
|
108
110
|
|
|
109
|
-
Audit the
|
|
111
|
+
Audit the context files and the backlog for:
|
|
110
112
|
|
|
111
113
|
- stale information \u2014 verify claims against the actual code
|
|
112
114
|
- contradictions between context files, or between the map and the code
|
|
@@ -117,6 +119,8 @@ Audit the five context files for:
|
|
|
117
119
|
- missing canonical entry points for significant subsystems
|
|
118
120
|
- unsupported assumptions \u2014 anything stated as fact that you cannot verify,
|
|
119
121
|
or that lacks a confidence marker
|
|
122
|
+
- backlog tasks that are finished but carry no date \u2014 the retention window
|
|
123
|
+
cannot age those out, so they stay forever
|
|
120
124
|
|
|
121
125
|
Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
|
|
122
126
|
and anything you could not verify.`
|
|
@@ -138,7 +142,8 @@ Follow the rules for the file in your system prompt, then report what you change
|
|
|
138
142
|
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
139
143
|
|
|
140
144
|
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
141
|
-
slot's load against its weight
|
|
145
|
+
slot's load against its ceiling (twice the slot's weight), and which models
|
|
146
|
+
are cooling.
|
|
142
147
|
|
|
143
148
|
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
144
149
|
user's opencode config for an \`omniroute\` block with \`enabled: true\`. If it
|
|
@@ -175,7 +180,7 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
|
|
|
175
180
|
2. Resolve the instruction above into a target slot table. These are the rules the loader enforces \u2014 never write a file that breaks one:
|
|
176
181
|
- one slot per line: \`<provider>/<model-id> [weight]\`
|
|
177
182
|
- at most ${MAX_SLOTS} slots, and a model may appear only once
|
|
178
|
-
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the
|
|
183
|
+
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's share of the rotation \u2014 how many turns it takes per cycle \u2014 and its concurrency ceiling is twice that. NOT a priority; pick order follows line order.
|
|
179
184
|
- \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
|
|
180
185
|
If the instruction names a model ambiguously, or would need more than ${MAX_SLOTS} slots, stop and ask rather than guessing.
|
|
181
186
|
|