@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 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.7.1...HEAD
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`) and injects
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 five generated context files and any agent's inference about what
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 five files, and what the log lines mean. |
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 \`-N\` variant when a refusal
93
- names one \u2014 the refusal means the pool has a routing problem, not that you
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 five files and nothing else:
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. For each significant subsystem, give a short
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
- - a previously undocumented invariant
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 five files for:
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 five files you own. Never modify source code
650
- during a review. Report what you found, what you fixed, and what you
651
- could not verify.
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 is scoped to the one file too: the directive is about AGENTS.md, and
761
- // nothing else is any of this agent's business.
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 the five context files is read-only for this run. Do not
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 five context files for:
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 ceiling, and which models are cooling.
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 soft ceiling on concurrent claims and the slot's share of the rotation \u2014 NOT a priority; pick order follows line order.
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