@lawaty/lacode 0.4.1 → 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 +15 -0
- package/README.md +119 -551
- package/dist/agents.js +48 -3
- package/dist/commands.js +49 -15
- package/dist/index.js +226 -57
- package/dist/lacode.js +253 -11
- package/dist/pool.js +15 -4
- package/docs/CONFIGURATION.md +111 -0
- package/docs/INSTALL.md +209 -0
- package/docs/OMNIROUTE.md +232 -0
- package/package.json +1 -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)));
|
|
@@ -572,12 +575,17 @@ function idleFor(s, claim, now) {
|
|
|
572
575
|
const seen = s.activity.get(target);
|
|
573
576
|
return seen ? now - seen : Infinity;
|
|
574
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
|
+
}
|
|
575
584
|
function findStuck(s, now = Date.now()) {
|
|
576
585
|
const stuck = [];
|
|
577
586
|
for (const [key, claim] of s.own) {
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
stuck.push({ key, claim });
|
|
587
|
+
const reason = condemnReason(s, claim, now);
|
|
588
|
+
if (reason) stuck.push({ key, claim, reason });
|
|
581
589
|
}
|
|
582
590
|
return stuck;
|
|
583
591
|
}
|
|
@@ -1012,6 +1020,30 @@ You own exactly these five files and nothing else:
|
|
|
1012
1020
|
during work that are not already recorded in the project's own decision
|
|
1013
1021
|
log. Concise; variable length.
|
|
1014
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
|
+
|
|
1015
1047
|
Never create, modify, or delete anything outside \`.opencode/context/\`. The
|
|
1016
1048
|
\`edit\` permission enforces this. Do not attempt to work around it with bash.
|
|
1017
1049
|
|
|
@@ -1046,6 +1078,11 @@ Then write the five files. For each significant subsystem, give a short
|
|
|
1046
1078
|
reason. This is the single most valuable thing you produce: where an agent
|
|
1047
1079
|
should open its editor next.
|
|
1048
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
|
+
|
|
1049
1086
|
# Incremental update
|
|
1050
1087
|
|
|
1051
1088
|
Trigger: \`.opencode/context/\` exists and work has just been done here.
|
|
@@ -1073,6 +1110,14 @@ that is already stated in another context file \u2014 one fact has one home, and
|
|
|
1073
1110
|
the other files link to it. If nothing material changed, make no edits and
|
|
1074
1111
|
report "no update needed".
|
|
1075
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
|
+
|
|
1076
1121
|
# Minimal exploration
|
|
1077
1122
|
|
|
1078
1123
|
Default shape:
|
|
@@ -1167,9 +1212,14 @@ Audit the five files for:
|
|
|
1167
1212
|
- duplicated information \u2014 one canonical place per fact
|
|
1168
1213
|
- missing canonical entry points for significant subsystems
|
|
1169
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
|
|
1170
1219
|
|
|
1171
|
-
Fix problems only in
|
|
1172
|
-
|
|
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.
|
|
1173
1223
|
|
|
1174
1224
|
# Reporting
|
|
1175
1225
|
|
|
@@ -1187,7 +1237,9 @@ Finish with a short report:
|
|
|
1187
1237
|
"edit": {
|
|
1188
1238
|
"*": "deny",
|
|
1189
1239
|
"*/.opencode/context/**": "allow",
|
|
1190
|
-
".opencode/context/**": "allow"
|
|
1240
|
+
".opencode/context/**": "allow",
|
|
1241
|
+
"*/.opencode/context/manager.md": "deny",
|
|
1242
|
+
".opencode/context/manager.md": "deny"
|
|
1191
1243
|
},
|
|
1192
1244
|
"bash": {
|
|
1193
1245
|
"*": "deny",
|
|
@@ -1289,7 +1341,13 @@ Remember: write conclusions, not inventories. Link to the project's existing
|
|
|
1289
1341
|
documentation instead of duplicating it.
|
|
1290
1342
|
|
|
1291
1343
|
Report what you created or updated, what you deliberately left out, and
|
|
1292
|
-
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.`
|
|
1293
1351
|
},
|
|
1294
1352
|
"context-update": {
|
|
1295
1353
|
description: "Incrementally update .opencode/context/ after recent work via the context agent.",
|
|
@@ -1320,8 +1378,9 @@ timestamp-only edits. If nothing material changed, make no edits and report
|
|
|
1320
1378
|
subtask: true,
|
|
1321
1379
|
template: `Context review.
|
|
1322
1380
|
|
|
1323
|
-
Everything outside
|
|
1324
|
-
|
|
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.
|
|
1325
1384
|
|
|
1326
1385
|
Audit the five context files for:
|
|
1327
1386
|
|
|
@@ -1334,6 +1393,9 @@ Audit the five context files for:
|
|
|
1334
1393
|
- missing canonical entry points for significant subsystems
|
|
1335
1394
|
- unsupported assumptions \u2014 anything stated as fact that you cannot verify,
|
|
1336
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
|
|
1337
1399
|
|
|
1338
1400
|
Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
|
|
1339
1401
|
and anything you could not verify.`
|
|
@@ -1364,7 +1426,8 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
|
1364
1426
|
trap: an id that is a valid gateway route but is NOT in the plugin's
|
|
1365
1427
|
\`omniroute.models\` allowlist fails with
|
|
1366
1428
|
\`ProviderModelNotFoundError: Model not found\`; the fix is to add that id to
|
|
1367
|
-
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.
|
|
1368
1431
|
|
|
1369
1432
|
Change nothing. Print a final one-line summary of failures.`
|
|
1370
1433
|
}
|
|
@@ -1383,22 +1446,43 @@ function applyCommands(cfg, commands = COMMANDS) {
|
|
|
1383
1446
|
}
|
|
1384
1447
|
return { added, kept };
|
|
1385
1448
|
}
|
|
1449
|
+
var INSTRUCTION_MARKER = "<!-- lacode:context-map-instruction:v1 -->";
|
|
1450
|
+
var INSTRUCTION_OPT_OUT = "<!-- lacode:context-map-instruction:off -->";
|
|
1386
1451
|
var STANDING_INSTRUCTION = `## Repository context map
|
|
1387
1452
|
|
|
1388
1453
|
This project may keep a navigation map in \`.opencode/context/\`. Before broad
|
|
1389
1454
|
exploration, check whether it exists (\`ls .opencode/context\` \u2014 never the \`glob\`
|
|
1390
|
-
tool, which skips hidden directories). If
|
|
1391
|
-
the
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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.`;
|
|
1398
1478
|
function applyInstructions(cfg) {
|
|
1399
|
-
const existing = cfg.instructions;
|
|
1400
|
-
|
|
1401
|
-
|
|
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}`];
|
|
1402
1486
|
return { added: true };
|
|
1403
1487
|
}
|
|
1404
1488
|
|
|
@@ -1451,7 +1535,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1451
1535
|
const stuck = findStuck(s);
|
|
1452
1536
|
if (stuck.length === 0) return;
|
|
1453
1537
|
const released = [];
|
|
1454
|
-
for (const { key, claim } of stuck) {
|
|
1538
|
+
for (const { key, claim, reason } of stuck) {
|
|
1455
1539
|
if (!claim.g) backfillChild(s, claim);
|
|
1456
1540
|
const idle = idleFor(s, claim, Date.now());
|
|
1457
1541
|
const since = Number.isFinite(idle) ? `${Math.round(idle / 1e3)}s of silence` : "no child session ever bound";
|
|
@@ -1463,7 +1547,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1463
1547
|
s.reaped++;
|
|
1464
1548
|
await log(
|
|
1465
1549
|
"warn",
|
|
1466
|
-
`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}`,
|
|
1467
1551
|
{ base: claim.v, model: claim.m, slot: claim.k, session: claim.g, aborted: true }
|
|
1468
1552
|
);
|
|
1469
1553
|
await coolForHang(claim, log);
|
|
@@ -1510,7 +1594,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1510
1594
|
timer.unref?.();
|
|
1511
1595
|
void log(
|
|
1512
1596
|
"info",
|
|
1513
|
-
`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`
|
|
1514
1598
|
);
|
|
1515
1599
|
return {
|
|
1516
1600
|
config: async (cfg) => {
|
|
@@ -1589,9 +1673,12 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1589
1673
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
1590
1674
|
const haystack = JSON.stringify(info.error) ?? "";
|
|
1591
1675
|
const status = typeof info.error.data?.statusCode === "number" ? info.error.data.statusCode : void 0;
|
|
1592
|
-
|
|
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;
|
|
1593
1680
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
1594
|
-
if (isUnavailableText(haystack)) {
|
|
1681
|
+
if (isUnavailableText(haystack) || !limited && transientGateway) {
|
|
1595
1682
|
const transient = coolUnavailable(s, model);
|
|
1596
1683
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
1597
1684
|
return;
|
|
@@ -1683,7 +1770,9 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1683
1770
|
};
|
|
1684
1771
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
1685
1772
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
1686
|
-
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;
|
|
1687
1776
|
return {
|
|
1688
1777
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
1689
1778
|
output: [
|
|
@@ -1697,6 +1786,7 @@ round robin in slot order, each slot taking as many turns as its weight says, so
|
|
|
1697
1786
|
"",
|
|
1698
1787
|
`${total} claim(s) in flight across ${snap.siblings.length + 1} process(es).`,
|
|
1699
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).`,
|
|
1700
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).`,
|
|
1701
1791
|
coolingNow.length === 0 ? "no models cooling down." : `cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`
|
|
1702
1792
|
].join("\n")
|
|
@@ -1713,28 +1803,10 @@ round robin in slot order, each slot taking as many turns as its weight says, so
|
|
|
1713
1803
|
});
|
|
1714
1804
|
|
|
1715
1805
|
// src/plugins/context-autoupdate.ts
|
|
1716
|
-
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";
|
|
1717
1807
|
import * as path2 from "node:path";
|
|
1718
1808
|
|
|
1719
|
-
// src/lib/
|
|
1720
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1721
|
-
import * as path from "node:path";
|
|
1722
|
-
var CONTEXT_DIR = path.join(".opencode", "context");
|
|
1723
|
-
var WRITER = "context";
|
|
1724
|
-
var toPosix = (value) => value.split(path.sep).join("/");
|
|
1725
|
-
var compilePattern = (pattern) => {
|
|
1726
|
-
const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1727
|
-
return new RegExp(`^${escaped}$`, "s");
|
|
1728
|
-
};
|
|
1729
|
-
var winningRule = (rule, file) => {
|
|
1730
|
-
if (typeof rule === "string") return { pattern: "<whole tool>", action: rule };
|
|
1731
|
-
if (!rule || typeof rule !== "object") return void 0;
|
|
1732
|
-
let winner;
|
|
1733
|
-
for (const [pattern, action] of Object.entries(rule)) {
|
|
1734
|
-
if (compilePattern(pattern).test(file)) winner = { pattern, action: String(action) };
|
|
1735
|
-
}
|
|
1736
|
-
return winner;
|
|
1737
|
-
};
|
|
1809
|
+
// src/lib/jsonc.ts
|
|
1738
1810
|
var stripJsonc = (raw) => {
|
|
1739
1811
|
let out = "";
|
|
1740
1812
|
let i = 0;
|
|
@@ -1773,9 +1845,36 @@ var stripJsonc = (raw) => {
|
|
|
1773
1845
|
}
|
|
1774
1846
|
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
1775
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
|
+
};
|
|
1776
1875
|
var readAgents = (raw) => {
|
|
1777
1876
|
try {
|
|
1778
|
-
const agents =
|
|
1877
|
+
const agents = parseJsonc(raw).agent;
|
|
1779
1878
|
if (agents && typeof agents === "object" && !Array.isArray(agents)) return agents;
|
|
1780
1879
|
return {};
|
|
1781
1880
|
} catch {
|
|
@@ -1787,6 +1886,10 @@ var evaluatedForm = (mapDir, projectRoot) => {
|
|
|
1787
1886
|
const absolute = toPosix(path.join(mapDir, "architecture.md"));
|
|
1788
1887
|
return projectRoot ? toPosix(path.relative(projectRoot, absolute)) : absolute;
|
|
1789
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
|
+
};
|
|
1790
1893
|
var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
1791
1894
|
let raw;
|
|
1792
1895
|
try {
|
|
@@ -1818,6 +1921,18 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
|
1818
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.`;
|
|
1819
1922
|
}
|
|
1820
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
|
+
}
|
|
1821
1936
|
return void 0;
|
|
1822
1937
|
};
|
|
1823
1938
|
|
|
@@ -1844,6 +1959,51 @@ var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
1844
1959
|
".cache",
|
|
1845
1960
|
".git"
|
|
1846
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
|
+
};
|
|
1847
2007
|
var toPosix2 = (value) => value.split(path2.sep).join("/");
|
|
1848
2008
|
var findProjectConfig = () => {
|
|
1849
2009
|
let dir = import.meta.dir ?? path2.dirname(new URL(import.meta.url).pathname);
|
|
@@ -1887,6 +2047,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
1887
2047
|
});
|
|
1888
2048
|
const root = path2.resolve(directory);
|
|
1889
2049
|
const contextAbs = path2.join(root, CONTEXT_DIR2);
|
|
2050
|
+
if (ensureManagerFile(root).created) void log("info", `created ${CONTEXT_DIR2}/manager.md`);
|
|
1890
2051
|
const config = findProjectConfig();
|
|
1891
2052
|
const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
|
|
1892
2053
|
if (writerProblem) void log("error", writerProblem);
|
|
@@ -1946,7 +2107,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
1946
2107
|
const requeue = () => {
|
|
1947
2108
|
for (const file of files) pending.add(file);
|
|
1948
2109
|
};
|
|
1949
|
-
const mode = existsSync2(contextAbs) ? "update" : "bootstrap";
|
|
2110
|
+
const mode = existsSync2(path2.join(contextAbs, "contexts.md")) ? "update" : "bootstrap";
|
|
1950
2111
|
init(state2, (m) => void log("warn", m));
|
|
1951
2112
|
const snap = snapshot(state2);
|
|
1952
2113
|
const slot = pickSlot(state2, snap);
|
|
@@ -2846,7 +3007,7 @@ var omniRouteHooks = async (input, options, deps = {}) => {
|
|
|
2846
3007
|
|
|
2847
3008
|
// src/lib/selfupdate.ts
|
|
2848
3009
|
import { spawn as spawn2 } from "node:child_process";
|
|
2849
|
-
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";
|
|
2850
3011
|
import { homedir as homedir2 } from "node:os";
|
|
2851
3012
|
import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2852
3013
|
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -2871,9 +3032,17 @@ function compareSemver(a, b) {
|
|
|
2871
3032
|
}
|
|
2872
3033
|
return 0;
|
|
2873
3034
|
}
|
|
2874
|
-
function readInstalledVersion(packageRoot) {
|
|
3035
|
+
function readInstalledVersion(packageRoot, read = (path4) => {
|
|
3036
|
+
try {
|
|
3037
|
+
return readFileSync4(path4, "utf8");
|
|
3038
|
+
} catch {
|
|
3039
|
+
return void 0;
|
|
3040
|
+
}
|
|
3041
|
+
}) {
|
|
2875
3042
|
try {
|
|
2876
|
-
const
|
|
3043
|
+
const raw = read(join6(packageRoot, "package.json"));
|
|
3044
|
+
if (raw === void 0) return void 0;
|
|
3045
|
+
const manifest = JSON.parse(raw);
|
|
2877
3046
|
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
2878
3047
|
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
2879
3048
|
} catch {
|
|
@@ -2932,8 +3101,8 @@ function readState(path4 = STATE_PATH) {
|
|
|
2932
3101
|
}
|
|
2933
3102
|
function writeState(state2, path4 = STATE_PATH) {
|
|
2934
3103
|
try {
|
|
2935
|
-
|
|
2936
|
-
|
|
3104
|
+
mkdirSync3(dirname3(path4), { recursive: true });
|
|
3105
|
+
writeFileSync3(path4, JSON.stringify(state2));
|
|
2937
3106
|
} catch {
|
|
2938
3107
|
}
|
|
2939
3108
|
}
|
|
@@ -2959,7 +3128,7 @@ function acquireLock(path4 = LOCK_PATH, now = Date.now()) {
|
|
|
2959
3128
|
};
|
|
2960
3129
|
};
|
|
2961
3130
|
try {
|
|
2962
|
-
|
|
3131
|
+
mkdirSync3(dirname3(path4), { recursive: true });
|
|
2963
3132
|
return claim();
|
|
2964
3133
|
} catch {
|
|
2965
3134
|
let orphaned = false;
|
|
@@ -3148,7 +3317,7 @@ var LaCode = (async (input, options) => {
|
|
|
3148
3317
|
await log("info", `commands: added ${cmds.added.join(", ")}; kept ${cmds.kept.join(", ") || "none"} already defined`);
|
|
3149
3318
|
}
|
|
3150
3319
|
if (applyInstructions(cfg).added) {
|
|
3151
|
-
await log("info", "instructions:
|
|
3320
|
+
await log("info", "instructions: ensured the standing context-map rule is present");
|
|
3152
3321
|
}
|
|
3153
3322
|
await omniroute.config?.(cfg);
|
|
3154
3323
|
await pool.config?.(cfg);
|