@bunshin-cc/bunshin 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +761 -390
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -155,8 +155,8 @@ async function askClaudeCli(opts) {
155
155
  return raw;
156
156
  }
157
157
  async function askCodexCli(opts) {
158
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-codex-"));
159
- const outFile = path.join(dir, "answer.txt");
158
+ const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "bunshin-codex-"));
159
+ const outFile = path.join(dir2, "answer.txt");
160
160
  const args = ["exec", "--skip-git-repo-check", "--ephemeral", "-s", "read-only", "-o", outFile];
161
161
  if (opts.model) args.push("-m", opts.model);
162
162
  if (opts.effort) args.push("-c", `model_reasoning_effort="${opts.effort}"`);
@@ -170,7 +170,7 @@ ${opts.user}`;
170
170
  await run("codex", args, { stdin: prompt, cwd: opts.cwd, timeoutMs: opts.timeoutMs ?? 3e5, backend: "codex-cli" });
171
171
  return fs.readFileSync(outFile, "utf8");
172
172
  } finally {
173
- fs.rmSync(dir, { recursive: true, force: true });
173
+ fs.rmSync(dir2, { recursive: true, force: true });
174
174
  }
175
175
  }
176
176
  async function askPiCli(opts) {
@@ -312,8 +312,8 @@ import path2 from "node:path";
312
312
  function newId(prefix) {
313
313
  return `${prefix}_${randomBytes(4).toString("hex")}`;
314
314
  }
315
- function ensureDir(dir, mode = 448) {
316
- fs2.mkdirSync(dir, { recursive: true, mode });
315
+ function ensureDir(dir2, mode = 448) {
316
+ fs2.mkdirSync(dir2, { recursive: true, mode });
317
317
  }
318
318
  function realish(p) {
319
319
  const abs = path2.resolve(p);
@@ -450,9 +450,9 @@ function trustStore(root) {
450
450
  const real = realish(root);
451
451
  const list2 = trustedStores();
452
452
  if (list2.includes(real)) return;
453
- const dir = configDir();
454
- fs3.mkdirSync(dir, { recursive: true, mode: 448 });
455
- fs3.chmodSync(dir, 448);
453
+ const dir2 = configDir();
454
+ fs3.mkdirSync(dir2, { recursive: true, mode: 448 });
455
+ fs3.chmodSync(dir2, 448);
456
456
  const file = trustFile();
457
457
  const tmp = `${file}.${process.pid}.${randomBytes2(3).toString("hex")}.tmp`;
458
458
  fs3.writeFileSync(tmp, JSON.stringify({ stores: [...list2, real] }, null, 2) + "\n", { mode: 384 });
@@ -616,13 +616,13 @@ var init_store = __esm({
616
616
  }
617
617
  static candidates(cwd) {
618
618
  const out = [];
619
- let dir = path4.resolve(cwd);
619
+ let dir2 = path4.resolve(cwd);
620
620
  for (; ; ) {
621
- const candidate = path4.join(dir, ".bunshin");
621
+ const candidate = path4.join(dir2, ".bunshin");
622
622
  if (fileExists(path4.join(candidate, "config.json"))) out.push(candidate);
623
- const parent = path4.dirname(dir);
624
- if (parent === dir) return out;
625
- dir = parent;
623
+ const parent = path4.dirname(dir2);
624
+ if (parent === dir2) return out;
625
+ dir2 = parent;
626
626
  }
627
627
  }
628
628
  static init(root, config) {
@@ -1200,10 +1200,10 @@ function isLocalLayer(layer) {
1200
1200
  return kind === "user" || kind === "robot";
1201
1201
  }
1202
1202
  function withScope(layers, scope) {
1203
- const path28 = scopePath(scope?.tree, scope?.source);
1204
- if (!path28.length) return layers;
1203
+ const path29 = scopePath(scope?.tree, scope?.source);
1204
+ if (!path29.length) return layers;
1205
1205
  const i = layers.findIndex((l) => isLocalLayer(l));
1206
- return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
1206
+ return i < 0 ? [...layers, ...path29] : [...layers.slice(0, i), ...path29, ...layers.slice(i)];
1207
1207
  }
1208
1208
  function readerFor(ctx, ownerClearance, scope) {
1209
1209
  if (ctx.kind === "robot" && !scope?.source) scope = { ...scope, source: `robot:${ctx.id}` };
@@ -1250,11 +1250,11 @@ function baseAffinity(section, reader) {
1250
1250
  const kind = layerKind(section.layer);
1251
1251
  if (kind === "group" || kind === "source") {
1252
1252
  if (!reader.layers.includes(section.layer)) return 0.05;
1253
- const path28 = reader.layers.filter((l) => {
1253
+ const path29 = reader.layers.filter((l) => {
1254
1254
  const k = layerKind(l);
1255
1255
  return k === "group" || k === "source";
1256
1256
  });
1257
- return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
1257
+ return 0.6 + 0.4 * ((path29.indexOf(section.layer) + 1) / path29.length);
1258
1258
  }
1259
1259
  if (isLocalLayer(section.layer)) return reader.layers.includes(section.layer) ? 1 : 0;
1260
1260
  return 0.5;
@@ -1454,19 +1454,19 @@ var init_document = __esm({
1454
1454
  function repoLayer(repo) {
1455
1455
  return `source:${repo.includes("/") ? "repo" : "folder"}:${repo}`;
1456
1456
  }
1457
- function repoSectionId(repo, path28, anchor = "") {
1458
- return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
1457
+ function repoSectionId(repo, path29, anchor = "") {
1458
+ return anchor ? `repo:${repo}/${path29}#${anchor}` : `repo:${repo}/${path29}`;
1459
1459
  }
1460
- function docKind(path28) {
1461
- const base = path28.split("/").pop()?.toLowerCase() ?? "";
1460
+ function docKind(path29) {
1461
+ const base = path29.split("/").pop()?.toLowerCase() ?? "";
1462
1462
  if (base === "testing.md") return "verification";
1463
1463
  if (base === "skill.md") return "skill";
1464
1464
  return "spec";
1465
1465
  }
1466
- function defaultPaths(path28) {
1467
- const dir = path28.split("/").slice(0, -1).join("/");
1468
- if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
1469
- return [`${dir}/**`];
1466
+ function defaultPaths(path29) {
1467
+ const dir2 = path29.split("/").slice(0, -1).join("/");
1468
+ if (!dir2 || dir2.startsWith(".claude/") || dir2.startsWith(".codex/")) return [];
1469
+ return [`${dir2}/**`];
1470
1470
  }
1471
1471
  function parseRepoDoc(doc, opts) {
1472
1472
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
@@ -1609,11 +1609,11 @@ function lintSkills(sections) {
1609
1609
  }
1610
1610
  const state = /* @__PURE__ */ new Map();
1611
1611
  const cycles = /* @__PURE__ */ new Set();
1612
- const dfs = (n, path28) => {
1612
+ const dfs = (n, path29) => {
1613
1613
  state.set(n, 1);
1614
1614
  for (const d of edges[n] ?? []) {
1615
- if (state.get(d) === 1) cycles.add([...path28.slice(path28.indexOf(d)), d].join(" -> "));
1616
- else if (!state.get(d)) dfs(d, [...path28, d]);
1615
+ if (state.get(d) === 1) cycles.add([...path29.slice(path29.indexOf(d)), d].join(" -> "));
1616
+ else if (!state.get(d)) dfs(d, [...path29, d]);
1617
1617
  }
1618
1618
  state.set(n, 2);
1619
1619
  };
@@ -1700,8 +1700,8 @@ function parsePythonSkills(text2) {
1700
1700
  }
1701
1701
  return { skills, constants };
1702
1702
  }
1703
- function harnessSectionId(path28, symbol) {
1704
- return `harness:${path28}#${symbol}`;
1703
+ function harnessSectionId(path29, symbol) {
1704
+ return `harness:${path29}#${symbol}`;
1705
1705
  }
1706
1706
  var DEF_RE;
1707
1707
  var init_python = __esm({
@@ -1827,8 +1827,8 @@ function globToRegExp(glob) {
1827
1827
  globCache.set(glob, out);
1828
1828
  return out;
1829
1829
  }
1830
- function matchesGlob(path28, glob) {
1831
- const p = path28.replace(/^\.\//, "").replace(/\\/g, "/");
1830
+ function matchesGlob(path29, glob) {
1831
+ const p = path29.replace(/^\.\//, "").replace(/\\/g, "/");
1832
1832
  return globToRegExp(glob.replace(/^\.\//, "")).test(p);
1833
1833
  }
1834
1834
  var DEFAULT_EMBEDDERS, globCache;
@@ -1920,10 +1920,10 @@ function push(sections, reader, query) {
1920
1920
  for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
1921
1921
  const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
1922
1922
  for (const t of triggers) {
1923
- const path28 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1924
- const entity = path28 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1925
- const skill = path28 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1926
- const trigger = path28 ? `path:${path28}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1923
+ const path29 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1924
+ const entity = path29 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1925
+ const skill = path29 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1926
+ const trigger = path29 ? `path:${path29}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1927
1927
  if (trigger) {
1928
1928
  hits.set(s.id, { section: s, trigger, anchor: t.anchor });
1929
1929
  break;
@@ -2078,6 +2078,53 @@ var init_engine = __esm({
2078
2078
  }
2079
2079
  });
2080
2080
 
2081
+ // ../core/src/v2/recall/router.ts
2082
+ function routerMessage(prompt, ctx, inContext2) {
2083
+ const lines = ["Session context:"];
2084
+ lines.push(ctx.first ? `- First prompt of this session: ${ctx.first.slice(0, 600)}` : "- This is the first prompt of the session.");
2085
+ if (ctx.recent?.length) lines.push("- Recent user prompts (oldest first):", ...ctx.recent.slice(-3).map((r) => ` \xB7 ${r.slice(0, 300)}`));
2086
+ if (ctx.reply) lines.push(`- Agent's last reply: ${ctx.reply.slice(0, 1500)}`);
2087
+ lines.push(`- Already in the agent's context: ${inContext2.length ? inContext2.join(", ") : "nothing from the library"}`);
2088
+ return `${lines.join("\n")}
2089
+
2090
+ Latest user prompt (verbatim, may be truncated):
2091
+ <<<
2092
+ ${prompt.slice(0, 6e3)}
2093
+ >>>
2094
+ Return the JSON line.`;
2095
+ }
2096
+ function parseRouterAnswer(text2, items, skip = []) {
2097
+ const m = /\{[\s\S]*\}/.exec(text2);
2098
+ if (!m) return null;
2099
+ let o;
2100
+ try {
2101
+ o = JSON.parse(m[0]);
2102
+ } catch {
2103
+ return null;
2104
+ }
2105
+ const skipped = new Set(skip);
2106
+ const byId = new Map(items.filter((i) => !skipped.has(i.id)).map((i) => [i.id, i]));
2107
+ const ids = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && byId.has(x)) : [];
2108
+ const serve = typeof o.serve === "string" && byId.has(o.serve) ? o.serve : void 0;
2109
+ const rules = [...new Set(ids(o.rules).filter((id) => byId.get(id).lesson && id !== serve))].slice(0, MAX_RULES);
2110
+ const hint = ids(o.hints).filter((id) => id !== serve && !rules.includes(id)).slice(0, 2);
2111
+ return { ...serve ? { serve } : {}, rules, hint };
2112
+ }
2113
+ var MAX_RULES, RULES;
2114
+ var init_router = __esm({
2115
+ "../core/src/v2/recall/router.ts"() {
2116
+ "use strict";
2117
+ MAX_RULES = 3;
2118
+ RULES = `You are the prompt-time recall router for an engineering team's agent library. A coding agent is about to act on the user's latest prompt. You see the prompt plus session context: the session's first prompt, recent user prompts, the agent's last reply, and the items already in the agent's context. Decide what, if anything, to inject now.
2119
+
2120
+ - "serve": at most ONE skill or document, injected in full (long). Serve only when this prompt starts or clearly redirects work that the item is the procedure for, and it is not already in context. Follow-ups, approvals, status questions, chatter, and continuing work already under way need none.
2121
+ - "rules": at most ${MAX_RULES} LESSONS (short corrections or preferences learned from this team before), injected in full. Include a lesson only when what the agent will DO in response to this prompt \u2014 including the steps the work it starts normally leads to (a ticket ends in a PR and a report; a fix ends in evidence and a merge) \u2014 is the lesson's situation, even if the user does not mention the lesson. Not for shared topic words, not for preferences that would not change the agent's next actions, not if already in context. When unsure, include none.
2122
+ - "hints": up to 2 other possibly relevant document ids, shown as titles only.
2123
+
2124
+ Only use ids from the library. Output exactly one line of JSON and nothing else: {"serve": "<id or null>", "rules": ["<lesson id>", ...], "hints": ["<id>", ...]}`;
2125
+ }
2126
+ });
2127
+
2081
2128
  // ../core/src/v2/guidelines.ts
2082
2129
  function reflectionGuidelines(sections, reader, opts = {}) {
2083
2130
  const covering = opts.layer ? /* @__PURE__ */ new Set(["org", opts.layer, ...opts.tree ? ancestorScopes(opts.tree, opts.layer) : []]) : void 0;
@@ -2770,8 +2817,17 @@ A lesson is read by agents on other tasks, months from now. Write the rule, not
2770
2817
 
2771
2818
  Every entry is a whole document (a skill, a spec, a policy). Prefer editing an existing document
2772
2819
  over creating one, and aim the edit: "anchor" names the heading (from its headings list) the text
2773
- belongs under, so it lands in the right part of the document. Library documents relevant to this
2774
- session:
2820
+ belongs under, so it lands in the right part of the document.
2821
+
2822
+ Steering and approach lessons (a, e) are the exception: each is its own short lesson. Use op
2823
+ "create" with kind "rule": a title naming the behaviour, a "when" naming the situations it applies
2824
+ in (as the user would phrase a request there, even one that never mentions it), and a body of at
2825
+ most about 120 words: the rule, why, and when it was learned. Do not append such a lesson inside a
2826
+ long skill or procedure: a rule buried in a long document goes unfollowed even when the document is
2827
+ loaded, and the user ends up repeating it. If a short rule in the list below already says it, append
2828
+ to that rule instead. Traps, procedures and facts (b, c, d) still go into the document they belong to.
2829
+
2830
+ Library documents relevant to this session:
2775
2831
  ${index}
2776
2832
  ${input.skills?.length ? `
2777
2833
  Skills in the library (target any of them by id, listed here or not):
@@ -2791,9 +2847,9 @@ A fact, command or habit of this project \u2192 this project. True of every proj
2791
2847
  that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
2792
2848
  are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
2793
2849
  "layer" in the target of a new document (an edit keeps the document's own scope).
2794
- ` : ""}${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
2795
- not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
2796
- A lesson for anyone doing the task belongs in the task's document instead.
2850
+ ` : ""}${input.profileId ? `- How THIS person works in every task alike (tone, length, how they like to be asked) \u2192
2851
+ op "append" to their profile ${input.profileId}, anchor "how-i-work". A preference that applies in
2852
+ one kind of situation (how they want PRs, reports, deploys) is a short rule lesson in their layer.
2797
2853
  ` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
2798
2854
  - Cross-repo facts, another team's rules, non-code processes \u2192 a Bunshin section at the right layer.
2799
2855
  - If it only holds in some contexts, use op "append" with a "condition" (e.g. "objects over 50 g").
@@ -3152,6 +3208,7 @@ var init_v2 = __esm({
3152
3208
  init_python();
3153
3209
  init_text_index();
3154
3210
  init_engine();
3211
+ init_router();
3155
3212
  init_reflect();
3156
3213
  init_digest();
3157
3214
  init_reflect_context();
@@ -3316,17 +3373,17 @@ function listDocs(root) {
3316
3373
  if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs6.existsSync(path5.join(root, rel)) && !deployedSkill(root, rel)).sort();
3317
3374
  const out = [];
3318
3375
  let visited = 0;
3319
- const walk = (dir, rel, depth) => {
3376
+ const walk = (dir2, rel, depth) => {
3320
3377
  if (depth > 8 || visited++ > 2e4) return;
3321
3378
  let entries;
3322
3379
  try {
3323
- entries = fs6.readdirSync(dir, { withFileTypes: true });
3380
+ entries = fs6.readdirSync(dir2, { withFileTypes: true });
3324
3381
  } catch {
3325
3382
  return;
3326
3383
  }
3327
3384
  for (const e of entries) {
3328
3385
  if (e.isDirectory()) {
3329
- if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir, e.name, ".git"))) walk(path5.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
3386
+ if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir2, e.name, ".git"))) walk(path5.join(dir2, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
3330
3387
  } else if (e.isFile()) {
3331
3388
  const r = rel ? `${rel}/${e.name}` : e.name;
3332
3389
  if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
@@ -4065,6 +4122,14 @@ var init_src = __esm({
4065
4122
  });
4066
4123
 
4067
4124
  // src/v2/store.ts
4125
+ var store_exports = {};
4126
+ __export(store_exports, {
4127
+ V2Store: () => V2Store,
4128
+ addUnique: () => addUnique,
4129
+ fileSafe: () => fileSafe,
4130
+ hexId: () => hexId,
4131
+ normalizeSession: () => normalizeSession
4132
+ });
4068
4133
  import fs8 from "node:fs";
4069
4134
  import path7 from "node:path";
4070
4135
  import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
@@ -4183,12 +4248,12 @@ var init_store2 = __esm({
4183
4248
  return s;
4184
4249
  }
4185
4250
  listSessions() {
4186
- const dir = this.p("sessions");
4187
- if (!fileExists(dir)) return [];
4251
+ const dir2 = this.p("sessions");
4252
+ if (!fileExists(dir2)) return [];
4188
4253
  const out = [];
4189
- for (const f of fs8.readdirSync(dir)) {
4254
+ for (const f of fs8.readdirSync(dir2)) {
4190
4255
  if (!f.endsWith(".json") || f === "current.json") continue;
4191
- const raw = readJson(path7.join(dir, f), null);
4256
+ const raw = readJson(path7.join(dir2, f), null);
4192
4257
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
4193
4258
  }
4194
4259
  return out;
@@ -4286,12 +4351,12 @@ var init_store2 = __esm({
4286
4351
  }
4287
4352
  }
4288
4353
  listDir(name) {
4289
- const dir = this.p(name);
4290
- if (!fileExists(dir)) return [];
4354
+ const dir2 = this.p(name);
4355
+ if (!fileExists(dir2)) return [];
4291
4356
  const out = [];
4292
- for (const f of fs8.readdirSync(dir)) {
4357
+ for (const f of fs8.readdirSync(dir2)) {
4293
4358
  if (!f.endsWith(".json")) continue;
4294
- const v = readJson(path7.join(dir, f), null);
4359
+ const v = readJson(path7.join(dir2, f), null);
4295
4360
  if (v) out.push(v);
4296
4361
  }
4297
4362
  return out;
@@ -4365,8 +4430,8 @@ function removeFile(file) {
4365
4430
  } catch {
4366
4431
  }
4367
4432
  }
4368
- function pruneDirs(dir, stop) {
4369
- let d = dir;
4433
+ function pruneDirs(dir2, stop) {
4434
+ let d = dir2;
4370
4435
  while (d.startsWith(stop) && d !== stop) {
4371
4436
  try {
4372
4437
  if (fs9.readdirSync(d).length) return;
@@ -4377,16 +4442,16 @@ function pruneDirs(dir, stop) {
4377
4442
  d = path8.dirname(d);
4378
4443
  }
4379
4444
  }
4380
- function listFiles(dir, base = dir) {
4445
+ function listFiles(dir2, base = dir2) {
4381
4446
  let entries;
4382
4447
  try {
4383
- entries = fs9.readdirSync(dir, { withFileTypes: true });
4448
+ entries = fs9.readdirSync(dir2, { withFileTypes: true });
4384
4449
  } catch {
4385
4450
  return [];
4386
4451
  }
4387
4452
  const out = [];
4388
4453
  for (const e of entries) {
4389
- const abs = path8.join(dir, e.name);
4454
+ const abs = path8.join(dir2, e.name);
4390
4455
  if (e.isDirectory()) out.push(...listFiles(abs, base));
4391
4456
  else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
4392
4457
  }
@@ -4394,8 +4459,8 @@ function listFiles(dir, base = dir) {
4394
4459
  }
4395
4460
  function childDir(root, name) {
4396
4461
  if (!name || !SAFE_NAME.test(name)) return null;
4397
- const dir = path8.join(root, name);
4398
- return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
4462
+ const dir2 = path8.join(root, name);
4463
+ return path8.dirname(path8.resolve(dir2)) === path8.resolve(root) && insideRoot(root, dir2) ? dir2 : null;
4399
4464
  }
4400
4465
  function layerRank(layer, tree) {
4401
4466
  const kind = layerKind(layer);
@@ -4424,17 +4489,17 @@ function planDeploy(bundle, p, harnesses) {
4424
4489
  const contents = /* @__PURE__ */ new Map();
4425
4490
  const dirs = [];
4426
4491
  for (const root of roots) {
4427
- const dir = childDir(root, name);
4428
- const body = dir && safeJoin(dir, "SKILL.md");
4429
- if (!dir || !body) {
4492
+ const dir2 = childDir(root, name);
4493
+ const body = dir2 && safeJoin(dir2, "SKILL.md");
4494
+ if (!dir2 || !body) {
4430
4495
  warnings.push(`skipped skill ${name} in ${root}: its folder leads outside ${root}`);
4431
4496
  continue;
4432
4497
  }
4433
- dirs.push(dir);
4498
+ dirs.push(dir2);
4434
4499
  targets.push({ abs: body, rel: "SKILL.md", role: "body" });
4435
4500
  contents.set(body, s.body);
4436
4501
  for (const [rel, f] of Object.entries(s.skill.files ?? {})) {
4437
- const abs = safeJoin(dir, rel);
4502
+ const abs = safeJoin(dir2, rel);
4438
4503
  if (!abs || rel === "SKILL.md" || rel === MANIFEST) continue;
4439
4504
  targets.push({ abs, rel, role: "file", ...f.mode ? { mode: f.mode } : {} });
4440
4505
  contents.set(abs, f.content);
@@ -4574,10 +4639,10 @@ function captureDoc(c, doc, out) {
4574
4639
  if (chosen) byRel.set(key, chosen);
4575
4640
  }
4576
4641
  if (doc.kind === "skill") {
4577
- for (const dir of doc.dirs ?? []) {
4578
- for (const rel of listFiles(dir)) {
4642
+ for (const dir2 of doc.dirs ?? []) {
4643
+ for (const rel of listFiles(dir2)) {
4579
4644
  if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
4580
- const abs = path8.join(dir, rel);
4645
+ const abs = path8.join(dir2, rel);
4581
4646
  const content = readText(abs);
4582
4647
  if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
4583
4648
  byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
@@ -4621,23 +4686,23 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4621
4686
  continue;
4622
4687
  }
4623
4688
  for (const name of names) {
4624
- const dir = path8.join(root, name);
4625
- if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
4626
- const skillMd = readText(path8.join(dir, "SKILL.md"));
4689
+ const dir2 = path8.join(root, name);
4690
+ if (owned.has(dir2) || incoming.has(name) || only && name !== only) continue;
4691
+ const skillMd = readText(path8.join(dir2, "SKILL.md"));
4627
4692
  if (skillMd === null) continue;
4628
4693
  let born = 0;
4629
4694
  try {
4630
- const st = fs9.statSync(path8.join(dir, "SKILL.md"));
4695
+ const st = fs9.statSync(path8.join(dir2, "SKILL.md"));
4631
4696
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4632
4697
  } catch {
4633
4698
  continue;
4634
4699
  }
4635
4700
  if (!(born >= since - BORN_SLACK_MS) && !only) continue;
4636
4701
  const files = {};
4637
- for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
4702
+ for (const rel of listFiles(dir2).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
4638
4703
  if (rel === "SKILL.md" || rel === MANIFEST) continue;
4639
- const content = readText(path8.join(dir, rel));
4640
- if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir, rel)) ? { mode: "x" } : {} };
4704
+ const content = readText(path8.join(dir2, rel));
4705
+ if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir2, rel)) ? { mode: "x" } : {} };
4641
4706
  }
4642
4707
  const hash = sha(JSON.stringify([skillMd, files]));
4643
4708
  if (state.created[name]?.hash === hash) continue;
@@ -4758,11 +4823,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4758
4823
  const prevByAbs = new Map((prev?.targets ?? []).map((t) => [`${t.abs}#${t.role}`, t]));
4759
4824
  const foreign = /* @__PURE__ */ new Set();
4760
4825
  if (d.kind === "skill" && !prev) {
4761
- for (const dir of d.dirs ?? []) {
4762
- const cur = readText(path8.join(dir, "SKILL.md"));
4826
+ for (const dir2 of d.dirs ?? []) {
4827
+ const cur = readText(path8.join(dir2, "SKILL.md"));
4763
4828
  if (cur === null || cur === d.section.body || state.created[d.name]) continue;
4764
- foreign.add(dir);
4765
- report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
4829
+ foreign.add(dir2);
4830
+ report.warnings.push(`${dir2} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
4766
4831
  }
4767
4832
  delete state.created[d.name];
4768
4833
  }
@@ -4771,7 +4836,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4771
4836
  targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
4772
4837
  continue;
4773
4838
  }
4774
- if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
4839
+ if ([...foreign].some((dir2) => t.abs.startsWith(dir2 + path8.sep))) continue;
4775
4840
  if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
4776
4841
  report.warnings.push(`${t.abs} not written: it leads outside its folder`);
4777
4842
  continue;
@@ -4801,20 +4866,20 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4801
4866
  }
4802
4867
  }
4803
4868
  if (d.kind === "skill") {
4804
- for (const dir of prev?.dirs ?? []) {
4805
- if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4806
- removeFile(path8.join(dir, MANIFEST));
4807
- pruneDirs(dir, path8.dirname(dir));
4808
- }
4809
- for (const dir of d.dirs ?? []) {
4810
- const manifest = safeJoin(dir, MANIFEST);
4811
- if (foreign.has(dir) || !manifest || !fs9.existsSync(path8.join(dir, "SKILL.md"))) continue;
4869
+ for (const dir2 of prev?.dirs ?? []) {
4870
+ if ((d.dirs ?? []).includes(dir2) || !removable(dir2)) continue;
4871
+ removeFile(path8.join(dir2, MANIFEST));
4872
+ pruneDirs(dir2, path8.dirname(dir2));
4873
+ }
4874
+ for (const dir2 of d.dirs ?? []) {
4875
+ const manifest = safeJoin(dir2, MANIFEST);
4876
+ if (foreign.has(dir2) || !manifest || !fs9.existsSync(path8.join(dir2, "SKILL.md"))) continue;
4812
4877
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4813
4878
  }
4814
4879
  report.skills++;
4815
4880
  } else if (d.kind === "pack") report.packFiles += targets.length;
4816
4881
  if (d.kind === "instructions") report.instructions++;
4817
- const dirs = d.dirs?.filter((dir) => !foreign.has(dir));
4882
+ const dirs = d.dirs?.filter((dir2) => !foreign.has(dir2));
4818
4883
  next[id] = { sectionId: id, kind: d.kind, name: d.name, version: d.section.version, targets, ...dirs ? { dirs } : {} };
4819
4884
  }
4820
4885
  for (const [id, doc] of Object.entries(state.docs)) {
@@ -4831,14 +4896,14 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4831
4896
  report.removed++;
4832
4897
  }
4833
4898
  }
4834
- for (const dir of doc.dirs ?? []) {
4835
- if (!removable(dir)) continue;
4836
- removeFile(path8.join(dir, MANIFEST));
4837
- pruneDirs(dir, path8.dirname(dir));
4838
- if (fs9.existsSync(dir)) {
4839
- const left = listFiles(dir);
4840
- if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4841
- else pruneDirs(dir, path8.dirname(dir));
4899
+ for (const dir2 of doc.dirs ?? []) {
4900
+ if (!removable(dir2)) continue;
4901
+ removeFile(path8.join(dir2, MANIFEST));
4902
+ pruneDirs(dir2, path8.dirname(dir2));
4903
+ if (fs9.existsSync(dir2)) {
4904
+ const left = listFiles(dir2);
4905
+ if (left.length) report.warnings.push(`${dir2} kept: it has files Bunshin did not write`);
4906
+ else pruneDirs(dir2, path8.dirname(dir2));
4842
4907
  }
4843
4908
  }
4844
4909
  }
@@ -4874,9 +4939,9 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4874
4939
  const packs = new Set(plan.skills.concat([...plan.docs.values()].map((d) => d.section)).map((s) => s.pack).filter((x) => !!x));
4875
4940
  if (harnesses.has("claude-code")) {
4876
4941
  for (const pack of packs) {
4877
- const dir = childDir(p.library, pack);
4878
- if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
4879
- const link = path8.join(dir, ".claude", "skills");
4942
+ const dir2 = childDir(p.library, pack);
4943
+ if (!dir2 || !insideRoot(dir2, path8.join(dir2, ".claude", "x"))) continue;
4944
+ const link = path8.join(dir2, ".claude", "skills");
4880
4945
  try {
4881
4946
  fs9.lstatSync(link);
4882
4947
  if (fs9.readlinkSync(link) !== p.claudeSkills) {
@@ -4931,10 +4996,10 @@ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
4931
4996
  const s = readState(paths, config.orgId);
4932
4997
  return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
4933
4998
  }
4934
- function materialize(sections, dir) {
4999
+ function materialize(sections, dir2) {
4935
5000
  let files = 0;
4936
5001
  const put = (rel, content, mode) => {
4937
- const abs = safeJoin(dir, rel);
5002
+ const abs = safeJoin(dir2, rel);
4938
5003
  if (!abs) return;
4939
5004
  ensureDir(path8.dirname(abs));
4940
5005
  writeFile(abs, content, mode);
@@ -4961,20 +5026,20 @@ function linkSkills(p, root, harnesses, names) {
4961
5026
  for (const h of harnesses) {
4962
5027
  const spec = LINK_DIRS[h];
4963
5028
  if (!spec) continue;
4964
- const dir = path8.join(abs, spec.rel);
5029
+ const dir2 = path8.join(abs, spec.rel);
4965
5030
  const canonical = path8.resolve(p[spec.key]);
4966
5031
  const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
4967
5032
  let entries = [];
4968
5033
  try {
4969
- entries = fs9.readdirSync(dir, { withFileTypes: true });
5034
+ entries = fs9.readdirSync(dir2, { withFileTypes: true });
4970
5035
  } catch {
4971
5036
  }
4972
5037
  for (const e of entries) {
4973
5038
  if (!e.isSymbolicLink()) continue;
4974
- const link = path8.join(dir, e.name);
5039
+ const link = path8.join(dir2, e.name);
4975
5040
  let target = "";
4976
5041
  try {
4977
- target = path8.resolve(dir, fs9.readlinkSync(link));
5042
+ target = path8.resolve(dir2, fs9.readlinkSync(link));
4978
5043
  } catch {
4979
5044
  continue;
4980
5045
  }
@@ -4988,7 +5053,7 @@ function linkSkills(p, root, harnesses, names) {
4988
5053
  out.removed++;
4989
5054
  }
4990
5055
  for (const name of want) {
4991
- const link = path8.join(dir, name);
5056
+ const link = path8.join(dir2, name);
4992
5057
  try {
4993
5058
  fs9.lstatSync(link);
4994
5059
  out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
@@ -4996,7 +5061,7 @@ function linkSkills(p, root, harnesses, names) {
4996
5061
  } catch {
4997
5062
  }
4998
5063
  try {
4999
- ensureDir(dir);
5064
+ ensureDir(dir2);
5000
5065
  fs9.symlinkSync(path8.join(canonical, name), link, "dir");
5001
5066
  excluded.push(path8.join(spec.rel, name));
5002
5067
  out.linked++;
@@ -5004,7 +5069,7 @@ function linkSkills(p, root, harnesses, names) {
5004
5069
  out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
5005
5070
  }
5006
5071
  }
5007
- pruneDirs(dir, abs);
5072
+ pruneDirs(dir2, abs);
5008
5073
  }
5009
5074
  out.kept = excluded.length > 0;
5010
5075
  gitExclude(abs, excluded);
@@ -5909,19 +5974,19 @@ function renderFeed(f) {
5909
5974
  return out.join("\n").trimEnd() + "\n";
5910
5975
  }
5911
5976
  function latestFeed(v2) {
5912
- const dir = v2.p("ui");
5977
+ const dir2 = v2.p("ui");
5913
5978
  let best = null;
5914
5979
  try {
5915
- for (const f2 of fs13.readdirSync(dir)) {
5980
+ for (const f2 of fs13.readdirSync(dir2)) {
5916
5981
  if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
5917
- const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
5982
+ const m = fs13.statSync(path9.join(dir2, f2)).mtimeMs;
5918
5983
  if (!best || m > best.m) best = { f: f2, m };
5919
5984
  }
5920
5985
  } catch {
5921
5986
  return null;
5922
5987
  }
5923
5988
  if (!best) return null;
5924
- const f = readJson(path9.join(dir, best.f), null);
5989
+ const f = readJson(path9.join(dir2, best.f), null);
5925
5990
  return f && f.v === UI_FEED_VERSION ? f : null;
5926
5991
  }
5927
5992
  var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
@@ -6438,6 +6503,202 @@ var init_reflect3 = __esm({
6438
6503
  }
6439
6504
  });
6440
6505
 
6506
+ // src/v2/route.ts
6507
+ var route_exports = {};
6508
+ __export(route_exports, {
6509
+ ROUTER_MODEL: () => ROUTER_MODEL,
6510
+ lastReply: () => lastReply,
6511
+ promptContext: () => promptContext,
6512
+ readAnswer: () => readAnswer,
6513
+ readJob: () => readJob,
6514
+ routeJob: () => routeJob,
6515
+ sweepRouter: () => sweepRouter,
6516
+ writeJob: () => writeJob
6517
+ });
6518
+ import { spawn as spawn4 } from "node:child_process";
6519
+ import { createHash as createHash5 } from "node:crypto";
6520
+ import fs16 from "node:fs";
6521
+ import path11 from "node:path";
6522
+ function readJson2(file) {
6523
+ try {
6524
+ return JSON.parse(fs16.readFileSync(file, "utf8"));
6525
+ } catch {
6526
+ return null;
6527
+ }
6528
+ }
6529
+ function lastReply(transcriptPath, maxBytes = 256e3) {
6530
+ if (!transcriptPath) return void 0;
6531
+ try {
6532
+ const fd = fs16.openSync(transcriptPath, "r");
6533
+ try {
6534
+ const size = fs16.fstatSync(fd).size;
6535
+ const len = Math.min(size, maxBytes);
6536
+ const buf = Buffer.alloc(len);
6537
+ fs16.readSync(fd, buf, 0, len, size - len);
6538
+ const lines = buf.toString("utf8").split("\n");
6539
+ for (let i = lines.length - 1; i >= 0; i--) {
6540
+ let m;
6541
+ try {
6542
+ m = JSON.parse(lines[i]);
6543
+ } catch {
6544
+ continue;
6545
+ }
6546
+ if (m?.type !== "assistant" || m.isSidechain) continue;
6547
+ const c = m.message?.content;
6548
+ const text2 = Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("\n") : typeof c === "string" ? c : "";
6549
+ if (text2.trim()) return redact2(text2.trim()).slice(0, 1500);
6550
+ }
6551
+ } finally {
6552
+ fs16.closeSync(fd);
6553
+ }
6554
+ } catch {
6555
+ }
6556
+ return void 0;
6557
+ }
6558
+ function promptContext(session) {
6559
+ const recent = session.recentPrompts ?? [];
6560
+ if (!recent.length) return {};
6561
+ const reply = lastReply(session.transcriptPath);
6562
+ return { ...session.task ? { first: session.task.slice(0, 1e3) } : {}, recent, ...reply ? { reply } : {} };
6563
+ }
6564
+ async function routerIndex(store, v2, source) {
6565
+ const file = path11.join(dir(v2), `index-${createHash5("sha256").update(source).digest("hex").slice(0, 12)}.json`);
6566
+ const cached = readJson2(file);
6567
+ if (cached && Date.now() - Date.parse(cached.fetchedAt) < INDEX_TTL_MS) return cached;
6568
+ try {
6569
+ const res = await v2api(
6570
+ store.config(),
6571
+ "POST",
6572
+ "/recall/index",
6573
+ { ...source ? { source } : {}, ...cached ? { have: cached.hash } : {} },
6574
+ { timeoutMs: 15e3 }
6575
+ );
6576
+ const next = res.unchanged && cached ? { ...cached, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() } : { hash: res.hash, system: res.system ?? "", items: res.items ?? [], source, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
6577
+ if (!next.system) throw new Error("the server sent no index");
6578
+ writeJsonAtomic(file, next);
6579
+ return next;
6580
+ } catch (err) {
6581
+ if (cached) return cached;
6582
+ throw err;
6583
+ }
6584
+ }
6585
+ function askClaude(systemFile, message, model, cwd) {
6586
+ const bin = process.env.BUNSHIN_CLAUDE_BIN || "claude";
6587
+ const args = [
6588
+ "-p",
6589
+ "--model",
6590
+ model,
6591
+ "--effort",
6592
+ "low",
6593
+ "--settings",
6594
+ JSON.stringify({ disableAllHooks: true }),
6595
+ "--setting-sources",
6596
+ "",
6597
+ "--tools",
6598
+ "",
6599
+ "--no-session-persistence",
6600
+ "--exclude-dynamic-system-prompt-sections",
6601
+ "--system-prompt-file",
6602
+ systemFile,
6603
+ "--output-format",
6604
+ "json",
6605
+ message
6606
+ ];
6607
+ return new Promise((resolve, reject) => {
6608
+ const child = spawn4(bin, args, { cwd, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
6609
+ let out = "";
6610
+ let err = "";
6611
+ let done = false;
6612
+ const finish = (fn) => {
6613
+ if (done) return;
6614
+ done = true;
6615
+ clearTimeout(timer);
6616
+ child.kill("SIGTERM");
6617
+ fn();
6618
+ };
6619
+ const timer = setTimeout(() => finish(() => reject(new Error(`claude took over ${CLAUDE_TIMEOUT_MS / 1e3} s`))), CLAUDE_TIMEOUT_MS);
6620
+ child.stdout.on("data", (d) => {
6621
+ out += d.toString();
6622
+ try {
6623
+ const parsed = JSON.parse(out);
6624
+ finish(() => resolve(parsed));
6625
+ } catch {
6626
+ }
6627
+ });
6628
+ child.stderr.on("data", (d) => err += d.toString());
6629
+ child.on("error", (e) => finish(() => reject(e)));
6630
+ child.on("close", (code2) => finish(() => reject(new Error(`claude exited ${code2}: ${(err || out).trim().slice(0, 300)}`))));
6631
+ });
6632
+ }
6633
+ async function routeJob(store, sessionId, seq) {
6634
+ const v2 = new V2Store(store);
6635
+ const config = store.config();
6636
+ const job = readJob(v2, sessionId);
6637
+ if (!job || job.seq !== seq || job.cancelled) return null;
6638
+ const started = Date.parse(job.at) || Date.now();
6639
+ const write = (a) => {
6640
+ const answer = { seq, at: (/* @__PURE__ */ new Date()).toISOString(), ms: Date.now() - started, ...a };
6641
+ writeJsonAtomic(answerFile(v2, sessionId, seq), answer);
6642
+ v2.log("v2-router", { session: sessionId, seq, serve: answer.serve ?? null, rules: answer.rules ?? [], hint: answer.hint ?? [], ms: answer.ms, modelMs: answer.modelMs, error: answer.error, dense: job.dense?.serve ?? null, usage: answer.usage });
6643
+ return answer;
6644
+ };
6645
+ try {
6646
+ const session = v2.readSession(sessionId);
6647
+ if (!session?.lastPrompt) return write({ error: "no prompt in the session file" });
6648
+ if (!canCall(config)) return write({ error: "offline: the router's index comes from the server" });
6649
+ const index = await routerIndex(store, v2, session.source ?? "");
6650
+ const systemFile = path11.join(dir(v2), `system-${index.hash}.txt`);
6651
+ if (!fs16.existsSync(systemFile)) fs16.writeFileSync(systemFile, index.system);
6652
+ const cwd = path11.join(dir(v2), "cwd");
6653
+ fs16.mkdirSync(cwd, { recursive: true });
6654
+ const inContext2 = session.inContext ?? [...session.loaded, ...session.pushed];
6655
+ const titles = new Map((v2.readBundle()?.sections ?? []).map((s) => [s.id, s.skill?.name ?? s.title]));
6656
+ const message = routerMessage(session.lastPrompt, promptContext(session), inContext2.map((id) => titles.get(id)).filter((t) => !!t));
6657
+ const res = await askClaude(systemFile, message, config.recall.routerModel || process.env.BUNSHIN_ROUTER_MODEL || ROUTER_MODEL, cwd);
6658
+ const u = res.usage ?? {};
6659
+ const usage = { input: u.input_tokens ?? 0, cacheRead: u.cache_read_input_tokens ?? 0, cacheWrite: u.cache_creation_input_tokens ?? 0, output: u.output_tokens ?? 0, ...u.output_tokens_details?.thinking_tokens ? { thinking: u.output_tokens_details.thinking_tokens } : {} };
6660
+ const meta = { ...res.duration_ms ? { modelMs: res.duration_ms } : {}, usage };
6661
+ if (res.is_error) return write({ ...meta, error: `claude: ${(res.result ?? "error").slice(0, 300)}` });
6662
+ const picks = parseRouterAnswer(res.result ?? "", index.items, inContext2);
6663
+ if (!picks) return write({ ...meta, error: `not JSON: ${(res.result ?? "").slice(0, 120)}` });
6664
+ return write({ ...picks, ...meta });
6665
+ } catch (err) {
6666
+ return write({ error: (err instanceof Error ? err.message : String(err)).slice(0, 300) });
6667
+ }
6668
+ }
6669
+ function sweepRouter(v2, now = Date.now()) {
6670
+ try {
6671
+ for (const f of fs16.readdirSync(dir(v2))) {
6672
+ if (!/\.(job|answer)\.json$/.test(f)) continue;
6673
+ const p = path11.join(dir(v2), f);
6674
+ if (now - fs16.statSync(p).mtimeMs > 864e5) fs16.rmSync(p, { force: true });
6675
+ }
6676
+ } catch {
6677
+ }
6678
+ }
6679
+ var ROUTER_MODEL, INDEX_TTL_MS, CLAUDE_TIMEOUT_MS, dir, safe2, jobFile, answerFile, readJob, writeJob, readAnswer;
6680
+ var init_route = __esm({
6681
+ "src/v2/route.ts"() {
6682
+ "use strict";
6683
+ init_v22();
6684
+ init_llm();
6685
+ init_redact2();
6686
+ init_util();
6687
+ init_api();
6688
+ init_store2();
6689
+ ROUTER_MODEL = "claude-haiku-5-5";
6690
+ INDEX_TTL_MS = 10 * 6e4;
6691
+ CLAUDE_TIMEOUT_MS = 6e4;
6692
+ dir = (v2) => path11.join(v2.dir, "router");
6693
+ safe2 = (id) => id.replace(/[^\w.-]/g, "_");
6694
+ jobFile = (v2, session) => path11.join(dir(v2), `${safe2(session)}.job.json`);
6695
+ answerFile = (v2, session, seq) => path11.join(dir(v2), `${safe2(session)}.${seq}.answer.json`);
6696
+ readJob = (v2, session) => readJson2(jobFile(v2, session));
6697
+ writeJob = (v2, session, job) => writeJsonAtomic(jobFile(v2, session), job);
6698
+ readAnswer = (v2, session, seq) => readJson2(answerFile(v2, session, seq));
6699
+ }
6700
+ });
6701
+
6441
6702
  // src/v2/uicmd.ts
6442
6703
  var uicmd_exports = {};
6443
6704
  __export(uicmd_exports, {
@@ -6450,51 +6711,51 @@ __export(uicmd_exports, {
6450
6711
  modSource: () => modSource
6451
6712
  });
6452
6713
  import { spawnSync } from "node:child_process";
6453
- import fs16 from "node:fs";
6714
+ import fs17 from "node:fs";
6454
6715
  import os3 from "node:os";
6455
- import path11 from "node:path";
6716
+ import path12 from "node:path";
6456
6717
  import { fileURLToPath } from "node:url";
6457
6718
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
6458
- return path11.join(home, ".bunshin", "claude-code-ui");
6719
+ return path12.join(home, ".bunshin", "claude-code-ui");
6459
6720
  }
6460
6721
  function modSource() {
6461
- const here = path11.dirname(fileURLToPath(import.meta.url));
6462
- for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
6463
- if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6722
+ const here = path12.dirname(fileURLToPath(import.meta.url));
6723
+ for (const dir2 of [path12.join(here, "claude-code-ui"), path12.join(here, "..", "claude-code-ui"), path12.resolve(here, "../../../../adapters/claude-code-ui")]) {
6724
+ if (fs17.existsSync(path12.join(dir2, ".claude-plugin", "plugin.json"))) return dir2;
6464
6725
  }
6465
6726
  return null;
6466
6727
  }
6467
6728
  function installedModVersion(home) {
6468
6729
  try {
6469
- return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6730
+ return JSON.parse(fs17.readFileSync(path12.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6470
6731
  } catch {
6471
6732
  return null;
6472
6733
  }
6473
6734
  }
6474
6735
  function copyDir(from, to) {
6475
- fs16.mkdirSync(to, { recursive: true });
6476
- for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
6736
+ fs17.mkdirSync(to, { recursive: true });
6737
+ for (const e of fs17.readdirSync(from, { withFileTypes: true })) {
6477
6738
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
6478
6739
  if (e.name === "node_modules") continue;
6479
- const a = path11.join(from, e.name);
6480
- const b = path11.join(to, e.name);
6740
+ const a = path12.join(from, e.name);
6741
+ const b = path12.join(to, e.name);
6481
6742
  if (e.isDirectory()) copyDir(a, b);
6482
- else fs16.copyFileSync(a, b);
6743
+ else fs17.copyFileSync(a, b);
6483
6744
  }
6484
6745
  }
6485
6746
  function installMod(version2, opts = {}) {
6486
6747
  const src = modSource();
6487
6748
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
6488
6749
  const home = modHome(opts.home);
6489
- const dir = path11.join(home, MOD_PLUGIN);
6490
- fs16.rmSync(dir, { recursive: true, force: true });
6491
- copyDir(src, dir);
6492
- const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
6493
- const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
6494
- fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6495
- fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
6496
- fs16.writeFileSync(
6497
- path11.join(home, ".claude-plugin", "marketplace.json"),
6750
+ const dir2 = path12.join(home, MOD_PLUGIN);
6751
+ fs17.rmSync(dir2, { recursive: true, force: true });
6752
+ copyDir(src, dir2);
6753
+ const manifest = path12.join(dir2, ".claude-plugin", "plugin.json");
6754
+ const m = JSON.parse(fs17.readFileSync(manifest, "utf8"));
6755
+ fs17.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6756
+ fs17.mkdirSync(path12.join(home, ".claude-plugin"), { recursive: true });
6757
+ fs17.writeFileSync(
6758
+ path12.join(home, ".claude-plugin", "marketplace.json"),
6498
6759
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6499
6760
  );
6500
6761
  const run2 = opts.run ?? spawnSync;
@@ -6508,7 +6769,7 @@ function installMod(version2, opts = {}) {
6508
6769
  };
6509
6770
  if (!step(["plugin", "marketplace", "add", home])) step(["plugin", "marketplace", "update", MOD_MARKETPLACE]);
6510
6771
  if (!step(["plugin", "install", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`, "--scope", "user"])) step(["plugin", "update", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`]);
6511
- return { dir, version: version2, steps };
6772
+ return { dir: dir2, version: version2, steps };
6512
6773
  }
6513
6774
  async function cmdUi(store, args, out, err, version2) {
6514
6775
  const sub = args._[1];
@@ -6529,7 +6790,7 @@ async function cmdUi(store, args, out, err, version2) {
6529
6790
  `);
6530
6791
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6531
6792
  `);
6532
- out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path11.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6793
+ out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path12.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6533
6794
  }
6534
6795
  return installed || !failed.length ? 0 : 1;
6535
6796
  }
@@ -6610,19 +6871,19 @@ __export(update_exports, {
6610
6871
  updateCheckDue: () => updateCheckDue,
6611
6872
  updateNotice: () => updateNotice
6612
6873
  });
6613
- import fs17 from "node:fs";
6614
- import path12 from "node:path";
6874
+ import fs18 from "node:fs";
6875
+ import path13 from "node:path";
6615
6876
  function readState2() {
6616
6877
  try {
6617
- return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6878
+ return JSON.parse(fs18.readFileSync(stateFile(), "utf8"));
6618
6879
  } catch {
6619
6880
  return {};
6620
6881
  }
6621
6882
  }
6622
6883
  function writeState(s) {
6623
6884
  try {
6624
- fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
6625
- fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6885
+ fs18.mkdirSync(path13.dirname(stateFile()), { recursive: true });
6886
+ fs18.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6626
6887
  } catch {
6627
6888
  }
6628
6889
  }
@@ -6672,14 +6933,14 @@ var init_update = __esm({
6672
6933
  PKG = "@bunshin-cc/bunshin";
6673
6934
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6674
6935
  DAY = 864e5;
6675
- stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6936
+ stateFile = () => path13.join(homeDir(), ".bunshin", "update.json");
6676
6937
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6677
6938
  }
6678
6939
  });
6679
6940
 
6680
6941
  // src/v2/hooks.ts
6681
- import fs18 from "node:fs";
6682
- import path13 from "node:path";
6942
+ import fs19 from "node:fs";
6943
+ import path14 from "node:path";
6683
6944
  function inContext(s) {
6684
6945
  return s.inContext ?? [...s.loaded, ...s.pushed];
6685
6946
  }
@@ -6746,10 +7007,10 @@ async function keepModCurrent(ctx, config) {
6746
7007
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6747
7008
  const current = version2();
6748
7009
  if (!modSource2() || installedModVersion2() === current) return;
6749
- const marker = path13.join(modHome2(), `.attempted-${current}`);
6750
- if (fs18.existsSync(marker)) return;
6751
- fs18.mkdirSync(modHome2(), { recursive: true });
6752
- fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
7010
+ const marker = path14.join(modHome2(), `.attempted-${current}`);
7011
+ if (fs19.existsSync(marker)) return;
7012
+ fs19.mkdirSync(modHome2(), { recursive: true });
7013
+ fs19.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6753
7014
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6754
7015
  } catch {
6755
7016
  }
@@ -6878,13 +7139,13 @@ function skillHint(sec, others) {
6878
7139
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6879
7140
  return `Bunshin: the \`${sec.skill.name}\` skill may apply: ${sec.skill.description.replace(/\s+/g, " ").trim()} If it does, invoke it (Skill tool, or \`/${sec.skill.name}\`) before doing this by hand.${also}`;
6880
7141
  }
6881
- async function remoteAuto(ctx, prompt, exclude, source) {
7142
+ async function remoteAuto(ctx, prompt, exclude, source, context) {
6882
7143
  const config = ctx.store.config();
6883
7144
  if (!canCall(config) || !config.recall.useMothership) return null;
6884
7145
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6885
7146
  if (budget < 300) return null;
6886
7147
  try {
6887
- const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
7148
+ const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {}, ...context ? { context } : {} }, { timeoutMs: budget });
6888
7149
  return res.available ? res : null;
6889
7150
  } catch {
6890
7151
  return null;
@@ -6901,6 +7162,14 @@ function invokedSkill(prompt, sections) {
6901
7162
  const name = m[1].split(":").pop().toLowerCase();
6902
7163
  return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
6903
7164
  }
7165
+ function startRoute(v2, ctx, sessionId, now) {
7166
+ const prev = readJob(v2, sessionId);
7167
+ if (prev && !prev.delivered && !prev.cancelled) v2.log("v2-router-undelivered", { session: sessionId, seq: prev.seq, answered: !!readAnswer(v2, sessionId, prev.seq) });
7168
+ const job = { seq: (prev?.seq ?? 0) + 1, at: now };
7169
+ writeJob(v2, sessionId, job);
7170
+ ctx.detach(["--root", ctx.store.root, "route", "--session", sessionId, "--seq", String(job.seq), "--quiet"], { cwd: ctx.cwd });
7171
+ return job;
7172
+ }
6904
7173
  async function onPrompt(ctx) {
6905
7174
  const prompt = ctx.input.prompt ?? "";
6906
7175
  if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
@@ -6908,6 +7177,7 @@ async function onPrompt(ctx) {
6908
7177
  const now = (/* @__PURE__ */ new Date()).toISOString();
6909
7178
  const session = touch(v2, ctx, (s) => {
6910
7179
  if (!s.task && prompt.trim()) s.task = redact2(prompt.trim()).slice(0, 2e3);
7180
+ if (s.lastPrompt && prompt.trim()) s.recentPrompts = [...s.recentPrompts ?? [], s.lastPrompt.slice(0, 600)].slice(-3);
6911
7181
  s.lastPrompt = redact2(prompt).slice(0, 4e3);
6912
7182
  s.lastActivityAt = now;
6913
7183
  });
@@ -6921,7 +7191,9 @@ async function onPrompt(ctx) {
6921
7191
  const config = ctx.store.config();
6922
7192
  const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6923
7193
  const injected = inContext(session);
7194
+ const job = config.recall.router === "claude-cli" && canCall(config) ? startRoute(v2, ctx, session.sessionId, now) : void 0;
6924
7195
  const named = invokedSkill(prompt, view.sections);
7196
+ if (named && job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6925
7197
  if (named) {
6926
7198
  const why = `you ran /${named.skill.name}`;
6927
7199
  const deployed = deployEnabled(config, view.ctx);
@@ -6939,19 +7211,51 @@ async function onPrompt(ctx) {
6939
7211
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6940
7212
  }
6941
7213
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6942
- const remote = await remoteAuto(ctx, prompt, injected, session.source);
7214
+ const recent = session.recentPrompts ?? [];
7215
+ const context = {
7216
+ ...recent.length && session.task ? { first: session.task.slice(0, 1e3) } : {},
7217
+ ...recent.length ? { recent } : {}
7218
+ };
7219
+ const reply = recent.length ? lastReply(ctx.input.transcript_path ?? session.transcriptPath) : void 0;
7220
+ if (reply) context.reply = reply;
7221
+ const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
7222
+ const local = (id) => view.sections.find((s) => s.id === id);
7223
+ if (job && !remote?.router) {
7224
+ const dense = remote ? { serve: remote.section?.id, hint: (remote.hint ?? []).map((h) => h.id), reason: remote.reason } : { serve: res.section?.id, hint: res.hint.map((h) => h.id), reason: res.reason };
7225
+ writeJob(v2, session.sessionId, { ...job, dense: { ...dense, hint: dense.hint.slice(0, 3) } });
7226
+ return withLive({});
7227
+ }
7228
+ if (job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6943
7229
  if (remote) {
6944
- const local = (id) => view.sections.find((s) => s.id === id);
7230
+ const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
6945
7231
  if (remote.section) {
6946
7232
  res.section = local(remote.section.id) ?? remote.section;
6947
7233
  res.anchor = void 0;
6948
- res.reason = `dense ${remote.reason ?? ""}`.trim();
7234
+ res.reason = why;
6949
7235
  } else {
6950
7236
  res.section = void 0;
6951
7237
  res.hint = (remote.hint ?? []).map((h) => ({ id: h.id, title: local(h.id)?.title ?? h.title }));
6952
- res.reason = `dense ${remote.reason ?? ""}`.trim();
7238
+ res.reason = why;
6953
7239
  }
6954
7240
  }
7241
+ const rules = (remote?.rules ?? []).filter((r) => !injected.includes(r.id) && r.id !== res.section?.id).map((r) => local(r.id) ?? r);
7242
+ let lessons = "";
7243
+ if (rules.length) {
7244
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
7245
+ for (const r of rules) {
7246
+ addUnique(s.served, r.id);
7247
+ addUnique(s.loaded, r.id);
7248
+ markInContext(s, r.id);
7249
+ }
7250
+ });
7251
+ v2.log("v2-auto-rules", { session: session.sessionId, sections: rules.map((r) => r.id), reason: res.reason });
7252
+ updateFeed(v2, session.sessionId, (f) => {
7253
+ for (const r of [...rules].reverse()) addServed(f, { id: r.id, title: r.title, kind: r.kind, how: "auto", why: "a lesson learned before applies here" });
7254
+ addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
7255
+ });
7256
+ lessons = rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })).join("\n\n");
7257
+ }
7258
+ const withLessons = (text2) => withLive(additional("UserPromptSubmit", [text2, lessons].filter(Boolean).join("\n\n")));
6955
7259
  if (res.section) {
6956
7260
  const sec = res.section;
6957
7261
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
@@ -6965,10 +7269,10 @@ async function onPrompt(ctx) {
6965
7269
  addServed(f, { id: sec.id, title: sec.title, kind: sec.kind, ...sec.skill ? { skill: sec.skill.name } : {}, how: "auto", why });
6966
7270
  addEvent(f, "served", `served ${label({ ...sec.skill ? { skill: sec.skill.name } : {}, title: sec.title, kind: sec.kind })} \u2014 ${why}`);
6967
7271
  });
6968
- return withLive(additional("UserPromptSubmit", renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) })));
7272
+ return withLessons(renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) }));
6969
7273
  }
6970
- const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
6971
- if (!fresh.length) return withLive({});
7274
+ const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id) && !rules.some((r) => r.id === h.id)).slice(0, 3);
7275
+ if (!fresh.length) return withLessons("");
6972
7276
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
6973
7277
  s.hinted = s.hinted ?? [];
6974
7278
  addUnique(s.hinted, ...fresh.map((h) => h.id));
@@ -6982,8 +7286,8 @@ async function onPrompt(ctx) {
6982
7286
  addEvent(f, "hint", `possibly relevant: ${fresh.map((h) => h.title).join(", ")}`);
6983
7287
  });
6984
7288
  const topSkill = view.sections.find((s) => s.id === fresh[0].id && s.skill);
6985
- if (topSkill && deployEnabled(config, view.ctx)) return withLive(additional("UserPromptSubmit", skillHint(topSkill, fresh.slice(1).map((h) => h.title))));
6986
- return withLive(additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness)));
7289
+ if (topSkill && deployEnabled(config, view.ctx)) return withLessons(skillHint(topSkill, fresh.slice(1).map((h) => h.title)));
7290
+ return withLessons(hintLine(fresh.map((h) => h.title), config.harness));
6987
7291
  }
6988
7292
  function toolPaths(input) {
6989
7293
  if (!input || typeof input !== "object") return [];
@@ -6994,15 +7298,69 @@ function toolPaths(input) {
6994
7298
  }
6995
7299
  return [...new Set(out)];
6996
7300
  }
6997
- function onPreToolUse(ctx) {
7301
+ async function routedText(v2, ctx, session) {
7302
+ const config = ctx.store.config();
7303
+ if (config.recall.router !== "claude-cli") return "";
7304
+ const job = readJob(v2, session.sessionId);
7305
+ if (!job || job.delivered || job.cancelled) return "";
7306
+ const answer = readAnswer(v2, session.sessionId, job.seq);
7307
+ if (!answer) return "";
7308
+ const fallback = !!answer.error;
7309
+ writeJob(v2, session.sessionId, { ...job, delivered: { at: (/* @__PURE__ */ new Date()).toISOString(), ...ctx.input.tool_name ? { tool: ctx.input.tool_name } : {}, ...fallback ? { fallback } : {} } });
7310
+ const seen = new Set(inContext(session));
7311
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
7312
+ const find = async (id) => {
7313
+ const here = view.sections.find((s) => s.id === id);
7314
+ if (here || !canCall(config)) return here;
7315
+ try {
7316
+ return (await v2api(config, "GET", `/sections/${encodeURIComponent(id)}`, void 0, { timeoutMs: Math.max(300, Math.min(2e3, ctx.deadline - Date.now() - 300)) })).section;
7317
+ } catch {
7318
+ return void 0;
7319
+ }
7320
+ };
7321
+ const serveId = fallback ? job.dense?.serve : answer.serve;
7322
+ const fresh = (ids2) => [...new Set(ids2.filter((id) => !!id && !seen.has(id)))];
7323
+ const [doc] = await Promise.all(fresh([serveId]).map(find));
7324
+ const rules = (await Promise.all(fresh(fallback ? [] : answer.rules ?? []).filter((id) => id !== doc?.id).map(find))).filter((s) => !!s);
7325
+ const hints = fresh(fallback ? job.dense?.hint ?? [] : answer.hint ?? []).filter((id) => id !== doc?.id && !session.hinted?.includes(id));
7326
+ const hintTitles = hints.map((id) => view.sections.find((s) => s.id === id)?.title).filter((t) => !!t);
7327
+ v2.log("v2-router-delivered", { session: session.sessionId, seq: job.seq, tool: ctx.input.tool_name, afterMs: Date.now() - Date.parse(job.at), serve: doc?.id ?? null, rules: rules.map((r) => r.id), hint: hints, fallback, ...answer.error ? { error: answer.error } : {} });
7328
+ const ids = [...doc ? [doc.id] : [], ...rules.map((r) => r.id)];
7329
+ if (!ids.length && !hintTitles.length) return "";
7330
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
7331
+ addUnique(s.served, ...ids, ...hints);
7332
+ addUnique(s.loaded, ...ids);
7333
+ markInContext(s, ...ids);
7334
+ s.hinted = s.hinted ?? [];
7335
+ addUnique(s.hinted, ...hints);
7336
+ });
7337
+ const why = fallback ? "your request matches it" : "the router picked it for the user's latest request";
7338
+ updateFeed(v2, session.sessionId, (f) => {
7339
+ for (const r of [...rules].reverse()) addServed(f, { id: r.id, title: r.title, kind: r.kind, how: "auto", why: "a lesson learned before applies here" });
7340
+ if (doc) addServed(f, { id: doc.id, title: doc.title, kind: doc.kind, ...doc.skill ? { skill: doc.skill.name } : {}, how: "auto", why });
7341
+ if (doc) addEvent(f, "served", `served ${label({ ...doc.skill ? { skill: doc.skill.name } : {}, title: doc.title, kind: doc.kind })} \u2014 ${fallback ? "dense (router failed)" : "router"}`);
7342
+ if (rules.length) addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
7343
+ });
7344
+ return [
7345
+ doc ? renderSection(doc, why, { deployed: !!doc.skill && deployEnabled(config, view.ctx) }) : "",
7346
+ ...rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })),
7347
+ hintTitles.length ? hintLine(hintTitles, config.harness) : ""
7348
+ ].filter(Boolean).join("\n\n");
7349
+ }
7350
+ async function onPreToolUse(ctx) {
6998
7351
  const v2 = new V2Store(ctx.store);
6999
7352
  const session = touch(v2, ctx, (s) => {
7000
7353
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
7001
7354
  });
7002
7355
  const live = liveUpdate(v2, ctx, session);
7356
+ let routed = "";
7357
+ try {
7358
+ routed = await routedText(v2, ctx, session);
7359
+ } catch {
7360
+ }
7003
7361
  const withLive = (out) => {
7004
- if (!live) return out;
7005
- return additional("PreToolUse", [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n"));
7362
+ const text3 = [live, routed, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
7363
+ return text3 === (out.hookSpecificOutput?.additionalContext ?? "") ? out : additional("PreToolUse", text3);
7006
7364
  };
7007
7365
  const raw = toolPaths(ctx.input.tool_input);
7008
7366
  if (!raw.length) return withLive({});
@@ -7011,7 +7369,7 @@ function onPreToolUse(ctx) {
7011
7369
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
7012
7370
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
7013
7371
  if (!paths.length && !text2) return withLive({});
7014
- const seen = new Set(inContext(session));
7372
+ const seen = new Set(inContext(v2.readSession(session.sessionId) ?? session));
7015
7373
  const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
7016
7374
  if (!hits.length) return withLive({});
7017
7375
  const ids = hits.map((h) => h.section.id);
@@ -7262,8 +7620,10 @@ var init_hooks = __esm({
7262
7620
  init_signals();
7263
7621
  init_store2();
7264
7622
  init_ui();
7623
+ init_route();
7265
7624
  init_source();
7266
7625
  init_view();
7626
+ init_route();
7267
7627
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
7268
7628
  NUDGE_INTERVAL_MS = 60 * 6e4;
7269
7629
  CHECKPOINT_MS = 30 * 6e4;
@@ -7277,8 +7637,8 @@ var init_hooks = __esm({
7277
7637
  });
7278
7638
 
7279
7639
  // src/hooks.ts
7280
- import fs19 from "node:fs";
7281
- import path14 from "node:path";
7640
+ import fs20 from "node:fs";
7641
+ import path15 from "node:path";
7282
7642
  function normalizeEvent(raw) {
7283
7643
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
7284
7644
  }
@@ -7346,19 +7706,19 @@ async function handleHook(rawEvent, input, opts = {}) {
7346
7706
  }
7347
7707
  function deferToProject(cwd, env = process.env) {
7348
7708
  if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
7349
- let dir = path14.resolve(cwd);
7709
+ let dir2 = path15.resolve(cwd);
7350
7710
  for (let i = 0; i < 40; i++) {
7351
7711
  for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
7352
7712
  try {
7353
- const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
7713
+ const text2 = fs20.readFileSync(path15.join(dir2, f), "utf8");
7354
7714
  if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
7355
7715
  } catch {
7356
7716
  }
7357
7717
  }
7358
- if (fs19.existsSync(path14.join(dir, ".git"))) return false;
7359
- const up = path14.dirname(dir);
7360
- if (up === dir) return false;
7361
- dir = up;
7718
+ if (fs20.existsSync(path15.join(dir2, ".git"))) return false;
7719
+ const up = path15.dirname(dir2);
7720
+ if (up === dir2) return false;
7721
+ dir2 = up;
7362
7722
  }
7363
7723
  return false;
7364
7724
  }
@@ -7404,11 +7764,11 @@ var init_hooks2 = __esm({
7404
7764
  });
7405
7765
 
7406
7766
  // src/project-hooks.ts
7407
- import fs20 from "node:fs";
7408
- import path15 from "node:path";
7767
+ import fs21 from "node:fs";
7768
+ import path16 from "node:path";
7409
7769
  import { execFileSync as execFileSync4 } from "node:child_process";
7410
7770
  function codexProjectHooks(cwd) {
7411
- const legacyFile = path15.join(cwd, ".codex", "hooks.json");
7771
+ const legacyFile = path16.join(cwd, ".codex", "hooks.json");
7412
7772
  const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
7413
7773
  encoding: "utf8",
7414
7774
  stdio: ["ignore", "pipe", "ignore"],
@@ -7422,14 +7782,14 @@ function codexProjectHooks(cwd) {
7422
7782
  } catch {
7423
7783
  return { file: legacyFile, shared: false, legacyFile };
7424
7784
  }
7425
- const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
7785
+ const linked = fs21.realpathSync(common) !== fs21.realpathSync(directory);
7426
7786
  if (linked) {
7427
- if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
7787
+ if (path16.basename(common) !== ".git" || !fs21.statSync(common).isDirectory()) {
7428
7788
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
7429
7789
  }
7430
- root = path15.dirname(common);
7790
+ root = path16.dirname(common);
7431
7791
  }
7432
- const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
7792
+ const file = fs21.realpathSync(root) === fs21.realpathSync(cwd) ? legacyFile : path16.join(root, ".codex", "hooks.json");
7433
7793
  return { file, shared: linked, legacyFile };
7434
7794
  }
7435
7795
  var init_project_hooks = __esm({
@@ -7441,6 +7801,7 @@ var init_project_hooks = __esm({
7441
7801
  // src/init.ts
7442
7802
  var init_exports = {};
7443
7803
  __export(init_exports, {
7804
+ ALL_TOOLS_MATCHER: () => ALL_TOOLS_MATCHER,
7444
7805
  BORROW_COMMAND: () => BORROW_COMMAND,
7445
7806
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
7446
7807
  HOOK_EVENTS: () => HOOK_EVENTS,
@@ -7462,8 +7823,8 @@ __export(init_exports, {
7462
7823
  runInit: () => runInit
7463
7824
  });
7464
7825
  import { execFileSync as execFileSync5 } from "node:child_process";
7465
- import fs21 from "node:fs";
7466
- import path16 from "node:path";
7826
+ import fs22 from "node:fs";
7827
+ import path17 from "node:path";
7467
7828
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
7468
7829
  function isBunshinEntry(entry) {
7469
7830
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -7488,7 +7849,7 @@ function removeBunshinHooks(settings) {
7488
7849
  else delete out.hooks;
7489
7850
  return out;
7490
7851
  }
7491
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
7852
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false, allTools = false } = {}) {
7492
7853
  const base = removeBunshinHooks(settings);
7493
7854
  const hooks = { ...base.hooks ?? {} };
7494
7855
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
@@ -7496,7 +7857,8 @@ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code
7496
7857
  const hook = bunshinHookEntry(arg, timeout, bin);
7497
7858
  hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
7498
7859
  if (harness === "codex") delete hook._bunshin;
7499
- groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
7860
+ const m = matcher && allTools && event === "PreToolUse" ? ALL_TOOLS_MATCHER : matcher;
7861
+ groups.push(m ? { matcher: m, hooks: [hook] } : { hooks: [hook] });
7500
7862
  hooks[event] = groups;
7501
7863
  }
7502
7864
  return { ...base, hooks };
@@ -7508,16 +7870,16 @@ function countBunshinHooks(settings) {
7508
7870
  );
7509
7871
  }
7510
7872
  function claudeSettingsPath() {
7511
- return path16.join(homeDir(), ".claude", "settings.json");
7873
+ return path17.join(homeDir(), ".claude", "settings.json");
7512
7874
  }
7513
7875
  function installPiExtension(cwd, remove = false, scope = "repo") {
7514
- const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
7876
+ const file = scope === "global" ? path17.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path17.join(cwd, ".pi/extensions/bunshin.js");
7515
7877
  const marker = "// Managed by Bunshin setup.";
7516
7878
  try {
7517
- const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
7879
+ const previous = fileExists(file) ? fs22.readFileSync(file, "utf8") : "";
7518
7880
  if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
7519
7881
  if (remove) {
7520
- if (previous) fs21.unlinkSync(file);
7882
+ if (previous) fs22.unlinkSync(file);
7521
7883
  return { file, installed: 0, changed: !!previous };
7522
7884
  }
7523
7885
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -7526,24 +7888,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
7526
7888
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
7527
7889
  `;
7528
7890
  if (text2 !== previous) {
7529
- ensureDir(path16.dirname(file));
7530
- fs21.writeFileSync(file, text2);
7891
+ ensureDir(path17.dirname(file));
7892
+ fs22.writeFileSync(file, text2);
7531
7893
  }
7532
7894
  return { file, installed: 1, changed: text2 !== previous };
7533
7895
  } catch (error) {
7534
7896
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
7535
7897
  }
7536
7898
  }
7537
- function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
7899
+ function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false, allTools = false } = {}) {
7538
7900
  try {
7539
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7540
- const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
7901
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7902
+ const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user, allTools });
7541
7903
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7542
7904
  if (changed) {
7543
- ensureDir(path16.dirname(file));
7905
+ ensureDir(path17.dirname(file));
7544
7906
  if (fileExists(file)) {
7545
7907
  try {
7546
- fs21.copyFileSync(file, `${file}.bunshin-backup`);
7908
+ fs22.copyFileSync(file, `${file}.bunshin-backup`);
7547
7909
  } catch {
7548
7910
  }
7549
7911
  }
@@ -7558,23 +7920,23 @@ function mcpLauncher(bin) {
7558
7920
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7559
7921
  let script = process.argv[1] ?? "";
7560
7922
  try {
7561
- script = fs21.realpathSync(script);
7923
+ script = fs22.realpathSync(script);
7562
7924
  } catch {
7563
7925
  }
7564
7926
  return { command: process.execPath, args: [script, "mcp"] };
7565
7927
  }
7566
7928
  function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7567
7929
  try {
7568
- const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7930
+ const current = fileExists(file) ? fs22.readFileSync(file, "utf8") : null;
7569
7931
  const ours = current === null || current === content;
7570
7932
  if (remove) {
7571
- if (current !== null && ours) fs21.unlinkSync(file);
7933
+ if (current !== null && ours) fs22.unlinkSync(file);
7572
7934
  return { file, installed: 0, changed: current !== null && ours };
7573
7935
  }
7574
- if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
7936
+ if (!ours) return { file, installed: 0, changed: false, error: `a different ${path17.basename(file)} is already there; left as it is` };
7575
7937
  if (current === null) {
7576
- fs21.mkdirSync(path16.dirname(file), { recursive: true });
7577
- fs21.writeFileSync(file, content);
7938
+ fs22.mkdirSync(path17.dirname(file), { recursive: true });
7939
+ fs22.writeFileSync(file, content);
7578
7940
  }
7579
7941
  return { file, installed: 1, changed: current === null };
7580
7942
  } catch (err) {
@@ -7583,7 +7945,7 @@ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND })
7583
7945
  }
7584
7946
  function installMcpConfig({ file, launcher, remove = false }) {
7585
7947
  try {
7586
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7948
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7587
7949
  const servers = { ...before.mcpServers ?? {} };
7588
7950
  if (remove) delete servers.bunshin;
7589
7951
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7591,7 +7953,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7591
7953
  if (!Object.keys(servers).length) delete after.mcpServers;
7592
7954
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7593
7955
  if (changed) {
7594
- if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7956
+ if (!Object.keys(after).length && fileExists(file)) fs22.unlinkSync(file);
7595
7957
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7596
7958
  }
7597
7959
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7600,7 +7962,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7600
7962
  }
7601
7963
  }
7602
7964
  function installUserMcp({ launcher, remove = false, claude = "claude" }) {
7603
- const file = path16.join(homeDir(), ".claude.json");
7965
+ const file = path17.join(homeDir(), ".claude.json");
7604
7966
  const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
7605
7967
  try {
7606
7968
  let had = false;
@@ -7626,22 +7988,22 @@ function runInit(opts = {}) {
7626
7988
  const cwd = opts.cwd ?? process.cwd();
7627
7989
  const warnings = [];
7628
7990
  const scope = opts.scope ?? "repo";
7629
- const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
7630
- if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
7991
+ const root = scope === "global" ? path17.join(homeDir(), ".bunshin") : path17.join(cwd, ".bunshin");
7992
+ if (fs22.existsSync(root) && !new Store(root).trusted && fs22.readdirSync(root).length) {
7631
7993
  const aside = `${root}.untrusted-${Date.now()}`;
7632
- fs21.renameSync(root, aside);
7994
+ fs22.renameSync(root, aside);
7633
7995
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7634
7996
  }
7635
- const existed = fileExists(path16.join(root, "config.json"));
7997
+ const existed = fileExists(path17.join(root, "config.json"));
7636
7998
  const store = new Store(root);
7637
7999
  store.ensureLayout();
7638
- fs21.chmodSync(root, 448);
8000
+ fs22.chmodSync(root, 448);
7639
8001
  const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
7640
- if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
7641
- const ignore = path16.join(cwd, ".gitignore");
7642
- const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
8002
+ if (scope === "repo" && (hookScope === "project" || fileExists(path17.join(cwd, ".git")))) {
8003
+ const ignore = path17.join(cwd, ".gitignore");
8004
+ const current = fileExists(ignore) ? fs22.readFileSync(ignore, "utf8") : "";
7643
8005
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7644
- fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
8006
+ fs22.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7645
8007
  `);
7646
8008
  }
7647
8009
  }
@@ -7671,17 +8033,17 @@ function runInit(opts = {}) {
7671
8033
  }
7672
8034
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7673
8035
  const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7674
- const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "hooks.json") : hookScope === "project" ? path16.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
7675
- hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
8036
+ const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "hooks.json") : hookScope === "project" ? path17.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
8037
+ hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile, allTools: config.recall.router === "claude-cli" });
7676
8038
  if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
7677
- const local = path16.join(cwd, ".claude", "settings.local.json");
7678
- if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
8039
+ const local = path17.join(cwd, ".claude", "settings.local.json");
8040
+ if (fileExists(local) && path17.resolve(local) !== path17.resolve(file)) installClaudeHooks({ file: local, remove: true });
7679
8041
  }
7680
8042
  if (projectHooks?.shared) {
7681
8043
  hooks.sharedWithWorktrees = true;
7682
- if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
8044
+ if (!hooks.error && !opts.removeHooks) ensureDir(path17.dirname(projectHooks.legacyFile));
7683
8045
  }
7684
- if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
8046
+ if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs22.realpathSync(projectHooks.file) !== fs22.realpathSync(projectHooks.legacyFile)) {
7685
8047
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7686
8048
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7687
8049
  }
@@ -7691,15 +8053,15 @@ function runInit(opts = {}) {
7691
8053
  let learnCommand;
7692
8054
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
7693
8055
  const user = hookScope === "user";
7694
- mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path16.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
8056
+ mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path17.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
7695
8057
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
7696
- learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
8058
+ learnCommand = installLearnCommand({ file: path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7697
8059
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7698
8060
  }
7699
8061
  let borrowCommand;
7700
8062
  if (opts.removeHooks || !opts.noHooks) {
7701
8063
  const user = hookScope === "user";
7702
- const file = config.harness === "claude-code" ? path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path16.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path16.join(cwd, ".pi", "prompts", "borrow.md") : null;
8064
+ const file = config.harness === "claude-code" ? path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path17.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path17.join(cwd, ".pi", "prompts", "borrow.md") : null;
7703
8065
  if (file) {
7704
8066
  borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
7705
8067
  if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
@@ -7725,7 +8087,7 @@ function otherHarnessInstructions() {
7725
8087
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7726
8088
  ].join("\n");
7727
8089
  }
7728
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
8090
+ var PRE_TOOL_MATCHER, ALL_TOOLS_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
7729
8091
  var init_init = __esm({
7730
8092
  "src/init.ts"() {
7731
8093
  "use strict";
@@ -7735,6 +8097,7 @@ var init_init = __esm({
7735
8097
  init_project_hooks();
7736
8098
  init_trust();
7737
8099
  PRE_TOOL_MATCHER = "Read|Edit|Write|MultiEdit|NotebookEdit";
8100
+ ALL_TOOLS_MATCHER = "*";
7738
8101
  HOOK_EVENTS = [
7739
8102
  { event: "SessionStart", arg: "session-start", timeout: 10 },
7740
8103
  { event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
@@ -7856,9 +8219,9 @@ __export(commands_exports, {
7856
8219
  setupRobot: () => setupRobot,
7857
8220
  v2Status: () => v2Status
7858
8221
  });
7859
- import fs22 from "node:fs";
7860
- import path17 from "node:path";
7861
- import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
8222
+ import fs23 from "node:fs";
8223
+ import path18 from "node:path";
8224
+ import { createHash as createHash6, randomBytes as randomBytes4 } from "node:crypto";
7862
8225
  function cmdReview(store, args, cwd, out, err) {
7863
8226
  const v2 = new V2Store(store);
7864
8227
  const sub = args._[1] ?? "list";
@@ -8028,11 +8391,11 @@ function cmdSuggest(store, args, cwd, out, err) {
8028
8391
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
8029
8392
  return 1;
8030
8393
  }
8031
- const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8032
- const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
8394
+ const harnessDir = path18.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8395
+ const file = str3(args.flags.file) ? path18.resolve(cwd, str3(args.flags.file)) : path18.join(harnessDir, rel);
8033
8396
  let content;
8034
8397
  try {
8035
- content = fs22.readFileSync(file, "utf8");
8398
+ content = fs23.readFileSync(file, "utf8");
8036
8399
  } catch {
8037
8400
  err(`bunshin suggest: cannot read ${file}
8038
8401
  `);
@@ -8153,15 +8516,15 @@ async function cmdQuestions(store, args, out, err) {
8153
8516
  return 0;
8154
8517
  }
8155
8518
  function saveCredential(storeRoot, apiUrl, apiKey, id) {
8156
- const dir = path17.join(homeDir(), ".config/bunshin/credentials");
8157
- fs22.mkdirSync(dir, { recursive: true, mode: 448 });
8158
- fs22.chmodSync(dir, 448);
8159
- const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
8519
+ const dir2 = path18.join(homeDir(), ".config/bunshin/credentials");
8520
+ fs23.mkdirSync(dir2, { recursive: true, mode: 448 });
8521
+ fs23.chmodSync(dir2, 448);
8522
+ const file = path18.join(dir2, `${createHash6("sha256").update(`${storeRoot}
8160
8523
  ${apiUrl}
8161
8524
  ${id}`).digest("hex")}.json`);
8162
8525
  const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
8163
- fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8164
- fs22.renameSync(tmp, file);
8526
+ fs23.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8527
+ fs23.renameSync(tmp, file);
8165
8528
  return file;
8166
8529
  }
8167
8530
  async function setupRobot(opts) {
@@ -8169,10 +8532,10 @@ async function setupRobot(opts) {
8169
8532
  const existing = Store.discover(opts.cwd);
8170
8533
  let store;
8171
8534
  const harness = opts.harness ?? "other";
8172
- if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
8535
+ if (existing && path18.resolve(path18.dirname(existing.root)) === path18.resolve(opts.cwd) && harness === "other") store = existing;
8173
8536
  else {
8174
8537
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
8175
- store = new Store(path17.join(opts.cwd, ".bunshin"));
8538
+ store = new Store(path18.join(opts.cwd, ".bunshin"));
8176
8539
  }
8177
8540
  let config = store.config();
8178
8541
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -8326,16 +8689,16 @@ __export(setup_exports, {
8326
8689
  setupProject: () => setupProject,
8327
8690
  shellQuote: () => shellQuote
8328
8691
  });
8329
- import fs23 from "node:fs";
8330
- import path18 from "node:path";
8331
- import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
8692
+ import fs24 from "node:fs";
8693
+ import path19 from "node:path";
8694
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "node:crypto";
8332
8695
  function executable(name) {
8333
- const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
8696
+ const directories = [...(process.env.PATH ?? "").split(path19.delimiter), path19.join(homeDir(), ".local/bin")];
8334
8697
  for (const directory of directories) {
8335
- const file = path18.resolve(directory, name);
8698
+ const file = path19.resolve(directory, name);
8336
8699
  try {
8337
- fs23.accessSync(file, fs23.constants.X_OK);
8338
- if (fs23.statSync(file).isFile()) return file;
8700
+ fs24.accessSync(file, fs24.constants.X_OK);
8701
+ if (fs24.statSync(file).isFile()) return file;
8339
8702
  } catch {
8340
8703
  }
8341
8704
  }
@@ -8343,8 +8706,8 @@ function executable(name) {
8343
8706
  }
8344
8707
  function hookBinary(harness) {
8345
8708
  const agent = executable(harness === "claude-code" ? "claude" : harness);
8346
- const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
8347
- return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
8709
+ const directories = [.../* @__PURE__ */ new Set([path19.dirname(process.execPath), path19.join(homeDir(), ".local/bin"), ...agent ? [path19.dirname(agent)] : []])];
8710
+ return `PATH=${shellQuote(directories.join(path19.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs24.realpathSync(process.argv[1]))}`;
8348
8711
  }
8349
8712
  function apiAddress(raw) {
8350
8713
  const url = new URL(raw);
@@ -8413,23 +8776,23 @@ async function setupProject(opts) {
8413
8776
  const apiKey = opts.apiKey.trim();
8414
8777
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
8415
8778
  const profile = await connectionProfile(apiUrl, apiKey);
8416
- const root = path18.join(opts.cwd, ".bunshin");
8779
+ const root = path19.join(opts.cwd, ".bunshin");
8417
8780
  const store = new Store(root);
8418
8781
  const previous = store.config();
8419
8782
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
8420
8783
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
8421
8784
  }
8422
- const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
8423
- fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8424
- fs23.chmodSync(credentialDirectory, 448);
8425
- const name = createHash6("sha256").update(`${root}
8785
+ const credentialDirectory = path19.join(homeDir(), ".config/bunshin/credentials");
8786
+ fs24.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8787
+ fs24.chmodSync(credentialDirectory, 448);
8788
+ const name = createHash7("sha256").update(`${root}
8426
8789
  ${apiUrl}
8427
8790
  ${profile.orgId}
8428
8791
  ${profile.userId}`).digest("hex");
8429
- const credentialFile = path18.join(credentialDirectory, `${name}.json`);
8792
+ const credentialFile = path19.join(credentialDirectory, `${name}.json`);
8430
8793
  const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
8431
- fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8432
- fs23.renameSync(temporary, credentialFile);
8794
+ fs24.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8795
+ fs24.renameSync(temporary, credentialFile);
8433
8796
  const result = runInit({
8434
8797
  cwd: opts.cwd,
8435
8798
  harness: opts.harness,
@@ -8781,9 +9144,9 @@ __export(sync_exports2, {
8781
9144
  resolveConflict: () => resolveConflict,
8782
9145
  runSync: () => runSync
8783
9146
  });
8784
- import { createHash as createHash7 } from "node:crypto";
8785
- import path19 from "node:path";
8786
- import fs24 from "node:fs";
9147
+ import { createHash as createHash8 } from "node:crypto";
9148
+ import path20 from "node:path";
9149
+ import fs25 from "node:fs";
8787
9150
  async function api(config, method, route, body, timeoutMs = 1e4) {
8788
9151
  const ctrl = new AbortController();
8789
9152
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8821,22 +9184,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8821
9184
  }
8822
9185
  }
8823
9186
  async function runSync(store) {
8824
- const lock = path19.join(store.root, "sync.lock");
9187
+ const lock = path20.join(store.root, "sync.lock");
8825
9188
  const deadline = Date.now() + 12e4;
8826
9189
  for (; ; ) {
8827
9190
  try {
8828
- fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
9191
+ fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8829
9192
  break;
8830
9193
  } catch (error) {
8831
9194
  if (error.code !== "EEXIST") throw error;
8832
9195
  try {
8833
- const owner = Number(fs24.readFileSync(lock, "utf8"));
9196
+ const owner = Number(fs25.readFileSync(lock, "utf8"));
8834
9197
  if (Number.isInteger(owner) && owner > 0) {
8835
9198
  try {
8836
9199
  process.kill(owner, 0);
8837
9200
  } catch (error2) {
8838
9201
  if (error2.code === "ESRCH") {
8839
- fs24.unlinkSync(lock);
9202
+ fs25.unlinkSync(lock);
8840
9203
  continue;
8841
9204
  }
8842
9205
  }
@@ -8855,7 +9218,7 @@ async function runSync(store) {
8855
9218
  return await syncStore(store);
8856
9219
  } finally {
8857
9220
  try {
8858
- fs24.unlinkSync(lock);
9221
+ fs25.unlinkSync(lock);
8859
9222
  } catch {
8860
9223
  }
8861
9224
  }
@@ -8883,9 +9246,9 @@ async function syncStore(store) {
8883
9246
  result.errors.push(String(e instanceof Error ? e.message : e));
8884
9247
  }
8885
9248
  }
8886
- const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
9249
+ const acknowledgementsFile = path20.join(store.root, "synced-traces.json");
8887
9250
  const acknowledgements = readJson(acknowledgementsFile, {});
8888
- const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
9251
+ const fingerprint2 = (trace) => createHash8("sha256").update(JSON.stringify(trace)).digest("hex");
8889
9252
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8890
9253
  for (const t of traces) {
8891
9254
  try {
@@ -9138,11 +9501,11 @@ __export(demo_exports, {
9138
9501
  runDemo: () => runDemo
9139
9502
  });
9140
9503
  import assert from "node:assert/strict";
9141
- import fs25 from "node:fs";
9504
+ import fs26 from "node:fs";
9142
9505
  import os4 from "node:os";
9143
- import path20 from "node:path";
9506
+ import path21 from "node:path";
9144
9507
  async function runDemo(write) {
9145
- const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
9508
+ const root = fs26.mkdtempSync(path21.join(os4.tmpdir(), "bunshin-demo-"));
9146
9509
  try {
9147
9510
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
9148
9511
  const { lessons, constraint, workflow } = aprScenario();
@@ -9159,7 +9522,7 @@ Task: ${APR_TASK}
9159
9522
  write(result.text);
9160
9523
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
9161
9524
  } finally {
9162
- fs25.rmSync(root, { recursive: true, force: true });
9525
+ fs26.rmSync(root, { recursive: true, force: true });
9163
9526
  }
9164
9527
  }
9165
9528
  var init_demo = __esm({
@@ -9172,21 +9535,21 @@ var init_demo = __esm({
9172
9535
  });
9173
9536
 
9174
9537
  // src/v2/gitremote.ts
9175
- import fs26 from "node:fs";
9176
- import path21 from "node:path";
9177
- import { createHash as createHash8 } from "node:crypto";
9538
+ import fs27 from "node:fs";
9539
+ import path22 from "node:path";
9540
+ import { createHash as createHash9 } from "node:crypto";
9178
9541
  function saveGitCredential(apiUrl, apiKey) {
9179
9542
  const file = gitCredentialFile();
9180
- fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
9543
+ fs27.mkdirSync(path22.dirname(file), { recursive: true, mode: 448 });
9181
9544
  let all = {};
9182
9545
  try {
9183
- all = JSON.parse(fs26.readFileSync(file, "utf8"));
9546
+ all = JSON.parse(fs27.readFileSync(file, "utf8"));
9184
9547
  } catch {
9185
9548
  }
9186
9549
  all[apiUrl.replace(/\/$/, "")] = apiKey;
9187
9550
  const tmp = `${file}.${process.pid}.tmp`;
9188
- fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9189
- fs26.renameSync(tmp, file);
9551
+ fs27.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9552
+ fs27.renameSync(tmp, file);
9190
9553
  }
9191
9554
  function resolveGitKey(apiUrl) {
9192
9555
  const base = apiUrl.replace(/\/$/, "");
@@ -9194,20 +9557,20 @@ function resolveGitKey(apiUrl) {
9194
9557
  const origin = urlOrigin(base);
9195
9558
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
9196
9559
  try {
9197
- const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
9560
+ const all = JSON.parse(fs27.readFileSync(gitCredentialFile(), "utf8"));
9198
9561
  if (all[base]) return all[base];
9199
9562
  } catch {
9200
9563
  }
9201
- const dir = path21.join(homeDir(), ".config/bunshin/credentials");
9564
+ const dir2 = path22.join(homeDir(), ".config/bunshin/credentials");
9202
9565
  let files = [];
9203
9566
  try {
9204
- files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
9567
+ files = fs27.readdirSync(dir2).filter((f) => f.endsWith(".json"));
9205
9568
  } catch {
9206
9569
  return void 0;
9207
9570
  }
9208
9571
  for (const f of files.sort()) {
9209
9572
  try {
9210
- const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
9573
+ const c = JSON.parse(fs27.readFileSync(path22.join(dir2, f), "utf8"));
9211
9574
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
9212
9575
  } catch {
9213
9576
  }
@@ -9215,7 +9578,7 @@ function resolveGitKey(apiUrl) {
9215
9578
  return void 0;
9216
9579
  }
9217
9580
  function refsDigest(refs) {
9218
- return createHash8("sha256").update(
9581
+ return createHash9("sha256").update(
9219
9582
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
9220
9583
  ).digest("hex").slice(0, 12);
9221
9584
  }
@@ -9228,7 +9591,7 @@ var init_gitremote = __esm({
9228
9591
  ZERO_SHA = "0".repeat(40);
9229
9592
  REMOTE_PATH = "/v1/git/repos/";
9230
9593
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
9231
- gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
9594
+ gitCredentialFile = () => path22.join(homeDir(), ".config/bunshin/git-credentials.json");
9232
9595
  GitApiError = class extends Error {
9233
9596
  constructor(status, message, body = null) {
9234
9597
  super(message);
@@ -9320,8 +9683,8 @@ __export(gitrepo_exports, {
9320
9683
  githubReferences: () => githubReferences,
9321
9684
  repoFromUrl: () => repoFromUrl
9322
9685
  });
9323
- import fs27 from "node:fs";
9324
- import path22 from "node:path";
9686
+ import fs28 from "node:fs";
9687
+ import path23 from "node:path";
9325
9688
  import { spawnSync as spawnSync2 } from "node:child_process";
9326
9689
  function git3(cwd, args, opts = {}) {
9327
9690
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
@@ -9330,9 +9693,9 @@ function git3(cwd, args, opts = {}) {
9330
9693
  return res;
9331
9694
  }
9332
9695
  function onPath(name) {
9333
- for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
9696
+ for (const dir2 of (process.env.PATH ?? "").split(path23.delimiter)) {
9334
9697
  try {
9335
- fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
9698
+ fs28.accessSync(path23.join(dir2, name), fs28.constants.X_OK);
9336
9699
  return true;
9337
9700
  } catch {
9338
9701
  }
@@ -9383,9 +9746,9 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
9383
9746
  if (!reuseKey || !process.env.BUNSHIN_API_KEY || key !== process.env.BUNSHIN_API_KEY) saveGitCredential(apiUrl, key);
9384
9747
  return client;
9385
9748
  }
9386
- function toplevel(dir) {
9387
- const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
9388
- if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
9749
+ function toplevel(dir2) {
9750
+ const r = git3(dir2, ["rev-parse", "--show-toplevel"], { allowFail: true });
9751
+ if (!r.ok) throw new Error(`${dir2} is not inside a git repository`);
9389
9752
  return r.out.trim();
9390
9753
  }
9391
9754
  function githubReferences(root, repo) {
@@ -9416,11 +9779,11 @@ function switchOrigin(root, url, out) {
9416
9779
  }
9417
9780
  async function cmdImport(args, cwd, out, err) {
9418
9781
  requireHelper();
9419
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9782
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9420
9783
  if (git3(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
9421
9784
  const rs = remotes(root);
9422
9785
  const originUrl = rs.get("origin");
9423
- const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
9786
+ const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path23.basename(path23.dirname(root))}/${path23.basename(root)}`;
9424
9787
  const client = await connect(args, cwd);
9425
9788
  const url = remoteUrl(client.apiUrl, name);
9426
9789
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
@@ -9496,7 +9859,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
9496
9859
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
9497
9860
  `);
9498
9861
  }
9499
- if (fs27.existsSync(path22.join(root, ".github"))) out(`
9862
+ if (fs28.existsSync(path23.join(root, ".github"))) out(`
9500
9863
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
9501
9864
  `);
9502
9865
  out(`
@@ -9507,7 +9870,7 @@ New machines clone with: bunshin repo clone ${name}
9507
9870
  }
9508
9871
  async function cmdAttach(args, cwd, out, err) {
9509
9872
  requireHelper();
9510
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9873
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9511
9874
  const rs = remotes(root);
9512
9875
  const originUrl = rs.get("origin");
9513
9876
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -9552,15 +9915,15 @@ async function cmdClone(args, cwd, out) {
9552
9915
  const name = args._[1];
9553
9916
  if (!name) throw new Error("usage: bunshin repo clone owner/name [directory]");
9554
9917
  const client = await connect(args, cwd);
9555
- const dir = args._[2] ?? name.split("/")[1];
9556
- const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
9918
+ const dir2 = args._[2] ?? name.split("/")[1];
9919
+ const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir2], { cwd, stdio: "inherit" });
9557
9920
  if (r.status !== 0) return r.status ?? 1;
9558
- out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
9921
+ out(`Cloned ${name} into ${path23.resolve(cwd, dir2)}
9559
9922
  `);
9560
9923
  return 0;
9561
9924
  }
9562
9925
  async function cmdVerify(args, cwd, out, err) {
9563
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9926
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9564
9927
  const rs = remotes(root);
9565
9928
  const originUrl = rs.get("origin") ?? "";
9566
9929
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -9723,22 +10086,22 @@ __export(runner_exports, {
9723
10086
  setupText: () => setupText
9724
10087
  });
9725
10088
  import { spawnSync as spawnSync3 } from "node:child_process";
9726
- import fs28 from "node:fs";
10089
+ import fs29 from "node:fs";
9727
10090
  import os5 from "node:os";
9728
- import path23 from "node:path";
10091
+ import path24 from "node:path";
9729
10092
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9730
- return path23.join(home, ".bunshin", "runner");
10093
+ return path24.join(home, ".bunshin", "runner");
9731
10094
  }
9732
10095
  function readInstalled(home) {
9733
10096
  try {
9734
- return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
10097
+ return JSON.parse(fs29.readFileSync(path24.join(runnerHome(home), "config.json"), "utf8"));
9735
10098
  } catch {
9736
10099
  return null;
9737
10100
  }
9738
10101
  }
9739
10102
  function connection(args, cwd) {
9740
10103
  const saved = readInstalled();
9741
- const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
10104
+ const store = saved && path24.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9742
10105
  const config = store?.config();
9743
10106
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9744
10107
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9798,9 +10161,9 @@ async function runOnce(c, args, out, err) {
9798
10161
  const [job] = await lease(c, args);
9799
10162
  if (!job) break;
9800
10163
  const started = Date.now();
9801
- const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
10164
+ const scratch = fs29.mkdtempSync(path24.join(os5.tmpdir(), "bunshin-job-"));
9802
10165
  const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
9803
- () => fs28.rmSync(scratch, { recursive: true, force: true })
10166
+ () => fs29.rmSync(scratch, { recursive: true, force: true })
9804
10167
  );
9805
10168
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9806
10169
  if (!res.ok) {
@@ -9890,24 +10253,24 @@ function renderCheck(id, c) {
9890
10253
  }
9891
10254
  function logAnswer(entry) {
9892
10255
  try {
9893
- fs28.mkdirSync(runnerHome(), { recursive: true });
9894
- fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
10256
+ fs29.mkdirSync(runnerHome(), { recursive: true });
10257
+ fs29.appendFileSync(path24.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9895
10258
  `);
9896
10259
  } catch {
9897
10260
  }
9898
10261
  }
9899
10262
  function which(name) {
9900
- for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
9901
- if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
10263
+ for (const d of [...(process.env.PATH || "").split(path24.delimiter), path24.join(os5.homedir(), ".local", "bin")]) {
10264
+ if (d && fs29.existsSync(path24.join(d, name))) return path24.join(d, name);
9902
10265
  }
9903
10266
  return null;
9904
10267
  }
9905
10268
  function scheduledCommand(backend) {
9906
- const node = path23.dirname(process.execPath);
9907
- const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
10269
+ const node = path24.dirname(process.execPath);
10270
+ const npx = fs29.existsSync(path24.join(node, "npx")) ? path24.join(node, "npx") : which("npx") ?? "npx";
9908
10271
  const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
9909
- const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path23.dirname(bin)] : [], path23.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
9910
- return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
10272
+ const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path24.dirname(bin)] : [], path24.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
10273
+ return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path24.delimiter) };
9911
10274
  }
9912
10275
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9913
10276
  const cmd = scheduledCommand(backend);
@@ -9922,8 +10285,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9922
10285
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9923
10286
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9924
10287
  <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
9925
- <key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9926
- <key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
10288
+ <key>StandardOutPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
10289
+ <key>StandardErrorPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
9927
10290
  <key>ProcessType</key><string>Background</string>
9928
10291
  <key>LowPriorityIO</key><true/>
9929
10292
  </dict>
@@ -9934,19 +10297,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9934
10297
  const cmd = scheduledCommand(backend);
9935
10298
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
9936
10299
  const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
9937
- return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
10300
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path24.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
9938
10301
  }
9939
10302
  function launchAgentsDir(opts) {
9940
- return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
10303
+ return opts.launchAgents ?? path24.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
9941
10304
  }
9942
10305
  function installSchedule(backend, everyMinutes, opts = {}) {
9943
10306
  const platform = opts.platform ?? process.platform;
9944
10307
  const run2 = opts.run ?? realRun;
9945
10308
  if (platform === "darwin") {
9946
- const dir = launchAgentsDir(opts);
9947
- const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
9948
- fs28.mkdirSync(dir, { recursive: true });
9949
- fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
10309
+ const dir2 = launchAgentsDir(opts);
10310
+ const plist = path24.join(dir2, `${LAUNCHD_LABEL}.plist`);
10311
+ fs29.mkdirSync(dir2, { recursive: true });
10312
+ fs29.writeFileSync(plist, launchdPlist(backend, everyMinutes));
9950
10313
  const domain = `gui/${process.getuid?.() ?? 501}`;
9951
10314
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
9952
10315
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -9966,10 +10329,10 @@ function removeSchedule(opts = {}) {
9966
10329
  const platform = opts.platform ?? process.platform;
9967
10330
  const run2 = opts.run ?? realRun;
9968
10331
  if (platform === "darwin") {
9969
- const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
10332
+ const plist = path24.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9970
10333
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
9971
- if (!fs28.existsSync(plist)) return "no schedule";
9972
- fs28.rmSync(plist, { force: true });
10334
+ if (!fs29.existsSync(plist)) return "no schedule";
10335
+ fs29.rmSync(plist, { force: true });
9973
10336
  return `removed ${plist}`;
9974
10337
  }
9975
10338
  const current = run2("crontab", ["-l"]);
@@ -9996,10 +10359,10 @@ async function install(args, cwd, out, err, opts = {}) {
9996
10359
  }
9997
10360
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
9998
10361
  const home = runnerHome();
9999
- fs28.mkdirSync(home, { recursive: true, mode: 448 });
10362
+ fs29.mkdirSync(home, { recursive: true, mode: 448 });
10000
10363
  const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
10001
- fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
10002
- fs28.chmodSync(path23.join(home, "config.json"), 384);
10364
+ fs29.writeFileSync(path24.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
10365
+ fs29.chmodSync(path24.join(home, "config.json"), 384);
10003
10366
  let schedule;
10004
10367
  try {
10005
10368
  schedule = installSchedule(backend, every, opts);
@@ -10012,7 +10375,7 @@ Run it yourself: npx -y ${PKG2} runner --once
10012
10375
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
10013
10376
  `);
10014
10377
  out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
10015
- ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
10378
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path24.join(home, "runner.log")}
10016
10379
  `);
10017
10380
  out(` Stop: npx -y ${PKG2} runner uninstall
10018
10381
  `);
@@ -10029,9 +10392,9 @@ Answering what waits now\u2026
10029
10392
  function uninstall(out, opts = {}) {
10030
10393
  out(`${removeSchedule(opts)}
10031
10394
  `);
10032
- const file = path23.join(runnerHome(), "config.json");
10033
- if (fs28.existsSync(file)) {
10034
- fs28.rmSync(file, { force: true });
10395
+ const file = path24.join(runnerHome(), "config.json");
10396
+ if (fs29.existsSync(file)) {
10397
+ fs29.rmSync(file, { force: true });
10035
10398
  out(`removed ${file} (revoke the key in the console: Agent keys)
10036
10399
  `);
10037
10400
  }
@@ -10211,7 +10574,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10211
10574
  out(text2);
10212
10575
  return 0;
10213
10576
  }
10214
- fs28.writeFileSync(file, text2, { mode: 384 });
10577
+ fs29.writeFileSync(file, text2, { mode: 384 });
10215
10578
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
10216
10579
  out(
10217
10580
  [
@@ -10229,7 +10592,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10229
10592
  const id = args._[2];
10230
10593
  const file = args._[3] ?? "-";
10231
10594
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
10232
- const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
10595
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10233
10596
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
10234
10597
  out(renderCheck(id, r.body));
10235
10598
  return r.body.ok ? 0 : 1;
@@ -10238,7 +10601,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10238
10601
  const id = args._[2];
10239
10602
  const file = args._[3] ?? "-";
10240
10603
  if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
10241
- const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
10604
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10242
10605
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
10243
10606
  if (check?.body && (check.status < 400 || check.body.problems)) {
10244
10607
  out(renderCheck(id, check.body));
@@ -10262,7 +10625,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10262
10625
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
10263
10626
  const entries = (() => {
10264
10627
  try {
10265
- return fs28.readFileSync(path23.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
10628
+ return fs29.readFileSync(path24.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
10266
10629
  } catch {
10267
10630
  return [];
10268
10631
  }
@@ -10332,8 +10695,8 @@ var init_runner = __esm({
10332
10695
 
10333
10696
  // src/v2/importer.ts
10334
10697
  import { execFileSync as execFileSync6 } from "node:child_process";
10335
- import fs29 from "node:fs";
10336
- import path24 from "node:path";
10698
+ import fs30 from "node:fs";
10699
+ import path25 from "node:path";
10337
10700
  function rewriteRepoPaths(text2, repoName2, pack) {
10338
10701
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
10339
10702
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -10368,17 +10731,17 @@ function buildDocs(files, opts) {
10368
10731
  const claimed = /* @__PURE__ */ new Set();
10369
10732
  const all = [...files.keys()].sort();
10370
10733
  const skillDirs = all.map(skillDirOf).filter((d) => !!d);
10371
- for (const dir of skillDirs) {
10372
- const main2 = `${dir}/SKILL.md`;
10734
+ for (const dir2 of skillDirs) {
10735
+ const main2 = `${dir2}/SKILL.md`;
10373
10736
  const text2 = rw(files.get(main2).content);
10374
10737
  const fm = frontmatterDescription(text2);
10375
- const name = path24.posix.basename(dir);
10738
+ const name = path25.posix.basename(dir2);
10376
10739
  const bundle = {};
10377
- for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
10740
+ for (const p of all.filter((x) => x.startsWith(`${dir2}/`))) {
10378
10741
  claimed.add(p);
10379
10742
  if (p === main2) continue;
10380
10743
  const f = files.get(p);
10381
- bundle[p.slice(dir.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10744
+ bundle[p.slice(dir2.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10382
10745
  }
10383
10746
  const description = fm.description.replace(/\s+/g, " ").trim() || firstSentence(text2) || name;
10384
10747
  const { name: _n, description: _d, ...rest } = fm.data;
@@ -10405,9 +10768,9 @@ function buildDocs(files, opts) {
10405
10768
  if (/\.md$/i.test(p) && looksGenerated(files.get(p).content)) claimed.add(p);
10406
10769
  }
10407
10770
  const dirs = new Set(all.filter((x) => x.includes("/") && !claimed.has(x)).map((x) => x.split("/")[0]));
10408
- for (const dir of [...dirs].sort()) {
10409
- if (skillParents.has(dir) && all.filter((x) => x.startsWith(`${dir}/`) && !claimed.has(x)).length === 0) continue;
10410
- const members = all.filter((x) => x.startsWith(`${dir}/`) && !claimed.has(x));
10771
+ for (const dir2 of [...dirs].sort()) {
10772
+ if (skillParents.has(dir2) && all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x)).length === 0) continue;
10773
+ const members = all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x));
10411
10774
  if (!members.length) continue;
10412
10775
  const readme = members.find((x) => /^[^/]+\/README(\.md)?$/i.test(x));
10413
10776
  const bundle = {};
@@ -10418,15 +10781,15 @@ function buildDocs(files, opts) {
10418
10781
  bundle[p] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10419
10782
  }
10420
10783
  const tool = members.some((p) => files.get(p).mode === "x" || SCRIPT.test(p));
10421
- const body = readme ? rw(files.get(readme).content) : `# ${dir}
10784
+ const body = readme ? rw(files.get(readme).content) : `# ${dir2}
10422
10785
 
10423
10786
  ${members.map((p) => `- \`${p}\``).join("\n")}
10424
10787
  `;
10425
10788
  docs.push({
10426
- path: readme ?? `${dir}/#index`,
10789
+ path: readme ?? `${dir2}/#index`,
10427
10790
  kind: tool ? "tool" : "reference",
10428
- title: dir,
10429
- when: readme ? firstSentence(body) || `The ${dir} ${tool ? "tool" : "folder"}` : `The ${dir} ${tool ? "tool" : "folder"} of ${opts.pack}`,
10791
+ title: dir2,
10792
+ when: readme ? firstSentence(body) || `The ${dir2} ${tool ? "tool" : "folder"}` : `The ${dir2} ${tool ? "tool" : "folder"} of ${opts.pack}`,
10430
10793
  body,
10431
10794
  files: bundle
10432
10795
  });
@@ -10469,23 +10832,23 @@ function isGitRepo(root) {
10469
10832
  }
10470
10833
  function workingTreeFiles(root) {
10471
10834
  const out = /* @__PURE__ */ new Map();
10472
- const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
10473
- return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
10835
+ const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir2, base = "") {
10836
+ return fs30.readdirSync(dir2, { withFileTypes: true }).flatMap((e) => {
10474
10837
  if (e.name === ".git" || e.name === "node_modules") return [];
10475
10838
  const rel = base ? `${base}/${e.name}` : e.name;
10476
- return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
10839
+ return e.isDirectory() ? walk(path25.join(dir2, e.name), rel) : e.isFile() ? [rel] : [];
10477
10840
  });
10478
10841
  })(root);
10479
10842
  for (const rel of list2) {
10480
- const abs = path24.join(root, rel);
10843
+ const abs = path25.join(root, rel);
10481
10844
  let buf;
10482
10845
  try {
10483
- buf = fs29.readFileSync(abs);
10846
+ buf = fs30.readFileSync(abs);
10484
10847
  } catch {
10485
10848
  continue;
10486
10849
  }
10487
10850
  if (!isText(buf)) continue;
10488
- const exec = (fs29.statSync(abs).mode & 73) !== 0;
10851
+ const exec = (fs30.statSync(abs).mode & 73) !== 0;
10489
10852
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
10490
10853
  }
10491
10854
  return out;
@@ -10519,7 +10882,7 @@ function diffDocs(prev, next) {
10519
10882
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
10520
10883
  }
10521
10884
  function readSkillSystem(root, opts) {
10522
- const repoName2 = path24.basename(path24.resolve(root));
10885
+ const repoName2 = path25.basename(path25.resolve(root));
10523
10886
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
10524
10887
  const tree = build(workingTreeFiles(root));
10525
10888
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -10596,7 +10959,7 @@ var init_importer = __esm({
10596
10959
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10597
10960
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
10598
10961
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
10599
- isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10962
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path25.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10600
10963
  fingerprint = (d) => JSON.stringify(d);
10601
10964
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
10602
10965
 
@@ -10632,8 +10995,8 @@ __export(skills_exports, {
10632
10995
  cmdSkill: () => cmdSkill,
10633
10996
  parsePin: () => parsePin
10634
10997
  });
10635
- import fs30 from "node:fs";
10636
- import path25 from "node:path";
10998
+ import fs31 from "node:fs";
10999
+ import path26 from "node:path";
10637
11000
  function requireOnline(store, what) {
10638
11001
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
10639
11002
  }
@@ -10642,9 +11005,9 @@ async function cmdImport2(store, args, cwd, out, err) {
10642
11005
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
10643
11006
  return 1;
10644
11007
  }
10645
- const root = path25.resolve(cwd, args._[2]);
10646
- if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
10647
- const pack = str6(args.flags.pack) ?? path25.basename(root);
11008
+ const root = path26.resolve(cwd, args._[2]);
11009
+ if (!fs31.existsSync(root)) throw new Error(`${root} does not exist`);
11010
+ const pack = str6(args.flags.pack) ?? path26.basename(root);
10648
11011
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
10649
11012
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
10650
11013
  const kinds = /* @__PURE__ */ new Map();
@@ -10679,7 +11042,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10679
11042
  const propose = args.flags["propose-rewrites"];
10680
11043
  if (propose) {
10681
11044
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
10682
- await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
11045
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path26.basename(root), out, err);
10683
11046
  }
10684
11047
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10685
11048
  return 0;
@@ -10819,7 +11182,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10819
11182
  }
10820
11183
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10821
11184
  if (graph) {
10822
- fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
11185
+ fs31.writeFileSync(path26.resolve(cwd, graph), renderSkillGraph(report));
10823
11186
  out(`graph written to ${graph}
10824
11187
  `);
10825
11188
  }
@@ -10909,9 +11272,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10909
11272
  `;
10910
11273
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10911
11274
  if (file) {
10912
- const abs = path25.resolve(cwd, file);
10913
- fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10914
- fs30.writeFileSync(abs, line);
11275
+ const abs = path26.resolve(cwd, file);
11276
+ fs31.mkdirSync(path26.dirname(abs), { recursive: true });
11277
+ fs31.writeFileSync(abs, line);
10915
11278
  out(`${file}: ${line}`);
10916
11279
  } else out(line);
10917
11280
  return 0;
@@ -10921,16 +11284,16 @@ async function cmdLibrary(store, args, cwd, out, err) {
10921
11284
  const to = str6(args.flags.to);
10922
11285
  if (!to) throw new Error("--to <dir> is required");
10923
11286
  const pinFile = str6(args.flags.pin);
10924
- const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
11287
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs31.readFileSync(path26.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10925
11288
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10926
11289
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10927
11290
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
10928
- const dir = path25.resolve(cwd, to);
10929
- if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
11291
+ const dir2 = path26.resolve(cwd, to);
11292
+ if (fs31.existsSync(dir2) && fs31.readdirSync(dir2).length && !bool4(args.flags.force)) throw new Error(`${dir2} is not empty (use --force)`);
10930
11293
  const pack = str6(args.flags.pack);
10931
11294
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
10932
- const res = materialize(docs, dir);
10933
- out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir}
11295
+ const res = materialize(docs, dir2);
11296
+ out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir2}
10934
11297
  `);
10935
11298
  return 0;
10936
11299
  }
@@ -10965,27 +11328,27 @@ __export(reflect_exports, {
10965
11328
  buildLesson: () => buildLesson,
10966
11329
  runReflection: () => runReflection
10967
11330
  });
10968
- import fs31 from "node:fs";
10969
- import path26 from "node:path";
10970
- import { createHash as createHash9 } from "node:crypto";
11331
+ import fs32 from "node:fs";
11332
+ import path27 from "node:path";
11333
+ import { createHash as createHash10 } from "node:crypto";
10971
11334
  async function runReflection(store, opts) {
10972
11335
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
10973
- const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
11336
+ const lock = path27.join(store.root, `reflect-${createHash10("sha256").update(opts.sessionId).digest("hex")}.lock`);
10974
11337
  const deadline = Date.now() + 66e4;
10975
11338
  for (; ; ) {
10976
11339
  try {
10977
- fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
11340
+ fs32.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10978
11341
  break;
10979
11342
  } catch (error) {
10980
11343
  if (error.code !== "EEXIST") throw error;
10981
11344
  try {
10982
- const owner = Number(fs31.readFileSync(lock, "utf8"));
11345
+ const owner = Number(fs32.readFileSync(lock, "utf8"));
10983
11346
  if (Number.isInteger(owner) && owner > 0) {
10984
11347
  try {
10985
11348
  process.kill(owner, 0);
10986
11349
  } catch (error2) {
10987
11350
  if (error2.code === "ESRCH") {
10988
- fs31.unlinkSync(lock);
11351
+ fs32.unlinkSync(lock);
10989
11352
  continue;
10990
11353
  }
10991
11354
  }
@@ -11000,7 +11363,7 @@ async function runReflection(store, opts) {
11000
11363
  return await reflectSession(store, opts);
11001
11364
  } finally {
11002
11365
  try {
11003
- fs31.unlinkSync(lock);
11366
+ fs32.unlinkSync(lock);
11004
11367
  } catch {
11005
11368
  }
11006
11369
  }
@@ -11342,8 +11705,8 @@ __export(cli_exports, {
11342
11705
  parseArgs: () => parseArgs,
11343
11706
  version: () => version
11344
11707
  });
11345
- import fs32 from "node:fs";
11346
- import path27 from "node:path";
11708
+ import fs33 from "node:fs";
11709
+ import path28 from "node:path";
11347
11710
  function parseArgs(argv) {
11348
11711
  const out = { _: [], flags: {} };
11349
11712
  const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace", "across"]);
@@ -11443,7 +11806,7 @@ async function main(argv, opts = {}) {
11443
11806
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
11444
11807
  const agent = harness === "claude-code" ? "claude" : harness;
11445
11808
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
11446
- const prior = new Store(path27.join(cwd, ".bunshin"));
11809
+ const prior = new Store(path28.join(cwd, ".bunshin"));
11447
11810
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
11448
11811
  const apiUrl = str7(args.flags["api-url"]);
11449
11812
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -11472,7 +11835,7 @@ async function main(argv, opts = {}) {
11472
11835
  });
11473
11836
  if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
11474
11837
  const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
11475
- await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
11838
+ await askSources2(new Store(path28.join(cwd, ".bunshin")), out);
11476
11839
  }
11477
11840
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
11478
11841
  `);
@@ -11509,7 +11872,7 @@ Key saved privately outside the project.
11509
11872
  if (cmd === "mcp") {
11510
11873
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
11511
11874
  const root = str7(args.flags.root);
11512
- await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
11875
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path28.resolve(root)) } : {}, version: version() });
11513
11876
  return 0;
11514
11877
  }
11515
11878
  if (cmd === "demo") {
@@ -11572,6 +11935,14 @@ Key saved privately outside the project.
11572
11935
  case "meta-reflect":
11573
11936
  case "meta":
11574
11937
  return await cmdMetaReflect(store, args, cwd, out);
11938
+ case "route": {
11939
+ const { routeJob: routeJob2, sweepRouter: sweepRouter2 } = await Promise.resolve().then(() => (init_route(), route_exports));
11940
+ const answer = await routeJob2(store, str7(args.flags.session) ?? "", Number(args.flags.seq));
11941
+ sweepRouter2(new (await Promise.resolve().then(() => (init_store2(), store_exports))).V2Store(store));
11942
+ if (!bool5(args.flags.quiet)) out(`${JSON.stringify(answer)}
11943
+ `);
11944
+ return answer && !answer.error ? 0 : 1;
11945
+ }
11575
11946
  case "update-check": {
11576
11947
  const latest = await (await Promise.resolve().then(() => (init_update(), update_exports))).checkForUpdate();
11577
11948
  if (!bool5(args.flags.quiet)) out(latest ? `latest ${latest} (this is ${version()})
@@ -11613,7 +11984,7 @@ ${HELP2}`);
11613
11984
  function openStore(args, cwd) {
11614
11985
  const root = str7(args.flags.root);
11615
11986
  if (root) {
11616
- const store = new Store(path27.resolve(root));
11987
+ const store = new Store(path28.resolve(root));
11617
11988
  store.ensureLayout();
11618
11989
  return store;
11619
11990
  }
@@ -11629,7 +12000,7 @@ async function cmdInit(args, cwd, out, err) {
11629
12000
  orgId: str7(args.flags["org-id"]),
11630
12001
  userId: str7(args.flags["user-id"]),
11631
12002
  apiUrl: str7(args.flags["api-url"]),
11632
- apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
12003
+ apiKey: bool5(args.flags["key-stdin"]) ? fs33.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
11633
12004
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
11634
12005
  harness,
11635
12006
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -11672,10 +12043,10 @@ function cmdStatus(store, args, out, v2) {
11672
12043
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
11673
12044
  const metrics = store.readMetrics(void 0);
11674
12045
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
11675
- const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path27.join(process.env.CODEX_HOME ?? path27.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path27.dirname(store.root)).file])] : config.harness === "claude-code" ? [path27.join(homeDir(), ".claude", "settings.json"), path27.join(path27.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path27.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path27.join(path27.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
12046
+ const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path28.join(process.env.CODEX_HOME ?? path28.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path28.dirname(store.root)).file])] : config.harness === "claude-code" ? [path28.join(homeDir(), ".claude", "settings.json"), path28.join(path28.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path28.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path28.join(path28.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
11676
12047
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
11677
12048
  try {
11678
- return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
12049
+ return fs33.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11679
12050
  } catch {
11680
12051
  return false;
11681
12052
  }
@@ -11693,7 +12064,7 @@ function cmdStatus(store, args, out, v2) {
11693
12064
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
11694
12065
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
11695
12066
  hooks: { [config.harness]: hooks },
11696
- hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
12067
+ hookFiles: hookFiles.filter((file) => fs33.existsSync(file)),
11697
12068
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
11698
12069
  v2
11699
12070
  };
@@ -11736,7 +12107,7 @@ function cmdStatus(store, args, out, v2) {
11736
12107
  }
11737
12108
  function hookCount(file) {
11738
12109
  try {
11739
- const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
12110
+ const raw = JSON.parse(fs33.readFileSync(file, "utf8"));
11740
12111
  let n = 0;
11741
12112
  for (const groups of Object.values(raw.hooks ?? {})) {
11742
12113
  for (const g of groups ?? []) {
@@ -11856,7 +12227,7 @@ function cmdLessonAdd(store, args, out, err) {
11856
12227
  let content = str7(args.flags.content) ?? "";
11857
12228
  if (!content || content === "-" || args._.includes("-")) {
11858
12229
  try {
11859
- content = fs32.readFileSync(0, "utf8");
12230
+ content = fs33.readFileSync(0, "utf8");
11860
12231
  } catch {
11861
12232
  }
11862
12233
  }
@@ -11922,7 +12293,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11922
12293
  }
11923
12294
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11924
12295
  const result = await runReflection2(store, {
11925
- sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
12296
+ sessionId: str7(args.flags.session) ?? path28.basename(transcript).replace(/\.jsonl?$/, ""),
11926
12297
  transcriptPath: transcript,
11927
12298
  cwd,
11928
12299
  force: bool5(args.flags.force)
@@ -12125,7 +12496,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
12125
12496
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
12126
12497
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
12127
12498
  `;
12128
- untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path27.dirname(root)} to trust it.`;
12499
+ untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path28.dirname(root)} to trust it.`;
12129
12500
  }
12130
12501
  });
12131
12502