@lawaty/lacode 0.4.2 → 0.6.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
@@ -9,6 +9,54 @@ commit as the changelog entry it describes.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.6.0] — a client-only /pool command, and README corrections
13
+
14
+ ### Added
15
+
16
+ - **`/pool <instruction>`** — a client-only command that rewrites this machine's
17
+ pool override, `~/.config/lacode/pool.txt`, from a short instruction. It
18
+ applies the loader's rules (at most 4 distinct slots, weight in (0, 100], line
19
+ order is the rotation), writes the WHOLE file rather than a patch, reports the
20
+ before → after table, and reminds you that nothing hot-reloads.
21
+
22
+ ### Fixed
23
+
24
+ - README: the count of agent keys deliberately *not* injected was stated as
25
+ three, not the five `docs/CONFIGURATION.md` lists; and the `lacode` CLI section
26
+ was missing `version`, `update`, `receive` and `tunnel setup`.
27
+ `docs/INSTALL.md`'s CLI list gained `version` and `update`. A stale test title
28
+ ("three commands") was corrected to match what it asserts.
29
+
30
+ ## [0.5.0] — a user-only /manager-directive, instant limit failover, and the longcat preset
31
+
32
+ Released on the npm **`beta`** dist-tag (not `latest`) while the project is
33
+ experimental; `latest` remains 0.4.2.
34
+
35
+ ### Added
36
+
37
+ - **`/manager-directive <text>`** — a client-only command that runs a hidden
38
+ `manager` agent to apply your instruction to `.opencode/context/manager.md`
39
+ (record it, move it between sections, rewrite `## Current`, …). The `manager`
40
+ agent is the only agent permitted to edit that file, its grant is scoped to
41
+ `manager.md` alone, and no other agent can spawn it — the command is the only
42
+ route in. The one-writer guard (`writer-rule.ts` Direction 3) now exempts
43
+ exactly that agent name and still reports every other writer.
44
+
45
+ ### Changed
46
+
47
+ - **Bundled pool preset** — `opencode-go/step-5-preview-free` and
48
+ `opencode/step-5-preview-free` were retired upstream. Slots 2 and 4 now name
49
+ `longcat-2.5-preview-free` on the same providers (`opencode-go`, `opencode`).
50
+ Update `~/.config/lacode/pool.txt` if you pinned the old ids there.
51
+
52
+ ### Fixed
53
+
54
+ - **Immediate failover on a usage limit** — when a limit-shaped assistant error or
55
+ a limit-shaped `session.status` retry names the session an in-flight claim is
56
+ running on, the pool aborts that session at once instead of leaving it for the
57
+ ~5-minute hang reaper. The claim is released and the next spawn routes to a slot
58
+ that answers; a limit on an unrelated session never kills a claim.
59
+
12
60
  ## [0.4.2] — fix silent claim eviction, unkillable chatty tasks, and gateway 5xx misclassification
13
61
 
14
62
  ### Fixed
@@ -166,7 +214,9 @@ commit as the changelog entry it describes.
166
214
 
167
215
  - First published release: pooled subagents over a shared set of models.
168
216
 
169
- [Unreleased]: https://github.com/lawaty/lacode/compare/v0.4.1...HEAD
217
+ [Unreleased]: https://github.com/lawaty/lacode/compare/v0.6.0...HEAD
218
+ [0.6.0]: https://github.com/lawaty/lacode/compare/v0.5.0...v0.6.0
219
+ [0.5.0]: https://github.com/lawaty/lacode/compare/v0.4.2...v0.5.0
170
220
  [0.4.1]: https://github.com/lawaty/lacode/compare/v0.4.0...v0.4.1
171
221
  [0.4.0]: https://github.com/lawaty/lacode/compare/v0.3.0...v0.4.0
172
222
  [0.3.0]: https://github.com/lawaty/lacode/compare/v0.2.2...v0.3.0
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  > Ultimate opencode setup for Go users
4
4
 
5
+ > **Beta — experimental, changing fast.** For the next month LaCode ships on the
6
+ > npm **`beta`** dist-tag rather than `latest`, so nothing pulls it by accident:
7
+ > install `@lawaty/lacode@beta`, or pin an exact version. Interfaces here can
8
+ > change between releases without a major bump while the project stabilises.
9
+
5
10
  LaCode is an opencode plugin. It routes delegated work onto a weighted pool of
6
11
  cheap models instead of the expensive one, reaps subagents that hang without
7
12
  telling anyone, keeps a maintained map of your repository so agents stop
@@ -178,11 +183,12 @@ only thing that works: naming a variant lets the caller choose the provider
178
183
  directly, which is exactly what the rotation and the per-slot ceiling exist to
179
184
  prevent, so it is refused.
180
185
 
181
- Four commands: `/context-init`, `/context-update`, `/context-review` and
182
- `/lacode-doctor`. Everything is added **only when your config does not already
186
+ Six commands: `/context-init`, `/context-update`, `/context-review`,
187
+ `/lacode-doctor`, `/manager-directive` and `/pool`. Everything is added **only
188
+ when your config does not already
183
189
  define it** — an agent or command you wrote yourself is left byte-identical,
184
190
  never patched or partially merged. `{"enabled": false}` turns the whole thing off
185
- without leaving a trace. The exact injection list, and the three keys that are
191
+ without leaving a trace. The exact injection list, and the five keys that are
186
192
  deliberately *not* injected, are in
187
193
  [docs/CONFIGURATION.md](docs/CONFIGURATION.md#what-gets-injected).
188
194
 
@@ -212,6 +218,8 @@ are reading:
212
218
  /context-update # incremental refresh; accepts a focus, e.g. /context-update src/payments
213
219
  /context-review # audit it for staleness, contradictions, duplication and bloat
214
220
  /lacode-doctor # check the pool, model resolution and the OmniRoute gateway. Report only
221
+ /manager-directive # record a managerial direction in manager.md — client-only, see below
222
+ /pool # rewrite the pool slot table in ~/.config/lacode/pool.txt; restart to apply
215
223
  ```
216
224
 
217
225
  The map is local and untracked — it describes your checkout on your machine, so
@@ -251,6 +259,15 @@ agent working in your live session may write it only when you explicitly ask
251
259
  it to, and only what you asked for — never on its own initiative, and never
252
260
  to "keep it up to date".
253
261
 
262
+ **`/manager-directive` — the one sanctioned write path.** Type
263
+ `/manager-directive <text>` and the `manager` agent applies your instruction to
264
+ `manager.md` — recording it, moving it between sections, rewriting `## Current`,
265
+ whatever you asked — then reports what it changed. It is a normal agent turn, not
266
+ a dead append: tell it what you want and review the result. The `manager` agent
267
+ is the one agent permitted to edit that file, its grant is scoped to
268
+ `manager.md` alone, and no other agent can spawn it — the command is the only
269
+ route in.
270
+
254
271
  ## Cost
255
272
 
256
273
  The mechanism is specific and small: bulk exploration, implementation and
@@ -322,6 +339,13 @@ must never be able to fail the process that tried to notify you. `lacode forward
322
339
  HOST -- COMMAND` runs a command on another host over ssh and exits with its
323
340
  status.
324
341
 
342
+ The full CLI is `notify`, `forward`, `doctor`, `version`, `update`, `receive`
343
+ and `tunnel setup`. `version` reports which copy of LaCode is running — and
344
+ whether opencode is loading that same copy — and `update [--check]` refreshes an
345
+ installed one. `receive` is the desktop end of the tunnel (the forced command a
346
+ remote key is restricted to) and `tunnel setup` provisions the key and the
347
+ forward.
348
+
325
349
  Configuration, the payload format, the receiver contract and holding the forward
326
350
  open are in [docs/TUNNEL.md](docs/TUNNEL.md).
327
351
 
package/dist/agents.js CHANGED
@@ -46,6 +46,9 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
46
46
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
47
47
  var CHILD_MEMORY_MS = 30 * 6e4;
48
48
 
49
+ // src/lib/manager-directive.ts
50
+ var MANAGER_AGENT_NAME = "manager";
51
+
49
52
  // src/agents.ts
50
53
  var POOLED_BASES = ["explore", "implement", "context"];
51
54
  var AGENTS = {
@@ -722,6 +725,68 @@ Finish with a short report:
722
725
  }
723
726
  }
724
727
  };
728
+ var MANAGER_AGENT = {
729
+ description: "Records and edits the client's .opencode/context/manager.md. Run only by the /manager-directive command; no agent or subagent can spawn it.",
730
+ mode: "subagent",
731
+ hidden: true,
732
+ temperature: 0.1,
733
+ prompt: `# Role
734
+
735
+ You maintain one file and nothing else: \`.opencode/context/manager.md\`, the
736
+ client's own statement of intent. You are not a coding agent and you touch no
737
+ other path.
738
+
739
+ You are run only by the \`/manager-directive\` command. The client tells you what
740
+ they want recorded or changed in the file; you apply that to the file and report
741
+ what you changed. You have no permission to edit anything else, and you need
742
+ none.
743
+
744
+ # The file
745
+
746
+ It records what is being worked on (\`## Current\`), what comes next
747
+ (\`## Next\`), what the client has ratified (\`## Confirmed\`), and standing
748
+ direction (\`## Instructions\`). The client may use any headings \u2014 every heading
749
+ is direction, and the four above are a convenience, not a schema.
750
+
751
+ # How to apply a directive
752
+
753
+ - Do what the client asked. "Append", "rewrite Current", "move X to Confirmed",
754
+ "add a heading" all mean exactly that; a bare sentence goes under the most
755
+ fitting existing heading, and you create one only if none fits.
756
+ - Never invent facts, never record your own inference as the client's, and never
757
+ tidy, reorder or delete content the client did not ask you to touch.
758
+ - \`## Current\` is rewritten, not appended to, so it does not become a running
759
+ history.
760
+ - Keep the file concise. If it is growing past roughly 150 lines, say so.
761
+
762
+ # Reporting
763
+
764
+ Report briefly what you changed and where. Do not edit any other file.`,
765
+ permission: {
766
+ // Read is scoped to the one file too: the directive is about manager.md, and
767
+ // nothing else is any of this agent's business.
768
+ read: {
769
+ "*": "deny",
770
+ ".opencode/context/manager.md": "allow",
771
+ "*/.opencode/context/manager.md": "allow"
772
+ },
773
+ glob: "deny",
774
+ grep: "deny",
775
+ list: "deny",
776
+ edit: {
777
+ "*": "deny",
778
+ ".opencode/context/manager.md": "allow",
779
+ "*/.opencode/context/manager.md": "allow"
780
+ },
781
+ bash: "deny",
782
+ webfetch: "deny",
783
+ websearch: "deny",
784
+ question: "allow",
785
+ task: {
786
+ "*": "deny"
787
+ }
788
+ }
789
+ };
725
790
  function buildAgentSet() {
726
791
  const set = { ...AGENTS };
727
792
  for (const base of POOLED_BASES) {
@@ -737,6 +802,7 @@ function buildAgentSet() {
737
802
  set[`${base}-${slot}`] = variant;
738
803
  }
739
804
  }
805
+ set[MANAGER_AGENT_NAME] = MANAGER_AGENT;
740
806
  return set;
741
807
  }
742
808
  function applyAgents(cfg, agents = buildAgentSet(), opts = {}) {
@@ -756,6 +822,7 @@ function applyAgents(cfg, agents = buildAgentSet(), opts = {}) {
756
822
  }
757
823
  export {
758
824
  AGENTS,
825
+ MANAGER_AGENT,
759
826
  POOLED_BASES,
760
827
  applyAgents,
761
828
  buildAgentSet
package/dist/commands.js CHANGED
@@ -1,3 +1,55 @@
1
+ // src/lib/manager-directive.ts
2
+ var MANAGER_AGENT_NAME = "manager";
3
+
4
+ // src/lib/pool.ts
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join as join2 } from "node:path";
8
+
9
+ // src/lib/package-root.ts
10
+ import { readFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ var PACKAGE_NAME = "@lawaty/lacode";
14
+ var findPackageRoot = (start) => {
15
+ let dir = start;
16
+ for (let up = 0; up < 6; up++) {
17
+ try {
18
+ if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === PACKAGE_NAME) return dir;
19
+ } catch {
20
+ }
21
+ const parent = dirname(dir);
22
+ if (parent === dir) break;
23
+ dir = parent;
24
+ }
25
+ return join(start, "..", "..");
26
+ };
27
+ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
28
+
29
+ // src/lib/pool.ts
30
+ var CLAIM_TTL_MS = 10 * 60 * 1e3;
31
+ var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
32
+ var ORPHAN_TMP_MS = 60 * 1e3;
33
+ var CATALOG_TTL_MS = 10 * 60 * 1e3;
34
+ var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
35
+ var STUCK_IDLE_MS = 3 * 60 * 1e3;
36
+ var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
37
+ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
38
+ var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
39
+ var MAX_WEIGHT = 100;
40
+ var MAX_SLOTS = 4;
41
+ var COOLDOWN_BASE_MS = 60 * 1e3;
42
+ var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
43
+ var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
44
+ var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
45
+ var STRIKE_DECAY_MS = 30 * 60 * 1e3;
46
+ var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
47
+ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
48
+ var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
49
+ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
50
+ var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
51
+ var CHILD_MEMORY_MS = 30 * 6e4;
52
+
1
53
  // src/commands.ts
2
54
  var COMMANDS = {
3
55
  "context-init": {
@@ -77,6 +129,16 @@ Audit the five context files for:
77
129
 
78
130
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
79
131
  and anything you could not verify.`
132
+ },
133
+ "manager-directive": {
134
+ description: "Apply a client instruction to .opencode/context/manager.md. Client-only \u2014 runs the manager agent, the only writer of that file.",
135
+ agent: MANAGER_AGENT_NAME,
136
+ subtask: true,
137
+ template: `Apply this instruction to \`.opencode/context/manager.md\`, and edit only that file:
138
+
139
+ $ARGUMENTS
140
+
141
+ Follow the rules for the file in your system prompt, then report what you changed.`
80
142
  },
81
143
  "lacode-doctor": {
82
144
  description: "Check the LaCode setup \u2014 pool slots, model resolution, and the OmniRoute gateway. Report only; changes nothing.",
@@ -108,6 +170,36 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
108
170
  whether or not the gateway advertises it, so it needs no allowlist entry.
109
171
 
110
172
  Change nothing. Print a final one-line summary of failures.`
173
+ },
174
+ "pool": {
175
+ description: "Modify this machine's LaCode pool from a short instruction \u2014 the shared free-model slot table in ~/.config/lacode/pool.txt. Restarts opencode afterwards.",
176
+ template: `Apply this change to the LaCode pool on this machine:
177
+
178
+ $ARGUMENTS
179
+
180
+ The pool is ONE global slot table shared by every pooled base (explore, implement, context) \u2014 there are no per-base model lists, so "put X on explore" means "give X a slot", not a per-base override.
181
+
182
+ 1. Establish the baseline. Read \`${USER_MODELS_TXT}\`. If it is absent, the pool is resolving from the bundled preset \`presets/free-tier.json\`; read that file to learn what is in effect. Call the \`pool_status\` tool only if you need live load or cooldown state.
183
+
184
+ 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:
185
+ - one slot per line: \`<provider>/<model-id> [weight]\`
186
+ - at most ${MAX_SLOTS} slots, and a model may appear only once
187
+ - 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.
188
+ - \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
189
+ If the instruction names a model ambiguously, or would need more than ${MAX_SLOTS} slots, stop and ask rather than guessing.
190
+
191
+ 3. Write the WHOLE file, not a patch. This file is an override, not an edit target: any slot you drop disappears, and slot numbers are line positions, so a partial write silently renumbers the rotation. Keep the existing comment header. If the write fails on a missing directory, create \`~/.config/lacode\` first and retry.
192
+
193
+ 4. Report the before \u2192 after slot table, the resolved path, and this reminder in substance: nothing hot-reloads, so every running opencode process must be restarted before the new pool takes effect.
194
+
195
+ Two traps that fail silently, so check and state them explicitly:
196
+ - A slot's provider must already exist in the user's opencode config, or that slot cannot resolve at all. If the instruction adds a model on a provider that is not configured, say so.
197
+ - If a slot model is \`omniroute/<id>\`, that id must also appear in the plugin's \`omniroute.models\` allowlist (or be the \`combo\` id, which needs no entry) \u2014 otherwise the pool resolves and every spawn fails with \`ProviderModelNotFoundError\`.
198
+ - A provider \`whitelist\` is an allowlist, and the plugin appends your model to it at config time. Nothing for you to do there.
199
+
200
+ If the instruction is to RESET or REVERT rather than to change models, delete \`${USER_MODELS_TXT}\` instead, and report that the pool falls back to the bundled preset \u2014 with the same restart reminder.
201
+
202
+ Change nothing else. In particular do not touch \`presets/free-tier.json\`, \`~/.config/lacode/pool.json\`, \`FALLBACK_SLOTS\` in \`src/lib/pool.ts\`, or \`tests/models-snapshot.json\`: this command owns the per-machine override and nothing else.`
111
203
  }
112
204
  };
113
205
  function applyCommands(cfg, commands = COMMANDS) {
package/dist/index.js CHANGED
@@ -49,9 +49,9 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
49
49
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
50
50
  var FALLBACK_SLOTS = [
51
51
  { index: 1, model: "opencode/space-bunny-free", weight: 3 },
52
- { index: 2, model: "opencode-go/step-5-preview-free", weight: 2 },
52
+ { index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
53
53
  { index: 3, model: "opencode/big-pickle", weight: 1 },
54
- { index: 4, model: "opencode/step-5-preview-free", weight: 1 }
54
+ { index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
55
55
  ];
56
56
  function parseSlots(parsed, origin) {
57
57
  const problems = [];
@@ -611,6 +611,9 @@ function coolProvider(s, provider, until, reason) {
611
611
  return affected;
612
612
  }
613
613
 
614
+ // src/lib/manager-directive.ts
615
+ var MANAGER_AGENT_NAME = "manager";
616
+
614
617
  // src/agents.ts
615
618
  var POOLED_BASES = ["explore", "implement", "context"];
616
619
  var AGENTS = {
@@ -1287,6 +1290,68 @@ Finish with a short report:
1287
1290
  }
1288
1291
  }
1289
1292
  };
1293
+ var MANAGER_AGENT = {
1294
+ description: "Records and edits the client's .opencode/context/manager.md. Run only by the /manager-directive command; no agent or subagent can spawn it.",
1295
+ mode: "subagent",
1296
+ hidden: true,
1297
+ temperature: 0.1,
1298
+ prompt: `# Role
1299
+
1300
+ You maintain one file and nothing else: \`.opencode/context/manager.md\`, the
1301
+ client's own statement of intent. You are not a coding agent and you touch no
1302
+ other path.
1303
+
1304
+ You are run only by the \`/manager-directive\` command. The client tells you what
1305
+ they want recorded or changed in the file; you apply that to the file and report
1306
+ what you changed. You have no permission to edit anything else, and you need
1307
+ none.
1308
+
1309
+ # The file
1310
+
1311
+ It records what is being worked on (\`## Current\`), what comes next
1312
+ (\`## Next\`), what the client has ratified (\`## Confirmed\`), and standing
1313
+ direction (\`## Instructions\`). The client may use any headings \u2014 every heading
1314
+ is direction, and the four above are a convenience, not a schema.
1315
+
1316
+ # How to apply a directive
1317
+
1318
+ - Do what the client asked. "Append", "rewrite Current", "move X to Confirmed",
1319
+ "add a heading" all mean exactly that; a bare sentence goes under the most
1320
+ fitting existing heading, and you create one only if none fits.
1321
+ - Never invent facts, never record your own inference as the client's, and never
1322
+ tidy, reorder or delete content the client did not ask you to touch.
1323
+ - \`## Current\` is rewritten, not appended to, so it does not become a running
1324
+ history.
1325
+ - Keep the file concise. If it is growing past roughly 150 lines, say so.
1326
+
1327
+ # Reporting
1328
+
1329
+ Report briefly what you changed and where. Do not edit any other file.`,
1330
+ permission: {
1331
+ // Read is scoped to the one file too: the directive is about manager.md, and
1332
+ // nothing else is any of this agent's business.
1333
+ read: {
1334
+ "*": "deny",
1335
+ ".opencode/context/manager.md": "allow",
1336
+ "*/.opencode/context/manager.md": "allow"
1337
+ },
1338
+ glob: "deny",
1339
+ grep: "deny",
1340
+ list: "deny",
1341
+ edit: {
1342
+ "*": "deny",
1343
+ ".opencode/context/manager.md": "allow",
1344
+ "*/.opencode/context/manager.md": "allow"
1345
+ },
1346
+ bash: "deny",
1347
+ webfetch: "deny",
1348
+ websearch: "deny",
1349
+ question: "allow",
1350
+ task: {
1351
+ "*": "deny"
1352
+ }
1353
+ }
1354
+ };
1290
1355
  function buildAgentSet() {
1291
1356
  const set = { ...AGENTS };
1292
1357
  for (const base of POOLED_BASES) {
@@ -1302,6 +1367,7 @@ function buildAgentSet() {
1302
1367
  set[`${base}-${slot}`] = variant;
1303
1368
  }
1304
1369
  }
1370
+ set[MANAGER_AGENT_NAME] = MANAGER_AGENT;
1305
1371
  return set;
1306
1372
  }
1307
1373
  function applyAgents(cfg, agents = buildAgentSet(), opts = {}) {
@@ -1399,6 +1465,16 @@ Audit the five context files for:
1399
1465
 
1400
1466
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
1401
1467
  and anything you could not verify.`
1468
+ },
1469
+ "manager-directive": {
1470
+ description: "Apply a client instruction to .opencode/context/manager.md. Client-only \u2014 runs the manager agent, the only writer of that file.",
1471
+ agent: MANAGER_AGENT_NAME,
1472
+ subtask: true,
1473
+ template: `Apply this instruction to \`.opencode/context/manager.md\`, and edit only that file:
1474
+
1475
+ $ARGUMENTS
1476
+
1477
+ Follow the rules for the file in your system prompt, then report what you changed.`
1402
1478
  },
1403
1479
  "lacode-doctor": {
1404
1480
  description: "Check the LaCode setup \u2014 pool slots, model resolution, and the OmniRoute gateway. Report only; changes nothing.",
@@ -1430,6 +1506,36 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
1430
1506
  whether or not the gateway advertises it, so it needs no allowlist entry.
1431
1507
 
1432
1508
  Change nothing. Print a final one-line summary of failures.`
1509
+ },
1510
+ "pool": {
1511
+ description: "Modify this machine's LaCode pool from a short instruction \u2014 the shared free-model slot table in ~/.config/lacode/pool.txt. Restarts opencode afterwards.",
1512
+ template: `Apply this change to the LaCode pool on this machine:
1513
+
1514
+ $ARGUMENTS
1515
+
1516
+ The pool is ONE global slot table shared by every pooled base (explore, implement, context) \u2014 there are no per-base model lists, so "put X on explore" means "give X a slot", not a per-base override.
1517
+
1518
+ 1. Establish the baseline. Read \`${USER_MODELS_TXT}\`. If it is absent, the pool is resolving from the bundled preset \`presets/free-tier.json\`; read that file to learn what is in effect. Call the \`pool_status\` tool only if you need live load or cooldown state.
1519
+
1520
+ 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:
1521
+ - one slot per line: \`<provider>/<model-id> [weight]\`
1522
+ - at most ${MAX_SLOTS} slots, and a model may appear only once
1523
+ - 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.
1524
+ - \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
1525
+ If the instruction names a model ambiguously, or would need more than ${MAX_SLOTS} slots, stop and ask rather than guessing.
1526
+
1527
+ 3. Write the WHOLE file, not a patch. This file is an override, not an edit target: any slot you drop disappears, and slot numbers are line positions, so a partial write silently renumbers the rotation. Keep the existing comment header. If the write fails on a missing directory, create \`~/.config/lacode\` first and retry.
1528
+
1529
+ 4. Report the before \u2192 after slot table, the resolved path, and this reminder in substance: nothing hot-reloads, so every running opencode process must be restarted before the new pool takes effect.
1530
+
1531
+ Two traps that fail silently, so check and state them explicitly:
1532
+ - A slot's provider must already exist in the user's opencode config, or that slot cannot resolve at all. If the instruction adds a model on a provider that is not configured, say so.
1533
+ - If a slot model is \`omniroute/<id>\`, that id must also appear in the plugin's \`omniroute.models\` allowlist (or be the \`combo\` id, which needs no entry) \u2014 otherwise the pool resolves and every spawn fails with \`ProviderModelNotFoundError\`.
1534
+ - A provider \`whitelist\` is an allowlist, and the plugin appends your model to it at config time. Nothing for you to do there.
1535
+
1536
+ If the instruction is to RESET or REVERT rather than to change models, delete \`${USER_MODELS_TXT}\` instead, and report that the pool falls back to the bundled preset \u2014 with the same restart reminder.
1537
+
1538
+ Change nothing else. In particular do not touch \`presets/free-tier.json\`, \`~/.config/lacode/pool.json\`, \`FALLBACK_SLOTS\` in \`src/lib/pool.ts\`, or \`tests/models-snapshot.json\`: this command owns the per-machine override and nothing else.`
1433
1539
  }
1434
1540
  };
1435
1541
  function applyCommands(cfg, commands = COMMANDS) {
@@ -1527,6 +1633,19 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1527
1633
  const strike = recordStrike(s, claim.m, "hang", HANG_FALLBACK_MS);
1528
1634
  if (strike) await log2("warn", `cooling ${claim.m} for ${Math.round((strike.until - now) / 1e3)}s (hang)`);
1529
1635
  };
1636
+ const failOver = async (sessionID, model, reason) => {
1637
+ if (!sessionID) return;
1638
+ const entry = [...s.own.entries()].find(([, claim2]) => claim2.g === sessionID);
1639
+ if (!entry) return;
1640
+ const [key, claim] = entry;
1641
+ const kill = await abortSession(sessionID);
1642
+ release(s, key);
1643
+ await log(
1644
+ "warn",
1645
+ kill.ok ? `limit (${reason}) on ${model}; aborted in-flight ${claim.v} session ${sessionID} to fail over now` : `limit (${reason}) on ${model}; released in-flight ${claim.v} claim after a failed abort (${kill.why})`,
1646
+ { base: claim.v, model: claim.m, slot: claim.k, session: sessionID, aborted: kill.ok, reason }
1647
+ );
1648
+ };
1530
1649
  const reapStuck = async () => {
1531
1650
  if (s.reaping) return;
1532
1651
  s.reaping = true;
@@ -1642,18 +1761,21 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1642
1761
  try {
1643
1762
  const props = event.properties ?? {};
1644
1763
  if (event.type === "session.status") {
1764
+ const sessionID = typeof props.sessionID === "string" ? props.sessionID : void 0;
1645
1765
  const status2 = props.status;
1646
- if (status2?.type === "retry" && status2.action?.reason === "free_tier_limit" && status2.action.provider) {
1766
+ const reason2 = status2?.action?.reason ?? "";
1767
+ if (status2?.type === "retry" && status2.action?.provider && (reason2 === "free_tier_limit" || isLimitText(reason2))) {
1647
1768
  const provider = status2.action.provider;
1648
1769
  const until = typeof status2.next === "number" && status2.next > Date.now() ? status2.next : Date.now() + HANG_FALLBACK_MS;
1649
- const cooled = coolProvider(s, provider, until, "free_tier_limit");
1770
+ const cooled = coolProvider(s, provider, until, reason2 || "rate-limited");
1650
1771
  if (cooled.length > 0) {
1651
- await log("warn", `free tier exhausted on provider ${provider}; cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
1772
+ await log("warn", `limit on provider ${provider} (${reason2 || "rate-limited"}); cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
1652
1773
  provider,
1653
1774
  until,
1654
1775
  models: cooled.map((c) => c.model)
1655
1776
  });
1656
1777
  }
1778
+ await failOver(sessionID, provider, reason2 || "rate-limited");
1657
1779
  }
1658
1780
  return;
1659
1781
  }
@@ -1687,6 +1809,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1687
1809
  const reason = haystack.match(/rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded/i)?.[0]?.toLowerCase().replace(/[\s_]/g, "-");
1688
1810
  const strike = recordStrike(s, model, reason ?? "rate-limited", stated);
1689
1811
  if (strike) await log("warn", `cooling ${model} for ${Math.round((strike.until - Date.now()) / 1e3)}s (${reason ?? "rate-limited"}, strike ${strike.strikes})`);
1812
+ await failOver(info.sessionID, model, reason ?? "rate-limited");
1690
1813
  } catch (error) {
1691
1814
  await log("error", `limit detection failed: ${String(error)}`);
1692
1815
  }
@@ -1858,6 +1981,7 @@ import { readFileSync as readFileSync3 } from "node:fs";
1858
1981
  import * as path from "node:path";
1859
1982
  var CONTEXT_DIR = path.join(".opencode", "context");
1860
1983
  var WRITER = "context";
1984
+ var MANAGER_WRITER = MANAGER_AGENT_NAME;
1861
1985
  var toPosix = (value) => value.split(path.sep).join("/");
1862
1986
  var compilePattern = (pattern) => {
1863
1987
  const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
@@ -1926,11 +2050,12 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
1926
2050
  if (agentDefs) {
1927
2051
  const writers = [];
1928
2052
  for (const [name, def] of Object.entries(agentDefs)) {
2053
+ if (name === MANAGER_WRITER) continue;
1929
2054
  const win = winningRule(permissionOf(def), managerPath);
1930
2055
  if (win?.action === "allow") writers.push(`${name} (edit rule "${win.pattern}": "${win.action}")`);
1931
2056
  }
1932
2057
  if (writers.length > 0) {
1933
- return `${CONTEXT_DIR}/manager.md is writable by permission: ${writers.join(", ")}. It is the client's own file \u2014 what they are working on, what is ratified, what every agent must respect \u2014 and no agent writes it. Deny it LAST, after any broad allow, because opencode takes the LAST matching rule: "*": "allow", "*/${CONTEXT_DIR}/manager.md": "deny", "${CONTEXT_DIR}/manager.md": "deny". Declare this on every agent above, including \`${WRITER}\`: an agent you define yourself replaces the shipped one wholesale, permission and all.`;
2058
+ return `${CONTEXT_DIR}/manager.md is writable by permission: ${writers.join(", ")}. It is the client's own file \u2014 what they are working on, what is ratified, what every agent must respect \u2014 and only the \`${MANAGER_WRITER}\` agent (run solely by /manager-directive) may write it. Deny it LAST on every other agent, after any broad allow, because opencode takes the LAST matching rule: "*": "allow", "*/${CONTEXT_DIR}/manager.md": "deny", "${CONTEXT_DIR}/manager.md": "deny". Declare this on every agent above, including \`${WRITER}\`: an agent you define yourself replaces the shipped one wholesale, permission and all.`;
1934
2059
  }
1935
2060
  }
1936
2061
  return void 0;
package/dist/pool.js CHANGED
@@ -49,9 +49,9 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
49
49
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
50
50
  var FALLBACK_SLOTS = [
51
51
  { index: 1, model: "opencode/space-bunny-free", weight: 3 },
52
- { index: 2, model: "opencode-go/step-5-preview-free", weight: 2 },
52
+ { index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
53
53
  { index: 3, model: "opencode/big-pickle", weight: 1 },
54
- { index: 4, model: "opencode/step-5-preview-free", weight: 1 }
54
+ { index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
55
55
  ];
56
56
  function parseSlots(parsed, origin) {
57
57
  const problems = [];
@@ -75,9 +75,11 @@ process.
75
75
 
76
76
  ## What gets injected
77
77
 
78
- **17 agent keys**: the 5 bases `build`, `plan`, `explore`, `implement`,
78
+ **18 agent keys**: the 5 bases `build`, `plan`, `explore`, `implement`,
79
79
  `context`, plus one hidden
80
- `<base>-1`..`-4` pool variant for each of the 3 pooled bases.
80
+ `<base>-1`..`-4` pool variant for each of the 3 pooled bases, plus the hidden
81
+ `manager` agent — the one writer of `.opencode/context/manager.md`, reachable
82
+ only from the `/manager-directive` command.
81
83
 
82
84
  `build` and `plan` exist in opencode as built-ins, so on a stock config those
83
85
  two keys are already taken and merge-without-clobber keeps the built-in — the
package/docs/INSTALL.md CHANGED
@@ -182,7 +182,8 @@ or no GNU `timeout` — rather than passing silently or failing confusingly.
182
182
 
183
183
  ## The `lacode` CLI
184
184
 
185
- The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
185
+ The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `version`,
186
+ `update`, `receive`, `tunnel setup`) — see
186
187
  [Remote hosts](../README.md#remote-hosts). The plugin install does **not** put it on your
187
188
  `PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
188
189
  globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
package/docs/OMNIROUTE.md CHANGED
@@ -102,7 +102,7 @@ your pool like any other model:
102
102
  # ~/.config/lacode/pool.txt
103
103
  omniroute/auto 3
104
104
  opencode/space-bunny-free 2
105
- opencode-go/step-5-preview-free 1
105
+ opencode-go/longcat-2.5-preview-free 1
106
106
  opencode/big-pickle 1
107
107
  ```
108
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.4.2",
3
+ "version": "0.6.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",
@@ -60,5 +60,8 @@
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@opencode-ai/plugin": "1.18.34"
63
+ },
64
+ "publishConfig": {
65
+ "tag": "beta"
63
66
  }
64
67
  }
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/step-5-preview-free 3
39
+ opencode-go/longcat-2.5-preview-free 3
40
40
  opencode/space-bunny-free 2
41
41
  opencode/big-pickle 1
42
- opencode/step-5-preview-free 1
42
+ opencode/longcat-2.5-preview-free 1
43
43
  ```
44
44
 
45
45
  `#` starts a comment (whole-line or trailing), blank lines are ignored, and the
@@ -25,8 +25,8 @@
25
25
  " 3. once EVERY slot is at its ceiling, the overflow goes to the least loaded slot,",
26
26
  " so a burst past the summed ceilings degrades to even spreading and never blocks",
27
27
  "",
28
- "With this file: space-bunny takes 3 consecutive turns, then step-5-preview",
29
- "(opencode-go) takes 2, then big-pickle and step-5-preview (Zen) take 1 each -- sequence 1,1,1,2,2,3,4,...",
28
+ "With this file: space-bunny takes 3 consecutive turns, then longcat-2.5-preview",
29
+ "(opencode-go) takes 2, then big-pickle and longcat-2.5-preview (Zen) take 1 each -- sequence 1,1,1,2,2,3,4,...",
30
30
  "Equal weights give a plain round robin in list order. 2,2,1,1 reproduces the old",
31
31
  "primary/overflow cycle exactly, which is why the old tier values still work.",
32
32
  "",
@@ -67,8 +67,8 @@
67
67
  ],
68
68
  "slots": [
69
69
  { "model": "opencode/space-bunny-free", "weight": 3 },
70
- { "model": "opencode-go/step-5-preview-free", "weight": 2 },
70
+ { "model": "opencode-go/longcat-2.5-preview-free", "weight": 2 },
71
71
  { "model": "opencode/big-pickle", "weight": 1 },
72
- { "model": "opencode/step-5-preview-free", "weight": 1 }
72
+ { "model": "opencode/longcat-2.5-preview-free", "weight": 1 }
73
73
  ]
74
74
  }