@bunshin-cc/bunshin 0.6.5 → 0.7.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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/bin.js +838 -391
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1344,17 +1344,17 @@ var init_document = __esm({
1344
1344
  });
1345
1345
 
1346
1346
  // ../core/src/v2/sections/markdown.ts
1347
- function repoSectionId(repo, path26, anchor = "") {
1348
- return anchor ? `repo:${repo}/${path26}#${anchor}` : `repo:${repo}/${path26}`;
1347
+ function repoSectionId(repo, path28, anchor = "") {
1348
+ return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
1349
1349
  }
1350
- function docKind(path26) {
1351
- const base = path26.split("/").pop()?.toLowerCase() ?? "";
1350
+ function docKind(path28) {
1351
+ const base = path28.split("/").pop()?.toLowerCase() ?? "";
1352
1352
  if (base === "testing.md") return "verification";
1353
1353
  if (base === "skill.md") return "skill";
1354
1354
  return "spec";
1355
1355
  }
1356
- function defaultPaths(path26) {
1357
- const dir = path26.split("/").slice(0, -1).join("/");
1356
+ function defaultPaths(path28) {
1357
+ const dir = path28.split("/").slice(0, -1).join("/");
1358
1358
  if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
1359
1359
  return [`${dir}/**`];
1360
1360
  }
@@ -1499,11 +1499,11 @@ function lintSkills(sections) {
1499
1499
  }
1500
1500
  const state = /* @__PURE__ */ new Map();
1501
1501
  const cycles = /* @__PURE__ */ new Set();
1502
- const dfs = (n, path26) => {
1502
+ const dfs = (n, path28) => {
1503
1503
  state.set(n, 1);
1504
1504
  for (const d of edges[n] ?? []) {
1505
- if (state.get(d) === 1) cycles.add([...path26.slice(path26.indexOf(d)), d].join(" -> "));
1506
- else if (!state.get(d)) dfs(d, [...path26, d]);
1505
+ if (state.get(d) === 1) cycles.add([...path28.slice(path28.indexOf(d)), d].join(" -> "));
1506
+ else if (!state.get(d)) dfs(d, [...path28, d]);
1507
1507
  }
1508
1508
  state.set(n, 2);
1509
1509
  };
@@ -1590,8 +1590,8 @@ function parsePythonSkills(text2) {
1590
1590
  }
1591
1591
  return { skills, constants };
1592
1592
  }
1593
- function harnessSectionId(path26, symbol) {
1594
- return `harness:${path26}#${symbol}`;
1593
+ function harnessSectionId(path28, symbol) {
1594
+ return `harness:${path28}#${symbol}`;
1595
1595
  }
1596
1596
  var DEF_RE;
1597
1597
  var init_python = __esm({
@@ -1717,8 +1717,8 @@ function globToRegExp(glob) {
1717
1717
  globCache.set(glob, out);
1718
1718
  return out;
1719
1719
  }
1720
- function matchesGlob(path26, glob) {
1721
- const p = path26.replace(/^\.\//, "").replace(/\\/g, "/");
1720
+ function matchesGlob(path28, glob) {
1721
+ const p = path28.replace(/^\.\//, "").replace(/\\/g, "/");
1722
1722
  return globToRegExp(glob.replace(/^\.\//, "")).test(p);
1723
1723
  }
1724
1724
  var DEFAULT_EMBEDDERS, globCache;
@@ -1809,10 +1809,10 @@ function push(sections, reader, query) {
1809
1809
  for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
1810
1810
  const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
1811
1811
  for (const t of triggers) {
1812
- const path26 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1813
- const entity = path26 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1814
- const skill = path26 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1815
- const trigger = path26 ? `path:${path26}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1812
+ const path28 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1813
+ const entity = path28 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1814
+ const skill = path28 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1815
+ const trigger = path28 ? `path:${path28}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1816
1816
  if (trigger) {
1817
1817
  hits.set(s.id, { section: s, trigger, anchor: t.anchor });
1818
1818
  break;
@@ -2060,17 +2060,6 @@ function identLike(rest) {
2060
2060
  return humps.length > 0 && m[1].length / humps.length >= 3;
2061
2061
  });
2062
2062
  }
2063
- function luhnOk(digits) {
2064
- let total = 0;
2065
- let alt = false;
2066
- for (let i = digits.length - 1; i >= 0; i--) {
2067
- let d = digits.charCodeAt(i) - 48;
2068
- if (alt && (d *= 2) > 9) d -= 9;
2069
- total += d;
2070
- alt = !alt;
2071
- }
2072
- return total % 10 === 0;
2073
- }
2074
2063
  function issuerOk(d) {
2075
2064
  const n = d.length;
2076
2065
  if (d[0] === "4") return n === 13 || n === 16 || n === 19;
@@ -2082,23 +2071,44 @@ function issuerOk(d) {
2082
2071
  if (/^(?:50|5[6-8]|6\d)/.test(d)) return n >= 12 && n <= 19;
2083
2072
  return false;
2084
2073
  }
2074
+ function luhnDigits(d, n) {
2075
+ let total = 0;
2076
+ let alt = false;
2077
+ for (let i = n - 1; i >= 0; i--) {
2078
+ let x = d[i];
2079
+ if (alt && (x *= 2) > 9) x -= 9;
2080
+ total += x;
2081
+ alt = !alt;
2082
+ }
2083
+ return total % 10 === 0;
2084
+ }
2085
2085
  function cardPrefix(s) {
2086
- const digits = s.replace(/\D/g, "");
2087
- if (/^3[1-3]|^2[019]/.test(digits)) return 0;
2088
- const sep = s.search(/[ -]/);
2089
- if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
2086
+ const d = CARD_DIGITS;
2090
2087
  const ends = [];
2091
- let n = 0;
2088
+ const counts = [];
2089
+ let digits = 0;
2090
+ let sep = -1;
2092
2091
  for (let i = 0, start = 0; i <= s.length; i++) {
2093
- if (i < s.length && s.charCodeAt(i) >= 48 && s.charCodeAt(i) <= 57) continue;
2092
+ const c = i < s.length ? s.charCodeAt(i) : -1;
2093
+ if (c >= 48 && c <= 57) {
2094
+ if (digits < d.length) d[digits] = c - 48;
2095
+ digits++;
2096
+ continue;
2097
+ }
2098
+ if (sep < 0 && c !== -1) sep = i;
2094
2099
  if (ends.length && i - start < 3) break;
2095
- n += i - start;
2096
- ends.push([i, n]);
2100
+ ends.push(i);
2101
+ counts.push(digits);
2097
2102
  start = i + 1;
2098
2103
  }
2104
+ if (d[0] === 3 && d[1] >= 1 && d[1] <= 3 || d[0] === 2 && (d[1] === 0 || d[1] === 1 || d[1] === 9)) return 0;
2105
+ if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
2099
2106
  for (let k = ends.length - 1; k >= 0; k--) {
2100
- const [end, count] = ends[k];
2101
- if (count >= 13 && count <= 19 && luhnOk(digits.slice(0, count)) && issuerOk(digits.slice(0, count))) return end;
2107
+ const count = counts[k];
2108
+ if (count < 13 || count > 19 || !luhnDigits(d, count)) continue;
2109
+ let str8 = "";
2110
+ for (let j = 0; j < count; j++) str8 += String.fromCharCode(48 + d[j]);
2111
+ if (issuerOk(str8)) return ends[k];
2102
2112
  }
2103
2113
  return 0;
2104
2114
  }
@@ -2181,7 +2191,7 @@ function redactDeep(value, opts = {}) {
2181
2191
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
2182
2192
  return value;
2183
2193
  }
2184
- var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
2194
+ var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, CARD_DIGITS, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
2185
2195
  var init_redact = __esm({
2186
2196
  "../core/src/redact.ts"() {
2187
2197
  "use strict";
@@ -2257,6 +2267,7 @@ var init_redact = __esm({
2257
2267
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2258
2268
  PATH_RE = /^(?:~|\.{1,2})?\/[\w.@%+~/-]*$|^[A-Za-z]:\\/;
2259
2269
  credentialish = (v) => /[\d\W_]/.test(v) || v.length >= 16 && /[a-z]/.test(v) && /[A-Z]/.test(v);
2270
+ CARD_DIGITS = new Uint8Array(64);
2260
2271
  ASSET_EXT = /^\.(?:m?js|cjs|jsx|tsx?|css|scss|map|json|html?|wasm|woff2?|ttf|otf|eot|png|jpe?g|gif|svg|webp|avif|ico|bmp|mp[34]|webm|wav|ogg|pdf|txt|md|zip|gz|br|tgz|tar|whl|jar|bin|dat|lock|log|ya?ml|xml|csv)\b/;
2261
2272
  NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
2262
2273
  NAME = String.raw`(?=[A-Za-z_$])(?=[\w.$-]{0,64}?(?:key|token|secret|pass|pwd|credential|auth|cookie|session|creds))[A-Za-z_$][\w.$-]{0,127}`;
@@ -2610,7 +2621,7 @@ ${input.digestNote ? `${input.digestNote}
2610
2621
 
2611
2622
  ` : ""}${input.transcriptDigest}
2612
2623
  ` : ""}
2613
- ${input.focus ? `## What to capture
2624
+ ${input.feedback ? feedbackSection(input.feedback) : ""}${input.focus ? `## What to capture
2614
2625
 
2615
2626
  ${input.focus}
2616
2627
  ` : ""}${guidelinesSection(input.guidelines, { forReflection: true })}
@@ -2690,6 +2701,34 @@ At most ${max} suggestions. Schema:
2690
2701
  "rationale": string, "urgency": "normal"|"urgent",
2691
2702
  "signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
2692
2703
  }
2704
+ function feedbackSection(f) {
2705
+ return `
2706
+ ## Feedback from the person (human steering)
2707
+
2708
+ ${f.lesson ? "A person reviewed a lesson reflection took from this session" : "The person who worked in this session read what reflection took from it"} and wrote:
2709
+
2710
+ <feedback>
2711
+ ${f.text.trim()}
2712
+ </feedback>
2713
+ ${f.lesson ? `
2714
+ The lesson it is about:
2715
+
2716
+ <lesson>
2717
+ ${f.lesson.trim()}
2718
+ </lesson>
2719
+ ` : ""}
2720
+ Treat it as a correction from a person (signal "steering"): the strongest evidence there is. Act on it:
2721
+ - If it says the session taught something that was missed, capture that lesson in the right
2722
+ document, written as the guidelines below ask. ${f.transcript ? "Find it in the session below." : "The session's trace is no longer available: work from the feedback itself (the person's own account), the lessons already taken and the documents."}
2723
+ - If it corrects a lesson (too specific, wrong, in the wrong place), propose the corrected lesson
2724
+ (op "replace" if it already landed in a document, otherwise a new one) and name the lesson it
2725
+ corrects in the rationale. Lessons listed as already taken may be corrected this way.
2726
+ - If it says how lessons should be captured or written in general, propose a rule for the
2727
+ reflection guidelines, merged with the rules already there: never repeat one; refine an existing
2728
+ rule with op "replace" when the feedback sharpens it.
2729
+ Answer "nothing_new" only if everything it asks for is already in the library or proposed.
2730
+ `;
2731
+ }
2693
2732
  function documentsSection(docs) {
2694
2733
  if (!docs?.length) return "";
2695
2734
  const body = docs.map((d) => `### ${d.id}: ${d.title} (${d.why})
@@ -2954,6 +2993,16 @@ var init_profile = __esm({
2954
2993
  }
2955
2994
  });
2956
2995
 
2996
+ // ../core/src/v2/sources.ts
2997
+ function sourceId(kind, name) {
2998
+ return `${kind}:${name.trim()}`;
2999
+ }
3000
+ var init_sources = __esm({
3001
+ "../core/src/v2/sources.ts"() {
3002
+ "use strict";
3003
+ }
3004
+ });
3005
+
2957
3006
  // ../core/src/v2/index.ts
2958
3007
  var init_v2 = __esm({
2959
3008
  "../core/src/v2/index.ts"() {
@@ -2975,6 +3024,7 @@ var init_v2 = __esm({
2975
3024
  init_research();
2976
3025
  init_profile();
2977
3026
  init_guidelines();
3027
+ init_sources();
2978
3028
  }
2979
3029
  });
2980
3030
 
@@ -3595,7 +3645,7 @@ var init_src = __esm({
3595
3645
  // src/v2/store.ts
3596
3646
  import fs6 from "node:fs";
3597
3647
  import path5 from "node:path";
3598
- import { randomBytes as randomBytes3 } from "node:crypto";
3648
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
3599
3649
  function hexId(prefix) {
3600
3650
  return `${prefix}_${randomBytes3(6).toString("hex")}`;
3601
3651
  }
@@ -3617,7 +3667,7 @@ function normalizeSession(id, raw) {
3617
3667
  hinted: list2(raw.hinted)
3618
3668
  };
3619
3669
  }
3620
- var SAFE_ID, fileSafe, V2Store, addUnique;
3670
+ var SAFE_ID, fileSafe, V2Store, rootKey, addUnique;
3621
3671
  var init_store2 = __esm({
3622
3672
  "src/v2/store.ts"() {
3623
3673
  "use strict";
@@ -3752,11 +3802,12 @@ var init_store2 = __esm({
3752
3802
  return { status: raw.status, source: raw.source || "robot", ...raw.metrics ? { metrics: raw.metrics } : {}, at };
3753
3803
  }
3754
3804
  // ── repo index / sync state ──
3755
- readRepoIndex() {
3756
- return readJson(this.p("repo-index.json"), null);
3805
+ /** One index per working tree: a store covering a folder of repositories keeps each one's. */
3806
+ readRepoIndex(root) {
3807
+ return readJson(this.p("repo-index", `${rootKey(root)}.json`), null);
3757
3808
  }
3758
- writeRepoIndex(index) {
3759
- writeJsonAtomic(this.p("repo-index.json"), index);
3809
+ writeRepoIndex(root, index) {
3810
+ writeJsonAtomic(this.p("repo-index", `${rootKey(root)}.json`), index);
3760
3811
  }
3761
3812
  readSyncState() {
3762
3813
  return readJson(this.p("sync.json"), {});
@@ -3824,6 +3875,7 @@ var init_store2 = __esm({
3824
3875
  return out;
3825
3876
  }
3826
3877
  };
3878
+ rootKey = (root) => createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 16);
3827
3879
  addUnique = (list2, ...ids) => {
3828
3880
  for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
3829
3881
  };
@@ -3831,7 +3883,8 @@ var init_store2 = __esm({
3831
3883
  });
3832
3884
 
3833
3885
  // src/v2/deploy.ts
3834
- import { createHash as createHash2 } from "node:crypto";
3886
+ import { execFileSync } from "node:child_process";
3887
+ import { createHash as createHash3 } from "node:crypto";
3835
3888
  import fs7 from "node:fs";
3836
3889
  import os2 from "node:os";
3837
3890
  import path6 from "node:path";
@@ -3844,46 +3897,31 @@ function deployWorkspace(env = process.env) {
3844
3897
  return null;
3845
3898
  }
3846
3899
  }
3847
- function deployPaths(env = process.env) {
3900
+ function deployPaths(env = process.env, orgId) {
3848
3901
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3849
3902
  const ws = deployWorkspace(env);
3903
+ const state = path6.join(home, ".bunshin", "deploy");
3904
+ const shared = { claude: path6.join(ws ?? home, ".claude", "skills"), codex: path6.join(ws ?? home, ".codex", "skills") };
3905
+ const org = orgId ? path6.join(state, orgKey(orgId)) : null;
3850
3906
  return {
3851
3907
  home,
3852
- claudeSkills: path6.join(ws ?? home, ".claude", "skills"),
3853
- codexSkills: path6.join(ws ?? home, ".codex", "skills"),
3908
+ claudeSkills: org ? path6.join(org, "claude-skills") : shared.claude,
3909
+ codexSkills: org ? path6.join(org, "codex-skills") : shared.codex,
3910
+ ...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path6.join(home, ".claude", "skills"), path6.join(home, ".codex", "skills"), path6.join(home, ".bunshin", "library")])] } : {},
3854
3911
  claudeMd: ws ? path6.join(ws, "CLAUDE.md") : path6.join(home, ".claude", "CLAUDE.md"),
3855
3912
  agentsMd: ws ? path6.join(ws, "AGENTS.md") : path6.join(home, ".codex", "AGENTS.md"),
3856
- library: path6.join(home, ".bunshin", "library"),
3913
+ library: org ? path6.join(org, "library") : path6.join(home, ".bunshin", "library"),
3914
+ // Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
3857
3915
  bin: path6.join(home, ".bunshin", "bin"),
3858
- state: path6.join(home, ".bunshin", "deploy")
3916
+ state
3859
3917
  };
3860
3918
  }
3861
- function setDeployWorkspace(dir, env = process.env) {
3862
- const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3863
- const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
3864
- if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
3865
- const stateDir = path6.join(home, ".bunshin", "deploy");
3866
- const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
3867
- if (f === "settings.json" || !f.endsWith(".json")) return false;
3868
- try {
3869
- return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
3870
- } catch {
3871
- return false;
3872
- }
3873
- });
3874
- if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
3875
- throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
3876
- }
3877
- fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
3878
- fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
3879
- return ws;
3880
- }
3881
3919
  function statePath(p, orgId) {
3882
- return path6.join(p.state, `${orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org"}.json`);
3920
+ return path6.join(p.state, `${orgKey(orgId)}.json`);
3883
3921
  }
3884
3922
  function readState(p, orgId) {
3885
3923
  const s = readJson(statePath(p, orgId), null);
3886
- return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
3924
+ return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.linked?.length ? { linked: s.linked } : {}, ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
3887
3925
  }
3888
3926
  function readText(file) {
3889
3927
  try {
@@ -4205,7 +4243,7 @@ function captureInto(v2, config, ctx, state, p, harnesses, now, incoming, captur
4205
4243
  return captured.map((s) => ({ suggestionId: s.id, op: s.op.op, ...s.target.sectionId ? { sectionId: s.target.sectionId } : {}, name: s.summary ?? s.op.op }));
4206
4244
  }
4207
4245
  function captureEdits(v2, config, ctx, opts = {}) {
4208
- const p = opts.paths ?? deployPaths();
4246
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4209
4247
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4210
4248
  const release = lockDeploy(p);
4211
4249
  if (!release) return [];
@@ -4245,11 +4283,14 @@ function lockDeploy(p) {
4245
4283
  }
4246
4284
  return null;
4247
4285
  }
4286
+ function linkHarnesses(config) {
4287
+ return [.../* @__PURE__ */ new Set([config.harness, ...config.deploy?.harnesses ?? []])].filter((h) => h === "claude-code" || h === "codex");
4288
+ }
4248
4289
  function deployEnabled(config, ctx) {
4249
4290
  return ctx.kind === "person" && config.deploy?.enabled !== false;
4250
4291
  }
4251
4292
  function deployLibrary(v2, config, ctx, bundle, opts = {}) {
4252
- const p = opts.paths ?? deployPaths();
4293
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4253
4294
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4254
4295
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4255
4296
  const release = lockDeploy(p);
@@ -4266,6 +4307,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4266
4307
  const plan = planDeploy(bundle, p, harnesses);
4267
4308
  report.warnings.push(...plan.warnings);
4268
4309
  const owned = (abs) => [p.claudeSkills, p.codexSkills, p.library].some((r) => insideRoot(r, abs));
4310
+ const removable = (abs) => owned(abs) || (p.legacy ?? []).some((r) => insideRoot(r, abs));
4269
4311
  if (opts.capture !== false) report.captured = captureInto(v2, config, ctx, state, p, harnesses, now, plan.skills.map((x) => x.skill.name), opts.captureNewSkills);
4270
4312
  const decided = new Map(bundle.decisions.map((d) => [d.suggestionId, d.decision.status]));
4271
4313
  const localStatus = (id) => decided.get(id) ?? v2.readSuggestion(id)?.status;
@@ -4323,7 +4365,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4323
4365
  prevByAbs.delete(`${t.abs}#${t.role}`);
4324
4366
  }
4325
4367
  for (const old of prevByAbs.values()) {
4326
- if (old.role === "block" || d.contents.has(old.abs) || !owned(old.abs)) continue;
4368
+ if (old.role === "block" || d.contents.has(old.abs) || !removable(old.abs)) continue;
4327
4369
  const cur = readText(old.abs);
4328
4370
  if (cur !== null && sha(cur) === old.sha) {
4329
4371
  removeFile(old.abs);
@@ -4332,6 +4374,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4332
4374
  }
4333
4375
  }
4334
4376
  if (d.kind === "skill") {
4377
+ for (const dir of prev?.dirs ?? []) {
4378
+ if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4379
+ removeFile(path6.join(dir, MANIFEST));
4380
+ pruneDirs(dir, path6.dirname(dir));
4381
+ }
4335
4382
  for (const dir of d.dirs ?? []) {
4336
4383
  const manifest = safeJoin(dir, MANIFEST);
4337
4384
  if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
@@ -4350,7 +4397,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4350
4397
  continue;
4351
4398
  }
4352
4399
  for (const t of doc.targets) {
4353
- if (t.role === "block" || !owned(t.abs)) continue;
4400
+ if (t.role === "block" || !removable(t.abs)) continue;
4354
4401
  const cur = readText(t.abs);
4355
4402
  if (cur !== null && sha(cur) === t.sha) {
4356
4403
  removeFile(t.abs);
@@ -4358,7 +4405,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4358
4405
  }
4359
4406
  }
4360
4407
  for (const dir of doc.dirs ?? []) {
4361
- if (!owned(dir)) continue;
4408
+ if (!removable(dir)) continue;
4362
4409
  removeFile(path6.join(dir, MANIFEST));
4363
4410
  pruneDirs(dir, path6.dirname(dir));
4364
4411
  if (fs7.existsSync(dir)) {
@@ -4405,6 +4452,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4405
4452
  const link = path6.join(dir, ".claude", "skills");
4406
4453
  try {
4407
4454
  fs7.lstatSync(link);
4455
+ if (fs7.readlinkSync(link) !== p.claudeSkills) {
4456
+ fs7.unlinkSync(link);
4457
+ fs7.symlinkSync(p.claudeSkills, link);
4458
+ }
4408
4459
  } catch {
4409
4460
  ensureDir(path6.dirname(link));
4410
4461
  ensureDir(p.claudeSkills);
@@ -4423,14 +4474,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4423
4474
  } catch {
4424
4475
  }
4425
4476
  }
4426
- const saved = { ...state, docs: next, links: [...links], ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
4477
+ let linked = state.linked ?? [];
4478
+ if (p.legacy) {
4479
+ const names = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length).map((d) => d.name);
4480
+ const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path6.resolve(opts.linkRoot)] : []])];
4481
+ linked = [];
4482
+ for (const root of roots) {
4483
+ const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
4484
+ report.warnings.push(...res.warnings);
4485
+ if (res.kept) linked.push(root);
4486
+ }
4487
+ }
4488
+ const saved = { ...state, docs: next, links: [...links], ...linked.length ? { linked } : {}, ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
4489
+ if (!linked.length) delete saved.linked;
4427
4490
  writeJsonAtomic(statePath(p, config.orgId), saved);
4428
4491
  report.libraryVersion = bundle.libraryVersion;
4429
4492
  return report;
4430
4493
  }
4431
- function deployedState(config, paths = deployPaths()) {
4494
+ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
4432
4495
  const s = readState(paths, config.orgId);
4433
- return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, docs: Object.values(s.docs) };
4496
+ return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
4434
4497
  }
4435
4498
  function materialize(sections, dir) {
4436
4499
  let files = 0;
@@ -4453,7 +4516,89 @@ function materialize(sections, dir) {
4453
4516
  }
4454
4517
  return { files };
4455
4518
  }
4456
- var settingsFile, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS;
4519
+ function linkSkills(p, root, harnesses, names) {
4520
+ const out = { linked: 0, removed: 0, kept: false, warnings: [] };
4521
+ const abs = path6.resolve(root);
4522
+ if (abs === path6.resolve(p.home) || abs === path6.resolve(os2.homedir()) || !fs7.existsSync(abs)) return out;
4523
+ const managed = (target) => target.startsWith(path6.resolve(p.state) + path6.sep);
4524
+ const excluded = [];
4525
+ for (const h of harnesses) {
4526
+ const spec = LINK_DIRS[h];
4527
+ if (!spec) continue;
4528
+ const dir = path6.join(abs, spec.rel);
4529
+ const canonical = path6.resolve(p[spec.key]);
4530
+ const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs7.existsSync(path6.join(canonical, n, "SKILL.md"))));
4531
+ let entries = [];
4532
+ try {
4533
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
4534
+ } catch {
4535
+ }
4536
+ for (const e of entries) {
4537
+ if (!e.isSymbolicLink()) continue;
4538
+ const link = path6.join(dir, e.name);
4539
+ let target = "";
4540
+ try {
4541
+ target = path6.resolve(dir, fs7.readlinkSync(link));
4542
+ } catch {
4543
+ continue;
4544
+ }
4545
+ if (!managed(target)) continue;
4546
+ if (want.has(e.name) && target === path6.join(canonical, e.name)) {
4547
+ want.delete(e.name);
4548
+ excluded.push(path6.join(spec.rel, e.name));
4549
+ continue;
4550
+ }
4551
+ removeFile(link);
4552
+ out.removed++;
4553
+ }
4554
+ for (const name of want) {
4555
+ const link = path6.join(dir, name);
4556
+ try {
4557
+ fs7.lstatSync(link);
4558
+ out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
4559
+ continue;
4560
+ } catch {
4561
+ }
4562
+ try {
4563
+ ensureDir(dir);
4564
+ fs7.symlinkSync(path6.join(canonical, name), link, "dir");
4565
+ excluded.push(path6.join(spec.rel, name));
4566
+ out.linked++;
4567
+ } catch (err) {
4568
+ out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
4569
+ }
4570
+ }
4571
+ pruneDirs(dir, abs);
4572
+ }
4573
+ out.kept = excluded.length > 0;
4574
+ gitExclude(abs, excluded);
4575
+ return out;
4576
+ }
4577
+ function gitExclude(root, rels) {
4578
+ if (!fs7.existsSync(path6.join(root, ".git"))) return;
4579
+ let file;
4580
+ try {
4581
+ file = path6.resolve(root, execFileSync("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
4582
+ } catch {
4583
+ return;
4584
+ }
4585
+ const cur = readText(file) ?? "";
4586
+ const start = cur.indexOf(EXCLUDE_BEGIN);
4587
+ const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
4588
+ const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
4589
+ const block2 = rels.length ? `${EXCLUDE_BEGIN}
4590
+ ${[...new Set(rels)].sort().map((r) => `/${r.split(path6.sep).join("/")}`).join("\n")}
4591
+ ${EXCLUDE_END}
4592
+ ` : "";
4593
+ const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
4594
+ if (next === cur) return;
4595
+ try {
4596
+ ensureDir(path6.dirname(file));
4597
+ fs7.writeFileSync(file, next);
4598
+ } catch {
4599
+ }
4600
+ }
4601
+ var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
4457
4602
  var init_deploy = __esm({
4458
4603
  "src/v2/deploy.ts"() {
4459
4604
  "use strict";
@@ -4461,7 +4606,8 @@ var init_deploy = __esm({
4461
4606
  init_util();
4462
4607
  init_store2();
4463
4608
  settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
4464
- sha = (text2) => createHash2("sha256").update(text2).digest("hex");
4609
+ orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
4610
+ sha = (text2) => createHash3("sha256").update(text2).digest("hex");
4465
4611
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
4466
4612
  BLOCK_END = "<!-- bunshin:end -->";
4467
4613
  MANIFEST = ".bunshin.json";
@@ -4479,6 +4625,12 @@ var init_deploy = __esm({
4479
4625
  LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
4480
4626
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
4481
4627
  BORN_SLACK_MS = 2e3;
4628
+ LINK_DIRS = {
4629
+ "claude-code": { rel: path6.join(".claude", "skills"), key: "claudeSkills" },
4630
+ codex: { rel: path6.join(".codex", "skills"), key: "codexSkills" }
4631
+ };
4632
+ EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
4633
+ EXCLUDE_END = "# <<< bunshin";
4482
4634
  }
4483
4635
  });
4484
4636
 
@@ -4810,10 +4962,10 @@ var init_transcript = __esm({
4810
4962
  // src/v2/repo.ts
4811
4963
  import fs9 from "node:fs";
4812
4964
  import path7 from "node:path";
4813
- import { execFileSync } from "node:child_process";
4965
+ import { execFileSync as execFileSync2 } from "node:child_process";
4814
4966
  function git(cwd, args, timeout = 1500) {
4815
4967
  try {
4816
- return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
4968
+ return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
4817
4969
  } catch {
4818
4970
  return null;
4819
4971
  }
@@ -4883,7 +5035,7 @@ function listDocs(root) {
4883
5035
  function indexWorkingTree(v2, config, cwd, opts = {}) {
4884
5036
  const root = repoRoot(cwd, v2.store.root);
4885
5037
  const now = opts.now ?? Date.now();
4886
- const cached = v2.readRepoIndex();
5038
+ const cached = v2.readRepoIndex(root);
4887
5039
  const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
4888
5040
  const sameRepo = cached && cached.ownerTeam === ownerTeam;
4889
5041
  const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
@@ -4921,7 +5073,7 @@ function indexWorkingTree(v2, config, cwd, opts = {}) {
4921
5073
  };
4922
5074
  if (changed) {
4923
5075
  try {
4924
- v2.writeRepoIndex(index);
5076
+ v2.writeRepoIndex(root, index);
4925
5077
  } catch {
4926
5078
  }
4927
5079
  }
@@ -5506,6 +5658,110 @@ var init_ui = __esm({
5506
5658
  }
5507
5659
  });
5508
5660
 
5661
+ // src/v2/source.ts
5662
+ var source_exports = {};
5663
+ __export(source_exports, {
5664
+ cwdInPool: () => cwdInPool,
5665
+ inPool: () => inPool,
5666
+ listFolders: () => listFolders,
5667
+ projectRootFor: () => projectRootFor,
5668
+ select: () => select,
5669
+ sourceFor: () => sourceFor,
5670
+ storeHome: () => storeHome,
5671
+ topFolder: () => topFolder
5672
+ });
5673
+ import fs13 from "node:fs";
5674
+ import path9 from "node:path";
5675
+ import { execFileSync as execFileSync3 } from "node:child_process";
5676
+ function storeHome(storeRoot) {
5677
+ return path9.dirname(path9.resolve(storeRoot));
5678
+ }
5679
+ function topFolder(cwd, storeRoot) {
5680
+ const rel = path9.relative(storeHome(storeRoot), path9.resolve(cwd));
5681
+ if (!rel || rel.startsWith("..") || path9.isAbsolute(rel)) return "";
5682
+ return rel.split(path9.sep)[0] ?? "";
5683
+ }
5684
+ function inPool(selection, folder) {
5685
+ if (!folder || !selection) return true;
5686
+ if (selection.exclude?.includes(folder)) return false;
5687
+ return selection.only ? selection.only.includes(folder) : true;
5688
+ }
5689
+ function cwdInPool(config, cwd, storeRoot) {
5690
+ return inPool(config.sources, topFolder(cwd, storeRoot));
5691
+ }
5692
+ function git2(cwd, args) {
5693
+ try {
5694
+ return execFileSync3("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
5695
+ } catch {
5696
+ return null;
5697
+ }
5698
+ }
5699
+ function sourceFor(cwd, storeRoot, ctx) {
5700
+ if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
5701
+ const home = storeHome(storeRoot);
5702
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
5703
+ if (top) {
5704
+ const remote = git2(top, ["remote", "get-url", "origin"]);
5705
+ const named = remote ? parseRemote(remote) : null;
5706
+ if (named) return sourceId("repo", named);
5707
+ const rel = path9.relative(home, top);
5708
+ return sourceId("folder", rel && !rel.startsWith("..") && !path9.isAbsolute(rel) ? rel.split(path9.sep).join("/") : path9.basename(top));
5709
+ }
5710
+ const folder = topFolder(cwd, storeRoot);
5711
+ return folder ? sourceId("folder", folder) : void 0;
5712
+ }
5713
+ function projectRootFor(cwd, storeRoot) {
5714
+ const home = storeHome(storeRoot);
5715
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
5716
+ if (top) {
5717
+ const rel = path9.relative(home, top);
5718
+ if (!rel.startsWith("..") && !path9.isAbsolute(rel)) return top;
5719
+ }
5720
+ const folder = topFolder(cwd, storeRoot);
5721
+ return folder ? path9.join(home, folder) : home;
5722
+ }
5723
+ function listFolders(storeRoot, selection, opts = {}) {
5724
+ const home = storeHome(storeRoot);
5725
+ let names = [];
5726
+ try {
5727
+ names = fs13.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
5728
+ } catch {
5729
+ }
5730
+ return names.map((folder) => ({
5731
+ folder,
5732
+ included: inPool(selection, folder),
5733
+ ...opts.sources ? { source: sourceFor(path9.join(home, folder), storeRoot) } : {}
5734
+ }));
5735
+ }
5736
+ function select(current, action, folders) {
5737
+ const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
5738
+ const only = current?.only;
5739
+ const exclude = current?.exclude ?? [];
5740
+ let next;
5741
+ switch (action) {
5742
+ case "all":
5743
+ return void 0;
5744
+ case "only":
5745
+ next = { only: uniq(folders) };
5746
+ break;
5747
+ case "include":
5748
+ next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
5749
+ break;
5750
+ case "exclude":
5751
+ next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
5752
+ break;
5753
+ }
5754
+ if (!next.exclude?.length) delete next.exclude;
5755
+ return next.only || next.exclude ? next : void 0;
5756
+ }
5757
+ var init_source = __esm({
5758
+ "src/v2/source.ts"() {
5759
+ "use strict";
5760
+ init_v22();
5761
+ init_repo();
5762
+ }
5763
+ });
5764
+
5509
5765
  // src/v2/sync.ts
5510
5766
  var sync_exports = {};
5511
5767
  __export(sync_exports, {
@@ -5515,8 +5771,8 @@ __export(sync_exports, {
5515
5771
  syncV2: () => syncV2,
5516
5772
  uploadIndex: () => uploadIndex
5517
5773
  });
5518
- import fs13 from "node:fs";
5519
- import path9 from "node:path";
5774
+ import fs14 from "node:fs";
5775
+ import path10 from "node:path";
5520
5776
  function episodePayload(e) {
5521
5777
  const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
5522
5778
  void _o, _u, _s, _rs, _rl, _tp;
@@ -5550,7 +5806,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5550
5806
  if (e.upload === "header-only" || e.cliVersion || !e.transcript) continue;
5551
5807
  if (now - Date.parse(e.endedAt || e.startedAt) > days * 864e5) continue;
5552
5808
  const file = e.transcriptPath ?? v2.readSession(e.sessionId)?.transcriptPath;
5553
- if (!file || !fs13.existsSync(file)) {
5809
+ if (!file || !fs14.existsSync(file)) {
5554
5810
  missing++;
5555
5811
  continue;
5556
5812
  }
@@ -5565,7 +5821,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5565
5821
  async function syncV2(store, opts = {}) {
5566
5822
  const v2 = new V2Store(store);
5567
5823
  const config = store.config();
5568
- const cwd = opts.cwd ?? path9.dirname(store.root);
5824
+ const cwd = opts.cwd ?? path10.dirname(store.root);
5569
5825
  const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
5570
5826
  const deadline = Date.now() + (opts.budgetMs ?? 24e4);
5571
5827
  if (report.offline) {
@@ -5681,7 +5937,8 @@ async function syncV2(store, opts = {}) {
5681
5937
  const bundle = v2.readBundle();
5682
5938
  if (bundle) {
5683
5939
  try {
5684
- report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false });
5940
+ const linkRoot = cwd && cwdInPool(config, cwd, store.root) ? projectRootFor(cwd, store.root) : void 0;
5941
+ report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false, ...linkRoot ? { linkRoot } : {}, linkHarnesses: linkHarnesses(config) });
5685
5942
  } catch (err) {
5686
5943
  report.errors.push(`deploy: ${err instanceof Error ? err.message : String(err)}`);
5687
5944
  }
@@ -5711,7 +5968,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
5711
5968
  const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
5712
5969
  const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
5713
5970
  try {
5714
- return [{ path: rel, text: fs13.readFileSync(path9.join(root, rel), "utf8") }];
5971
+ return [{ path: rel, text: fs14.readFileSync(path10.join(root, rel), "utf8") }];
5715
5972
  } catch {
5716
5973
  return [];
5717
5974
  }
@@ -5729,6 +5986,7 @@ var init_sync = __esm({
5729
5986
  init_api();
5730
5987
  init_apply();
5731
5988
  init_deploy();
5989
+ init_source();
5732
5990
  init_repo();
5733
5991
  init_store2();
5734
5992
  init_view();
@@ -5739,10 +5997,10 @@ var init_sync = __esm({
5739
5997
 
5740
5998
  // src/v2/reflect.ts
5741
5999
  import { spawn as spawn3 } from "node:child_process";
5742
- import { createHash as createHash3 } from "node:crypto";
5743
- import fs14 from "node:fs";
6000
+ import { createHash as createHash4 } from "node:crypto";
6001
+ import fs15 from "node:fs";
5744
6002
  function episodeIdFor(contextId, sessionId) {
5745
- return `e_${createHash3("sha256").update(`${contextId}
6003
+ return `e_${createHash4("sha256").update(`${contextId}
5746
6004
  ${sessionId}`).digest("hex").slice(0, 16)}`;
5747
6005
  }
5748
6006
  function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT_MS } = {}) {
@@ -5775,7 +6033,7 @@ function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT
5775
6033
  };
5776
6034
  let child;
5777
6035
  try {
5778
- child = spawn3(bin, args, { cwd: cwd && fs14.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
6036
+ child = spawn3(bin, args, { cwd: cwd && fs15.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
5779
6037
  } catch (e) {
5780
6038
  resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
5781
6039
  return;
@@ -5820,7 +6078,7 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
5820
6078
  return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
5821
6079
  }
5822
6080
  function humanTurnsOf(transcriptPath) {
5823
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return [];
6081
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return [];
5824
6082
  try {
5825
6083
  return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
5826
6084
  } catch {
@@ -5828,7 +6086,7 @@ function humanTurnsOf(transcriptPath) {
5828
6086
  }
5829
6087
  }
5830
6088
  function readFacts(transcriptPath) {
5831
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return null;
6089
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return null;
5832
6090
  try {
5833
6091
  return parseTranscript(transcriptPath);
5834
6092
  } catch {
@@ -5891,7 +6149,7 @@ async function runReflectionV2(store, opts) {
5891
6149
  result.method = "fork";
5892
6150
  } else result.error = fork.error;
5893
6151
  }
5894
- if (result.method === "none" && session.transcriptPath && fs14.existsSync(session.transcriptPath)) {
6152
+ if (result.method === "none" && session.transcriptPath && fs15.existsSync(session.transcriptPath)) {
5895
6153
  const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
5896
6154
  const res = await askJson(config, {
5897
6155
  system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
@@ -5912,8 +6170,10 @@ async function runReflectionV2(store, opts) {
5912
6170
  result.nothingNew = result.method !== "none" && parsed.length === 0;
5913
6171
  const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
5914
6172
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6173
+ const source = session.source !== void 0 ? session.source || void 0 : sourceFor(session.cwd || opts.cwd || process.cwd(), store.root, view.ctx);
5915
6174
  for (const p of parsed) {
5916
6175
  const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
6176
+ if (source) s.source = source;
5917
6177
  s.summary = summarize(s.op);
5918
6178
  if (apply) {
5919
6179
  const outcome = applyLocally(v2, view, s);
@@ -5997,6 +6257,7 @@ var init_reflect3 = __esm({
5997
6257
  init_signals();
5998
6258
  init_ui();
5999
6259
  init_view();
6260
+ init_source();
6000
6261
  FORK_TIMEOUT_MS = 12e4;
6001
6262
  FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
6002
6263
  }
@@ -6014,51 +6275,51 @@ __export(uicmd_exports, {
6014
6275
  modSource: () => modSource
6015
6276
  });
6016
6277
  import { spawnSync } from "node:child_process";
6017
- import fs15 from "node:fs";
6278
+ import fs16 from "node:fs";
6018
6279
  import os3 from "node:os";
6019
- import path10 from "node:path";
6280
+ import path11 from "node:path";
6020
6281
  import { fileURLToPath } from "node:url";
6021
6282
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
6022
- return path10.join(home, ".bunshin", "claude-code-ui");
6283
+ return path11.join(home, ".bunshin", "claude-code-ui");
6023
6284
  }
6024
6285
  function modSource() {
6025
- const here = path10.dirname(fileURLToPath(import.meta.url));
6026
- for (const dir of [path10.join(here, "claude-code-ui"), path10.join(here, "..", "claude-code-ui"), path10.resolve(here, "../../../../adapters/claude-code-ui")]) {
6027
- if (fs15.existsSync(path10.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6286
+ const here = path11.dirname(fileURLToPath(import.meta.url));
6287
+ for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
6288
+ if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6028
6289
  }
6029
6290
  return null;
6030
6291
  }
6031
6292
  function installedModVersion(home) {
6032
6293
  try {
6033
- return JSON.parse(fs15.readFileSync(path10.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6294
+ return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6034
6295
  } catch {
6035
6296
  return null;
6036
6297
  }
6037
6298
  }
6038
6299
  function copyDir(from, to) {
6039
- fs15.mkdirSync(to, { recursive: true });
6040
- for (const e of fs15.readdirSync(from, { withFileTypes: true })) {
6300
+ fs16.mkdirSync(to, { recursive: true });
6301
+ for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
6041
6302
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
6042
6303
  if (e.name === "node_modules") continue;
6043
- const a = path10.join(from, e.name);
6044
- const b = path10.join(to, e.name);
6304
+ const a = path11.join(from, e.name);
6305
+ const b = path11.join(to, e.name);
6045
6306
  if (e.isDirectory()) copyDir(a, b);
6046
- else fs15.copyFileSync(a, b);
6307
+ else fs16.copyFileSync(a, b);
6047
6308
  }
6048
6309
  }
6049
6310
  function installMod(version2, opts = {}) {
6050
6311
  const src = modSource();
6051
6312
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
6052
6313
  const home = modHome(opts.home);
6053
- const dir = path10.join(home, MOD_PLUGIN);
6054
- fs15.rmSync(dir, { recursive: true, force: true });
6314
+ const dir = path11.join(home, MOD_PLUGIN);
6315
+ fs16.rmSync(dir, { recursive: true, force: true });
6055
6316
  copyDir(src, dir);
6056
- const manifest = path10.join(dir, ".claude-plugin", "plugin.json");
6057
- const m = JSON.parse(fs15.readFileSync(manifest, "utf8"));
6058
- fs15.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6059
- fs15.mkdirSync(path10.join(home, ".claude-plugin"), { recursive: true });
6060
- fs15.writeFileSync(
6061
- path10.join(home, ".claude-plugin", "marketplace.json"),
6317
+ const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
6318
+ const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
6319
+ fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6320
+ fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
6321
+ fs16.writeFileSync(
6322
+ path11.join(home, ".claude-plugin", "marketplace.json"),
6062
6323
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6063
6324
  );
6064
6325
  const run2 = opts.run ?? spawnSync;
@@ -6093,7 +6354,7 @@ async function cmdUi(store, args, out, err, version2) {
6093
6354
  `);
6094
6355
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6095
6356
  `);
6096
- 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 " + path10.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6357
+ 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");
6097
6358
  }
6098
6359
  return installed || !failed.length ? 0 : 1;
6099
6360
  }
@@ -6174,19 +6435,19 @@ __export(update_exports, {
6174
6435
  updateCheckDue: () => updateCheckDue,
6175
6436
  updateNotice: () => updateNotice
6176
6437
  });
6177
- import fs16 from "node:fs";
6178
- import path11 from "node:path";
6438
+ import fs17 from "node:fs";
6439
+ import path12 from "node:path";
6179
6440
  function readState2() {
6180
6441
  try {
6181
- return JSON.parse(fs16.readFileSync(stateFile(), "utf8"));
6442
+ return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6182
6443
  } catch {
6183
6444
  return {};
6184
6445
  }
6185
6446
  }
6186
6447
  function writeState(s) {
6187
6448
  try {
6188
- fs16.mkdirSync(path11.dirname(stateFile()), { recursive: true });
6189
- fs16.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6449
+ fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
6450
+ fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6190
6451
  } catch {
6191
6452
  }
6192
6453
  }
@@ -6236,14 +6497,14 @@ var init_update = __esm({
6236
6497
  PKG = "@bunshin-cc/bunshin";
6237
6498
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6238
6499
  DAY = 864e5;
6239
- stateFile = () => path11.join(homeDir(), ".bunshin", "update.json");
6500
+ stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6240
6501
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6241
6502
  }
6242
6503
  });
6243
6504
 
6244
6505
  // src/v2/hooks.ts
6245
- import fs17 from "node:fs";
6246
- import path12 from "node:path";
6506
+ import fs18 from "node:fs";
6507
+ import path13 from "node:path";
6247
6508
  function inContext(s) {
6248
6509
  return s.inContext ?? [...s.loaded, ...s.pushed];
6249
6510
  }
@@ -6258,6 +6519,12 @@ function touch(v2, ctx, patch = () => {
6258
6519
  const s = v2.updateSession(id, ctx.cwd, (s2) => {
6259
6520
  s2.cwd = ctx.cwd || s2.cwd;
6260
6521
  s2.harness = ctx.store.config().harness;
6522
+ if (s2.source === void 0) {
6523
+ try {
6524
+ s2.source = sourceFor(s2.cwd, ctx.store.root, contextFor(v2, ctx.store.config())) ?? "";
6525
+ } catch {
6526
+ }
6527
+ }
6261
6528
  if (ctx.input.transcript_path) s2.transcriptPath = ctx.input.transcript_path;
6262
6529
  patch(s2);
6263
6530
  });
@@ -6304,10 +6571,10 @@ async function keepModCurrent(ctx, config) {
6304
6571
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6305
6572
  const current = version2();
6306
6573
  if (!modSource2() || installedModVersion2() === current) return;
6307
- const marker = path12.join(modHome2(), `.attempted-${current}`);
6308
- if (fs17.existsSync(marker)) return;
6309
- fs17.mkdirSync(modHome2(), { recursive: true });
6310
- fs17.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6574
+ const marker = path13.join(modHome2(), `.attempted-${current}`);
6575
+ if (fs18.existsSync(marker)) return;
6576
+ fs18.mkdirSync(modHome2(), { recursive: true });
6577
+ fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6311
6578
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6312
6579
  } catch {
6313
6580
  }
@@ -6411,7 +6678,7 @@ function liveUpdate(v2, ctx, session) {
6411
6678
  }
6412
6679
  const config = ctx.store.config();
6413
6680
  const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
6414
- const paths = deployed ? deployPaths() : null;
6681
+ const paths = deployed ? deployPaths(process.env, config.orgId) : null;
6415
6682
  const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
6416
6683
  const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
6417
6684
  const { text: text2, next } = changesSince(session.live, bundle, skillPath);
@@ -6436,13 +6703,13 @@ function skillHint(sec, others) {
6436
6703
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6437
6704
  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}`;
6438
6705
  }
6439
- async function remoteAuto(ctx, prompt, exclude) {
6706
+ async function remoteAuto(ctx, prompt, exclude, source) {
6440
6707
  const config = ctx.store.config();
6441
6708
  if (!canCall(config) || !config.recall.useMothership) return null;
6442
6709
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6443
6710
  if (budget < 300) return null;
6444
6711
  try {
6445
- const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude }, { timeoutMs: budget });
6712
+ const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
6446
6713
  return res.available ? res : null;
6447
6714
  } catch {
6448
6715
  return null;
@@ -6497,7 +6764,7 @@ async function onPrompt(ctx) {
6497
6764
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6498
6765
  }
6499
6766
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6500
- const remote = await remoteAuto(ctx, prompt, injected);
6767
+ const remote = await remoteAuto(ctx, prompt, injected, session.source);
6501
6768
  if (remote) {
6502
6769
  const local = (id) => view.sections.find((s) => s.id === id);
6503
6770
  if (remote.section) {
@@ -6649,6 +6916,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6649
6916
  const v2 = new V2Store(store);
6650
6917
  const config = store.config();
6651
6918
  const view = localView(v2, config, session.cwd, { index: false });
6919
+ const source = sessionSource(store, session, view.ctx);
6652
6920
  const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
6653
6921
  const outcome = v2.readOutcome(session.sessionId);
6654
6922
  const turns = scan.facts.turns.length;
@@ -6688,6 +6956,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6688
6956
  ...outcome ? { outcome } : {},
6689
6957
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
6690
6958
  cliVersion: version(),
6959
+ ...source ? { source } : {},
6691
6960
  uploaded: false,
6692
6961
  ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
6693
6962
  ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
@@ -6695,6 +6964,14 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6695
6964
  };
6696
6965
  return { episode, gate, scan };
6697
6966
  }
6967
+ function sessionSource(store, session, ctx) {
6968
+ if (session.source !== void 0) return session.source || void 0;
6969
+ try {
6970
+ return sourceFor(session.cwd, store.root, ctx);
6971
+ } catch {
6972
+ return void 0;
6973
+ }
6974
+ }
6698
6975
  function checkpointDue(session, scan, now) {
6699
6976
  const since = Date.parse(session.checkpointAt ?? session.startedAt);
6700
6977
  if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
@@ -6809,6 +7086,7 @@ var init_hooks = __esm({
6809
7086
  init_signals();
6810
7087
  init_store2();
6811
7088
  init_ui();
7089
+ init_source();
6812
7090
  init_view();
6813
7091
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
6814
7092
  NUDGE_INTERVAL_MS = 60 * 6e4;
@@ -6823,6 +7101,8 @@ var init_hooks = __esm({
6823
7101
  });
6824
7102
 
6825
7103
  // src/hooks.ts
7104
+ import fs19 from "node:fs";
7105
+ import path14 from "node:path";
6826
7106
  function normalizeEvent(raw) {
6827
7107
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
6828
7108
  }
@@ -6866,6 +7146,8 @@ async function handleHook(rawEvent, input, opts = {}) {
6866
7146
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
6867
7147
  if (!store) return {};
6868
7148
  const config = store.config();
7149
+ if (!cwdInPool(config, cwd, store.root)) return {};
7150
+ if (deferToProject(cwd)) return {};
6869
7151
  const budgetMs = Math.min(opts.budgetMs ?? config.recall.hookBudgetMs ?? HARD_BUDGET_MS, HARD_BUDGET_MS);
6870
7152
  const deadline = Date.now() + budgetMs;
6871
7153
  try {
@@ -6886,6 +7168,24 @@ async function handleHook(rawEvent, input, opts = {}) {
6886
7168
  return {};
6887
7169
  }
6888
7170
  }
7171
+ function deferToProject(cwd, env = process.env) {
7172
+ if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
7173
+ let dir = path14.resolve(cwd);
7174
+ for (let i = 0; i < 40; i++) {
7175
+ for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
7176
+ try {
7177
+ const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
7178
+ if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
7179
+ } catch {
7180
+ }
7181
+ }
7182
+ if (fs19.existsSync(path14.join(dir, ".git"))) return false;
7183
+ const up = path14.dirname(dir);
7184
+ if (up === dir) return false;
7185
+ dir = up;
7186
+ }
7187
+ return false;
7188
+ }
6889
7189
  async function hookCommand(rawEvent, opts = {}) {
6890
7190
  const write = opts.write ?? ((s) => process.stdout.write(s));
6891
7191
  let output = {};
@@ -6908,6 +7208,7 @@ var init_hooks2 = __esm({
6908
7208
  init_detach();
6909
7209
  init_store();
6910
7210
  init_hooks();
7211
+ init_source();
6911
7212
  HARD_BUDGET_MS = 4e3;
6912
7213
  EVENT_NAMES = {
6913
7214
  "session-start": "session-start",
@@ -6927,32 +7228,32 @@ var init_hooks2 = __esm({
6927
7228
  });
6928
7229
 
6929
7230
  // src/project-hooks.ts
6930
- import fs18 from "node:fs";
6931
- import path13 from "node:path";
6932
- import { execFileSync as execFileSync2 } from "node:child_process";
7231
+ import fs20 from "node:fs";
7232
+ import path15 from "node:path";
7233
+ import { execFileSync as execFileSync4 } from "node:child_process";
6933
7234
  function codexProjectHooks(cwd) {
6934
- const legacyFile = path13.join(cwd, ".codex", "hooks.json");
6935
- const git4 = (...args) => execFileSync2("git", ["-C", cwd, "rev-parse", ...args], {
7235
+ const legacyFile = path15.join(cwd, ".codex", "hooks.json");
7236
+ const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
6936
7237
  encoding: "utf8",
6937
7238
  stdio: ["ignore", "pipe", "ignore"],
6938
7239
  timeout: 2e3
6939
7240
  }).trim();
6940
7241
  let root, common, directory;
6941
7242
  try {
6942
- root = git4("--show-toplevel");
6943
- common = git4("--path-format=absolute", "--git-common-dir");
6944
- directory = git4("--absolute-git-dir");
7243
+ root = git5("--show-toplevel");
7244
+ common = git5("--path-format=absolute", "--git-common-dir");
7245
+ directory = git5("--absolute-git-dir");
6945
7246
  } catch {
6946
7247
  return { file: legacyFile, shared: false, legacyFile };
6947
7248
  }
6948
- const linked = fs18.realpathSync(common) !== fs18.realpathSync(directory);
7249
+ const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
6949
7250
  if (linked) {
6950
- if (path13.basename(common) !== ".git" || !fs18.statSync(common).isDirectory()) {
7251
+ if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
6951
7252
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
6952
7253
  }
6953
- root = path13.dirname(common);
7254
+ root = path15.dirname(common);
6954
7255
  }
6955
- const file = fs18.realpathSync(root) === fs18.realpathSync(cwd) ? legacyFile : path13.join(root, ".codex", "hooks.json");
7256
+ const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
6956
7257
  return { file, shared: linked, legacyFile };
6957
7258
  }
6958
7259
  var init_project_hooks = __esm({
@@ -6975,6 +7276,7 @@ __export(init_exports, {
6975
7276
  installLearnCommand: () => installLearnCommand,
6976
7277
  installMcpConfig: () => installMcpConfig,
6977
7278
  installPiExtension: () => installPiExtension,
7279
+ installUserMcp: () => installUserMcp,
6978
7280
  isBunshinEntry: () => isBunshinEntry,
6979
7281
  mcpLauncher: () => mcpLauncher,
6980
7282
  mergeClaudeSettings: () => mergeClaudeSettings,
@@ -6982,8 +7284,9 @@ __export(init_exports, {
6982
7284
  removeBunshinHooks: () => removeBunshinHooks,
6983
7285
  runInit: () => runInit
6984
7286
  });
6985
- import fs19 from "node:fs";
6986
- import path14 from "node:path";
7287
+ import { execFileSync as execFileSync5 } from "node:child_process";
7288
+ import fs21 from "node:fs";
7289
+ import path16 from "node:path";
6987
7290
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
6988
7291
  function isBunshinEntry(entry) {
6989
7292
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -7008,13 +7311,13 @@ function removeBunshinHooks(settings) {
7008
7311
  else delete out.hooks;
7009
7312
  return out;
7010
7313
  }
7011
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code" } = {}) {
7314
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
7012
7315
  const base = removeBunshinHooks(settings);
7013
7316
  const hooks = { ...base.hooks ?? {} };
7014
7317
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
7015
7318
  const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
7016
7319
  const hook = bunshinHookEntry(arg, timeout, bin);
7017
- hook.command = `BUNSHIN_HARNESS=${harness} ${hook.command}`;
7320
+ hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
7018
7321
  if (harness === "codex") delete hook._bunshin;
7019
7322
  groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
7020
7323
  hooks[event] = groups;
@@ -7028,16 +7331,16 @@ function countBunshinHooks(settings) {
7028
7331
  );
7029
7332
  }
7030
7333
  function claudeSettingsPath() {
7031
- return path14.join(homeDir(), ".claude", "settings.json");
7334
+ return path16.join(homeDir(), ".claude", "settings.json");
7032
7335
  }
7033
7336
  function installPiExtension(cwd, remove = false, scope = "repo") {
7034
- const file = scope === "global" ? path14.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path14.join(cwd, ".pi/extensions/bunshin.js");
7337
+ const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
7035
7338
  const marker = "// Managed by Bunshin setup.";
7036
7339
  try {
7037
- const previous = fileExists(file) ? fs19.readFileSync(file, "utf8") : "";
7340
+ const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
7038
7341
  if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
7039
7342
  if (remove) {
7040
- if (previous) fs19.unlinkSync(file);
7343
+ if (previous) fs21.unlinkSync(file);
7041
7344
  return { file, installed: 0, changed: !!previous };
7042
7345
  }
7043
7346
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -7046,24 +7349,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
7046
7349
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
7047
7350
  `;
7048
7351
  if (text2 !== previous) {
7049
- ensureDir(path14.dirname(file));
7050
- fs19.writeFileSync(file, text2);
7352
+ ensureDir(path16.dirname(file));
7353
+ fs21.writeFileSync(file, text2);
7051
7354
  }
7052
7355
  return { file, installed: 1, changed: text2 !== previous };
7053
7356
  } catch (error) {
7054
7357
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
7055
7358
  }
7056
7359
  }
7057
- function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
7360
+ function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
7058
7361
  try {
7059
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7060
- const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
7362
+ const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7363
+ const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
7061
7364
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7062
7365
  if (changed) {
7063
- ensureDir(path14.dirname(file));
7366
+ ensureDir(path16.dirname(file));
7064
7367
  if (fileExists(file)) {
7065
7368
  try {
7066
- fs19.copyFileSync(file, `${file}.bunshin-backup`);
7369
+ fs21.copyFileSync(file, `${file}.bunshin-backup`);
7067
7370
  } catch {
7068
7371
  }
7069
7372
  }
@@ -7078,23 +7381,23 @@ function mcpLauncher(bin) {
7078
7381
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7079
7382
  let script = process.argv[1] ?? "";
7080
7383
  try {
7081
- script = fs19.realpathSync(script);
7384
+ script = fs21.realpathSync(script);
7082
7385
  } catch {
7083
7386
  }
7084
7387
  return { command: process.execPath, args: [script, "mcp"] };
7085
7388
  }
7086
7389
  function installLearnCommand({ file, remove = false }) {
7087
7390
  try {
7088
- const current = fileExists(file) ? fs19.readFileSync(file, "utf8") : null;
7391
+ const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7089
7392
  const ours = current === null || current === LEARN_COMMAND;
7090
7393
  if (remove) {
7091
- if (current !== null && ours) fs19.unlinkSync(file);
7394
+ if (current !== null && ours) fs21.unlinkSync(file);
7092
7395
  return { file, installed: 0, changed: current !== null && ours };
7093
7396
  }
7094
7397
  if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
7095
7398
  if (current === null) {
7096
- fs19.mkdirSync(path14.dirname(file), { recursive: true });
7097
- fs19.writeFileSync(file, LEARN_COMMAND);
7399
+ fs21.mkdirSync(path16.dirname(file), { recursive: true });
7400
+ fs21.writeFileSync(file, LEARN_COMMAND);
7098
7401
  }
7099
7402
  return { file, installed: 1, changed: current === null };
7100
7403
  } catch (err) {
@@ -7103,7 +7406,7 @@ function installLearnCommand({ file, remove = false }) {
7103
7406
  }
7104
7407
  function installMcpConfig({ file, launcher, remove = false }) {
7105
7408
  try {
7106
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7409
+ const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7107
7410
  const servers = { ...before.mcpServers ?? {} };
7108
7411
  if (remove) delete servers.bunshin;
7109
7412
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7111,7 +7414,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7111
7414
  if (!Object.keys(servers).length) delete after.mcpServers;
7112
7415
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7113
7416
  if (changed) {
7114
- if (!Object.keys(after).length && fileExists(file)) fs19.unlinkSync(file);
7417
+ if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7115
7418
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7116
7419
  }
7117
7420
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7119,25 +7422,49 @@ function installMcpConfig({ file, launcher, remove = false }) {
7119
7422
  return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
7120
7423
  }
7121
7424
  }
7425
+ function installUserMcp({ launcher, remove = false, claude = "claude" }) {
7426
+ const file = path16.join(homeDir(), ".claude.json");
7427
+ const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
7428
+ try {
7429
+ let had = false;
7430
+ try {
7431
+ run2(["mcp", "get", "bunshin"]);
7432
+ had = true;
7433
+ } catch {
7434
+ }
7435
+ if (had) {
7436
+ try {
7437
+ run2(["mcp", "remove", "bunshin", "-s", "user"]);
7438
+ } catch {
7439
+ }
7440
+ }
7441
+ if (remove) return { file, installed: 0, changed: had };
7442
+ run2(["mcp", "add-json", "bunshin", JSON.stringify({ type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } }), "-s", "user"]);
7443
+ return { file, installed: 1, changed: true };
7444
+ } catch (err) {
7445
+ return { file, installed: 0, changed: false, error: err instanceof Error ? err.message.split("\n")[0] : String(err) };
7446
+ }
7447
+ }
7122
7448
  function runInit(opts = {}) {
7123
7449
  const cwd = opts.cwd ?? process.cwd();
7124
7450
  const warnings = [];
7125
7451
  const scope = opts.scope ?? "repo";
7126
- const root = scope === "global" ? path14.join(homeDir(), ".bunshin") : path14.join(cwd, ".bunshin");
7127
- if (fs19.existsSync(root) && !new Store(root).trusted && fs19.readdirSync(root).length) {
7452
+ const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
7453
+ if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
7128
7454
  const aside = `${root}.untrusted-${Date.now()}`;
7129
- fs19.renameSync(root, aside);
7455
+ fs21.renameSync(root, aside);
7130
7456
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7131
7457
  }
7132
- const existed = fileExists(path14.join(root, "config.json"));
7458
+ const existed = fileExists(path16.join(root, "config.json"));
7133
7459
  const store = new Store(root);
7134
7460
  store.ensureLayout();
7135
- fs19.chmodSync(root, 448);
7136
- if (scope === "repo") {
7137
- const ignore = path14.join(cwd, ".gitignore");
7138
- const current = fileExists(ignore) ? fs19.readFileSync(ignore, "utf8") : "";
7461
+ fs21.chmodSync(root, 448);
7462
+ const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
7463
+ if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
7464
+ const ignore = path16.join(cwd, ".gitignore");
7465
+ const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
7139
7466
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7140
- fs19.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7467
+ fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7141
7468
  `);
7142
7469
  }
7143
7470
  }
@@ -7145,6 +7472,7 @@ function runInit(opts = {}) {
7145
7472
  const config = {
7146
7473
  ...previous,
7147
7474
  scope,
7475
+ hooks: hookScope,
7148
7476
  orgId: opts.orgId ?? previous.orgId,
7149
7477
  userId: opts.userId ?? previous.userId,
7150
7478
  role: opts.role ?? previous.role,
@@ -7161,18 +7489,22 @@ function runInit(opts = {}) {
7161
7489
  if (!fileExists(store.indexFile)) store.rebuildIndex();
7162
7490
  let hooks;
7163
7491
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "pi") {
7164
- hooks = installPiExtension(cwd, opts.removeHooks, scope);
7492
+ hooks = installPiExtension(cwd, opts.removeHooks, hookScope === "user" ? "global" : "repo");
7165
7493
  if (hooks.error) warnings.push(hooks.error);
7166
7494
  }
7167
7495
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7168
- const projectHooks = config.harness === "codex" && scope === "repo" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7169
- const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path14.join(process.env.CODEX_HOME ?? path14.join(homeDir(), ".codex"), "hooks.json") : scope === "repo" ? path14.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
7170
- hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness });
7496
+ const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7497
+ 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());
7498
+ hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
7499
+ if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
7500
+ const local = path16.join(cwd, ".claude", "settings.local.json");
7501
+ if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
7502
+ }
7171
7503
  if (projectHooks?.shared) {
7172
7504
  hooks.sharedWithWorktrees = true;
7173
- if (!hooks.error && !opts.removeHooks) ensureDir(path14.dirname(projectHooks.legacyFile));
7505
+ if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
7174
7506
  }
7175
- if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs19.realpathSync(projectHooks.file) !== fs19.realpathSync(projectHooks.legacyFile)) {
7507
+ if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
7176
7508
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7177
7509
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7178
7510
  }
@@ -7180,10 +7512,11 @@ function runInit(opts = {}) {
7180
7512
  }
7181
7513
  let mcp;
7182
7514
  let learnCommand;
7183
- if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code" && scope === "repo") {
7184
- mcp = installMcpConfig({ file: path14.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
7515
+ if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
7516
+ const user = hookScope === "user";
7517
+ 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 });
7185
7518
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
7186
- learnCommand = installLearnCommand({ file: path14.join(cwd, ".claude", "commands", "learn.md"), remove: opts.removeHooks });
7519
+ learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7187
7520
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7188
7521
  }
7189
7522
  if (!resolveBackend(config)) {
@@ -7318,6 +7651,7 @@ __export(commands_exports, {
7318
7651
  cmdReflectFork: () => cmdReflectFork,
7319
7652
  cmdReview: () => cmdReview,
7320
7653
  cmdReviewDecide: () => cmdReviewDecide,
7654
+ cmdSources: () => cmdSources,
7321
7655
  cmdSuggest: () => cmdSuggest,
7322
7656
  cmdSync: () => cmdSync,
7323
7657
  robotAttrsFromFlags: () => robotAttrsFromFlags,
@@ -7325,9 +7659,9 @@ __export(commands_exports, {
7325
7659
  setupRobot: () => setupRobot,
7326
7660
  v2Status: () => v2Status
7327
7661
  });
7328
- import fs20 from "node:fs";
7329
- import path15 from "node:path";
7330
- import { createHash as createHash4, randomBytes as randomBytes4 } from "node:crypto";
7662
+ import fs22 from "node:fs";
7663
+ import path17 from "node:path";
7664
+ import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
7331
7665
  function cmdReview(store, args, cwd, out, err) {
7332
7666
  const v2 = new V2Store(store);
7333
7667
  const sub = args._[1] ?? "list";
@@ -7496,11 +7830,11 @@ function cmdSuggest(store, args, cwd, out, err) {
7496
7830
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
7497
7831
  return 1;
7498
7832
  }
7499
- const harnessDir = path15.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
7500
- const file = str3(args.flags.file) ? path15.resolve(cwd, str3(args.flags.file)) : path15.join(harnessDir, rel);
7833
+ const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
7834
+ const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
7501
7835
  let content;
7502
7836
  try {
7503
- content = fs20.readFileSync(file, "utf8");
7837
+ content = fs22.readFileSync(file, "utf8");
7504
7838
  } catch {
7505
7839
  err(`bunshin suggest: cannot read ${file}
7506
7840
  `);
@@ -7621,15 +7955,15 @@ async function cmdQuestions(store, args, out, err) {
7621
7955
  return 0;
7622
7956
  }
7623
7957
  function saveCredential(storeRoot, apiUrl, apiKey, id) {
7624
- const dir = path15.join(homeDir(), ".config/bunshin/credentials");
7625
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
7626
- fs20.chmodSync(dir, 448);
7627
- const file = path15.join(dir, `${createHash4("sha256").update(`${storeRoot}
7958
+ const dir = path17.join(homeDir(), ".config/bunshin/credentials");
7959
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
7960
+ fs22.chmodSync(dir, 448);
7961
+ const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
7628
7962
  ${apiUrl}
7629
7963
  ${id}`).digest("hex")}.json`);
7630
7964
  const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
7631
- fs20.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7632
- fs20.renameSync(tmp, file);
7965
+ fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7966
+ fs22.renameSync(tmp, file);
7633
7967
  return file;
7634
7968
  }
7635
7969
  async function setupRobot(opts) {
@@ -7637,10 +7971,10 @@ async function setupRobot(opts) {
7637
7971
  const existing = Store.discover(opts.cwd);
7638
7972
  let store;
7639
7973
  const harness = opts.harness ?? "other";
7640
- if (existing && path15.resolve(path15.dirname(existing.root)) === path15.resolve(opts.cwd) && harness === "other") store = existing;
7974
+ if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
7641
7975
  else {
7642
7976
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
7643
- store = new Store(path15.join(opts.cwd, ".bunshin"));
7977
+ store = new Store(path17.join(opts.cwd, ".bunshin"));
7644
7978
  }
7645
7979
  let config = store.config();
7646
7980
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -7703,8 +8037,10 @@ function v2Status(store, cwd) {
7703
8037
  }
7704
8038
  const view = localView(v2, config, cwd, { index: false });
7705
8039
  const sync = v2.readSyncState();
8040
+ const pooled = cwdInPool(config, cwd, store.root);
7706
8041
  return {
7707
8042
  context: { id: view.ctx.id, kind: view.ctx.kind, layers: view.layers },
8043
+ source: { id: pooled ? sourceFor(cwd, store.root, view.ctx) ?? null : null, inPool: pooled, folder: topFolder(cwd, store.root) || null },
7708
8044
  bundle: bundle ? { sections: bundle.sections.length, generatedAt: bundle.generatedAt, release: bundle.release?.version ?? null } : null,
7709
8045
  repo: { name: repo, sections: repoSections },
7710
8046
  overlays: v2.readOverlays().length,
@@ -7715,6 +8051,46 @@ function v2Status(store, cwd) {
7715
8051
  session: v2.readCurrent()?.sessionId ?? null
7716
8052
  };
7717
8053
  }
8054
+ function cmdSources(store, args, cwd, out, err) {
8055
+ const config = store.config();
8056
+ const action = args._[1];
8057
+ const names = args._.slice(2).flatMap((x) => x.split(",")).map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
8058
+ if (action) {
8059
+ if (action !== "all" && action !== "only" && action !== "include" && action !== "exclude") {
8060
+ err("usage: bunshin sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>]\n");
8061
+ return 1;
8062
+ }
8063
+ if (action !== "all" && !names.length) {
8064
+ err(`bunshin sources ${action}: name at least one folder
8065
+ `);
8066
+ return 1;
8067
+ }
8068
+ const known = new Set(listFolders(store.root, void 0).map((f) => f.folder));
8069
+ const unknown = names.filter((n) => !known.has(n));
8070
+ if (unknown.length) {
8071
+ err(`bunshin sources: not a folder in ${storeHome(store.root)}: ${unknown.join(", ")}
8072
+ `);
8073
+ return 1;
8074
+ }
8075
+ const next = select(config.sources, action, names);
8076
+ const { sources: _drop, ...rest } = config;
8077
+ void _drop;
8078
+ store.saveConfig(next ? { ...rest, sources: next } : rest);
8079
+ }
8080
+ const selection = store.config().sources;
8081
+ const folders = listFolders(store.root, selection, { sources: true });
8082
+ if (bool2(args.flags.json)) {
8083
+ out(JSON.stringify({ home: storeHome(store.root), selection: selection ?? null, folders }, null, 2) + "\n");
8084
+ return 0;
8085
+ }
8086
+ const here = topFolder(cwd, store.root);
8087
+ out(`Learning pool of ${storeHome(store.root)}: ${!selection ? "every folder (new ones join)" : selection.only ? "only the folders marked in (new ones stay out)" : "every folder but those marked out (new ones join)"}
8088
+ `);
8089
+ if (!folders.length) out(" (no folders here yet)\n");
8090
+ for (const f of folders) out(` ${f.included ? "in " : "out"} ${f.folder}${f.source ? ` (${f.source})` : ""}${f.folder === here ? " \u2190 here" : ""}
8091
+ `);
8092
+ return 0;
8093
+ }
7718
8094
  var str3, bool2, list;
7719
8095
  var init_commands = __esm({
7720
8096
  "src/v2/commands.ts"() {
@@ -7731,6 +8107,7 @@ var init_commands = __esm({
7731
8107
  init_repo();
7732
8108
  init_store2();
7733
8109
  init_sync();
8110
+ init_source();
7734
8111
  init_view();
7735
8112
  str3 = (v) => typeof v === "string" ? v : void 0;
7736
8113
  bool2 = (v) => v === true || v === "true";
@@ -7743,6 +8120,7 @@ var setup_exports = {};
7743
8120
  __export(setup_exports, {
7744
8121
  HOSTED_API: () => HOSTED_API,
7745
8122
  apiAddress: () => apiAddress,
8123
+ askSources: () => askSources,
7746
8124
  connectionProfile: () => connectionProfile,
7747
8125
  executable: () => executable,
7748
8126
  hookBinary: () => hookBinary,
@@ -7750,16 +8128,16 @@ __export(setup_exports, {
7750
8128
  setupProject: () => setupProject,
7751
8129
  shellQuote: () => shellQuote
7752
8130
  });
7753
- import fs21 from "node:fs";
7754
- import path16 from "node:path";
7755
- import { createHash as createHash5, randomBytes as randomBytes5 } from "node:crypto";
8131
+ import fs23 from "node:fs";
8132
+ import path18 from "node:path";
8133
+ import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
7756
8134
  function executable(name) {
7757
- const directories = [...(process.env.PATH ?? "").split(path16.delimiter), path16.join(homeDir(), ".local/bin")];
8135
+ const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
7758
8136
  for (const directory of directories) {
7759
- const file = path16.resolve(directory, name);
8137
+ const file = path18.resolve(directory, name);
7760
8138
  try {
7761
- fs21.accessSync(file, fs21.constants.X_OK);
7762
- if (fs21.statSync(file).isFile()) return file;
8139
+ fs23.accessSync(file, fs23.constants.X_OK);
8140
+ if (fs23.statSync(file).isFile()) return file;
7763
8141
  } catch {
7764
8142
  }
7765
8143
  }
@@ -7767,8 +8145,8 @@ function executable(name) {
7767
8145
  }
7768
8146
  function hookBinary(harness) {
7769
8147
  const agent = executable(harness === "claude-code" ? "claude" : harness);
7770
- const directories = [.../* @__PURE__ */ new Set([path16.dirname(process.execPath), path16.join(homeDir(), ".local/bin"), ...agent ? [path16.dirname(agent)] : []])];
7771
- return `PATH=${shellQuote(directories.join(path16.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs21.realpathSync(process.argv[1]))}`;
8148
+ const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
8149
+ return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
7772
8150
  }
7773
8151
  function apiAddress(raw) {
7774
8152
  const url = new URL(raw);
@@ -7837,33 +8215,37 @@ async function setupProject(opts) {
7837
8215
  const apiKey = opts.apiKey.trim();
7838
8216
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
7839
8217
  const profile = await connectionProfile(apiUrl, apiKey);
7840
- const root = path16.join(opts.cwd, ".bunshin");
8218
+ const root = path18.join(opts.cwd, ".bunshin");
7841
8219
  const store = new Store(root);
7842
8220
  const previous = store.config();
7843
8221
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
7844
8222
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
7845
8223
  }
7846
- const credentialDirectory = path16.join(homeDir(), ".config/bunshin/credentials");
7847
- fs21.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
7848
- fs21.chmodSync(credentialDirectory, 448);
7849
- const name = createHash5("sha256").update(`${root}
8224
+ const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
8225
+ fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8226
+ fs23.chmodSync(credentialDirectory, 448);
8227
+ const name = createHash6("sha256").update(`${root}
7850
8228
  ${apiUrl}
7851
8229
  ${profile.orgId}
7852
8230
  ${profile.userId}`).digest("hex");
7853
- const credentialFile = path16.join(credentialDirectory, `${name}.json`);
8231
+ const credentialFile = path18.join(credentialDirectory, `${name}.json`);
7854
8232
  const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
7855
- fs21.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7856
- fs21.renameSync(temporary, credentialFile);
8233
+ fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8234
+ fs23.renameSync(temporary, credentialFile);
7857
8235
  const result = runInit({
7858
8236
  cwd: opts.cwd,
7859
8237
  harness: opts.harness,
7860
8238
  apiUrl,
7861
8239
  ...profile,
7862
8240
  noHooks: opts.noHooks,
7863
- bin: opts.bin ?? hookBinary(opts.harness)
8241
+ bin: opts.bin ?? hookBinary(opts.harness),
8242
+ ...opts.hooks ? { hooks: opts.hooks } : {}
7864
8243
  });
8244
+ const { sources: kept, ...base } = result.config;
8245
+ const sources = opts.sources === void 0 ? kept : opts.sources ?? void 0;
7865
8246
  store.saveConfig({
7866
- ...result.config,
8247
+ ...base,
8248
+ ...sources ? { sources } : {},
7867
8249
  ...profile,
7868
8250
  apiKey: void 0,
7869
8251
  credentialFile,
@@ -7874,6 +8256,39 @@ ${profile.userId}`).digest("hex");
7874
8256
  trustStore(root);
7875
8257
  return { ...result, profile, credentialFile };
7876
8258
  }
8259
+ async function askSources(store, out) {
8260
+ const { listFolders: listFolders2, select: select2 } = await Promise.resolve().then(() => (init_source(), source_exports));
8261
+ const config = store.config();
8262
+ const folders = listFolders2(store.root, void 0);
8263
+ if (folders.length < 2) return;
8264
+ const readline2 = await import("node:readline/promises");
8265
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
8266
+ try {
8267
+ out(`This folder holds ${folders.length} folders:
8268
+ `);
8269
+ folders.forEach((f, i) => out(` ${String(i + 1).padStart(2)}. ${f.folder}
8270
+ `));
8271
+ const answer = (await rl.question("Learn from [a]ll of them, all [e]xcept some, or [o]nly some? [a] ")).trim().toLowerCase();
8272
+ const mode = answer.startsWith("e") || answer.startsWith("x") ? "exclude" : answer.startsWith("o") || answer.startsWith("p") ? "only" : "all";
8273
+ const { sources: _drop, ...rest } = config;
8274
+ void _drop;
8275
+ if (mode === "all") {
8276
+ store.saveConfig(rest);
8277
+ out("Learning from every folder here; new folders join automatically.\n");
8278
+ return;
8279
+ }
8280
+ const raw = await rl.question(mode === "exclude" ? "Folders to leave out (numbers or names, comma-separated): " : "Folders to learn from (numbers or names, comma-separated): ");
8281
+ const picked = [...new Set(raw.split(/[,\s]+/).map((x) => x.trim()).filter(Boolean).map((x) => /^\d+$/.test(x) ? folders[Number(x) - 1]?.folder : folders.find((f) => f.folder === x)?.folder).filter((x) => !!x))];
8282
+ const next = select2(void 0, mode, picked);
8283
+ store.saveConfig(next ? { ...rest, sources: next } : rest);
8284
+ if (mode === "exclude") out(picked.length ? `Learning from every folder but ${picked.join(", ")}; new folders join automatically.
8285
+ ` : "Learning from every folder here; new folders join automatically.\n");
8286
+ else out(picked.length ? `Learning from ${picked.join(", ")} only; new folders stay out until you add them (bunshin sources include <folder>).
8287
+ ` : "Nothing here takes part until you add a folder (bunshin sources include <folder>).\n");
8288
+ } finally {
8289
+ rl.close();
8290
+ }
8291
+ }
7877
8292
  var shellQuote;
7878
8293
  var init_setup = __esm({
7879
8294
  "src/setup.ts"() {
@@ -7924,6 +8339,7 @@ async function handleMcp(msg, opts) {
7924
8339
  const cwd = opts.cwd ?? process.cwd();
7925
8340
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
7926
8341
  if (!store) return reply(text("Bunshin is not set up in this project (run `bunshin setup`). Proceed with general knowledge.", true));
8342
+ if (!cwdInPool(store.config(), cwd, store.root)) return reply(text("This folder is not part of Bunshin's learning pool (`bunshin sources`). Proceed with general knowledge.", true));
7927
8343
  try {
7928
8344
  if (name === "bunshin_recall") {
7929
8345
  const taskAbstract = typeof args.task_abstract === "string" ? args.task_abstract : "";
@@ -7987,6 +8403,7 @@ var init_mcp = __esm({
7987
8403
  "src/v2/mcp.ts"() {
7988
8404
  "use strict";
7989
8405
  init_v22();
8406
+ init_source();
7990
8407
  init_store();
7991
8408
  init_library();
7992
8409
  DEFAULT_PROTOCOL = "2025-06-18";
@@ -8164,9 +8581,9 @@ __export(sync_exports2, {
8164
8581
  resolveConflict: () => resolveConflict,
8165
8582
  runSync: () => runSync
8166
8583
  });
8167
- import { createHash as createHash6 } from "node:crypto";
8168
- import path17 from "node:path";
8169
- import fs22 from "node:fs";
8584
+ import { createHash as createHash7 } from "node:crypto";
8585
+ import path19 from "node:path";
8586
+ import fs24 from "node:fs";
8170
8587
  async function api(config, method, route, body, timeoutMs = 1e4) {
8171
8588
  const ctrl = new AbortController();
8172
8589
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8204,22 +8621,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8204
8621
  }
8205
8622
  }
8206
8623
  async function runSync(store) {
8207
- const lock = path17.join(store.root, "sync.lock");
8624
+ const lock = path19.join(store.root, "sync.lock");
8208
8625
  const deadline = Date.now() + 12e4;
8209
8626
  for (; ; ) {
8210
8627
  try {
8211
- fs22.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8628
+ fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8212
8629
  break;
8213
8630
  } catch (error) {
8214
8631
  if (error.code !== "EEXIST") throw error;
8215
8632
  try {
8216
- const owner = Number(fs22.readFileSync(lock, "utf8"));
8633
+ const owner = Number(fs24.readFileSync(lock, "utf8"));
8217
8634
  if (Number.isInteger(owner) && owner > 0) {
8218
8635
  try {
8219
8636
  process.kill(owner, 0);
8220
8637
  } catch (error2) {
8221
8638
  if (error2.code === "ESRCH") {
8222
- fs22.unlinkSync(lock);
8639
+ fs24.unlinkSync(lock);
8223
8640
  continue;
8224
8641
  }
8225
8642
  }
@@ -8238,7 +8655,7 @@ async function runSync(store) {
8238
8655
  return await syncStore(store);
8239
8656
  } finally {
8240
8657
  try {
8241
- fs22.unlinkSync(lock);
8658
+ fs24.unlinkSync(lock);
8242
8659
  } catch {
8243
8660
  }
8244
8661
  }
@@ -8266,9 +8683,9 @@ async function syncStore(store) {
8266
8683
  result.errors.push(String(e instanceof Error ? e.message : e));
8267
8684
  }
8268
8685
  }
8269
- const acknowledgementsFile = path17.join(store.root, "synced-traces.json");
8686
+ const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
8270
8687
  const acknowledgements = readJson(acknowledgementsFile, {});
8271
- const fingerprint2 = (trace) => createHash6("sha256").update(JSON.stringify(trace)).digest("hex");
8688
+ const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
8272
8689
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8273
8690
  for (const t of traces) {
8274
8691
  try {
@@ -8521,11 +8938,11 @@ __export(demo_exports, {
8521
8938
  runDemo: () => runDemo
8522
8939
  });
8523
8940
  import assert from "node:assert/strict";
8524
- import fs23 from "node:fs";
8941
+ import fs25 from "node:fs";
8525
8942
  import os4 from "node:os";
8526
- import path18 from "node:path";
8943
+ import path20 from "node:path";
8527
8944
  async function runDemo(write) {
8528
- const root = fs23.mkdtempSync(path18.join(os4.tmpdir(), "bunshin-demo-"));
8945
+ const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
8529
8946
  try {
8530
8947
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
8531
8948
  const { lessons, constraint, workflow } = aprScenario();
@@ -8542,7 +8959,7 @@ Task: ${APR_TASK}
8542
8959
  write(result.text);
8543
8960
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
8544
8961
  } finally {
8545
- fs23.rmSync(root, { recursive: true, force: true });
8962
+ fs25.rmSync(root, { recursive: true, force: true });
8546
8963
  }
8547
8964
  }
8548
8965
  var init_demo = __esm({
@@ -8555,21 +8972,21 @@ var init_demo = __esm({
8555
8972
  });
8556
8973
 
8557
8974
  // src/v2/gitremote.ts
8558
- import fs24 from "node:fs";
8559
- import path19 from "node:path";
8560
- import { createHash as createHash7 } from "node:crypto";
8975
+ import fs26 from "node:fs";
8976
+ import path21 from "node:path";
8977
+ import { createHash as createHash8 } from "node:crypto";
8561
8978
  function saveGitCredential(apiUrl, apiKey) {
8562
8979
  const file = gitCredentialFile();
8563
- fs24.mkdirSync(path19.dirname(file), { recursive: true, mode: 448 });
8980
+ fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
8564
8981
  let all = {};
8565
8982
  try {
8566
- all = JSON.parse(fs24.readFileSync(file, "utf8"));
8983
+ all = JSON.parse(fs26.readFileSync(file, "utf8"));
8567
8984
  } catch {
8568
8985
  }
8569
8986
  all[apiUrl.replace(/\/$/, "")] = apiKey;
8570
8987
  const tmp = `${file}.${process.pid}.tmp`;
8571
- fs24.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8572
- fs24.renameSync(tmp, file);
8988
+ fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8989
+ fs26.renameSync(tmp, file);
8573
8990
  }
8574
8991
  function resolveGitKey(apiUrl) {
8575
8992
  const base = apiUrl.replace(/\/$/, "");
@@ -8577,20 +8994,20 @@ function resolveGitKey(apiUrl) {
8577
8994
  const origin = urlOrigin(base);
8578
8995
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
8579
8996
  try {
8580
- const all = JSON.parse(fs24.readFileSync(gitCredentialFile(), "utf8"));
8997
+ const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
8581
8998
  if (all[base]) return all[base];
8582
8999
  } catch {
8583
9000
  }
8584
- const dir = path19.join(homeDir(), ".config/bunshin/credentials");
9001
+ const dir = path21.join(homeDir(), ".config/bunshin/credentials");
8585
9002
  let files = [];
8586
9003
  try {
8587
- files = fs24.readdirSync(dir).filter((f) => f.endsWith(".json"));
9004
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
8588
9005
  } catch {
8589
9006
  return void 0;
8590
9007
  }
8591
9008
  for (const f of files.sort()) {
8592
9009
  try {
8593
- const c = JSON.parse(fs24.readFileSync(path19.join(dir, f), "utf8"));
9010
+ const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
8594
9011
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
8595
9012
  } catch {
8596
9013
  }
@@ -8598,7 +9015,7 @@ function resolveGitKey(apiUrl) {
8598
9015
  return void 0;
8599
9016
  }
8600
9017
  function refsDigest(refs) {
8601
- return createHash7("sha256").update(
9018
+ return createHash8("sha256").update(
8602
9019
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
8603
9020
  ).digest("hex").slice(0, 12);
8604
9021
  }
@@ -8611,7 +9028,7 @@ var init_gitremote = __esm({
8611
9028
  ZERO_SHA = "0".repeat(40);
8612
9029
  REMOTE_PATH = "/v1/git/repos/";
8613
9030
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
8614
- gitCredentialFile = () => path19.join(homeDir(), ".config/bunshin/git-credentials.json");
9031
+ gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
8615
9032
  GitApiError = class extends Error {
8616
9033
  constructor(status, message, body = null) {
8617
9034
  super(message);
@@ -8703,19 +9120,19 @@ __export(gitrepo_exports, {
8703
9120
  githubReferences: () => githubReferences,
8704
9121
  repoFromUrl: () => repoFromUrl
8705
9122
  });
8706
- import fs25 from "node:fs";
8707
- import path20 from "node:path";
9123
+ import fs27 from "node:fs";
9124
+ import path22 from "node:path";
8708
9125
  import { spawnSync as spawnSync2 } from "node:child_process";
8709
- function git2(cwd, args, opts = {}) {
9126
+ function git3(cwd, args, opts = {}) {
8710
9127
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
8711
9128
  const res = { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
8712
9129
  if (!res.ok && !opts.allowFail) throw new Error(`git ${args.join(" ")} failed: ${(res.err || res.out).trim()}`);
8713
9130
  return res;
8714
9131
  }
8715
9132
  function onPath(name) {
8716
- for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
9133
+ for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
8717
9134
  try {
8718
- fs25.accessSync(path20.join(dir, name), fs25.constants.X_OK);
9135
+ fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
8719
9136
  return true;
8720
9137
  } catch {
8721
9138
  }
@@ -8767,7 +9184,7 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
8767
9184
  return client;
8768
9185
  }
8769
9186
  function toplevel(dir) {
8770
- const r = git2(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
9187
+ const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
8771
9188
  if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
8772
9189
  return r.out.trim();
8773
9190
  }
@@ -8775,7 +9192,7 @@ function githubReferences(root, repo) {
8775
9192
  const [owner, name] = repo.split("/");
8776
9193
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8777
9194
  const pattern = `github\\.com[:/]${esc(owner)}/${esc(name)}|--repo[ =]${esc(owner)}/${esc(name)}|gh repo clone ${esc(owner)}/${esc(name)}`;
8778
- const r = git2(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
9195
+ const r = git3(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
8779
9196
  return r.out.split("\n").filter(Boolean).map((l) => l.replace(/^HEAD:/, ""));
8780
9197
  }
8781
9198
  function switchOrigin(root, url, out) {
@@ -8784,41 +9201,41 @@ function switchOrigin(root, url, out) {
8784
9201
  let keptAs = null;
8785
9202
  if (old && old !== url) {
8786
9203
  keptAs = rs.has("github") ? `github-${Date.now()}` : "github";
8787
- git2(root, ["remote", "rename", "origin", keptAs]);
9204
+ git3(root, ["remote", "rename", "origin", keptAs]);
8788
9205
  }
8789
- if (rs.has("bunshin") && rs.get("bunshin") === url) git2(root, ["remote", "rename", "bunshin", "origin"]);
8790
- else if (old !== url) git2(root, ["remote", "add", "origin", url]);
8791
- git2(root, ["fetch", "--quiet", "--tags", "origin"]);
8792
- git2(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
9206
+ if (rs.has("bunshin") && rs.get("bunshin") === url) git3(root, ["remote", "rename", "bunshin", "origin"]);
9207
+ else if (old !== url) git3(root, ["remote", "add", "origin", url]);
9208
+ git3(root, ["fetch", "--quiet", "--tags", "origin"]);
9209
+ git3(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
8793
9210
  const originRefs = refMap(root, "refs/remotes/origin");
8794
- for (const b of git2(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
8795
- if (originRefs.has(`refs/remotes/origin/${b}`)) git2(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
9211
+ for (const b of git3(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
9212
+ if (originRefs.has(`refs/remotes/origin/${b}`)) git3(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
8796
9213
  }
8797
9214
  out(`origin \u2192 ${url}${keptAs ? ` (the old origin is now the remote "${keptAs}")` : ""}
8798
9215
  `);
8799
9216
  }
8800
9217
  async function cmdImport(args, cwd, out, err) {
8801
9218
  requireHelper();
8802
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
8803
- if (git2(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
9219
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9220
+ 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");
8804
9221
  const rs = remotes(root);
8805
9222
  const originUrl = rs.get("origin");
8806
- const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path20.basename(path20.dirname(root))}/${path20.basename(root)}`;
9223
+ const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
8807
9224
  const client = await connect(args, cwd);
8808
9225
  const url = remoteUrl(client.apiUrl, name);
8809
9226
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
8810
9227
  if (fromOrigin) {
8811
9228
  out(`Fetching ${originUrl} \u2026
8812
9229
  `);
8813
- const f = git2(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
9230
+ const f = git3(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
8814
9231
  if (!f.ok) err(`! could not fetch origin (${f.err.trim().split("\n")[0]}); importing what this clone already has from it
8815
9232
  `);
8816
9233
  }
8817
9234
  const branches = fromOrigin ? [...refMap(root, "refs/remotes/origin").entries()].filter(([ref]) => ref !== "refs/remotes/origin/HEAD").map(([ref, sha2]) => ({ src: ref, dst: ref.replace("refs/remotes/origin/", "refs/heads/"), sha: sha2 })) : [...refMap(root, "refs/heads").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
8818
9235
  const tags = [...refMap(root, "refs/tags").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
8819
9236
  if (!branches.length) throw new Error("nothing to import: no branches found");
8820
- const originHead = git2(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
8821
- const current = git2(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
9237
+ const originHead = git3(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
9238
+ const current = git3(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
8822
9239
  const defaultBranch = str4(args.flags["default-branch"]) ?? (fromOrigin && originHead ? originHead : current || "main");
8823
9240
  if (!branches.some((b) => b.dst === `refs/heads/${defaultBranch}`)) throw new Error(`default branch ${defaultBranch} is not among the imported branches`);
8824
9241
  if (fromOrigin) {
@@ -8828,7 +9245,7 @@ async function cmdImport(args, cwd, out, err) {
8828
9245
  if (!remote) err(`! local branch ${b} is not on the origin and was not imported (push it to Bunshin afterwards if you need it)
8829
9246
  `);
8830
9247
  else if (remote !== sha2) {
8831
- const ahead = git2(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
9248
+ const ahead = git3(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
8832
9249
  if (ahead && ahead !== "0") err(`! ${b} has ${ahead} local commit${ahead === "1" ? "" : "s"} not on the origin; after the switch, \`git push\` sends them to Bunshin
8833
9250
  `);
8834
9251
  }
@@ -8851,13 +9268,13 @@ async function cmdImport(args, cwd, out, err) {
8851
9268
  out(`${name} exists and is empty; importing into it
8852
9269
  `);
8853
9270
  }
8854
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8855
- else git2(root, ["remote", "add", "bunshin", url]);
9271
+ if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
9272
+ else git3(root, ["remote", "add", "bunshin", url]);
8856
9273
  const specs = [...branches, ...tags].map((r) => `${r.src}:${r.dst}`);
8857
- const commits = git2(root, ["rev-list", "--count", "--all"]).out.trim();
9274
+ const commits = git3(root, ["rev-list", "--count", "--all"]).out.trim();
8858
9275
  out(`Pushing ${branches.length} branch${branches.length === 1 ? "" : "es"}, ${tags.length} tag${tags.length === 1 ? "" : "s"} (${commits} commits) \u2026
8859
9276
  `);
8860
- const push2 = git2(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
9277
+ const push2 = git3(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
8861
9278
  if (!push2.ok) throw new Error(`push to Bunshin failed: ${push2.err.trim()}`);
8862
9279
  const info = await client.info(name);
8863
9280
  const want = [...branches, ...tags].map((r) => ({ ref: r.dst, sha: r.sha }));
@@ -8879,7 +9296,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
8879
9296
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
8880
9297
  `);
8881
9298
  }
8882
- if (fs25.existsSync(path20.join(root, ".github"))) out(`
9299
+ if (fs27.existsSync(path22.join(root, ".github"))) out(`
8883
9300
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
8884
9301
  `);
8885
9302
  out(`
@@ -8890,7 +9307,7 @@ New machines clone with: bunshin repo clone ${name}
8890
9307
  }
8891
9308
  async function cmdAttach(args, cwd, out, err) {
8892
9309
  requireHelper();
8893
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9310
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8894
9311
  const rs = remotes(root);
8895
9312
  const originUrl = rs.get("origin");
8896
9313
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -8904,9 +9321,9 @@ async function cmdAttach(args, cwd, out, err) {
8904
9321
  }
8905
9322
  const info = await client.info(name);
8906
9323
  if (!info.refs.length) throw new Error(`${name} in Bunshin is empty; import it first (bunshin repo import)`);
8907
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8908
- else git2(root, ["remote", "add", "bunshin", url]);
8909
- git2(root, ["fetch", "--quiet", "--tags", "bunshin"]);
9324
+ if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
9325
+ else git3(root, ["remote", "add", "bunshin", url]);
9326
+ git3(root, ["fetch", "--quiet", "--tags", "bunshin"]);
8910
9327
  if (originUrl) {
8911
9328
  const behind = [];
8912
9329
  for (const [ref, sha2] of refMap(root, "refs/remotes/origin")) {
@@ -8914,7 +9331,7 @@ async function cmdAttach(args, cwd, out, err) {
8914
9331
  const b = ref.replace("refs/remotes/origin/", "");
8915
9332
  const theirs = info.refs.find((r) => r.ref === `refs/heads/${b}`)?.sha;
8916
9333
  if (!theirs) behind.push(`${b} (missing in Bunshin)`);
8917
- else if (theirs !== sha2 && !git2(root, ["merge-base", "--is-ancestor", sha2, theirs], { allowFail: true }).ok) behind.push(`${b} (the old origin has commits Bunshin lacks)`);
9334
+ else if (theirs !== sha2 && !git3(root, ["merge-base", "--is-ancestor", sha2, theirs], { allowFail: true }).ok) behind.push(`${b} (the old origin has commits Bunshin lacks)`);
8918
9335
  }
8919
9336
  if (behind.length && !bool3(args.flags.force)) {
8920
9337
  err(`The old origin has work that is not in Bunshin:
@@ -8925,7 +9342,7 @@ Push it to Bunshin first (git push bunshin <branch>), or re-run with --force.
8925
9342
  }
8926
9343
  }
8927
9344
  switchOrigin(root, url, out);
8928
- const status = git2(root, ["status", "-sb"]).out.split("\n")[0];
9345
+ const status = git3(root, ["status", "-sb"]).out.split("\n")[0];
8929
9346
  out(`${status}
8930
9347
  `);
8931
9348
  return 0;
@@ -8938,12 +9355,12 @@ async function cmdClone(args, cwd, out) {
8938
9355
  const dir = args._[2] ?? name.split("/")[1];
8939
9356
  const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
8940
9357
  if (r.status !== 0) return r.status ?? 1;
8941
- out(`Cloned ${name} into ${path20.resolve(cwd, dir)}
9358
+ out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
8942
9359
  `);
8943
9360
  return 0;
8944
9361
  }
8945
9362
  async function cmdVerify(args, cwd, out, err) {
8946
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9363
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8947
9364
  const rs = remotes(root);
8948
9365
  const originUrl = rs.get("origin") ?? "";
8949
9366
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -8964,7 +9381,7 @@ async function cmdVerify(args, cwd, out, err) {
8964
9381
  `);
8965
9382
  }
8966
9383
  }
8967
- const fsck = git2(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
9384
+ const fsck = git3(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
8968
9385
  if (!fsck.ok) {
8969
9386
  problems++;
8970
9387
  err(`! git fsck: ${fsck.err.trim().split("\n")[0]}
@@ -9054,12 +9471,12 @@ var init_gitrepo = __esm({
9054
9471
  bool3 = (v) => v === true || v === "true";
9055
9472
  remotes = (root) => {
9056
9473
  const m = /* @__PURE__ */ new Map();
9057
- for (const name of git2(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git2(root, ["remote", "get-url", name]).out.trim());
9474
+ for (const name of git3(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git3(root, ["remote", "get-url", name]).out.trim());
9058
9475
  return m;
9059
9476
  };
9060
9477
  refMap = (root, prefix) => {
9061
9478
  const m = /* @__PURE__ */ new Map();
9062
- for (const line of git2(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
9479
+ for (const line of git3(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
9063
9480
  const [sha2, ref] = line.split(" ");
9064
9481
  m.set(ref, sha2);
9065
9482
  }
@@ -9106,22 +9523,22 @@ __export(runner_exports, {
9106
9523
  setupText: () => setupText
9107
9524
  });
9108
9525
  import { spawnSync as spawnSync3 } from "node:child_process";
9109
- import fs26 from "node:fs";
9526
+ import fs28 from "node:fs";
9110
9527
  import os5 from "node:os";
9111
- import path21 from "node:path";
9528
+ import path23 from "node:path";
9112
9529
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9113
- return path21.join(home, ".bunshin", "runner");
9530
+ return path23.join(home, ".bunshin", "runner");
9114
9531
  }
9115
9532
  function readInstalled(home) {
9116
9533
  try {
9117
- return JSON.parse(fs26.readFileSync(path21.join(runnerHome(home), "config.json"), "utf8"));
9534
+ return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
9118
9535
  } catch {
9119
9536
  return null;
9120
9537
  }
9121
9538
  }
9122
9539
  function connection(args, cwd) {
9123
9540
  const saved = readInstalled();
9124
- const store = saved && path21.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9541
+ const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9125
9542
  const config = store?.config();
9126
9543
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9127
9544
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9181,9 +9598,9 @@ async function runOnce(c, args, out, err) {
9181
9598
  const [job] = await lease(c, args);
9182
9599
  if (!job) break;
9183
9600
  const started = Date.now();
9184
- const scratch = fs26.mkdtempSync(path21.join(os5.tmpdir(), "bunshin-job-"));
9601
+ const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
9185
9602
  const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
9186
- () => fs26.rmSync(scratch, { recursive: true, force: true })
9603
+ () => fs28.rmSync(scratch, { recursive: true, force: true })
9187
9604
  );
9188
9605
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9189
9606
  if (!res.ok) {
@@ -9273,24 +9690,24 @@ function renderCheck(id, c) {
9273
9690
  }
9274
9691
  function logAnswer(entry) {
9275
9692
  try {
9276
- fs26.mkdirSync(runnerHome(), { recursive: true });
9277
- fs26.appendFileSync(path21.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9693
+ fs28.mkdirSync(runnerHome(), { recursive: true });
9694
+ fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9278
9695
  `);
9279
9696
  } catch {
9280
9697
  }
9281
9698
  }
9282
9699
  function which(name) {
9283
- for (const d of [...(process.env.PATH || "").split(path21.delimiter), path21.join(os5.homedir(), ".local", "bin")]) {
9284
- if (d && fs26.existsSync(path21.join(d, name))) return path21.join(d, name);
9700
+ for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
9701
+ if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
9285
9702
  }
9286
9703
  return null;
9287
9704
  }
9288
9705
  function scheduledCommand(backend) {
9289
- const node = path21.dirname(process.execPath);
9290
- const npx = fs26.existsSync(path21.join(node, "npx")) ? path21.join(node, "npx") : which("npx") ?? "npx";
9706
+ const node = path23.dirname(process.execPath);
9707
+ const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
9291
9708
  const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
9292
- const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path21.dirname(bin)] : [], path21.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
9293
- return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path21.delimiter) };
9709
+ 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"])];
9710
+ return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
9294
9711
  }
9295
9712
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9296
9713
  const cmd = scheduledCommand(backend);
@@ -9305,8 +9722,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9305
9722
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9306
9723
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9307
9724
  <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
9308
- <key>StandardOutPath</key><string>${xml(path21.join(home, "runner.log"))}</string>
9309
- <key>StandardErrorPath</key><string>${xml(path21.join(home, "runner.log"))}</string>
9725
+ <key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9726
+ <key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9310
9727
  <key>ProcessType</key><string>Background</string>
9311
9728
  <key>LowPriorityIO</key><true/>
9312
9729
  </dict>
@@ -9317,19 +9734,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9317
9734
  const cmd = scheduledCommand(backend);
9318
9735
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
9319
9736
  const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
9320
- return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path21.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
9737
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
9321
9738
  }
9322
9739
  function launchAgentsDir(opts) {
9323
- return opts.launchAgents ?? path21.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
9740
+ return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
9324
9741
  }
9325
9742
  function installSchedule(backend, everyMinutes, opts = {}) {
9326
9743
  const platform = opts.platform ?? process.platform;
9327
9744
  const run2 = opts.run ?? realRun;
9328
9745
  if (platform === "darwin") {
9329
9746
  const dir = launchAgentsDir(opts);
9330
- const plist = path21.join(dir, `${LAUNCHD_LABEL}.plist`);
9331
- fs26.mkdirSync(dir, { recursive: true });
9332
- fs26.writeFileSync(plist, launchdPlist(backend, everyMinutes));
9747
+ const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
9748
+ fs28.mkdirSync(dir, { recursive: true });
9749
+ fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
9333
9750
  const domain = `gui/${process.getuid?.() ?? 501}`;
9334
9751
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
9335
9752
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -9349,10 +9766,10 @@ function removeSchedule(opts = {}) {
9349
9766
  const platform = opts.platform ?? process.platform;
9350
9767
  const run2 = opts.run ?? realRun;
9351
9768
  if (platform === "darwin") {
9352
- const plist = path21.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9769
+ const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9353
9770
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
9354
- if (!fs26.existsSync(plist)) return "no schedule";
9355
- fs26.rmSync(plist, { force: true });
9771
+ if (!fs28.existsSync(plist)) return "no schedule";
9772
+ fs28.rmSync(plist, { force: true });
9356
9773
  return `removed ${plist}`;
9357
9774
  }
9358
9775
  const current = run2("crontab", ["-l"]);
@@ -9379,10 +9796,10 @@ async function install(args, cwd, out, err, opts = {}) {
9379
9796
  }
9380
9797
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
9381
9798
  const home = runnerHome();
9382
- fs26.mkdirSync(home, { recursive: true, mode: 448 });
9799
+ fs28.mkdirSync(home, { recursive: true, mode: 448 });
9383
9800
  const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
9384
- fs26.writeFileSync(path21.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
9385
- fs26.chmodSync(path21.join(home, "config.json"), 384);
9801
+ fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
9802
+ fs28.chmodSync(path23.join(home, "config.json"), 384);
9386
9803
  let schedule;
9387
9804
  try {
9388
9805
  schedule = installSchedule(backend, every, opts);
@@ -9395,7 +9812,7 @@ Run it yourself: npx -y ${PKG2} runner --once
9395
9812
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
9396
9813
  `);
9397
9814
  out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
9398
- ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path21.join(home, "runner.log")}
9815
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
9399
9816
  `);
9400
9817
  out(` Stop: npx -y ${PKG2} runner uninstall
9401
9818
  `);
@@ -9412,9 +9829,9 @@ Answering what waits now\u2026
9412
9829
  function uninstall(out, opts = {}) {
9413
9830
  out(`${removeSchedule(opts)}
9414
9831
  `);
9415
- const file = path21.join(runnerHome(), "config.json");
9416
- if (fs26.existsSync(file)) {
9417
- fs26.rmSync(file, { force: true });
9832
+ const file = path23.join(runnerHome(), "config.json");
9833
+ if (fs28.existsSync(file)) {
9834
+ fs28.rmSync(file, { force: true });
9418
9835
  out(`removed ${file} (revoke the key in the console: Agent keys)
9419
9836
  `);
9420
9837
  }
@@ -9594,7 +10011,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9594
10011
  out(text2);
9595
10012
  return 0;
9596
10013
  }
9597
- fs26.writeFileSync(file, text2, { mode: 384 });
10014
+ fs28.writeFileSync(file, text2, { mode: 384 });
9598
10015
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
9599
10016
  out(
9600
10017
  [
@@ -9612,7 +10029,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9612
10029
  const id = args._[2];
9613
10030
  const file = args._[3] ?? "-";
9614
10031
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
9615
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10032
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9616
10033
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
9617
10034
  out(renderCheck(id, r.body));
9618
10035
  return r.body.ok ? 0 : 1;
@@ -9621,7 +10038,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9621
10038
  const id = args._[2];
9622
10039
  const file = args._[3] ?? "-";
9623
10040
  if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
9624
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10041
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9625
10042
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
9626
10043
  if (check?.body && (check.status < 400 || check.body.problems)) {
9627
10044
  out(renderCheck(id, check.body));
@@ -9645,7 +10062,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9645
10062
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
9646
10063
  const entries = (() => {
9647
10064
  try {
9648
- return fs26.readFileSync(path21.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
10065
+ 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);
9649
10066
  } catch {
9650
10067
  return [];
9651
10068
  }
@@ -9714,9 +10131,9 @@ var init_runner = __esm({
9714
10131
  });
9715
10132
 
9716
10133
  // src/v2/importer.ts
9717
- import { execFileSync as execFileSync3 } from "node:child_process";
9718
- import fs27 from "node:fs";
9719
- import path22 from "node:path";
10134
+ import { execFileSync as execFileSync6 } from "node:child_process";
10135
+ import fs29 from "node:fs";
10136
+ import path24 from "node:path";
9720
10137
  function rewriteRepoPaths(text2, repoName2, pack) {
9721
10138
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
9722
10139
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -9755,7 +10172,7 @@ function buildDocs(files, opts) {
9755
10172
  const main2 = `${dir}/SKILL.md`;
9756
10173
  const text2 = rw(files.get(main2).content);
9757
10174
  const fm = frontmatterDescription(text2);
9758
- const name = path22.posix.basename(dir);
10175
+ const name = path24.posix.basename(dir);
9759
10176
  const bundle = {};
9760
10177
  for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
9761
10178
  claimed.add(p);
@@ -9837,45 +10254,45 @@ ${Object.keys(loose).map((p) => `- \`${p}\``).join("\n")}
9837
10254
  }
9838
10255
  return docs.sort((a, b) => a.path.localeCompare(b.path));
9839
10256
  }
9840
- function git3(root, args) {
9841
- return execFileSync3("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
10257
+ function git4(root, args) {
10258
+ return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
9842
10259
  }
9843
10260
  function gitBuf(root, args) {
9844
- return execFileSync3("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
10261
+ return execFileSync6("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
9845
10262
  }
9846
10263
  function isGitRepo(root) {
9847
10264
  try {
9848
- return git3(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
10265
+ return git4(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
9849
10266
  } catch {
9850
10267
  return false;
9851
10268
  }
9852
10269
  }
9853
10270
  function workingTreeFiles(root) {
9854
10271
  const out = /* @__PURE__ */ new Map();
9855
- const list2 = isGitRepo(root) ? git3(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
9856
- return fs27.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
10272
+ const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
10273
+ return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
9857
10274
  if (e.name === ".git" || e.name === "node_modules") return [];
9858
10275
  const rel = base ? `${base}/${e.name}` : e.name;
9859
- return e.isDirectory() ? walk(path22.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
10276
+ return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
9860
10277
  });
9861
10278
  })(root);
9862
10279
  for (const rel of list2) {
9863
- const abs = path22.join(root, rel);
10280
+ const abs = path24.join(root, rel);
9864
10281
  let buf;
9865
10282
  try {
9866
- buf = fs27.readFileSync(abs);
10283
+ buf = fs29.readFileSync(abs);
9867
10284
  } catch {
9868
10285
  continue;
9869
10286
  }
9870
10287
  if (!isText(buf)) continue;
9871
- const exec = (fs27.statSync(abs).mode & 73) !== 0;
10288
+ const exec = (fs29.statSync(abs).mode & 73) !== 0;
9872
10289
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
9873
10290
  }
9874
10291
  return out;
9875
10292
  }
9876
10293
  function commitsOf(root, since) {
9877
10294
  const range = since ? [`${since}..HEAD`] : ["HEAD"];
9878
- return git3(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
10295
+ return git4(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
9879
10296
  const [sha2, author, at, subject] = l.split("");
9880
10297
  return { sha: sha2, author, at, subject: subject ?? "" };
9881
10298
  });
@@ -9888,7 +10305,7 @@ function readAt(root, sha2, rel, mode) {
9888
10305
  }
9889
10306
  function treeAt(root, sha2) {
9890
10307
  const out = /* @__PURE__ */ new Map();
9891
- for (const line of git3(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
10308
+ for (const line of git4(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
9892
10309
  const m = /^(\d+) blob [0-9a-f]+\t(.+)$/.exec(line);
9893
10310
  if (!m) continue;
9894
10311
  const f = readAt(root, sha2, m[2], m[1]);
@@ -9902,7 +10319,7 @@ function diffDocs(prev, next) {
9902
10319
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
9903
10320
  }
9904
10321
  function readSkillSystem(root, opts) {
9905
- const repoName2 = path22.basename(path22.resolve(root));
10322
+ const repoName2 = path24.basename(path24.resolve(root));
9906
10323
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
9907
10324
  const tree = build(workingTreeFiles(root));
9908
10325
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -9914,7 +10331,7 @@ function readSkillSystem(root, opts) {
9914
10331
  commits.forEach((c, i) => {
9915
10332
  if (!prevSha) files = treeAt(root, c.sha);
9916
10333
  else {
9917
- const changes = git3(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
10334
+ const changes = git4(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
9918
10335
  for (let k = 0; k + 1 < changes.length; k += 2) {
9919
10336
  const m = /^:\d+ (\d+) [0-9a-f]+ [0-9a-f]+ ([A-Z])/.exec(changes[k]);
9920
10337
  const rel = changes[k + 1];
@@ -9979,7 +10396,7 @@ var init_importer = __esm({
9979
10396
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9980
10397
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
9981
10398
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
9982
- isUpdateHook = (p, text2) => /auto-?update/i.test(path22.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10399
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
9983
10400
  fingerprint = (d) => JSON.stringify(d);
9984
10401
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
9985
10402
 
@@ -10015,8 +10432,8 @@ __export(skills_exports, {
10015
10432
  cmdSkill: () => cmdSkill,
10016
10433
  parsePin: () => parsePin
10017
10434
  });
10018
- import fs28 from "node:fs";
10019
- import path23 from "node:path";
10435
+ import fs30 from "node:fs";
10436
+ import path25 from "node:path";
10020
10437
  function requireOnline(store, what) {
10021
10438
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
10022
10439
  }
@@ -10025,9 +10442,9 @@ async function cmdImport2(store, args, cwd, out, err) {
10025
10442
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
10026
10443
  return 1;
10027
10444
  }
10028
- const root = path23.resolve(cwd, args._[2]);
10029
- if (!fs28.existsSync(root)) throw new Error(`${root} does not exist`);
10030
- const pack = str6(args.flags.pack) ?? path23.basename(root);
10445
+ const root = path25.resolve(cwd, args._[2]);
10446
+ if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
10447
+ const pack = str6(args.flags.pack) ?? path25.basename(root);
10031
10448
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
10032
10449
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
10033
10450
  const kinds = /* @__PURE__ */ new Map();
@@ -10062,7 +10479,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10062
10479
  const propose = args.flags["propose-rewrites"];
10063
10480
  if (propose) {
10064
10481
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
10065
- await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path23.basename(root), out, err);
10482
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
10066
10483
  }
10067
10484
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10068
10485
  return 0;
@@ -10202,7 +10619,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10202
10619
  }
10203
10620
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10204
10621
  if (graph) {
10205
- fs28.writeFileSync(path23.resolve(cwd, graph), renderSkillGraph(report));
10622
+ fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
10206
10623
  out(`graph written to ${graph}
10207
10624
  `);
10208
10625
  }
@@ -10269,12 +10686,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10269
10686
  }
10270
10687
  case "status": {
10271
10688
  const d = deployedState(config);
10272
- const p = deployPaths();
10689
+ const p = deployPaths(process.env, config.orgId);
10273
10690
  const pending = d.docs.filter((x) => x.pending);
10274
10691
  out(`deployed: ${d.libraryVersion ? `lib@${d.libraryVersion}` : "nothing yet"}; ${d.docs.filter((x) => x.kind === "skill").length} skills, ${d.docs.filter((x) => x.kind === "instructions").length} instruction documents, ${d.docs.filter((x) => x.kind === "pack").length} pack documents
10275
10692
  `);
10276
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10277
- instructions: ${p.claudeMd}, ${p.agentsMd}
10693
+ out(` skills: ${p.claudeSkills}, ${p.codexSkills} (this workspace's own copy)
10694
+ linked into: ${d.linked.length ? d.linked.join(", ") : "no project yet (each project gets links when a session runs there)"}
10278
10695
  files: ${p.library}
10279
10696
  `);
10280
10697
  for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
@@ -10282,16 +10699,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
10282
10699
  return 0;
10283
10700
  }
10284
10701
  case "workspace": {
10285
- const target = args._[2];
10286
- if (bool4(args.flags.clear)) setDeployWorkspace(null);
10287
- else if (target) setDeployWorkspace(target);
10288
- const ws = deployWorkspace();
10289
- const p = deployPaths();
10290
- out(ws ? `workspace: ${ws}
10291
- ` : "workspace: none (skills and instructions are user-wide)\n");
10292
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10293
- instructions: ${p.claudeMd}, ${p.agentsMd}
10294
- `);
10702
+ out("Skills no longer go to one shared folder: each workspace keeps its own copy and links it into the projects its sessions run in, so they never reach another workspace's projects. See `bunshin library status`.\n");
10295
10703
  return 0;
10296
10704
  }
10297
10705
  case "pin": {
@@ -10301,9 +10709,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10301
10709
  `;
10302
10710
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10303
10711
  if (file) {
10304
- const abs = path23.resolve(cwd, file);
10305
- fs28.mkdirSync(path23.dirname(abs), { recursive: true });
10306
- fs28.writeFileSync(abs, line);
10712
+ const abs = path25.resolve(cwd, file);
10713
+ fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10714
+ fs30.writeFileSync(abs, line);
10307
10715
  out(`${file}: ${line}`);
10308
10716
  } else out(line);
10309
10717
  return 0;
@@ -10313,12 +10721,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10313
10721
  const to = str6(args.flags.to);
10314
10722
  if (!to) throw new Error("--to <dir> is required");
10315
10723
  const pinFile = str6(args.flags.pin);
10316
- const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs28.readFileSync(path23.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10724
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10317
10725
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10318
10726
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10319
10727
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
10320
- const dir = path23.resolve(cwd, to);
10321
- if (fs28.existsSync(dir) && fs28.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
10728
+ const dir = path25.resolve(cwd, to);
10729
+ if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
10322
10730
  const pack = str6(args.flags.pack);
10323
10731
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
10324
10732
  const res = materialize(docs, dir);
@@ -10357,27 +10765,27 @@ __export(reflect_exports, {
10357
10765
  buildLesson: () => buildLesson,
10358
10766
  runReflection: () => runReflection
10359
10767
  });
10360
- import fs29 from "node:fs";
10361
- import path24 from "node:path";
10362
- import { createHash as createHash8 } from "node:crypto";
10768
+ import fs31 from "node:fs";
10769
+ import path26 from "node:path";
10770
+ import { createHash as createHash9 } from "node:crypto";
10363
10771
  async function runReflection(store, opts) {
10364
10772
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
10365
- const lock = path24.join(store.root, `reflect-${createHash8("sha256").update(opts.sessionId).digest("hex")}.lock`);
10773
+ const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
10366
10774
  const deadline = Date.now() + 66e4;
10367
10775
  for (; ; ) {
10368
10776
  try {
10369
- fs29.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10777
+ fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10370
10778
  break;
10371
10779
  } catch (error) {
10372
10780
  if (error.code !== "EEXIST") throw error;
10373
10781
  try {
10374
- const owner = Number(fs29.readFileSync(lock, "utf8"));
10782
+ const owner = Number(fs31.readFileSync(lock, "utf8"));
10375
10783
  if (Number.isInteger(owner) && owner > 0) {
10376
10784
  try {
10377
10785
  process.kill(owner, 0);
10378
10786
  } catch (error2) {
10379
10787
  if (error2.code === "ESRCH") {
10380
- fs29.unlinkSync(lock);
10788
+ fs31.unlinkSync(lock);
10381
10789
  continue;
10382
10790
  }
10383
10791
  }
@@ -10392,7 +10800,7 @@ async function runReflection(store, opts) {
10392
10800
  return await reflectSession(store, opts);
10393
10801
  } finally {
10394
10802
  try {
10395
- fs29.unlinkSync(lock);
10803
+ fs31.unlinkSync(lock);
10396
10804
  } catch {
10397
10805
  }
10398
10806
  }
@@ -10734,8 +11142,8 @@ __export(cli_exports, {
10734
11142
  parseArgs: () => parseArgs,
10735
11143
  version: () => version
10736
11144
  });
10737
- import fs30 from "node:fs";
10738
- import path25 from "node:path";
11145
+ import fs32 from "node:fs";
11146
+ import path27 from "node:path";
10739
11147
  function parseArgs(argv) {
10740
11148
  const out = { _: [], flags: {} };
10741
11149
  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"]);
@@ -10835,7 +11243,7 @@ async function main(argv, opts = {}) {
10835
11243
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
10836
11244
  const agent = harness === "claude-code" ? "claude" : harness;
10837
11245
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
10838
- const prior = new Store(path25.join(cwd, ".bunshin"));
11246
+ const prior = new Store(path27.join(cwd, ".bunshin"));
10839
11247
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
10840
11248
  const apiUrl = str7(args.flags["api-url"]);
10841
11249
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -10847,11 +11255,30 @@ async function main(argv, opts = {}) {
10847
11255
  process.stdin.on("end", () => resolve(value.trim()));
10848
11256
  process.stdin.on("error", reject);
10849
11257
  }) : process.env.BUNSHIN_API_KEY || (!apiUrl || apiUrl === existing.apiUrl) && existing.apiKey || await readKey2();
11258
+ const hooksFlag = str7(args.flags.hooks);
11259
+ if (hooksFlag && hooksFlag !== "user" && hooksFlag !== "project") throw new Error("--hooks must be user (every session under this folder) or project (this folder only)");
11260
+ const only = str7(args.flags.only);
11261
+ const exclude = str7(args.flags.exclude);
11262
+ const listed = (v) => v.split(",").map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
10850
11263
  out("Connecting to Bunshin\u2026\n");
10851
- const result = await setupProject2({ cwd, harness, apiKey: key, apiUrl, noHooks: bool5(args.flags["no-hooks"]) });
11264
+ const result = await setupProject2({
11265
+ cwd,
11266
+ harness,
11267
+ apiKey: key,
11268
+ apiUrl,
11269
+ noHooks: bool5(args.flags["no-hooks"]),
11270
+ ...hooksFlag ? { hooks: hooksFlag } : {},
11271
+ ...bool5(args.flags.all) ? { sources: null } : only || exclude ? { sources: { ...only ? { only: listed(only) } : {}, ...exclude ? { exclude: listed(exclude) } : {} } } : {}
11272
+ });
11273
+ if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
11274
+ const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
11275
+ await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
11276
+ }
10852
11277
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
10853
11278
  `);
10854
- out(`Project: ${cwd}
11279
+ out(result.config.hooks === "user" ? `Folder: ${cwd} (every session in it or below it uses this workspace)
11280
+ Key saved privately outside the folder.
11281
+ ` : `Project: ${cwd}
10855
11282
  Key saved privately outside the project.
10856
11283
  `);
10857
11284
  if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
@@ -10864,6 +11291,7 @@ Key saved privately outside the project.
10864
11291
  if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
10865
11292
  if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the five Bunshin hooks.\n");
10866
11293
  else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
11294
+ else if (result.hooks && result.config.hooks === "user") out("Open a new Claude Code session in any project under this folder to load the hooks. Sessions elsewhere are untouched.\nChange which folders take part with: bunshin sources\n");
10867
11295
  else if (result.hooks) out(`Open a new Claude Code session in this project to load the hooks${result.mcp?.installed ? " and approve the bunshin MCP server (.mcp.json)" : ""}.
10868
11296
  `);
10869
11297
  else out("Hooks are disabled. Run setup again without --no-hooks to configure them.\n");
@@ -10879,7 +11307,7 @@ Key saved privately outside the project.
10879
11307
  if (cmd === "mcp") {
10880
11308
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
10881
11309
  const root = str7(args.flags.root);
10882
- await runMcpServer2({ cwd, ...root ? { store: new Store(path25.resolve(root)) } : {}, version: version() });
11310
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
10883
11311
  return 0;
10884
11312
  }
10885
11313
  if (cmd === "demo") {
@@ -10906,6 +11334,14 @@ Key saved privately outside the project.
10906
11334
  }
10907
11335
  if (skipped.length && cmd === "status") err(`! ${untrustedNote(skipped[0])}
10908
11336
  `);
11337
+ if (["learn", "reflect", "recall", "load", "suggest"].includes(cmd) && !str7(args.flags.root)) {
11338
+ const { cwdInPool: cwdInPool2, topFolder: topFolder2 } = await Promise.resolve().then(() => (init_source(), source_exports));
11339
+ if (!cwdInPool2(store.config(), cwd, store.root)) {
11340
+ err(`bunshin: ${topFolder2(cwd, store.root)} is not part of this store's learning pool. Add it with \`bunshin sources include ${topFolder2(cwd, store.root)}\`.
11341
+ `);
11342
+ return 1;
11343
+ }
11344
+ }
10909
11345
  try {
10910
11346
  switch (cmd) {
10911
11347
  case "status":
@@ -10957,6 +11393,9 @@ Key saved privately outside the project.
10957
11393
  return await (await Promise.resolve().then(() => (init_skills(), skills_exports))).cmdLibrary(store, args, cwd, out, err);
10958
11394
  case "ui":
10959
11395
  return await (await Promise.resolve().then(() => (init_uicmd(), uicmd_exports))).cmdUi(store, args, out, err, version());
11396
+ case "sources":
11397
+ case "source":
11398
+ return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdSources(store, args, cwd, out, err);
10960
11399
  default:
10961
11400
  err(`bunshin: unknown command "${cmd}"
10962
11401
 
@@ -10972,7 +11411,7 @@ ${HELP2}`);
10972
11411
  function openStore(args, cwd) {
10973
11412
  const root = str7(args.flags.root);
10974
11413
  if (root) {
10975
- const store = new Store(path25.resolve(root));
11414
+ const store = new Store(path27.resolve(root));
10976
11415
  store.ensureLayout();
10977
11416
  return store;
10978
11417
  }
@@ -10988,7 +11427,7 @@ async function cmdInit(args, cwd, out, err) {
10988
11427
  orgId: str7(args.flags["org-id"]),
10989
11428
  userId: str7(args.flags["user-id"]),
10990
11429
  apiUrl: str7(args.flags["api-url"]),
10991
- apiKey: bool5(args.flags["key-stdin"]) ? fs30.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
11430
+ apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
10992
11431
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
10993
11432
  harness,
10994
11433
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -11031,10 +11470,10 @@ function cmdStatus(store, args, out, v2) {
11031
11470
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
11032
11471
  const metrics = store.readMetrics(void 0);
11033
11472
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
11034
- const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path25.join(process.env.CODEX_HOME ?? path25.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path25.dirname(store.root)).file])] : config.harness === "claude-code" ? [path25.join(homeDir(), ".claude", "settings.json"), path25.join(path25.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path25.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path25.join(path25.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
11473
+ 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")] : [];
11035
11474
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
11036
11475
  try {
11037
- return fs30.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11476
+ return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11038
11477
  } catch {
11039
11478
  return false;
11040
11479
  }
@@ -11052,7 +11491,7 @@ function cmdStatus(store, args, out, v2) {
11052
11491
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
11053
11492
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
11054
11493
  hooks: { [config.harness]: hooks },
11055
- hookFiles: hookFiles.filter((file) => fs30.existsSync(file)),
11494
+ hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
11056
11495
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
11057
11496
  v2
11058
11497
  };
@@ -11068,6 +11507,8 @@ function cmdStatus(store, args, out, v2) {
11068
11507
  out(`user ${config.userId} \u2014 ${config.role} @ ${config.orgUnit} (org ${config.orgId})
11069
11508
  `);
11070
11509
  out(`context ${v2.context.id} (${v2.context.kind}; layers ${v2.context.layers.join(", ")})
11510
+ `);
11511
+ if (v2.source) out(`source ${!v2.source.inPool ? `${v2.source.folder} is outside the learning pool (\`bunshin sources\`)` : v2.source.id ?? "none (the store's own folder)"}
11071
11512
  `);
11072
11513
  out(`library ${v2.bundle ? `${v2.bundle.sections} bundle section(s), generated ${v2.bundle.generatedAt}` : "no bundle yet"}; ${v2.repo.sections} working-tree section(s)${v2.repo.name ? ` in ${v2.repo.name}` : ""}
11073
11514
  `);
@@ -11093,7 +11534,7 @@ function cmdStatus(store, args, out, v2) {
11093
11534
  }
11094
11535
  function hookCount(file) {
11095
11536
  try {
11096
- const raw = JSON.parse(fs30.readFileSync(file, "utf8"));
11537
+ const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
11097
11538
  let n = 0;
11098
11539
  for (const groups of Object.values(raw.hooks ?? {})) {
11099
11540
  for (const g of groups ?? []) {
@@ -11213,7 +11654,7 @@ function cmdLessonAdd(store, args, out, err) {
11213
11654
  let content = str7(args.flags.content) ?? "";
11214
11655
  if (!content || content === "-" || args._.includes("-")) {
11215
11656
  try {
11216
- content = fs30.readFileSync(0, "utf8");
11657
+ content = fs32.readFileSync(0, "utf8");
11217
11658
  } catch {
11218
11659
  }
11219
11660
  }
@@ -11279,7 +11720,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11279
11720
  }
11280
11721
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11281
11722
  const result = await runReflection2(store, {
11282
- sessionId: str7(args.flags.session) ?? path25.basename(transcript).replace(/\.jsonl?$/, ""),
11723
+ sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
11283
11724
  transcriptPath: transcript,
11284
11725
  cwd,
11285
11726
  force: bool5(args.flags.force)
@@ -11404,6 +11845,10 @@ Usage: bunshin <command> [options]
11404
11845
  setup --harness codex|claude-code|pi connect this project to your hosted workspace
11405
11846
  --api-url <url> --no-hooks paste the key at the hidden prompt; profile is read from Bunshin
11406
11847
  --key-stdin read the key from stdin (for automated setup)
11848
+ --hooks user a folder of projects: hooks in your user settings, so every
11849
+ session under this folder uses its workspace (default: project)
11850
+ --only a,b | --exclude c | --all
11851
+ which of the folder's projects take part (asked when omitted)
11407
11852
  setup --robot --robot-id <id> --arm so101 --gripper <g> --mount <m> --cameras scene,wrist
11408
11853
  --domain <d> --site <s> --cell <c> [--depth] [--api-url <url>] [--harness claude-code]
11409
11854
  register this machine as a robot context (x-bunshin-context)
@@ -11413,6 +11858,8 @@ Usage: bunshin <command> [options]
11413
11858
  --harness claude-code|codex|pi|other --no-hooks --remove-hooks
11414
11859
 
11415
11860
  status [--json] store, context, library, outbox, hooks and sync health
11861
+ sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
11862
+ which folders under this store's folder take part (learning pool)
11416
11863
  recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
11417
11864
  pull candidates from the library (offline: local view)
11418
11865
  load <id> [--anchor <heading>] [--json]
@@ -11475,7 +11922,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
11475
11922
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
11476
11923
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
11477
11924
  `;
11478
- 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 ${path25.dirname(root)} to trust it.`;
11925
+ 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.`;
11479
11926
  }
11480
11927
  });
11481
11928