@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/cli.js CHANGED
@@ -121,13 +121,13 @@ function elideImageParts(content, hit, stub) {
121
121
  }
122
122
  function mintImageRef(path, mime) {
123
123
  const token = randomBytes(16).toString("hex");
124
- let bytes = 0;
124
+ let bytes2 = 0;
125
125
  try {
126
- bytes = Math.ceil(statSync3(path).size * 4 / 3);
126
+ bytes2 = Math.ceil(statSync3(path).size * 4 / 3);
127
127
  } catch (e) {
128
128
  log3.warn(`image ref stat failed, weighing as 0: ${path}`, e);
129
129
  }
130
- refRegistry.set(token, { mime, path, bytes });
130
+ refRegistry.set(token, { mime, path, bytes: bytes2 });
131
131
  while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
132
132
  return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;
133
133
  }
@@ -825,6 +825,7 @@ var init_tools_structured = __esm({
825
825
  };
826
826
  writeTool = {
827
827
  name: "Write",
828
+ writes: true,
828
829
  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).",
829
830
  parameters: {
830
831
  type: "object",
@@ -850,6 +851,7 @@ var init_tools_structured = __esm({
850
851
  };
851
852
  multiEditTool = {
852
853
  name: "MultiEdit",
854
+ writes: true,
853
855
  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.",
854
856
  parameters: {
855
857
  type: "object",
@@ -891,6 +893,7 @@ var init_tools_structured = __esm({
891
893
  };
892
894
  applyEditsTool = {
893
895
  name: "ApplyEdits",
896
+ writes: true,
894
897
  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.",
895
898
  parameters: {
896
899
  type: "object",
@@ -1584,6 +1587,7 @@ var init_tools = __esm({
1584
1587
  };
1585
1588
  bashTool = {
1586
1589
  name: "bash",
1590
+ writes: true,
1587
1591
  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`.",
1588
1592
  parameters: {
1589
1593
  type: "object",
@@ -1635,14 +1639,14 @@ var init_tools = __esm({
1635
1639
  if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
1636
1640
  return imageRefResult(real, IMG_MIME[ext]);
1637
1641
  }
1638
- const bytes = await fs.readFileBytes(path);
1639
- const b64 = Buffer.from(bytes).toString("base64");
1642
+ const bytes2 = await fs.readFileBytes(path);
1643
+ const b64 = Buffer.from(bytes2).toString("base64");
1640
1644
  return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });
1641
1645
  }
1642
1646
  const raw = await ctx.fs.readFile(path);
1643
1647
  if (isBinary(raw)) {
1644
- const bytes = await byteSize(ctx.fs, path, raw);
1645
- 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}.]`;
1648
+ const bytes2 = await byteSize(ctx.fs, path, raw);
1649
+ 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}.]`;
1646
1650
  }
1647
1651
  ctx.readState.set(ctx.fs.resolvePath(path), raw);
1648
1652
  if (raw === "") return `[empty file: ${path} \u2014 0 bytes]`;
@@ -1661,6 +1665,7 @@ var init_tools = __esm({
1661
1665
  };
1662
1666
  editTool = {
1663
1667
  name: "Edit",
1668
+ writes: true,
1664
1669
  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.",
1665
1670
  parameters: {
1666
1671
  type: "object",
@@ -1972,8 +1977,8 @@ var init_tools_jobs = __esm({
1972
1977
  return [...this.jobs.values()].map((j) => this.info(j));
1973
1978
  }
1974
1979
  info(j) {
1975
- const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
1976
- return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
1980
+ const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes: bytes2, holds, result } = j;
1981
+ return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes: bytes2, holds, result };
1977
1982
  }
1978
1983
  /** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
1979
1984
  async wait(id, timeoutMs) {
@@ -2568,7 +2573,8 @@ var init_JailedFilesystem = __esm({
2568
2573
  violate(op, abs, rule) {
2569
2574
  this.note(op, abs, rule);
2570
2575
  if (rule === "deny" || rule === "confine") throw new Error(`File not found: ${abs}`);
2571
- throw new Error(`Blocked by filesystem jail (${rule}): ${abs}`);
2576
+ const hint = rule === "readonly" && this.options.readonlyHint ? ` \u2014 ${this.options.readonlyHint}` : "";
2577
+ throw new Error(`Blocked by filesystem jail (${rule}): ${abs}${hint}`);
2572
2578
  }
2573
2579
  /** Fire the audit hook without throwing (for boolean probes that must still return false). */
2574
2580
  note(op, abs, rule) {
@@ -2693,6 +2699,8 @@ var init_JailedFilesystem = __esm({
2693
2699
  deny = [...DEFAULT_DENY];
2694
2700
  /** Globs readable but not writable/deletable (the pinned constitution). */
2695
2701
  readonly = [];
2702
+ /** Appended to readonly-violation errors (e.g. "edit it via the kb tool instead"). */
2703
+ readonlyHint;
2696
2704
  /** If set, only paths matching these globs are accessible at all. */
2697
2705
  confineTo;
2698
2706
  /** Audit hook fired on every blocked operation (drives the circuit breaker). */
@@ -2992,6 +3000,7 @@ function makeRealShellTool(options) {
2992
3000
  };
2993
3001
  return {
2994
3002
  name: "Shell",
3003
+ writes: true,
2995
3004
  // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The
2996
3005
  // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like
2997
3006
  // shraga-ee, to the parent's session), so a background job started from the child would run
@@ -3514,69 +3523,39 @@ function normalizeQuestion(input) {
3514
3523
  return { question: q2.question, header: typeof q2.header === "string" ? q2.header : void 0, options, multiSelect: q2.multiSelect === true };
3515
3524
  }
3516
3525
 
3517
- // src/relevance.ts
3518
- var STOP = /* @__PURE__ */ new Set([
3519
- "the",
3520
- "and",
3521
- "for",
3522
- "with",
3523
- "that",
3524
- "this",
3525
- "from",
3526
- "into",
3527
- "your",
3528
- "you",
3529
- "are",
3530
- "was",
3531
- "will",
3532
- "use",
3533
- "using",
3534
- "run",
3535
- "add",
3536
- "fix",
3537
- "make",
3538
- "file",
3539
- "files",
3540
- "code",
3541
- "please",
3542
- "need",
3543
- "want",
3544
- "should",
3545
- "all"
3546
- ]);
3547
- function tokenize(s) {
3548
- const out = /* @__PURE__ */ new Set();
3549
- for (const w of String(s ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? []) {
3550
- if (w.length >= 3 && !STOP.has(w)) out.add(w);
3551
- }
3552
- return out;
3553
- }
3554
- function idfWeights(corpus) {
3555
- const N = corpus.length;
3556
- if (N === 0) return /* @__PURE__ */ new Map();
3557
- const df = /* @__PURE__ */ new Map();
3558
- for (const doc of corpus) for (const t of tokenize(doc)) df.set(t, (df.get(t) ?? 0) + 1);
3559
- const idf = /* @__PURE__ */ new Map();
3560
- for (const [t, n] of df) idf.set(t, Math.log((N + 1) / (n + 1)) + 1);
3561
- return idf;
3562
- }
3563
- function relevanceScore(text, queryTokens, idf) {
3564
- if (queryTokens.size === 0) return 0;
3565
- const t = tokenize(text);
3566
- let score = 0;
3567
- for (const q2 of queryTokens) if (t.has(q2)) score += idf?.get(q2) ?? 1;
3568
- return score;
3569
- }
3570
- function topByRelevance(items, query, text, k, corpus) {
3571
- if (!Number.isInteger(k) || k < 1) return { kept: items, rest: [] };
3572
- if (items.length <= k || !query.trim()) return { kept: items, rest: [] };
3573
- const q2 = tokenize(query);
3574
- if (q2.size === 0) return { kept: items.slice(0, k), rest: items.slice(k) };
3575
- const idf = corpus?.length ? idfWeights(corpus) : void 0;
3576
- const scored = items.map((x, i) => ({ i, s: relevanceScore(text(x), q2, idf) }));
3577
- scored.sort((a, b) => b.s - a.s || a.i - b.i);
3578
- const keep = new Set(scored.slice(0, k).map((e) => e.i));
3579
- return { kept: items.filter((_, i) => keep.has(i)), rest: items.filter((_, i) => !keep.has(i)) };
3526
+ // src/catalog.ts
3527
+ var DEFAULT_CATALOG_BUDGET = 6e3;
3528
+ var enc = new TextEncoder();
3529
+ var bytes = (s) => enc.encode(s).length;
3530
+ function clip(s, n) {
3531
+ const cps = [...s];
3532
+ if (cps.length <= n) return s;
3533
+ return n <= 1 ? "\u2026" : cps.slice(0, n - 1).join("").trimEnd() + "\u2026";
3534
+ }
3535
+ function formatCatalog(entries, opts) {
3536
+ const budget = opts.budgetBytes ?? DEFAULT_CATALOG_BUDGET;
3537
+ const pre = opts.namePrefix ?? "";
3538
+ const wrap = (body) => [opts.title, body, opts.footer].filter((x) => x !== void 0 && x !== "").join("\n");
3539
+ const render = (max) => wrap(entries.map((e) => {
3540
+ const d = (e.description ?? "").replace(/\s+/g, " ").trim();
3541
+ const desc = d && max > 0 ? ` \u2014 ${clip(d, max)}` : "";
3542
+ return `- **${pre}${e.name}**${desc}${e.path ? ` (\`${e.path}\`)` : ""}`;
3543
+ }).join("\n"));
3544
+ const longest = Math.max(0, ...entries.map((e) => [...e.description ?? ""].length));
3545
+ const full = render(longest);
3546
+ if (bytes(full) <= budget) return full;
3547
+ if (bytes(render(0)) <= budget) {
3548
+ let lo = 0, hi = longest;
3549
+ while (lo < hi) {
3550
+ const mid = Math.ceil((lo + hi) / 2);
3551
+ if (bytes(render(mid)) <= budget) lo = mid;
3552
+ else hi = mid - 1;
3553
+ }
3554
+ return render(lo);
3555
+ }
3556
+ const names = wrap(entries.map((e) => `- **${pre}${e.name}**`).join("\n"));
3557
+ if (bytes(names) <= budget) return names;
3558
+ return wrap(`- ${entries.map((e) => pre + e.name).join(", ")}`);
3580
3559
  }
3581
3560
 
3582
3561
  // src/frontmatter.ts
@@ -3598,7 +3577,6 @@ function grabField(block, key) {
3598
3577
  }
3599
3578
 
3600
3579
  // src/commands.ts
3601
- var MAX_CATALOG = 25;
3602
3580
  function parseFrontmatter(md) {
3603
3581
  const { block, body } = splitFrontmatter(md);
3604
3582
  return { description: grabField(block, "description"), body };
@@ -3655,9 +3633,12 @@ async function scanCommands(fs, dir) {
3655
3633
  async function loadCommands(fs, dir, opts = {}) {
3656
3634
  const commands = await scanCommands(fs, dir);
3657
3635
  if (commands.length === 0) return { commands, catalog: "" };
3658
- const { kept, rest } = topByRelevance(commands, opts.relevanceHint ?? "", (c) => `${c.name} ${c.description}`, opts.max ?? MAX_CATALOG);
3659
- const catalog = "## Slash commands (reusable prompt templates)\n" + kept.map((c) => `- **/${c.name}** \u2014 ${c.description} (\`${c.path}\`)`).join("\n") + (rest.length ? `
3660
- - (${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.";
3636
+ const catalog = formatCatalog(commands, {
3637
+ title: "## Slash commands (reusable prompt templates)",
3638
+ footer: "To run one, call the `SlashCommand` tool with its name (and optional `args`); it returns the expanded template to act on.",
3639
+ namePrefix: "/",
3640
+ budgetBytes: opts.budgetBytes
3641
+ });
3661
3642
  const tool = {
3662
3643
  name: "SlashCommand",
3663
3644
  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.",
@@ -3685,7 +3666,6 @@ async function loadCommands(fs, dir, opts = {}) {
3685
3666
  }
3686
3667
 
3687
3668
  // src/skills.ts
3688
- var MAX_CATALOG2 = 25;
3689
3669
  function parseFrontmatter2(md) {
3690
3670
  const { block } = splitFrontmatter(md);
3691
3671
  const b = block || md.slice(0, 600);
@@ -3711,9 +3691,11 @@ async function scanSkills(fs, dir) {
3711
3691
  async function loadSkills(fs, dir, opts = {}) {
3712
3692
  const skills = await scanSkills(fs, dir);
3713
3693
  if (skills.length === 0) return { skills, catalog: "" };
3714
- const { kept, rest } = topByRelevance(skills, opts.relevanceHint ?? "", (s) => `${s.name} ${s.description}`, opts.max ?? MAX_CATALOG2);
3715
- 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 ? `
3716
- - (${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.";
3694
+ const catalog = formatCatalog(skills, {
3695
+ title: "## Skills (load one before acting on a matching task)",
3696
+ footer: "When a task matches a skill, call the `Skill` tool with its name to load its full instructions.",
3697
+ budgetBytes: opts.budgetBytes
3698
+ });
3717
3699
  const tool = {
3718
3700
  name: "Skill",
3719
3701
  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.",
@@ -3738,7 +3720,73 @@ async function loadSkills(fs, dir, opts = {}) {
3738
3720
 
3739
3721
  // src/memory.ts
3740
3722
  init_tools_structured();
3741
- var MAX_INDEX = 25;
3723
+
3724
+ // src/relevance.ts
3725
+ var STOP = /* @__PURE__ */ new Set([
3726
+ "the",
3727
+ "and",
3728
+ "for",
3729
+ "with",
3730
+ "that",
3731
+ "this",
3732
+ "from",
3733
+ "into",
3734
+ "your",
3735
+ "you",
3736
+ "are",
3737
+ "was",
3738
+ "will",
3739
+ "use",
3740
+ "using",
3741
+ "run",
3742
+ "add",
3743
+ "fix",
3744
+ "make",
3745
+ "file",
3746
+ "files",
3747
+ "code",
3748
+ "please",
3749
+ "need",
3750
+ "want",
3751
+ "should",
3752
+ "all"
3753
+ ]);
3754
+ function tokenize(s) {
3755
+ const out = /* @__PURE__ */ new Set();
3756
+ for (const w of String(s ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? []) {
3757
+ if (w.length >= 3 && !STOP.has(w)) out.add(w);
3758
+ }
3759
+ return out;
3760
+ }
3761
+ function idfWeights(corpus) {
3762
+ const N = corpus.length;
3763
+ if (N === 0) return /* @__PURE__ */ new Map();
3764
+ const df = /* @__PURE__ */ new Map();
3765
+ for (const doc of corpus) for (const t of tokenize(doc)) df.set(t, (df.get(t) ?? 0) + 1);
3766
+ const idf = /* @__PURE__ */ new Map();
3767
+ for (const [t, n] of df) idf.set(t, Math.log((N + 1) / (n + 1)) + 1);
3768
+ return idf;
3769
+ }
3770
+ function relevanceScore(text, queryTokens, idf) {
3771
+ if (queryTokens.size === 0) return 0;
3772
+ const t = tokenize(text);
3773
+ let score = 0;
3774
+ for (const q2 of queryTokens) if (t.has(q2)) score += idf?.get(q2) ?? 1;
3775
+ return score;
3776
+ }
3777
+ function topByRelevance(items, query, text, k, corpus) {
3778
+ if (!Number.isInteger(k) || k < 1) return { kept: items, rest: [] };
3779
+ if (items.length <= k || !query.trim()) return { kept: items, rest: [] };
3780
+ const q2 = tokenize(query);
3781
+ if (q2.size === 0) return { kept: items.slice(0, k), rest: items.slice(k) };
3782
+ const idf = corpus?.length ? idfWeights(corpus) : void 0;
3783
+ const scored = items.map((x, i) => ({ i, s: relevanceScore(text(x), q2, idf) }));
3784
+ scored.sort((a, b) => b.s - a.s || a.i - b.i);
3785
+ const keep = new Set(scored.slice(0, k).map((e) => e.i));
3786
+ return { kept: items.filter((_, i) => keep.has(i)), rest: items.filter((_, i) => !keep.has(i)) };
3787
+ }
3788
+
3789
+ // src/memory.ts
3742
3790
  var MAX_INDEX_LINES = 200;
3743
3791
  var MAX_INDEX_BYTES = 25e3;
3744
3792
  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.';
@@ -3773,10 +3821,18 @@ async function loadMemory(fs, dir, opts = {}) {
3773
3821
  }
3774
3822
  }
3775
3823
  if (!hasContent) return { index: MEMORY_PROMPT, tools };
3776
- const { kept, rest } = topByRelevance(allPointers, opts.relevanceHint ?? "", (l) => l, opts.max ?? MAX_INDEX);
3777
- const restSlugs = rest.map((l) => l.match(/\]\(([^)]+)\.md\)/)?.[1] ?? l.match(/\[([^\]]+)\]/)?.[1] ?? "").filter(Boolean);
3778
- const index = MEMORY_PROMPT + "\n\n## Memory index (persistent context \u2014 recalled across sessions)\n" + (header ? header + "\n" : "") + kept.join("\n") + (restSlugs.length ? `
3779
- - (${restSlugs.length} more learnings, slugs only \u2014 call \`Recall\` if relevant): ${restSlugs.join(", ")}` : "");
3824
+ const entries = allPointers.map((l) => {
3825
+ const m = l.match(/\]\(([^)]+)\.md\)/);
3826
+ const slug2 = m?.[1] ?? l;
3827
+ const title = m ? l.slice(l.indexOf("[") + 1, m.index).trim() : "";
3828
+ const hook = m ? l.slice(m.index + m[0].length).replace(/^\s*(?:[—:-]\s*)?/, "").trim() : "";
3829
+ return { name: slug2, description: title && title !== slug2 ? hook ? `${title} \u2014 ${hook}` : title : hook };
3830
+ });
3831
+ const index = MEMORY_PROMPT + "\n\n" + formatCatalog(entries, {
3832
+ title: "## Memory index (persistent context \u2014 recalled across sessions)" + (header ? "\n" + header : ""),
3833
+ footer: "Call `Recall` with a slug to load its full fact.",
3834
+ budgetBytes: opts.budgetBytes
3835
+ });
3780
3836
  return { index, tools };
3781
3837
  }
3782
3838
  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
@@ -3967,8 +4023,8 @@ function memorySearchTool(fs, dirs) {
3967
4023
  return `Error: invalid regex: ${e}`;
3968
4024
  }
3969
4025
  } else {
3970
- const lower = q2.toLowerCase();
3971
- matcher = (l) => l.toLowerCase().includes(lower);
4026
+ const lower2 = q2.toLowerCase();
4027
+ matcher = (l) => l.toLowerCase().includes(lower2);
3972
4028
  }
3973
4029
  const loaded = [];
3974
4030
  for (const slug2 of slugs) {
@@ -4076,6 +4132,25 @@ ${sections.join("\n\n---\n\n")}`;
4076
4132
  }
4077
4133
  return "";
4078
4134
  }
4135
+ async function loadContextFiles(fs, specs = []) {
4136
+ const sections = [];
4137
+ for (const { path, maxChars, label } of specs) {
4138
+ if (!await fs.exists(path)) continue;
4139
+ let md = (await fs.readFile(path)).trim();
4140
+ if (!md) continue;
4141
+ if (maxChars !== void 0) {
4142
+ const cps = [...md], n = Math.max(0, maxChars);
4143
+ if (cps.length > n) md = `${cps.slice(0, n).join("").trimEnd()}
4144
+ \u2026 (truncated \u2014 full file: ${path})`;
4145
+ }
4146
+ sections.push(`<!-- ${path} -->
4147
+ ${label ? `### ${label}
4148
+ ` : ""}${md}`);
4149
+ }
4150
+ return sections.length ? `## Context files
4151
+
4152
+ ${sections.join("\n\n---\n\n")}` : "";
4153
+ }
4079
4154
 
4080
4155
  // src/subagent.ts
4081
4156
  init_tools();
@@ -4618,6 +4693,11 @@ init_logging();
4618
4693
  var log8 = forComponent("modes");
4619
4694
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "ApplyEdits"]);
4620
4695
  var PLANNER_BLOCKED = /* @__PURE__ */ new Set([...WRITE_TOOLS, "bash", "Shell"]);
4696
+ var lower = (xs) => new Set([...xs].map((x) => x.toLowerCase()));
4697
+ var PLANNER_BLOCKED_LC = lower(PLANNER_BLOCKED);
4698
+ var WRITE_TOOLS_LC = lower(WRITE_TOOLS);
4699
+ var plannerBlocks = (t) => !!t.writes || PLANNER_BLOCKED_LC.has(t.name.toLowerCase());
4700
+ var isFileWrite = (t) => WRITE_TOOLS_LC.has(t.name.toLowerCase());
4621
4701
  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.";
4622
4702
  var PLAN_FIRST = "Refused: check your plan with `Advise` before the first write (files, data shape, the hard parts). Then write.";
4623
4703
  var ADVISE_LIMIT = 3;
@@ -4640,7 +4720,7 @@ function advisorFor(worker, advisor) {
4640
4720
  return advisor && pairOk("advisor", worker, advisor) ? advisor : void 0;
4641
4721
  }
4642
4722
  function delegateTools(tools, planner) {
4643
- return tools.map((t) => !PLANNER_BLOCKED.has(t.name) ? t : {
4723
+ return tools.map((t) => !plannerBlocks(t) ? t : {
4644
4724
  ...t,
4645
4725
  run: async (...a) => a[1]?.model === planner ? PLANNER_REFUSAL : t.run(...a)
4646
4726
  });
@@ -4710,7 +4790,7 @@ ${question}` }
4710
4790
  }
4711
4791
  }
4712
4792
  };
4713
- const gate = (tools) => tools.map((t) => !WRITE_TOOLS.has(t.name) ? t : { ...t, run: async (...a) => used ? t.run(...a) : PLAN_FIRST });
4793
+ const gate = (tools) => tools.map((t) => !isFileWrite(t) ? t : { ...t, run: async (...a) => used ? t.run(...a) : PLAN_FIRST });
4714
4794
  return { tool, gate, used: () => used };
4715
4795
  }
4716
4796
  function meterWorkers(ai, main2) {
@@ -4753,6 +4833,13 @@ function meterWorkers(ai, main2) {
4753
4833
  resolved = void 0;
4754
4834
  } };
4755
4835
  }
4836
+ var CC_WRITE_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
4837
+ function ccModeOptions(kind, hostHooks, used) {
4838
+ if (kind === "delegate") return { bridge: /* @__PURE__ */ new Set(["Task", "TaskBatch"]), disallow: [...CC_WRITE_TOOLS, "Bash", "Task", "Agent"] };
4839
+ if (kind !== "advisor") return { bridge: /* @__PURE__ */ new Set(), disallow: [] };
4840
+ const gate = { matcher: CC_WRITE_TOOLS.join("|"), hooks: [async () => used() ? {} : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: PLAN_FIRST.replace("`Advise`", "`mcp__agentx__Advise`") } }] };
4841
+ return { bridge: /* @__PURE__ */ new Set(["Advise"]), disallow: [], hooks: { ...hostHooks, PreToolUse: [...hostHooks?.PreToolUse ?? [], gate] } };
4842
+ }
4756
4843
 
4757
4844
  // src/lint.ts
4758
4845
  var CODE_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
@@ -4953,6 +5040,8 @@ var AgentOptions = class {
4953
5040
  * Walks the VFS tree and merges all found files (general → specific, like Claude Code).
4954
5041
  * `true` (default) = auto-discover standard names. `string[]` = custom names. `false` = skip. */
4955
5042
  instructionFiles = true;
5043
+ /** Extra files injected right after the instructions (missing → skipped; over `maxChars` → head + pointer to the full file). */
5044
+ contextFiles;
4956
5045
  /** Host interaction channel (human-in-the-loop). If set: adds the `AskUserQuestion` tool. */
4957
5046
  host;
4958
5047
  /** Add the `AskUserQuestion` tool when a host is present (default true). Set false for an agent that
@@ -5080,6 +5169,8 @@ var Agent = class _Agent {
5080
5169
  meter;
5081
5170
  // delegate/advisor: meters the second model's calls
5082
5171
  modeModel;
5172
+ modeKind;
5173
+ adv;
5083
5174
  lastTask = "";
5084
5175
  // the current turn's text — what the advisor reads as the request
5085
5176
  /** claude-code bridge for agentx-only tools (ccBridge.ts) + the current step's executor it calls. */
@@ -5184,12 +5275,15 @@ var Agent = class _Agent {
5184
5275
  const o = this.options;
5185
5276
  if (!o.model.startsWith("claude-code/")) return void 0;
5186
5277
  const po = { steer: () => this.drainPendingSteers() };
5187
- const bridged = (t) => o.ccBridge === "all" || CC_BRIDGED.has(t.name);
5278
+ const mode = ccModeOptions(this.modeKind, o.providerOptions?.hooks, () => this.adv?.used() ?? 0);
5279
+ const bridged = (t) => o.ccBridge === "all" || CC_BRIDGED.has(t.name) || mode.bridge.has(t.name);
5188
5280
  if (o.providerOptions?.claudeCodeSession && this.activeTools.some(bridged)) {
5189
5281
  this.ccServer ??= ccToolServer("agentx", () => this.activeTools.filter(bridged), (n, a) => this.ccExec(n, a));
5190
5282
  po.mcpServers = { ...o.providerOptions?.mcpServers, agentx: this.ccServer };
5191
5283
  }
5192
- const denyTools = claudeCodeDisallowedTools(o.permissions);
5284
+ const denyTools = [...claudeCodeDisallowedTools(o.permissions), ...mode.disallow];
5285
+ if (mode.hooks) po.hooks = mode.hooks;
5286
+ if (this.modeKind && o.providerOptions?.claudeCodeSession) po.claudeCodeSession = `${o.providerOptions.claudeCodeSession}:${this.modeKind}`;
5193
5287
  if (denyTools.length) {
5194
5288
  const prior = o.providerOptions?.disallowedTools ?? [];
5195
5289
  po.disallowedTools = [.../* @__PURE__ */ new Set([...prior, ...denyTools])];
@@ -5276,7 +5370,7 @@ var Agent = class _Agent {
5276
5370
  * across turns (and the frozen prompt pairs well with prompt caching). Does NOT touch
5277
5371
  * the transcript; `run`/`send` own that.
5278
5372
  */
5279
- async prepare(taskHint) {
5373
+ async prepare() {
5280
5374
  if (this.prepared) return this.systemPromptCache;
5281
5375
  const o = this.options;
5282
5376
  const fs = o.fs;
@@ -5287,14 +5381,19 @@ var Agent = class _Agent {
5287
5381
  const advisor = !worker && o.advisor ? advisorFor(o.model, o.advisor.model) : void 0;
5288
5382
  this.meter = worker || advisor ? meterWorkers(o.ai, o.model) : void 0;
5289
5383
  const ai = this.meter?.ai ?? o.ai;
5290
- const adv = advisor ? makeAdvisor({ ai, advisor, request: () => this.lastTask, snapshot: o.advisor.snapshot, limit: o.advisor.limit }) : void 0;
5384
+ this.modeKind = worker ? "delegate" : advisor ? "advisor" : void 0;
5385
+ const adv = this.adv = advisor ? makeAdvisor({ ai, advisor, request: () => this.lastTask, snapshot: o.advisor.snapshot, limit: o.advisor.limit }) : void 0;
5291
5386
  if (o.instructionFiles !== false) {
5292
5387
  const names = Array.isArray(o.instructionFiles) ? o.instructionFiles : void 0;
5293
5388
  const ins = await loadInstructions(fs, names);
5294
5389
  if (ins) systemPrompt += "\n\n" + ins;
5295
5390
  }
5391
+ if (o.contextFiles?.length) {
5392
+ const ctxFiles = await loadContextFiles(fs, o.contextFiles);
5393
+ if (ctxFiles) systemPrompt += "\n\n" + ctxFiles;
5394
+ }
5296
5395
  if (o.memoryDir) {
5297
- const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { relevanceHint: taskHint, userDir: o.memoryUserDir });
5396
+ const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { userDir: o.memoryUserDir });
5298
5397
  if (index) systemPrompt += "\n\n" + index;
5299
5398
  tools = [...tools, ...memTools];
5300
5399
  }
@@ -5304,7 +5403,7 @@ var Agent = class _Agent {
5304
5403
  const c = (await scanCommands(fs, commandsDir)).find((x) => x.name === name);
5305
5404
  return c ? { path: c.path, stripFrontmatter: true } : void 0;
5306
5405
  } : void 0;
5307
- const { catalog, tool } = await loadSkills(fs, skillsDir, { relevanceHint: taskHint, sibling });
5406
+ const { catalog, tool } = await loadSkills(fs, skillsDir, { sibling });
5308
5407
  if (catalog) systemPrompt += "\n\n" + catalog;
5309
5408
  if (tool) tools = [...tools, tool];
5310
5409
  }
@@ -5313,7 +5412,7 @@ var Agent = class _Agent {
5313
5412
  const s = (await scanSkills(fs, skillsDir)).find((x) => x.name === name);
5314
5413
  return s ? { path: s.path, stripFrontmatter: false } : void 0;
5315
5414
  } : void 0;
5316
- const { catalog, tool } = await loadCommands(fs, commandsDir, { relevanceHint: taskHint, sibling });
5415
+ const { catalog, tool } = await loadCommands(fs, commandsDir, { sibling });
5317
5416
  if (catalog) systemPrompt += "\n\n" + catalog;
5318
5417
  if (tool) tools = [...tools, tool];
5319
5418
  }
@@ -5372,7 +5471,7 @@ var Agent = class _Agent {
5372
5471
  this.invokedTools.clear();
5373
5472
  this.fabricatedClaims.clear();
5374
5473
  this.lastTask = contentText(task);
5375
- const systemPrompt = await this.prepare(contentText(task));
5474
+ const systemPrompt = await this.prepare();
5376
5475
  const startCtx = await this.fireSessionStart();
5377
5476
  const userContent = await this.applyPromptSubmit(task);
5378
5477
  this.transcript = [
@@ -5408,7 +5507,7 @@ var Agent = class _Agent {
5408
5507
  async send(task) {
5409
5508
  await this.ensureFs();
5410
5509
  this.lastTask = contentText(task);
5411
- const systemPrompt = await this.prepare(contentText(task));
5510
+ const systemPrompt = await this.prepare();
5412
5511
  const startCtx = await this.fireSessionStart();
5413
5512
  const userContent = await this.applyPromptSubmit(task);
5414
5513
  const sys = systemPrompt + (startCtx ? "\n\n" + startCtx : "");
@@ -6232,9 +6331,9 @@ function capOversizedBodies(messages, target, keepRecentImages = 1) {
6232
6331
  return out;
6233
6332
  }
6234
6333
  function elideBody(x, cap) {
6235
- const note = (bytes, imgs2) => `
6334
+ const note = (bytes2, imgs2) => `
6236
6335
 
6237
- [${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` : ""}]`;
6336
+ [${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` : ""}]`;
6238
6337
  if (!Array.isArray(x.content)) {
6239
6338
  const text = contentText(x.content);
6240
6339
  if (text.length <= cap) return x;
@@ -7389,8 +7488,8 @@ function isTranscriptComplete(text) {
7389
7488
  const lastWord = words4[words4.length - 1];
7390
7489
  if (COMPLETE_ENDINGS.has(lastWord)) return true;
7391
7490
  if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
7392
- const lower = trimmed.toLowerCase();
7393
- for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
7491
+ const lower2 = trimmed.toLowerCase();
7492
+ for (const marker of CONTINUATION_MARKERS) if (lower2.endsWith(marker)) return false;
7394
7493
  if (words4.length <= 2) return false;
7395
7494
  return true;
7396
7495
  }
@@ -12393,11 +12492,11 @@ Another agent just implemented the above. Independently check the CURRENT state
12393
12492
  const list = id ? [this.tasks.get(String(id))].filter(Boolean) : [...this.tasks.values()];
12394
12493
  if (!list.length) return id ? `No task '${id}'.` : "No background tasks.";
12395
12494
  const cap = id ? 1500 : 300;
12396
- const clip = (x) => x.length > cap ? `${x.slice(0, cap)}\u2026 (truncated)` : x;
12495
+ const clip2 = (x) => x.length > cap ? `${x.slice(0, cap)}\u2026 (truncated)` : x;
12397
12496
  return list.map((t) => {
12398
12497
  const head = `${t.id} (${t.label}): ${t.status}`;
12399
12498
  const body = (t.status === "done" ? t.spokenText?.trim() || t.result?.trim() : t.status === "error" ? t.result?.trim() : "") ?? "";
12400
- return body ? `${head}. ${t.status === "error" ? "Error" : "Result"}: ${clip(body)}` : head;
12499
+ return body ? `${head}. ${t.status === "error" ? "Error" : "Result"}: ${clip2(body)}` : head;
12401
12500
  }).join("\n");
12402
12501
  }
12403
12502
  };
@@ -14986,6 +15085,7 @@ function readJsonFile(path, fallback) {
14986
15085
  }
14987
15086
 
14988
15087
  // cli/core.ts
15088
+ init_models();
14989
15089
  init_hostPath();
14990
15090
  init_platform();
14991
15091
  var DEFAULT_TOOLS = ["bash", "Read", "Edit", "Write", "Grep", "Glob", "MultiEdit", "ApplyEdits", "RepoMap", "TodoWrite"];
@@ -15182,6 +15282,14 @@ Reference files in them by their mount path (the left side).`;
15182
15282
  const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
15183
15283
  return po ? { providerOptions: o.lean && isClaudeCode ? { ...po, settingSources: [] } : po } : {};
15184
15284
  })(),
15285
+ // A child on ANOTHER provider (delegate mode's anthropic planner → claude-code/* worker, or back) gets that
15286
+ // runtime's own options (cwd, env, MCP) — the Agent default inherits only same-provider options, so a claude-code
15287
+ // worker under an anthropic planner ran with none. Same provider: the Agent default (parent's, minus its resume).
15288
+ providerOptionsFor: (m) => {
15289
+ const a = agentRef.agent, po = a?.options.providerOptions;
15290
+ if (modelProvider(m) === modelProvider(a?.options.model ?? "")) return po ? { ...po, resume: void 0, onClaudeCodeSession: void 0 } : void 0;
15291
+ return providerOptionsFor(m, cwd, o.mcpServers, o.hostMcpServers);
15292
+ },
15185
15293
  ...(() => {
15186
15294
  const now4 = /* @__PURE__ */ new Date();
15187
15295
  const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
@@ -19732,7 +19840,7 @@ function makeHost(format = "text", opts) {
19732
19840
  spinner.start();
19733
19841
  return;
19734
19842
  }
19735
- const clip = (s) => s.length > 200 ? s.slice(0, 200) + "\u2026" : s;
19843
+ const clip2 = (s) => s.length > 200 ? s.slice(0, 200) + "\u2026" : s;
19736
19844
  const terminal = a.status === "completed" || a.status === "error";
19737
19845
  if (!terminal || !a.id || !activitySeen.has(a.id)) {
19738
19846
  if (a.id) activitySeen.add(a.id);
@@ -19751,8 +19859,8 @@ function makeHost(format = "text", opts) {
19751
19859
  }
19752
19860
  const lines = text.split("\n");
19753
19861
  const shown = lines.slice(0, previewLines());
19754
- err(paint(" \u23BF " + clip(shown[0])) + "\n");
19755
- for (const ln of shown.slice(1)) err(dim(" " + clip(ln)) + "\n");
19862
+ err(paint(" \u23BF " + clip2(shown[0])) + "\n");
19863
+ for (const ln of shown.slice(1)) err(dim(" " + clip2(ln)) + "\n");
19756
19864
  const more = lines.length - shown.length;
19757
19865
  if (more > 0 || a.truncated?.result) err(dim(` \u2026 (${more > 0 ? `+${more} more line${more > 1 ? "s" : ""}` : "truncated by backend"}${a.truncated?.result && more > 0 ? ", truncated" : ""} \xB7 ctrl+o expands)
19758
19866
  `));
@@ -19988,7 +20096,7 @@ function formatTranscriptFull(messages, opts) {
19988
20096
  if (!shown.length) return dim(" (empty transcript)\n");
19989
20097
  const results = /* @__PURE__ */ new Map();
19990
20098
  for (const m of shown) if (m.role === "tool" && m.tool_call_id) results.set(m.tool_call_id, m);
19991
- const clip = (s) => {
20099
+ const clip2 = (s) => {
19992
20100
  const lines = s.split("\n");
19993
20101
  return lines.length > cap ? lines.slice(0, cap).join("\n") + `
19994
20102
  \u2026 (+${lines.length - cap} more lines)` : s;
@@ -20009,7 +20117,7 @@ function formatTranscriptFull(messages, opts) {
20009
20117
  }
20010
20118
  out.push(cyan(" \u2699 ") + tc.function.name + dim(" " + summarizeCall(tc.function.name, args)) + "\n");
20011
20119
  const r = results.get(tc.id);
20012
- if (r) out.push(dim(" " + clip(contentText(r.content).trimEnd()).replace(/\n/g, "\n ")) + "\n");
20120
+ if (r) out.push(dim(" " + clip2(contentText(r.content).trimEnd()).replace(/\n/g, "\n ")) + "\n");
20013
20121
  }
20014
20122
  }
20015
20123
  }