@livx.cc/agentx 0.99.122 → 0.99.124

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
@@ -120,13 +120,13 @@ function elideImageParts(content, hit, stub) {
120
120
  }
121
121
  function mintImageRef(path, mime) {
122
122
  const token = randomBytes(16).toString("hex");
123
- let bytes = 0;
123
+ let bytes2 = 0;
124
124
  try {
125
- bytes = Math.ceil(statSync2(path).size * 4 / 3);
125
+ bytes2 = Math.ceil(statSync2(path).size * 4 / 3);
126
126
  } catch (e) {
127
127
  log3.warn(`image ref stat failed, weighing as 0: ${path}`, e);
128
128
  }
129
- refRegistry.set(token, { mime, path, bytes });
129
+ refRegistry.set(token, { mime, path, bytes: bytes2 });
130
130
  while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
131
131
  return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;
132
132
  }
@@ -799,6 +799,7 @@ var init_tools_structured = __esm({
799
799
  };
800
800
  writeTool = {
801
801
  name: "Write",
802
+ writes: true,
802
803
  description: "Create or overwrite a file with the given contents, creating parent directories as needed. Use for new files instead of `bash echo >`. To replace an EXISTING file you must Read it first \u2014 overwriting an un-Read file is refused (it would destroy unseen content).",
803
804
  parameters: {
804
805
  type: "object",
@@ -824,6 +825,7 @@ var init_tools_structured = __esm({
824
825
  };
825
826
  multiEditTool = {
826
827
  name: "MultiEdit",
828
+ writes: true,
827
829
  description: "Apply several exact-substring replacements to one file in order, in a single call. Requires a prior Read. Each `old_string` must be unique at the time it is applied. All-or-nothing: if any edit fails, none are written.",
828
830
  parameters: {
829
831
  type: "object",
@@ -865,6 +867,7 @@ var init_tools_structured = __esm({
865
867
  };
866
868
  applyEditsTool = {
867
869
  name: "ApplyEdits",
870
+ writes: true,
868
871
  description: "Apply edits across one or MORE files in a single call \u2014 for cross-file refactors (rename/extract/move). edits=[{path, old_string?, new_string}]. WITH old_string: replace that exact substring (must be UNIQUE in the file \u2014 add surrounding context; read fresh + verified, no prior Read needed). WITHOUT old_string: write new_string as the whole file (creates it + parent dirs; but to OVERWRITE a file that already exists you must Read it first, else use old_string to edit part of it). Locate sites first with Grep (its output shows the exact text). Atomic: validated across all files before any write.",
869
872
  parameters: {
870
873
  type: "object",
@@ -1579,6 +1582,7 @@ var init_tools = __esm({
1579
1582
  };
1580
1583
  bashTool = {
1581
1584
  name: "bash",
1585
+ writes: true,
1582
1586
  description: "Run a shell command. Supports ls, cat, grep, find, head, tail, echo, mkdir, rm, mv, cp, wc, pipes (|), redirects (>, >>), and chaining (&&, ||, ;). Best for: running tests/builds, file operations (mkdir/mv/rm), and piped workflows. For searching file contents, prefer `Grep` (structured results, no re-parse). For finding files by name, prefer `Glob`.",
1583
1587
  parameters: {
1584
1588
  type: "object",
@@ -1630,14 +1634,14 @@ var init_tools = __esm({
1630
1634
  if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
1631
1635
  return imageRefResult(real, IMG_MIME[ext]);
1632
1636
  }
1633
- const bytes = await fs.readFileBytes(path);
1634
- const b64 = Buffer.from(bytes).toString("base64");
1637
+ const bytes2 = await fs.readFileBytes(path);
1638
+ const b64 = Buffer.from(bytes2).toString("base64");
1635
1639
  return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });
1636
1640
  }
1637
1641
  const raw = await ctx.fs.readFile(path);
1638
1642
  if (isBinary(raw)) {
1639
- const bytes = await byteSize(ctx.fs, path, raw);
1640
- return `[binary file: ${path} \u2014 ${bytes} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;
1643
+ const bytes2 = await byteSize(ctx.fs, path, raw);
1644
+ return `[binary file: ${path} \u2014 ${bytes2} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;
1641
1645
  }
1642
1646
  ctx.readState.set(ctx.fs.resolvePath(path), raw);
1643
1647
  if (raw === "") return `[empty file: ${path} \u2014 0 bytes]`;
@@ -1656,6 +1660,7 @@ var init_tools = __esm({
1656
1660
  };
1657
1661
  editTool = {
1658
1662
  name: "Edit",
1663
+ writes: true,
1659
1664
  description: "Replace an exact substring in a file. Requires a prior Read of the same file. `old_string` must occur exactly once \u2014 include surrounding context to disambiguate.",
1660
1665
  parameters: {
1661
1666
  type: "object",
@@ -1967,8 +1972,8 @@ var init_tools_jobs = __esm({
1967
1972
  return [...this.jobs.values()].map((j) => this.info(j));
1968
1973
  }
1969
1974
  info(j) {
1970
- const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
1971
- return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
1975
+ const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes: bytes2, holds, result } = j;
1976
+ return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes: bytes2, holds, result };
1972
1977
  }
1973
1978
  /** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
1974
1979
  async wait(id, timeoutMs) {
@@ -2402,7 +2407,8 @@ var init_JailedFilesystem = __esm({
2402
2407
  violate(op, abs, rule) {
2403
2408
  this.note(op, abs, rule);
2404
2409
  if (rule === "deny" || rule === "confine") throw new Error(`File not found: ${abs}`);
2405
- throw new Error(`Blocked by filesystem jail (${rule}): ${abs}`);
2410
+ const hint = rule === "readonly" && this.options.readonlyHint ? ` \u2014 ${this.options.readonlyHint}` : "";
2411
+ throw new Error(`Blocked by filesystem jail (${rule}): ${abs}${hint}`);
2406
2412
  }
2407
2413
  /** Fire the audit hook without throwing (for boolean probes that must still return false). */
2408
2414
  note(op, abs, rule) {
@@ -2527,6 +2533,8 @@ var init_JailedFilesystem = __esm({
2527
2533
  deny = [...DEFAULT_DENY];
2528
2534
  /** Globs readable but not writable/deletable (the pinned constitution). */
2529
2535
  readonly = [];
2536
+ /** Appended to readonly-violation errors (e.g. "edit it via the kb tool instead"). */
2537
+ readonlyHint;
2530
2538
  /** If set, only paths matching these globs are accessible at all. */
2531
2539
  confineTo;
2532
2540
  /** Audit hook fired on every blocked operation (drives the circuit breaker). */
@@ -2792,6 +2800,7 @@ function makeRealShellTool(options) {
2792
2800
  };
2793
2801
  return {
2794
2802
  name: "Shell",
2803
+ writes: true,
2795
2804
  // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The
2796
2805
  // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like
2797
2806
  // shraga-ee, to the parent's session), so a background job started from the child would run
@@ -3281,69 +3290,39 @@ ${prompt} [y/N] `);
3281
3290
  }
3282
3291
  };
3283
3292
 
3284
- // src/relevance.ts
3285
- var STOP = /* @__PURE__ */ new Set([
3286
- "the",
3287
- "and",
3288
- "for",
3289
- "with",
3290
- "that",
3291
- "this",
3292
- "from",
3293
- "into",
3294
- "your",
3295
- "you",
3296
- "are",
3297
- "was",
3298
- "will",
3299
- "use",
3300
- "using",
3301
- "run",
3302
- "add",
3303
- "fix",
3304
- "make",
3305
- "file",
3306
- "files",
3307
- "code",
3308
- "please",
3309
- "need",
3310
- "want",
3311
- "should",
3312
- "all"
3313
- ]);
3314
- function tokenize(s) {
3315
- const out = /* @__PURE__ */ new Set();
3316
- for (const w of String(s ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? []) {
3317
- if (w.length >= 3 && !STOP.has(w)) out.add(w);
3318
- }
3319
- return out;
3320
- }
3321
- function idfWeights(corpus) {
3322
- const N = corpus.length;
3323
- if (N === 0) return /* @__PURE__ */ new Map();
3324
- const df = /* @__PURE__ */ new Map();
3325
- for (const doc of corpus) for (const t of tokenize(doc)) df.set(t, (df.get(t) ?? 0) + 1);
3326
- const idf = /* @__PURE__ */ new Map();
3327
- for (const [t, n] of df) idf.set(t, Math.log((N + 1) / (n + 1)) + 1);
3328
- return idf;
3329
- }
3330
- function relevanceScore(text, queryTokens, idf) {
3331
- if (queryTokens.size === 0) return 0;
3332
- const t = tokenize(text);
3333
- let score = 0;
3334
- for (const q of queryTokens) if (t.has(q)) score += idf?.get(q) ?? 1;
3335
- return score;
3336
- }
3337
- function topByRelevance(items, query, text, k, corpus) {
3338
- if (!Number.isInteger(k) || k < 1) return { kept: items, rest: [] };
3339
- if (items.length <= k || !query.trim()) return { kept: items, rest: [] };
3340
- const q = tokenize(query);
3341
- if (q.size === 0) return { kept: items.slice(0, k), rest: items.slice(k) };
3342
- const idf = corpus?.length ? idfWeights(corpus) : void 0;
3343
- const scored = items.map((x, i) => ({ i, s: relevanceScore(text(x), q, idf) }));
3344
- scored.sort((a, b) => b.s - a.s || a.i - b.i);
3345
- const keep = new Set(scored.slice(0, k).map((e) => e.i));
3346
- return { kept: items.filter((_, i) => keep.has(i)), rest: items.filter((_, i) => !keep.has(i)) };
3293
+ // src/catalog.ts
3294
+ var DEFAULT_CATALOG_BUDGET = 6e3;
3295
+ var enc = new TextEncoder();
3296
+ var bytes = (s) => enc.encode(s).length;
3297
+ function clip(s, n) {
3298
+ const cps = [...s];
3299
+ if (cps.length <= n) return s;
3300
+ return n <= 1 ? "\u2026" : cps.slice(0, n - 1).join("").trimEnd() + "\u2026";
3301
+ }
3302
+ function formatCatalog(entries, opts) {
3303
+ const budget = opts.budgetBytes ?? DEFAULT_CATALOG_BUDGET;
3304
+ const pre = opts.namePrefix ?? "";
3305
+ const wrap = (body) => [opts.title, body, opts.footer].filter((x) => x !== void 0 && x !== "").join("\n");
3306
+ const render = (max) => wrap(entries.map((e) => {
3307
+ const d = (e.description ?? "").replace(/\s+/g, " ").trim();
3308
+ const desc = d && max > 0 ? ` \u2014 ${clip(d, max)}` : "";
3309
+ return `- **${pre}${e.name}**${desc}${e.path ? ` (\`${e.path}\`)` : ""}`;
3310
+ }).join("\n"));
3311
+ const longest = Math.max(0, ...entries.map((e) => [...e.description ?? ""].length));
3312
+ const full = render(longest);
3313
+ if (bytes(full) <= budget) return full;
3314
+ if (bytes(render(0)) <= budget) {
3315
+ let lo = 0, hi = longest;
3316
+ while (lo < hi) {
3317
+ const mid = Math.ceil((lo + hi) / 2);
3318
+ if (bytes(render(mid)) <= budget) lo = mid;
3319
+ else hi = mid - 1;
3320
+ }
3321
+ return render(lo);
3322
+ }
3323
+ const names = wrap(entries.map((e) => `- **${pre}${e.name}**`).join("\n"));
3324
+ if (bytes(names) <= budget) return names;
3325
+ return wrap(`- ${entries.map((e) => pre + e.name).join(", ")}`);
3347
3326
  }
3348
3327
 
3349
3328
  // src/frontmatter.ts
@@ -3365,7 +3344,6 @@ function grabField(block, key) {
3365
3344
  }
3366
3345
 
3367
3346
  // src/commands.ts
3368
- var MAX_CATALOG = 25;
3369
3347
  function parseFrontmatter(md) {
3370
3348
  const { block, body } = splitFrontmatter(md);
3371
3349
  return { description: grabField(block, "description"), body };
@@ -3422,9 +3400,12 @@ async function scanCommands(fs, dir) {
3422
3400
  async function loadCommands(fs, dir, opts = {}) {
3423
3401
  const commands = await scanCommands(fs, dir);
3424
3402
  if (commands.length === 0) return { commands, catalog: "" };
3425
- const { kept, rest } = topByRelevance(commands, opts.relevanceHint ?? "", (c) => `${c.name} ${c.description}`, opts.max ?? MAX_CATALOG);
3426
- const catalog = "## Slash commands (reusable prompt templates)\n" + kept.map((c) => `- **/${c.name}** \u2014 ${c.description} (\`${c.path}\`)`).join("\n") + (rest.length ? `
3427
- - (${rest.length} more, names only \u2014 call \`SlashCommand\` to run): ${rest.map((c) => c.name).join(", ")}` : "") + "\nTo run one, call the `SlashCommand` tool with its name (and optional `args`); it returns the expanded template to act on.";
3403
+ const catalog = formatCatalog(commands, {
3404
+ title: "## Slash commands (reusable prompt templates)",
3405
+ footer: "To run one, call the `SlashCommand` tool with its name (and optional `args`); it returns the expanded template to act on.",
3406
+ namePrefix: "/",
3407
+ budgetBytes: opts.budgetBytes
3408
+ });
3428
3409
  const tool = {
3429
3410
  name: "SlashCommand",
3430
3411
  description: "Run a slash command (or skill) by name \u2014 returns its expanded prompt template/instructions (with your `args` substituted/appended). Resolves both the command and skill namespaces. Use when a task matches a listed command or skill.",
@@ -3452,7 +3433,6 @@ async function loadCommands(fs, dir, opts = {}) {
3452
3433
  }
3453
3434
 
3454
3435
  // src/skills.ts
3455
- var MAX_CATALOG2 = 25;
3456
3436
  function parseFrontmatter2(md) {
3457
3437
  const { block } = splitFrontmatter(md);
3458
3438
  const b = block || md.slice(0, 600);
@@ -3478,9 +3458,11 @@ async function scanSkills(fs, dir) {
3478
3458
  async function loadSkills(fs, dir, opts = {}) {
3479
3459
  const skills = await scanSkills(fs, dir);
3480
3460
  if (skills.length === 0) return { skills, catalog: "" };
3481
- const { kept, rest } = topByRelevance(skills, opts.relevanceHint ?? "", (s) => `${s.name} ${s.description}`, opts.max ?? MAX_CATALOG2);
3482
- const catalog = "## Skills (load one before acting on a matching task)\n" + kept.map((s) => `- **${s.name}** \u2014 ${s.description} (\`${s.path}\`)`).join("\n") + (rest.length ? `
3483
- - (${rest.length} more, names only \u2014 call \`Skill\` to load): ${rest.map((s) => s.name).join(", ")}` : "") + "\nWhen a task matches a skill, call the `Skill` tool with its name to load its full instructions.";
3461
+ const catalog = formatCatalog(skills, {
3462
+ title: "## Skills (load one before acting on a matching task)",
3463
+ footer: "When a task matches a skill, call the `Skill` tool with its name to load its full instructions.",
3464
+ budgetBytes: opts.budgetBytes
3465
+ });
3484
3466
  const tool = {
3485
3467
  name: "Skill",
3486
3468
  description: "Load a skill (or slash command) by name \u2014 returns its full instructions (SKILL.md / expanded template), with any `args` substituted/appended. Resolves both the skill and command namespaces. Use when a task matches a listed skill or command.",
@@ -3505,7 +3487,73 @@ async function loadSkills(fs, dir, opts = {}) {
3505
3487
 
3506
3488
  // src/memory.ts
3507
3489
  init_tools_structured();
3508
- var MAX_INDEX = 25;
3490
+
3491
+ // src/relevance.ts
3492
+ var STOP = /* @__PURE__ */ new Set([
3493
+ "the",
3494
+ "and",
3495
+ "for",
3496
+ "with",
3497
+ "that",
3498
+ "this",
3499
+ "from",
3500
+ "into",
3501
+ "your",
3502
+ "you",
3503
+ "are",
3504
+ "was",
3505
+ "will",
3506
+ "use",
3507
+ "using",
3508
+ "run",
3509
+ "add",
3510
+ "fix",
3511
+ "make",
3512
+ "file",
3513
+ "files",
3514
+ "code",
3515
+ "please",
3516
+ "need",
3517
+ "want",
3518
+ "should",
3519
+ "all"
3520
+ ]);
3521
+ function tokenize(s) {
3522
+ const out = /* @__PURE__ */ new Set();
3523
+ for (const w of String(s ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? []) {
3524
+ if (w.length >= 3 && !STOP.has(w)) out.add(w);
3525
+ }
3526
+ return out;
3527
+ }
3528
+ function idfWeights(corpus) {
3529
+ const N = corpus.length;
3530
+ if (N === 0) return /* @__PURE__ */ new Map();
3531
+ const df = /* @__PURE__ */ new Map();
3532
+ for (const doc of corpus) for (const t of tokenize(doc)) df.set(t, (df.get(t) ?? 0) + 1);
3533
+ const idf = /* @__PURE__ */ new Map();
3534
+ for (const [t, n] of df) idf.set(t, Math.log((N + 1) / (n + 1)) + 1);
3535
+ return idf;
3536
+ }
3537
+ function relevanceScore(text, queryTokens, idf) {
3538
+ if (queryTokens.size === 0) return 0;
3539
+ const t = tokenize(text);
3540
+ let score = 0;
3541
+ for (const q of queryTokens) if (t.has(q)) score += idf?.get(q) ?? 1;
3542
+ return score;
3543
+ }
3544
+ function topByRelevance(items, query, text, k, corpus) {
3545
+ if (!Number.isInteger(k) || k < 1) return { kept: items, rest: [] };
3546
+ if (items.length <= k || !query.trim()) return { kept: items, rest: [] };
3547
+ const q = tokenize(query);
3548
+ if (q.size === 0) return { kept: items.slice(0, k), rest: items.slice(k) };
3549
+ const idf = corpus?.length ? idfWeights(corpus) : void 0;
3550
+ const scored = items.map((x, i) => ({ i, s: relevanceScore(text(x), q, idf) }));
3551
+ scored.sort((a, b) => b.s - a.s || a.i - b.i);
3552
+ const keep = new Set(scored.slice(0, k).map((e) => e.i));
3553
+ return { kept: items.filter((_, i) => keep.has(i)), rest: items.filter((_, i) => !keep.has(i)) };
3554
+ }
3555
+
3556
+ // src/memory.ts
3509
3557
  var MAX_INDEX_LINES = 200;
3510
3558
  var MAX_INDEX_BYTES = 25e3;
3511
3559
  var MEMORY_PROMPT = '## Memory guidelines\n**When to Remember:** user corrections ("no, do X instead"), preferences ("I prefer Y"), project constraints, recurring gotchas, role/context ("I\'m a data scientist"), workflow patterns confirmed by the user. Capture the WHY, not just the what.\n**When NOT to Remember:** task-specific scratch, transient state, things derivable from code/git, conversation filler, anything already in instruction files. If removing the memory wouldn\'t change future behavior, don\'t save it.\n**Types** (pass to Remember \u2014 this determines where the memory is stored):\n- `user` \u2014 role, preferences, knowledge level, collaboration style (GLOBAL \u2014 follows you across projects)\n- `feedback` \u2014 corrections, what to avoid/repeat, with WHY (GLOBAL)\n- `project` \u2014 ongoing work, goals, constraints, deadlines (LOCAL to this project; convert relative dates to absolute)\n- `reference` \u2014 pointers to external resources, URLs, vendor docs (LOCAL to this project)\n**Before acting on a recalled memory:** verify it\'s still true \u2014 a memory naming a file/function/flag is a claim from when it was written. Grep/read to confirm before recommending. Stale memory \u2192 update via Remember (same slug overwrites).\n**Forget:** when the user asks to remove/delete/forget a memory, call `Forget` with its slug \u2014 it deletes the fact and its index line. Never "remove" by re-Remembering a note that says it was removed: that keeps the memory.\n**Dedup:** before writing, check if a similar memory exists (Recall or MemorySearch). Update the existing one rather than creating duplicates.\nCall `Recall` with a slug (or multiple slugs/a pattern) to load full bodies when relevant.\nCall `MemorySearch` with a query to find memories by content when you don\'t know the slug.\nCall `Remember` to persist a durable fact for future sessions.';
@@ -3540,10 +3588,18 @@ async function loadMemory(fs, dir, opts = {}) {
3540
3588
  }
3541
3589
  }
3542
3590
  if (!hasContent) return { index: MEMORY_PROMPT, tools };
3543
- const { kept, rest } = topByRelevance(allPointers, opts.relevanceHint ?? "", (l) => l, opts.max ?? MAX_INDEX);
3544
- const restSlugs = rest.map((l) => l.match(/\]\(([^)]+)\.md\)/)?.[1] ?? l.match(/\[([^\]]+)\]/)?.[1] ?? "").filter(Boolean);
3545
- const index = MEMORY_PROMPT + "\n\n## Memory index (persistent context \u2014 recalled across sessions)\n" + (header ? header + "\n" : "") + kept.join("\n") + (restSlugs.length ? `
3546
- - (${restSlugs.length} more learnings, slugs only \u2014 call \`Recall\` if relevant): ${restSlugs.join(", ")}` : "");
3591
+ const entries = allPointers.map((l) => {
3592
+ const m = l.match(/\]\(([^)]+)\.md\)/);
3593
+ const slug2 = m?.[1] ?? l;
3594
+ const title = m ? l.slice(l.indexOf("[") + 1, m.index).trim() : "";
3595
+ const hook = m ? l.slice(m.index + m[0].length).replace(/^\s*(?:[—:-]\s*)?/, "").trim() : "";
3596
+ return { name: slug2, description: title && title !== slug2 ? hook ? `${title} \u2014 ${hook}` : title : hook };
3597
+ });
3598
+ const index = MEMORY_PROMPT + "\n\n" + formatCatalog(entries, {
3599
+ title: "## Memory index (persistent context \u2014 recalled across sessions)" + (header ? "\n" + header : ""),
3600
+ footer: "Call `Recall` with a slug to load its full fact.",
3601
+ budgetBytes: opts.budgetBytes
3602
+ });
3547
3603
  return { index, tools };
3548
3604
  }
3549
3605
  var TERM_STOPWORDS = new Set(`the and for but not you your yours our ours his her hers its their they them this that these those
@@ -3708,8 +3764,8 @@ function memorySearchTool(fs, dirs) {
3708
3764
  return `Error: invalid regex: ${e}`;
3709
3765
  }
3710
3766
  } else {
3711
- const lower = q.toLowerCase();
3712
- matcher = (l) => l.toLowerCase().includes(lower);
3767
+ const lower2 = q.toLowerCase();
3768
+ matcher = (l) => l.toLowerCase().includes(lower2);
3713
3769
  }
3714
3770
  const loaded = [];
3715
3771
  for (const slug2 of slugs) {
@@ -3817,6 +3873,25 @@ ${sections.join("\n\n---\n\n")}`;
3817
3873
  }
3818
3874
  return "";
3819
3875
  }
3876
+ async function loadContextFiles(fs, specs = []) {
3877
+ const sections = [];
3878
+ for (const { path, maxChars, label } of specs) {
3879
+ if (!await fs.exists(path)) continue;
3880
+ let md = (await fs.readFile(path)).trim();
3881
+ if (!md) continue;
3882
+ if (maxChars !== void 0) {
3883
+ const cps = [...md], n = Math.max(0, maxChars);
3884
+ if (cps.length > n) md = `${cps.slice(0, n).join("").trimEnd()}
3885
+ \u2026 (truncated \u2014 full file: ${path})`;
3886
+ }
3887
+ sections.push(`<!-- ${path} -->
3888
+ ${label ? `### ${label}
3889
+ ` : ""}${md}`);
3890
+ }
3891
+ return sections.length ? `## Context files
3892
+
3893
+ ${sections.join("\n\n---\n\n")}` : "";
3894
+ }
3820
3895
 
3821
3896
  // src/subagent.ts
3822
3897
  init_tools();
@@ -4509,6 +4584,11 @@ init_logging();
4509
4584
  var log8 = forComponent("modes");
4510
4585
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "ApplyEdits"]);
4511
4586
  var PLANNER_BLOCKED = /* @__PURE__ */ new Set([...WRITE_TOOLS, "bash", "Shell"]);
4587
+ var lower = (xs) => new Set([...xs].map((x) => x.toLowerCase()));
4588
+ var PLANNER_BLOCKED_LC = lower(PLANNER_BLOCKED);
4589
+ var WRITE_TOOLS_LC = lower(WRITE_TOOLS);
4590
+ var plannerBlocks = (t) => !!t.writes || PLANNER_BLOCKED_LC.has(t.name.toLowerCase());
4591
+ var isFileWrite = (t) => WRITE_TOOLS_LC.has(t.name.toLowerCase());
4512
4592
  var PLANNER_REFUSAL = "Refused: you are the planner and do not write files. Delegate the change with Task/TaskBatch (model set to the worker) and give the worker a full spec.";
4513
4593
  var PLAN_FIRST = "Refused: check your plan with `Advise` before the first write (files, data shape, the hard parts). Then write.";
4514
4594
  var ADVISE_LIMIT = 3;
@@ -4531,7 +4611,7 @@ function advisorFor(worker, advisor) {
4531
4611
  return advisor && pairOk("advisor", worker, advisor) ? advisor : void 0;
4532
4612
  }
4533
4613
  function delegateTools(tools, planner) {
4534
- return tools.map((t) => !PLANNER_BLOCKED.has(t.name) ? t : {
4614
+ return tools.map((t) => !plannerBlocks(t) ? t : {
4535
4615
  ...t,
4536
4616
  run: async (...a) => a[1]?.model === planner ? PLANNER_REFUSAL : t.run(...a)
4537
4617
  });
@@ -4601,7 +4681,7 @@ ${question}` }
4601
4681
  }
4602
4682
  }
4603
4683
  };
4604
- const gate = (tools) => tools.map((t) => !WRITE_TOOLS.has(t.name) ? t : { ...t, run: async (...a) => used ? t.run(...a) : PLAN_FIRST });
4684
+ const gate = (tools) => tools.map((t) => !isFileWrite(t) ? t : { ...t, run: async (...a) => used ? t.run(...a) : PLAN_FIRST });
4605
4685
  return { tool, gate, used: () => used };
4606
4686
  }
4607
4687
  function meterWorkers(ai, main) {
@@ -4644,6 +4724,13 @@ function meterWorkers(ai, main) {
4644
4724
  resolved = void 0;
4645
4725
  } };
4646
4726
  }
4727
+ var CC_WRITE_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
4728
+ function ccModeOptions(kind, hostHooks, used) {
4729
+ if (kind === "delegate") return { bridge: /* @__PURE__ */ new Set(["Task", "TaskBatch"]), disallow: [...CC_WRITE_TOOLS, "Bash", "Task", "Agent"] };
4730
+ if (kind !== "advisor") return { bridge: /* @__PURE__ */ new Set(), disallow: [] };
4731
+ const gate = { matcher: CC_WRITE_TOOLS.join("|"), hooks: [async () => used() ? {} : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: PLAN_FIRST.replace("`Advise`", "`mcp__agentx__Advise`") } }] };
4732
+ return { bridge: /* @__PURE__ */ new Set(["Advise"]), disallow: [], hooks: { ...hostHooks, PreToolUse: [...hostHooks?.PreToolUse ?? [], gate] } };
4733
+ }
4647
4734
 
4648
4735
  // src/lint.ts
4649
4736
  var CODE_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
@@ -4844,6 +4931,8 @@ var AgentOptions = class {
4844
4931
  * Walks the VFS tree and merges all found files (general → specific, like Claude Code).
4845
4932
  * `true` (default) = auto-discover standard names. `string[]` = custom names. `false` = skip. */
4846
4933
  instructionFiles = true;
4934
+ /** Extra files injected right after the instructions (missing → skipped; over `maxChars` → head + pointer to the full file). */
4935
+ contextFiles;
4847
4936
  /** Host interaction channel (human-in-the-loop). If set: adds the `AskUserQuestion` tool. */
4848
4937
  host;
4849
4938
  /** Add the `AskUserQuestion` tool when a host is present (default true). Set false for an agent that
@@ -4971,6 +5060,8 @@ var Agent = class _Agent {
4971
5060
  meter;
4972
5061
  // delegate/advisor: meters the second model's calls
4973
5062
  modeModel;
5063
+ modeKind;
5064
+ adv;
4974
5065
  lastTask = "";
4975
5066
  // the current turn's text — what the advisor reads as the request
4976
5067
  /** claude-code bridge for agentx-only tools (ccBridge.ts) + the current step's executor it calls. */
@@ -5075,12 +5166,15 @@ var Agent = class _Agent {
5075
5166
  const o = this.options;
5076
5167
  if (!o.model.startsWith("claude-code/")) return void 0;
5077
5168
  const po = { steer: () => this.drainPendingSteers() };
5078
- const bridged = (t) => o.ccBridge === "all" || CC_BRIDGED.has(t.name);
5169
+ const mode = ccModeOptions(this.modeKind, o.providerOptions?.hooks, () => this.adv?.used() ?? 0);
5170
+ const bridged = (t) => o.ccBridge === "all" || CC_BRIDGED.has(t.name) || mode.bridge.has(t.name);
5079
5171
  if (o.providerOptions?.claudeCodeSession && this.activeTools.some(bridged)) {
5080
5172
  this.ccServer ??= ccToolServer("agentx", () => this.activeTools.filter(bridged), (n, a) => this.ccExec(n, a));
5081
5173
  po.mcpServers = { ...o.providerOptions?.mcpServers, agentx: this.ccServer };
5082
5174
  }
5083
- const denyTools = claudeCodeDisallowedTools(o.permissions);
5175
+ const denyTools = [...claudeCodeDisallowedTools(o.permissions), ...mode.disallow];
5176
+ if (mode.hooks) po.hooks = mode.hooks;
5177
+ if (this.modeKind && o.providerOptions?.claudeCodeSession) po.claudeCodeSession = `${o.providerOptions.claudeCodeSession}:${this.modeKind}`;
5084
5178
  if (denyTools.length) {
5085
5179
  const prior = o.providerOptions?.disallowedTools ?? [];
5086
5180
  po.disallowedTools = [.../* @__PURE__ */ new Set([...prior, ...denyTools])];
@@ -5167,7 +5261,7 @@ var Agent = class _Agent {
5167
5261
  * across turns (and the frozen prompt pairs well with prompt caching). Does NOT touch
5168
5262
  * the transcript; `run`/`send` own that.
5169
5263
  */
5170
- async prepare(taskHint) {
5264
+ async prepare() {
5171
5265
  if (this.prepared) return this.systemPromptCache;
5172
5266
  const o = this.options;
5173
5267
  const fs = o.fs;
@@ -5178,14 +5272,19 @@ var Agent = class _Agent {
5178
5272
  const advisor = !worker && o.advisor ? advisorFor(o.model, o.advisor.model) : void 0;
5179
5273
  this.meter = worker || advisor ? meterWorkers(o.ai, o.model) : void 0;
5180
5274
  const ai = this.meter?.ai ?? o.ai;
5181
- const adv = advisor ? makeAdvisor({ ai, advisor, request: () => this.lastTask, snapshot: o.advisor.snapshot, limit: o.advisor.limit }) : void 0;
5275
+ this.modeKind = worker ? "delegate" : advisor ? "advisor" : void 0;
5276
+ const adv = this.adv = advisor ? makeAdvisor({ ai, advisor, request: () => this.lastTask, snapshot: o.advisor.snapshot, limit: o.advisor.limit }) : void 0;
5182
5277
  if (o.instructionFiles !== false) {
5183
5278
  const names = Array.isArray(o.instructionFiles) ? o.instructionFiles : void 0;
5184
5279
  const ins = await loadInstructions(fs, names);
5185
5280
  if (ins) systemPrompt += "\n\n" + ins;
5186
5281
  }
5282
+ if (o.contextFiles?.length) {
5283
+ const ctxFiles = await loadContextFiles(fs, o.contextFiles);
5284
+ if (ctxFiles) systemPrompt += "\n\n" + ctxFiles;
5285
+ }
5187
5286
  if (o.memoryDir) {
5188
- const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { relevanceHint: taskHint, userDir: o.memoryUserDir });
5287
+ const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { userDir: o.memoryUserDir });
5189
5288
  if (index) systemPrompt += "\n\n" + index;
5190
5289
  tools = [...tools, ...memTools];
5191
5290
  }
@@ -5195,7 +5294,7 @@ var Agent = class _Agent {
5195
5294
  const c = (await scanCommands(fs, commandsDir)).find((x) => x.name === name);
5196
5295
  return c ? { path: c.path, stripFrontmatter: true } : void 0;
5197
5296
  } : void 0;
5198
- const { catalog, tool } = await loadSkills(fs, skillsDir, { relevanceHint: taskHint, sibling });
5297
+ const { catalog, tool } = await loadSkills(fs, skillsDir, { sibling });
5199
5298
  if (catalog) systemPrompt += "\n\n" + catalog;
5200
5299
  if (tool) tools = [...tools, tool];
5201
5300
  }
@@ -5204,7 +5303,7 @@ var Agent = class _Agent {
5204
5303
  const s = (await scanSkills(fs, skillsDir)).find((x) => x.name === name);
5205
5304
  return s ? { path: s.path, stripFrontmatter: false } : void 0;
5206
5305
  } : void 0;
5207
- const { catalog, tool } = await loadCommands(fs, commandsDir, { relevanceHint: taskHint, sibling });
5306
+ const { catalog, tool } = await loadCommands(fs, commandsDir, { sibling });
5208
5307
  if (catalog) systemPrompt += "\n\n" + catalog;
5209
5308
  if (tool) tools = [...tools, tool];
5210
5309
  }
@@ -5263,7 +5362,7 @@ var Agent = class _Agent {
5263
5362
  this.invokedTools.clear();
5264
5363
  this.fabricatedClaims.clear();
5265
5364
  this.lastTask = contentText(task);
5266
- const systemPrompt = await this.prepare(contentText(task));
5365
+ const systemPrompt = await this.prepare();
5267
5366
  const startCtx = await this.fireSessionStart();
5268
5367
  const userContent = await this.applyPromptSubmit(task);
5269
5368
  this.transcript = [
@@ -5299,7 +5398,7 @@ var Agent = class _Agent {
5299
5398
  async send(task) {
5300
5399
  await this.ensureFs();
5301
5400
  this.lastTask = contentText(task);
5302
- const systemPrompt = await this.prepare(contentText(task));
5401
+ const systemPrompt = await this.prepare();
5303
5402
  const startCtx = await this.fireSessionStart();
5304
5403
  const userContent = await this.applyPromptSubmit(task);
5305
5404
  const sys = systemPrompt + (startCtx ? "\n\n" + startCtx : "");
@@ -6123,9 +6222,9 @@ function capOversizedBodies(messages, target, keepRecentImages = 1) {
6123
6222
  return out;
6124
6223
  }
6125
6224
  function elideBody(x, cap) {
6126
- const note = (bytes, imgs2) => `
6225
+ const note = (bytes2, imgs2) => `
6127
6226
 
6128
- [${bytes} bytes elided by /compact${x.role === "tool" ? " \u2014 re-run the tool to view" : ""}${imgs2 ? `; ${imgs2} image part(s) KEPT in full \u2014 they still cost context` : ""}]`;
6227
+ [${bytes2} bytes elided by /compact${x.role === "tool" ? " \u2014 re-run the tool to view" : ""}${imgs2 ? `; ${imgs2} image part(s) KEPT in full \u2014 they still cost context` : ""}]`;
6129
6228
  if (!Array.isArray(x.content)) {
6130
6229
  const text = contentText(x.content);
6131
6230
  if (text.length <= cap) return x;
@@ -7365,8 +7464,8 @@ function isTranscriptComplete(text) {
7365
7464
  const lastWord = words3[words3.length - 1];
7366
7465
  if (COMPLETE_ENDINGS.has(lastWord)) return true;
7367
7466
  if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
7368
- const lower = trimmed.toLowerCase();
7369
- for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
7467
+ const lower2 = trimmed.toLowerCase();
7468
+ for (const marker of CONTINUATION_MARKERS) if (lower2.endsWith(marker)) return false;
7370
7469
  if (words3.length <= 2) return false;
7371
7470
  return true;
7372
7471
  }
@@ -12061,11 +12160,11 @@ Another agent just implemented the above. Independently check the CURRENT state
12061
12160
  const list = id ? [this.tasks.get(String(id))].filter(Boolean) : [...this.tasks.values()];
12062
12161
  if (!list.length) return id ? `No task '${id}'.` : "No background tasks.";
12063
12162
  const cap = id ? 1500 : 300;
12064
- const clip = (x) => x.length > cap ? `${x.slice(0, cap)}\u2026 (truncated)` : x;
12163
+ const clip2 = (x) => x.length > cap ? `${x.slice(0, cap)}\u2026 (truncated)` : x;
12065
12164
  return list.map((t) => {
12066
12165
  const head = `${t.id} (${t.label}): ${t.status}`;
12067
12166
  const body = (t.status === "done" ? t.spokenText?.trim() || t.result?.trim() : t.status === "error" ? t.result?.trim() : "") ?? "";
12068
- return body ? `${head}. ${t.status === "error" ? "Error" : "Result"}: ${clip(body)}` : head;
12167
+ return body ? `${head}. ${t.status === "error" ? "Error" : "Result"}: ${clip2(body)}` : head;
12069
12168
  }).join("\n");
12070
12169
  }
12071
12170
  };
@@ -13961,10 +14060,12 @@ export {
13961
14060
  Agent,
13962
14061
  AgentOptions,
13963
14062
  BodDbFilesystem,
14063
+ CC_WRITE_TOOLS,
13964
14064
  CartesiaTTS,
13965
14065
  CartesiaTTSOptions,
13966
14066
  CommandExecutor2 as CommandExecutor,
13967
14067
  ConsoleHostBridge,
14068
+ DEFAULT_CATALOG_BUDGET,
13968
14069
  DEFAULT_DENY,
13969
14070
  DEFAULT_MAX_DEPTH,
13970
14071
  DEFAULT_MUTATING,
@@ -14022,6 +14123,7 @@ export {
14022
14123
  askUserQuestionTool,
14023
14124
  bashTool,
14024
14125
  buildMcpCatalog,
14126
+ ccModeOptions,
14025
14127
  checkpointTool,
14026
14128
  checkpointTools,
14027
14129
  claudeCodeDisallowedTools,
@@ -14054,6 +14156,7 @@ export {
14054
14156
  findGitRoot,
14055
14157
  forComponent,
14056
14158
  forgetFact,
14159
+ formatCatalog,
14057
14160
  formatJobNotice,
14058
14161
  fullAgentOptions,
14059
14162
  getOrCreateWorktree,
@@ -14072,6 +14175,7 @@ export {
14072
14175
  lessonCapture,
14073
14176
  loadAgents,
14074
14177
  loadCommands,
14178
+ loadContextFiles,
14075
14179
  loadInstructions,
14076
14180
  loadMemory,
14077
14181
  loadSkills,
@@ -14107,6 +14211,7 @@ export {
14107
14211
  parseImageRef,
14108
14212
  pickTier,
14109
14213
  planMode,
14214
+ plannerBlocks,
14110
14215
  raceAttempts,
14111
14216
  readTool,
14112
14217
  reasoningToChatFragment,