@bunshin-cc/bunshin 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +709 -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,202 @@ 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
+ v2.log("v2-router", { session: sessionId, seq, serve: answer.serve ?? null, rules: answer.rules ?? [], hint: answer.hint ?? [], ms: answer.ms, modelMs: answer.modelMs, error: answer.error, dense: job.dense?.serve ?? null, usage: answer.usage });
6643
+ return answer;
6644
+ };
6645
+ try {
6646
+ const session = v2.readSession(sessionId);
6647
+ if (!session?.lastPrompt) return write({ error: "no prompt in the session file" });
6648
+ if (!canCall(config)) return write({ error: "offline: the router's index comes from the server" });
6649
+ const index = await routerIndex(store, v2, session.source ?? "");
6650
+ const systemFile = path11.join(dir(v2), `system-${index.hash}.txt`);
6651
+ if (!fs16.existsSync(systemFile)) fs16.writeFileSync(systemFile, index.system);
6652
+ const cwd = path11.join(dir(v2), "cwd");
6653
+ fs16.mkdirSync(cwd, { recursive: true });
6654
+ const inContext2 = session.inContext ?? [...session.loaded, ...session.pushed];
6655
+ const titles = new Map((v2.readBundle()?.sections ?? []).map((s) => [s.id, s.skill?.name ?? s.title]));
6656
+ const message = routerMessage(session.lastPrompt, promptContext(session), inContext2.map((id) => titles.get(id)).filter((t) => !!t));
6657
+ const res = await askClaude(systemFile, message, config.recall.routerModel || process.env.BUNSHIN_ROUTER_MODEL || ROUTER_MODEL, cwd);
6658
+ const u = res.usage ?? {};
6659
+ const usage = { input: u.input_tokens ?? 0, cacheRead: u.cache_read_input_tokens ?? 0, cacheWrite: u.cache_creation_input_tokens ?? 0, output: u.output_tokens ?? 0, ...u.output_tokens_details?.thinking_tokens ? { thinking: u.output_tokens_details.thinking_tokens } : {} };
6660
+ const meta = { ...res.duration_ms ? { modelMs: res.duration_ms } : {}, usage };
6661
+ if (res.is_error) return write({ ...meta, error: `claude: ${(res.result ?? "error").slice(0, 300)}` });
6662
+ const picks = parseRouterAnswer(res.result ?? "", index.items, inContext2);
6663
+ if (!picks) return write({ ...meta, error: `not JSON: ${(res.result ?? "").slice(0, 120)}` });
6664
+ return write({ ...picks, ...meta });
6665
+ } catch (err) {
6666
+ return write({ error: (err instanceof Error ? err.message : String(err)).slice(0, 300) });
6667
+ }
6668
+ }
6669
+ function sweepRouter(v2, now = Date.now()) {
6670
+ try {
6671
+ for (const f of fs16.readdirSync(dir(v2))) {
6672
+ if (!/\.(job|answer)\.json$/.test(f)) continue;
6673
+ const p = path11.join(dir(v2), f);
6674
+ if (now - fs16.statSync(p).mtimeMs > 864e5) fs16.rmSync(p, { force: true });
6675
+ }
6676
+ } catch {
6677
+ }
6678
+ }
6679
+ var ROUTER_MODEL, INDEX_TTL_MS, CLAUDE_TIMEOUT_MS, dir, safe2, jobFile, answerFile, readJob, writeJob, readAnswer;
6680
+ var init_route = __esm({
6681
+ "src/v2/route.ts"() {
6682
+ "use strict";
6683
+ init_v22();
6684
+ init_llm();
6685
+ init_redact2();
6686
+ init_util();
6687
+ init_api();
6688
+ init_store2();
6689
+ ROUTER_MODEL = "claude-haiku-5-5";
6690
+ INDEX_TTL_MS = 10 * 6e4;
6691
+ CLAUDE_TIMEOUT_MS = 6e4;
6692
+ dir = (v2) => path11.join(v2.dir, "router");
6693
+ safe2 = (id) => id.replace(/[^\w.-]/g, "_");
6694
+ jobFile = (v2, session) => path11.join(dir(v2), `${safe2(session)}.job.json`);
6695
+ answerFile = (v2, session, seq) => path11.join(dir(v2), `${safe2(session)}.${seq}.answer.json`);
6696
+ readJob = (v2, session) => readJson2(jobFile(v2, session));
6697
+ writeJob = (v2, session, job) => writeJsonAtomic(jobFile(v2, session), job);
6698
+ readAnswer = (v2, session, seq) => readJson2(answerFile(v2, session, seq));
6699
+ }
6700
+ });
6701
+
6450
6702
  // src/v2/uicmd.ts
6451
6703
  var uicmd_exports = {};
6452
6704
  __export(uicmd_exports, {
@@ -6459,51 +6711,51 @@ __export(uicmd_exports, {
6459
6711
  modSource: () => modSource
6460
6712
  });
6461
6713
  import { spawnSync } from "node:child_process";
6462
- import fs16 from "node:fs";
6714
+ import fs17 from "node:fs";
6463
6715
  import os3 from "node:os";
6464
- import path11 from "node:path";
6716
+ import path12 from "node:path";
6465
6717
  import { fileURLToPath } from "node:url";
6466
6718
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
6467
- return path11.join(home, ".bunshin", "claude-code-ui");
6719
+ return path12.join(home, ".bunshin", "claude-code-ui");
6468
6720
  }
6469
6721
  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;
6722
+ const here = path12.dirname(fileURLToPath(import.meta.url));
6723
+ for (const dir2 of [path12.join(here, "claude-code-ui"), path12.join(here, "..", "claude-code-ui"), path12.resolve(here, "../../../../adapters/claude-code-ui")]) {
6724
+ if (fs17.existsSync(path12.join(dir2, ".claude-plugin", "plugin.json"))) return dir2;
6473
6725
  }
6474
6726
  return null;
6475
6727
  }
6476
6728
  function installedModVersion(home) {
6477
6729
  try {
6478
- return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6730
+ return JSON.parse(fs17.readFileSync(path12.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6479
6731
  } catch {
6480
6732
  return null;
6481
6733
  }
6482
6734
  }
6483
6735
  function copyDir(from, to) {
6484
- fs16.mkdirSync(to, { recursive: true });
6485
- for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
6736
+ fs17.mkdirSync(to, { recursive: true });
6737
+ for (const e of fs17.readdirSync(from, { withFileTypes: true })) {
6486
6738
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
6487
6739
  if (e.name === "node_modules") continue;
6488
- const a = path11.join(from, e.name);
6489
- const b = path11.join(to, e.name);
6740
+ const a = path12.join(from, e.name);
6741
+ const b = path12.join(to, e.name);
6490
6742
  if (e.isDirectory()) copyDir(a, b);
6491
- else fs16.copyFileSync(a, b);
6743
+ else fs17.copyFileSync(a, b);
6492
6744
  }
6493
6745
  }
6494
6746
  function installMod(version2, opts = {}) {
6495
6747
  const src = modSource();
6496
6748
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
6497
6749
  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"),
6750
+ const dir2 = path12.join(home, MOD_PLUGIN);
6751
+ fs17.rmSync(dir2, { recursive: true, force: true });
6752
+ copyDir(src, dir2);
6753
+ const manifest = path12.join(dir2, ".claude-plugin", "plugin.json");
6754
+ const m = JSON.parse(fs17.readFileSync(manifest, "utf8"));
6755
+ fs17.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6756
+ fs17.mkdirSync(path12.join(home, ".claude-plugin"), { recursive: true });
6757
+ fs17.writeFileSync(
6758
+ path12.join(home, ".claude-plugin", "marketplace.json"),
6507
6759
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6508
6760
  );
6509
6761
  const run2 = opts.run ?? spawnSync;
@@ -6517,7 +6769,7 @@ function installMod(version2, opts = {}) {
6517
6769
  };
6518
6770
  if (!step(["plugin", "marketplace", "add", home])) step(["plugin", "marketplace", "update", MOD_MARKETPLACE]);
6519
6771
  if (!step(["plugin", "install", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`, "--scope", "user"])) step(["plugin", "update", `${MOD_PLUGIN}@${MOD_MARKETPLACE}`]);
6520
- return { dir, version: version2, steps };
6772
+ return { dir: dir2, version: version2, steps };
6521
6773
  }
6522
6774
  async function cmdUi(store, args, out, err, version2) {
6523
6775
  const sub = args._[1];
@@ -6538,7 +6790,7 @@ async function cmdUi(store, args, out, err, version2) {
6538
6790
  `);
6539
6791
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6540
6792
  `);
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");
6793
+ out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path12.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6542
6794
  }
6543
6795
  return installed || !failed.length ? 0 : 1;
6544
6796
  }
@@ -6619,19 +6871,19 @@ __export(update_exports, {
6619
6871
  updateCheckDue: () => updateCheckDue,
6620
6872
  updateNotice: () => updateNotice
6621
6873
  });
6622
- import fs17 from "node:fs";
6623
- import path12 from "node:path";
6874
+ import fs18 from "node:fs";
6875
+ import path13 from "node:path";
6624
6876
  function readState2() {
6625
6877
  try {
6626
- return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6878
+ return JSON.parse(fs18.readFileSync(stateFile(), "utf8"));
6627
6879
  } catch {
6628
6880
  return {};
6629
6881
  }
6630
6882
  }
6631
6883
  function writeState(s) {
6632
6884
  try {
6633
- fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
6634
- fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6885
+ fs18.mkdirSync(path13.dirname(stateFile()), { recursive: true });
6886
+ fs18.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6635
6887
  } catch {
6636
6888
  }
6637
6889
  }
@@ -6681,14 +6933,14 @@ var init_update = __esm({
6681
6933
  PKG = "@bunshin-cc/bunshin";
6682
6934
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6683
6935
  DAY = 864e5;
6684
- stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6936
+ stateFile = () => path13.join(homeDir(), ".bunshin", "update.json");
6685
6937
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6686
6938
  }
6687
6939
  });
6688
6940
 
6689
6941
  // src/v2/hooks.ts
6690
- import fs18 from "node:fs";
6691
- import path13 from "node:path";
6942
+ import fs19 from "node:fs";
6943
+ import path14 from "node:path";
6692
6944
  function inContext(s) {
6693
6945
  return s.inContext ?? [...s.loaded, ...s.pushed];
6694
6946
  }
@@ -6755,10 +7007,10 @@ async function keepModCurrent(ctx, config) {
6755
7007
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6756
7008
  const current = version2();
6757
7009
  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());
7010
+ const marker = path14.join(modHome2(), `.attempted-${current}`);
7011
+ if (fs19.existsSync(marker)) return;
7012
+ fs19.mkdirSync(modHome2(), { recursive: true });
7013
+ fs19.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6762
7014
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6763
7015
  } catch {
6764
7016
  }
@@ -6887,35 +7139,6 @@ function skillHint(sec, others) {
6887
7139
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6888
7140
  return `Bunshin: the \`${sec.skill.name}\` skill may apply: ${sec.skill.description.replace(/\s+/g, " ").trim()} If it does, invoke it (Skill tool, or \`/${sec.skill.name}\`) before doing this by hand.${also}`;
6889
7141
  }
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
7142
  async function remoteAuto(ctx, prompt, exclude, source, context) {
6920
7143
  const config = ctx.store.config();
6921
7144
  if (!canCall(config) || !config.recall.useMothership) return null;
@@ -6939,6 +7162,14 @@ function invokedSkill(prompt, sections) {
6939
7162
  const name = m[1].split(":").pop().toLowerCase();
6940
7163
  return sections.find((s) => s.skill && s.skill.name.toLowerCase() === name);
6941
7164
  }
7165
+ function startRoute(v2, ctx, sessionId, now) {
7166
+ const prev = readJob(v2, sessionId);
7167
+ if (prev && !prev.delivered && !prev.cancelled) v2.log("v2-router-undelivered", { session: sessionId, seq: prev.seq, answered: !!readAnswer(v2, sessionId, prev.seq) });
7168
+ const job = { seq: (prev?.seq ?? 0) + 1, at: now };
7169
+ writeJob(v2, sessionId, job);
7170
+ ctx.detach(["--root", ctx.store.root, "route", "--session", sessionId, "--seq", String(job.seq), "--quiet"], { cwd: ctx.cwd });
7171
+ return job;
7172
+ }
6942
7173
  async function onPrompt(ctx) {
6943
7174
  const prompt = ctx.input.prompt ?? "";
6944
7175
  if (prompt.trimStart().startsWith(REFLECT_MARKER)) return {};
@@ -6960,7 +7191,9 @@ async function onPrompt(ctx) {
6960
7191
  const config = ctx.store.config();
6961
7192
  const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6962
7193
  const injected = inContext(session);
7194
+ const job = config.recall.router === "claude-cli" && canCall(config) ? startRoute(v2, ctx, session.sessionId, now) : void 0;
6963
7195
  const named = invokedSkill(prompt, view.sections);
7196
+ if (named && job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6964
7197
  if (named) {
6965
7198
  const why = `you ran /${named.skill.name}`;
6966
7199
  const deployed = deployEnabled(config, view.ctx);
@@ -6987,6 +7220,12 @@ async function onPrompt(ctx) {
6987
7220
  if (reply) context.reply = reply;
6988
7221
  const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
6989
7222
  const local = (id) => view.sections.find((s) => s.id === id);
7223
+ if (job && !remote?.router) {
7224
+ const dense = remote ? { serve: remote.section?.id, hint: (remote.hint ?? []).map((h) => h.id), reason: remote.reason } : { serve: res.section?.id, hint: res.hint.map((h) => h.id), reason: res.reason };
7225
+ writeJob(v2, session.sessionId, { ...job, dense: { ...dense, hint: dense.hint.slice(0, 3) } });
7226
+ return withLive({});
7227
+ }
7228
+ if (job) writeJob(v2, session.sessionId, { ...job, cancelled: true });
6990
7229
  if (remote) {
6991
7230
  const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
6992
7231
  if (remote.section) {
@@ -7059,15 +7298,69 @@ function toolPaths(input) {
7059
7298
  }
7060
7299
  return [...new Set(out)];
7061
7300
  }
7062
- function onPreToolUse(ctx) {
7301
+ async function routedText(v2, ctx, session) {
7302
+ const config = ctx.store.config();
7303
+ if (config.recall.router !== "claude-cli") return "";
7304
+ const job = readJob(v2, session.sessionId);
7305
+ if (!job || job.delivered || job.cancelled) return "";
7306
+ const answer = readAnswer(v2, session.sessionId, job.seq);
7307
+ if (!answer) return "";
7308
+ const fallback = !!answer.error;
7309
+ writeJob(v2, session.sessionId, { ...job, delivered: { at: (/* @__PURE__ */ new Date()).toISOString(), ...ctx.input.tool_name ? { tool: ctx.input.tool_name } : {}, ...fallback ? { fallback } : {} } });
7310
+ const seen = new Set(inContext(session));
7311
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
7312
+ const find = async (id) => {
7313
+ const here = view.sections.find((s) => s.id === id);
7314
+ if (here || !canCall(config)) return here;
7315
+ try {
7316
+ return (await v2api(config, "GET", `/sections/${encodeURIComponent(id)}`, void 0, { timeoutMs: Math.max(300, Math.min(2e3, ctx.deadline - Date.now() - 300)) })).section;
7317
+ } catch {
7318
+ return void 0;
7319
+ }
7320
+ };
7321
+ const serveId = fallback ? job.dense?.serve : answer.serve;
7322
+ const fresh = (ids2) => [...new Set(ids2.filter((id) => !!id && !seen.has(id)))];
7323
+ const [doc] = await Promise.all(fresh([serveId]).map(find));
7324
+ const rules = (await Promise.all(fresh(fallback ? [] : answer.rules ?? []).filter((id) => id !== doc?.id).map(find))).filter((s) => !!s);
7325
+ const hints = fresh(fallback ? job.dense?.hint ?? [] : answer.hint ?? []).filter((id) => id !== doc?.id && !session.hinted?.includes(id));
7326
+ const hintTitles = hints.map((id) => view.sections.find((s) => s.id === id)?.title).filter((t) => !!t);
7327
+ v2.log("v2-router-delivered", { session: session.sessionId, seq: job.seq, tool: ctx.input.tool_name, afterMs: Date.now() - Date.parse(job.at), serve: doc?.id ?? null, rules: rules.map((r) => r.id), hint: hints, fallback, ...answer.error ? { error: answer.error } : {} });
7328
+ const ids = [...doc ? [doc.id] : [], ...rules.map((r) => r.id)];
7329
+ if (!ids.length && !hintTitles.length) return "";
7330
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
7331
+ addUnique(s.served, ...ids, ...hints);
7332
+ addUnique(s.loaded, ...ids);
7333
+ markInContext(s, ...ids);
7334
+ s.hinted = s.hinted ?? [];
7335
+ addUnique(s.hinted, ...hints);
7336
+ });
7337
+ const why = fallback ? "your request matches it" : "the router picked it for the user's latest request";
7338
+ updateFeed(v2, session.sessionId, (f) => {
7339
+ for (const r of [...rules].reverse()) addServed(f, { id: r.id, title: r.title, kind: r.kind, how: "auto", why: "a lesson learned before applies here" });
7340
+ if (doc) addServed(f, { id: doc.id, title: doc.title, kind: doc.kind, ...doc.skill ? { skill: doc.skill.name } : {}, how: "auto", why });
7341
+ if (doc) addEvent(f, "served", `served ${label({ ...doc.skill ? { skill: doc.skill.name } : {}, title: doc.title, kind: doc.kind })} \u2014 ${fallback ? "dense (router failed)" : "router"}`);
7342
+ if (rules.length) addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
7343
+ });
7344
+ return [
7345
+ doc ? renderSection(doc, why, { deployed: !!doc.skill && deployEnabled(config, view.ctx) }) : "",
7346
+ ...rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })),
7347
+ hintTitles.length ? hintLine(hintTitles, config.harness) : ""
7348
+ ].filter(Boolean).join("\n\n");
7349
+ }
7350
+ async function onPreToolUse(ctx) {
7063
7351
  const v2 = new V2Store(ctx.store);
7064
7352
  const session = touch(v2, ctx, (s) => {
7065
7353
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
7066
7354
  });
7067
7355
  const live = liveUpdate(v2, ctx, session);
7356
+ let routed = "";
7357
+ try {
7358
+ routed = await routedText(v2, ctx, session);
7359
+ } catch {
7360
+ }
7068
7361
  const withLive = (out) => {
7069
- if (!live) return out;
7070
- return additional("PreToolUse", [live, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n"));
7362
+ const text3 = [live, routed, out.hookSpecificOutput?.additionalContext ?? ""].filter(Boolean).join("\n\n");
7363
+ return text3 === (out.hookSpecificOutput?.additionalContext ?? "") ? out : additional("PreToolUse", text3);
7071
7364
  };
7072
7365
  const raw = toolPaths(ctx.input.tool_input);
7073
7366
  if (!raw.length) return withLive({});
@@ -7076,7 +7369,7 @@ function onPreToolUse(ctx) {
7076
7369
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
7077
7370
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
7078
7371
  if (!paths.length && !text2) return withLive({});
7079
- const seen = new Set(inContext(session));
7372
+ const seen = new Set(inContext(v2.readSession(session.sessionId) ?? session));
7080
7373
  const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
7081
7374
  if (!hits.length) return withLive({});
7082
7375
  const ids = hits.map((h) => h.section.id);
@@ -7327,8 +7620,10 @@ var init_hooks = __esm({
7327
7620
  init_signals();
7328
7621
  init_store2();
7329
7622
  init_ui();
7623
+ init_route();
7330
7624
  init_source();
7331
7625
  init_view();
7626
+ init_route();
7332
7627
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
7333
7628
  NUDGE_INTERVAL_MS = 60 * 6e4;
7334
7629
  CHECKPOINT_MS = 30 * 6e4;
@@ -7342,8 +7637,8 @@ var init_hooks = __esm({
7342
7637
  });
7343
7638
 
7344
7639
  // src/hooks.ts
7345
- import fs19 from "node:fs";
7346
- import path14 from "node:path";
7640
+ import fs20 from "node:fs";
7641
+ import path15 from "node:path";
7347
7642
  function normalizeEvent(raw) {
7348
7643
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
7349
7644
  }
@@ -7411,19 +7706,19 @@ async function handleHook(rawEvent, input, opts = {}) {
7411
7706
  }
7412
7707
  function deferToProject(cwd, env = process.env) {
7413
7708
  if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
7414
- let dir = path14.resolve(cwd);
7709
+ let dir2 = path15.resolve(cwd);
7415
7710
  for (let i = 0; i < 40; i++) {
7416
7711
  for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
7417
7712
  try {
7418
- const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
7713
+ const text2 = fs20.readFileSync(path15.join(dir2, f), "utf8");
7419
7714
  if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
7420
7715
  } catch {
7421
7716
  }
7422
7717
  }
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;
7718
+ if (fs20.existsSync(path15.join(dir2, ".git"))) return false;
7719
+ const up = path15.dirname(dir2);
7720
+ if (up === dir2) return false;
7721
+ dir2 = up;
7427
7722
  }
7428
7723
  return false;
7429
7724
  }
@@ -7469,11 +7764,11 @@ var init_hooks2 = __esm({
7469
7764
  });
7470
7765
 
7471
7766
  // src/project-hooks.ts
7472
- import fs20 from "node:fs";
7473
- import path15 from "node:path";
7767
+ import fs21 from "node:fs";
7768
+ import path16 from "node:path";
7474
7769
  import { execFileSync as execFileSync4 } from "node:child_process";
7475
7770
  function codexProjectHooks(cwd) {
7476
- const legacyFile = path15.join(cwd, ".codex", "hooks.json");
7771
+ const legacyFile = path16.join(cwd, ".codex", "hooks.json");
7477
7772
  const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
7478
7773
  encoding: "utf8",
7479
7774
  stdio: ["ignore", "pipe", "ignore"],
@@ -7487,14 +7782,14 @@ function codexProjectHooks(cwd) {
7487
7782
  } catch {
7488
7783
  return { file: legacyFile, shared: false, legacyFile };
7489
7784
  }
7490
- const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
7785
+ const linked = fs21.realpathSync(common) !== fs21.realpathSync(directory);
7491
7786
  if (linked) {
7492
- if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
7787
+ if (path16.basename(common) !== ".git" || !fs21.statSync(common).isDirectory()) {
7493
7788
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
7494
7789
  }
7495
- root = path15.dirname(common);
7790
+ root = path16.dirname(common);
7496
7791
  }
7497
- const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
7792
+ const file = fs21.realpathSync(root) === fs21.realpathSync(cwd) ? legacyFile : path16.join(root, ".codex", "hooks.json");
7498
7793
  return { file, shared: linked, legacyFile };
7499
7794
  }
7500
7795
  var init_project_hooks = __esm({
@@ -7506,6 +7801,7 @@ var init_project_hooks = __esm({
7506
7801
  // src/init.ts
7507
7802
  var init_exports = {};
7508
7803
  __export(init_exports, {
7804
+ ALL_TOOLS_MATCHER: () => ALL_TOOLS_MATCHER,
7509
7805
  BORROW_COMMAND: () => BORROW_COMMAND,
7510
7806
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
7511
7807
  HOOK_EVENTS: () => HOOK_EVENTS,
@@ -7527,8 +7823,8 @@ __export(init_exports, {
7527
7823
  runInit: () => runInit
7528
7824
  });
7529
7825
  import { execFileSync as execFileSync5 } from "node:child_process";
7530
- import fs21 from "node:fs";
7531
- import path16 from "node:path";
7826
+ import fs22 from "node:fs";
7827
+ import path17 from "node:path";
7532
7828
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
7533
7829
  function isBunshinEntry(entry) {
7534
7830
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -7553,7 +7849,7 @@ function removeBunshinHooks(settings) {
7553
7849
  else delete out.hooks;
7554
7850
  return out;
7555
7851
  }
7556
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
7852
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false, allTools = false } = {}) {
7557
7853
  const base = removeBunshinHooks(settings);
7558
7854
  const hooks = { ...base.hooks ?? {} };
7559
7855
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
@@ -7561,7 +7857,8 @@ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code
7561
7857
  const hook = bunshinHookEntry(arg, timeout, bin);
7562
7858
  hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
7563
7859
  if (harness === "codex") delete hook._bunshin;
7564
- groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
7860
+ const m = matcher && allTools && event === "PreToolUse" ? ALL_TOOLS_MATCHER : matcher;
7861
+ groups.push(m ? { matcher: m, hooks: [hook] } : { hooks: [hook] });
7565
7862
  hooks[event] = groups;
7566
7863
  }
7567
7864
  return { ...base, hooks };
@@ -7573,16 +7870,16 @@ function countBunshinHooks(settings) {
7573
7870
  );
7574
7871
  }
7575
7872
  function claudeSettingsPath() {
7576
- return path16.join(homeDir(), ".claude", "settings.json");
7873
+ return path17.join(homeDir(), ".claude", "settings.json");
7577
7874
  }
7578
7875
  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");
7876
+ const file = scope === "global" ? path17.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path17.join(cwd, ".pi/extensions/bunshin.js");
7580
7877
  const marker = "// Managed by Bunshin setup.";
7581
7878
  try {
7582
- const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
7879
+ const previous = fileExists(file) ? fs22.readFileSync(file, "utf8") : "";
7583
7880
  if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
7584
7881
  if (remove) {
7585
- if (previous) fs21.unlinkSync(file);
7882
+ if (previous) fs22.unlinkSync(file);
7586
7883
  return { file, installed: 0, changed: !!previous };
7587
7884
  }
7588
7885
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -7591,24 +7888,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
7591
7888
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
7592
7889
  `;
7593
7890
  if (text2 !== previous) {
7594
- ensureDir(path16.dirname(file));
7595
- fs21.writeFileSync(file, text2);
7891
+ ensureDir(path17.dirname(file));
7892
+ fs22.writeFileSync(file, text2);
7596
7893
  }
7597
7894
  return { file, installed: 1, changed: text2 !== previous };
7598
7895
  } catch (error) {
7599
7896
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
7600
7897
  }
7601
7898
  }
7602
- function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
7899
+ function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false, allTools = false } = {}) {
7603
7900
  try {
7604
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7605
- const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
7901
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7902
+ const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user, allTools });
7606
7903
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7607
7904
  if (changed) {
7608
- ensureDir(path16.dirname(file));
7905
+ ensureDir(path17.dirname(file));
7609
7906
  if (fileExists(file)) {
7610
7907
  try {
7611
- fs21.copyFileSync(file, `${file}.bunshin-backup`);
7908
+ fs22.copyFileSync(file, `${file}.bunshin-backup`);
7612
7909
  } catch {
7613
7910
  }
7614
7911
  }
@@ -7623,23 +7920,23 @@ function mcpLauncher(bin) {
7623
7920
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7624
7921
  let script = process.argv[1] ?? "";
7625
7922
  try {
7626
- script = fs21.realpathSync(script);
7923
+ script = fs22.realpathSync(script);
7627
7924
  } catch {
7628
7925
  }
7629
7926
  return { command: process.execPath, args: [script, "mcp"] };
7630
7927
  }
7631
7928
  function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7632
7929
  try {
7633
- const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7930
+ const current = fileExists(file) ? fs22.readFileSync(file, "utf8") : null;
7634
7931
  const ours = current === null || current === content;
7635
7932
  if (remove) {
7636
- if (current !== null && ours) fs21.unlinkSync(file);
7933
+ if (current !== null && ours) fs22.unlinkSync(file);
7637
7934
  return { file, installed: 0, changed: current !== null && ours };
7638
7935
  }
7639
- if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
7936
+ if (!ours) return { file, installed: 0, changed: false, error: `a different ${path17.basename(file)} is already there; left as it is` };
7640
7937
  if (current === null) {
7641
- fs21.mkdirSync(path16.dirname(file), { recursive: true });
7642
- fs21.writeFileSync(file, content);
7938
+ fs22.mkdirSync(path17.dirname(file), { recursive: true });
7939
+ fs22.writeFileSync(file, content);
7643
7940
  }
7644
7941
  return { file, installed: 1, changed: current === null };
7645
7942
  } catch (err) {
@@ -7648,7 +7945,7 @@ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND })
7648
7945
  }
7649
7946
  function installMcpConfig({ file, launcher, remove = false }) {
7650
7947
  try {
7651
- const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7948
+ const before = fileExists(file) ? JSON.parse(fs22.readFileSync(file, "utf8")) : {};
7652
7949
  const servers = { ...before.mcpServers ?? {} };
7653
7950
  if (remove) delete servers.bunshin;
7654
7951
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7656,7 +7953,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7656
7953
  if (!Object.keys(servers).length) delete after.mcpServers;
7657
7954
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7658
7955
  if (changed) {
7659
- if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7956
+ if (!Object.keys(after).length && fileExists(file)) fs22.unlinkSync(file);
7660
7957
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7661
7958
  }
7662
7959
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7665,7 +7962,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7665
7962
  }
7666
7963
  }
7667
7964
  function installUserMcp({ launcher, remove = false, claude = "claude" }) {
7668
- const file = path16.join(homeDir(), ".claude.json");
7965
+ const file = path17.join(homeDir(), ".claude.json");
7669
7966
  const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
7670
7967
  try {
7671
7968
  let had = false;
@@ -7691,22 +7988,22 @@ function runInit(opts = {}) {
7691
7988
  const cwd = opts.cwd ?? process.cwd();
7692
7989
  const warnings = [];
7693
7990
  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) {
7991
+ const root = scope === "global" ? path17.join(homeDir(), ".bunshin") : path17.join(cwd, ".bunshin");
7992
+ if (fs22.existsSync(root) && !new Store(root).trusted && fs22.readdirSync(root).length) {
7696
7993
  const aside = `${root}.untrusted-${Date.now()}`;
7697
- fs21.renameSync(root, aside);
7994
+ fs22.renameSync(root, aside);
7698
7995
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7699
7996
  }
7700
- const existed = fileExists(path16.join(root, "config.json"));
7997
+ const existed = fileExists(path17.join(root, "config.json"));
7701
7998
  const store = new Store(root);
7702
7999
  store.ensureLayout();
7703
- fs21.chmodSync(root, 448);
8000
+ fs22.chmodSync(root, 448);
7704
8001
  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") : "";
8002
+ if (scope === "repo" && (hookScope === "project" || fileExists(path17.join(cwd, ".git")))) {
8003
+ const ignore = path17.join(cwd, ".gitignore");
8004
+ const current = fileExists(ignore) ? fs22.readFileSync(ignore, "utf8") : "";
7708
8005
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7709
- fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
8006
+ fs22.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7710
8007
  `);
7711
8008
  }
7712
8009
  }
@@ -7736,17 +8033,17 @@ function runInit(opts = {}) {
7736
8033
  }
7737
8034
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7738
8035
  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 });
8036
+ const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "hooks.json") : hookScope === "project" ? path17.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
8037
+ hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile, allTools: config.recall.router === "claude-cli" });
7741
8038
  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 });
8039
+ const local = path17.join(cwd, ".claude", "settings.local.json");
8040
+ if (fileExists(local) && path17.resolve(local) !== path17.resolve(file)) installClaudeHooks({ file: local, remove: true });
7744
8041
  }
7745
8042
  if (projectHooks?.shared) {
7746
8043
  hooks.sharedWithWorktrees = true;
7747
- if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
8044
+ if (!hooks.error && !opts.removeHooks) ensureDir(path17.dirname(projectHooks.legacyFile));
7748
8045
  }
7749
- if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
8046
+ if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs22.realpathSync(projectHooks.file) !== fs22.realpathSync(projectHooks.legacyFile)) {
7750
8047
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7751
8048
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7752
8049
  }
@@ -7756,15 +8053,15 @@ function runInit(opts = {}) {
7756
8053
  let learnCommand;
7757
8054
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
7758
8055
  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 });
8056
+ mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path17.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
7760
8057
  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 });
8058
+ learnCommand = installLearnCommand({ file: path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7762
8059
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7763
8060
  }
7764
8061
  let borrowCommand;
7765
8062
  if (opts.removeHooks || !opts.noHooks) {
7766
8063
  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;
8064
+ const file = config.harness === "claude-code" ? path17.join(user ? path17.join(homeDir(), ".claude") : path17.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path17.join(process.env.CODEX_HOME ?? path17.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path17.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path17.join(cwd, ".pi", "prompts", "borrow.md") : null;
7768
8065
  if (file) {
7769
8066
  borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
7770
8067
  if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
@@ -7790,7 +8087,7 @@ function otherHarnessInstructions() {
7790
8087
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7791
8088
  ].join("\n");
7792
8089
  }
7793
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
8090
+ var PRE_TOOL_MATCHER, ALL_TOOLS_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
7794
8091
  var init_init = __esm({
7795
8092
  "src/init.ts"() {
7796
8093
  "use strict";
@@ -7800,6 +8097,7 @@ var init_init = __esm({
7800
8097
  init_project_hooks();
7801
8098
  init_trust();
7802
8099
  PRE_TOOL_MATCHER = "Read|Edit|Write|MultiEdit|NotebookEdit";
8100
+ ALL_TOOLS_MATCHER = "*";
7803
8101
  HOOK_EVENTS = [
7804
8102
  { event: "SessionStart", arg: "session-start", timeout: 10 },
7805
8103
  { event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
@@ -7921,9 +8219,9 @@ __export(commands_exports, {
7921
8219
  setupRobot: () => setupRobot,
7922
8220
  v2Status: () => v2Status
7923
8221
  });
7924
- import fs22 from "node:fs";
7925
- import path17 from "node:path";
7926
- import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
8222
+ import fs23 from "node:fs";
8223
+ import path18 from "node:path";
8224
+ import { createHash as createHash6, randomBytes as randomBytes4 } from "node:crypto";
7927
8225
  function cmdReview(store, args, cwd, out, err) {
7928
8226
  const v2 = new V2Store(store);
7929
8227
  const sub = args._[1] ?? "list";
@@ -8093,11 +8391,11 @@ function cmdSuggest(store, args, cwd, out, err) {
8093
8391
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
8094
8392
  return 1;
8095
8393
  }
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);
8394
+ const harnessDir = path18.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8395
+ const file = str3(args.flags.file) ? path18.resolve(cwd, str3(args.flags.file)) : path18.join(harnessDir, rel);
8098
8396
  let content;
8099
8397
  try {
8100
- content = fs22.readFileSync(file, "utf8");
8398
+ content = fs23.readFileSync(file, "utf8");
8101
8399
  } catch {
8102
8400
  err(`bunshin suggest: cannot read ${file}
8103
8401
  `);
@@ -8218,15 +8516,15 @@ async function cmdQuestions(store, args, out, err) {
8218
8516
  return 0;
8219
8517
  }
8220
8518
  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}
8519
+ const dir2 = path18.join(homeDir(), ".config/bunshin/credentials");
8520
+ fs23.mkdirSync(dir2, { recursive: true, mode: 448 });
8521
+ fs23.chmodSync(dir2, 448);
8522
+ const file = path18.join(dir2, `${createHash6("sha256").update(`${storeRoot}
8225
8523
  ${apiUrl}
8226
8524
  ${id}`).digest("hex")}.json`);
8227
8525
  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);
8526
+ fs23.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8527
+ fs23.renameSync(tmp, file);
8230
8528
  return file;
8231
8529
  }
8232
8530
  async function setupRobot(opts) {
@@ -8234,10 +8532,10 @@ async function setupRobot(opts) {
8234
8532
  const existing = Store.discover(opts.cwd);
8235
8533
  let store;
8236
8534
  const harness = opts.harness ?? "other";
8237
- if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
8535
+ if (existing && path18.resolve(path18.dirname(existing.root)) === path18.resolve(opts.cwd) && harness === "other") store = existing;
8238
8536
  else {
8239
8537
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
8240
- store = new Store(path17.join(opts.cwd, ".bunshin"));
8538
+ store = new Store(path18.join(opts.cwd, ".bunshin"));
8241
8539
  }
8242
8540
  let config = store.config();
8243
8541
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -8391,16 +8689,16 @@ __export(setup_exports, {
8391
8689
  setupProject: () => setupProject,
8392
8690
  shellQuote: () => shellQuote
8393
8691
  });
8394
- import fs23 from "node:fs";
8395
- import path18 from "node:path";
8396
- import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
8692
+ import fs24 from "node:fs";
8693
+ import path19 from "node:path";
8694
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "node:crypto";
8397
8695
  function executable(name) {
8398
- const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
8696
+ const directories = [...(process.env.PATH ?? "").split(path19.delimiter), path19.join(homeDir(), ".local/bin")];
8399
8697
  for (const directory of directories) {
8400
- const file = path18.resolve(directory, name);
8698
+ const file = path19.resolve(directory, name);
8401
8699
  try {
8402
- fs23.accessSync(file, fs23.constants.X_OK);
8403
- if (fs23.statSync(file).isFile()) return file;
8700
+ fs24.accessSync(file, fs24.constants.X_OK);
8701
+ if (fs24.statSync(file).isFile()) return file;
8404
8702
  } catch {
8405
8703
  }
8406
8704
  }
@@ -8408,8 +8706,8 @@ function executable(name) {
8408
8706
  }
8409
8707
  function hookBinary(harness) {
8410
8708
  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]))}`;
8709
+ const directories = [.../* @__PURE__ */ new Set([path19.dirname(process.execPath), path19.join(homeDir(), ".local/bin"), ...agent ? [path19.dirname(agent)] : []])];
8710
+ return `PATH=${shellQuote(directories.join(path19.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs24.realpathSync(process.argv[1]))}`;
8413
8711
  }
8414
8712
  function apiAddress(raw) {
8415
8713
  const url = new URL(raw);
@@ -8478,23 +8776,23 @@ async function setupProject(opts) {
8478
8776
  const apiKey = opts.apiKey.trim();
8479
8777
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
8480
8778
  const profile = await connectionProfile(apiUrl, apiKey);
8481
- const root = path18.join(opts.cwd, ".bunshin");
8779
+ const root = path19.join(opts.cwd, ".bunshin");
8482
8780
  const store = new Store(root);
8483
8781
  const previous = store.config();
8484
8782
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
8485
8783
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
8486
8784
  }
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}
8785
+ const credentialDirectory = path19.join(homeDir(), ".config/bunshin/credentials");
8786
+ fs24.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8787
+ fs24.chmodSync(credentialDirectory, 448);
8788
+ const name = createHash7("sha256").update(`${root}
8491
8789
  ${apiUrl}
8492
8790
  ${profile.orgId}
8493
8791
  ${profile.userId}`).digest("hex");
8494
- const credentialFile = path18.join(credentialDirectory, `${name}.json`);
8792
+ const credentialFile = path19.join(credentialDirectory, `${name}.json`);
8495
8793
  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);
8794
+ fs24.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8795
+ fs24.renameSync(temporary, credentialFile);
8498
8796
  const result = runInit({
8499
8797
  cwd: opts.cwd,
8500
8798
  harness: opts.harness,
@@ -8846,9 +9144,9 @@ __export(sync_exports2, {
8846
9144
  resolveConflict: () => resolveConflict,
8847
9145
  runSync: () => runSync
8848
9146
  });
8849
- import { createHash as createHash7 } from "node:crypto";
8850
- import path19 from "node:path";
8851
- import fs24 from "node:fs";
9147
+ import { createHash as createHash8 } from "node:crypto";
9148
+ import path20 from "node:path";
9149
+ import fs25 from "node:fs";
8852
9150
  async function api(config, method, route, body, timeoutMs = 1e4) {
8853
9151
  const ctrl = new AbortController();
8854
9152
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8886,22 +9184,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8886
9184
  }
8887
9185
  }
8888
9186
  async function runSync(store) {
8889
- const lock = path19.join(store.root, "sync.lock");
9187
+ const lock = path20.join(store.root, "sync.lock");
8890
9188
  const deadline = Date.now() + 12e4;
8891
9189
  for (; ; ) {
8892
9190
  try {
8893
- fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
9191
+ fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8894
9192
  break;
8895
9193
  } catch (error) {
8896
9194
  if (error.code !== "EEXIST") throw error;
8897
9195
  try {
8898
- const owner = Number(fs24.readFileSync(lock, "utf8"));
9196
+ const owner = Number(fs25.readFileSync(lock, "utf8"));
8899
9197
  if (Number.isInteger(owner) && owner > 0) {
8900
9198
  try {
8901
9199
  process.kill(owner, 0);
8902
9200
  } catch (error2) {
8903
9201
  if (error2.code === "ESRCH") {
8904
- fs24.unlinkSync(lock);
9202
+ fs25.unlinkSync(lock);
8905
9203
  continue;
8906
9204
  }
8907
9205
  }
@@ -8920,7 +9218,7 @@ async function runSync(store) {
8920
9218
  return await syncStore(store);
8921
9219
  } finally {
8922
9220
  try {
8923
- fs24.unlinkSync(lock);
9221
+ fs25.unlinkSync(lock);
8924
9222
  } catch {
8925
9223
  }
8926
9224
  }
@@ -8948,9 +9246,9 @@ async function syncStore(store) {
8948
9246
  result.errors.push(String(e instanceof Error ? e.message : e));
8949
9247
  }
8950
9248
  }
8951
- const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
9249
+ const acknowledgementsFile = path20.join(store.root, "synced-traces.json");
8952
9250
  const acknowledgements = readJson(acknowledgementsFile, {});
8953
- const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
9251
+ const fingerprint2 = (trace) => createHash8("sha256").update(JSON.stringify(trace)).digest("hex");
8954
9252
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8955
9253
  for (const t of traces) {
8956
9254
  try {
@@ -9203,11 +9501,11 @@ __export(demo_exports, {
9203
9501
  runDemo: () => runDemo
9204
9502
  });
9205
9503
  import assert from "node:assert/strict";
9206
- import fs25 from "node:fs";
9504
+ import fs26 from "node:fs";
9207
9505
  import os4 from "node:os";
9208
- import path20 from "node:path";
9506
+ import path21 from "node:path";
9209
9507
  async function runDemo(write) {
9210
- const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
9508
+ const root = fs26.mkdtempSync(path21.join(os4.tmpdir(), "bunshin-demo-"));
9211
9509
  try {
9212
9510
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
9213
9511
  const { lessons, constraint, workflow } = aprScenario();
@@ -9224,7 +9522,7 @@ Task: ${APR_TASK}
9224
9522
  write(result.text);
9225
9523
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
9226
9524
  } finally {
9227
- fs25.rmSync(root, { recursive: true, force: true });
9525
+ fs26.rmSync(root, { recursive: true, force: true });
9228
9526
  }
9229
9527
  }
9230
9528
  var init_demo = __esm({
@@ -9237,21 +9535,21 @@ var init_demo = __esm({
9237
9535
  });
9238
9536
 
9239
9537
  // src/v2/gitremote.ts
9240
- import fs26 from "node:fs";
9241
- import path21 from "node:path";
9242
- import { createHash as createHash8 } from "node:crypto";
9538
+ import fs27 from "node:fs";
9539
+ import path22 from "node:path";
9540
+ import { createHash as createHash9 } from "node:crypto";
9243
9541
  function saveGitCredential(apiUrl, apiKey) {
9244
9542
  const file = gitCredentialFile();
9245
- fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
9543
+ fs27.mkdirSync(path22.dirname(file), { recursive: true, mode: 448 });
9246
9544
  let all = {};
9247
9545
  try {
9248
- all = JSON.parse(fs26.readFileSync(file, "utf8"));
9546
+ all = JSON.parse(fs27.readFileSync(file, "utf8"));
9249
9547
  } catch {
9250
9548
  }
9251
9549
  all[apiUrl.replace(/\/$/, "")] = apiKey;
9252
9550
  const tmp = `${file}.${process.pid}.tmp`;
9253
- fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9254
- fs26.renameSync(tmp, file);
9551
+ fs27.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9552
+ fs27.renameSync(tmp, file);
9255
9553
  }
9256
9554
  function resolveGitKey(apiUrl) {
9257
9555
  const base = apiUrl.replace(/\/$/, "");
@@ -9259,20 +9557,20 @@ function resolveGitKey(apiUrl) {
9259
9557
  const origin = urlOrigin(base);
9260
9558
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
9261
9559
  try {
9262
- const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
9560
+ const all = JSON.parse(fs27.readFileSync(gitCredentialFile(), "utf8"));
9263
9561
  if (all[base]) return all[base];
9264
9562
  } catch {
9265
9563
  }
9266
- const dir = path21.join(homeDir(), ".config/bunshin/credentials");
9564
+ const dir2 = path22.join(homeDir(), ".config/bunshin/credentials");
9267
9565
  let files = [];
9268
9566
  try {
9269
- files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
9567
+ files = fs27.readdirSync(dir2).filter((f) => f.endsWith(".json"));
9270
9568
  } catch {
9271
9569
  return void 0;
9272
9570
  }
9273
9571
  for (const f of files.sort()) {
9274
9572
  try {
9275
- const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
9573
+ const c = JSON.parse(fs27.readFileSync(path22.join(dir2, f), "utf8"));
9276
9574
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
9277
9575
  } catch {
9278
9576
  }
@@ -9280,7 +9578,7 @@ function resolveGitKey(apiUrl) {
9280
9578
  return void 0;
9281
9579
  }
9282
9580
  function refsDigest(refs) {
9283
- return createHash8("sha256").update(
9581
+ return createHash9("sha256").update(
9284
9582
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
9285
9583
  ).digest("hex").slice(0, 12);
9286
9584
  }
@@ -9293,7 +9591,7 @@ var init_gitremote = __esm({
9293
9591
  ZERO_SHA = "0".repeat(40);
9294
9592
  REMOTE_PATH = "/v1/git/repos/";
9295
9593
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
9296
- gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
9594
+ gitCredentialFile = () => path22.join(homeDir(), ".config/bunshin/git-credentials.json");
9297
9595
  GitApiError = class extends Error {
9298
9596
  constructor(status, message, body = null) {
9299
9597
  super(message);
@@ -9385,8 +9683,8 @@ __export(gitrepo_exports, {
9385
9683
  githubReferences: () => githubReferences,
9386
9684
  repoFromUrl: () => repoFromUrl
9387
9685
  });
9388
- import fs27 from "node:fs";
9389
- import path22 from "node:path";
9686
+ import fs28 from "node:fs";
9687
+ import path23 from "node:path";
9390
9688
  import { spawnSync as spawnSync2 } from "node:child_process";
9391
9689
  function git3(cwd, args, opts = {}) {
9392
9690
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
@@ -9395,9 +9693,9 @@ function git3(cwd, args, opts = {}) {
9395
9693
  return res;
9396
9694
  }
9397
9695
  function onPath(name) {
9398
- for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
9696
+ for (const dir2 of (process.env.PATH ?? "").split(path23.delimiter)) {
9399
9697
  try {
9400
- fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
9698
+ fs28.accessSync(path23.join(dir2, name), fs28.constants.X_OK);
9401
9699
  return true;
9402
9700
  } catch {
9403
9701
  }
@@ -9448,9 +9746,9 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
9448
9746
  if (!reuseKey || !process.env.BUNSHIN_API_KEY || key !== process.env.BUNSHIN_API_KEY) saveGitCredential(apiUrl, key);
9449
9747
  return client;
9450
9748
  }
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`);
9749
+ function toplevel(dir2) {
9750
+ const r = git3(dir2, ["rev-parse", "--show-toplevel"], { allowFail: true });
9751
+ if (!r.ok) throw new Error(`${dir2} is not inside a git repository`);
9454
9752
  return r.out.trim();
9455
9753
  }
9456
9754
  function githubReferences(root, repo) {
@@ -9481,11 +9779,11 @@ function switchOrigin(root, url, out) {
9481
9779
  }
9482
9780
  async function cmdImport(args, cwd, out, err) {
9483
9781
  requireHelper();
9484
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9782
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9485
9783
  if (git3(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
9486
9784
  const rs = remotes(root);
9487
9785
  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)}`;
9786
+ const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path23.basename(path23.dirname(root))}/${path23.basename(root)}`;
9489
9787
  const client = await connect(args, cwd);
9490
9788
  const url = remoteUrl(client.apiUrl, name);
9491
9789
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
@@ -9561,7 +9859,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
9561
9859
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
9562
9860
  `);
9563
9861
  }
9564
- if (fs27.existsSync(path22.join(root, ".github"))) out(`
9862
+ if (fs28.existsSync(path23.join(root, ".github"))) out(`
9565
9863
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
9566
9864
  `);
9567
9865
  out(`
@@ -9572,7 +9870,7 @@ New machines clone with: bunshin repo clone ${name}
9572
9870
  }
9573
9871
  async function cmdAttach(args, cwd, out, err) {
9574
9872
  requireHelper();
9575
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9873
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9576
9874
  const rs = remotes(root);
9577
9875
  const originUrl = rs.get("origin");
9578
9876
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -9617,15 +9915,15 @@ async function cmdClone(args, cwd, out) {
9617
9915
  const name = args._[1];
9618
9916
  if (!name) throw new Error("usage: bunshin repo clone owner/name [directory]");
9619
9917
  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" });
9918
+ const dir2 = args._[2] ?? name.split("/")[1];
9919
+ const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir2], { cwd, stdio: "inherit" });
9622
9920
  if (r.status !== 0) return r.status ?? 1;
9623
- out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
9921
+ out(`Cloned ${name} into ${path23.resolve(cwd, dir2)}
9624
9922
  `);
9625
9923
  return 0;
9626
9924
  }
9627
9925
  async function cmdVerify(args, cwd, out, err) {
9628
- const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
9926
+ const root = toplevel(path23.resolve(str4(args.flags.path) ?? cwd));
9629
9927
  const rs = remotes(root);
9630
9928
  const originUrl = rs.get("origin") ?? "";
9631
9929
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -9788,22 +10086,22 @@ __export(runner_exports, {
9788
10086
  setupText: () => setupText
9789
10087
  });
9790
10088
  import { spawnSync as spawnSync3 } from "node:child_process";
9791
- import fs28 from "node:fs";
10089
+ import fs29 from "node:fs";
9792
10090
  import os5 from "node:os";
9793
- import path23 from "node:path";
10091
+ import path24 from "node:path";
9794
10092
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9795
- return path23.join(home, ".bunshin", "runner");
10093
+ return path24.join(home, ".bunshin", "runner");
9796
10094
  }
9797
10095
  function readInstalled(home) {
9798
10096
  try {
9799
- return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
10097
+ return JSON.parse(fs29.readFileSync(path24.join(runnerHome(home), "config.json"), "utf8"));
9800
10098
  } catch {
9801
10099
  return null;
9802
10100
  }
9803
10101
  }
9804
10102
  function connection(args, cwd) {
9805
10103
  const saved = readInstalled();
9806
- const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
10104
+ const store = saved && path24.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9807
10105
  const config = store?.config();
9808
10106
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9809
10107
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9863,9 +10161,9 @@ async function runOnce(c, args, out, err) {
9863
10161
  const [job] = await lease(c, args);
9864
10162
  if (!job) break;
9865
10163
  const started = Date.now();
9866
- const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
10164
+ const scratch = fs29.mkdtempSync(path24.join(os5.tmpdir(), "bunshin-job-"));
9867
10165
  const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
9868
- () => fs28.rmSync(scratch, { recursive: true, force: true })
10166
+ () => fs29.rmSync(scratch, { recursive: true, force: true })
9869
10167
  );
9870
10168
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9871
10169
  if (!res.ok) {
@@ -9955,24 +10253,24 @@ function renderCheck(id, c) {
9955
10253
  }
9956
10254
  function logAnswer(entry) {
9957
10255
  try {
9958
- fs28.mkdirSync(runnerHome(), { recursive: true });
9959
- fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
10256
+ fs29.mkdirSync(runnerHome(), { recursive: true });
10257
+ fs29.appendFileSync(path24.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9960
10258
  `);
9961
10259
  } catch {
9962
10260
  }
9963
10261
  }
9964
10262
  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);
10263
+ for (const d of [...(process.env.PATH || "").split(path24.delimiter), path24.join(os5.homedir(), ".local", "bin")]) {
10264
+ if (d && fs29.existsSync(path24.join(d, name))) return path24.join(d, name);
9967
10265
  }
9968
10266
  return null;
9969
10267
  }
9970
10268
  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";
10269
+ const node = path24.dirname(process.execPath);
10270
+ const npx = fs29.existsSync(path24.join(node, "npx")) ? path24.join(node, "npx") : which("npx") ?? "npx";
9973
10271
  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) };
10272
+ const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path24.dirname(bin)] : [], path24.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
10273
+ return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path24.delimiter) };
9976
10274
  }
9977
10275
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9978
10276
  const cmd = scheduledCommand(backend);
@@ -9987,8 +10285,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9987
10285
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9988
10286
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9989
10287
  <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>
10288
+ <key>StandardOutPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
10289
+ <key>StandardErrorPath</key><string>${xml(path24.join(home, "runner.log"))}</string>
9992
10290
  <key>ProcessType</key><string>Background</string>
9993
10291
  <key>LowPriorityIO</key><true/>
9994
10292
  </dict>
@@ -9999,19 +10297,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9999
10297
  const cmd = scheduledCommand(backend);
10000
10298
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
10001
10299
  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}`;
10300
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path24.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
10003
10301
  }
10004
10302
  function launchAgentsDir(opts) {
10005
- return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
10303
+ return opts.launchAgents ?? path24.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
10006
10304
  }
10007
10305
  function installSchedule(backend, everyMinutes, opts = {}) {
10008
10306
  const platform = opts.platform ?? process.platform;
10009
10307
  const run2 = opts.run ?? realRun;
10010
10308
  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));
10309
+ const dir2 = launchAgentsDir(opts);
10310
+ const plist = path24.join(dir2, `${LAUNCHD_LABEL}.plist`);
10311
+ fs29.mkdirSync(dir2, { recursive: true });
10312
+ fs29.writeFileSync(plist, launchdPlist(backend, everyMinutes));
10015
10313
  const domain = `gui/${process.getuid?.() ?? 501}`;
10016
10314
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
10017
10315
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -10031,10 +10329,10 @@ function removeSchedule(opts = {}) {
10031
10329
  const platform = opts.platform ?? process.platform;
10032
10330
  const run2 = opts.run ?? realRun;
10033
10331
  if (platform === "darwin") {
10034
- const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
10332
+ const plist = path24.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
10035
10333
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
10036
- if (!fs28.existsSync(plist)) return "no schedule";
10037
- fs28.rmSync(plist, { force: true });
10334
+ if (!fs29.existsSync(plist)) return "no schedule";
10335
+ fs29.rmSync(plist, { force: true });
10038
10336
  return `removed ${plist}`;
10039
10337
  }
10040
10338
  const current = run2("crontab", ["-l"]);
@@ -10061,10 +10359,10 @@ async function install(args, cwd, out, err, opts = {}) {
10061
10359
  }
10062
10360
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
10063
10361
  const home = runnerHome();
10064
- fs28.mkdirSync(home, { recursive: true, mode: 448 });
10362
+ fs29.mkdirSync(home, { recursive: true, mode: 448 });
10065
10363
  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);
10364
+ fs29.writeFileSync(path24.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
10365
+ fs29.chmodSync(path24.join(home, "config.json"), 384);
10068
10366
  let schedule;
10069
10367
  try {
10070
10368
  schedule = installSchedule(backend, every, opts);
@@ -10077,7 +10375,7 @@ Run it yourself: npx -y ${PKG2} runner --once
10077
10375
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
10078
10376
  `);
10079
10377
  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")}
10378
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path24.join(home, "runner.log")}
10081
10379
  `);
10082
10380
  out(` Stop: npx -y ${PKG2} runner uninstall
10083
10381
  `);
@@ -10094,9 +10392,9 @@ Answering what waits now\u2026
10094
10392
  function uninstall(out, opts = {}) {
10095
10393
  out(`${removeSchedule(opts)}
10096
10394
  `);
10097
- const file = path23.join(runnerHome(), "config.json");
10098
- if (fs28.existsSync(file)) {
10099
- fs28.rmSync(file, { force: true });
10395
+ const file = path24.join(runnerHome(), "config.json");
10396
+ if (fs29.existsSync(file)) {
10397
+ fs29.rmSync(file, { force: true });
10100
10398
  out(`removed ${file} (revoke the key in the console: Agent keys)
10101
10399
  `);
10102
10400
  }
@@ -10276,7 +10574,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10276
10574
  out(text2);
10277
10575
  return 0;
10278
10576
  }
10279
- fs28.writeFileSync(file, text2, { mode: 384 });
10577
+ fs29.writeFileSync(file, text2, { mode: 384 });
10280
10578
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
10281
10579
  out(
10282
10580
  [
@@ -10294,7 +10592,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10294
10592
  const id = args._[2];
10295
10593
  const file = args._[3] ?? "-";
10296
10594
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
10297
- const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
10595
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10298
10596
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
10299
10597
  out(renderCheck(id, r.body));
10300
10598
  return r.body.ok ? 0 : 1;
@@ -10303,7 +10601,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10303
10601
  const id = args._[2];
10304
10602
  const file = args._[3] ?? "-";
10305
10603
  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");
10604
+ const text2 = file === "-" ? fs29.readFileSync(0, "utf8") : fs29.readFileSync(file, "utf8");
10307
10605
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
10308
10606
  if (check?.body && (check.status < 400 || check.body.problems)) {
10309
10607
  out(renderCheck(id, check.body));
@@ -10327,7 +10625,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
10327
10625
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
10328
10626
  const entries = (() => {
10329
10627
  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);
10628
+ return fs29.readFileSync(path24.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
10331
10629
  } catch {
10332
10630
  return [];
10333
10631
  }
@@ -10397,8 +10695,8 @@ var init_runner = __esm({
10397
10695
 
10398
10696
  // src/v2/importer.ts
10399
10697
  import { execFileSync as execFileSync6 } from "node:child_process";
10400
- import fs29 from "node:fs";
10401
- import path24 from "node:path";
10698
+ import fs30 from "node:fs";
10699
+ import path25 from "node:path";
10402
10700
  function rewriteRepoPaths(text2, repoName2, pack) {
10403
10701
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
10404
10702
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -10433,17 +10731,17 @@ function buildDocs(files, opts) {
10433
10731
  const claimed = /* @__PURE__ */ new Set();
10434
10732
  const all = [...files.keys()].sort();
10435
10733
  const skillDirs = all.map(skillDirOf).filter((d) => !!d);
10436
- for (const dir of skillDirs) {
10437
- const main2 = `${dir}/SKILL.md`;
10734
+ for (const dir2 of skillDirs) {
10735
+ const main2 = `${dir2}/SKILL.md`;
10438
10736
  const text2 = rw(files.get(main2).content);
10439
10737
  const fm = frontmatterDescription(text2);
10440
- const name = path24.posix.basename(dir);
10738
+ const name = path25.posix.basename(dir2);
10441
10739
  const bundle = {};
10442
- for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
10740
+ for (const p of all.filter((x) => x.startsWith(`${dir2}/`))) {
10443
10741
  claimed.add(p);
10444
10742
  if (p === main2) continue;
10445
10743
  const f = files.get(p);
10446
- bundle[p.slice(dir.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10744
+ bundle[p.slice(dir2.length + 1)] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10447
10745
  }
10448
10746
  const description = fm.description.replace(/\s+/g, " ").trim() || firstSentence(text2) || name;
10449
10747
  const { name: _n, description: _d, ...rest } = fm.data;
@@ -10470,9 +10768,9 @@ function buildDocs(files, opts) {
10470
10768
  if (/\.md$/i.test(p) && looksGenerated(files.get(p).content)) claimed.add(p);
10471
10769
  }
10472
10770
  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));
10771
+ for (const dir2 of [...dirs].sort()) {
10772
+ if (skillParents.has(dir2) && all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x)).length === 0) continue;
10773
+ const members = all.filter((x) => x.startsWith(`${dir2}/`) && !claimed.has(x));
10476
10774
  if (!members.length) continue;
10477
10775
  const readme = members.find((x) => /^[^/]+\/README(\.md)?$/i.test(x));
10478
10776
  const bundle = {};
@@ -10483,15 +10781,15 @@ function buildDocs(files, opts) {
10483
10781
  bundle[p] = { content: rw(f.content), ...f.mode ? { mode: f.mode } : {} };
10484
10782
  }
10485
10783
  const tool = members.some((p) => files.get(p).mode === "x" || SCRIPT.test(p));
10486
- const body = readme ? rw(files.get(readme).content) : `# ${dir}
10784
+ const body = readme ? rw(files.get(readme).content) : `# ${dir2}
10487
10785
 
10488
10786
  ${members.map((p) => `- \`${p}\``).join("\n")}
10489
10787
  `;
10490
10788
  docs.push({
10491
- path: readme ?? `${dir}/#index`,
10789
+ path: readme ?? `${dir2}/#index`,
10492
10790
  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}`,
10791
+ title: dir2,
10792
+ when: readme ? firstSentence(body) || `The ${dir2} ${tool ? "tool" : "folder"}` : `The ${dir2} ${tool ? "tool" : "folder"} of ${opts.pack}`,
10495
10793
  body,
10496
10794
  files: bundle
10497
10795
  });
@@ -10534,23 +10832,23 @@ function isGitRepo(root) {
10534
10832
  }
10535
10833
  function workingTreeFiles(root) {
10536
10834
  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) => {
10835
+ const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir2, base = "") {
10836
+ return fs30.readdirSync(dir2, { withFileTypes: true }).flatMap((e) => {
10539
10837
  if (e.name === ".git" || e.name === "node_modules") return [];
10540
10838
  const rel = base ? `${base}/${e.name}` : e.name;
10541
- return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
10839
+ return e.isDirectory() ? walk(path25.join(dir2, e.name), rel) : e.isFile() ? [rel] : [];
10542
10840
  });
10543
10841
  })(root);
10544
10842
  for (const rel of list2) {
10545
- const abs = path24.join(root, rel);
10843
+ const abs = path25.join(root, rel);
10546
10844
  let buf;
10547
10845
  try {
10548
- buf = fs29.readFileSync(abs);
10846
+ buf = fs30.readFileSync(abs);
10549
10847
  } catch {
10550
10848
  continue;
10551
10849
  }
10552
10850
  if (!isText(buf)) continue;
10553
- const exec = (fs29.statSync(abs).mode & 73) !== 0;
10851
+ const exec = (fs30.statSync(abs).mode & 73) !== 0;
10554
10852
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
10555
10853
  }
10556
10854
  return out;
@@ -10584,7 +10882,7 @@ function diffDocs(prev, next) {
10584
10882
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
10585
10883
  }
10586
10884
  function readSkillSystem(root, opts) {
10587
- const repoName2 = path24.basename(path24.resolve(root));
10885
+ const repoName2 = path25.basename(path25.resolve(root));
10588
10886
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
10589
10887
  const tree = build(workingTreeFiles(root));
10590
10888
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -10661,7 +10959,7 @@ var init_importer = __esm({
10661
10959
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10662
10960
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
10663
10961
  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);
10962
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path25.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10665
10963
  fingerprint = (d) => JSON.stringify(d);
10666
10964
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
10667
10965
 
@@ -10697,8 +10995,8 @@ __export(skills_exports, {
10697
10995
  cmdSkill: () => cmdSkill,
10698
10996
  parsePin: () => parsePin
10699
10997
  });
10700
- import fs30 from "node:fs";
10701
- import path25 from "node:path";
10998
+ import fs31 from "node:fs";
10999
+ import path26 from "node:path";
10702
11000
  function requireOnline(store, what) {
10703
11001
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
10704
11002
  }
@@ -10707,9 +11005,9 @@ async function cmdImport2(store, args, cwd, out, err) {
10707
11005
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
10708
11006
  return 1;
10709
11007
  }
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);
11008
+ const root = path26.resolve(cwd, args._[2]);
11009
+ if (!fs31.existsSync(root)) throw new Error(`${root} does not exist`);
11010
+ const pack = str6(args.flags.pack) ?? path26.basename(root);
10713
11011
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
10714
11012
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
10715
11013
  const kinds = /* @__PURE__ */ new Map();
@@ -10744,7 +11042,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10744
11042
  const propose = args.flags["propose-rewrites"];
10745
11043
  if (propose) {
10746
11044
  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);
11045
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path26.basename(root), out, err);
10748
11046
  }
10749
11047
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10750
11048
  return 0;
@@ -10884,7 +11182,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10884
11182
  }
10885
11183
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10886
11184
  if (graph) {
10887
- fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
11185
+ fs31.writeFileSync(path26.resolve(cwd, graph), renderSkillGraph(report));
10888
11186
  out(`graph written to ${graph}
10889
11187
  `);
10890
11188
  }
@@ -10974,9 +11272,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10974
11272
  `;
10975
11273
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10976
11274
  if (file) {
10977
- const abs = path25.resolve(cwd, file);
10978
- fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10979
- fs30.writeFileSync(abs, line);
11275
+ const abs = path26.resolve(cwd, file);
11276
+ fs31.mkdirSync(path26.dirname(abs), { recursive: true });
11277
+ fs31.writeFileSync(abs, line);
10980
11278
  out(`${file}: ${line}`);
10981
11279
  } else out(line);
10982
11280
  return 0;
@@ -10986,16 +11284,16 @@ async function cmdLibrary(store, args, cwd, out, err) {
10986
11284
  const to = str6(args.flags.to);
10987
11285
  if (!to) throw new Error("--to <dir> is required");
10988
11286
  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] ?? "");
11287
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs31.readFileSync(path26.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10990
11288
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10991
11289
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10992
11290
  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)`);
11291
+ const dir2 = path26.resolve(cwd, to);
11292
+ if (fs31.existsSync(dir2) && fs31.readdirSync(dir2).length && !bool4(args.flags.force)) throw new Error(`${dir2} is not empty (use --force)`);
10995
11293
  const pack = str6(args.flags.pack);
10996
11294
  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}
11295
+ const res = materialize(docs, dir2);
11296
+ out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir2}
10999
11297
  `);
11000
11298
  return 0;
11001
11299
  }
@@ -11030,27 +11328,27 @@ __export(reflect_exports, {
11030
11328
  buildLesson: () => buildLesson,
11031
11329
  runReflection: () => runReflection
11032
11330
  });
11033
- import fs31 from "node:fs";
11034
- import path26 from "node:path";
11035
- import { createHash as createHash9 } from "node:crypto";
11331
+ import fs32 from "node:fs";
11332
+ import path27 from "node:path";
11333
+ import { createHash as createHash10 } from "node:crypto";
11036
11334
  async function runReflection(store, opts) {
11037
11335
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
11038
- const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
11336
+ const lock = path27.join(store.root, `reflect-${createHash10("sha256").update(opts.sessionId).digest("hex")}.lock`);
11039
11337
  const deadline = Date.now() + 66e4;
11040
11338
  for (; ; ) {
11041
11339
  try {
11042
- fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
11340
+ fs32.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
11043
11341
  break;
11044
11342
  } catch (error) {
11045
11343
  if (error.code !== "EEXIST") throw error;
11046
11344
  try {
11047
- const owner = Number(fs31.readFileSync(lock, "utf8"));
11345
+ const owner = Number(fs32.readFileSync(lock, "utf8"));
11048
11346
  if (Number.isInteger(owner) && owner > 0) {
11049
11347
  try {
11050
11348
  process.kill(owner, 0);
11051
11349
  } catch (error2) {
11052
11350
  if (error2.code === "ESRCH") {
11053
- fs31.unlinkSync(lock);
11351
+ fs32.unlinkSync(lock);
11054
11352
  continue;
11055
11353
  }
11056
11354
  }
@@ -11065,7 +11363,7 @@ async function runReflection(store, opts) {
11065
11363
  return await reflectSession(store, opts);
11066
11364
  } finally {
11067
11365
  try {
11068
- fs31.unlinkSync(lock);
11366
+ fs32.unlinkSync(lock);
11069
11367
  } catch {
11070
11368
  }
11071
11369
  }
@@ -11407,8 +11705,8 @@ __export(cli_exports, {
11407
11705
  parseArgs: () => parseArgs,
11408
11706
  version: () => version
11409
11707
  });
11410
- import fs32 from "node:fs";
11411
- import path27 from "node:path";
11708
+ import fs33 from "node:fs";
11709
+ import path28 from "node:path";
11412
11710
  function parseArgs(argv) {
11413
11711
  const out = { _: [], flags: {} };
11414
11712
  const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace", "across"]);
@@ -11508,7 +11806,7 @@ async function main(argv, opts = {}) {
11508
11806
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
11509
11807
  const agent = harness === "claude-code" ? "claude" : harness;
11510
11808
  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"));
11809
+ const prior = new Store(path28.join(cwd, ".bunshin"));
11512
11810
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
11513
11811
  const apiUrl = str7(args.flags["api-url"]);
11514
11812
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -11537,7 +11835,7 @@ async function main(argv, opts = {}) {
11537
11835
  });
11538
11836
  if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
11539
11837
  const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
11540
- await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
11838
+ await askSources2(new Store(path28.join(cwd, ".bunshin")), out);
11541
11839
  }
11542
11840
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
11543
11841
  `);
@@ -11574,7 +11872,7 @@ Key saved privately outside the project.
11574
11872
  if (cmd === "mcp") {
11575
11873
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
11576
11874
  const root = str7(args.flags.root);
11577
- await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
11875
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path28.resolve(root)) } : {}, version: version() });
11578
11876
  return 0;
11579
11877
  }
11580
11878
  if (cmd === "demo") {
@@ -11637,6 +11935,14 @@ Key saved privately outside the project.
11637
11935
  case "meta-reflect":
11638
11936
  case "meta":
11639
11937
  return await cmdMetaReflect(store, args, cwd, out);
11938
+ case "route": {
11939
+ const { routeJob: routeJob2, sweepRouter: sweepRouter2 } = await Promise.resolve().then(() => (init_route(), route_exports));
11940
+ const answer = await routeJob2(store, str7(args.flags.session) ?? "", Number(args.flags.seq));
11941
+ sweepRouter2(new (await Promise.resolve().then(() => (init_store2(), store_exports))).V2Store(store));
11942
+ if (!bool5(args.flags.quiet)) out(`${JSON.stringify(answer)}
11943
+ `);
11944
+ return answer && !answer.error ? 0 : 1;
11945
+ }
11640
11946
  case "update-check": {
11641
11947
  const latest = await (await Promise.resolve().then(() => (init_update(), update_exports))).checkForUpdate();
11642
11948
  if (!bool5(args.flags.quiet)) out(latest ? `latest ${latest} (this is ${version()})
@@ -11678,7 +11984,7 @@ ${HELP2}`);
11678
11984
  function openStore(args, cwd) {
11679
11985
  const root = str7(args.flags.root);
11680
11986
  if (root) {
11681
- const store = new Store(path27.resolve(root));
11987
+ const store = new Store(path28.resolve(root));
11682
11988
  store.ensureLayout();
11683
11989
  return store;
11684
11990
  }
@@ -11694,7 +12000,7 @@ async function cmdInit(args, cwd, out, err) {
11694
12000
  orgId: str7(args.flags["org-id"]),
11695
12001
  userId: str7(args.flags["user-id"]),
11696
12002
  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"]),
12003
+ apiKey: bool5(args.flags["key-stdin"]) ? fs33.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
11698
12004
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
11699
12005
  harness,
11700
12006
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -11737,10 +12043,10 @@ function cmdStatus(store, args, out, v2) {
11737
12043
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
11738
12044
  const metrics = store.readMetrics(void 0);
11739
12045
  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")] : [];
12046
+ const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path28.join(process.env.CODEX_HOME ?? path28.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path28.dirname(store.root)).file])] : config.harness === "claude-code" ? [path28.join(homeDir(), ".claude", "settings.json"), path28.join(path28.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path28.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path28.join(path28.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
11741
12047
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
11742
12048
  try {
11743
- return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
12049
+ return fs33.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11744
12050
  } catch {
11745
12051
  return false;
11746
12052
  }
@@ -11758,7 +12064,7 @@ function cmdStatus(store, args, out, v2) {
11758
12064
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
11759
12065
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
11760
12066
  hooks: { [config.harness]: hooks },
11761
- hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
12067
+ hookFiles: hookFiles.filter((file) => fs33.existsSync(file)),
11762
12068
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
11763
12069
  v2
11764
12070
  };
@@ -11801,7 +12107,7 @@ function cmdStatus(store, args, out, v2) {
11801
12107
  }
11802
12108
  function hookCount(file) {
11803
12109
  try {
11804
- const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
12110
+ const raw = JSON.parse(fs33.readFileSync(file, "utf8"));
11805
12111
  let n = 0;
11806
12112
  for (const groups of Object.values(raw.hooks ?? {})) {
11807
12113
  for (const g of groups ?? []) {
@@ -11921,7 +12227,7 @@ function cmdLessonAdd(store, args, out, err) {
11921
12227
  let content = str7(args.flags.content) ?? "";
11922
12228
  if (!content || content === "-" || args._.includes("-")) {
11923
12229
  try {
11924
- content = fs32.readFileSync(0, "utf8");
12230
+ content = fs33.readFileSync(0, "utf8");
11925
12231
  } catch {
11926
12232
  }
11927
12233
  }
@@ -11987,7 +12293,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11987
12293
  }
11988
12294
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11989
12295
  const result = await runReflection2(store, {
11990
- sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
12296
+ sessionId: str7(args.flags.session) ?? path28.basename(transcript).replace(/\.jsonl?$/, ""),
11991
12297
  transcriptPath: transcript,
11992
12298
  cwd,
11993
12299
  force: bool5(args.flags.force)
@@ -12190,7 +12496,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
12190
12496
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
12191
12497
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
12192
12498
  `;
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.`;
12499
+ untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path28.dirname(root)} to trust it.`;
12194
12500
  }
12195
12501
  });
12196
12502