@bunshin-cc/bunshin 0.9.0 → 0.10.1

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 +710 -403
  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;
@@ -3161,6 +3208,7 @@ var init_v2 = __esm({
3161
3208
  init_python();
3162
3209
  init_text_index();
3163
3210
  init_engine();
3211
+ init_router();
3164
3212
  init_reflect();
3165
3213
  init_digest();
3166
3214
  init_reflect_context();
@@ -3325,17 +3373,17 @@ function listDocs(root) {
3325
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();
3326
3374
  const out = [];
3327
3375
  let visited = 0;
3328
- const walk = (dir, rel, depth) => {
3376
+ const walk = (dir2, rel, depth) => {
3329
3377
  if (depth > 8 || visited++ > 2e4) return;
3330
3378
  let entries;
3331
3379
  try {
3332
- entries = fs6.readdirSync(dir, { withFileTypes: true });
3380
+ entries = fs6.readdirSync(dir2, { withFileTypes: true });
3333
3381
  } catch {
3334
3382
  return;
3335
3383
  }
3336
3384
  for (const e of entries) {
3337
3385
  if (e.isDirectory()) {
3338
- 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);
3339
3387
  } else if (e.isFile()) {
3340
3388
  const r = rel ? `${rel}/${e.name}` : e.name;
3341
3389
  if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
@@ -4074,6 +4122,14 @@ var init_src = __esm({
4074
4122
  });
4075
4123
 
4076
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
+ });
4077
4133
  import fs8 from "node:fs";
4078
4134
  import path7 from "node:path";
4079
4135
  import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
@@ -4192,12 +4248,12 @@ var init_store2 = __esm({
4192
4248
  return s;
4193
4249
  }
4194
4250
  listSessions() {
4195
- const dir = this.p("sessions");
4196
- if (!fileExists(dir)) return [];
4251
+ const dir2 = this.p("sessions");
4252
+ if (!fileExists(dir2)) return [];
4197
4253
  const out = [];
4198
- for (const f of fs8.readdirSync(dir)) {
4254
+ for (const f of fs8.readdirSync(dir2)) {
4199
4255
  if (!f.endsWith(".json") || f === "current.json") continue;
4200
- const raw = readJson(path7.join(dir, f), null);
4256
+ const raw = readJson(path7.join(dir2, f), null);
4201
4257
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
4202
4258
  }
4203
4259
  return out;
@@ -4295,12 +4351,12 @@ var init_store2 = __esm({
4295
4351
  }
4296
4352
  }
4297
4353
  listDir(name) {
4298
- const dir = this.p(name);
4299
- if (!fileExists(dir)) return [];
4354
+ const dir2 = this.p(name);
4355
+ if (!fileExists(dir2)) return [];
4300
4356
  const out = [];
4301
- for (const f of fs8.readdirSync(dir)) {
4357
+ for (const f of fs8.readdirSync(dir2)) {
4302
4358
  if (!f.endsWith(".json")) continue;
4303
- const v = readJson(path7.join(dir, f), null);
4359
+ const v = readJson(path7.join(dir2, f), null);
4304
4360
  if (v) out.push(v);
4305
4361
  }
4306
4362
  return out;
@@ -4374,8 +4430,8 @@ function removeFile(file) {
4374
4430
  } catch {
4375
4431
  }
4376
4432
  }
4377
- function pruneDirs(dir, stop) {
4378
- let d = dir;
4433
+ function pruneDirs(dir2, stop) {
4434
+ let d = dir2;
4379
4435
  while (d.startsWith(stop) && d !== stop) {
4380
4436
  try {
4381
4437
  if (fs9.readdirSync(d).length) return;
@@ -4386,16 +4442,16 @@ function pruneDirs(dir, stop) {
4386
4442
  d = path8.dirname(d);
4387
4443
  }
4388
4444
  }
4389
- function listFiles(dir, base = dir) {
4445
+ function listFiles(dir2, base = dir2) {
4390
4446
  let entries;
4391
4447
  try {
4392
- entries = fs9.readdirSync(dir, { withFileTypes: true });
4448
+ entries = fs9.readdirSync(dir2, { withFileTypes: true });
4393
4449
  } catch {
4394
4450
  return [];
4395
4451
  }
4396
4452
  const out = [];
4397
4453
  for (const e of entries) {
4398
- const abs = path8.join(dir, e.name);
4454
+ const abs = path8.join(dir2, e.name);
4399
4455
  if (e.isDirectory()) out.push(...listFiles(abs, base));
4400
4456
  else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
4401
4457
  }
@@ -4403,8 +4459,8 @@ function listFiles(dir, base = dir) {
4403
4459
  }
4404
4460
  function childDir(root, name) {
4405
4461
  if (!name || !SAFE_NAME.test(name)) return null;
4406
- const dir = path8.join(root, name);
4407
- 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;
4408
4464
  }
4409
4465
  function layerRank(layer, tree) {
4410
4466
  const kind = layerKind(layer);
@@ -4433,17 +4489,17 @@ function planDeploy(bundle, p, harnesses) {
4433
4489
  const contents = /* @__PURE__ */ new Map();
4434
4490
  const dirs = [];
4435
4491
  for (const root of roots) {
4436
- const dir = childDir(root, name);
4437
- const body = dir && safeJoin(dir, "SKILL.md");
4438
- if (!dir || !body) {
4492
+ const dir2 = childDir(root, name);
4493
+ const body = dir2 && safeJoin(dir2, "SKILL.md");
4494
+ if (!dir2 || !body) {
4439
4495
  warnings.push(`skipped skill ${name} in ${root}: its folder leads outside ${root}`);
4440
4496
  continue;
4441
4497
  }
4442
- dirs.push(dir);
4498
+ dirs.push(dir2);
4443
4499
  targets.push({ abs: body, rel: "SKILL.md", role: "body" });
4444
4500
  contents.set(body, s.body);
4445
4501
  for (const [rel, f] of Object.entries(s.skill.files ?? {})) {
4446
- const abs = safeJoin(dir, rel);
4502
+ const abs = safeJoin(dir2, rel);
4447
4503
  if (!abs || rel === "SKILL.md" || rel === MANIFEST) continue;
4448
4504
  targets.push({ abs, rel, role: "file", ...f.mode ? { mode: f.mode } : {} });
4449
4505
  contents.set(abs, f.content);
@@ -4583,10 +4639,10 @@ function captureDoc(c, doc, out) {
4583
4639
  if (chosen) byRel.set(key, chosen);
4584
4640
  }
4585
4641
  if (doc.kind === "skill") {
4586
- for (const dir of doc.dirs ?? []) {
4587
- for (const rel of listFiles(dir)) {
4642
+ for (const dir2 of doc.dirs ?? []) {
4643
+ for (const rel of listFiles(dir2)) {
4588
4644
  if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
4589
- const abs = path8.join(dir, rel);
4645
+ const abs = path8.join(dir2, rel);
4590
4646
  const content = readText(abs);
4591
4647
  if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
4592
4648
  byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
@@ -4630,23 +4686,23 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4630
4686
  continue;
4631
4687
  }
4632
4688
  for (const name of names) {
4633
- const dir = path8.join(root, name);
4634
- if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
4635
- 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"));
4636
4692
  if (skillMd === null) continue;
4637
4693
  let born = 0;
4638
4694
  try {
4639
- const st = fs9.statSync(path8.join(dir, "SKILL.md"));
4695
+ const st = fs9.statSync(path8.join(dir2, "SKILL.md"));
4640
4696
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4641
4697
  } catch {
4642
4698
  continue;
4643
4699
  }
4644
4700
  if (!(born >= since - BORN_SLACK_MS) && !only) continue;
4645
4701
  const files = {};
4646
- 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(".")))) {
4647
4703
  if (rel === "SKILL.md" || rel === MANIFEST) continue;
4648
- const content = readText(path8.join(dir, rel));
4649
- 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" } : {} };
4650
4706
  }
4651
4707
  const hash = sha(JSON.stringify([skillMd, files]));
4652
4708
  if (state.created[name]?.hash === hash) continue;
@@ -4767,11 +4823,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4767
4823
  const prevByAbs = new Map((prev?.targets ?? []).map((t) => [`${t.abs}#${t.role}`, t]));
4768
4824
  const foreign = /* @__PURE__ */ new Set();
4769
4825
  if (d.kind === "skill" && !prev) {
4770
- for (const dir of d.dirs ?? []) {
4771
- const cur = readText(path8.join(dir, "SKILL.md"));
4826
+ for (const dir2 of d.dirs ?? []) {
4827
+ const cur = readText(path8.join(dir2, "SKILL.md"));
4772
4828
  if (cur === null || cur === d.section.body || state.created[d.name]) continue;
4773
- foreign.add(dir);
4774
- 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`);
4775
4831
  }
4776
4832
  delete state.created[d.name];
4777
4833
  }
@@ -4780,7 +4836,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4780
4836
  targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
4781
4837
  continue;
4782
4838
  }
4783
- if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
4839
+ if ([...foreign].some((dir2) => t.abs.startsWith(dir2 + path8.sep))) continue;
4784
4840
  if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
4785
4841
  report.warnings.push(`${t.abs} not written: it leads outside its folder`);
4786
4842
  continue;
@@ -4810,20 +4866,20 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4810
4866
  }
4811
4867
  }
4812
4868
  if (d.kind === "skill") {
4813
- for (const dir of prev?.dirs ?? []) {
4814
- if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4815
- removeFile(path8.join(dir, MANIFEST));
4816
- pruneDirs(dir, path8.dirname(dir));
4817
- }
4818
- for (const dir of d.dirs ?? []) {
4819
- const manifest = safeJoin(dir, MANIFEST);
4820
- 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;
4821
4877
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4822
4878
  }
4823
4879
  report.skills++;
4824
4880
  } else if (d.kind === "pack") report.packFiles += targets.length;
4825
4881
  if (d.kind === "instructions") report.instructions++;
4826
- const dirs = d.dirs?.filter((dir) => !foreign.has(dir));
4882
+ const dirs = d.dirs?.filter((dir2) => !foreign.has(dir2));
4827
4883
  next[id] = { sectionId: id, kind: d.kind, name: d.name, version: d.section.version, targets, ...dirs ? { dirs } : {} };
4828
4884
  }
4829
4885
  for (const [id, doc] of Object.entries(state.docs)) {
@@ -4840,14 +4896,14 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4840
4896
  report.removed++;
4841
4897
  }
4842
4898
  }
4843
- for (const dir of doc.dirs ?? []) {
4844
- if (!removable(dir)) continue;
4845
- removeFile(path8.join(dir, MANIFEST));
4846
- pruneDirs(dir, path8.dirname(dir));
4847
- if (fs9.existsSync(dir)) {
4848
- const left = listFiles(dir);
4849
- if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4850
- 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));
4851
4907
  }
4852
4908
  }
4853
4909
  }
@@ -4883,9 +4939,9 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4883
4939
  const packs = new Set(plan.skills.concat([...plan.docs.values()].map((d) => d.section)).map((s) => s.pack).filter((x) => !!x));
4884
4940
  if (harnesses.has("claude-code")) {
4885
4941
  for (const pack of packs) {
4886
- const dir = childDir(p.library, pack);
4887
- if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
4888
- 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");
4889
4945
  try {
4890
4946
  fs9.lstatSync(link);
4891
4947
  if (fs9.readlinkSync(link) !== p.claudeSkills) {
@@ -4940,10 +4996,10 @@ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
4940
4996
  const s = readState(paths, config.orgId);
4941
4997
  return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
4942
4998
  }
4943
- function materialize(sections, dir) {
4999
+ function materialize(sections, dir2) {
4944
5000
  let files = 0;
4945
5001
  const put = (rel, content, mode) => {
4946
- const abs = safeJoin(dir, rel);
5002
+ const abs = safeJoin(dir2, rel);
4947
5003
  if (!abs) return;
4948
5004
  ensureDir(path8.dirname(abs));
4949
5005
  writeFile(abs, content, mode);
@@ -4970,20 +5026,20 @@ function linkSkills(p, root, harnesses, names) {
4970
5026
  for (const h of harnesses) {
4971
5027
  const spec = LINK_DIRS[h];
4972
5028
  if (!spec) continue;
4973
- const dir = path8.join(abs, spec.rel);
5029
+ const dir2 = path8.join(abs, spec.rel);
4974
5030
  const canonical = path8.resolve(p[spec.key]);
4975
5031
  const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
4976
5032
  let entries = [];
4977
5033
  try {
4978
- entries = fs9.readdirSync(dir, { withFileTypes: true });
5034
+ entries = fs9.readdirSync(dir2, { withFileTypes: true });
4979
5035
  } catch {
4980
5036
  }
4981
5037
  for (const e of entries) {
4982
5038
  if (!e.isSymbolicLink()) continue;
4983
- const link = path8.join(dir, e.name);
5039
+ const link = path8.join(dir2, e.name);
4984
5040
  let target = "";
4985
5041
  try {
4986
- target = path8.resolve(dir, fs9.readlinkSync(link));
5042
+ target = path8.resolve(dir2, fs9.readlinkSync(link));
4987
5043
  } catch {
4988
5044
  continue;
4989
5045
  }
@@ -4997,7 +5053,7 @@ function linkSkills(p, root, harnesses, names) {
4997
5053
  out.removed++;
4998
5054
  }
4999
5055
  for (const name of want) {
5000
- const link = path8.join(dir, name);
5056
+ const link = path8.join(dir2, name);
5001
5057
  try {
5002
5058
  fs9.lstatSync(link);
5003
5059
  out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
@@ -5005,7 +5061,7 @@ function linkSkills(p, root, harnesses, names) {
5005
5061
  } catch {
5006
5062
  }
5007
5063
  try {
5008
- ensureDir(dir);
5064
+ ensureDir(dir2);
5009
5065
  fs9.symlinkSync(path8.join(canonical, name), link, "dir");
5010
5066
  excluded.push(path8.join(spec.rel, name));
5011
5067
  out.linked++;
@@ -5013,7 +5069,7 @@ function linkSkills(p, root, harnesses, names) {
5013
5069
  out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
5014
5070
  }
5015
5071
  }
5016
- pruneDirs(dir, abs);
5072
+ pruneDirs(dir2, abs);
5017
5073
  }
5018
5074
  out.kept = excluded.length > 0;
5019
5075
  gitExclude(abs, excluded);
@@ -5918,19 +5974,19 @@ function renderFeed(f) {
5918
5974
  return out.join("\n").trimEnd() + "\n";
5919
5975
  }
5920
5976
  function latestFeed(v2) {
5921
- const dir = v2.p("ui");
5977
+ const dir2 = v2.p("ui");
5922
5978
  let best = null;
5923
5979
  try {
5924
- for (const f2 of fs13.readdirSync(dir)) {
5980
+ for (const f2 of fs13.readdirSync(dir2)) {
5925
5981
  if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
5926
- const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
5982
+ const m = fs13.statSync(path9.join(dir2, f2)).mtimeMs;
5927
5983
  if (!best || m > best.m) best = { f: f2, m };
5928
5984
  }
5929
5985
  } catch {
5930
5986
  return null;
5931
5987
  }
5932
5988
  if (!best) return null;
5933
- const f = readJson(path9.join(dir, best.f), null);
5989
+ const f = readJson(path9.join(dir2, best.f), null);
5934
5990
  return f && f.v === UI_FEED_VERSION ? f : null;
5935
5991
  }
5936
5992
  var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
@@ -6447,6 +6503,203 @@ var init_reflect3 = __esm({
6447
6503
  }
6448
6504
  });
6449
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
+ const dense = (readJob(v2, sessionId) ?? job).dense;
6643
+ 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: dense?.serve ?? null, denseHint: dense?.hint ?? [], usage: answer.usage });
6644
+ return answer;
6645
+ };
6646
+ try {
6647
+ const session = v2.readSession(sessionId);
6648
+ if (!session?.lastPrompt) return write({ error: "no prompt in the session file" });
6649
+ if (!canCall(config)) return write({ error: "offline: the router's index comes from the server" });
6650
+ const index = await routerIndex(store, v2, session.source ?? "");
6651
+ const systemFile = path11.join(dir(v2), `system-${index.hash}.txt`);
6652
+ if (!fs16.existsSync(systemFile)) fs16.writeFileSync(systemFile, index.system);
6653
+ const cwd = path11.join(dir(v2), "cwd");
6654
+ fs16.mkdirSync(cwd, { recursive: true });
6655
+ const inContext2 = session.inContext ?? [...session.loaded, ...session.pushed];
6656
+ const titles = new Map((v2.readBundle()?.sections ?? []).map((s) => [s.id, s.skill?.name ?? s.title]));
6657
+ const message = routerMessage(session.lastPrompt, promptContext(session), inContext2.map((id) => titles.get(id)).filter((t) => !!t));
6658
+ const res = await askClaude(systemFile, message, config.recall.routerModel || process.env.BUNSHIN_ROUTER_MODEL || ROUTER_MODEL, cwd);
6659
+ const u = res.usage ?? {};
6660
+ 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 } : {} };
6661
+ const meta = { ...res.duration_ms ? { modelMs: res.duration_ms } : {}, usage };
6662
+ if (res.is_error) return write({ ...meta, error: `claude: ${(res.result ?? "error").slice(0, 300)}` });
6663
+ const picks = parseRouterAnswer(res.result ?? "", index.items, inContext2);
6664
+ if (!picks) return write({ ...meta, error: `not JSON: ${(res.result ?? "").slice(0, 120)}` });
6665
+ return write({ ...picks, ...meta });
6666
+ } catch (err) {
6667
+ return write({ error: (err instanceof Error ? err.message : String(err)).slice(0, 300) });
6668
+ }
6669
+ }
6670
+ function sweepRouter(v2, now = Date.now()) {
6671
+ try {
6672
+ for (const f of fs16.readdirSync(dir(v2))) {
6673
+ if (!/\.(job|answer)\.json$/.test(f)) continue;
6674
+ const p = path11.join(dir(v2), f);
6675
+ if (now - fs16.statSync(p).mtimeMs > 864e5) fs16.rmSync(p, { force: true });
6676
+ }
6677
+ } catch {
6678
+ }
6679
+ }
6680
+ var ROUTER_MODEL, INDEX_TTL_MS, CLAUDE_TIMEOUT_MS, dir, safe2, jobFile, answerFile, readJob, writeJob, readAnswer;
6681
+ var init_route = __esm({
6682
+ "src/v2/route.ts"() {
6683
+ "use strict";
6684
+ init_v22();
6685
+ init_llm();
6686
+ init_redact2();
6687
+ init_util();
6688
+ init_api();
6689
+ init_store2();
6690
+ ROUTER_MODEL = "claude-haiku-5-5";
6691
+ INDEX_TTL_MS = 10 * 6e4;
6692
+ CLAUDE_TIMEOUT_MS = 6e4;
6693
+ dir = (v2) => path11.join(v2.dir, "router");
6694
+ safe2 = (id) => id.replace(/[^\w.-]/g, "_");
6695
+ jobFile = (v2, session) => path11.join(dir(v2), `${safe2(session)}.job.json`);
6696
+ answerFile = (v2, session, seq) => path11.join(dir(v2), `${safe2(session)}.${seq}.answer.json`);
6697
+ readJob = (v2, session) => readJson2(jobFile(v2, session));
6698
+ writeJob = (v2, session, job) => writeJsonAtomic(jobFile(v2, session), job);
6699
+ readAnswer = (v2, session, seq) => readJson2(answerFile(v2, session, seq));
6700
+ }
6701
+ });
6702
+
6450
6703
  // src/v2/uicmd.ts
6451
6704
  var uicmd_exports = {};
6452
6705
  __export(uicmd_exports, {
@@ -6459,51 +6712,51 @@ __export(uicmd_exports, {
6459
6712
  modSource: () => modSource
6460
6713
  });
6461
6714
  import { spawnSync } from "node:child_process";
6462
- import fs16 from "node:fs";
6715
+ import fs17 from "node:fs";
6463
6716
  import os3 from "node:os";
6464
- import path11 from "node:path";
6717
+ import path12 from "node:path";
6465
6718
  import { fileURLToPath } from "node:url";
6466
6719
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
6467
- return path11.join(home, ".bunshin", "claude-code-ui");
6720
+ return path12.join(home, ".bunshin", "claude-code-ui");
6468
6721
  }
6469
6722
  function modSource() {
6470
- const here = path11.dirname(fileURLToPath(import.meta.url));
6471
- for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
6472
- if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6723
+ const here = path12.dirname(fileURLToPath(import.meta.url));
6724
+ for (const dir2 of [path12.join(here, "claude-code-ui"), path12.join(here, "..", "claude-code-ui"), path12.resolve(here, "../../../../adapters/claude-code-ui")]) {
6725
+ if (fs17.existsSync(path12.join(dir2, ".claude-plugin", "plugin.json"))) return dir2;
6473
6726
  }
6474
6727
  return null;
6475
6728
  }
6476
6729
  function installedModVersion(home) {
6477
6730
  try {
6478
- return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6731
+ return JSON.parse(fs17.readFileSync(path12.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6479
6732
  } catch {
6480
6733
  return null;
6481
6734
  }
6482
6735
  }
6483
6736
  function copyDir(from, to) {
6484
- fs16.mkdirSync(to, { recursive: true });
6485
- for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
6737
+ fs17.mkdirSync(to, { recursive: true });
6738
+ for (const e of fs17.readdirSync(from, { withFileTypes: true })) {
6486
6739
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
6487
6740
  if (e.name === "node_modules") continue;
6488
- const a = path11.join(from, e.name);
6489
- const b = path11.join(to, e.name);
6741
+ const a = path12.join(from, e.name);
6742
+ const b = path12.join(to, e.name);
6490
6743
  if (e.isDirectory()) copyDir(a, b);
6491
- else fs16.copyFileSync(a, b);
6744
+ else fs17.copyFileSync(a, b);
6492
6745
  }
6493
6746
  }
6494
6747
  function installMod(version2, opts = {}) {
6495
6748
  const src = modSource();
6496
6749
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
6497
6750
  const home = modHome(opts.home);
6498
- const dir = path11.join(home, MOD_PLUGIN);
6499
- fs16.rmSync(dir, { recursive: true, force: true });
6500
- copyDir(src, dir);
6501
- const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
6502
- const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
6503
- fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6504
- fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
6505
- fs16.writeFileSync(
6506
- path11.join(home, ".claude-plugin", "marketplace.json"),
6751
+ const dir2 = path12.join(home, MOD_PLUGIN);
6752
+ fs17.rmSync(dir2, { recursive: true, force: true });
6753
+ copyDir(src, dir2);
6754
+ const manifest = path12.join(dir2, ".claude-plugin", "plugin.json");
6755
+ const m = JSON.parse(fs17.readFileSync(manifest, "utf8"));
6756
+ fs17.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6757
+ fs17.mkdirSync(path12.join(home, ".claude-plugin"), { recursive: true });
6758
+ fs17.writeFileSync(
6759
+ path12.join(home, ".claude-plugin", "marketplace.json"),
6507
6760
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6508
6761
  );
6509
6762
  const run2 = opts.run ?? spawnSync;
@@ -6517,7 +6770,7 @@ function installMod(version2, opts = {}) {
6517
6770
  };
6518
6771
  if (!step(["plugin", "marketplace", "add", home])) step(["plugin", "marketplace", "update", MOD_MARKETPLACE]);
6519
6772
  if (!step(["plugin", "install", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`, "--scope", "user"])) step(["plugin", "update", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`]);
6520
- return { dir, version: version2, steps };
6773
+ return { dir: dir2, version: version2, steps };
6521
6774
  }
6522
6775
  async function cmdUi(store, args, out, err, version2) {
6523
6776
  const sub = args._[1];
@@ -6538,7 +6791,7 @@ async function cmdUi(store, args, out, err, version2) {
6538
6791
  `);
6539
6792
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6540
6793
  `);
6541
- 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");
6794
+ 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");
6542
6795
  }
6543
6796
  return installed || !failed.length ? 0 : 1;
6544
6797
  }
@@ -6619,19 +6872,19 @@ __export(update_exports, {
6619
6872
  updateCheckDue: () => updateCheckDue,
6620
6873
  updateNotice: () => updateNotice
6621
6874
  });
6622
- import fs17 from "node:fs";
6623
- import path12 from "node:path";
6875
+ import fs18 from "node:fs";
6876
+ import path13 from "node:path";
6624
6877
  function readState2() {
6625
6878
  try {
6626
- return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6879
+ return JSON.parse(fs18.readFileSync(stateFile(), "utf8"));
6627
6880
  } catch {
6628
6881
  return {};
6629
6882
  }
6630
6883
  }
6631
6884
  function writeState(s) {
6632
6885
  try {
6633
- fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
6634
- fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6886
+ fs18.mkdirSync(path13.dirname(stateFile()), { recursive: true });
6887
+ fs18.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6635
6888
  } catch {
6636
6889
  }
6637
6890
  }
@@ -6681,14 +6934,14 @@ var init_update = __esm({
6681
6934
  PKG = "@bunshin-cc/bunshin";
6682
6935
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6683
6936
  DAY = 864e5;
6684
- stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6937
+ stateFile = () => path13.join(homeDir(), ".bunshin", "update.json");
6685
6938
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6686
6939
  }
6687
6940
  });
6688
6941
 
6689
6942
  // src/v2/hooks.ts
6690
- import fs18 from "node:fs";
6691
- import path13 from "node:path";
6943
+ import fs19 from "node:fs";
6944
+ import path14 from "node:path";
6692
6945
  function inContext(s) {
6693
6946
  return s.inContext ?? [...s.loaded, ...s.pushed];
6694
6947
  }
@@ -6755,10 +7008,10 @@ async function keepModCurrent(ctx, config) {
6755
7008
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6756
7009
  const current = version2();
6757
7010
  if (!modSource2() || installedModVersion2() === current) return;
6758
- const marker = path13.join(modHome2(), `.attempted-${current}`);
6759
- if (fs18.existsSync(marker)) return;
6760
- fs18.mkdirSync(modHome2(), { recursive: true });
6761
- fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
7011
+ const marker = path14.join(modHome2(), `.attempted-${current}`);
7012
+ if (fs19.existsSync(marker)) return;
7013
+ fs19.mkdirSync(modHome2(), { recursive: true });
7014
+ fs19.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6762
7015
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6763
7016
  } catch {
6764
7017
  }
@@ -6887,35 +7140,6 @@ function skillHint(sec, others) {
6887
7140
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6888
7141
  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}`;
6889
7142
  }
6890
- function lastReply(transcriptPath, maxBytes = 256e3) {
6891
- if (!transcriptPath) return void 0;
6892
- try {
6893
- const fd = fs18.openSync(transcriptPath, "r");
6894
- try {
6895
- const size = fs18.fstatSync(fd).size;
6896
- const len = Math.min(size, maxBytes);
6897
- const buf = Buffer.alloc(len);
6898
- fs18.readSync(fd, buf, 0, len, size - len);
6899
- const lines = buf.toString("utf8").split("\n");
6900
- for (let i = lines.length - 1; i >= 0; i--) {
6901
- let m;
6902
- try {
6903
- m = JSON.parse(lines[i]);
6904
- } catch {
6905
- continue;
6906
- }
6907
- if (m?.type !== "assistant" || m.isSidechain) continue;
6908
- const c = m.message?.content;
6909
- const text2 = Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("\n") : typeof c === "string" ? c : "";
6910
- if (text2.trim()) return redact2(text2.trim()).slice(0, 1500);
6911
- }
6912
- } finally {
6913
- fs18.closeSync(fd);
6914
- }
6915
- } catch {
6916
- }
6917
- return void 0;
6918
- }
6919
7143
  async function remoteAuto(ctx, prompt, exclude, source, context) {
6920
7144
  const config = ctx.store.config();
6921
7145
  if (!canCall(config) || !config.recall.useMothership) return null;
@@ -6939,6 +7163,14 @@ function invokedSkill(prompt, sections) {
6939
7163
  const name = m[1].split(":").pop().toLowerCase();
6940
7164
  return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
6941
7165
  }
7166
+ function startRoute(v2, ctx, sessionId, now) {
7167
+ const prev = readJob(v2, sessionId);
7168
+ if (prev && !prev.delivered && !prev.cancelled) v2.log("v2-router-undelivered", { session: sessionId, seq: prev.seq, answered: !!readAnswer(v2, sessionId, prev.seq) });
7169
+ const job = { seq: (prev?.seq ?? 0) + 1, at: now };
7170
+ writeJob(v2, sessionId, job);
7171
+ ctx.detach(["--root", ctx.store.root, "route", "--session", sessionId, "--seq", String(job.seq), "--quiet"], { cwd: ctx.cwd });
7172
+ return job;
7173
+ }
6942
7174
  async function onPrompt(ctx) {
6943
7175
  const prompt = ctx.input.prompt ?? "";
6944
7176
  if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
@@ -6960,7 +7192,9 @@ async function onPrompt(ctx) {
6960
7192
  const config = ctx.store.config();
6961
7193
  const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6962
7194
  const injected = inContext(session);
7195
+ const job = config.recall.router === "claude-cli" && canCall(config) ? startRoute(v2, ctx, session.sessionId, now) : void 0;
6963
7196
  const named = invokedSkill(prompt, view.sections);
7197
+ if (named && job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6964
7198
  if (named) {
6965
7199
  const why = `you ran /${named.skill.name}`;
6966
7200
  const deployed = deployEnabled(config, view.ctx);
@@ -6987,6 +7221,12 @@ async function onPrompt(ctx) {
6987
7221
  if (reply) context.reply = reply;
6988
7222
  const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
6989
7223
  const local = (id) => view.sections.find((s) => s.id === id);
7224
+ if (job && !remote?.router) {
7225
+ 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 };
7226
+ writeJob(v2, session.sessionId, { ...job, dense: { ...dense, hint: dense.hint.slice(0, 3) } });
7227
+ return withLive({});
7228
+ }
7229
+ if (job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6990
7230
  if (remote) {
6991
7231
  const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
6992
7232
  if (remote.section) {
@@ -7059,15 +7299,69 @@ function toolPaths(input) {
7059
7299
  }
7060
7300
  return [...new Set(out)];
7061
7301
  }
7062
- function onPreToolUse(ctx) {
7302
+ async function routedText(v2, ctx, session) {
7303
+ const config = ctx.store.config();
7304
+ if (config.recall.router !== "claude-cli") return "";
7305
+ const job = readJob(v2, session.sessionId);
7306
+ if (!job || job.delivered || job.cancelled) return "";
7307
+ const answer = readAnswer(v2, session.sessionId, job.seq);
7308
+ if (!answer) return "";
7309
+ const fallback = !!answer.error;
7310
+ writeJob(v2, session.sessionId, { ...job, delivered: { at: (/* @__PURE__ */ new Date()).toISOString(), ...ctx.input.tool_name ? { tool: ctx.input.tool_name } : {}, ...fallback ? { fallback } : {} } });
7311
+ const seen = new Set(inContext(session));
7312
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
7313
+ const find = async (id) => {
7314
+ const here = view.sections.find((s) => s.id === id);
7315
+ if (here || !canCall(config)) return here;
7316
+ try {
7317
+ return (await v2api(config, "GET", `/sections/${encodeURIComponent(id)}`, void 0, { timeoutMs: Math.max(300, Math.min(2e3, ctx.deadline - Date.now() - 300)) })).section;
7318
+ } catch {
7319
+ return void 0;
7320
+ }
7321
+ };
7322
+ const serveId = fallback ? job.dense?.serve : answer.serve;
7323
+ const fresh = (ids2) => [...new Set(ids2.filter((id) => !!id && !seen.has(id)))];
7324
+ const [doc] = await Promise.all(fresh([serveId]).map(find));
7325
+ const rules = (await Promise.all(fresh(fallback ? [] : answer.rules ?? []).filter((id) => id !== doc?.id).map(find))).filter((s) => !!s);
7326
+ const hints = fresh(fallback ? job.dense?.hint ?? [] : answer.hint ?? []).filter((id) => id !== doc?.id && !session.hinted?.includes(id));
7327
+ const hintTitles = hints.map((id) => view.sections.find((s) => s.id === id)?.title).filter((t) => !!t);
7328
+ 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, dense: job.dense?.serve ?? null, ...answer.error ? { error: answer.error } : {} });
7329
+ const ids = [...doc ? [doc.id] : [], ...rules.map((r) => r.id)];
7330
+ if (!ids.length && !hintTitles.length) return "";
7331
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
7332
+ addUnique(s.served, ...ids, ...hints);
7333
+ addUnique(s.loaded, ...ids);
7334
+ markInContext(s, ...ids);
7335
+ s.hinted = s.hinted ?? [];
7336
+ addUnique(s.hinted, ...hints);
7337
+ });
7338
+ const why = fallback ? "your request matches it" : "the router picked it for the user's latest request";
7339
+ updateFeed(v2, session.sessionId, (f) => {
7340
+ 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" });
7341
+ if (doc) addServed(f, { id: doc.id, title: doc.title, kind: doc.kind, ...doc.skill ? { skill: doc.skill.name } : {}, how: "auto", why });
7342
+ 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"}`);
7343
+ if (rules.length) addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
7344
+ });
7345
+ return [
7346
+ doc ? renderSection(doc, why, { deployed: !!doc.skill && deployEnabled(config, view.ctx) }) : "",
7347
+ ...rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })),
7348
+ hintTitles.length ? hintLine(hintTitles, config.harness) : ""
7349
+ ].filter(Boolean).join("\n\n");
7350
+ }
7351
+ async function onPreToolUse(ctx) {
7063
7352
  const v2 = new V2Store(ctx.store);
7064
7353
  const session = touch(v2, ctx, (s) => {
7065
7354
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
7066
7355
  });
7067
7356
  const live = liveUpdate(v2, ctx, session);
7357
+ let routed = "";
7358
+ try {
7359
+ routed = await routedText(v2, ctx, session);
7360
+ } catch {
7361
+ }
7068
7362
  const withLive = (out) => {
7069
- if (!live) return out;
7070
- return additional("PreToolUse", [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n"));
7363
+ const text3 = [live, routed, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
7364
+ return text3 === (out.hookSpecificOutput?.additionalContext ?? "") ? out : additional("PreToolUse", text3);
7071
7365
  };
7072
7366
  const raw = toolPaths(ctx.input.tool_input);
7073
7367
  if (!raw.length) return withLive({});
@@ -7076,7 +7370,7 @@ function onPreToolUse(ctx) {
7076
7370
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
7077
7371
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
7078
7372
  if (!paths.length && !text2) return withLive({});
7079
- const seen = new Set(inContext(session));
7373
+ const seen = new Set(inContext(v2.readSession(session.sessionId) ?? session));
7080
7374
  const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
7081
7375
  if (!hits.length) return withLive({});
7082
7376
  const ids = hits.map((h) => h.section.id);
@@ -7327,8 +7621,10 @@ var init_hooks = __esm({
7327
7621
  init_signals();
7328
7622
  init_store2();
7329
7623
  init_ui();
7624
+ init_route();
7330
7625
  init_source();
7331
7626
  init_view();
7627
+ init_route();
7332
7628
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
7333
7629
  NUDGE_INTERVAL_MS = 60 * 6e4;
7334
7630
  CHECKPOINT_MS = 30 * 6e4;
@@ -7342,8 +7638,8 @@ var init_hooks = __esm({
7342
7638
  });
7343
7639
 
7344
7640
  // src/hooks.ts
7345
- import fs19 from "node:fs";
7346
- import path14 from "node:path";
7641
+ import fs20 from "node:fs";
7642
+ import path15 from "node:path";
7347
7643
  function normalizeEvent(raw) {
7348
7644
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
7349
7645
  }
@@ -7411,19 +7707,19 @@ async function handleHook(rawEvent, input, opts = {}) {
7411
7707
  }
7412
7708
  function deferToProject(cwd, env = process.env) {
7413
7709
  if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
7414
- let dir = path14.resolve(cwd);
7710
+ let dir2 = path15.resolve(cwd);
7415
7711
  for (let i = 0; i < 40; i++) {
7416
7712
  for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
7417
7713
  try {
7418
- const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
7714
+ const text2 = fs20.readFileSync(path15.join(dir2, f), "utf8");
7419
7715
  if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
7420
7716
  } catch {
7421
7717
  }
7422
7718
  }
7423
- if (fs19.existsSync(path14.join(dir, ".git"))) return false;
7424
- const up = path14.dirname(dir);
7425
- if (up === dir) return false;
7426
- dir = up;
7719
+ if (fs20.existsSync(path15.join(dir2, ".git"))) return false;
7720
+ const up = path15.dirname(dir2);
7721
+ if (up === dir2) return false;
7722
+ dir2 = up;
7427
7723
  }
7428
7724
  return false;
7429
7725
  }
@@ -7469,11 +7765,11 @@ var init_hooks2 = __esm({
7469
7765
  });
7470
7766
 
7471
7767
  // src/project-hooks.ts
7472
- import fs20 from "node:fs";
7473
- import path15 from "node:path";
7768
+ import fs21 from "node:fs";
7769
+ import path16 from "node:path";
7474
7770
  import { execFileSync as execFileSync4 } from "node:child_process";
7475
7771
  function codexProjectHooks(cwd) {
7476
- const legacyFile = path15.join(cwd, ".codex", "hooks.json");
7772
+ const legacyFile = path16.join(cwd, ".codex", "hooks.json");
7477
7773
  const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
7478
7774
  encoding: "utf8",
7479
7775
  stdio: ["ignore", "pipe", "ignore"],
@@ -7487,14 +7783,14 @@ function codexProjectHooks(cwd) {
7487
7783
  } catch {
7488
7784
  return { file: legacyFile, shared: false, legacyFile };
7489
7785
  }
7490
- const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
7786
+ const linked = fs21.realpathSync(common) !== fs21.realpathSync(directory);
7491
7787
  if (linked) {
7492
- if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
7788
+ if (path16.basename(common) !== ".git" || !fs21.statSync(common).isDirectory()) {
7493
7789
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
7494
7790
  }
7495
- root = path15.dirname(common);
7791
+ root = path16.dirname(common);
7496
7792
  }
7497
- const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
7793
+ const file = fs21.realpathSync(root) === fs21.realpathSync(cwd) ? legacyFile : path16.join(root, ".codex", "hooks.json");
7498
7794
  return { file, shared: linked, legacyFile };
7499
7795
  }
7500
7796
  var init_project_hooks = __esm({
@@ -7506,6 +7802,7 @@ var init_project_hooks = __esm({
7506
7802
  // src/init.ts
7507
7803
  var init_exports = {};
7508
7804
  __export(init_exports, {
7805
+ ALL_TOOLS_MATCHER: () => ALL_TOOLS_MATCHER,
7509
7806
  BORROW_COMMAND: () => BORROW_COMMAND,
7510
7807
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
7511
7808
  HOOK_EVENTS: () => HOOK_EVENTS,
@@ -7527,8 +7824,8 @@ __export(init_exports, {
7527
7824
  runInit: () => runInit
7528
7825
  });
7529
7826
  import { execFileSync as execFileSync5 } from "node:child_process";
7530
- import fs21 from "node:fs";
7531
- import path16 from "node:path";
7827
+ import fs22 from "node:fs";
7828
+ import path17 from "node:path";
7532
7829
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
7533
7830
  function isBunshinEntry(entry) {
7534
7831
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -7553,7 +7850,7 @@ function removeBunshinHooks(settings) {
7553
7850
  else delete out.hooks;
7554
7851
  return out;
7555
7852
  }
7556
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
7853
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false, allTools = false } = {}) {
7557
7854
  const base = removeBunshinHooks(settings);
7558
7855
  const hooks = { ...base.hooks ?? {} };
7559
7856
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
@@ -7561,7 +7858,8 @@ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code
7561
7858
  const hook = bunshinHookEntry(arg, timeout, bin);
7562
7859
  hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
7563
7860
  if (harness === "codex") delete hook._bunshin;
7564
- groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
7861
+ const m = matcher && allTools && event === "PreToolUse" ? ALL_TOOLS_MATCHER : matcher;
7862
+ groups.push(m ? { matcher: m, hooks: [hook] } : { hooks: [hook] });
7565
7863
  hooks[event] = groups;
7566
7864
  }
7567
7865
  return { ...base, hooks };
@@ -7573,16 +7871,16 @@ function countBunshinHooks(settings) {
7573
7871
  );
7574
7872
  }
7575
7873
  function claudeSettingsPath() {
7576
- return path16.join(homeDir(), ".claude", "settings.json");
7874
+ return path17.join(homeDir(), ".claude", "settings.json");
7577
7875
  }
7578
7876
  function installPiExtension(cwd, remove = false, scope = "repo") {
7579
- const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
7877
+ const file = scope === "global" ? path17.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path17.join(cwd, ".pi/extensions/bunshin.js");
7580
7878
  const marker = "// Managed by Bunshin setup.";
7581
7879
  try {
7582
- const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
7880
+ const previous = fileExists(file) ? fs22.readFileSync(file, "utf8") : "";
7583
7881
  if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
7584
7882
  if (remove) {
7585
- if (previous) fs21.unlinkSync(file);
7883
+ if (previous) fs22.unlinkSync(file);
7586
7884
  return { file, installed: 0, changed: !!previous };
7587
7885
  }
7588
7886
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -7591,24 +7889,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
7591
7889
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
7592
7890
  `;
7593
7891
  if (text2 !== previous) {
7594
- ensureDir(path16.dirname(file));
7595
- fs21.writeFileSync(file, text2);
7892
+ ensureDir(path17.dirname(file));
7893
+ fs22.writeFileSync(file, text2);
7596
7894
  }
7597
7895
  return { file, installed: 1, changed: text2 !== previous };
7598
7896
  } catch (error) {
7599
7897
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
7600
7898
  }
7601
7899
  }
7602
- function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
7900
+ function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false, allTools = false } = {}) {
7603
7901
  try {
7604
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7605
- const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
7902
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7903
+ const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user, allTools });
7606
7904
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7607
7905
  if (changed) {
7608
- ensureDir(path16.dirname(file));
7906
+ ensureDir(path17.dirname(file));
7609
7907
  if (fileExists(file)) {
7610
7908
  try {
7611
- fs21.copyFileSync(file, `${file}.bunshin-backup`);
7909
+ fs22.copyFileSync(file, `${file}.bunshin-backup`);
7612
7910
  } catch {
7613
7911
  }
7614
7912
  }
@@ -7623,23 +7921,23 @@ function mcpLauncher(bin) {
7623
7921
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7624
7922
  let script = process.argv[1] ?? "";
7625
7923
  try {
7626
- script = fs21.realpathSync(script);
7924
+ script = fs22.realpathSync(script);
7627
7925
  } catch {
7628
7926
  }
7629
7927
  return { command: process.execPath, args: [script, "mcp"] };
7630
7928
  }
7631
7929
  function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7632
7930
  try {
7633
- const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7931
+ const current = fileExists(file) ? fs22.readFileSync(file, "utf8") : null;
7634
7932
  const ours = current === null || current === content;
7635
7933
  if (remove) {
7636
- if (current !== null && ours) fs21.unlinkSync(file);
7934
+ if (current !== null && ours) fs22.unlinkSync(file);
7637
7935
  return { file, installed: 0, changed: current !== null && ours };
7638
7936
  }
7639
- if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
7937
+ if (!ours) return { file, installed: 0, changed: false, error: `a different ${path17.basename(file)} is already there; left as it is` };
7640
7938
  if (current === null) {
7641
- fs21.mkdirSync(path16.dirname(file), { recursive: true });
7642
- fs21.writeFileSync(file, content);
7939
+ fs22.mkdirSync(path17.dirname(file), { recursive: true });
7940
+ fs22.writeFileSync(file, content);
7643
7941
  }
7644
7942
  return { file, installed: 1, changed: current === null };
7645
7943
  } catch (err) {
@@ -7648,7 +7946,7 @@ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND })
7648
7946
  }
7649
7947
  function installMcpConfig({ file, launcher, remove = false }) {
7650
7948
  try {
7651
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7949
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7652
7950
  const servers = { ...before.mcpServers ?? {} };
7653
7951
  if (remove) delete servers.bunshin;
7654
7952
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7656,7 +7954,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7656
7954
  if (!Object.keys(servers).length) delete after.mcpServers;
7657
7955
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7658
7956
  if (changed) {
7659
- if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7957
+ if (!Object.keys(after).length && fileExists(file)) fs22.unlinkSync(file);
7660
7958
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7661
7959
  }
7662
7960
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7665,7 +7963,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7665
7963
  }
7666
7964
  }
7667
7965
  function installUserMcp({ launcher, remove = false, claude = "claude" }) {
7668
- const file = path16.join(homeDir(), ".claude.json");
7966
+ const file = path17.join(homeDir(), ".claude.json");
7669
7967
  const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
7670
7968
  try {
7671
7969
  let had = false;
@@ -7691,22 +7989,22 @@ function runInit(opts = {}) {
7691
7989
  const cwd = opts.cwd ?? process.cwd();
7692
7990
  const warnings = [];
7693
7991
  const scope = opts.scope ?? "repo";
7694
- const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
7695
- if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
7992
+ const root = scope === "global" ? path17.join(homeDir(), ".bunshin") : path17.join(cwd, ".bunshin");
7993
+ if (fs22.existsSync(root) && !new Store(root).trusted && fs22.readdirSync(root).length) {
7696
7994
  const aside = `${root}.untrusted-${Date.now()}`;
7697
- fs21.renameSync(root, aside);
7995
+ fs22.renameSync(root, aside);
7698
7996
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7699
7997
  }
7700
- const existed = fileExists(path16.join(root, "config.json"));
7998
+ const existed = fileExists(path17.join(root, "config.json"));
7701
7999
  const store = new Store(root);
7702
8000
  store.ensureLayout();
7703
- fs21.chmodSync(root, 448);
8001
+ fs22.chmodSync(root, 448);
7704
8002
  const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
7705
- if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
7706
- const ignore = path16.join(cwd, ".gitignore");
7707
- const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
8003
+ if (scope === "repo" && (hookScope === "project" || fileExists(path17.join(cwd, ".git")))) {
8004
+ const ignore = path17.join(cwd, ".gitignore");
8005
+ const current = fileExists(ignore) ? fs22.readFileSync(ignore, "utf8") : "";
7708
8006
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7709
- fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
8007
+ fs22.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7710
8008
  `);
7711
8009
  }
7712
8010
  }
@@ -7736,17 +8034,17 @@ function runInit(opts = {}) {
7736
8034
  }
7737
8035
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7738
8036
  const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7739
- 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());
7740
- hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
8037
+ 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());
8038
+ hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile, allTools: config.recall.router === "claude-cli" });
7741
8039
  if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
7742
- const local = path16.join(cwd, ".claude", "settings.local.json");
7743
- if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
8040
+ const local = path17.join(cwd, ".claude", "settings.local.json");
8041
+ if (fileExists(local) && path17.resolve(local) !== path17.resolve(file)) installClaudeHooks({ file: local, remove: true });
7744
8042
  }
7745
8043
  if (projectHooks?.shared) {
7746
8044
  hooks.sharedWithWorktrees = true;
7747
- if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
8045
+ if (!hooks.error && !opts.removeHooks) ensureDir(path17.dirname(projectHooks.legacyFile));
7748
8046
  }
7749
- if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
8047
+ if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs22.realpathSync(projectHooks.file) !== fs22.realpathSync(projectHooks.legacyFile)) {
7750
8048
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7751
8049
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7752
8050
  }
@@ -7756,15 +8054,15 @@ function runInit(opts = {}) {
7756
8054
  let learnCommand;
7757
8055
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
7758
8056
  const user = hookScope === "user";
7759
- 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 });
8057
+ 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 });
7760
8058
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
7761
- learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
8059
+ learnCommand = installLearnCommand({ file: path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7762
8060
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7763
8061
  }
7764
8062
  let borrowCommand;
7765
8063
  if (opts.removeHooks || !opts.noHooks) {
7766
8064
  const user = hookScope === "user";
7767
- 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;
8065
+ 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;
7768
8066
  if (file) {
7769
8067
  borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
7770
8068
  if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
@@ -7790,7 +8088,7 @@ function otherHarnessInstructions() {
7790
8088
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7791
8089
  ].join("\n");
7792
8090
  }
7793
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
8091
+ var PRE_TOOL_MATCHER, ALL_TOOLS_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
7794
8092
  var init_init = __esm({
7795
8093
  "src/init.ts"() {
7796
8094
  "use strict";
@@ -7800,6 +8098,7 @@ var init_init = __esm({
7800
8098
  init_project_hooks();
7801
8099
  init_trust();
7802
8100
  PRE_TOOL_MATCHER = "Read|Edit|Write|MultiEdit|NotebookEdit";
8101
+ ALL_TOOLS_MATCHER = "*";
7803
8102
  HOOK_EVENTS = [
7804
8103
  { event: "SessionStart", arg: "session-start", timeout: 10 },
7805
8104
  { event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
@@ -7921,9 +8220,9 @@ __export(commands_exports, {
7921
8220
  setupRobot: () => setupRobot,
7922
8221
  v2Status: () => v2Status
7923
8222
  });
7924
- import fs22 from "node:fs";
7925
- import path17 from "node:path";
7926
- import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
8223
+ import fs23 from "node:fs";
8224
+ import path18 from "node:path";
8225
+ import { createHash as createHash6, randomBytes as randomBytes4 } from "node:crypto";
7927
8226
  function cmdReview(store, args, cwd, out, err) {
7928
8227
  const v2 = new V2Store(store);
7929
8228
  const sub = args._[1] ?? "list";
@@ -8093,11 +8392,11 @@ function cmdSuggest(store, args, cwd, out, err) {
8093
8392
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
8094
8393
  return 1;
8095
8394
  }
8096
- const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8097
- const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
8395
+ const harnessDir = path18.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8396
+ const file = str3(args.flags.file) ? path18.resolve(cwd, str3(args.flags.file)) : path18.join(harnessDir, rel);
8098
8397
  let content;
8099
8398
  try {
8100
- content = fs22.readFileSync(file, "utf8");
8399
+ content = fs23.readFileSync(file, "utf8");
8101
8400
  } catch {
8102
8401
  err(`bunshin suggest: cannot read ${file}
8103
8402
  `);
@@ -8218,15 +8517,15 @@ async function cmdQuestions(store, args, out, err) {
8218
8517
  return 0;
8219
8518
  }
8220
8519
  function saveCredential(storeRoot, apiUrl, apiKey, id) {
8221
- const dir = path17.join(homeDir(), ".config/bunshin/credentials");
8222
- fs22.mkdirSync(dir, { recursive: true, mode: 448 });
8223
- fs22.chmodSync(dir, 448);
8224
- const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
8520
+ const dir2 = path18.join(homeDir(), ".config/bunshin/credentials");
8521
+ fs23.mkdirSync(dir2, { recursive: true, mode: 448 });
8522
+ fs23.chmodSync(dir2, 448);
8523
+ const file = path18.join(dir2, `${createHash6("sha256").update(`${storeRoot}
8225
8524
  ${apiUrl}
8226
8525
  ${id}`).digest("hex")}.json`);
8227
8526
  const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
8228
- fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8229
- fs22.renameSync(tmp, file);
8527
+ fs23.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8528
+ fs23.renameSync(tmp, file);
8230
8529
  return file;
8231
8530
  }
8232
8531
  async function setupRobot(opts) {
@@ -8234,10 +8533,10 @@ async function setupRobot(opts) {
8234
8533
  const existing = Store.discover(opts.cwd);
8235
8534
  let store;
8236
8535
  const harness = opts.harness ?? "other";
8237
- if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
8536
+ if (existing && path18.resolve(path18.dirname(existing.root)) === path18.resolve(opts.cwd) && harness === "other") store = existing;
8238
8537
  else {
8239
8538
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
8240
- store = new Store(path17.join(opts.cwd, ".bunshin"));
8539
+ store = new Store(path18.join(opts.cwd, ".bunshin"));
8241
8540
  }
8242
8541
  let config = store.config();
8243
8542
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -8391,16 +8690,16 @@ __export(setup_exports, {
8391
8690
  setupProject: () => setupProject,
8392
8691
  shellQuote: () => shellQuote
8393
8692
  });
8394
- import fs23 from "node:fs";
8395
- import path18 from "node:path";
8396
- import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
8693
+ import fs24 from "node:fs";
8694
+ import path19 from "node:path";
8695
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "node:crypto";
8397
8696
  function executable(name) {
8398
- const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
8697
+ const directories = [...(process.env.PATH ?? "").split(path19.delimiter), path19.join(homeDir(), ".local/bin")];
8399
8698
  for (const directory of directories) {
8400
- const file = path18.resolve(directory, name);
8699
+ const file = path19.resolve(directory, name);
8401
8700
  try {
8402
- fs23.accessSync(file, fs23.constants.X_OK);
8403
- if (fs23.statSync(file).isFile()) return file;
8701
+ fs24.accessSync(file, fs24.constants.X_OK);
8702
+ if (fs24.statSync(file).isFile()) return file;
8404
8703
  } catch {
8405
8704
  }
8406
8705
  }
@@ -8408,8 +8707,8 @@ function executable(name) {
8408
8707
  }
8409
8708
  function hookBinary(harness) {
8410
8709
  const agent = executable(harness === "claude-code" ? "claude" : harness);
8411
- const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
8412
- return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
8710
+ const directories = [.../* @__PURE__ */ new Set([path19.dirname(process.execPath), path19.join(homeDir(), ".local/bin"), ...agent ? [path19.dirname(agent)] : []])];
8711
+ return `PATH=${shellQuote(directories.join(path19.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs24.realpathSync(process.argv[1]))}`;
8413
8712
  }
8414
8713
  function apiAddress(raw) {
8415
8714
  const url = new URL(raw);
@@ -8478,23 +8777,23 @@ async function setupProject(opts) {
8478
8777
  const apiKey = opts.apiKey.trim();
8479
8778
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
8480
8779
  const profile = await connectionProfile(apiUrl, apiKey);
8481
- const root = path18.join(opts.cwd, ".bunshin");
8780
+ const root = path19.join(opts.cwd, ".bunshin");
8482
8781
  const store = new Store(root);
8483
8782
  const previous = store.config();
8484
8783
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
8485
8784
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
8486
8785
  }
8487
- const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
8488
- fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8489
- fs23.chmodSync(credentialDirectory, 448);
8490
- const name = createHash6("sha256").update(`${root}
8786
+ const credentialDirectory = path19.join(homeDir(), ".config/bunshin/credentials");
8787
+ fs24.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8788
+ fs24.chmodSync(credentialDirectory, 448);
8789
+ const name = createHash7("sha256").update(`${root}
8491
8790
  ${apiUrl}
8492
8791
  ${profile.orgId}
8493
8792
  ${profile.userId}`).digest("hex");
8494
- const credentialFile = path18.join(credentialDirectory, `${name}.json`);
8793
+ const credentialFile = path19.join(credentialDirectory, `${name}.json`);
8495
8794
  const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
8496
- fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8497
- fs23.renameSync(temporary, credentialFile);
8795
+ fs24.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8796
+ fs24.renameSync(temporary, credentialFile);
8498
8797
  const result = runInit({
8499
8798
  cwd: opts.cwd,
8500
8799
  harness: opts.harness,
@@ -8846,9 +9145,9 @@ __export(sync_exports2, {
8846
9145
  resolveConflict: () => resolveConflict,
8847
9146
  runSync: () => runSync
8848
9147
  });
8849
- import { createHash as createHash7 } from "node:crypto";
8850
- import path19 from "node:path";
8851
- import fs24 from "node:fs";
9148
+ import { createHash as createHash8 } from "node:crypto";
9149
+ import path20 from "node:path";
9150
+ import fs25 from "node:fs";
8852
9151
  async function api(config, method, route, body, timeoutMs = 1e4) {
8853
9152
  const ctrl = new AbortController();
8854
9153
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8886,22 +9185,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8886
9185
  }
8887
9186
  }
8888
9187
  async function runSync(store) {
8889
- const lock = path19.join(store.root, "sync.lock");
9188
+ const lock = path20.join(store.root, "sync.lock");
8890
9189
  const deadline = Date.now() + 12e4;
8891
9190
  for (; ; ) {
8892
9191
  try {
8893
- fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
9192
+ fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8894
9193
  break;
8895
9194
  } catch (error) {
8896
9195
  if (error.code !== "EEXIST") throw error;
8897
9196
  try {
8898
- const owner = Number(fs24.readFileSync(lock, "utf8"));
9197
+ const owner = Number(fs25.readFileSync(lock, "utf8"));
8899
9198
  if (Number.isInteger(owner) && owner > 0) {
8900
9199
  try {
8901
9200
  process.kill(owner, 0);
8902
9201
  } catch (error2) {
8903
9202
  if (error2.code === "ESRCH") {
8904
- fs24.unlinkSync(lock);
9203
+ fs25.unlinkSync(lock);
8905
9204
  continue;
8906
9205
  }
8907
9206
  }
@@ -8920,7 +9219,7 @@ async function runSync(store) {
8920
9219
  return await syncStore(store);
8921
9220
  } finally {
8922
9221
  try {
8923
- fs24.unlinkSync(lock);
9222
+ fs25.unlinkSync(lock);
8924
9223
  } catch {
8925
9224
  }
8926
9225
  }
@@ -8948,9 +9247,9 @@ async function syncStore(store) {
8948
9247
  result.errors.push(String(e instanceof Error ? e.message : e));
8949
9248
  }
8950
9249
  }
8951
- const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
9250
+ const acknowledgementsFile = path20.join(store.root, "synced-traces.json");
8952
9251
  const acknowledgements = readJson(acknowledgementsFile, {});
8953
- const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
9252
+ const fingerprint2 = (trace) => createHash8("sha256").update(JSON.stringify(trace)).digest("hex");
8954
9253
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8955
9254
  for (const t of traces) {
8956
9255
  try {
@@ -9203,11 +9502,11 @@ __export(demo_exports, {
9203
9502
  runDemo: () => runDemo
9204
9503
  });
9205
9504
  import assert from "node:assert/strict";
9206
- import fs25 from "node:fs";
9505
+ import fs26 from "node:fs";
9207
9506
  import os4 from "node:os";
9208
- import path20 from "node:path";
9507
+ import path21 from "node:path";
9209
9508
  async function runDemo(write) {
9210
- const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
9509
+ const root = fs26.mkdtempSync(path21.join(os4.tmpdir(), "bunshin-demo-"));
9211
9510
  try {
9212
9511
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
9213
9512
  const { lessons, constraint, workflow } = aprScenario();
@@ -9224,7 +9523,7 @@ Task: ${APR_TASK}
9224
9523
  write(result.text);
9225
9524
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
9226
9525
  } finally {
9227
- fs25.rmSync(root, { recursive: true, force: true });
9526
+ fs26.rmSync(root, { recursive: true, force: true });
9228
9527
  }
9229
9528
  }
9230
9529
  var init_demo = __esm({
@@ -9237,21 +9536,21 @@ var init_demo = __esm({
9237
9536
  });
9238
9537
 
9239
9538
  // src/v2/gitremote.ts
9240
- import fs26 from "node:fs";
9241
- import path21 from "node:path";
9242
- import { createHash as createHash8 } from "node:crypto";
9539
+ import fs27 from "node:fs";
9540
+ import path22 from "node:path";
9541
+ import { createHash as createHash9 } from "node:crypto";
9243
9542
  function saveGitCredential(apiUrl, apiKey) {
9244
9543
  const file = gitCredentialFile();
9245
- fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
9544
+ fs27.mkdirSync(path22.dirname(file), { recursive: true, mode: 448 });
9246
9545
  let all = {};
9247
9546
  try {
9248
- all = JSON.parse(fs26.readFileSync(file, "utf8"));
9547
+ all = JSON.parse(fs27.readFileSync(file, "utf8"));
9249
9548
  } catch {
9250
9549
  }
9251
9550
  all[apiUrl.replace(/\/$/, "")] = apiKey;
9252
9551
  const tmp = `${file}.${process.pid}.tmp`;
9253
- fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9254
- fs26.renameSync(tmp, file);
9552
+ fs27.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9553
+ fs27.renameSync(tmp, file);
9255
9554
  }
9256
9555
  function resolveGitKey(apiUrl) {
9257
9556
  const base = apiUrl.replace(/\/$/, "");
@@ -9259,20 +9558,20 @@ function resolveGitKey(apiUrl) {
9259
9558
  const origin = urlOrigin(base);
9260
9559
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
9261
9560
  try {
9262
- const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
9561
+ const all = JSON.parse(fs27.readFileSync(gitCredentialFile(), "utf8"));
9263
9562
  if (all[base]) return all[base];
9264
9563
  } catch {
9265
9564
  }
9266
- const dir = path21.join(homeDir(), ".config/bunshin/credentials");
9565
+ const dir2 = path22.join(homeDir(), ".config/bunshin/credentials");
9267
9566
  let files = [];
9268
9567
  try {
9269
- files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
9568
+ files = fs27.readdirSync(dir2).filter((f) => f.endsWith(".json"));
9270
9569
  } catch {
9271
9570
  return void 0;
9272
9571
  }
9273
9572
  for (const f of files.sort()) {
9274
9573
  try {
9275
- const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
9574
+ const c = JSON.parse(fs27.readFileSync(path22.join(dir2, f), "utf8"));
9276
9575
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
9277
9576
  } catch {
9278
9577
  }
@@ -9280,7 +9579,7 @@ function resolveGitKey(apiUrl) {
9280
9579
  return void 0;
9281
9580
  }
9282
9581
  function refsDigest(refs) {
9283
- return createHash8("sha256").update(
9582
+ return createHash9("sha256").update(
9284
9583
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
9285
9584
  ).digest("hex").slice(0, 12);
9286
9585
  }
@@ -9293,7 +9592,7 @@ var init_gitremote = __esm({
9293
9592
  ZERO_SHA = "0".repeat(40);
9294
9593
  REMOTE_PATH = "/v1/git/repos/";
9295
9594
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
9296
- gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
9595
+ gitCredentialFile = () => path22.join(homeDir(), ".config/bunshin/git-credentials.json");
9297
9596
  GitApiError = class extends Error {
9298
9597
  constructor(status, message, body = null) {
9299
9598
  super(message);
@@ -9385,8 +9684,8 @@ __export(gitrepo_exports, {
9385
9684
  githubReferences: () => githubReferences,
9386
9685
  repoFromUrl: () => repoFromUrl
9387
9686
  });
9388
- import fs27 from "node:fs";
9389
- import path22 from "node:path";
9687
+ import fs28 from "node:fs";
9688
+ import path23 from "node:path";
9390
9689
  import { spawnSync as spawnSync2 } from "node:child_process";
9391
9690
  function git3(cwd, args, opts = {}) {
9392
9691
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
@@ -9395,9 +9694,9 @@ function git3(cwd, args, opts = {}) {
9395
9694
  return res;
9396
9695
  }
9397
9696
  function onPath(name) {
9398
- for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
9697
+ for (const dir2 of (process.env.PATH ?? "").split(path23.delimiter)) {
9399
9698
  try {
9400
- fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
9699
+ fs28.accessSync(path23.join(dir2, name), fs28.constants.X_OK);
9401
9700
  return true;
9402
9701
  } catch {
9403
9702
  }
@@ -9448,9 +9747,9 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
9448
9747
  if (!reuseKey || !process.env.BUNSHIN_API_KEY || key !== process.env.BUNSHIN_API_KEY) saveGitCredential(apiUrl, key);
9449
9748
  return client;
9450
9749
  }
9451
- function toplevel(dir) {
9452
- const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
9453
- if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
9750
+ function toplevel(dir2) {
9751
+ const r = git3(dir2, ["rev-parse", "--show-toplevel"], { allowFail: true });
9752
+ if (!r.ok) throw new Error(`${dir2} is not inside a git repository`);
9454
9753
  return r.out.trim();
9455
9754
  }
9456
9755
  function githubReferences(root, repo) {
@@ -9481,11 +9780,11 @@ function switchOrigin(root, url, out) {
9481
9780
  }
9482
9781
  async function cmdImport(args, cwd, out, err) {
9483
9782
  requireHelper();
9484
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9783
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9485
9784
  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");
9486
9785
  const rs = remotes(root);
9487
9786
  const originUrl = rs.get("origin");
9488
- const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
9787
+ const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path23.basename(path23.dirname(root))}/${path23.basename(root)}`;
9489
9788
  const client = await connect(args, cwd);
9490
9789
  const url = remoteUrl(client.apiUrl, name);
9491
9790
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
@@ -9561,7 +9860,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
9561
9860
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
9562
9861
  `);
9563
9862
  }
9564
- if (fs27.existsSync(path22.join(root, ".github"))) out(`
9863
+ if (fs28.existsSync(path23.join(root, ".github"))) out(`
9565
9864
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
9566
9865
  `);
9567
9866
  out(`
@@ -9572,7 +9871,7 @@ New machines clone with: bunshin repo clone ${name}
9572
9871
  }
9573
9872
  async function cmdAttach(args, cwd, out, err) {
9574
9873
  requireHelper();
9575
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9874
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9576
9875
  const rs = remotes(root);
9577
9876
  const originUrl = rs.get("origin");
9578
9877
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -9617,15 +9916,15 @@ async function cmdClone(args, cwd, out) {
9617
9916
  const name = args._[1];
9618
9917
  if (!name) throw new Error("usage: bunshin repo clone owner/name [directory]");
9619
9918
  const client = await connect(args, cwd);
9620
- const dir = args._[2] ?? name.split("/")[1];
9621
- const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
9919
+ const dir2 = args._[2] ?? name.split("/")[1];
9920
+ const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir2], { cwd, stdio: "inherit" });
9622
9921
  if (r.status !== 0) return r.status ?? 1;
9623
- out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
9922
+ out(`Cloned ${name} into ${path23.resolve(cwd, dir2)}
9624
9923
  `);
9625
9924
  return 0;
9626
9925
  }
9627
9926
  async function cmdVerify(args, cwd, out, err) {
9628
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9927
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9629
9928
  const rs = remotes(root);
9630
9929
  const originUrl = rs.get("origin") ?? "";
9631
9930
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -9788,22 +10087,22 @@ __export(runner_exports, {
9788
10087
  setupText: () => setupText
9789
10088
  });
9790
10089
  import { spawnSync as spawnSync3 } from "node:child_process";
9791
- import fs28 from "node:fs";
10090
+ import fs29 from "node:fs";
9792
10091
  import os5 from "node:os";
9793
- import path23 from "node:path";
10092
+ import path24 from "node:path";
9794
10093
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9795
- return path23.join(home, ".bunshin", "runner");
10094
+ return path24.join(home, ".bunshin", "runner");
9796
10095
  }
9797
10096
  function readInstalled(home) {
9798
10097
  try {
9799
- return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
10098
+ return JSON.parse(fs29.readFileSync(path24.join(runnerHome(home), "config.json"), "utf8"));
9800
10099
  } catch {
9801
10100
  return null;
9802
10101
  }
9803
10102
  }
9804
10103
  function connection(args, cwd) {
9805
10104
  const saved = readInstalled();
9806
- const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
10105
+ const store = saved && path24.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9807
10106
  const config = store?.config();
9808
10107
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9809
10108
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9863,9 +10162,9 @@ async function runOnce(c, args, out, err) {
9863
10162
  const [job] = await lease(c, args);
9864
10163
  if (!job) break;
9865
10164
  const started = Date.now();
9866
- const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
10165
+ const scratch = fs29.mkdtempSync(path24.join(os5.tmpdir(), "bunshin-job-"));
9867
10166
  const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
9868
- () => fs28.rmSync(scratch, { recursive: true, force: true })
10167
+ () => fs29.rmSync(scratch, { recursive: true, force: true })
9869
10168
  );
9870
10169
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9871
10170
  if (!res.ok) {
@@ -9955,24 +10254,24 @@ function renderCheck(id, c) {
9955
10254
  }
9956
10255
  function logAnswer(entry) {
9957
10256
  try {
9958
- fs28.mkdirSync(runnerHome(), { recursive: true });
9959
- fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
10257
+ fs29.mkdirSync(runnerHome(), { recursive: true });
10258
+ fs29.appendFileSync(path24.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9960
10259
  `);
9961
10260
  } catch {
9962
10261
  }
9963
10262
  }
9964
10263
  function which(name) {
9965
- for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
9966
- if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
10264
+ for (const d of [...(process.env.PATH || "").split(path24.delimiter), path24.join(os5.homedir(), ".local", "bin")]) {
10265
+ if (d && fs29.existsSync(path24.join(d, name))) return path24.join(d, name);
9967
10266
  }
9968
10267
  return null;
9969
10268
  }
9970
10269
  function scheduledCommand(backend) {
9971
- const node = path23.dirname(process.execPath);
9972
- const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
10270
+ const node = path24.dirname(process.execPath);
10271
+ const npx = fs29.existsSync(path24.join(node, "npx")) ? path24.join(node, "npx") : which("npx") ?? "npx";
9973
10272
  const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
9974
- 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"])];
9975
- return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
10273
+ 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"])];
10274
+ return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path24.delimiter) };
9976
10275
  }
9977
10276
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9978
10277
  const cmd = scheduledCommand(backend);
@@ -9987,8 +10286,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9987
10286
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9988
10287
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9989
10288
  <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
9990
- <key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9991
- <key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
10289
+ <key>StandardOutPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
10290
+ <key>StandardErrorPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
9992
10291
  <key>ProcessType</key><string>Background</string>
9993
10292
  <key>LowPriorityIO</key><true/>
9994
10293
  </dict>
@@ -9999,19 +10298,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9999
10298
  const cmd = scheduledCommand(backend);
10000
10299
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
10001
10300
  const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
10002
- return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
10301
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path24.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
10003
10302
  }
10004
10303
  function launchAgentsDir(opts) {
10005
- return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
10304
+ return opts.launchAgents ?? path24.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
10006
10305
  }
10007
10306
  function installSchedule(backend, everyMinutes, opts = {}) {
10008
10307
  const platform = opts.platform ?? process.platform;
10009
10308
  const run2 = opts.run ?? realRun;
10010
10309
  if (platform === "darwin") {
10011
- const dir = launchAgentsDir(opts);
10012
- const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
10013
- fs28.mkdirSync(dir, { recursive: true });
10014
- fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
10310
+ const dir2 = launchAgentsDir(opts);
10311
+ const plist = path24.join(dir2, `${LAUNCHD_LABEL}.plist`);
10312
+ fs29.mkdirSync(dir2, { recursive: true });
10313
+ fs29.writeFileSync(plist, launchdPlist(backend, everyMinutes));
10015
10314
  const domain = `gui/${process.getuid?.() ?? 501}`;
10016
10315
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
10017
10316
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -10031,10 +10330,10 @@ function removeSchedule(opts = {}) {
10031
10330
  const platform = opts.platform ?? process.platform;
10032
10331
  const run2 = opts.run ?? realRun;
10033
10332
  if (platform === "darwin") {
10034
- const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
10333
+ const plist = path24.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
10035
10334
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
10036
- if (!fs28.existsSync(plist)) return "no schedule";
10037
- fs28.rmSync(plist, { force: true });
10335
+ if (!fs29.existsSync(plist)) return "no schedule";
10336
+ fs29.rmSync(plist, { force: true });
10038
10337
  return `removed ${plist}`;
10039
10338
  }
10040
10339
  const current = run2("crontab", ["-l"]);
@@ -10061,10 +10360,10 @@ async function install(args, cwd, out, err, opts = {}) {
10061
10360
  }
10062
10361
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
10063
10362
  const home = runnerHome();
10064
- fs28.mkdirSync(home, { recursive: true, mode: 448 });
10363
+ fs29.mkdirSync(home, { recursive: true, mode: 448 });
10065
10364
  const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
10066
- fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
10067
- fs28.chmodSync(path23.join(home, "config.json"), 384);
10365
+ fs29.writeFileSync(path24.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
10366
+ fs29.chmodSync(path24.join(home, "config.json"), 384);
10068
10367
  let schedule;
10069
10368
  try {
10070
10369
  schedule = installSchedule(backend, every, opts);
@@ -10077,7 +10376,7 @@ Run it yourself: npx -y ${PKG2} runner --once
10077
10376
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
10078
10377
  `);
10079
10378
  out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
10080
- ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
10379
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path24.join(home, "runner.log")}
10081
10380
  `);
10082
10381
  out(` Stop: npx -y ${PKG2} runner uninstall
10083
10382
  `);
@@ -10094,9 +10393,9 @@ Answering what waits now\u2026
10094
10393
  function uninstall(out, opts = {}) {
10095
10394
  out(`${removeSchedule(opts)}
10096
10395
  `);
10097
- const file = path23.join(runnerHome(), "config.json");
10098
- if (fs28.existsSync(file)) {
10099
- fs28.rmSync(file, { force: true });
10396
+ const file = path24.join(runnerHome(), "config.json");
10397
+ if (fs29.existsSync(file)) {
10398
+ fs29.rmSync(file, { force: true });
10100
10399
  out(`removed ${file} (revoke the key in the console: Agent keys)
10101
10400
  `);
10102
10401
  }
@@ -10276,7 +10575,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10276
10575
  out(text2);
10277
10576
  return 0;
10278
10577
  }
10279
- fs28.writeFileSync(file, text2, { mode: 384 });
10578
+ fs29.writeFileSync(file, text2, { mode: 384 });
10280
10579
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
10281
10580
  out(
10282
10581
  [
@@ -10294,7 +10593,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10294
10593
  const id = args._[2];
10295
10594
  const file = args._[3] ?? "-";
10296
10595
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
10297
- const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
10596
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10298
10597
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
10299
10598
  out(renderCheck(id, r.body));
10300
10599
  return r.body.ok ? 0 : 1;
@@ -10303,7 +10602,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10303
10602
  const id = args._[2];
10304
10603
  const file = args._[3] ?? "-";
10305
10604
  if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
10306
- const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
10605
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10307
10606
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
10308
10607
  if (check?.body && (check.status < 400 || check.body.problems)) {
10309
10608
  out(renderCheck(id, check.body));
@@ -10327,7 +10626,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10327
10626
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
10328
10627
  const entries = (() => {
10329
10628
  try {
10330
- 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);
10629
+ 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);
10331
10630
  } catch {
10332
10631
  return [];
10333
10632
  }
@@ -10397,8 +10696,8 @@ var init_runner = __esm({
10397
10696
 
10398
10697
  // src/v2/importer.ts
10399
10698
  import { execFileSync as execFileSync6 } from "node:child_process";
10400
- import fs29 from "node:fs";
10401
- import path24 from "node:path";
10699
+ import fs30 from "node:fs";
10700
+ import path25 from "node:path";
10402
10701
  function rewriteRepoPaths(text2, repoName2, pack) {
10403
10702
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
10404
10703
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -10433,17 +10732,17 @@ function buildDocs(files, opts) {
10433
10732
  const claimed = /* @__PURE__ */ new Set();
10434
10733
  const all = [...files.keys()].sort();
10435
10734
  const skillDirs = all.map(skillDirOf).filter((d) => !!d);
10436
- for (const dir of skillDirs) {
10437
- const main2 = `${dir}/SKILL.md`;
10735
+ for (const dir2 of skillDirs) {
10736
+ const main2 = `${dir2}/SKILL.md`;
10438
10737
  const text2 = rw(files.get(main2).content);
10439
10738
  const fm = frontmatterDescription(text2);
10440
- const name = path24.posix.basename(dir);
10739
+ const name = path25.posix.basename(dir2);
10441
10740
  const bundle = {};
10442
- for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
10741
+ for (const p of all.filter((x) => x.startsWith(`${dir2}/`))) {
10443
10742
  claimed.add(p);
10444
10743
  if (p === main2) continue;
10445
10744
  const f = files.get(p);
10446
- bundle[p.slice(dir.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10745
+ bundle[p.slice(dir2.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10447
10746
  }
10448
10747
  const description = fm.description.replace(/\s+/g, " ").trim() || firstSentence(text2) || name;
10449
10748
  const { name: _n, description: _d, ...rest } = fm.data;
@@ -10470,9 +10769,9 @@ function buildDocs(files, opts) {
10470
10769
  if (/\.md$/i.test(p) && looksGenerated(files.get(p).content)) claimed.add(p);
10471
10770
  }
10472
10771
  const dirs = new Set(all.filter((x) => x.includes("/") && !claimed.has(x)).map((x) => x.split("/")[0]));
10473
- for (const dir of [...dirs].sort()) {
10474
- if (skillParents.has(dir) && all.filter((x) => x.startsWith(`${dir}/`) && !claimed.has(x)).length === 0) continue;
10475
- const members = all.filter((x) => x.startsWith(`${dir}/`) && !claimed.has(x));
10772
+ for (const dir2 of [...dirs].sort()) {
10773
+ if (skillParents.has(dir2) && all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x)).length === 0) continue;
10774
+ const members = all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x));
10476
10775
  if (!members.length) continue;
10477
10776
  const readme = members.find((x) => /^[^/]+\/README(\.md)?$/i.test(x));
10478
10777
  const bundle = {};
@@ -10483,15 +10782,15 @@ function buildDocs(files, opts) {
10483
10782
  bundle[p] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10484
10783
  }
10485
10784
  const tool = members.some((p) => files.get(p).mode === "x" || SCRIPT.test(p));
10486
- const body = readme ? rw(files.get(readme).content) : `# ${dir}
10785
+ const body = readme ? rw(files.get(readme).content) : `# ${dir2}
10487
10786
 
10488
10787
  ${members.map((p) => `- \`${p}\``).join("\n")}
10489
10788
  `;
10490
10789
  docs.push({
10491
- path: readme ?? `${dir}/#index`,
10790
+ path: readme ?? `${dir2}/#index`,
10492
10791
  kind: tool ? "tool" : "reference",
10493
- title: dir,
10494
- when: readme ? firstSentence(body) || `The ${dir} ${tool ? "tool" : "folder"}` : `The ${dir} ${tool ? "tool" : "folder"} of ${opts.pack}`,
10792
+ title: dir2,
10793
+ when: readme ? firstSentence(body) || `The ${dir2} ${tool ? "tool" : "folder"}` : `The ${dir2} ${tool ? "tool" : "folder"} of ${opts.pack}`,
10495
10794
  body,
10496
10795
  files: bundle
10497
10796
  });
@@ -10534,23 +10833,23 @@ function isGitRepo(root) {
10534
10833
  }
10535
10834
  function workingTreeFiles(root) {
10536
10835
  const out = /* @__PURE__ */ new Map();
10537
- const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
10538
- return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
10836
+ const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir2, base = "") {
10837
+ return fs30.readdirSync(dir2, { withFileTypes: true }).flatMap((e) => {
10539
10838
  if (e.name === ".git" || e.name === "node_modules") return [];
10540
10839
  const rel = base ? `${base}/${e.name}` : e.name;
10541
- return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
10840
+ return e.isDirectory() ? walk(path25.join(dir2, e.name), rel) : e.isFile() ? [rel] : [];
10542
10841
  });
10543
10842
  })(root);
10544
10843
  for (const rel of list2) {
10545
- const abs = path24.join(root, rel);
10844
+ const abs = path25.join(root, rel);
10546
10845
  let buf;
10547
10846
  try {
10548
- buf = fs29.readFileSync(abs);
10847
+ buf = fs30.readFileSync(abs);
10549
10848
  } catch {
10550
10849
  continue;
10551
10850
  }
10552
10851
  if (!isText(buf)) continue;
10553
- const exec = (fs29.statSync(abs).mode & 73) !== 0;
10852
+ const exec = (fs30.statSync(abs).mode & 73) !== 0;
10554
10853
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
10555
10854
  }
10556
10855
  return out;
@@ -10584,7 +10883,7 @@ function diffDocs(prev, next) {
10584
10883
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
10585
10884
  }
10586
10885
  function readSkillSystem(root, opts) {
10587
- const repoName2 = path24.basename(path24.resolve(root));
10886
+ const repoName2 = path25.basename(path25.resolve(root));
10588
10887
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
10589
10888
  const tree = build(workingTreeFiles(root));
10590
10889
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -10661,7 +10960,7 @@ var init_importer = __esm({
10661
10960
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10662
10961
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
10663
10962
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
10664
- isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10963
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path25.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10665
10964
  fingerprint = (d) => JSON.stringify(d);
10666
10965
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
10667
10966
 
@@ -10697,8 +10996,8 @@ __export(skills_exports, {
10697
10996
  cmdSkill: () => cmdSkill,
10698
10997
  parsePin: () => parsePin
10699
10998
  });
10700
- import fs30 from "node:fs";
10701
- import path25 from "node:path";
10999
+ import fs31 from "node:fs";
11000
+ import path26 from "node:path";
10702
11001
  function requireOnline(store, what) {
10703
11002
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
10704
11003
  }
@@ -10707,9 +11006,9 @@ async function cmdImport2(store, args, cwd, out, err) {
10707
11006
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
10708
11007
  return 1;
10709
11008
  }
10710
- const root = path25.resolve(cwd, args._[2]);
10711
- if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
10712
- const pack = str6(args.flags.pack) ?? path25.basename(root);
11009
+ const root = path26.resolve(cwd, args._[2]);
11010
+ if (!fs31.existsSync(root)) throw new Error(`${root} does not exist`);
11011
+ const pack = str6(args.flags.pack) ?? path26.basename(root);
10713
11012
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
10714
11013
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
10715
11014
  const kinds = /* @__PURE__ */ new Map();
@@ -10744,7 +11043,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10744
11043
  const propose = args.flags["propose-rewrites"];
10745
11044
  if (propose) {
10746
11045
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
10747
- await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
11046
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path26.basename(root), out, err);
10748
11047
  }
10749
11048
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10750
11049
  return 0;
@@ -10884,7 +11183,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10884
11183
  }
10885
11184
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10886
11185
  if (graph) {
10887
- fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
11186
+ fs31.writeFileSync(path26.resolve(cwd, graph), renderSkillGraph(report));
10888
11187
  out(`graph written to ${graph}
10889
11188
  `);
10890
11189
  }
@@ -10974,9 +11273,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10974
11273
  `;
10975
11274
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10976
11275
  if (file) {
10977
- const abs = path25.resolve(cwd, file);
10978
- fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10979
- fs30.writeFileSync(abs, line);
11276
+ const abs = path26.resolve(cwd, file);
11277
+ fs31.mkdirSync(path26.dirname(abs), { recursive: true });
11278
+ fs31.writeFileSync(abs, line);
10980
11279
  out(`${file}: ${line}`);
10981
11280
  } else out(line);
10982
11281
  return 0;
@@ -10986,16 +11285,16 @@ async function cmdLibrary(store, args, cwd, out, err) {
10986
11285
  const to = str6(args.flags.to);
10987
11286
  if (!to) throw new Error("--to <dir> is required");
10988
11287
  const pinFile = str6(args.flags.pin);
10989
- const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
11288
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs31.readFileSync(path26.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10990
11289
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10991
11290
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10992
11291
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
10993
- const dir = path25.resolve(cwd, to);
10994
- if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
11292
+ const dir2 = path26.resolve(cwd, to);
11293
+ if (fs31.existsSync(dir2) && fs31.readdirSync(dir2).length && !bool4(args.flags.force)) throw new Error(`${dir2} is not empty (use --force)`);
10995
11294
  const pack = str6(args.flags.pack);
10996
11295
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
10997
- const res = materialize(docs, dir);
10998
- out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir}
11296
+ const res = materialize(docs, dir2);
11297
+ out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir2}
10999
11298
  `);
11000
11299
  return 0;
11001
11300
  }
@@ -11030,27 +11329,27 @@ __export(reflect_exports, {
11030
11329
  buildLesson: () => buildLesson,
11031
11330
  runReflection: () => runReflection
11032
11331
  });
11033
- import fs31 from "node:fs";
11034
- import path26 from "node:path";
11035
- import { createHash as createHash9 } from "node:crypto";
11332
+ import fs32 from "node:fs";
11333
+ import path27 from "node:path";
11334
+ import { createHash as createHash10 } from "node:crypto";
11036
11335
  async function runReflection(store, opts) {
11037
11336
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
11038
- const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
11337
+ const lock = path27.join(store.root, `reflect-${createHash10("sha256").update(opts.sessionId).digest("hex")}.lock`);
11039
11338
  const deadline = Date.now() + 66e4;
11040
11339
  for (; ; ) {
11041
11340
  try {
11042
- fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
11341
+ fs32.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
11043
11342
  break;
11044
11343
  } catch (error) {
11045
11344
  if (error.code !== "EEXIST") throw error;
11046
11345
  try {
11047
- const owner = Number(fs31.readFileSync(lock, "utf8"));
11346
+ const owner = Number(fs32.readFileSync(lock, "utf8"));
11048
11347
  if (Number.isInteger(owner) && owner > 0) {
11049
11348
  try {
11050
11349
  process.kill(owner, 0);
11051
11350
  } catch (error2) {
11052
11351
  if (error2.code === "ESRCH") {
11053
- fs31.unlinkSync(lock);
11352
+ fs32.unlinkSync(lock);
11054
11353
  continue;
11055
11354
  }
11056
11355
  }
@@ -11065,7 +11364,7 @@ async function runReflection(store, opts) {
11065
11364
  return await reflectSession(store, opts);
11066
11365
  } finally {
11067
11366
  try {
11068
- fs31.unlinkSync(lock);
11367
+ fs32.unlinkSync(lock);
11069
11368
  } catch {
11070
11369
  }
11071
11370
  }
@@ -11407,8 +11706,8 @@ __export(cli_exports, {
11407
11706
  parseArgs: () => parseArgs,
11408
11707
  version: () => version
11409
11708
  });
11410
- import fs32 from "node:fs";
11411
- import path27 from "node:path";
11709
+ import fs33 from "node:fs";
11710
+ import path28 from "node:path";
11412
11711
  function parseArgs(argv) {
11413
11712
  const out = { _: [], flags: {} };
11414
11713
  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"]);
@@ -11508,7 +11807,7 @@ async function main(argv, opts = {}) {
11508
11807
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
11509
11808
  const agent = harness === "claude-code" ? "claude" : harness;
11510
11809
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
11511
- const prior = new Store(path27.join(cwd, ".bunshin"));
11810
+ const prior = new Store(path28.join(cwd, ".bunshin"));
11512
11811
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
11513
11812
  const apiUrl = str7(args.flags["api-url"]);
11514
11813
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -11537,7 +11836,7 @@ async function main(argv, opts = {}) {
11537
11836
  });
11538
11837
  if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
11539
11838
  const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
11540
- await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
11839
+ await askSources2(new Store(path28.join(cwd, ".bunshin")), out);
11541
11840
  }
11542
11841
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
11543
11842
  `);
@@ -11574,7 +11873,7 @@ Key saved privately outside the project.
11574
11873
  if (cmd === "mcp") {
11575
11874
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
11576
11875
  const root = str7(args.flags.root);
11577
- await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
11876
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path28.resolve(root)) } : {}, version: version() });
11578
11877
  return 0;
11579
11878
  }
11580
11879
  if (cmd === "demo") {
@@ -11637,6 +11936,14 @@ Key saved privately outside the project.
11637
11936
  case "meta-reflect":
11638
11937
  case "meta":
11639
11938
  return await cmdMetaReflect(store, args, cwd, out);
11939
+ case "route": {
11940
+ const { routeJob: routeJob2, sweepRouter: sweepRouter2 } = await Promise.resolve().then(() => (init_route(), route_exports));
11941
+ const answer = await routeJob2(store, str7(args.flags.session) ?? "", Number(args.flags.seq));
11942
+ sweepRouter2(new (await Promise.resolve().then(() => (init_store2(), store_exports))).V2Store(store));
11943
+ if (!bool5(args.flags.quiet)) out(`${JSON.stringify(answer)}
11944
+ `);
11945
+ return answer && !answer.error ? 0 : 1;
11946
+ }
11640
11947
  case "update-check": {
11641
11948
  const latest = await (await Promise.resolve().then(() => (init_update(), update_exports))).checkForUpdate();
11642
11949
  if (!bool5(args.flags.quiet)) out(latest ? `latest ${latest} (this is ${version()})
@@ -11678,7 +11985,7 @@ ${HELP2}`);
11678
11985
  function openStore(args, cwd) {
11679
11986
  const root = str7(args.flags.root);
11680
11987
  if (root) {
11681
- const store = new Store(path27.resolve(root));
11988
+ const store = new Store(path28.resolve(root));
11682
11989
  store.ensureLayout();
11683
11990
  return store;
11684
11991
  }
@@ -11694,7 +12001,7 @@ async function cmdInit(args, cwd, out, err) {
11694
12001
  orgId: str7(args.flags["org-id"]),
11695
12002
  userId: str7(args.flags["user-id"]),
11696
12003
  apiUrl: str7(args.flags["api-url"]),
11697
- apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
12004
+ apiKey: bool5(args.flags["key-stdin"]) ? fs33.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
11698
12005
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
11699
12006
  harness,
11700
12007
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -11737,10 +12044,10 @@ function cmdStatus(store, args, out, v2) {
11737
12044
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
11738
12045
  const metrics = store.readMetrics(void 0);
11739
12046
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
11740
- 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")] : [];
12047
+ 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")] : [];
11741
12048
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
11742
12049
  try {
11743
- return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
12050
+ return fs33.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11744
12051
  } catch {
11745
12052
  return false;
11746
12053
  }
@@ -11758,7 +12065,7 @@ function cmdStatus(store, args, out, v2) {
11758
12065
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
11759
12066
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
11760
12067
  hooks: { [config.harness]: hooks },
11761
- hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
12068
+ hookFiles: hookFiles.filter((file) => fs33.existsSync(file)),
11762
12069
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
11763
12070
  v2
11764
12071
  };
@@ -11801,7 +12108,7 @@ function cmdStatus(store, args, out, v2) {
11801
12108
  }
11802
12109
  function hookCount(file) {
11803
12110
  try {
11804
- const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
12111
+ const raw = JSON.parse(fs33.readFileSync(file, "utf8"));
11805
12112
  let n = 0;
11806
12113
  for (const groups of Object.values(raw.hooks ?? {})) {
11807
12114
  for (const g of groups ?? []) {
@@ -11921,7 +12228,7 @@ function cmdLessonAdd(store, args, out, err) {
11921
12228
  let content = str7(args.flags.content) ?? "";
11922
12229
  if (!content || content === "-" || args._.includes("-")) {
11923
12230
  try {
11924
- content = fs32.readFileSync(0, "utf8");
12231
+ content = fs33.readFileSync(0, "utf8");
11925
12232
  } catch {
11926
12233
  }
11927
12234
  }
@@ -11987,7 +12294,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11987
12294
  }
11988
12295
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11989
12296
  const result = await runReflection2(store, {
11990
- sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
12297
+ sessionId: str7(args.flags.session) ?? path28.basename(transcript).replace(/\.jsonl?$/, ""),
11991
12298
  transcriptPath: transcript,
11992
12299
  cwd,
11993
12300
  force: bool5(args.flags.force)
@@ -12190,7 +12497,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
12190
12497
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
12191
12498
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
12192
12499
  `;
12193
- 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.`;
12500
+ 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.`;
12194
12501
  }
12195
12502
  });
12196
12503