@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 +51 -1
- package/README.md +27 -3
- package/dist/agents.js +67 -0
- package/dist/commands.js +92 -0
- package/dist/index.js +131 -6
- package/dist/pool.js +2 -2
- package/docs/CONFIGURATION.md +4 -2
- package/docs/INSTALL.md +2 -1
- package/docs/OMNIROUTE.md +1 -1
- package/package.json +4 -1
- package/presets/README.md +2 -2
- package/presets/free-tier.json +4 -4
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.
|
|
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
|
-
|
|
182
|
-
`/lacode-doctor`. Everything is added **only
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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, "
|
|
1770
|
+
const cooled = coolProvider(s, provider, until, reason2 || "rate-limited");
|
|
1650
1771
|
if (cooled.length > 0) {
|
|
1651
|
-
await log("warn", `
|
|
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
|
|
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/
|
|
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/
|
|
54
|
+
{ index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
|
|
55
55
|
];
|
|
56
56
|
function parseSlots(parsed, origin) {
|
|
57
57
|
const problems = [];
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -75,9 +75,11 @@ process.
|
|
|
75
75
|
|
|
76
76
|
## What gets injected
|
|
77
77
|
|
|
78
|
-
**
|
|
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`, `
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
39
|
+
opencode-go/longcat-2.5-preview-free 3
|
|
40
40
|
opencode/space-bunny-free 2
|
|
41
41
|
opencode/big-pickle 1
|
|
42
|
-
opencode/
|
|
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
|
package/presets/free-tier.json
CHANGED
|
@@ -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
|
|
29
|
-
"(opencode-go) takes 2, then big-pickle and
|
|
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/
|
|
70
|
+
{ "model": "opencode-go/longcat-2.5-preview-free", "weight": 2 },
|
|
71
71
|
{ "model": "opencode/big-pickle", "weight": 1 },
|
|
72
|
-
{ "model": "opencode/
|
|
72
|
+
{ "model": "opencode/longcat-2.5-preview-free", "weight": 1 }
|
|
73
73
|
]
|
|
74
74
|
}
|