@lawaty/lacode 0.4.0 → 0.4.2
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 +183 -0
- package/README.md +122 -548
- package/dist/agents.js +53 -16
- package/dist/commands.js +49 -15
- package/dist/index.js +248 -77
- package/dist/lacode.js +253 -11
- package/dist/pool.js +21 -4
- package/docs/CONFIGURATION.md +111 -0
- package/docs/INSTALL.md +209 -0
- package/docs/OMNIROUTE.md +232 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ var ORPHAN_TMP_MS = 60 * 1e3;
|
|
|
30
30
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
31
31
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
32
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
|
+
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
33
34
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
34
35
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
35
36
|
var MAX_WEIGHT = 100;
|
|
@@ -203,6 +204,8 @@ var LIMIT_PATTERNS = [
|
|
|
203
204
|
];
|
|
204
205
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text));
|
|
205
206
|
var isUnavailableText = (text) => UNAVAILABLE_PATTERN.test(text);
|
|
207
|
+
var TRANSIENT_GATEWAY_PATTERNS = [/queue budget/i, /exceeded queue/i, /aborted due to timeout/i, /timeout/i];
|
|
208
|
+
var isTransientGatewayText = (text) => TRANSIENT_GATEWAY_PATTERNS.some((re) => re.test(text));
|
|
206
209
|
var statedCooldown = (text) => {
|
|
207
210
|
const hm = /resets?\s+in\s+(\d+)\s*(?:h|hr|hour)s?\s*(\d+)?\s*(?:m|min|minute)?s?/i.exec(text);
|
|
208
211
|
if (hm) return (Number(hm[1]) * 60 + (hm[2] ? Number(hm[2]) : 0)) * 60 * 1e3;
|
|
@@ -291,7 +294,7 @@ function publish(s) {
|
|
|
291
294
|
if (!s.dirReady) return;
|
|
292
295
|
const now = Date.now();
|
|
293
296
|
const payload = {};
|
|
294
|
-
for (const [key, claim] of s.own) if (now - claim.t <= CLAIM_TTL_MS) payload[key] = claim;
|
|
297
|
+
for (const [key, claim] of s.own) if (now - (claim.hb ?? claim.t) <= CLAIM_TTL_MS) payload[key] = claim;
|
|
295
298
|
try {
|
|
296
299
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
297
300
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -510,6 +513,11 @@ function pickSlot(s, snap, options) {
|
|
|
510
513
|
function agentFor(base, slot) {
|
|
511
514
|
return `${base}-${slot}`;
|
|
512
515
|
}
|
|
516
|
+
function manualVariant(name) {
|
|
517
|
+
const direct = /^(.*)-([1-9][0-9]*)$/.exec(name);
|
|
518
|
+
if (!direct) return void 0;
|
|
519
|
+
return BASES.includes(direct[1]) ? direct[1] : void 0;
|
|
520
|
+
}
|
|
513
521
|
function acquire(s, base, slot, ownerSession, key, target) {
|
|
514
522
|
const now = Date.now();
|
|
515
523
|
s.own.set(key, { v: base, k: slot.index, m: slot.model, t: now, hb: now, s: ownerSession, g: target });
|
|
@@ -567,12 +575,17 @@ function idleFor(s, claim, now) {
|
|
|
567
575
|
const seen = s.activity.get(target);
|
|
568
576
|
return seen ? now - seen : Infinity;
|
|
569
577
|
}
|
|
578
|
+
function condemnReason(s, claim, now = Date.now()) {
|
|
579
|
+
const age = now - claim.t;
|
|
580
|
+
if (age > MAX_CLAIM_AGE_MS) return "overran";
|
|
581
|
+
if (age > STUCK_MIN_AGE_MS && idleFor(s, claim, now) >= STUCK_IDLE_MS) return "hung";
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
570
584
|
function findStuck(s, now = Date.now()) {
|
|
571
585
|
const stuck = [];
|
|
572
586
|
for (const [key, claim] of s.own) {
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
stuck.push({ key, claim });
|
|
587
|
+
const reason = condemnReason(s, claim, now);
|
|
588
|
+
if (reason) stuck.push({ key, claim, reason });
|
|
576
589
|
}
|
|
577
590
|
return stuck;
|
|
578
591
|
}
|
|
@@ -626,15 +639,9 @@ Every question of the form "what does this file do", "where is X handled", or
|
|
|
626
639
|
that proves it. Never one whole refactor handed over in a single piece \u2014
|
|
627
640
|
see \`# Task size\` below.
|
|
628
641
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
\`context-1\`..). The plugin owns which model every pooled spawn runs on: it
|
|
633
|
-
picks the next slot from the pool's weights, current load and cooldowns, and
|
|
634
|
-
rewrites your spawn to that variant for you. Naming a variant bypasses that
|
|
635
|
-
decision, defeats failover, and can pin work to a cooling or unhealthy model.
|
|
636
|
-
If you catch yourself choosing a slot number, you are doing the plugin's job \u2014
|
|
637
|
-
spawn \`explore\` and let it decide.
|
|
642
|
+
Spawn the BARE pooled base \u2014 \`explore\`, \`implement\` or \`context\`. The pool
|
|
643
|
+
refuses a \`-N\` variant, and a refusal means spawn the bare base, never a
|
|
644
|
+
different number.
|
|
638
645
|
|
|
639
646
|
# When the pool is unavailable
|
|
640
647
|
|
|
@@ -754,10 +761,8 @@ When you need to understand the codebase:
|
|
|
754
761
|
2. Invoke @explore with specific, focused instructions (e.g. "read src/auth.ts", "grep for 'migration' in *.py files", "list the src/ directory")
|
|
755
762
|
3. @explore will return its findings \u2014 you process them and plan accordingly
|
|
756
763
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
doing so bypasses the pool's weights, load and cooldowns, which is the one thing
|
|
760
|
-
the plugin exists to decide.
|
|
764
|
+
Spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`); the pool
|
|
765
|
+
refuses \`-N\` variants, and a refusal means spawn the bare base, not another number.
|
|
761
766
|
|
|
762
767
|
If the pool cannot serve a spawn \u2014 every slot cooling or exhausted, or repeated
|
|
763
768
|
failures \u2014 do NOT fall back to reading or planning from your own inspection.
|
|
@@ -1015,6 +1020,30 @@ You own exactly these five files and nothing else:
|
|
|
1015
1020
|
during work that are not already recorded in the project's own decision
|
|
1016
1021
|
log. Concise; variable length.
|
|
1017
1022
|
|
|
1023
|
+
A sixth file sits in the same directory and is not yours:
|
|
1024
|
+
\`.opencode/context/manager.md\`. It is the client's own statement of intent
|
|
1025
|
+
\u2014 what is being worked on, what comes next, what they have settled, and
|
|
1026
|
+
what every agent should keep in mind. It deliberately says nothing about
|
|
1027
|
+
where code lives: your five files and the source itself are the only
|
|
1028
|
+
authority on that, and this file never contradicts them. It has no fixed
|
|
1029
|
+
schema \u2014 read the whole file, because every heading in it is direction.
|
|
1030
|
+
|
|
1031
|
+
You never create, edit, rename, or delete it \u2014 not at bootstrap, not in an
|
|
1032
|
+
update, not in a review, and not when a run of your own concludes the work
|
|
1033
|
+
has moved on. The client edits it, or asks a live agent to edit it on
|
|
1034
|
+
their explicit instruction. Either way it is their hand, never yours.
|
|
1035
|
+
|
|
1036
|
+
Read it before you write anything. Its \`## Current\` and \`## Next\`
|
|
1037
|
+
sections tell you what is actually being worked on, and \`## Confirmed\`
|
|
1038
|
+
tells you which of your facts the client has ratified. A fact you are
|
|
1039
|
+
writing that appears under \`## Confirmed\` may be stated plainly, without
|
|
1040
|
+
a confidence marker; anything else follows your normal rules \u2014 infer
|
|
1041
|
+
carefully, mark uncertainty. Never copy its content into the five files.
|
|
1042
|
+
|
|
1043
|
+
A missing or empty section is not an instruction. It means the client has
|
|
1044
|
+
not said, not that you may assume anything \u2014 proceed exactly as you would
|
|
1045
|
+
without the file.
|
|
1046
|
+
|
|
1018
1047
|
Never create, modify, or delete anything outside \`.opencode/context/\`. The
|
|
1019
1048
|
\`edit\` permission enforces this. Do not attempt to work around it with bash.
|
|
1020
1049
|
|
|
@@ -1049,6 +1078,11 @@ Then write the five files. For each significant subsystem, give a short
|
|
|
1049
1078
|
reason. This is the single most valuable thing you produce: where an agent
|
|
1050
1079
|
should open its editor next.
|
|
1051
1080
|
|
|
1081
|
+
manager.md is created for the client by the plugin, not by you; if it is
|
|
1082
|
+
missing anyway, mention once in your final report that they can add it
|
|
1083
|
+
themselves to record what is being worked on, what comes next, and what
|
|
1084
|
+
they have ratified. Never create it.
|
|
1085
|
+
|
|
1052
1086
|
# Incremental update
|
|
1053
1087
|
|
|
1054
1088
|
Trigger: \`.opencode/context/\` exists and work has just been done here.
|
|
@@ -1076,6 +1110,14 @@ that is already stated in another context file \u2014 one fact has one home, and
|
|
|
1076
1110
|
the other files link to it. If nothing material changed, make no edits and
|
|
1077
1111
|
report "no update needed".
|
|
1078
1112
|
|
|
1113
|
+
Before editing, re-read manager.md: its \`## Current\` and \`## Next\`
|
|
1114
|
+
sections tell you what the client is actually working on, and its
|
|
1115
|
+
\`## Confirmed\` section settles which of your facts are ratified. If what
|
|
1116
|
+
you are about to write would place the code somewhere the client never
|
|
1117
|
+
described, trust the code, make the edit the code justifies, and report
|
|
1118
|
+
the gap in your final message \u2014 never reconcile the two sides silently,
|
|
1119
|
+
and never edit manager.md to close it.
|
|
1120
|
+
|
|
1079
1121
|
# Minimal exploration
|
|
1080
1122
|
|
|
1081
1123
|
Default shape:
|
|
@@ -1170,9 +1212,14 @@ Audit the five files for:
|
|
|
1170
1212
|
- duplicated information \u2014 one canonical place per fact
|
|
1171
1213
|
- missing canonical entry points for significant subsystems
|
|
1172
1214
|
- unsupported assumptions \u2014 anything unmarked that you cannot verify
|
|
1215
|
+
- manager.md, if present: is \`## Current\` and \`## Next\` still true of
|
|
1216
|
+
recent work, is every \`## Confirmed\` entry still true of the code, has
|
|
1217
|
+
it outgrown roughly 150 lines \u2014 report findings, but never edit, prune,
|
|
1218
|
+
or reorganize it; that is the client's call
|
|
1173
1219
|
|
|
1174
|
-
Fix problems only in
|
|
1175
|
-
|
|
1220
|
+
Fix problems only in the five files you own \u2014 never in manager.md, which
|
|
1221
|
+
belongs to the client. Never modify source code during a review. Report
|
|
1222
|
+
what you found, what you fixed, and what you could not verify.
|
|
1176
1223
|
|
|
1177
1224
|
# Reporting
|
|
1178
1225
|
|
|
@@ -1190,7 +1237,9 @@ Finish with a short report:
|
|
|
1190
1237
|
"edit": {
|
|
1191
1238
|
"*": "deny",
|
|
1192
1239
|
"*/.opencode/context/**": "allow",
|
|
1193
|
-
".opencode/context/**": "allow"
|
|
1240
|
+
".opencode/context/**": "allow",
|
|
1241
|
+
"*/.opencode/context/manager.md": "deny",
|
|
1242
|
+
".opencode/context/manager.md": "deny"
|
|
1194
1243
|
},
|
|
1195
1244
|
"bash": {
|
|
1196
1245
|
"*": "deny",
|
|
@@ -1292,7 +1341,13 @@ Remember: write conclusions, not inventories. Link to the project's existing
|
|
|
1292
1341
|
documentation instead of duplicating it.
|
|
1293
1342
|
|
|
1294
1343
|
Report what you created or updated, what you deliberately left out, and
|
|
1295
|
-
anything that contradicts the code or existing docs
|
|
1344
|
+
anything that contradicts the code or existing docs.
|
|
1345
|
+
|
|
1346
|
+
If \`.opencode/context/manager.md\` does not exist, mention in your final
|
|
1347
|
+
report that the client can add it themselves \u2014 their own file with
|
|
1348
|
+
\`## Current\`, \`## Next\`, \`## Confirmed\` and \`## Instructions\` \u2014 for what
|
|
1349
|
+
is being worked on, what comes next, what they have ratified, and standing
|
|
1350
|
+
direction. Never create it.`
|
|
1296
1351
|
},
|
|
1297
1352
|
"context-update": {
|
|
1298
1353
|
description: "Incrementally update .opencode/context/ after recent work via the context agent.",
|
|
@@ -1323,8 +1378,9 @@ timestamp-only edits. If nothing material changed, make no edits and report
|
|
|
1323
1378
|
subtask: true,
|
|
1324
1379
|
template: `Context review.
|
|
1325
1380
|
|
|
1326
|
-
Everything outside
|
|
1327
|
-
|
|
1381
|
+
Everything outside the five context files is read-only for this run \u2014
|
|
1382
|
+
including \`manager.md\`, which belongs to the client. Do not modify source
|
|
1383
|
+
code under any circumstances.
|
|
1328
1384
|
|
|
1329
1385
|
Audit the five context files for:
|
|
1330
1386
|
|
|
@@ -1337,6 +1393,9 @@ Audit the five context files for:
|
|
|
1337
1393
|
- missing canonical entry points for significant subsystems
|
|
1338
1394
|
- unsupported assumptions \u2014 anything stated as fact that you cannot verify,
|
|
1339
1395
|
or that lacks a confidence marker
|
|
1396
|
+
- \`manager.md\`, if present: is \`## Current\` and \`## Next\` still true of
|
|
1397
|
+
recent work, is every \`## Confirmed\` entry still true of the code, has it
|
|
1398
|
+
outgrown roughly 150 lines \u2014 report findings, but never edit it
|
|
1340
1399
|
|
|
1341
1400
|
Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
|
|
1342
1401
|
and anything you could not verify.`
|
|
@@ -1367,7 +1426,8 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
|
1367
1426
|
trap: an id that is a valid gateway route but is NOT in the plugin's
|
|
1368
1427
|
\`omniroute.models\` allowlist fails with
|
|
1369
1428
|
\`ProviderModelNotFoundError: Model not found\`; the fix is to add that id to
|
|
1370
|
-
the \`models\` array, or
|
|
1429
|
+
the \`models\` array, or set it as \`combo\` \u2014 the combo id is registered
|
|
1430
|
+
whether or not the gateway advertises it, so it needs no allowlist entry.
|
|
1371
1431
|
|
|
1372
1432
|
Change nothing. Print a final one-line summary of failures.`
|
|
1373
1433
|
}
|
|
@@ -1386,22 +1446,43 @@ function applyCommands(cfg, commands = COMMANDS) {
|
|
|
1386
1446
|
}
|
|
1387
1447
|
return { added, kept };
|
|
1388
1448
|
}
|
|
1449
|
+
var INSTRUCTION_MARKER = "<!-- lacode:context-map-instruction:v1 -->";
|
|
1450
|
+
var INSTRUCTION_OPT_OUT = "<!-- lacode:context-map-instruction:off -->";
|
|
1389
1451
|
var STANDING_INSTRUCTION = `## Repository context map
|
|
1390
1452
|
|
|
1391
1453
|
This project may keep a navigation map in \`.opencode/context/\`. Before broad
|
|
1392
1454
|
exploration, check whether it exists (\`ls .opencode/context\` \u2014 never the \`glob\`
|
|
1393
|
-
tool, which skips hidden directories). If
|
|
1394
|
-
the
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1455
|
+
tool, which skips hidden directories). If \`manager.md\` exists there, read it
|
|
1456
|
+
first: it is the client's own statement of intent \u2014 what is being worked on,
|
|
1457
|
+
what comes next, \`## Confirmed\` lists what the client has ratified, and
|
|
1458
|
+
\`## Instructions\` holds standing managerial directions. It outranks every
|
|
1459
|
+
other context file and every inference of yours about what matters right now.
|
|
1460
|
+
It says nothing about where code lives, so it cannot contradict the code;
|
|
1461
|
+
treat its \`## Confirmed\` entries as settled decisions, not as a licence to
|
|
1462
|
+
skip reading the code. Any heading in it is direction, so read the whole
|
|
1463
|
+
file rather than only the sections above. The rest of the map remains usable,
|
|
1464
|
+
just unratified where \`## Confirmed\` does not cover it. Then read only the
|
|
1465
|
+
file relevant to the task \u2014 \`architecture.md\`, \`contexts.md\`,
|
|
1466
|
+
\`conventions.md\`, \`workflows.md\`, \`decisions.md\` \u2014 and start from the
|
|
1467
|
+
canonical entry points it lists.
|
|
1468
|
+
|
|
1469
|
+
Source code is authoritative: if the map disagrees with the code,
|
|
1470
|
+
trust the code and say so; never edit source to match it.
|
|
1471
|
+
Never edit the five context files yourself \u2014 the context agent owns them.
|
|
1472
|
+
\`manager.md\` is the client's own file and the context agent is explicitly
|
|
1473
|
+
permission-denied from writing it. You may write it only when the client
|
|
1474
|
+
explicitly asks you to in this conversation, and then only what they asked
|
|
1475
|
+
for; never on your own initiative, and never to "keep it up to date". Say in
|
|
1476
|
+
your final report if your work revealed something that belongs to the map, or
|
|
1477
|
+
if \`manager.md\` and the code disagree.`;
|
|
1401
1478
|
function applyInstructions(cfg) {
|
|
1402
|
-
const existing = cfg.instructions;
|
|
1403
|
-
|
|
1404
|
-
|
|
1479
|
+
const existing = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
1480
|
+
const texts = existing.filter((e) => typeof e === "string");
|
|
1481
|
+
if (texts.some((e) => e.includes(INSTRUCTION_OPT_OUT))) return { added: false };
|
|
1482
|
+
if (texts.some((e) => e.includes(INSTRUCTION_MARKER))) return { added: false };
|
|
1483
|
+
cfg.instructions = [...existing, `${INSTRUCTION_MARKER}
|
|
1484
|
+
|
|
1485
|
+
${STANDING_INSTRUCTION}`];
|
|
1405
1486
|
return { added: true };
|
|
1406
1487
|
}
|
|
1407
1488
|
|
|
@@ -1454,7 +1535,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1454
1535
|
const stuck = findStuck(s);
|
|
1455
1536
|
if (stuck.length === 0) return;
|
|
1456
1537
|
const released = [];
|
|
1457
|
-
for (const { key, claim } of stuck) {
|
|
1538
|
+
for (const { key, claim, reason } of stuck) {
|
|
1458
1539
|
if (!claim.g) backfillChild(s, claim);
|
|
1459
1540
|
const idle = idleFor(s, claim, Date.now());
|
|
1460
1541
|
const since = Number.isFinite(idle) ? `${Math.round(idle / 1e3)}s of silence` : "no child session ever bound";
|
|
@@ -1466,7 +1547,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1466
1547
|
s.reaped++;
|
|
1467
1548
|
await log(
|
|
1468
1549
|
"warn",
|
|
1469
|
-
`hung ${claim.v} on ${claim.m} after ${age}s with ${since}; aborted session ${claim.g}`,
|
|
1550
|
+
reason === "overran" ? `overran ${claim.v} on ${claim.m} after ${age}s; aborted session ${claim.g}` : `hung ${claim.v} on ${claim.m} after ${age}s with ${since}; aborted session ${claim.g}`,
|
|
1470
1551
|
{ base: claim.v, model: claim.m, slot: claim.k, session: claim.g, aborted: true }
|
|
1471
1552
|
);
|
|
1472
1553
|
await coolForHang(claim, log);
|
|
@@ -1513,7 +1594,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1513
1594
|
timer.unref?.();
|
|
1514
1595
|
void log(
|
|
1515
1596
|
"info",
|
|
1516
|
-
`reaper armed: every ${Math.round(reapMs / 1e3)}s, aborts a claim older than ${Math.round(STUCK_MIN_AGE_MS / 6e4)}m whose target has been silent ${Math.round(STUCK_IDLE_MS / 6e4)}m`
|
|
1597
|
+
`reaper armed: every ${Math.round(reapMs / 1e3)}s, aborts a claim older than ${Math.round(STUCK_MIN_AGE_MS / 6e4)}m whose target has been silent ${Math.round(STUCK_IDLE_MS / 6e4)}m, or any claim past the ${Math.round(MAX_CLAIM_AGE_MS / 6e4)}m ceiling however chatty`
|
|
1517
1598
|
);
|
|
1518
1599
|
return {
|
|
1519
1600
|
config: async (cfg) => {
|
|
@@ -1592,9 +1673,12 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1592
1673
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
1593
1674
|
const haystack = JSON.stringify(info.error) ?? "";
|
|
1594
1675
|
const status = typeof info.error.data?.statusCode === "number" ? info.error.data.statusCode : void 0;
|
|
1595
|
-
|
|
1676
|
+
const limited = isLimitText(haystack) || status === 429;
|
|
1677
|
+
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
1678
|
+
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
1679
|
+
if (!limited && !transientGateway) return;
|
|
1596
1680
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
1597
|
-
if (isUnavailableText(haystack)) {
|
|
1681
|
+
if (isUnavailableText(haystack) || !limited && transientGateway) {
|
|
1598
1682
|
const transient = coolUnavailable(s, model);
|
|
1599
1683
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
1600
1684
|
return;
|
|
@@ -1609,9 +1693,19 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1609
1693
|
},
|
|
1610
1694
|
"tool.execute.before": async (input, output) => {
|
|
1611
1695
|
if (input.tool !== "task") return;
|
|
1696
|
+
const requested = output.args?.subagent_type;
|
|
1697
|
+
if (typeof requested !== "string") return;
|
|
1698
|
+
const manual = manualVariant(requested);
|
|
1699
|
+
if (manual) {
|
|
1700
|
+
await log("warn", `refused manual spawn of slot variant ${requested}; spawn "${manual}" instead`, {
|
|
1701
|
+
variant: requested,
|
|
1702
|
+
base: manual
|
|
1703
|
+
});
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`${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.`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1612
1708
|
try {
|
|
1613
|
-
const requested = output.args?.subagent_type;
|
|
1614
|
-
if (typeof requested !== "string") return;
|
|
1615
1709
|
await reapStuck();
|
|
1616
1710
|
refreshCatalog(s, serverUrl);
|
|
1617
1711
|
const snap = snapshot(s);
|
|
@@ -1633,11 +1727,6 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1633
1727
|
});
|
|
1634
1728
|
return;
|
|
1635
1729
|
}
|
|
1636
|
-
const direct = /^(.*)-([1-9][0-9]*)$/.exec(requested);
|
|
1637
|
-
if (direct && BASES.includes(direct[1])) {
|
|
1638
|
-
const slot = s.slots.find((sl) => sl.index === Number(direct[2]));
|
|
1639
|
-
if (slot) acquire(s, direct[1], slot, input.sessionID, claimKey(input.sessionID, input.callID));
|
|
1640
|
-
}
|
|
1641
1730
|
} catch (error) {
|
|
1642
1731
|
await log("error", `routing failed, spawn unchanged: ${String(error)}`);
|
|
1643
1732
|
}
|
|
@@ -1681,7 +1770,9 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1681
1770
|
};
|
|
1682
1771
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
1683
1772
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
1684
|
-
const
|
|
1773
|
+
const verdicts = [...snap.own.values()].map((claim) => condemnReason(s, claim, now));
|
|
1774
|
+
const hung = verdicts.filter((verdict) => verdict === "hung").length;
|
|
1775
|
+
const overran = verdicts.filter((verdict) => verdict === "overran").length;
|
|
1685
1776
|
return {
|
|
1686
1777
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
1687
1778
|
output: [
|
|
@@ -1695,6 +1786,7 @@ round robin in slot order, each slot taking as many turns as its weight says, so
|
|
|
1695
1786
|
"",
|
|
1696
1787
|
`${total} claim(s) in flight across ${snap.siblings.length + 1} process(es).`,
|
|
1697
1788
|
`hung past ${Math.round(STUCK_MIN_AGE_MS / 6e4)}min with ${Math.round(STUCK_IDLE_MS / 6e4)}min silence: ${hung} (auto-aborted); reaped total: ${s.reaped}.`,
|
|
1789
|
+
`overran past ${Math.round(MAX_CLAIM_AGE_MS / 6e4)}min: ${overran} (auto-aborted).`,
|
|
1698
1790
|
`watchdog alive: last sweep ${s.lastSweep ? `${Math.max(0, Math.round((now - s.lastSweep) / 1e3))}s ago` : "never"} (every ${Math.round(reapMs / 1e3)}s).`,
|
|
1699
1791
|
coolingNow.length === 0 ? "no models cooling down." : `cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`
|
|
1700
1792
|
].join("\n")
|
|
@@ -1711,28 +1803,10 @@ round robin in slot order, each slot taking as many turns as its weight says, so
|
|
|
1711
1803
|
});
|
|
1712
1804
|
|
|
1713
1805
|
// src/plugins/context-autoupdate.ts
|
|
1714
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
1806
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1715
1807
|
import * as path2 from "node:path";
|
|
1716
1808
|
|
|
1717
|
-
// src/lib/
|
|
1718
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1719
|
-
import * as path from "node:path";
|
|
1720
|
-
var CONTEXT_DIR = path.join(".opencode", "context");
|
|
1721
|
-
var WRITER = "context";
|
|
1722
|
-
var toPosix = (value) => value.split(path.sep).join("/");
|
|
1723
|
-
var compilePattern = (pattern) => {
|
|
1724
|
-
const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1725
|
-
return new RegExp(`^${escaped}$`, "s");
|
|
1726
|
-
};
|
|
1727
|
-
var winningRule = (rule, file) => {
|
|
1728
|
-
if (typeof rule === "string") return { pattern: "<whole tool>", action: rule };
|
|
1729
|
-
if (!rule || typeof rule !== "object") return void 0;
|
|
1730
|
-
let winner;
|
|
1731
|
-
for (const [pattern, action] of Object.entries(rule)) {
|
|
1732
|
-
if (compilePattern(pattern).test(file)) winner = { pattern, action: String(action) };
|
|
1733
|
-
}
|
|
1734
|
-
return winner;
|
|
1735
|
-
};
|
|
1809
|
+
// src/lib/jsonc.ts
|
|
1736
1810
|
var stripJsonc = (raw) => {
|
|
1737
1811
|
let out = "";
|
|
1738
1812
|
let i = 0;
|
|
@@ -1771,9 +1845,36 @@ var stripJsonc = (raw) => {
|
|
|
1771
1845
|
}
|
|
1772
1846
|
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
1773
1847
|
};
|
|
1848
|
+
var parseJsonc = (raw) => {
|
|
1849
|
+
try {
|
|
1850
|
+
return JSON.parse(stripJsonc(raw));
|
|
1851
|
+
} catch {
|
|
1852
|
+
return void 0;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
// src/lib/writer-rule.ts
|
|
1857
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1858
|
+
import * as path from "node:path";
|
|
1859
|
+
var CONTEXT_DIR = path.join(".opencode", "context");
|
|
1860
|
+
var WRITER = "context";
|
|
1861
|
+
var toPosix = (value) => value.split(path.sep).join("/");
|
|
1862
|
+
var compilePattern = (pattern) => {
|
|
1863
|
+
const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1864
|
+
return new RegExp(`^${escaped}$`, "s");
|
|
1865
|
+
};
|
|
1866
|
+
var winningRule = (rule, file) => {
|
|
1867
|
+
if (typeof rule === "string") return { pattern: "<whole tool>", action: rule };
|
|
1868
|
+
if (!rule || typeof rule !== "object") return void 0;
|
|
1869
|
+
let winner;
|
|
1870
|
+
for (const [pattern, action] of Object.entries(rule)) {
|
|
1871
|
+
if (compilePattern(pattern).test(file)) winner = { pattern, action: String(action) };
|
|
1872
|
+
}
|
|
1873
|
+
return winner;
|
|
1874
|
+
};
|
|
1774
1875
|
var readAgents = (raw) => {
|
|
1775
1876
|
try {
|
|
1776
|
-
const agents =
|
|
1877
|
+
const agents = parseJsonc(raw).agent;
|
|
1777
1878
|
if (agents && typeof agents === "object" && !Array.isArray(agents)) return agents;
|
|
1778
1879
|
return {};
|
|
1779
1880
|
} catch {
|
|
@@ -1785,6 +1886,10 @@ var evaluatedForm = (mapDir, projectRoot) => {
|
|
|
1785
1886
|
const absolute = toPosix(path.join(mapDir, "architecture.md"));
|
|
1786
1887
|
return projectRoot ? toPosix(path.relative(projectRoot, absolute)) : absolute;
|
|
1787
1888
|
};
|
|
1889
|
+
var managerForm = (mapDir, projectRoot) => {
|
|
1890
|
+
const absolute = toPosix(path.join(mapDir, "manager.md"));
|
|
1891
|
+
return projectRoot ? toPosix(path.relative(projectRoot, absolute)) : absolute;
|
|
1892
|
+
};
|
|
1788
1893
|
var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
1789
1894
|
let raw;
|
|
1790
1895
|
try {
|
|
@@ -1816,6 +1921,18 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
|
1816
1921
|
return `context map has more than one writer: ${offenders.join(", ")} would be granted the map by permission, evaluating "${form}". Only ${WRITER} may write the map. Narrow the grant and put the deny LAST -- opencode takes the LAST matching rule, so "${CONTEXT_DIR}/**": "deny" written after "*": "allow" is what denies it: "*": "allow", "*/${CONTEXT_DIR}/**": "deny", "${CONTEXT_DIR}/**": "deny". Both spellings are required because the evaluated path depends on the project root.`;
|
|
1817
1922
|
}
|
|
1818
1923
|
}
|
|
1924
|
+
const managerPath = managerForm(mapDir, projectRoot);
|
|
1925
|
+
const agentDefs = readAgents(raw);
|
|
1926
|
+
if (agentDefs) {
|
|
1927
|
+
const writers = [];
|
|
1928
|
+
for (const [name, def] of Object.entries(agentDefs)) {
|
|
1929
|
+
const win = winningRule(permissionOf(def), managerPath);
|
|
1930
|
+
if (win?.action === "allow") writers.push(`${name} (edit rule "${win.pattern}": "${win.action}")`);
|
|
1931
|
+
}
|
|
1932
|
+
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.`;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1819
1936
|
return void 0;
|
|
1820
1937
|
};
|
|
1821
1938
|
|
|
@@ -1842,6 +1959,51 @@ var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
1842
1959
|
".cache",
|
|
1843
1960
|
".git"
|
|
1844
1961
|
]);
|
|
1962
|
+
var MANAGER_TEMPLATE = `# Managerial directions
|
|
1963
|
+
|
|
1964
|
+
This file is yours. No agent writes it: the context agent is explicitly denied
|
|
1965
|
+
permission to touch this path, and nothing changes it except you, or an agent
|
|
1966
|
+
acting on your explicit instruction in that conversation.
|
|
1967
|
+
|
|
1968
|
+
It records what you want done and how you want it approached \u2014 what is being
|
|
1969
|
+
worked on, what comes next, what you have settled, and what every agent should
|
|
1970
|
+
keep in mind. It deliberately says nothing about where code lives: the source
|
|
1971
|
+
and the five generated context files are the only authority on that, so this
|
|
1972
|
+
file can never contradict the code.
|
|
1973
|
+
|
|
1974
|
+
Add any headings you like. The four below are a convenience, not a schema \u2014
|
|
1975
|
+
every heading in this file is direction.
|
|
1976
|
+
|
|
1977
|
+
## Current
|
|
1978
|
+
|
|
1979
|
+
<!-- What is being worked on right now. Rewrite it; do not append a history. -->
|
|
1980
|
+
|
|
1981
|
+
## Next
|
|
1982
|
+
|
|
1983
|
+
<!-- What comes next, in order. -->
|
|
1984
|
+
|
|
1985
|
+
## Confirmed
|
|
1986
|
+
|
|
1987
|
+
<!-- What you have personally ratified. Nothing enters or leaves this list
|
|
1988
|
+
except by your hand \u2014 no agent promotes, prunes or rewrites it. -->
|
|
1989
|
+
|
|
1990
|
+
## Instructions
|
|
1991
|
+
|
|
1992
|
+
<!-- Standing direction that applies beyond the current task: priorities, things
|
|
1993
|
+
to care about, constraints that are not obvious from the code. -->`;
|
|
1994
|
+
var ensureManagerFile = (root) => {
|
|
1995
|
+
try {
|
|
1996
|
+
const target = path2.join(root, ".opencode", "context", "manager.md");
|
|
1997
|
+
if (existsSync2(target)) return { created: false };
|
|
1998
|
+
mkdirSync2(path2.dirname(target), { recursive: true });
|
|
1999
|
+
writeFileSync2(target, MANAGER_TEMPLATE, { flag: "wx" });
|
|
2000
|
+
return { created: true };
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (error?.code === "EEXIST") return { created: false };
|
|
2003
|
+
console.warn(`[context-autoupdate] ensureManagerFile failed: ${error}`);
|
|
2004
|
+
return { created: false };
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
1845
2007
|
var toPosix2 = (value) => value.split(path2.sep).join("/");
|
|
1846
2008
|
var findProjectConfig = () => {
|
|
1847
2009
|
let dir = import.meta.dir ?? path2.dirname(new URL(import.meta.url).pathname);
|
|
@@ -1885,6 +2047,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
1885
2047
|
});
|
|
1886
2048
|
const root = path2.resolve(directory);
|
|
1887
2049
|
const contextAbs = path2.join(root, CONTEXT_DIR2);
|
|
2050
|
+
if (ensureManagerFile(root).created) void log("info", `created ${CONTEXT_DIR2}/manager.md`);
|
|
1888
2051
|
const config = findProjectConfig();
|
|
1889
2052
|
const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
|
|
1890
2053
|
if (writerProblem) void log("error", writerProblem);
|
|
@@ -1944,7 +2107,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
1944
2107
|
const requeue = () => {
|
|
1945
2108
|
for (const file of files) pending.add(file);
|
|
1946
2109
|
};
|
|
1947
|
-
const mode = existsSync2(contextAbs) ? "update" : "bootstrap";
|
|
2110
|
+
const mode = existsSync2(path2.join(contextAbs, "contexts.md")) ? "update" : "bootstrap";
|
|
1948
2111
|
init(state2, (m) => void log("warn", m));
|
|
1949
2112
|
const snap = snapshot(state2);
|
|
1950
2113
|
const slot = pickSlot(state2, snap);
|
|
@@ -2844,7 +3007,7 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2844
3007
|
|
|
2845
3008
|
// src/lib/selfupdate.ts
|
|
2846
3009
|
import { spawn as spawn2 } from "node:child_process";
|
|
2847
|
-
import { closeSync, mkdirSync as
|
|
3010
|
+
import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3, writeSync } from "node:fs";
|
|
2848
3011
|
import { homedir as homedir2 } from "node:os";
|
|
2849
3012
|
import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2850
3013
|
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -2869,9 +3032,17 @@ function compareSemver(a, b) {
|
|
|
2869
3032
|
}
|
|
2870
3033
|
return 0;
|
|
2871
3034
|
}
|
|
2872
|
-
function readInstalledVersion(packageRoot) {
|
|
3035
|
+
function readInstalledVersion(packageRoot, read = (path4) => {
|
|
3036
|
+
try {
|
|
3037
|
+
return readFileSync4(path4, "utf8");
|
|
3038
|
+
} catch {
|
|
3039
|
+
return void 0;
|
|
3040
|
+
}
|
|
3041
|
+
}) {
|
|
2873
3042
|
try {
|
|
2874
|
-
const
|
|
3043
|
+
const raw = read(join6(packageRoot, "package.json"));
|
|
3044
|
+
if (raw === void 0) return void 0;
|
|
3045
|
+
const manifest = JSON.parse(raw);
|
|
2875
3046
|
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
2876
3047
|
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
2877
3048
|
} catch {
|
|
@@ -2930,8 +3101,8 @@ function readState(path4 = STATE_PATH) {
|
|
|
2930
3101
|
}
|
|
2931
3102
|
function writeState(state2, path4 = STATE_PATH) {
|
|
2932
3103
|
try {
|
|
2933
|
-
|
|
2934
|
-
|
|
3104
|
+
mkdirSync3(dirname3(path4), { recursive: true });
|
|
3105
|
+
writeFileSync3(path4, JSON.stringify(state2));
|
|
2935
3106
|
} catch {
|
|
2936
3107
|
}
|
|
2937
3108
|
}
|
|
@@ -2957,7 +3128,7 @@ function acquireLock(path4 = LOCK_PATH, now = Date.now()) {
|
|
|
2957
3128
|
};
|
|
2958
3129
|
};
|
|
2959
3130
|
try {
|
|
2960
|
-
|
|
3131
|
+
mkdirSync3(dirname3(path4), { recursive: true });
|
|
2961
3132
|
return claim();
|
|
2962
3133
|
} catch {
|
|
2963
3134
|
let orphaned = false;
|
|
@@ -3146,7 +3317,7 @@ var LaCode = (async (input, options) => {
|
|
|
3146
3317
|
await log("info", `commands: added ${cmds.added.join(", ")}; kept ${cmds.kept.join(", ") || "none"} already defined`);
|
|
3147
3318
|
}
|
|
3148
3319
|
if (applyInstructions(cfg).added) {
|
|
3149
|
-
await log("info", "instructions:
|
|
3320
|
+
await log("info", "instructions: ensured the standing context-map rule is present");
|
|
3150
3321
|
}
|
|
3151
3322
|
await omniroute.config?.(cfg);
|
|
3152
3323
|
await pool.config?.(cfg);
|