@lawaty/lacode 0.8.0 → 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,102 @@ 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
+
12
105
  ## [0.8.0] — cooldowns are probed instead of trusted, and the instructions agent can read the map
13
106
 
14
107
  ### Added
@@ -371,12 +464,14 @@ experimental; `latest` remains 0.4.2.
371
464
 
372
465
  - First published release: pooled subagents over a shared set of models.
373
466
 
374
- [Unreleased]: https://github.com/lawaty/lacode/compare/v0.8.0...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
375
469
  [0.8.0]: https://github.com/lawaty/lacode/compare/v0.7.1...v0.8.0
376
470
  [0.7.1]: https://github.com/lawaty/lacode/compare/v0.7.0...v0.7.1
377
471
  [0.7.0]: https://github.com/lawaty/lacode/compare/v0.6.0...v0.7.0
378
472
  [0.6.0]: https://github.com/lawaty/lacode/compare/v0.5.0...v0.6.0
379
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
380
475
  [0.4.1]: https://github.com/lawaty/lacode/compare/v0.4.0...v0.4.1
381
476
  [0.4.0]: https://github.com/lawaty/lacode/compare/v0.3.0...v0.4.0
382
477
  [0.3.0]: https://github.com/lawaty/lacode/compare/v0.2.2...v0.3.0
package/README.md CHANGED
@@ -213,8 +213,9 @@ deliberately *not* injected, are in
213
213
  Agents are stateless between tasks, and exploring your repository is the most
214
214
  expensive part of any task — so every agent pays for it again, re-deriving the
215
215
  same layout, entry points and boundaries from scratch. LaCode keeps a short,
216
- maintained map of the repository in `.opencode/context/` (five files:
217
- `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
218
219
  a standing rule that every agent reads the relevant file before exploring and
219
220
  starts from the canonical entry points it lists. The map holds conclusions, not
220
221
  inventories: it answers "what should I look at first?" and nothing else, which
@@ -251,7 +252,7 @@ in [docs/CONTEXT.md](docs/CONTEXT.md).
251
252
  The project's root `AGENTS.md` is your own statement of intent: it records
252
253
  what you want worked on and how you want it approached. It says nothing about
253
254
  where code lives, so it can never contradict the code, and within that domain
254
- 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
255
256
  matters right now.
256
257
 
257
258
  The file is per-clone and not committed, so it is yours rather than shared
@@ -369,7 +370,7 @@ want it.
369
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. |
370
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. |
371
372
  | [OmniRoute gateway](docs/OMNIROUTE.md) | The optional local gateway integration: setup, full option set, a latency-measured free combo, and its troubleshooting table. |
372
- | [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. |
373
374
  | [Remote hosts and notifications](docs/TUNNEL.md) | The SSH notification hop, payload format, receiver contract, and holding the forward open. |
374
375
  | [Benchmarks](docs/BENCHMARKS.md) | How the cost harness works, and why it deliberately publishes no savings number. |
375
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
@@ -90,8 +90,8 @@ failing, or a runtime message saying no slot is available \u2014 that is not a r
90
90
  to do the work yourself, and not a reason to stop and ask.
91
91
 
92
92
  **Retry.** A refused or cooling slot is transient and costs you nothing but a
93
- turn. Spawn the bare base again. Do not escalate a \`-N\` variant when a refusal
94
- 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
95
95
  picked the wrong number.
96
96
 
97
97
  Retry is always available and never wrong, which is why it is the default. But
@@ -450,7 +450,7 @@ if the blocker is understanding rather than execution, report that instead.`,
450
450
  }
451
451
  },
452
452
  "context": {
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. 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.",
454
454
  "mode": "subagent",
455
455
  "temperature": 0.1,
456
456
  prompt: `# Role
@@ -466,7 +466,7 @@ map is a derived index of it.
466
466
 
467
467
  # Owned files
468
468
 
469
- You own exactly these five files and nothing else:
469
+ You own exactly these files and nothing else:
470
470
 
471
471
  - \`.opencode/context/architecture.md\` \u2014 high-level architecture, major layers,
472
472
  boundaries, important entry points, dependency rules. Target 50\u2013150 lines.
@@ -481,6 +481,9 @@ You own exactly these five files and nothing else:
481
481
  - \`.opencode/context/decisions.md\` \u2014 architectural decisions discovered
482
482
  during work that are not already recorded in the project's own decision
483
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.
484
487
 
485
488
  # Current-state only
486
489
 
@@ -520,7 +523,8 @@ Explore progressively and stop as soon as you have a usable map:
520
523
  You are mapping a city, not auditing every house. Do not attempt to
521
524
  understand every file.
522
525
 
523
- 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
524
528
  "Start here" list \u2014 at most about five canonical files, each with a one-phrase
525
529
  reason. This is the single most valuable thing you produce: where an agent
526
530
  should open its editor next.
@@ -544,7 +548,7 @@ Trigger: \`.opencode/context/\` exists and work has just been done here.
544
548
  - an architectural decision
545
549
  - a new dependency between subsystems
546
550
  - a renamed or relocated subsystem
547
- - a previously undocumented invariant
551
+ - a previously undocumented invariant
548
552
 
549
553
  Do not update documentation merely because source files changed. Do not
550
554
  rewrite files wholesale. Do not make timestamp-only edits. Do not add a fact
@@ -552,6 +556,38 @@ that is already stated in another context file \u2014 one fact has one home, and
552
556
  the other files link to it. If nothing material changed, make no edits and
553
557
  report "no update needed".
554
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
+
555
591
  # Minimal exploration
556
592
 
557
593
  Default shape:
@@ -638,7 +674,7 @@ the lowest-value ones, and link out to authoritative project docs.
638
674
 
639
675
  Trigger: an explicit review (see the \`context-review\` command).
640
676
 
641
- Audit the five files for:
677
+ Audit the map files and the backlog for:
642
678
 
643
679
  - stale information \u2014 verify claims against the code
644
680
  - contradictions between files, or between the map and the code
@@ -646,10 +682,12 @@ Audit the five files for:
646
682
  - duplicated information \u2014 one canonical place per fact
647
683
  - missing canonical entry points for significant subsystems
648
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
649
687
 
650
- Fix problems only in the five files you own. Never modify source code
651
- during a review. Report what you found, what you fixed, and what you
652
- 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.
653
691
 
654
692
  # Reporting
655
693
 
@@ -817,7 +855,28 @@ function buildAgentSet() {
817
855
  const variant = {
818
856
  ...def,
819
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.`,
820
- 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"
821
880
  };
822
881
  delete variant.model;
823
882
  set[`${base}-${slot}`] = variant;
package/dist/commands.js CHANGED
@@ -67,7 +67,8 @@ your system prompt.
67
67
  Otherwise follow the bootstrap procedure in your system prompt: progressive
68
68
  exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level structure \u2192 build
69
69
  and framework config \u2192 entry points \u2192 selective source), then write the five
70
- 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.
71
72
 
72
73
  Remember: write conclusions, not inventories. Link to the project's existing
73
74
  documentation instead of duplicating it.
@@ -104,10 +105,10 @@ timestamp-only edits. If nothing material changed, make no edits and report
104
105
  subtask: true,
105
106
  template: `Context review.
106
107
 
107
- 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
108
109
  modify source code under any circumstances.
109
110
 
110
- Audit the five context files for:
111
+ Audit the context files and the backlog for:
111
112
 
112
113
  - stale information \u2014 verify claims against the actual code
113
114
  - contradictions between context files, or between the map and the code
@@ -118,6 +119,8 @@ Audit the five context files for:
118
119
  - missing canonical entry points for significant subsystems
119
120
  - unsupported assumptions \u2014 anything stated as fact that you cannot verify,
120
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
121
124
 
122
125
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
123
126
  and anything you could not verify.`
package/dist/index.js CHANGED
@@ -51,10 +51,10 @@ var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
51
51
  var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
52
52
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
53
53
  var FALLBACK_SLOTS = [
54
- { index: 1, model: "opencode/space-bunny-free", weight: 3 },
55
- { index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
56
- { index: 3, model: "opencode/big-pickle", weight: 1 },
57
- { index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
54
+ { index: 1, model: "opencode/big-pickle", weight: 3 },
55
+ { index: 2, model: "opencode-go/minimax-m3.1-flash-free", weight: 2 },
56
+ { index: 3, model: "opencode/deepseek-v4.1-flash-free", weight: 1 },
57
+ { index: 4, model: "opencode/glm-5.3-flash-free", weight: 1 }
58
58
  ];
59
59
  function parseSlots(parsed, origin) {
60
60
  const problems = [];
@@ -786,8 +786,8 @@ failing, or a runtime message saying no slot is available \u2014 that is not a r
786
786
  to do the work yourself, and not a reason to stop and ask.
787
787
 
788
788
  **Retry.** A refused or cooling slot is transient and costs you nothing but a
789
- turn. Spawn the bare base again. Do not escalate a \`-N\` variant when a refusal
790
- names one \u2014 the refusal means the pool has a routing problem, not that you
789
+ turn. Spawn the bare base again. Do not escalate to a numbered variant after a
790
+ refusal \u2014 the refusal means the pool has a routing problem, not that you
791
791
  picked the wrong number.
792
792
 
793
793
  Retry is always available and never wrong, which is why it is the default. But
@@ -1146,7 +1146,7 @@ if the blocker is understanding rather than execution, report that instead.`,
1146
1146
  }
1147
1147
  },
1148
1148
  "context": {
1149
- "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.",
1149
+ "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.",
1150
1150
  "mode": "subagent",
1151
1151
  "temperature": 0.1,
1152
1152
  prompt: `# Role
@@ -1162,7 +1162,7 @@ map is a derived index of it.
1162
1162
 
1163
1163
  # Owned files
1164
1164
 
1165
- You own exactly these five files and nothing else:
1165
+ You own exactly these files and nothing else:
1166
1166
 
1167
1167
  - \`.opencode/context/architecture.md\` \u2014 high-level architecture, major layers,
1168
1168
  boundaries, important entry points, dependency rules. Target 50\u2013150 lines.
@@ -1177,6 +1177,9 @@ You own exactly these five files and nothing else:
1177
1177
  - \`.opencode/context/decisions.md\` \u2014 architectural decisions discovered
1178
1178
  during work that are not already recorded in the project's own decision
1179
1179
  log. Concise; variable length.
1180
+ - \`.opencode/context/active.md\` \u2014 the active backlog: work in flight, grouped
1181
+ by the week it was opened. Not part of the navigation map; it is described
1182
+ under "Task backlog" below.
1180
1183
 
1181
1184
  # Current-state only
1182
1185
 
@@ -1216,7 +1219,8 @@ Explore progressively and stop as soon as you have a usable map:
1216
1219
  You are mapping a city, not auditing every house. Do not attempt to
1217
1220
  understand every file.
1218
1221
 
1219
- Then write the five files. For each significant subsystem, give a short
1222
+ Then write the five files. Also create \`.opencode/context/active.md\` with its
1223
+ heading and no tasks. For each significant subsystem, give a short
1220
1224
  "Start here" list \u2014 at most about five canonical files, each with a one-phrase
1221
1225
  reason. This is the single most valuable thing you produce: where an agent
1222
1226
  should open its editor next.
@@ -1240,7 +1244,7 @@ Trigger: \`.opencode/context/\` exists and work has just been done here.
1240
1244
  - an architectural decision
1241
1245
  - a new dependency between subsystems
1242
1246
  - a renamed or relocated subsystem
1243
- - a previously undocumented invariant
1247
+ - a previously undocumented invariant
1244
1248
 
1245
1249
  Do not update documentation merely because source files changed. Do not
1246
1250
  rewrite files wholesale. Do not make timestamp-only edits. Do not add a fact
@@ -1248,6 +1252,38 @@ that is already stated in another context file \u2014 one fact has one home, and
1248
1252
  the other files link to it. If nothing material changed, make no edits and
1249
1253
  report "no update needed".
1250
1254
 
1255
+ Also keep \`.opencode/context/active.md\` current: record work that is in
1256
+ flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.
1257
+
1258
+ # Task backlog
1259
+
1260
+ \`.opencode/context/active.md\` holds the work in flight. It is a rolling
1261
+ window, not a history: it answers "what is open, and what just finished".
1262
+
1263
+ Format \u2014 one line per task, under the ISO week the task was opened in:
1264
+
1265
+ ## 2026-W43
1266
+
1267
+ - [ ] Ship the release notes \u2014 opened 2026-W43
1268
+ - [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
1269
+
1270
+ The rules:
1271
+
1272
+ - A task that is not marked \`[x]\` is OPEN. An open task is never removed,
1273
+ however old it is.
1274
+ - A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\`
1275
+ date. Record the date it actually finished, as \`YYYY-MM-DD\`.
1276
+ - The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
1277
+ - A finished task is dropped once it is more than a week old. The plugin
1278
+ enforces that: record the finished task with its date and let the window
1279
+ close on its own. Do not delete old finished tasks by hand, and do not
1280
+ keep a finished task back to preserve it \u2014 the file is meant to stay
1281
+ small.
1282
+ - Never invent a task. Record work that actually happened, and mark a task
1283
+ finished only once it is finished.
1284
+ - Keep the file inside the window. If the backlog has grown past a handful
1285
+ of weeks, that is a signal to close finished tasks, not to add sections.
1286
+
1251
1287
  # Minimal exploration
1252
1288
 
1253
1289
  Default shape:
@@ -1334,7 +1370,7 @@ the lowest-value ones, and link out to authoritative project docs.
1334
1370
 
1335
1371
  Trigger: an explicit review (see the \`context-review\` command).
1336
1372
 
1337
- Audit the five files for:
1373
+ Audit the map files and the backlog for:
1338
1374
 
1339
1375
  - stale information \u2014 verify claims against the code
1340
1376
  - contradictions between files, or between the map and the code
@@ -1342,10 +1378,12 @@ Audit the five files for:
1342
1378
  - duplicated information \u2014 one canonical place per fact
1343
1379
  - missing canonical entry points for significant subsystems
1344
1380
  - unsupported assumptions \u2014 anything unmarked that you cannot verify
1381
+ - backlog tasks that are finished but carry no date \u2014 the retention window
1382
+ cannot age those out, so they stay forever
1345
1383
 
1346
- Fix problems only in the five files you own. Never modify source code
1347
- during a review. Report what you found, what you fixed, and what you
1348
- could not verify.
1384
+ Fix problems only in the files you own. Never modify source code during a
1385
+ review. Report what you found, what you fixed, and what you could not
1386
+ verify.
1349
1387
 
1350
1388
  # Reporting
1351
1389
 
@@ -1513,7 +1551,28 @@ function buildAgentSet() {
1513
1551
  const variant = {
1514
1552
  ...def,
1515
1553
  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.`,
1516
- hidden: true
1554
+ hidden: true,
1555
+ // `mode: "primary"` is what keeps a variant OUT of the model's system
1556
+ // prompt. `hidden` cannot do it.
1557
+ //
1558
+ // opencode builds the "Available agent types" block by filtering the
1559
+ // agent registry on `mode !== "primary"` and on the caller's task
1560
+ // permission. It never looks at `hidden`: that flag is honoured only by
1561
+ // the TUI and the @-mention pickers, by the "agent not found" message,
1562
+ // and by default-agent resolution. A variant left as `mode: "subagent"`
1563
+ // is therefore listed to the model under its own name -- "explore-1:
1564
+ // Hidden pool variant explore-1" -- and the model copies that name
1565
+ // straight into its next spawn. The pool then refuses it, because
1566
+ // naming a variant is how a caller would pick the provider and skip the
1567
+ // rotation. The refusal is right; the leak that provokes it is not.
1568
+ //
1569
+ // `mode` is the only field that filter respects, and setting it here is
1570
+ // safe: the task tool never reads an agent's mode -- it resolves the
1571
+ // agent by name and runs it -- and every other place that filters on
1572
+ // `mode !== "primary"` also filters on `!hidden`, which these already
1573
+ // are. opencode uses this same pair for its own compaction, title and
1574
+ // summary agents.
1575
+ mode: "primary"
1517
1576
  };
1518
1577
  delete variant.model;
1519
1578
  set[`${base}-${slot}`] = variant;
@@ -1553,7 +1612,8 @@ your system prompt.
1553
1612
  Otherwise follow the bootstrap procedure in your system prompt: progressive
1554
1613
  exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level structure \u2192 build
1555
1614
  and framework config \u2192 entry points \u2192 selective source), then write the five
1556
- context files inside their size targets.
1615
+ context files inside their size targets. Also create
1616
+ \`.opencode/context/active.md\` with its heading and no tasks.
1557
1617
 
1558
1618
  Remember: write conclusions, not inventories. Link to the project's existing
1559
1619
  documentation instead of duplicating it.
@@ -1590,10 +1650,10 @@ timestamp-only edits. If nothing material changed, make no edits and report
1590
1650
  subtask: true,
1591
1651
  template: `Context review.
1592
1652
 
1593
- Everything outside the five context files is read-only for this run. Do not
1653
+ Everything outside \`.opencode/context/\` is read-only for this run. Do not
1594
1654
  modify source code under any circumstances.
1595
1655
 
1596
- Audit the five context files for:
1656
+ Audit the context files and the backlog for:
1597
1657
 
1598
1658
  - stale information \u2014 verify claims against the actual code
1599
1659
  - contradictions between context files, or between the map and the code
@@ -1604,6 +1664,8 @@ Audit the five context files for:
1604
1664
  - missing canonical entry points for significant subsystems
1605
1665
  - unsupported assumptions \u2014 anything stated as fact that you cannot verify,
1606
1666
  or that lacks a confidence marker
1667
+ - backlog tasks that are finished but carry no date \u2014 the retention window
1668
+ cannot age those out, so they stay forever
1607
1669
 
1608
1670
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
1609
1671
  and anything you could not verify.`
@@ -2096,12 +2158,11 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
2096
2158
  if (typeof requested !== "string") return;
2097
2159
  const manual = manualVariant(requested);
2098
2160
  if (manual) {
2099
- await log("warn", `refused manual spawn of slot variant ${requested}; spawn "${manual}" instead`, {
2100
- variant: requested,
2161
+ await log("warn", `refused manual spawn of a pool slot variant; spawn "${manual}" instead`, {
2101
2162
  base: manual
2102
2163
  });
2103
2164
  throw new Error(
2104
- `${requested} is a pool slot variant and cannot be spawned by name. Naming it lets the caller pick the provider: it skips the round-robin rotation and the slot's ceiling, so one provider takes every concurrent claim. Spawn "${manual}" and let the pool choose the slot.`
2165
+ `That agent is a pool slot variant and cannot be spawned by name. Naming it lets the caller pick the provider: it skips the round-robin rotation and the slot's ceiling, so one provider takes every concurrent claim. Spawn "${manual}" and let the pool choose the slot.`
2105
2166
  );
2106
2167
  }
2107
2168
  try {
@@ -2226,10 +2287,110 @@ round robin in slot order, each slot taking as many turns as its weight says, so
2226
2287
  });
2227
2288
 
2228
2289
  // src/plugins/context-autoupdate.ts
2229
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
2290
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
2230
2291
  import { homedir as homedir2 } from "node:os";
2231
2292
  import * as path2 from "node:path";
2232
2293
 
2294
+ // src/lib/active-backlog.ts
2295
+ var ACTIVE_BACKLOG_FILE = "active.md";
2296
+ var RETENTION_DAYS = 7;
2297
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2298
+ var HEADER = `# Active backlog
2299
+
2300
+ Tasks in flight, grouped by the week they were opened. A finished task stays
2301
+ here for a week after it was finished and is then dropped; an open task stays
2302
+ however long it takes. Add a line under the right week, and give a finished
2303
+ task its date: \`- [x] Title \u2014 opened 2026-W43, finished 2026-10-19\`.`;
2304
+ var TASK_LINE = /^\s*(?:[-*+]|\d+[.)])\s*\[([ xX])\]\s*(.*)$/;
2305
+ var META = /(?:—|–|-)\s*opened\s+(\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3]))|[,—–-]\s*finished\s+(\d{4}-\d{2}-\d{2})/gi;
2306
+ var isCalendarDate = (s) => {
2307
+ const at = Date.parse(`${s}T00:00:00Z`);
2308
+ return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === s;
2309
+ };
2310
+ var parseBacklog = (text) => {
2311
+ const entries = [];
2312
+ for (const raw of text.split(/\r?\n/)) {
2313
+ const line = TASK_LINE.exec(raw);
2314
+ if (!line) continue;
2315
+ const finished = line[1] !== " ";
2316
+ let week;
2317
+ let finishedAt;
2318
+ const stripped = line[2].replace(META, (fragment, openedWeek, finishedDate) => {
2319
+ if (openedWeek !== void 0) {
2320
+ week ??= openedWeek.toUpperCase();
2321
+ return "";
2322
+ }
2323
+ if (finishedDate !== void 0 && isCalendarDate(finishedDate)) {
2324
+ finishedAt ??= finishedDate;
2325
+ return "";
2326
+ }
2327
+ return fragment;
2328
+ });
2329
+ const title = stripped.replace(/^[\s—–-]+|[\s—–-]+$/g, "");
2330
+ if (!title) continue;
2331
+ entries.push({ title, ...week ? { week } : {}, finished, ...finishedAt ? { finishedAt } : {} });
2332
+ }
2333
+ return entries;
2334
+ };
2335
+ var ISO_WEEK = /^(\d{4})-W(\d{2})$/i;
2336
+ var newestFirst = (a, b) => {
2337
+ const ma = ISO_WEEK.exec(a);
2338
+ const mb = ISO_WEEK.exec(b);
2339
+ if (ma && mb) return Number(mb[1]) - Number(ma[1]) || Number(mb[2]) - Number(ma[2]);
2340
+ if (ma) return -1;
2341
+ if (mb) return 1;
2342
+ return a < b ? 1 : a > b ? -1 : 0;
2343
+ };
2344
+ var renderLine = (entry) => {
2345
+ const box = entry.finished ? "[x]" : "[ ]";
2346
+ const meta = [];
2347
+ if (entry.week) meta.push(`opened ${entry.week}`);
2348
+ if (entry.finished && entry.finishedAt) meta.push(`finished ${entry.finishedAt}`);
2349
+ const head = `- ${box} ${entry.title}`;
2350
+ if (meta.length === 0) return head;
2351
+ return entry.week ? `${head} \u2014 ${meta.join(", ")}` : `${head}, ${meta.join(", ")}`;
2352
+ };
2353
+ var renderBacklog = (entries) => {
2354
+ const buckets = /* @__PURE__ */ new Map();
2355
+ for (const entry of entries) {
2356
+ const key = entry.week ? entry.week : null;
2357
+ const bucket = buckets.get(key);
2358
+ if (bucket) bucket.push(entry);
2359
+ else buckets.set(key, [entry]);
2360
+ }
2361
+ const weeks = [...buckets.keys()].filter((week) => week !== null).sort(newestFirst);
2362
+ let out = HEADER;
2363
+ for (const week of weeks) out += `
2364
+
2365
+ ## ${week}
2366
+
2367
+ ${buckets.get(week).map(renderLine).join("\n")}`;
2368
+ const unscheduled = buckets.get(null);
2369
+ if (unscheduled) out += `
2370
+
2371
+ ## Unscheduled
2372
+
2373
+ ${unscheduled.map(renderLine).join("\n")}`;
2374
+ return `${out}
2375
+ `;
2376
+ };
2377
+ var normalizeBacklog = (text, now = Date.now()) => {
2378
+ const entries = parseBacklog(text);
2379
+ const kept = [];
2380
+ const removed = [];
2381
+ for (const entry of entries) {
2382
+ if (entry.finished && entry.finishedAt) {
2383
+ const at = Date.parse(`${entry.finishedAt}T00:00:00Z`);
2384
+ if (Number.isFinite(at) && now - at > RETENTION_DAYS * DAY_MS) {
2385
+ removed.push(entry.title);
2386
+ continue;
2387
+ }
2388
+ }
2389
+ kept.push(entry);
2390
+ }
2391
+ return { text: renderBacklog(kept), removed };
2392
+ };
2393
+
2233
2394
  // src/lib/jsonc.ts
2234
2395
  var stripJsonc = (raw) => {
2235
2396
  let out = "";
@@ -2437,7 +2598,7 @@ var changedSince = (before, dir) => {
2437
2598
  };
2438
2599
  var RETAIN_TRANSCRIPTS = 20;
2439
2600
  var transcriptDir = () => process.env.LACODE_TRANSCRIPT_DIR ?? path2.join(homedir2(), ".local", "share", "opencode", "lacode", "cartographer");
2440
- var retireSession = async (client, directory, sessionID, mode) => {
2601
+ var retireSession = async (client, directory, sessionID, mode, log) => {
2441
2602
  try {
2442
2603
  const transcript = await client.session.messages({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0);
2443
2604
  const dir = transcriptDir();
@@ -2446,8 +2607,18 @@ var retireSession = async (client, directory, sessionID, mode) => {
2446
2607
  const file = path2.join(dir, `${stamp}-${mode}.json`);
2447
2608
  writeFileSync2(file, JSON.stringify({ sessionID, mode, messages: transcript ?? null }, null, 2));
2448
2609
  pruneTranscripts(dir);
2610
+ const gone = async () => await client.session.get({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0) === void 0;
2449
2611
  await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
2450
- } catch {
2612
+ if (await gone()) return;
2613
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
2614
+ await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
2615
+ if (await gone()) return;
2616
+ await log?.("warn", "the cartographer's session could not be removed from the session list; it stays as an empty entry", {
2617
+ sessionID,
2618
+ mode
2619
+ });
2620
+ } catch (error) {
2621
+ await log?.("warn", `could not retire the cartographer's session: ${String(error)}`, { sessionID, mode });
2451
2622
  }
2452
2623
  };
2453
2624
  var pruneTranscripts = (dir) => {
@@ -2471,6 +2642,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2471
2642
  });
2472
2643
  const root = path2.resolve(directory);
2473
2644
  const contextAbs = path2.join(root, CONTEXT_DIR2);
2645
+ const activeBacklogPath = path2.join(contextAbs, ACTIVE_BACKLOG_FILE);
2474
2646
  const config = findProjectConfig();
2475
2647
  const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
2476
2648
  if (writerProblem) void log("error", writerProblem);
@@ -2480,6 +2652,23 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2480
2652
  if (absolute === contextAbs || absolute.startsWith(contextAbs + path2.sep)) return false;
2481
2653
  return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
2482
2654
  };
2655
+ const normalizeActiveBacklog = async () => {
2656
+ try {
2657
+ if (!existsSync2(activeBacklogPath)) return { removed: 0 };
2658
+ const before = readFileSync4(activeBacklogPath, "utf8");
2659
+ const { text, removed } = normalizeBacklog(before);
2660
+ if (text !== before) writeFileSync2(activeBacklogPath, text);
2661
+ if (removed.length > 0) {
2662
+ await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
2663
+ removed: removed.length
2664
+ });
2665
+ }
2666
+ return { removed: removed.length };
2667
+ } catch {
2668
+ return { removed: 0 };
2669
+ }
2670
+ };
2671
+ void normalizeActiveBacklog();
2483
2672
  const listed = (files) => {
2484
2673
  const shown = files.slice(0, MAX_FILES_LISTED);
2485
2674
  const rest = files.length - shown.length;
@@ -2497,7 +2686,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2497
2686
  "",
2498
2687
  NO_BULK_READS,
2499
2688
  "",
2500
- "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their size targets. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
2689
+ "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their size targets. Also create `.opencode/context/active.md` with its heading and no tasks. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
2501
2690
  ].join("\n") : [
2502
2691
  "Incremental context update. Project root: " + directory,
2503
2692
  "",
@@ -2506,13 +2695,18 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2506
2695
  "",
2507
2696
  NO_BULK_READS,
2508
2697
  "",
2509
- 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or a previously undocumented invariant. Do not edit documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed".'
2698
+ 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or a previously undocumented invariant. Do not edit documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed". Also keep `.opencode/context/active.md` current: record work that is in flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.'
2510
2699
  ].join("\n");
2511
2700
  return {
2512
2701
  event: async ({ event }) => {
2513
2702
  try {
2514
2703
  if (event.type === "file.edited") {
2515
- if (isTracked(event.properties.file)) pending.add(event.properties.file);
2704
+ const file = event.properties.file;
2705
+ if (path2.resolve(root, file) === activeBacklogPath) {
2706
+ await normalizeActiveBacklog();
2707
+ return;
2708
+ }
2709
+ if (isTracked(file)) pending.add(file);
2516
2710
  return;
2517
2711
  }
2518
2712
  if (event.type !== "session.idle") return;
@@ -2551,22 +2745,36 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2551
2745
  return;
2552
2746
  }
2553
2747
  const claimKey2 = claimKey(target, "context-autoupdate");
2554
- acquire(state2, "context", slot, target, claimKey2, target);
2555
- await log("info", `borrowed pool slot ${slot.index} (${slot.model}, w${slot.weight}) for auto ${mode}`, {
2556
- slot: slot.index,
2557
- weight: slot.weight,
2558
- model: slot.model
2559
- });
2560
- lastRun = Date.now();
2561
- owned.set(target, Date.now());
2562
- const before = snapshotMap(contextAbs);
2563
- client.session.prompt({
2564
- path: { id: target },
2565
- query: { directory },
2566
- body: { agent, parts: [{ type: "text", text: compose(mode, files) }] }
2567
- }).then(async (result) => {
2748
+ let claimed = false;
2749
+ let before;
2750
+ let started;
2751
+ try {
2752
+ acquire(state2, "context", slot, target, claimKey2, target);
2753
+ claimed = true;
2754
+ await log("info", `borrowed pool slot ${slot.index} (${slot.model}, w${slot.weight}) for auto ${mode}`, {
2755
+ slot: slot.index,
2756
+ weight: slot.weight,
2757
+ model: slot.model
2758
+ });
2759
+ lastRun = Date.now();
2760
+ owned.set(target, Date.now());
2761
+ before = snapshotMap(contextAbs);
2762
+ started = client.session.prompt({
2763
+ path: { id: target },
2764
+ query: { directory },
2765
+ body: { agent, parts: [{ type: "text", text: compose(mode, files) }] }
2766
+ });
2767
+ } catch (error) {
2768
+ if (claimed) release(state2, claimKey2);
2769
+ owned.delete(target);
2770
+ await retireSession(client, directory, target, mode, log);
2771
+ requeue();
2772
+ await log("error", `auto ${mode} could not start: ${String(error)}`);
2773
+ return;
2774
+ }
2775
+ started.then(async (result) => {
2568
2776
  release(state2, claimKey2);
2569
- await retireSession(client, directory, target, mode);
2777
+ await retireSession(client, directory, target, mode, log);
2570
2778
  const failure = result.error ?? result.data?.info?.error;
2571
2779
  if (failure) {
2572
2780
  requeue();
@@ -2590,7 +2798,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
2590
2798
  }
2591
2799
  }).catch(async (error) => {
2592
2800
  release(state2, claimKey2);
2593
- await retireSession(client, directory, target, mode);
2801
+ await retireSession(client, directory, target, mode, log);
2594
2802
  requeue();
2595
2803
  await log("error", `auto ${mode} failed: ${String(error)}`);
2596
2804
  });
@@ -3487,7 +3695,7 @@ var omniRouteHooks = async (input, options, deps = {}) => {
3487
3695
 
3488
3696
  // src/lib/selfupdate.ts
3489
3697
  import { spawn as spawn2 } from "node:child_process";
3490
- import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3, writeSync } from "node:fs";
3698
+ import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3, writeSync } from "node:fs";
3491
3699
  import { homedir as homedir3 } from "node:os";
3492
3700
  import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
3493
3701
  var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -3514,7 +3722,7 @@ function compareSemver(a, b) {
3514
3722
  }
3515
3723
  function readInstalledVersion(packageRoot, read = (path4) => {
3516
3724
  try {
3517
- return readFileSync4(path4, "utf8");
3725
+ return readFileSync5(path4, "utf8");
3518
3726
  } catch {
3519
3727
  return void 0;
3520
3728
  }
@@ -3574,7 +3782,7 @@ async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
3574
3782
  }
3575
3783
  function readState(path4 = STATE_PATH) {
3576
3784
  try {
3577
- return JSON.parse(readFileSync4(path4, "utf8"));
3785
+ return JSON.parse(readFileSync5(path4, "utf8"));
3578
3786
  } catch {
3579
3787
  return {};
3580
3788
  }
@@ -3613,7 +3821,7 @@ function acquireLock(path4 = LOCK_PATH, now = Date.now()) {
3613
3821
  } catch {
3614
3822
  let orphaned = false;
3615
3823
  try {
3616
- const pid = Number.parseInt(readFileSync4(path4, "utf8").trim(), 10);
3824
+ const pid = Number.parseInt(readFileSync5(path4, "utf8").trim(), 10);
3617
3825
  orphaned = !Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid);
3618
3826
  } catch {
3619
3827
  orphaned = true;
package/dist/pool.js CHANGED
@@ -51,10 +51,10 @@ var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
51
51
  var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
52
52
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
53
53
  var FALLBACK_SLOTS = [
54
- { index: 1, model: "opencode/space-bunny-free", weight: 3 },
55
- { index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
56
- { index: 3, model: "opencode/big-pickle", weight: 1 },
57
- { index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
54
+ { index: 1, model: "opencode/big-pickle", weight: 3 },
55
+ { index: 2, model: "opencode-go/minimax-m3.1-flash-free", weight: 2 },
56
+ { index: 3, model: "opencode/deepseek-v4.1-flash-free", weight: 1 },
57
+ { index: 4, model: "opencode/glm-5.3-flash-free", weight: 1 }
58
58
  ];
59
59
  function parseSlots(parsed, origin) {
60
60
  const problems = [];
package/docs/CONTEXT.md CHANGED
@@ -29,10 +29,11 @@ the exploration it replaces is worse than no map:
29
29
  for order mutations", not "there is a file called OrderService in
30
30
  `src/orders/`". The cartographer is forbidden by name from writing directory
31
31
  listings, copying source, or restating what the README already says.
32
- - **One file per question.** Five files, each with a defined scope, so an agent
33
- reads one of them rather than all of them.
32
+ - **One file per question.** Five navigation files, each with a defined scope, so
33
+ an agent reads one of them rather than all of them. A sixth file,
34
+ `active.md`, holds the work in flight rather than a map of the repository.
34
35
  - **Canonical entry points.** The highest-value thing in the map is a short
35
- "Start here" list per subsystem: the five files an agent should open, each
36
+ "Start here" list per subsystem: the canonical files an agent should open, each
36
37
  with a one-phrase reason.
37
38
  - **It links out.** Your project's own README, ADRs and docs are authoritative
38
39
  and are not duplicated here.
@@ -47,7 +48,8 @@ harness that produces a number for your own workload.
47
48
 
48
49
  ## The files in the map
49
50
 
50
- `.opencode/context/` holds the five generated files below:
51
+ `.opencode/context/` holds the five navigation files below, plus one file that is
52
+ a work list rather than a map:
51
53
 
52
54
  | File | What belongs in it |
53
55
  | --- | --- |
@@ -56,6 +58,7 @@ harness that produces a number for your own workload.
56
58
  | `conventions.md` | Repository-specific rules: naming, module layout, error handling, testing, persistence. Read this before writing code, not after. |
57
59
  | `workflows.md` | The few runtime flows worth tracing end to end — how a request becomes a mutation, how a plugin hook reaches the thing it hooks. Read this when you need to know what actually happens at runtime. |
58
60
  | `decisions.md` | Architectural decisions that the repository's own decision log does not already record. Concise, variable length. |
61
+ | `active.md` | The active backlog: work in flight, grouped by the ISO week each task was opened in. A task that is not marked `[x]` is open and is never removed; a task that is finished and dated is kept for a week after it was finished, then dropped. The plugin enforces that window — see [How it stays current](#how-it-stays-current). Not a navigation file: there is no question it is the answer to. |
59
62
 
60
63
  The project's root `AGENTS.md` is the standing-instructions home: the client's
61
64
  own statement of intent, what they want worked on and how they want it
@@ -135,6 +138,18 @@ your source.
135
138
  If you have defined a command of the same name, yours wins and is left exactly
136
139
  as you wrote it — the same merge-without-clobber rule the agents follow.
137
140
 
141
+ `active.md` is the one file in the directory the plugin maintains rather than
142
+ the cartographer. The cartographer records what happened — work in flight, and a
143
+ task marked finished with the date it finished — and the plugin decides what is
144
+ still worth keeping: the file is regrouped by week and a task that was finished
145
+ more than a week ago is dropped. The arithmetic lives in
146
+ `src/lib/active-backlog.ts`, not in the cartographer's prompt, because a rule a
147
+ model applies is a rule the model applies when it feels like it, and the failure
148
+ is silent — the backlog just grows. The plugin normalizes the file on every
149
+ edit of it and once when the session starts, so a hand edit is grouped and
150
+ pruned whether the cartographer ran or not, and the file is rewritten only when
151
+ its canonical form differs from what is on disk, so the write cannot loop.
152
+
138
153
  ## One writer
139
154
 
140
155
  Only `context` writes the map. Every other agent is told never to edit
@@ -152,7 +167,7 @@ naming the one writer, and the permission rule — a permission denial tells an
152
167
  agent nothing about what it should have done instead.
153
168
 
154
169
  This exists because two writers produce a map that is neither current nor
155
- trustworthy. Concurrent sessions rewriting the same five files produce
170
+ trustworthy. Concurrent sessions rewriting the same files produce
156
171
  contradictions that no later reader can resolve, and the reader has no way to
157
172
  tell which version is newer.
158
173
 
@@ -193,7 +208,7 @@ startup check has no way to see a worktree it was not told about.
193
208
 
194
209
  How to detect it: open opencode from the repository root, not from a
195
210
  subdirectory of it. If `ls .opencode/context` from your shell's current
196
- directory does not list the five files, your shell is not at the root and
211
+ directory does not list the map files, your shell is not at the root and
197
212
  neither is opencode's worktree. The cost of getting it wrong is not a crash —
198
213
  it is a map maintained only by prompt, with no mechanism enforcing who writes it.
199
214
 
package/docs/OMNIROUTE.md CHANGED
@@ -103,8 +103,8 @@ work (exploration, implementation, cartography) through the gateway, add an
103
103
  ```
104
104
  # ~/.config/lacode/pool.txt
105
105
  omniroute/free-mix
106
- opencode/space-bunny-free
107
- opencode-go/longcat-2.5-preview-free
106
+ opencode/big-pickle
107
+ opencode-go/minimax-m3.1-flash-free
108
108
  ```
109
109
 
110
110
  OmniRoute is a provider like any other: the pool decides its place and weight,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/presets/README.md CHANGED
@@ -36,10 +36,10 @@ in place, no JSON and no objects:
36
36
 
37
37
  ```text
38
38
  # provider/model-id weight
39
- opencode-go/longcat-2.5-preview-free 3
40
- opencode/space-bunny-free 2
41
- opencode/big-pickle 1
42
- opencode/longcat-2.5-preview-free 1
39
+ opencode/big-pickle 3
40
+ opencode-go/minimax-m3.1-flash-free 2
41
+ opencode/deepseek-v4.1-flash-free 1
42
+ opencode/glm-5.3-flash-free 1
43
43
  ```
44
44
 
45
45
  `#` starts a comment (whole-line or trailing), blank lines are ignored, and the
@@ -26,11 +26,27 @@
26
26
  " 3. once EVERY slot is at its ceiling, the overflow goes to the least loaded slot,",
27
27
  " so a burst past the summed ceilings degrades to even spreading and never blocks",
28
28
  "",
29
- "With this file: space-bunny takes 3 consecutive turns, then longcat-2.5-preview",
30
- "(opencode-go) takes 2, then big-pickle and longcat-2.5-preview (Zen) take 1 each -- sequence 1,1,1,2,2,3,4,...",
29
+ "With this file: big-pickle takes 3 consecutive turns, then minimax-m3.1-flash-free",
30
+ "(opencode-go) takes 2, then deepseek-v4.1-flash-free and glm-5.3-flash-free take 1",
31
+ "each -- sequence 1,1,1,2,2,3,4,...",
31
32
  "Equal weights give a plain round robin in list order. 2,2,1,1 reproduces the old",
32
33
  "primary/overflow cycle exactly, which is why the old tier values still work.",
33
34
  "",
35
+ "WEIGHTS DESCEND, AND THAT IS NOT COSMETIC. The rotation is order-first: the run is",
36
+ "expanded in declaration order, so the heaviest slot must come first for a skip to",
37
+ "land on the heaviest slot that is LEFT. A heavier slot declared after a lighter one",
38
+ "is unreachable in the way the pool means.",
39
+ "",
40
+ "EVERY SLOT IS A MODEL THAT IS PROBABLY STILL THERE NEXT WEEK, AND NO TWO SLOTS",
41
+ "SHARE A FAMILY. A free model can be withdrawn by its provider without warning --",
42
+ "space-bunny-free was pulled from the opencode provider while it was still this",
43
+ "file's first slot, and every new session then died on 'Model not found'. So the",
44
+ "slots here are the long-lived free ones (big-pickle has been on the opencode",
45
+ "provider for years), spread over two providers and four model families, because a",
46
+ "pool where every slot is one family is one withdrawal away from being empty. If a",
47
+ "slot does go away, the plugin warns at startup and the rest keep working; the fix",
48
+ "is a one-line edit here, not a redesign.",
49
+ "",
34
50
  "Round-robin rather than heaviest-first because re-prioritising the heaviest slot on",
35
51
  "EVERY spawn drained the fastest provider's free-tier window early: one provider",
36
52
  "absorbed all work up to its ceiling and kept being refilled the moment a claim",
@@ -67,9 +83,9 @@
67
83
  "Changing this file requires restarting every opencode process: plugins are not hot-reloaded."
68
84
  ],
69
85
  "slots": [
70
- { "model": "opencode/space-bunny-free", "weight": 3 },
71
- { "model": "opencode-go/longcat-2.5-preview-free", "weight": 2 },
72
- { "model": "opencode/big-pickle", "weight": 1 },
73
- { "model": "opencode/longcat-2.5-preview-free", "weight": 1 }
86
+ { "model": "opencode/big-pickle", "weight": 3 },
87
+ { "model": "opencode-go/minimax-m3.1-flash-free", "weight": 2 },
88
+ { "model": "opencode/deepseek-v4.1-flash-free", "weight": 1 },
89
+ { "model": "opencode/glm-5.3-flash-free", "weight": 1 }
74
90
  ]
75
- }
91
+ }