@lawaty/lacode 0.4.1 → 0.5.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/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;
@@ -48,9 +49,9 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
48
49
  var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
49
50
  var FALLBACK_SLOTS = [
50
51
  { index: 1, model: "opencode/space-bunny-free", weight: 3 },
51
- { index: 2, model: "opencode-go/step-5-preview-free", weight: 2 },
52
+ { index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
52
53
  { index: 3, model: "opencode/big-pickle", weight: 1 },
53
- { index: 4, model: "opencode/step-5-preview-free", weight: 1 }
54
+ { index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
54
55
  ];
55
56
  function parseSlots(parsed, origin) {
56
57
  const problems = [];
@@ -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
- if (now - claim.t <= STUCK_MIN_AGE_MS) continue;
579
- if (idleFor(s, claim, now) < STUCK_IDLE_MS) continue;
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
  }
@@ -603,6 +611,9 @@ function coolProvider(s, provider, until, reason) {
603
611
  return affected;
604
612
  }
605
613
 
614
+ // src/lib/manager-directive.ts
615
+ var MANAGER_AGENT_NAME = "manager";
616
+
606
617
  // src/agents.ts
607
618
  var POOLED_BASES = ["explore", "implement", "context"];
608
619
  var AGENTS = {
@@ -1012,6 +1023,30 @@ You own exactly these five files and nothing else:
1012
1023
  during work that are not already recorded in the project's own decision
1013
1024
  log. Concise; variable length.
1014
1025
 
1026
+ A sixth file sits in the same directory and is not yours:
1027
+ \`.opencode/context/manager.md\`. It is the client's own statement of intent
1028
+ \u2014 what is being worked on, what comes next, what they have settled, and
1029
+ what every agent should keep in mind. It deliberately says nothing about
1030
+ where code lives: your five files and the source itself are the only
1031
+ authority on that, and this file never contradicts them. It has no fixed
1032
+ schema \u2014 read the whole file, because every heading in it is direction.
1033
+
1034
+ You never create, edit, rename, or delete it \u2014 not at bootstrap, not in an
1035
+ update, not in a review, and not when a run of your own concludes the work
1036
+ has moved on. The client edits it, or asks a live agent to edit it on
1037
+ their explicit instruction. Either way it is their hand, never yours.
1038
+
1039
+ Read it before you write anything. Its \`## Current\` and \`## Next\`
1040
+ sections tell you what is actually being worked on, and \`## Confirmed\`
1041
+ tells you which of your facts the client has ratified. A fact you are
1042
+ writing that appears under \`## Confirmed\` may be stated plainly, without
1043
+ a confidence marker; anything else follows your normal rules \u2014 infer
1044
+ carefully, mark uncertainty. Never copy its content into the five files.
1045
+
1046
+ A missing or empty section is not an instruction. It means the client has
1047
+ not said, not that you may assume anything \u2014 proceed exactly as you would
1048
+ without the file.
1049
+
1015
1050
  Never create, modify, or delete anything outside \`.opencode/context/\`. The
1016
1051
  \`edit\` permission enforces this. Do not attempt to work around it with bash.
1017
1052
 
@@ -1046,6 +1081,11 @@ Then write the five files. For each significant subsystem, give a short
1046
1081
  reason. This is the single most valuable thing you produce: where an agent
1047
1082
  should open its editor next.
1048
1083
 
1084
+ manager.md is created for the client by the plugin, not by you; if it is
1085
+ missing anyway, mention once in your final report that they can add it
1086
+ themselves to record what is being worked on, what comes next, and what
1087
+ they have ratified. Never create it.
1088
+
1049
1089
  # Incremental update
1050
1090
 
1051
1091
  Trigger: \`.opencode/context/\` exists and work has just been done here.
@@ -1073,6 +1113,14 @@ that is already stated in another context file \u2014 one fact has one home, and
1073
1113
  the other files link to it. If nothing material changed, make no edits and
1074
1114
  report "no update needed".
1075
1115
 
1116
+ Before editing, re-read manager.md: its \`## Current\` and \`## Next\`
1117
+ sections tell you what the client is actually working on, and its
1118
+ \`## Confirmed\` section settles which of your facts are ratified. If what
1119
+ you are about to write would place the code somewhere the client never
1120
+ described, trust the code, make the edit the code justifies, and report
1121
+ the gap in your final message \u2014 never reconcile the two sides silently,
1122
+ and never edit manager.md to close it.
1123
+
1076
1124
  # Minimal exploration
1077
1125
 
1078
1126
  Default shape:
@@ -1167,9 +1215,14 @@ Audit the five files for:
1167
1215
  - duplicated information \u2014 one canonical place per fact
1168
1216
  - missing canonical entry points for significant subsystems
1169
1217
  - unsupported assumptions \u2014 anything unmarked that you cannot verify
1218
+ - manager.md, if present: is \`## Current\` and \`## Next\` still true of
1219
+ recent work, is every \`## Confirmed\` entry still true of the code, has
1220
+ it outgrown roughly 150 lines \u2014 report findings, but never edit, prune,
1221
+ or reorganize it; that is the client's call
1170
1222
 
1171
- Fix problems only in \`.opencode/context/\`. Never modify source code during a
1172
- review. Report what you found, what you fixed, and what you could not verify.
1223
+ Fix problems only in the five files you own \u2014 never in manager.md, which
1224
+ belongs to the client. Never modify source code during a review. Report
1225
+ what you found, what you fixed, and what you could not verify.
1173
1226
 
1174
1227
  # Reporting
1175
1228
 
@@ -1187,7 +1240,9 @@ Finish with a short report:
1187
1240
  "edit": {
1188
1241
  "*": "deny",
1189
1242
  "*/.opencode/context/**": "allow",
1190
- ".opencode/context/**": "allow"
1243
+ ".opencode/context/**": "allow",
1244
+ "*/.opencode/context/manager.md": "deny",
1245
+ ".opencode/context/manager.md": "deny"
1191
1246
  },
1192
1247
  "bash": {
1193
1248
  "*": "deny",
@@ -1235,6 +1290,68 @@ Finish with a short report:
1235
1290
  }
1236
1291
  }
1237
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
+ };
1238
1355
  function buildAgentSet() {
1239
1356
  const set = { ...AGENTS };
1240
1357
  for (const base of POOLED_BASES) {
@@ -1250,6 +1367,7 @@ function buildAgentSet() {
1250
1367
  set[`${base}-${slot}`] = variant;
1251
1368
  }
1252
1369
  }
1370
+ set[MANAGER_AGENT_NAME] = MANAGER_AGENT;
1253
1371
  return set;
1254
1372
  }
1255
1373
  function applyAgents(cfg, agents = buildAgentSet(), opts = {}) {
@@ -1289,7 +1407,13 @@ Remember: write conclusions, not inventories. Link to the project's existing
1289
1407
  documentation instead of duplicating it.
1290
1408
 
1291
1409
  Report what you created or updated, what you deliberately left out, and
1292
- anything that contradicts the code or existing docs.`
1410
+ anything that contradicts the code or existing docs.
1411
+
1412
+ If \`.opencode/context/manager.md\` does not exist, mention in your final
1413
+ report that the client can add it themselves \u2014 their own file with
1414
+ \`## Current\`, \`## Next\`, \`## Confirmed\` and \`## Instructions\` \u2014 for what
1415
+ is being worked on, what comes next, what they have ratified, and standing
1416
+ direction. Never create it.`
1293
1417
  },
1294
1418
  "context-update": {
1295
1419
  description: "Incrementally update .opencode/context/ after recent work via the context agent.",
@@ -1320,8 +1444,9 @@ timestamp-only edits. If nothing material changed, make no edits and report
1320
1444
  subtask: true,
1321
1445
  template: `Context review.
1322
1446
 
1323
- Everything outside \`.opencode/context/\` is read-only for this run. Do not
1324
- modify source code under any circumstances.
1447
+ Everything outside the five context files is read-only for this run \u2014
1448
+ including \`manager.md\`, which belongs to the client. Do not modify source
1449
+ code under any circumstances.
1325
1450
 
1326
1451
  Audit the five context files for:
1327
1452
 
@@ -1334,9 +1459,22 @@ Audit the five context files for:
1334
1459
  - missing canonical entry points for significant subsystems
1335
1460
  - unsupported assumptions \u2014 anything stated as fact that you cannot verify,
1336
1461
  or that lacks a confidence marker
1462
+ - \`manager.md\`, if present: is \`## Current\` and \`## Next\` still true of
1463
+ recent work, is every \`## Confirmed\` entry still true of the code, has it
1464
+ outgrown roughly 150 lines \u2014 report findings, but never edit it
1337
1465
 
1338
1466
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
1339
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.`
1340
1478
  },
1341
1479
  "lacode-doctor": {
1342
1480
  description: "Check the LaCode setup \u2014 pool slots, model resolution, and the OmniRoute gateway. Report only; changes nothing.",
@@ -1364,7 +1502,8 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
1364
1502
  trap: an id that is a valid gateway route but is NOT in the plugin's
1365
1503
  \`omniroute.models\` allowlist fails with
1366
1504
  \`ProviderModelNotFoundError: Model not found\`; the fix is to add that id to
1367
- the \`models\` array, or use the always-registered \`auto\`.
1505
+ the \`models\` array, or set it as \`combo\` \u2014 the combo id is registered
1506
+ whether or not the gateway advertises it, so it needs no allowlist entry.
1368
1507
 
1369
1508
  Change nothing. Print a final one-line summary of failures.`
1370
1509
  }
@@ -1383,22 +1522,43 @@ function applyCommands(cfg, commands = COMMANDS) {
1383
1522
  }
1384
1523
  return { added, kept };
1385
1524
  }
1525
+ var INSTRUCTION_MARKER = "<!-- lacode:context-map-instruction:v1 -->";
1526
+ var INSTRUCTION_OPT_OUT = "<!-- lacode:context-map-instruction:off -->";
1386
1527
  var STANDING_INSTRUCTION = `## Repository context map
1387
1528
 
1388
1529
  This project may keep a navigation map in \`.opencode/context/\`. Before broad
1389
1530
  exploration, check whether it exists (\`ls .opencode/context\` \u2014 never the \`glob\`
1390
- tool, which skips hidden directories). If it does, read only the file relevant to
1391
- the task \u2014 architecture.md, contexts.md, conventions.md, workflows.md,
1392
- decisions.md \u2014 then start from the canonical entry points it lists.
1393
-
1394
- Source code is authoritative: if the map disagrees with the code, trust the code,
1395
- say so, and never edit source to match the map. Never edit \`.opencode/context/\`
1396
- yourself \u2014 the context agent owns it. Say in your final report if your
1397
- work revealed something that belongs in the map.`;
1531
+ tool, which skips hidden directories). If \`manager.md\` exists there, read it
1532
+ first: it is the client's own statement of intent \u2014 what is being worked on,
1533
+ what comes next, \`## Confirmed\` lists what the client has ratified, and
1534
+ \`## Instructions\` holds standing managerial directions. It outranks every
1535
+ other context file and every inference of yours about what matters right now.
1536
+ It says nothing about where code lives, so it cannot contradict the code;
1537
+ treat its \`## Confirmed\` entries as settled decisions, not as a licence to
1538
+ skip reading the code. Any heading in it is direction, so read the whole
1539
+ file rather than only the sections above. The rest of the map remains usable,
1540
+ just unratified where \`## Confirmed\` does not cover it. Then read only the
1541
+ file relevant to the task \u2014 \`architecture.md\`, \`contexts.md\`,
1542
+ \`conventions.md\`, \`workflows.md\`, \`decisions.md\` \u2014 and start from the
1543
+ canonical entry points it lists.
1544
+
1545
+ Source code is authoritative: if the map disagrees with the code,
1546
+ trust the code and say so; never edit source to match it.
1547
+ Never edit the five context files yourself \u2014 the context agent owns them.
1548
+ \`manager.md\` is the client's own file and the context agent is explicitly
1549
+ permission-denied from writing it. You may write it only when the client
1550
+ explicitly asks you to in this conversation, and then only what they asked
1551
+ for; never on your own initiative, and never to "keep it up to date". Say in
1552
+ your final report if your work revealed something that belongs to the map, or
1553
+ if \`manager.md\` and the code disagree.`;
1398
1554
  function applyInstructions(cfg) {
1399
- const existing = cfg.instructions;
1400
- if (Array.isArray(existing) && existing.length > 0) return { added: false };
1401
- cfg.instructions = [STANDING_INSTRUCTION];
1555
+ const existing = Array.isArray(cfg.instructions) ? cfg.instructions : [];
1556
+ const texts = existing.filter((e) => typeof e === "string");
1557
+ if (texts.some((e) => e.includes(INSTRUCTION_OPT_OUT))) return { added: false };
1558
+ if (texts.some((e) => e.includes(INSTRUCTION_MARKER))) return { added: false };
1559
+ cfg.instructions = [...existing, `${INSTRUCTION_MARKER}
1560
+
1561
+ ${STANDING_INSTRUCTION}`];
1402
1562
  return { added: true };
1403
1563
  }
1404
1564
 
@@ -1443,6 +1603,19 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1443
1603
  const strike = recordStrike(s, claim.m, "hang", HANG_FALLBACK_MS);
1444
1604
  if (strike) await log2("warn", `cooling ${claim.m} for ${Math.round((strike.until - now) / 1e3)}s (hang)`);
1445
1605
  };
1606
+ const failOver = async (sessionID, model, reason) => {
1607
+ if (!sessionID) return;
1608
+ const entry = [...s.own.entries()].find(([, claim2]) => claim2.g === sessionID);
1609
+ if (!entry) return;
1610
+ const [key, claim] = entry;
1611
+ const kill = await abortSession(sessionID);
1612
+ release(s, key);
1613
+ await log(
1614
+ "warn",
1615
+ 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})`,
1616
+ { base: claim.v, model: claim.m, slot: claim.k, session: sessionID, aborted: kill.ok, reason }
1617
+ );
1618
+ };
1446
1619
  const reapStuck = async () => {
1447
1620
  if (s.reaping) return;
1448
1621
  s.reaping = true;
@@ -1451,7 +1624,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1451
1624
  const stuck = findStuck(s);
1452
1625
  if (stuck.length === 0) return;
1453
1626
  const released = [];
1454
- for (const { key, claim } of stuck) {
1627
+ for (const { key, claim, reason } of stuck) {
1455
1628
  if (!claim.g) backfillChild(s, claim);
1456
1629
  const idle = idleFor(s, claim, Date.now());
1457
1630
  const since = Number.isFinite(idle) ? `${Math.round(idle / 1e3)}s of silence` : "no child session ever bound";
@@ -1463,7 +1636,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1463
1636
  s.reaped++;
1464
1637
  await log(
1465
1638
  "warn",
1466
- `hung ${claim.v} on ${claim.m} after ${age}s with ${since}; aborted session ${claim.g}`,
1639
+ 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
1640
  { base: claim.v, model: claim.m, slot: claim.k, session: claim.g, aborted: true }
1468
1641
  );
1469
1642
  await coolForHang(claim, log);
@@ -1510,7 +1683,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1510
1683
  timer.unref?.();
1511
1684
  void log(
1512
1685
  "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`
1686
+ `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
1687
  );
1515
1688
  return {
1516
1689
  config: async (cfg) => {
@@ -1558,18 +1731,21 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1558
1731
  try {
1559
1732
  const props = event.properties ?? {};
1560
1733
  if (event.type === "session.status") {
1734
+ const sessionID = typeof props.sessionID === "string" ? props.sessionID : void 0;
1561
1735
  const status2 = props.status;
1562
- if (status2?.type === "retry" && status2.action?.reason === "free_tier_limit" && status2.action.provider) {
1736
+ const reason2 = status2?.action?.reason ?? "";
1737
+ if (status2?.type === "retry" && status2.action?.provider && (reason2 === "free_tier_limit" || isLimitText(reason2))) {
1563
1738
  const provider = status2.action.provider;
1564
1739
  const until = typeof status2.next === "number" && status2.next > Date.now() ? status2.next : Date.now() + HANG_FALLBACK_MS;
1565
- const cooled = coolProvider(s, provider, until, "free_tier_limit");
1740
+ const cooled = coolProvider(s, provider, until, reason2 || "rate-limited");
1566
1741
  if (cooled.length > 0) {
1567
- await log("warn", `free tier exhausted on provider ${provider}; cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
1742
+ await log("warn", `limit on provider ${provider} (${reason2 || "rate-limited"}); cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
1568
1743
  provider,
1569
1744
  until,
1570
1745
  models: cooled.map((c) => c.model)
1571
1746
  });
1572
1747
  }
1748
+ await failOver(sessionID, provider, reason2 || "rate-limited");
1573
1749
  }
1574
1750
  return;
1575
1751
  }
@@ -1589,9 +1765,12 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1589
1765
  if (!info || info.role !== "assistant" || !info.error) return;
1590
1766
  const haystack = JSON.stringify(info.error) ?? "";
1591
1767
  const status = typeof info.error.data?.statusCode === "number" ? info.error.data.statusCode : void 0;
1592
- if (!isLimitText(haystack) && status !== 429) return;
1768
+ const limited = isLimitText(haystack) || status === 429;
1769
+ const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
1770
+ const transientGateway = gateway5xx || isTransientGatewayText(haystack);
1771
+ if (!limited && !transientGateway) return;
1593
1772
  const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
1594
- if (isUnavailableText(haystack)) {
1773
+ if (isUnavailableText(haystack) || !limited && transientGateway) {
1595
1774
  const transient = coolUnavailable(s, model);
1596
1775
  if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
1597
1776
  return;
@@ -1600,6 +1779,7 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1600
1779
  const reason = haystack.match(/rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded/i)?.[0]?.toLowerCase().replace(/[\s_]/g, "-");
1601
1780
  const strike = recordStrike(s, model, reason ?? "rate-limited", stated);
1602
1781
  if (strike) await log("warn", `cooling ${model} for ${Math.round((strike.until - Date.now()) / 1e3)}s (${reason ?? "rate-limited"}, strike ${strike.strikes})`);
1782
+ await failOver(info.sessionID, model, reason ?? "rate-limited");
1603
1783
  } catch (error) {
1604
1784
  await log("error", `limit detection failed: ${String(error)}`);
1605
1785
  }
@@ -1683,7 +1863,9 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
1683
1863
  };
1684
1864
  const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
1685
1865
  const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
1686
- const hung = [...snap.own.values()].filter((c) => now - c.t > STUCK_MIN_AGE_MS && idleFor(s, c, now) >= STUCK_IDLE_MS).length;
1866
+ const verdicts = [...snap.own.values()].map((claim) => condemnReason(s, claim, now));
1867
+ const hung = verdicts.filter((verdict) => verdict === "hung").length;
1868
+ const overran = verdicts.filter((verdict) => verdict === "overran").length;
1687
1869
  return {
1688
1870
  title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
1689
1871
  output: [
@@ -1697,6 +1879,7 @@ round robin in slot order, each slot taking as many turns as its weight says, so
1697
1879
  "",
1698
1880
  `${total} claim(s) in flight across ${snap.siblings.length + 1} process(es).`,
1699
1881
  `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}.`,
1882
+ `overran past ${Math.round(MAX_CLAIM_AGE_MS / 6e4)}min: ${overran} (auto-aborted).`,
1700
1883
  `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
1884
  coolingNow.length === 0 ? "no models cooling down." : `cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`
1702
1885
  ].join("\n")
@@ -1713,28 +1896,10 @@ round robin in slot order, each slot taking as many turns as its weight says, so
1713
1896
  });
1714
1897
 
1715
1898
  // src/plugins/context-autoupdate.ts
1716
- import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
1899
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
1717
1900
  import * as path2 from "node:path";
1718
1901
 
1719
- // src/lib/writer-rule.ts
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
- };
1902
+ // src/lib/jsonc.ts
1738
1903
  var stripJsonc = (raw) => {
1739
1904
  let out = "";
1740
1905
  let i = 0;
@@ -1773,9 +1938,37 @@ var stripJsonc = (raw) => {
1773
1938
  }
1774
1939
  return out.replace(/,(\s*[}\]])/g, "$1");
1775
1940
  };
1941
+ var parseJsonc = (raw) => {
1942
+ try {
1943
+ return JSON.parse(stripJsonc(raw));
1944
+ } catch {
1945
+ return void 0;
1946
+ }
1947
+ };
1948
+
1949
+ // src/lib/writer-rule.ts
1950
+ import { readFileSync as readFileSync3 } from "node:fs";
1951
+ import * as path from "node:path";
1952
+ var CONTEXT_DIR = path.join(".opencode", "context");
1953
+ var WRITER = "context";
1954
+ var MANAGER_WRITER = MANAGER_AGENT_NAME;
1955
+ var toPosix = (value) => value.split(path.sep).join("/");
1956
+ var compilePattern = (pattern) => {
1957
+ const escaped = toPosix(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1958
+ return new RegExp(`^${escaped}$`, "s");
1959
+ };
1960
+ var winningRule = (rule, file) => {
1961
+ if (typeof rule === "string") return { pattern: "<whole tool>", action: rule };
1962
+ if (!rule || typeof rule !== "object") return void 0;
1963
+ let winner;
1964
+ for (const [pattern, action] of Object.entries(rule)) {
1965
+ if (compilePattern(pattern).test(file)) winner = { pattern, action: String(action) };
1966
+ }
1967
+ return winner;
1968
+ };
1776
1969
  var readAgents = (raw) => {
1777
1970
  try {
1778
- const agents = JSON.parse(stripJsonc(raw)).agent;
1971
+ const agents = parseJsonc(raw).agent;
1779
1972
  if (agents && typeof agents === "object" && !Array.isArray(agents)) return agents;
1780
1973
  return {};
1781
1974
  } catch {
@@ -1787,6 +1980,10 @@ var evaluatedForm = (mapDir, projectRoot) => {
1787
1980
  const absolute = toPosix(path.join(mapDir, "architecture.md"));
1788
1981
  return projectRoot ? toPosix(path.relative(projectRoot, absolute)) : absolute;
1789
1982
  };
1983
+ var managerForm = (mapDir, projectRoot) => {
1984
+ const absolute = toPosix(path.join(mapDir, "manager.md"));
1985
+ return projectRoot ? toPosix(path.relative(projectRoot, absolute)) : absolute;
1986
+ };
1790
1987
  var verifyWriterRule = (configPath, mapDir, projectRoot) => {
1791
1988
  let raw;
1792
1989
  try {
@@ -1818,6 +2015,19 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
1818
2015
  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
2016
  }
1820
2017
  }
2018
+ const managerPath = managerForm(mapDir, projectRoot);
2019
+ const agentDefs = readAgents(raw);
2020
+ if (agentDefs) {
2021
+ const writers = [];
2022
+ for (const [name, def] of Object.entries(agentDefs)) {
2023
+ if (name === MANAGER_WRITER) continue;
2024
+ const win = winningRule(permissionOf(def), managerPath);
2025
+ if (win?.action === "allow") writers.push(`${name} (edit rule "${win.pattern}": "${win.action}")`);
2026
+ }
2027
+ if (writers.length > 0) {
2028
+ 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.`;
2029
+ }
2030
+ }
1821
2031
  return void 0;
1822
2032
  };
1823
2033
 
@@ -1844,6 +2054,51 @@ var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1844
2054
  ".cache",
1845
2055
  ".git"
1846
2056
  ]);
2057
+ var MANAGER_TEMPLATE = `# Managerial directions
2058
+
2059
+ This file is yours. No agent writes it: the context agent is explicitly denied
2060
+ permission to touch this path, and nothing changes it except you, or an agent
2061
+ acting on your explicit instruction in that conversation.
2062
+
2063
+ It records what you want done and how you want it approached \u2014 what is being
2064
+ worked on, what comes next, what you have settled, and what every agent should
2065
+ keep in mind. It deliberately says nothing about where code lives: the source
2066
+ and the five generated context files are the only authority on that, so this
2067
+ file can never contradict the code.
2068
+
2069
+ Add any headings you like. The four below are a convenience, not a schema \u2014
2070
+ every heading in this file is direction.
2071
+
2072
+ ## Current
2073
+
2074
+ <!-- What is being worked on right now. Rewrite it; do not append a history. -->
2075
+
2076
+ ## Next
2077
+
2078
+ <!-- What comes next, in order. -->
2079
+
2080
+ ## Confirmed
2081
+
2082
+ <!-- What you have personally ratified. Nothing enters or leaves this list
2083
+ except by your hand \u2014 no agent promotes, prunes or rewrites it. -->
2084
+
2085
+ ## Instructions
2086
+
2087
+ <!-- Standing direction that applies beyond the current task: priorities, things
2088
+ to care about, constraints that are not obvious from the code. -->`;
2089
+ var ensureManagerFile = (root) => {
2090
+ try {
2091
+ const target = path2.join(root, ".opencode", "context", "manager.md");
2092
+ if (existsSync2(target)) return { created: false };
2093
+ mkdirSync2(path2.dirname(target), { recursive: true });
2094
+ writeFileSync2(target, MANAGER_TEMPLATE, { flag: "wx" });
2095
+ return { created: true };
2096
+ } catch (error) {
2097
+ if (error?.code === "EEXIST") return { created: false };
2098
+ console.warn(`[context-autoupdate] ensureManagerFile failed: ${error}`);
2099
+ return { created: false };
2100
+ }
2101
+ };
1847
2102
  var toPosix2 = (value) => value.split(path2.sep).join("/");
1848
2103
  var findProjectConfig = () => {
1849
2104
  let dir = import.meta.dir ?? path2.dirname(new URL(import.meta.url).pathname);
@@ -1887,6 +2142,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
1887
2142
  });
1888
2143
  const root = path2.resolve(directory);
1889
2144
  const contextAbs = path2.join(root, CONTEXT_DIR2);
2145
+ if (ensureManagerFile(root).created) void log("info", `created ${CONTEXT_DIR2}/manager.md`);
1890
2146
  const config = findProjectConfig();
1891
2147
  const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
1892
2148
  if (writerProblem) void log("error", writerProblem);
@@ -1946,7 +2202,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
1946
2202
  const requeue = () => {
1947
2203
  for (const file of files) pending.add(file);
1948
2204
  };
1949
- const mode = existsSync2(contextAbs) ? "update" : "bootstrap";
2205
+ const mode = existsSync2(path2.join(contextAbs, "contexts.md")) ? "update" : "bootstrap";
1950
2206
  init(state2, (m) => void log("warn", m));
1951
2207
  const snap = snapshot(state2);
1952
2208
  const slot = pickSlot(state2, snap);
@@ -2846,7 +3102,7 @@ var omniRouteHooks = async (input, options, deps = {}) => {
2846
3102
 
2847
3103
  // src/lib/selfupdate.ts
2848
3104
  import { spawn as spawn2 } from "node:child_process";
2849
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
3105
+ 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
3106
  import { homedir as homedir2 } from "node:os";
2851
3107
  import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
2852
3108
  var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -2871,9 +3127,17 @@ function compareSemver(a, b) {
2871
3127
  }
2872
3128
  return 0;
2873
3129
  }
2874
- function readInstalledVersion(packageRoot) {
3130
+ function readInstalledVersion(packageRoot, read = (path4) => {
3131
+ try {
3132
+ return readFileSync4(path4, "utf8");
3133
+ } catch {
3134
+ return void 0;
3135
+ }
3136
+ }) {
2875
3137
  try {
2876
- const manifest = JSON.parse(readFileSync4(join6(packageRoot, "package.json"), "utf8"));
3138
+ const raw = read(join6(packageRoot, "package.json"));
3139
+ if (raw === void 0) return void 0;
3140
+ const manifest = JSON.parse(raw);
2877
3141
  if (manifest.name !== PACKAGE_NAME) return void 0;
2878
3142
  return typeof manifest.version === "string" ? manifest.version : void 0;
2879
3143
  } catch {
@@ -2932,8 +3196,8 @@ function readState(path4 = STATE_PATH) {
2932
3196
  }
2933
3197
  function writeState(state2, path4 = STATE_PATH) {
2934
3198
  try {
2935
- mkdirSync2(dirname3(path4), { recursive: true });
2936
- writeFileSync2(path4, JSON.stringify(state2));
3199
+ mkdirSync3(dirname3(path4), { recursive: true });
3200
+ writeFileSync3(path4, JSON.stringify(state2));
2937
3201
  } catch {
2938
3202
  }
2939
3203
  }
@@ -2959,7 +3223,7 @@ function acquireLock(path4 = LOCK_PATH, now = Date.now()) {
2959
3223
  };
2960
3224
  };
2961
3225
  try {
2962
- mkdirSync2(dirname3(path4), { recursive: true });
3226
+ mkdirSync3(dirname3(path4), { recursive: true });
2963
3227
  return claim();
2964
3228
  } catch {
2965
3229
  let orphaned = false;
@@ -3148,7 +3412,7 @@ var LaCode = (async (input, options) => {
3148
3412
  await log("info", `commands: added ${cmds.added.join(", ")}; kept ${cmds.kept.join(", ") || "none"} already defined`);
3149
3413
  }
3150
3414
  if (applyInstructions(cfg).added) {
3151
- await log("info", "instructions: added the standing context-map rule (no user instructions were present)");
3415
+ await log("info", "instructions: ensured the standing context-map rule is present");
3152
3416
  }
3153
3417
  await omniroute.config?.(cfg);
3154
3418
  await pool.config?.(cfg);