@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/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
- 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
  }
@@ -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 \`.opencode/context/\`. Never modify source code during a
1172
- review. Report what you found, what you fixed, and what you could not verify.
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 \`.opencode/context/\` is read-only for this run. Do not
1324
- modify source code under any circumstances.
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 use the always-registered \`auto\`.
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 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.`;
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
- if (Array.isArray(existing) && existing.length > 0) return { added: false };
1401
- cfg.instructions = [STANDING_INSTRUCTION];
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
- if (!isLimitText(haystack) && status !== 429) return;
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 hung = [...snap.own.values()].filter((c) => now - c.t > STUCK_MIN_AGE_MS && idleFor(s, c, now) >= STUCK_IDLE_MS).length;
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/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
- };
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 = JSON.parse(stripJsonc(raw)).agent;
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 mkdirSync2, openSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
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 manifest = JSON.parse(readFileSync4(join6(packageRoot, "package.json"), "utf8"));
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
- mkdirSync2(dirname3(path4), { recursive: true });
2936
- writeFileSync2(path4, JSON.stringify(state2));
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
- mkdirSync2(dirname3(path4), { recursive: true });
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: added the standing context-map rule (no user instructions were present)");
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);