@bunshin-cc/bunshin 0.6.4 → 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 +899 -394
  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;
@@ -1749,7 +1749,7 @@ function servable(sections, reader) {
1749
1749
  return [...out.values()];
1750
1750
  }
1751
1751
  function recallable(sections, reader) {
1752
- return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop");
1752
+ return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
1753
1753
  }
1754
1754
  function passagesOfSection(s) {
1755
1755
  const hit = passageCache.get(s);
@@ -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;
@@ -1965,6 +1965,50 @@ var init_engine = __esm({
1965
1965
  }
1966
1966
  });
1967
1967
 
1968
+ // ../core/src/v2/guidelines.ts
1969
+ function reflectionGuidelines(sections, reader, opts = {}) {
1970
+ const pool = reader ? servable(sections, reader) : sections;
1971
+ return pool.filter((s) => s.delivery === "reflect" && s.status === "active").filter((s) => !opts.layer || s.layer === "org" || opts.layer === s.layer || opts.layer.startsWith(`${s.layer}/`)).sort((a, b) => generality(a.layer) - generality(b.layer) || a.id.localeCompare(b.id)).map((s) => ({ id: s.id, title: s.title, layer: s.layer, body: s.body }));
1972
+ }
1973
+ function guidelineRules(body) {
1974
+ const at = body.search(/^## /m);
1975
+ const rules = (at < 0 ? body : body.slice(at)).replace(/^## Rules[^\n]*\n/m, "").trim();
1976
+ return rules;
1977
+ }
1978
+ function guidelinesSection(gs, opts = {}) {
1979
+ const list2 = gs ?? [];
1980
+ const withRules = list2.map((g) => ({ g, rules: guidelineRules(g.body) })).filter((x) => x.rules);
1981
+ const rules = withRules.length ? `These rules come from reviewers' feedback on earlier lessons. Follow them; where they conflict
1982
+ with the defaults below, they win.
1983
+
1984
+ ${withRules.map(({ g, rules: rules2 }) => `### ${g.title}${list2.length > 1 ? ` (${g.layer})` : ""}
1985
+
1986
+ ${rules2}`).join("\n\n")}
1987
+ ` : "";
1988
+ const target = list2[0];
1989
+ const route = opts.forReflection && target ? `${rules ? "\n" : ""}When the person in this session gives feedback about lessons themselves (how they should be
1990
+ captured or written, what to leave out: for example "/learn do not write ticket-specific details"),
1991
+ that is a rule for these guidelines, not for a task document: op "append" to ${target.id},
1992
+ anchor "${GUIDELINES_ANCHOR}", one general rule in the imperative, with the reason.
1993
+ ` : "";
1994
+ if (!rules && !route) return "";
1995
+ return `
1996
+ ## How this company wants lessons written (reflection guidelines)
1997
+
1998
+ ${rules}${route}`;
1999
+ }
2000
+ var GUIDELINES_ANCHOR, generality;
2001
+ var init_guidelines = __esm({
2002
+ "../core/src/v2/guidelines.ts"() {
2003
+ "use strict";
2004
+ init_types();
2005
+ init_layers();
2006
+ init_engine();
2007
+ GUIDELINES_ANCHOR = "rules";
2008
+ generality = (l) => ({ org: 0, dept: 1, fleet: 1, team: 2, arm: 3, mount: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
2009
+ }
2010
+ });
2011
+
1968
2012
  // ../core/src/redact.ts
1969
2013
  function secretWord(w) {
1970
2014
  if (/tokeniz/.test(w)) return false;
@@ -2016,17 +2060,6 @@ function identLike(rest) {
2016
2060
  return humps.length > 0 && m[1].length / humps.length >= 3;
2017
2061
  });
2018
2062
  }
2019
- function luhnOk(digits) {
2020
- let total = 0;
2021
- let alt = false;
2022
- for (let i = digits.length - 1; i >= 0; i--) {
2023
- let d = digits.charCodeAt(i) - 48;
2024
- if (alt && (d *= 2) > 9) d -= 9;
2025
- total += d;
2026
- alt = !alt;
2027
- }
2028
- return total % 10 === 0;
2029
- }
2030
2063
  function issuerOk(d) {
2031
2064
  const n = d.length;
2032
2065
  if (d[0] === "4") return n === 13 || n === 16 || n === 19;
@@ -2038,23 +2071,44 @@ function issuerOk(d) {
2038
2071
  if (/^(?:50|5[6-8]|6\d)/.test(d)) return n >= 12 && n <= 19;
2039
2072
  return false;
2040
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
+ }
2041
2085
  function cardPrefix(s) {
2042
- const digits = s.replace(/\D/g, "");
2043
- if (/^3[1-3]|^2[019]/.test(digits)) return 0;
2044
- const sep = s.search(/[ -]/);
2045
- if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
2086
+ const d = CARD_DIGITS;
2046
2087
  const ends = [];
2047
- let n = 0;
2088
+ const counts = [];
2089
+ let digits = 0;
2090
+ let sep = -1;
2048
2091
  for (let i = 0, start = 0; i <= s.length; i++) {
2049
- 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;
2050
2099
  if (ends.length && i - start < 3) break;
2051
- n += i - start;
2052
- ends.push([i, n]);
2100
+ ends.push(i);
2101
+ counts.push(digits);
2053
2102
  start = i + 1;
2054
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;
2055
2106
  for (let k = ends.length - 1; k >= 0; k--) {
2056
- const [end, count] = ends[k];
2057
- 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];
2058
2112
  }
2059
2113
  return 0;
2060
2114
  }
@@ -2137,7 +2191,7 @@ function redactDeep(value, opts = {}) {
2137
2191
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
2138
2192
  return value;
2139
2193
  }
2140
- 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;
2141
2195
  var init_redact = __esm({
2142
2196
  "../core/src/redact.ts"() {
2143
2197
  "use strict";
@@ -2213,6 +2267,7 @@ var init_redact = __esm({
2213
2267
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2214
2268
  PATH_RE = /^(?:~|\.{1,2})?\/[\w.@%+~/-]*$|^[A-Za-z]:\\/;
2215
2269
  credentialish = (v) => /[\d\W_]/.test(v) || v.length >= 16 && /[a-z]/.test(v) && /[A-Z]/.test(v);
2270
+ CARD_DIGITS = new Uint8Array(64);
2216
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/;
2217
2272
  NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
2218
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}`;
@@ -2566,10 +2621,10 @@ ${input.digestNote ? `${input.digestNote}
2566
2621
 
2567
2622
  ` : ""}${input.transcriptDigest}
2568
2623
  ` : ""}
2569
- ${input.focus ? `## What to capture
2624
+ ${input.feedback ? feedbackSection(input.feedback) : ""}${input.focus ? `## What to capture
2570
2625
 
2571
2626
  ${input.focus}
2572
- ` : ""}
2627
+ ` : ""}${guidelinesSection(input.guidelines, { forReflection: true })}
2573
2628
  ## When something is worth capturing
2574
2629
 
2575
2630
  Capture only what would change how a future agent acts, and is recorded nowhere durable:
@@ -2586,6 +2641,16 @@ Capture only what would change how a future agent acts, and is recorded nowhere
2586
2641
  Label each suggestion with its "signal": a \u2192 "steering", b \u2192 "environment", c \u2192 "knowledge",
2587
2642
  d \u2192 "capability", e \u2192 "approach".
2588
2643
 
2644
+ ## How to write it
2645
+
2646
+ A lesson is read by agents on other tasks, months from now. Write the rule, not the story:
2647
+ - Say when it applies, what to do, and why, so it holds beyond this session.
2648
+ - Leave out what only this session had: dates, how many tickets or items there were, ticket ids,
2649
+ the particular cases it happened on, who asked. Name a specific repo, service, file, command or
2650
+ spec only when the lesson is about that thing. One short generic example is fine when it makes
2651
+ the rule clearer.
2652
+ - The date and source are added automatically; never write them yourself.
2653
+
2589
2654
  ## Where it goes
2590
2655
 
2591
2656
  Every entry is a whole document (a skill, a spec, a policy). Prefer editing an existing document
@@ -2636,6 +2701,34 @@ At most ${max} suggestions. Schema:
2636
2701
  "rationale": string, "urgency": "normal"|"urgent",
2637
2702
  "signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
2638
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
+ }
2639
2732
  function documentsSection(docs) {
2640
2733
  if (!docs?.length) return "";
2641
2734
  const body = docs.map((d) => `### ${d.id}: ${d.title} (${d.why})
@@ -2772,6 +2865,7 @@ var REFLECT_MARKER, ENVIRONMENT_RECHECK_DAYS, SIGNAL_WORDS, REFLECT_SCHEMA, KIND
2772
2865
  var init_reflect = __esm({
2773
2866
  "../core/src/v2/reflect.ts"() {
2774
2867
  "use strict";
2868
+ init_guidelines();
2775
2869
  init_redact();
2776
2870
  init_types();
2777
2871
  REFLECT_MARKER = "BUNSHIN:reflect";
@@ -2866,6 +2960,7 @@ var init_reconcile = __esm({
2866
2960
  var init_merge = __esm({
2867
2961
  "../core/src/v2/merge.ts"() {
2868
2962
  "use strict";
2963
+ init_guidelines();
2869
2964
  init_text();
2870
2965
  init_reconcile();
2871
2966
  init_markdown();
@@ -2898,6 +2993,16 @@ var init_profile = __esm({
2898
2993
  }
2899
2994
  });
2900
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
+
2901
3006
  // ../core/src/v2/index.ts
2902
3007
  var init_v2 = __esm({
2903
3008
  "../core/src/v2/index.ts"() {
@@ -2918,6 +3023,8 @@ var init_v2 = __esm({
2918
3023
  init_release();
2919
3024
  init_research();
2920
3025
  init_profile();
3026
+ init_guidelines();
3027
+ init_sources();
2921
3028
  }
2922
3029
  });
2923
3030
 
@@ -3538,7 +3645,7 @@ var init_src = __esm({
3538
3645
  // src/v2/store.ts
3539
3646
  import fs6 from "node:fs";
3540
3647
  import path5 from "node:path";
3541
- import { randomBytes as randomBytes3 } from "node:crypto";
3648
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
3542
3649
  function hexId(prefix) {
3543
3650
  return `${prefix}_${randomBytes3(6).toString("hex")}`;
3544
3651
  }
@@ -3560,7 +3667,7 @@ function normalizeSession(id, raw) {
3560
3667
  hinted: list2(raw.hinted)
3561
3668
  };
3562
3669
  }
3563
- var SAFE_ID, fileSafe, V2Store, addUnique;
3670
+ var SAFE_ID, fileSafe, V2Store, rootKey, addUnique;
3564
3671
  var init_store2 = __esm({
3565
3672
  "src/v2/store.ts"() {
3566
3673
  "use strict";
@@ -3695,11 +3802,12 @@ var init_store2 = __esm({
3695
3802
  return { status: raw.status, source: raw.source || "robot", ...raw.metrics ? { metrics: raw.metrics } : {}, at };
3696
3803
  }
3697
3804
  // ── repo index / sync state ──
3698
- readRepoIndex() {
3699
- 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);
3700
3808
  }
3701
- writeRepoIndex(index) {
3702
- writeJsonAtomic(this.p("repo-index.json"), index);
3809
+ writeRepoIndex(root, index) {
3810
+ writeJsonAtomic(this.p("repo-index", `${rootKey(root)}.json`), index);
3703
3811
  }
3704
3812
  readSyncState() {
3705
3813
  return readJson(this.p("sync.json"), {});
@@ -3767,6 +3875,7 @@ var init_store2 = __esm({
3767
3875
  return out;
3768
3876
  }
3769
3877
  };
3878
+ rootKey = (root) => createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 16);
3770
3879
  addUnique = (list2, ...ids) => {
3771
3880
  for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
3772
3881
  };
@@ -3774,7 +3883,8 @@ var init_store2 = __esm({
3774
3883
  });
3775
3884
 
3776
3885
  // src/v2/deploy.ts
3777
- import { createHash as createHash2 } from "node:crypto";
3886
+ import { execFileSync } from "node:child_process";
3887
+ import { createHash as createHash3 } from "node:crypto";
3778
3888
  import fs7 from "node:fs";
3779
3889
  import os2 from "node:os";
3780
3890
  import path6 from "node:path";
@@ -3787,46 +3897,31 @@ function deployWorkspace(env = process.env) {
3787
3897
  return null;
3788
3898
  }
3789
3899
  }
3790
- function deployPaths(env = process.env) {
3900
+ function deployPaths(env = process.env, orgId) {
3791
3901
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3792
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;
3793
3906
  return {
3794
3907
  home,
3795
- claudeSkills: path6.join(ws ?? home, ".claude", "skills"),
3796
- 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")])] } : {},
3797
3911
  claudeMd: ws ? path6.join(ws, "CLAUDE.md") : path6.join(home, ".claude", "CLAUDE.md"),
3798
3912
  agentsMd: ws ? path6.join(ws, "AGENTS.md") : path6.join(home, ".codex", "AGENTS.md"),
3799
- 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).
3800
3915
  bin: path6.join(home, ".bunshin", "bin"),
3801
- state: path6.join(home, ".bunshin", "deploy")
3916
+ state
3802
3917
  };
3803
3918
  }
3804
- function setDeployWorkspace(dir, env = process.env) {
3805
- const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3806
- const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
3807
- if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
3808
- const stateDir = path6.join(home, ".bunshin", "deploy");
3809
- const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
3810
- if (f === "settings.json" || !f.endsWith(".json")) return false;
3811
- try {
3812
- return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
3813
- } catch {
3814
- return false;
3815
- }
3816
- });
3817
- if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
3818
- throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
3819
- }
3820
- fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
3821
- fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
3822
- return ws;
3823
- }
3824
3919
  function statePath(p, orgId) {
3825
- return path6.join(p.state, `${orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org"}.json`);
3920
+ return path6.join(p.state, `${orgKey(orgId)}.json`);
3826
3921
  }
3827
3922
  function readState(p, orgId) {
3828
3923
  const s = readJson(statePath(p, orgId), null);
3829
- 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 } : {} };
3830
3925
  }
3831
3926
  function readText(file) {
3832
3927
  try {
@@ -4148,7 +4243,7 @@ function captureInto(v2, config, ctx, state, p, harnesses, now, incoming, captur
4148
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 }));
4149
4244
  }
4150
4245
  function captureEdits(v2, config, ctx, opts = {}) {
4151
- const p = opts.paths ?? deployPaths();
4246
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4152
4247
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4153
4248
  const release = lockDeploy(p);
4154
4249
  if (!release) return [];
@@ -4188,11 +4283,14 @@ function lockDeploy(p) {
4188
4283
  }
4189
4284
  return null;
4190
4285
  }
4286
+ function linkHarnesses(config) {
4287
+ return [.../* @__PURE__ */ new Set([config.harness, ...config.deploy?.harnesses ?? []])].filter((h) => h === "claude-code" || h === "codex");
4288
+ }
4191
4289
  function deployEnabled(config, ctx) {
4192
4290
  return ctx.kind === "person" && config.deploy?.enabled !== false;
4193
4291
  }
4194
4292
  function deployLibrary(v2, config, ctx, bundle, opts = {}) {
4195
- const p = opts.paths ?? deployPaths();
4293
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4196
4294
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4197
4295
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4198
4296
  const release = lockDeploy(p);
@@ -4209,6 +4307,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4209
4307
  const plan = planDeploy(bundle, p, harnesses);
4210
4308
  report.warnings.push(...plan.warnings);
4211
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));
4212
4311
  if (opts.capture !== false) report.captured = captureInto(v2, config, ctx, state, p, harnesses, now, plan.skills.map((x) => x.skill.name), opts.captureNewSkills);
4213
4312
  const decided = new Map(bundle.decisions.map((d) => [d.suggestionId, d.decision.status]));
4214
4313
  const localStatus = (id) => decided.get(id) ?? v2.readSuggestion(id)?.status;
@@ -4266,7 +4365,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4266
4365
  prevByAbs.delete(`${t.abs}#${t.role}`);
4267
4366
  }
4268
4367
  for (const old of prevByAbs.values()) {
4269
- 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;
4270
4369
  const cur = readText(old.abs);
4271
4370
  if (cur !== null && sha(cur) === old.sha) {
4272
4371
  removeFile(old.abs);
@@ -4275,6 +4374,11 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4275
4374
  }
4276
4375
  }
4277
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
+ }
4278
4382
  for (const dir of d.dirs ?? []) {
4279
4383
  const manifest = safeJoin(dir, MANIFEST);
4280
4384
  if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
@@ -4293,7 +4397,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4293
4397
  continue;
4294
4398
  }
4295
4399
  for (const t of doc.targets) {
4296
- if (t.role === "block" || !owned(t.abs)) continue;
4400
+ if (t.role === "block" || !removable(t.abs)) continue;
4297
4401
  const cur = readText(t.abs);
4298
4402
  if (cur !== null && sha(cur) === t.sha) {
4299
4403
  removeFile(t.abs);
@@ -4301,7 +4405,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4301
4405
  }
4302
4406
  }
4303
4407
  for (const dir of doc.dirs ?? []) {
4304
- if (!owned(dir)) continue;
4408
+ if (!removable(dir)) continue;
4305
4409
  removeFile(path6.join(dir, MANIFEST));
4306
4410
  pruneDirs(dir, path6.dirname(dir));
4307
4411
  if (fs7.existsSync(dir)) {
@@ -4348,6 +4452,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4348
4452
  const link = path6.join(dir, ".claude", "skills");
4349
4453
  try {
4350
4454
  fs7.lstatSync(link);
4455
+ if (fs7.readlinkSync(link) !== p.claudeSkills) {
4456
+ fs7.unlinkSync(link);
4457
+ fs7.symlinkSync(p.claudeSkills, link);
4458
+ }
4351
4459
  } catch {
4352
4460
  ensureDir(path6.dirname(link));
4353
4461
  ensureDir(p.claudeSkills);
@@ -4366,14 +4474,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4366
4474
  } catch {
4367
4475
  }
4368
4476
  }
4369
- 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;
4370
4490
  writeJsonAtomic(statePath(p, config.orgId), saved);
4371
4491
  report.libraryVersion = bundle.libraryVersion;
4372
4492
  return report;
4373
4493
  }
4374
- function deployedState(config, paths = deployPaths()) {
4494
+ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
4375
4495
  const s = readState(paths, config.orgId);
4376
- 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) };
4377
4497
  }
4378
4498
  function materialize(sections, dir) {
4379
4499
  let files = 0;
@@ -4396,7 +4516,89 @@ function materialize(sections, dir) {
4396
4516
  }
4397
4517
  return { files };
4398
4518
  }
4399
- 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;
4400
4602
  var init_deploy = __esm({
4401
4603
  "src/v2/deploy.ts"() {
4402
4604
  "use strict";
@@ -4404,7 +4606,8 @@ var init_deploy = __esm({
4404
4606
  init_util();
4405
4607
  init_store2();
4406
4608
  settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
4407
- 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");
4408
4611
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
4409
4612
  BLOCK_END = "<!-- bunshin:end -->";
4410
4613
  MANIFEST = ".bunshin.json";
@@ -4422,6 +4625,12 @@ var init_deploy = __esm({
4422
4625
  LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
4423
4626
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
4424
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";
4425
4634
  }
4426
4635
  });
4427
4636
 
@@ -4753,10 +4962,10 @@ var init_transcript = __esm({
4753
4962
  // src/v2/repo.ts
4754
4963
  import fs9 from "node:fs";
4755
4964
  import path7 from "node:path";
4756
- import { execFileSync } from "node:child_process";
4965
+ import { execFileSync as execFileSync2 } from "node:child_process";
4757
4966
  function git(cwd, args, timeout = 1500) {
4758
4967
  try {
4759
- 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 });
4760
4969
  } catch {
4761
4970
  return null;
4762
4971
  }
@@ -4826,7 +5035,7 @@ function listDocs(root) {
4826
5035
  function indexWorkingTree(v2, config, cwd, opts = {}) {
4827
5036
  const root = repoRoot(cwd, v2.store.root);
4828
5037
  const now = opts.now ?? Date.now();
4829
- const cached = v2.readRepoIndex();
5038
+ const cached = v2.readRepoIndex(root);
4830
5039
  const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
4831
5040
  const sameRepo = cached && cached.ownerTeam === ownerTeam;
4832
5041
  const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
@@ -4864,7 +5073,7 @@ function indexWorkingTree(v2, config, cwd, opts = {}) {
4864
5073
  };
4865
5074
  if (changed) {
4866
5075
  try {
4867
- v2.writeRepoIndex(index);
5076
+ v2.writeRepoIndex(root, index);
4868
5077
  } catch {
4869
5078
  }
4870
5079
  }
@@ -5449,6 +5658,110 @@ var init_ui = __esm({
5449
5658
  }
5450
5659
  });
5451
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
+
5452
5765
  // src/v2/sync.ts
5453
5766
  var sync_exports = {};
5454
5767
  __export(sync_exports, {
@@ -5458,8 +5771,8 @@ __export(sync_exports, {
5458
5771
  syncV2: () => syncV2,
5459
5772
  uploadIndex: () => uploadIndex
5460
5773
  });
5461
- import fs13 from "node:fs";
5462
- import path9 from "node:path";
5774
+ import fs14 from "node:fs";
5775
+ import path10 from "node:path";
5463
5776
  function episodePayload(e) {
5464
5777
  const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
5465
5778
  void _o, _u, _s, _rs, _rl, _tp;
@@ -5493,7 +5806,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5493
5806
  if (e.upload === "header-only" || e.cliVersion || !e.transcript) continue;
5494
5807
  if (now - Date.parse(e.endedAt || e.startedAt) > days * 864e5) continue;
5495
5808
  const file = e.transcriptPath ?? v2.readSession(e.sessionId)?.transcriptPath;
5496
- if (!file || !fs13.existsSync(file)) {
5809
+ if (!file || !fs14.existsSync(file)) {
5497
5810
  missing++;
5498
5811
  continue;
5499
5812
  }
@@ -5508,7 +5821,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5508
5821
  async function syncV2(store, opts = {}) {
5509
5822
  const v2 = new V2Store(store);
5510
5823
  const config = store.config();
5511
- const cwd = opts.cwd ?? path9.dirname(store.root);
5824
+ const cwd = opts.cwd ?? path10.dirname(store.root);
5512
5825
  const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
5513
5826
  const deadline = Date.now() + (opts.budgetMs ?? 24e4);
5514
5827
  if (report.offline) {
@@ -5624,7 +5937,8 @@ async function syncV2(store, opts = {}) {
5624
5937
  const bundle = v2.readBundle();
5625
5938
  if (bundle) {
5626
5939
  try {
5627
- 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) });
5628
5942
  } catch (err) {
5629
5943
  report.errors.push(`deploy: ${err instanceof Error ? err.message : String(err)}`);
5630
5944
  }
@@ -5654,7 +5968,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
5654
5968
  const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
5655
5969
  const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
5656
5970
  try {
5657
- return [{ path: rel, text: fs13.readFileSync(path9.join(root, rel), "utf8") }];
5971
+ return [{ path: rel, text: fs14.readFileSync(path10.join(root, rel), "utf8") }];
5658
5972
  } catch {
5659
5973
  return [];
5660
5974
  }
@@ -5672,6 +5986,7 @@ var init_sync = __esm({
5672
5986
  init_api();
5673
5987
  init_apply();
5674
5988
  init_deploy();
5989
+ init_source();
5675
5990
  init_repo();
5676
5991
  init_store2();
5677
5992
  init_view();
@@ -5682,10 +5997,10 @@ var init_sync = __esm({
5682
5997
 
5683
5998
  // src/v2/reflect.ts
5684
5999
  import { spawn as spawn3 } from "node:child_process";
5685
- import { createHash as createHash3 } from "node:crypto";
5686
- import fs14 from "node:fs";
6000
+ import { createHash as createHash4 } from "node:crypto";
6001
+ import fs15 from "node:fs";
5687
6002
  function episodeIdFor(contextId, sessionId) {
5688
- return `e_${createHash3("sha256").update(`${contextId}
6003
+ return `e_${createHash4("sha256").update(`${contextId}
5689
6004
  ${sessionId}`).digest("hex").slice(0, 16)}`;
5690
6005
  }
5691
6006
  function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT_MS } = {}) {
@@ -5718,7 +6033,7 @@ function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT
5718
6033
  };
5719
6034
  let child;
5720
6035
  try {
5721
- 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"] });
5722
6037
  } catch (e) {
5723
6038
  resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
5724
6039
  return;
@@ -5763,7 +6078,7 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
5763
6078
  return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
5764
6079
  }
5765
6080
  function humanTurnsOf(transcriptPath) {
5766
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return [];
6081
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return [];
5767
6082
  try {
5768
6083
  return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
5769
6084
  } catch {
@@ -5771,7 +6086,7 @@ function humanTurnsOf(transcriptPath) {
5771
6086
  }
5772
6087
  }
5773
6088
  function readFacts(transcriptPath) {
5774
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return null;
6089
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return null;
5775
6090
  try {
5776
6091
  return parseTranscript(transcriptPath);
5777
6092
  } catch {
@@ -5814,7 +6129,8 @@ async function runReflectionV2(store, opts) {
5814
6129
  const from = reflectCursor(session, turns);
5815
6130
  const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
5816
6131
  const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
5817
- const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
6132
+ const guidelines = reflectionGuidelines(view.sections, view.reader);
6133
+ const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
5818
6134
  let raw = null;
5819
6135
  if (harness === "claude-code") {
5820
6136
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
@@ -5833,7 +6149,7 @@ async function runReflectionV2(store, opts) {
5833
6149
  result.method = "fork";
5834
6150
  } else result.error = fork.error;
5835
6151
  }
5836
- if (result.method === "none" && session.transcriptPath && fs14.existsSync(session.transcriptPath)) {
6152
+ if (result.method === "none" && session.transcriptPath && fs15.existsSync(session.transcriptPath)) {
5837
6153
  const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
5838
6154
  const res = await askJson(config, {
5839
6155
  system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
@@ -5854,8 +6170,10 @@ async function runReflectionV2(store, opts) {
5854
6170
  result.nothingNew = result.method !== "none" && parsed.length === 0;
5855
6171
  const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
5856
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);
5857
6174
  for (const p of parsed) {
5858
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;
5859
6177
  s.summary = summarize(s.op);
5860
6178
  if (apply) {
5861
6179
  const outcome = applyLocally(v2, view, s);
@@ -5939,6 +6257,7 @@ var init_reflect3 = __esm({
5939
6257
  init_signals();
5940
6258
  init_ui();
5941
6259
  init_view();
6260
+ init_source();
5942
6261
  FORK_TIMEOUT_MS = 12e4;
5943
6262
  FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
5944
6263
  }
@@ -5956,51 +6275,51 @@ __export(uicmd_exports, {
5956
6275
  modSource: () => modSource
5957
6276
  });
5958
6277
  import { spawnSync } from "node:child_process";
5959
- import fs15 from "node:fs";
6278
+ import fs16 from "node:fs";
5960
6279
  import os3 from "node:os";
5961
- import path10 from "node:path";
6280
+ import path11 from "node:path";
5962
6281
  import { fileURLToPath } from "node:url";
5963
6282
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
5964
- return path10.join(home, ".bunshin", "claude-code-ui");
6283
+ return path11.join(home, ".bunshin", "claude-code-ui");
5965
6284
  }
5966
6285
  function modSource() {
5967
- const here = path10.dirname(fileURLToPath(import.meta.url));
5968
- for (const dir of [path10.join(here, "claude-code-ui"), path10.join(here, "..", "claude-code-ui"), path10.resolve(here, "../../../../adapters/claude-code-ui")]) {
5969
- 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;
5970
6289
  }
5971
6290
  return null;
5972
6291
  }
5973
6292
  function installedModVersion(home) {
5974
6293
  try {
5975
- 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;
5976
6295
  } catch {
5977
6296
  return null;
5978
6297
  }
5979
6298
  }
5980
6299
  function copyDir(from, to) {
5981
- fs15.mkdirSync(to, { recursive: true });
5982
- 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 })) {
5983
6302
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
5984
6303
  if (e.name === "node_modules") continue;
5985
- const a = path10.join(from, e.name);
5986
- const b = path10.join(to, e.name);
6304
+ const a = path11.join(from, e.name);
6305
+ const b = path11.join(to, e.name);
5987
6306
  if (e.isDirectory()) copyDir(a, b);
5988
- else fs15.copyFileSync(a, b);
6307
+ else fs16.copyFileSync(a, b);
5989
6308
  }
5990
6309
  }
5991
6310
  function installMod(version2, opts = {}) {
5992
6311
  const src = modSource();
5993
6312
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
5994
6313
  const home = modHome(opts.home);
5995
- const dir = path10.join(home, MOD_PLUGIN);
5996
- fs15.rmSync(dir, { recursive: true, force: true });
6314
+ const dir = path11.join(home, MOD_PLUGIN);
6315
+ fs16.rmSync(dir, { recursive: true, force: true });
5997
6316
  copyDir(src, dir);
5998
- const manifest = path10.join(dir, ".claude-plugin", "plugin.json");
5999
- const m = JSON.parse(fs15.readFileSync(manifest, "utf8"));
6000
- fs15.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6001
- fs15.mkdirSync(path10.join(home, ".claude-plugin"), { recursive: true });
6002
- fs15.writeFileSync(
6003
- 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"),
6004
6323
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6005
6324
  );
6006
6325
  const run2 = opts.run ?? spawnSync;
@@ -6035,7 +6354,7 @@ async function cmdUi(store, args, out, err, version2) {
6035
6354
  `);
6036
6355
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6037
6356
  `);
6038
- 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");
6039
6358
  }
6040
6359
  return installed || !failed.length ? 0 : 1;
6041
6360
  }
@@ -6116,19 +6435,19 @@ __export(update_exports, {
6116
6435
  updateCheckDue: () => updateCheckDue,
6117
6436
  updateNotice: () => updateNotice
6118
6437
  });
6119
- import fs16 from "node:fs";
6120
- import path11 from "node:path";
6438
+ import fs17 from "node:fs";
6439
+ import path12 from "node:path";
6121
6440
  function readState2() {
6122
6441
  try {
6123
- return JSON.parse(fs16.readFileSync(stateFile(), "utf8"));
6442
+ return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6124
6443
  } catch {
6125
6444
  return {};
6126
6445
  }
6127
6446
  }
6128
6447
  function writeState(s) {
6129
6448
  try {
6130
- fs16.mkdirSync(path11.dirname(stateFile()), { recursive: true });
6131
- 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");
6132
6451
  } catch {
6133
6452
  }
6134
6453
  }
@@ -6178,14 +6497,14 @@ var init_update = __esm({
6178
6497
  PKG = "@bunshin-cc/bunshin";
6179
6498
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6180
6499
  DAY = 864e5;
6181
- stateFile = () => path11.join(homeDir(), ".bunshin", "update.json");
6500
+ stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6182
6501
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6183
6502
  }
6184
6503
  });
6185
6504
 
6186
6505
  // src/v2/hooks.ts
6187
- import fs17 from "node:fs";
6188
- import path12 from "node:path";
6506
+ import fs18 from "node:fs";
6507
+ import path13 from "node:path";
6189
6508
  function inContext(s) {
6190
6509
  return s.inContext ?? [...s.loaded, ...s.pushed];
6191
6510
  }
@@ -6200,6 +6519,12 @@ function touch(v2, ctx, patch = () => {
6200
6519
  const s = v2.updateSession(id, ctx.cwd, (s2) => {
6201
6520
  s2.cwd = ctx.cwd || s2.cwd;
6202
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
+ }
6203
6528
  if (ctx.input.transcript_path) s2.transcriptPath = ctx.input.transcript_path;
6204
6529
  patch(s2);
6205
6530
  });
@@ -6246,10 +6571,10 @@ async function keepModCurrent(ctx, config) {
6246
6571
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6247
6572
  const current = version2();
6248
6573
  if (!modSource2() || installedModVersion2() === current) return;
6249
- const marker = path12.join(modHome2(), `.attempted-${current}`);
6250
- if (fs17.existsSync(marker)) return;
6251
- fs17.mkdirSync(modHome2(), { recursive: true });
6252
- 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());
6253
6578
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6254
6579
  } catch {
6255
6580
  }
@@ -6353,7 +6678,7 @@ function liveUpdate(v2, ctx, session) {
6353
6678
  }
6354
6679
  const config = ctx.store.config();
6355
6680
  const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
6356
- const paths = deployed ? deployPaths() : null;
6681
+ const paths = deployed ? deployPaths(process.env, config.orgId) : null;
6357
6682
  const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
6358
6683
  const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
6359
6684
  const { text: text2, next } = changesSince(session.live, bundle, skillPath);
@@ -6378,13 +6703,13 @@ function skillHint(sec, others) {
6378
6703
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6379
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}`;
6380
6705
  }
6381
- async function remoteAuto(ctx, prompt, exclude) {
6706
+ async function remoteAuto(ctx, prompt, exclude, source) {
6382
6707
  const config = ctx.store.config();
6383
6708
  if (!canCall(config) || !config.recall.useMothership) return null;
6384
6709
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6385
6710
  if (budget < 300) return null;
6386
6711
  try {
6387
- 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 });
6388
6713
  return res.available ? res : null;
6389
6714
  } catch {
6390
6715
  return null;
@@ -6439,7 +6764,7 @@ async function onPrompt(ctx) {
6439
6764
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6440
6765
  }
6441
6766
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6442
- const remote = await remoteAuto(ctx, prompt, injected);
6767
+ const remote = await remoteAuto(ctx, prompt, injected, session.source);
6443
6768
  if (remote) {
6444
6769
  const local = (id) => view.sections.find((s) => s.id === id);
6445
6770
  if (remote.section) {
@@ -6591,6 +6916,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6591
6916
  const v2 = new V2Store(store);
6592
6917
  const config = store.config();
6593
6918
  const view = localView(v2, config, session.cwd, { index: false });
6919
+ const source = sessionSource(store, session, view.ctx);
6594
6920
  const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
6595
6921
  const outcome = v2.readOutcome(session.sessionId);
6596
6922
  const turns = scan.facts.turns.length;
@@ -6630,6 +6956,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6630
6956
  ...outcome ? { outcome } : {},
6631
6957
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
6632
6958
  cliVersion: version(),
6959
+ ...source ? { source } : {},
6633
6960
  uploaded: false,
6634
6961
  ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
6635
6962
  ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
@@ -6637,6 +6964,14 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6637
6964
  };
6638
6965
  return { episode, gate, scan };
6639
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
+ }
6640
6975
  function checkpointDue(session, scan, now) {
6641
6976
  const since = Date.parse(session.checkpointAt ?? session.startedAt);
6642
6977
  if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
@@ -6751,6 +7086,7 @@ var init_hooks = __esm({
6751
7086
  init_signals();
6752
7087
  init_store2();
6753
7088
  init_ui();
7089
+ init_source();
6754
7090
  init_view();
6755
7091
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
6756
7092
  NUDGE_INTERVAL_MS = 60 * 6e4;
@@ -6765,6 +7101,8 @@ var init_hooks = __esm({
6765
7101
  });
6766
7102
 
6767
7103
  // src/hooks.ts
7104
+ import fs19 from "node:fs";
7105
+ import path14 from "node:path";
6768
7106
  function normalizeEvent(raw) {
6769
7107
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
6770
7108
  }
@@ -6808,6 +7146,8 @@ async function handleHook(rawEvent, input, opts = {}) {
6808
7146
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
6809
7147
  if (!store) return {};
6810
7148
  const config = store.config();
7149
+ if (!cwdInPool(config, cwd, store.root)) return {};
7150
+ if (deferToProject(cwd)) return {};
6811
7151
  const budgetMs = Math.min(opts.budgetMs ?? config.recall.hookBudgetMs ?? HARD_BUDGET_MS, HARD_BUDGET_MS);
6812
7152
  const deadline = Date.now() + budgetMs;
6813
7153
  try {
@@ -6828,6 +7168,24 @@ async function handleHook(rawEvent, input, opts = {}) {
6828
7168
  return {};
6829
7169
  }
6830
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
+ }
6831
7189
  async function hookCommand(rawEvent, opts = {}) {
6832
7190
  const write = opts.write ?? ((s) => process.stdout.write(s));
6833
7191
  let output = {};
@@ -6850,6 +7208,7 @@ var init_hooks2 = __esm({
6850
7208
  init_detach();
6851
7209
  init_store();
6852
7210
  init_hooks();
7211
+ init_source();
6853
7212
  HARD_BUDGET_MS = 4e3;
6854
7213
  EVENT_NAMES = {
6855
7214
  "session-start": "session-start",
@@ -6869,32 +7228,32 @@ var init_hooks2 = __esm({
6869
7228
  });
6870
7229
 
6871
7230
  // src/project-hooks.ts
6872
- import fs18 from "node:fs";
6873
- import path13 from "node:path";
6874
- 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";
6875
7234
  function codexProjectHooks(cwd) {
6876
- const legacyFile = path13.join(cwd, ".codex", "hooks.json");
6877
- 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], {
6878
7237
  encoding: "utf8",
6879
7238
  stdio: ["ignore", "pipe", "ignore"],
6880
7239
  timeout: 2e3
6881
7240
  }).trim();
6882
7241
  let root, common, directory;
6883
7242
  try {
6884
- root = git4("--show-toplevel");
6885
- common = git4("--path-format=absolute", "--git-common-dir");
6886
- 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");
6887
7246
  } catch {
6888
7247
  return { file: legacyFile, shared: false, legacyFile };
6889
7248
  }
6890
- const linked = fs18.realpathSync(common) !== fs18.realpathSync(directory);
7249
+ const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
6891
7250
  if (linked) {
6892
- if (path13.basename(common) !== ".git" || !fs18.statSync(common).isDirectory()) {
7251
+ if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
6893
7252
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
6894
7253
  }
6895
- root = path13.dirname(common);
7254
+ root = path15.dirname(common);
6896
7255
  }
6897
- 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");
6898
7257
  return { file, shared: linked, legacyFile };
6899
7258
  }
6900
7259
  var init_project_hooks = __esm({
@@ -6917,6 +7276,7 @@ __export(init_exports, {
6917
7276
  installLearnCommand: () => installLearnCommand,
6918
7277
  installMcpConfig: () => installMcpConfig,
6919
7278
  installPiExtension: () => installPiExtension,
7279
+ installUserMcp: () => installUserMcp,
6920
7280
  isBunshinEntry: () => isBunshinEntry,
6921
7281
  mcpLauncher: () => mcpLauncher,
6922
7282
  mergeClaudeSettings: () => mergeClaudeSettings,
@@ -6924,8 +7284,9 @@ __export(init_exports, {
6924
7284
  removeBunshinHooks: () => removeBunshinHooks,
6925
7285
  runInit: () => runInit
6926
7286
  });
6927
- import fs19 from "node:fs";
6928
- 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";
6929
7290
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
6930
7291
  function isBunshinEntry(entry) {
6931
7292
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -6950,13 +7311,13 @@ function removeBunshinHooks(settings) {
6950
7311
  else delete out.hooks;
6951
7312
  return out;
6952
7313
  }
6953
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code" } = {}) {
7314
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
6954
7315
  const base = removeBunshinHooks(settings);
6955
7316
  const hooks = { ...base.hooks ?? {} };
6956
7317
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
6957
7318
  const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
6958
7319
  const hook = bunshinHookEntry(arg, timeout, bin);
6959
- hook.command = `BUNSHIN_HARNESS=${harness} ${hook.command}`;
7320
+ hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
6960
7321
  if (harness === "codex") delete hook._bunshin;
6961
7322
  groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
6962
7323
  hooks[event] = groups;
@@ -6970,16 +7331,16 @@ function countBunshinHooks(settings) {
6970
7331
  );
6971
7332
  }
6972
7333
  function claudeSettingsPath() {
6973
- return path14.join(homeDir(), ".claude", "settings.json");
7334
+ return path16.join(homeDir(), ".claude", "settings.json");
6974
7335
  }
6975
7336
  function installPiExtension(cwd, remove = false, scope = "repo") {
6976
- 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");
6977
7338
  const marker = "// Managed by Bunshin setup.";
6978
7339
  try {
6979
- const previous = fileExists(file) ? fs19.readFileSync(file, "utf8") : "";
7340
+ const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
6980
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.");
6981
7342
  if (remove) {
6982
- if (previous) fs19.unlinkSync(file);
7343
+ if (previous) fs21.unlinkSync(file);
6983
7344
  return { file, installed: 0, changed: !!previous };
6984
7345
  }
6985
7346
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -6988,24 +7349,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
6988
7349
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
6989
7350
  `;
6990
7351
  if (text2 !== previous) {
6991
- ensureDir(path14.dirname(file));
6992
- fs19.writeFileSync(file, text2);
7352
+ ensureDir(path16.dirname(file));
7353
+ fs21.writeFileSync(file, text2);
6993
7354
  }
6994
7355
  return { file, installed: 1, changed: text2 !== previous };
6995
7356
  } catch (error) {
6996
7357
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
6997
7358
  }
6998
7359
  }
6999
- 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 } = {}) {
7000
7361
  try {
7001
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7002
- 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 });
7003
7364
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7004
7365
  if (changed) {
7005
- ensureDir(path14.dirname(file));
7366
+ ensureDir(path16.dirname(file));
7006
7367
  if (fileExists(file)) {
7007
7368
  try {
7008
- fs19.copyFileSync(file, `${file}.bunshin-backup`);
7369
+ fs21.copyFileSync(file, `${file}.bunshin-backup`);
7009
7370
  } catch {
7010
7371
  }
7011
7372
  }
@@ -7020,23 +7381,23 @@ function mcpLauncher(bin) {
7020
7381
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7021
7382
  let script = process.argv[1] ?? "";
7022
7383
  try {
7023
- script = fs19.realpathSync(script);
7384
+ script = fs21.realpathSync(script);
7024
7385
  } catch {
7025
7386
  }
7026
7387
  return { command: process.execPath, args: [script, "mcp"] };
7027
7388
  }
7028
7389
  function installLearnCommand({ file, remove = false }) {
7029
7390
  try {
7030
- const current = fileExists(file) ? fs19.readFileSync(file, "utf8") : null;
7391
+ const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7031
7392
  const ours = current === null || current === LEARN_COMMAND;
7032
7393
  if (remove) {
7033
- if (current !== null && ours) fs19.unlinkSync(file);
7394
+ if (current !== null && ours) fs21.unlinkSync(file);
7034
7395
  return { file, installed: 0, changed: current !== null && ours };
7035
7396
  }
7036
7397
  if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
7037
7398
  if (current === null) {
7038
- fs19.mkdirSync(path14.dirname(file), { recursive: true });
7039
- fs19.writeFileSync(file, LEARN_COMMAND);
7399
+ fs21.mkdirSync(path16.dirname(file), { recursive: true });
7400
+ fs21.writeFileSync(file, LEARN_COMMAND);
7040
7401
  }
7041
7402
  return { file, installed: 1, changed: current === null };
7042
7403
  } catch (err) {
@@ -7045,7 +7406,7 @@ function installLearnCommand({ file, remove = false }) {
7045
7406
  }
7046
7407
  function installMcpConfig({ file, launcher, remove = false }) {
7047
7408
  try {
7048
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7409
+ const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7049
7410
  const servers = { ...before.mcpServers ?? {} };
7050
7411
  if (remove) delete servers.bunshin;
7051
7412
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7053,7 +7414,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7053
7414
  if (!Object.keys(servers).length) delete after.mcpServers;
7054
7415
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7055
7416
  if (changed) {
7056
- if (!Object.keys(after).length && fileExists(file)) fs19.unlinkSync(file);
7417
+ if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7057
7418
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7058
7419
  }
7059
7420
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7061,25 +7422,49 @@ function installMcpConfig({ file, launcher, remove = false }) {
7061
7422
  return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
7062
7423
  }
7063
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
+ }
7064
7448
  function runInit(opts = {}) {
7065
7449
  const cwd = opts.cwd ?? process.cwd();
7066
7450
  const warnings = [];
7067
7451
  const scope = opts.scope ?? "repo";
7068
- const root = scope === "global" ? path14.join(homeDir(), ".bunshin") : path14.join(cwd, ".bunshin");
7069
- 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) {
7070
7454
  const aside = `${root}.untrusted-${Date.now()}`;
7071
- fs19.renameSync(root, aside);
7455
+ fs21.renameSync(root, aside);
7072
7456
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7073
7457
  }
7074
- const existed = fileExists(path14.join(root, "config.json"));
7458
+ const existed = fileExists(path16.join(root, "config.json"));
7075
7459
  const store = new Store(root);
7076
7460
  store.ensureLayout();
7077
- fs19.chmodSync(root, 448);
7078
- if (scope === "repo") {
7079
- const ignore = path14.join(cwd, ".gitignore");
7080
- 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") : "";
7081
7466
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7082
- fs19.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7467
+ fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7083
7468
  `);
7084
7469
  }
7085
7470
  }
@@ -7087,6 +7472,7 @@ function runInit(opts = {}) {
7087
7472
  const config = {
7088
7473
  ...previous,
7089
7474
  scope,
7475
+ hooks: hookScope,
7090
7476
  orgId: opts.orgId ?? previous.orgId,
7091
7477
  userId: opts.userId ?? previous.userId,
7092
7478
  role: opts.role ?? previous.role,
@@ -7103,18 +7489,22 @@ function runInit(opts = {}) {
7103
7489
  if (!fileExists(store.indexFile)) store.rebuildIndex();
7104
7490
  let hooks;
7105
7491
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "pi") {
7106
- hooks = installPiExtension(cwd, opts.removeHooks, scope);
7492
+ hooks = installPiExtension(cwd, opts.removeHooks, hookScope === "user" ? "global" : "repo");
7107
7493
  if (hooks.error) warnings.push(hooks.error);
7108
7494
  }
7109
7495
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7110
- const projectHooks = config.harness === "codex" && scope === "repo" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7111
- 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());
7112
- 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
+ }
7113
7503
  if (projectHooks?.shared) {
7114
7504
  hooks.sharedWithWorktrees = true;
7115
- if (!hooks.error && !opts.removeHooks) ensureDir(path14.dirname(projectHooks.legacyFile));
7505
+ if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
7116
7506
  }
7117
- 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)) {
7118
7508
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7119
7509
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7120
7510
  }
@@ -7122,10 +7512,11 @@ function runInit(opts = {}) {
7122
7512
  }
7123
7513
  let mcp;
7124
7514
  let learnCommand;
7125
- if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code" && scope === "repo") {
7126
- 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 });
7127
7518
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
7128
- 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 });
7129
7520
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7130
7521
  }
7131
7522
  if (!resolveBackend(config)) {
@@ -7260,6 +7651,7 @@ __export(commands_exports, {
7260
7651
  cmdReflectFork: () => cmdReflectFork,
7261
7652
  cmdReview: () => cmdReview,
7262
7653
  cmdReviewDecide: () => cmdReviewDecide,
7654
+ cmdSources: () => cmdSources,
7263
7655
  cmdSuggest: () => cmdSuggest,
7264
7656
  cmdSync: () => cmdSync,
7265
7657
  robotAttrsFromFlags: () => robotAttrsFromFlags,
@@ -7267,9 +7659,9 @@ __export(commands_exports, {
7267
7659
  setupRobot: () => setupRobot,
7268
7660
  v2Status: () => v2Status
7269
7661
  });
7270
- import fs20 from "node:fs";
7271
- import path15 from "node:path";
7272
- 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";
7273
7665
  function cmdReview(store, args, cwd, out, err) {
7274
7666
  const v2 = new V2Store(store);
7275
7667
  const sub = args._[1] ?? "list";
@@ -7438,11 +7830,11 @@ function cmdSuggest(store, args, cwd, out, err) {
7438
7830
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
7439
7831
  return 1;
7440
7832
  }
7441
- const harnessDir = path15.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
7442
- 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);
7443
7835
  let content;
7444
7836
  try {
7445
- content = fs20.readFileSync(file, "utf8");
7837
+ content = fs22.readFileSync(file, "utf8");
7446
7838
  } catch {
7447
7839
  err(`bunshin suggest: cannot read ${file}
7448
7840
  `);
@@ -7563,15 +7955,15 @@ async function cmdQuestions(store, args, out, err) {
7563
7955
  return 0;
7564
7956
  }
7565
7957
  function saveCredential(storeRoot, apiUrl, apiKey, id) {
7566
- const dir = path15.join(homeDir(), ".config/bunshin/credentials");
7567
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
7568
- fs20.chmodSync(dir, 448);
7569
- 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}
7570
7962
  ${apiUrl}
7571
7963
  ${id}`).digest("hex")}.json`);
7572
7964
  const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
7573
- fs20.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7574
- fs20.renameSync(tmp, file);
7965
+ fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7966
+ fs22.renameSync(tmp, file);
7575
7967
  return file;
7576
7968
  }
7577
7969
  async function setupRobot(opts) {
@@ -7579,10 +7971,10 @@ async function setupRobot(opts) {
7579
7971
  const existing = Store.discover(opts.cwd);
7580
7972
  let store;
7581
7973
  const harness = opts.harness ?? "other";
7582
- 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;
7583
7975
  else {
7584
7976
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
7585
- store = new Store(path15.join(opts.cwd, ".bunshin"));
7977
+ store = new Store(path17.join(opts.cwd, ".bunshin"));
7586
7978
  }
7587
7979
  let config = store.config();
7588
7980
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -7645,8 +8037,10 @@ function v2Status(store, cwd) {
7645
8037
  }
7646
8038
  const view = localView(v2, config, cwd, { index: false });
7647
8039
  const sync = v2.readSyncState();
8040
+ const pooled = cwdInPool(config, cwd, store.root);
7648
8041
  return {
7649
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 },
7650
8044
  bundle: bundle ? { sections: bundle.sections.length, generatedAt: bundle.generatedAt, release: bundle.release?.version ?? null } : null,
7651
8045
  repo: { name: repo, sections: repoSections },
7652
8046
  overlays: v2.readOverlays().length,
@@ -7657,6 +8051,46 @@ function v2Status(store, cwd) {
7657
8051
  session: v2.readCurrent()?.sessionId ?? null
7658
8052
  };
7659
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
+ }
7660
8094
  var str3, bool2, list;
7661
8095
  var init_commands = __esm({
7662
8096
  "src/v2/commands.ts"() {
@@ -7673,6 +8107,7 @@ var init_commands = __esm({
7673
8107
  init_repo();
7674
8108
  init_store2();
7675
8109
  init_sync();
8110
+ init_source();
7676
8111
  init_view();
7677
8112
  str3 = (v) => typeof v === "string" ? v : void 0;
7678
8113
  bool2 = (v) => v === true || v === "true";
@@ -7685,6 +8120,7 @@ var setup_exports = {};
7685
8120
  __export(setup_exports, {
7686
8121
  HOSTED_API: () => HOSTED_API,
7687
8122
  apiAddress: () => apiAddress,
8123
+ askSources: () => askSources,
7688
8124
  connectionProfile: () => connectionProfile,
7689
8125
  executable: () => executable,
7690
8126
  hookBinary: () => hookBinary,
@@ -7692,16 +8128,16 @@ __export(setup_exports, {
7692
8128
  setupProject: () => setupProject,
7693
8129
  shellQuote: () => shellQuote
7694
8130
  });
7695
- import fs21 from "node:fs";
7696
- import path16 from "node:path";
7697
- 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";
7698
8134
  function executable(name) {
7699
- 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")];
7700
8136
  for (const directory of directories) {
7701
- const file = path16.resolve(directory, name);
8137
+ const file = path18.resolve(directory, name);
7702
8138
  try {
7703
- fs21.accessSync(file, fs21.constants.X_OK);
7704
- if (fs21.statSync(file).isFile()) return file;
8139
+ fs23.accessSync(file, fs23.constants.X_OK);
8140
+ if (fs23.statSync(file).isFile()) return file;
7705
8141
  } catch {
7706
8142
  }
7707
8143
  }
@@ -7709,8 +8145,8 @@ function executable(name) {
7709
8145
  }
7710
8146
  function hookBinary(harness) {
7711
8147
  const agent = executable(harness === "claude-code" ? "claude" : harness);
7712
- const directories = [.../* @__PURE__ */ new Set([path16.dirname(process.execPath), path16.join(homeDir(), ".local/bin"), ...agent ? [path16.dirname(agent)] : []])];
7713
- 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]))}`;
7714
8150
  }
7715
8151
  function apiAddress(raw) {
7716
8152
  const url = new URL(raw);
@@ -7779,33 +8215,37 @@ async function setupProject(opts) {
7779
8215
  const apiKey = opts.apiKey.trim();
7780
8216
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
7781
8217
  const profile = await connectionProfile(apiUrl, apiKey);
7782
- const root = path16.join(opts.cwd, ".bunshin");
8218
+ const root = path18.join(opts.cwd, ".bunshin");
7783
8219
  const store = new Store(root);
7784
8220
  const previous = store.config();
7785
8221
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
7786
8222
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
7787
8223
  }
7788
- const credentialDirectory = path16.join(homeDir(), ".config/bunshin/credentials");
7789
- fs21.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
7790
- fs21.chmodSync(credentialDirectory, 448);
7791
- 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}
7792
8228
  ${apiUrl}
7793
8229
  ${profile.orgId}
7794
8230
  ${profile.userId}`).digest("hex");
7795
- const credentialFile = path16.join(credentialDirectory, `${name}.json`);
8231
+ const credentialFile = path18.join(credentialDirectory, `${name}.json`);
7796
8232
  const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
7797
- fs21.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7798
- fs21.renameSync(temporary, credentialFile);
8233
+ fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8234
+ fs23.renameSync(temporary, credentialFile);
7799
8235
  const result = runInit({
7800
8236
  cwd: opts.cwd,
7801
8237
  harness: opts.harness,
7802
8238
  apiUrl,
7803
8239
  ...profile,
7804
8240
  noHooks: opts.noHooks,
7805
- bin: opts.bin ?? hookBinary(opts.harness)
8241
+ bin: opts.bin ?? hookBinary(opts.harness),
8242
+ ...opts.hooks ? { hooks: opts.hooks } : {}
7806
8243
  });
8244
+ const { sources: kept, ...base } = result.config;
8245
+ const sources = opts.sources === void 0 ? kept : opts.sources ?? void 0;
7807
8246
  store.saveConfig({
7808
- ...result.config,
8247
+ ...base,
8248
+ ...sources ? { sources } : {},
7809
8249
  ...profile,
7810
8250
  apiKey: void 0,
7811
8251
  credentialFile,
@@ -7816,6 +8256,39 @@ ${profile.userId}`).digest("hex");
7816
8256
  trustStore(root);
7817
8257
  return { ...result, profile, credentialFile };
7818
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
+ }
7819
8292
  var shellQuote;
7820
8293
  var init_setup = __esm({
7821
8294
  "src/setup.ts"() {
@@ -7866,6 +8339,7 @@ async function handleMcp(msg, opts) {
7866
8339
  const cwd = opts.cwd ?? process.cwd();
7867
8340
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
7868
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));
7869
8343
  try {
7870
8344
  if (name === "bunshin_recall") {
7871
8345
  const taskAbstract = typeof args.task_abstract === "string" ? args.task_abstract : "";
@@ -7929,6 +8403,7 @@ var init_mcp = __esm({
7929
8403
  "src/v2/mcp.ts"() {
7930
8404
  "use strict";
7931
8405
  init_v22();
8406
+ init_source();
7932
8407
  init_store();
7933
8408
  init_library();
7934
8409
  DEFAULT_PROTOCOL = "2025-06-18";
@@ -8106,9 +8581,9 @@ __export(sync_exports2, {
8106
8581
  resolveConflict: () => resolveConflict,
8107
8582
  runSync: () => runSync
8108
8583
  });
8109
- import { createHash as createHash6 } from "node:crypto";
8110
- import path17 from "node:path";
8111
- 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";
8112
8587
  async function api(config, method, route, body, timeoutMs = 1e4) {
8113
8588
  const ctrl = new AbortController();
8114
8589
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8146,22 +8621,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8146
8621
  }
8147
8622
  }
8148
8623
  async function runSync(store) {
8149
- const lock = path17.join(store.root, "sync.lock");
8624
+ const lock = path19.join(store.root, "sync.lock");
8150
8625
  const deadline = Date.now() + 12e4;
8151
8626
  for (; ; ) {
8152
8627
  try {
8153
- fs22.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8628
+ fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8154
8629
  break;
8155
8630
  } catch (error) {
8156
8631
  if (error.code !== "EEXIST") throw error;
8157
8632
  try {
8158
- const owner = Number(fs22.readFileSync(lock, "utf8"));
8633
+ const owner = Number(fs24.readFileSync(lock, "utf8"));
8159
8634
  if (Number.isInteger(owner) && owner > 0) {
8160
8635
  try {
8161
8636
  process.kill(owner, 0);
8162
8637
  } catch (error2) {
8163
8638
  if (error2.code === "ESRCH") {
8164
- fs22.unlinkSync(lock);
8639
+ fs24.unlinkSync(lock);
8165
8640
  continue;
8166
8641
  }
8167
8642
  }
@@ -8180,7 +8655,7 @@ async function runSync(store) {
8180
8655
  return await syncStore(store);
8181
8656
  } finally {
8182
8657
  try {
8183
- fs22.unlinkSync(lock);
8658
+ fs24.unlinkSync(lock);
8184
8659
  } catch {
8185
8660
  }
8186
8661
  }
@@ -8208,9 +8683,9 @@ async function syncStore(store) {
8208
8683
  result.errors.push(String(e instanceof Error ? e.message : e));
8209
8684
  }
8210
8685
  }
8211
- const acknowledgementsFile = path17.join(store.root, "synced-traces.json");
8686
+ const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
8212
8687
  const acknowledgements = readJson(acknowledgementsFile, {});
8213
- const fingerprint2 = (trace) => createHash6("sha256").update(JSON.stringify(trace)).digest("hex");
8688
+ const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
8214
8689
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8215
8690
  for (const t of traces) {
8216
8691
  try {
@@ -8463,11 +8938,11 @@ __export(demo_exports, {
8463
8938
  runDemo: () => runDemo
8464
8939
  });
8465
8940
  import assert from "node:assert/strict";
8466
- import fs23 from "node:fs";
8941
+ import fs25 from "node:fs";
8467
8942
  import os4 from "node:os";
8468
- import path18 from "node:path";
8943
+ import path20 from "node:path";
8469
8944
  async function runDemo(write) {
8470
- const root = fs23.mkdtempSync(path18.join(os4.tmpdir(), "bunshin-demo-"));
8945
+ const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
8471
8946
  try {
8472
8947
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
8473
8948
  const { lessons, constraint, workflow } = aprScenario();
@@ -8484,7 +8959,7 @@ Task: ${APR_TASK}
8484
8959
  write(result.text);
8485
8960
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
8486
8961
  } finally {
8487
- fs23.rmSync(root, { recursive: true, force: true });
8962
+ fs25.rmSync(root, { recursive: true, force: true });
8488
8963
  }
8489
8964
  }
8490
8965
  var init_demo = __esm({
@@ -8497,21 +8972,21 @@ var init_demo = __esm({
8497
8972
  });
8498
8973
 
8499
8974
  // src/v2/gitremote.ts
8500
- import fs24 from "node:fs";
8501
- import path19 from "node:path";
8502
- 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";
8503
8978
  function saveGitCredential(apiUrl, apiKey) {
8504
8979
  const file = gitCredentialFile();
8505
- fs24.mkdirSync(path19.dirname(file), { recursive: true, mode: 448 });
8980
+ fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
8506
8981
  let all = {};
8507
8982
  try {
8508
- all = JSON.parse(fs24.readFileSync(file, "utf8"));
8983
+ all = JSON.parse(fs26.readFileSync(file, "utf8"));
8509
8984
  } catch {
8510
8985
  }
8511
8986
  all[apiUrl.replace(/\/$/, "")] = apiKey;
8512
8987
  const tmp = `${file}.${process.pid}.tmp`;
8513
- fs24.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8514
- fs24.renameSync(tmp, file);
8988
+ fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8989
+ fs26.renameSync(tmp, file);
8515
8990
  }
8516
8991
  function resolveGitKey(apiUrl) {
8517
8992
  const base = apiUrl.replace(/\/$/, "");
@@ -8519,20 +8994,20 @@ function resolveGitKey(apiUrl) {
8519
8994
  const origin = urlOrigin(base);
8520
8995
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
8521
8996
  try {
8522
- const all = JSON.parse(fs24.readFileSync(gitCredentialFile(), "utf8"));
8997
+ const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
8523
8998
  if (all[base]) return all[base];
8524
8999
  } catch {
8525
9000
  }
8526
- const dir = path19.join(homeDir(), ".config/bunshin/credentials");
9001
+ const dir = path21.join(homeDir(), ".config/bunshin/credentials");
8527
9002
  let files = [];
8528
9003
  try {
8529
- files = fs24.readdirSync(dir).filter((f) => f.endsWith(".json"));
9004
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
8530
9005
  } catch {
8531
9006
  return void 0;
8532
9007
  }
8533
9008
  for (const f of files.sort()) {
8534
9009
  try {
8535
- const c = JSON.parse(fs24.readFileSync(path19.join(dir, f), "utf8"));
9010
+ const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
8536
9011
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
8537
9012
  } catch {
8538
9013
  }
@@ -8540,7 +9015,7 @@ function resolveGitKey(apiUrl) {
8540
9015
  return void 0;
8541
9016
  }
8542
9017
  function refsDigest(refs) {
8543
- return createHash7("sha256").update(
9018
+ return createHash8("sha256").update(
8544
9019
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
8545
9020
  ).digest("hex").slice(0, 12);
8546
9021
  }
@@ -8553,7 +9028,7 @@ var init_gitremote = __esm({
8553
9028
  ZERO_SHA = "0".repeat(40);
8554
9029
  REMOTE_PATH = "/v1/git/repos/";
8555
9030
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
8556
- gitCredentialFile = () => path19.join(homeDir(), ".config/bunshin/git-credentials.json");
9031
+ gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
8557
9032
  GitApiError = class extends Error {
8558
9033
  constructor(status, message, body = null) {
8559
9034
  super(message);
@@ -8645,19 +9120,19 @@ __export(gitrepo_exports, {
8645
9120
  githubReferences: () => githubReferences,
8646
9121
  repoFromUrl: () => repoFromUrl
8647
9122
  });
8648
- import fs25 from "node:fs";
8649
- import path20 from "node:path";
9123
+ import fs27 from "node:fs";
9124
+ import path22 from "node:path";
8650
9125
  import { spawnSync as spawnSync2 } from "node:child_process";
8651
- function git2(cwd, args, opts = {}) {
9126
+ function git3(cwd, args, opts = {}) {
8652
9127
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
8653
9128
  const res = { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
8654
9129
  if (!res.ok && !opts.allowFail) throw new Error(`git ${args.join(" ")} failed: ${(res.err || res.out).trim()}`);
8655
9130
  return res;
8656
9131
  }
8657
9132
  function onPath(name) {
8658
- for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
9133
+ for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
8659
9134
  try {
8660
- fs25.accessSync(path20.join(dir, name), fs25.constants.X_OK);
9135
+ fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
8661
9136
  return true;
8662
9137
  } catch {
8663
9138
  }
@@ -8709,7 +9184,7 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
8709
9184
  return client;
8710
9185
  }
8711
9186
  function toplevel(dir) {
8712
- const r = git2(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
9187
+ const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
8713
9188
  if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
8714
9189
  return r.out.trim();
8715
9190
  }
@@ -8717,7 +9192,7 @@ function githubReferences(root, repo) {
8717
9192
  const [owner, name] = repo.split("/");
8718
9193
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8719
9194
  const pattern = `github\\.com[:/]${esc(owner)}/${esc(name)}|--repo[ =]${esc(owner)}/${esc(name)}|gh repo clone ${esc(owner)}/${esc(name)}`;
8720
- 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 });
8721
9196
  return r.out.split("\n").filter(Boolean).map((l) => l.replace(/^HEAD:/, ""));
8722
9197
  }
8723
9198
  function switchOrigin(root, url, out) {
@@ -8726,41 +9201,41 @@ function switchOrigin(root, url, out) {
8726
9201
  let keptAs = null;
8727
9202
  if (old && old !== url) {
8728
9203
  keptAs = rs.has("github") ? `github-${Date.now()}` : "github";
8729
- git2(root, ["remote", "rename", "origin", keptAs]);
9204
+ git3(root, ["remote", "rename", "origin", keptAs]);
8730
9205
  }
8731
- if (rs.has("bunshin") && rs.get("bunshin") === url) git2(root, ["remote", "rename", "bunshin", "origin"]);
8732
- else if (old !== url) git2(root, ["remote", "add", "origin", url]);
8733
- git2(root, ["fetch", "--quiet", "--tags", "origin"]);
8734
- 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 });
8735
9210
  const originRefs = refMap(root, "refs/remotes/origin");
8736
- for (const b of git2(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
8737
- 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 });
8738
9213
  }
8739
9214
  out(`origin \u2192 ${url}${keptAs ? ` (the old origin is now the remote "${keptAs}")` : ""}
8740
9215
  `);
8741
9216
  }
8742
9217
  async function cmdImport(args, cwd, out, err) {
8743
9218
  requireHelper();
8744
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
8745
- 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");
8746
9221
  const rs = remotes(root);
8747
9222
  const originUrl = rs.get("origin");
8748
- 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)}`;
8749
9224
  const client = await connect(args, cwd);
8750
9225
  const url = remoteUrl(client.apiUrl, name);
8751
9226
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
8752
9227
  if (fromOrigin) {
8753
9228
  out(`Fetching ${originUrl} \u2026
8754
9229
  `);
8755
- const f = git2(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
9230
+ const f = git3(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
8756
9231
  if (!f.ok) err(`! could not fetch origin (${f.err.trim().split("\n")[0]}); importing what this clone already has from it
8757
9232
  `);
8758
9233
  }
8759
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 }));
8760
9235
  const tags = [...refMap(root, "refs/tags").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
8761
9236
  if (!branches.length) throw new Error("nothing to import: no branches found");
8762
- const originHead = git2(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
8763
- 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();
8764
9239
  const defaultBranch = str4(args.flags["default-branch"]) ?? (fromOrigin && originHead ? originHead : current || "main");
8765
9240
  if (!branches.some((b) => b.dst === `refs/heads/${defaultBranch}`)) throw new Error(`default branch ${defaultBranch} is not among the imported branches`);
8766
9241
  if (fromOrigin) {
@@ -8770,7 +9245,7 @@ async function cmdImport(args, cwd, out, err) {
8770
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)
8771
9246
  `);
8772
9247
  else if (remote !== sha2) {
8773
- 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();
8774
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
8775
9250
  `);
8776
9251
  }
@@ -8793,13 +9268,13 @@ async function cmdImport(args, cwd, out, err) {
8793
9268
  out(`${name} exists and is empty; importing into it
8794
9269
  `);
8795
9270
  }
8796
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8797
- 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]);
8798
9273
  const specs = [...branches, ...tags].map((r) => `${r.src}:${r.dst}`);
8799
- const commits = git2(root, ["rev-list", "--count", "--all"]).out.trim();
9274
+ const commits = git3(root, ["rev-list", "--count", "--all"]).out.trim();
8800
9275
  out(`Pushing ${branches.length} branch${branches.length === 1 ? "" : "es"}, ${tags.length} tag${tags.length === 1 ? "" : "s"} (${commits} commits) \u2026
8801
9276
  `);
8802
- const push2 = git2(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
9277
+ const push2 = git3(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
8803
9278
  if (!push2.ok) throw new Error(`push to Bunshin failed: ${push2.err.trim()}`);
8804
9279
  const info = await client.info(name);
8805
9280
  const want = [...branches, ...tags].map((r) => ({ ref: r.dst, sha: r.sha }));
@@ -8821,7 +9296,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
8821
9296
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
8822
9297
  `);
8823
9298
  }
8824
- if (fs25.existsSync(path20.join(root, ".github"))) out(`
9299
+ if (fs27.existsSync(path22.join(root, ".github"))) out(`
8825
9300
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
8826
9301
  `);
8827
9302
  out(`
@@ -8832,7 +9307,7 @@ New machines clone with: bunshin repo clone ${name}
8832
9307
  }
8833
9308
  async function cmdAttach(args, cwd, out, err) {
8834
9309
  requireHelper();
8835
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9310
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8836
9311
  const rs = remotes(root);
8837
9312
  const originUrl = rs.get("origin");
8838
9313
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -8846,9 +9321,9 @@ async function cmdAttach(args, cwd, out, err) {
8846
9321
  }
8847
9322
  const info = await client.info(name);
8848
9323
  if (!info.refs.length) throw new Error(`${name} in Bunshin is empty; import it first (bunshin repo import)`);
8849
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8850
- else git2(root, ["remote", "add", "bunshin", url]);
8851
- 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"]);
8852
9327
  if (originUrl) {
8853
9328
  const behind = [];
8854
9329
  for (const [ref, sha2] of refMap(root, "refs/remotes/origin")) {
@@ -8856,7 +9331,7 @@ async function cmdAttach(args, cwd, out, err) {
8856
9331
  const b = ref.replace("refs/remotes/origin/", "");
8857
9332
  const theirs = info.refs.find((r) => r.ref === `refs/heads/${b}`)?.sha;
8858
9333
  if (!theirs) behind.push(`${b} (missing in Bunshin)`);
8859
- 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)`);
8860
9335
  }
8861
9336
  if (behind.length && !bool3(args.flags.force)) {
8862
9337
  err(`The old origin has work that is not in Bunshin:
@@ -8867,7 +9342,7 @@ Push it to Bunshin first (git push bunshin <branch>), or re-run with --force.
8867
9342
  }
8868
9343
  }
8869
9344
  switchOrigin(root, url, out);
8870
- const status = git2(root, ["status", "-sb"]).out.split("\n")[0];
9345
+ const status = git3(root, ["status", "-sb"]).out.split("\n")[0];
8871
9346
  out(`${status}
8872
9347
  `);
8873
9348
  return 0;
@@ -8880,12 +9355,12 @@ async function cmdClone(args, cwd, out) {
8880
9355
  const dir = args._[2] ?? name.split("/")[1];
8881
9356
  const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
8882
9357
  if (r.status !== 0) return r.status ?? 1;
8883
- out(`Cloned ${name} into ${path20.resolve(cwd, dir)}
9358
+ out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
8884
9359
  `);
8885
9360
  return 0;
8886
9361
  }
8887
9362
  async function cmdVerify(args, cwd, out, err) {
8888
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9363
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8889
9364
  const rs = remotes(root);
8890
9365
  const originUrl = rs.get("origin") ?? "";
8891
9366
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -8906,7 +9381,7 @@ async function cmdVerify(args, cwd, out, err) {
8906
9381
  `);
8907
9382
  }
8908
9383
  }
8909
- const fsck = git2(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
9384
+ const fsck = git3(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
8910
9385
  if (!fsck.ok) {
8911
9386
  problems++;
8912
9387
  err(`! git fsck: ${fsck.err.trim().split("\n")[0]}
@@ -8996,12 +9471,12 @@ var init_gitrepo = __esm({
8996
9471
  bool3 = (v) => v === true || v === "true";
8997
9472
  remotes = (root) => {
8998
9473
  const m = /* @__PURE__ */ new Map();
8999
- 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());
9000
9475
  return m;
9001
9476
  };
9002
9477
  refMap = (root, prefix) => {
9003
9478
  const m = /* @__PURE__ */ new Map();
9004
- 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)) {
9005
9480
  const [sha2, ref] = line.split(" ");
9006
9481
  m.set(ref, sha2);
9007
9482
  }
@@ -9048,22 +9523,22 @@ __export(runner_exports, {
9048
9523
  setupText: () => setupText
9049
9524
  });
9050
9525
  import { spawnSync as spawnSync3 } from "node:child_process";
9051
- import fs26 from "node:fs";
9526
+ import fs28 from "node:fs";
9052
9527
  import os5 from "node:os";
9053
- import path21 from "node:path";
9528
+ import path23 from "node:path";
9054
9529
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9055
- return path21.join(home, ".bunshin", "runner");
9530
+ return path23.join(home, ".bunshin", "runner");
9056
9531
  }
9057
9532
  function readInstalled(home) {
9058
9533
  try {
9059
- 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"));
9060
9535
  } catch {
9061
9536
  return null;
9062
9537
  }
9063
9538
  }
9064
9539
  function connection(args, cwd) {
9065
9540
  const saved = readInstalled();
9066
- 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);
9067
9542
  const config = store?.config();
9068
9543
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9069
9544
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9123,9 +9598,9 @@ async function runOnce(c, args, out, err) {
9123
9598
  const [job] = await lease(c, args);
9124
9599
  if (!job) break;
9125
9600
  const started = Date.now();
9126
- const scratch = fs26.mkdtempSync(path21.join(os5.tmpdir(), "bunshin-job-"));
9601
+ const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
9127
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(
9128
- () => fs26.rmSync(scratch, { recursive: true, force: true })
9603
+ () => fs28.rmSync(scratch, { recursive: true, force: true })
9129
9604
  );
9130
9605
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9131
9606
  if (!res.ok) {
@@ -9215,24 +9690,24 @@ function renderCheck(id, c) {
9215
9690
  }
9216
9691
  function logAnswer(entry) {
9217
9692
  try {
9218
- fs26.mkdirSync(runnerHome(), { recursive: true });
9219
- 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 })}
9220
9695
  `);
9221
9696
  } catch {
9222
9697
  }
9223
9698
  }
9224
9699
  function which(name) {
9225
- for (const d of [...(process.env.PATH || "").split(path21.delimiter), path21.join(os5.homedir(), ".local", "bin")]) {
9226
- 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);
9227
9702
  }
9228
9703
  return null;
9229
9704
  }
9230
9705
  function scheduledCommand(backend) {
9231
- const node = path21.dirname(process.execPath);
9232
- 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";
9233
9708
  const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
9234
- 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"])];
9235
- 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) };
9236
9711
  }
9237
9712
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9238
9713
  const cmd = scheduledCommand(backend);
@@ -9247,8 +9722,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9247
9722
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9248
9723
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9249
9724
  <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
9250
- <key>StandardOutPath</key><string>${xml(path21.join(home, "runner.log"))}</string>
9251
- <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>
9252
9727
  <key>ProcessType</key><string>Background</string>
9253
9728
  <key>LowPriorityIO</key><true/>
9254
9729
  </dict>
@@ -9259,19 +9734,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9259
9734
  const cmd = scheduledCommand(backend);
9260
9735
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
9261
9736
  const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
9262
- 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}`;
9263
9738
  }
9264
9739
  function launchAgentsDir(opts) {
9265
- 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");
9266
9741
  }
9267
9742
  function installSchedule(backend, everyMinutes, opts = {}) {
9268
9743
  const platform = opts.platform ?? process.platform;
9269
9744
  const run2 = opts.run ?? realRun;
9270
9745
  if (platform === "darwin") {
9271
9746
  const dir = launchAgentsDir(opts);
9272
- const plist = path21.join(dir, `${LAUNCHD_LABEL}.plist`);
9273
- fs26.mkdirSync(dir, { recursive: true });
9274
- 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));
9275
9750
  const domain = `gui/${process.getuid?.() ?? 501}`;
9276
9751
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
9277
9752
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -9291,10 +9766,10 @@ function removeSchedule(opts = {}) {
9291
9766
  const platform = opts.platform ?? process.platform;
9292
9767
  const run2 = opts.run ?? realRun;
9293
9768
  if (platform === "darwin") {
9294
- const plist = path21.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9769
+ const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9295
9770
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
9296
- if (!fs26.existsSync(plist)) return "no schedule";
9297
- fs26.rmSync(plist, { force: true });
9771
+ if (!fs28.existsSync(plist)) return "no schedule";
9772
+ fs28.rmSync(plist, { force: true });
9298
9773
  return `removed ${plist}`;
9299
9774
  }
9300
9775
  const current = run2("crontab", ["-l"]);
@@ -9321,10 +9796,10 @@ async function install(args, cwd, out, err, opts = {}) {
9321
9796
  }
9322
9797
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
9323
9798
  const home = runnerHome();
9324
- fs26.mkdirSync(home, { recursive: true, mode: 448 });
9799
+ fs28.mkdirSync(home, { recursive: true, mode: 448 });
9325
9800
  const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
9326
- fs26.writeFileSync(path21.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
9327
- 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);
9328
9803
  let schedule;
9329
9804
  try {
9330
9805
  schedule = installSchedule(backend, every, opts);
@@ -9337,7 +9812,7 @@ Run it yourself: npx -y ${PKG2} runner --once
9337
9812
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
9338
9813
  `);
9339
9814
  out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
9340
- ` : ` 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")}
9341
9816
  `);
9342
9817
  out(` Stop: npx -y ${PKG2} runner uninstall
9343
9818
  `);
@@ -9354,9 +9829,9 @@ Answering what waits now\u2026
9354
9829
  function uninstall(out, opts = {}) {
9355
9830
  out(`${removeSchedule(opts)}
9356
9831
  `);
9357
- const file = path21.join(runnerHome(), "config.json");
9358
- if (fs26.existsSync(file)) {
9359
- fs26.rmSync(file, { force: true });
9832
+ const file = path23.join(runnerHome(), "config.json");
9833
+ if (fs28.existsSync(file)) {
9834
+ fs28.rmSync(file, { force: true });
9360
9835
  out(`removed ${file} (revoke the key in the console: Agent keys)
9361
9836
  `);
9362
9837
  }
@@ -9536,7 +10011,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9536
10011
  out(text2);
9537
10012
  return 0;
9538
10013
  }
9539
- fs26.writeFileSync(file, text2, { mode: 384 });
10014
+ fs28.writeFileSync(file, text2, { mode: 384 });
9540
10015
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
9541
10016
  out(
9542
10017
  [
@@ -9554,7 +10029,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9554
10029
  const id = args._[2];
9555
10030
  const file = args._[3] ?? "-";
9556
10031
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
9557
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10032
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9558
10033
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
9559
10034
  out(renderCheck(id, r.body));
9560
10035
  return r.body.ok ? 0 : 1;
@@ -9563,7 +10038,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9563
10038
  const id = args._[2];
9564
10039
  const file = args._[3] ?? "-";
9565
10040
  if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
9566
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10041
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9567
10042
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
9568
10043
  if (check?.body && (check.status < 400 || check.body.problems)) {
9569
10044
  out(renderCheck(id, check.body));
@@ -9587,7 +10062,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9587
10062
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
9588
10063
  const entries = (() => {
9589
10064
  try {
9590
- 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);
9591
10066
  } catch {
9592
10067
  return [];
9593
10068
  }
@@ -9656,9 +10131,9 @@ var init_runner = __esm({
9656
10131
  });
9657
10132
 
9658
10133
  // src/v2/importer.ts
9659
- import { execFileSync as execFileSync3 } from "node:child_process";
9660
- import fs27 from "node:fs";
9661
- 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";
9662
10137
  function rewriteRepoPaths(text2, repoName2, pack) {
9663
10138
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
9664
10139
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -9697,7 +10172,7 @@ function buildDocs(files, opts) {
9697
10172
  const main2 = `${dir}/SKILL.md`;
9698
10173
  const text2 = rw(files.get(main2).content);
9699
10174
  const fm = frontmatterDescription(text2);
9700
- const name = path22.posix.basename(dir);
10175
+ const name = path24.posix.basename(dir);
9701
10176
  const bundle = {};
9702
10177
  for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
9703
10178
  claimed.add(p);
@@ -9779,45 +10254,45 @@ ${Object.keys(loose).map((p) => `- \`${p}\``).join("\n")}
9779
10254
  }
9780
10255
  return docs.sort((a, b) => a.path.localeCompare(b.path));
9781
10256
  }
9782
- function git3(root, args) {
9783
- 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 });
9784
10259
  }
9785
10260
  function gitBuf(root, args) {
9786
- return execFileSync3("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
10261
+ return execFileSync6("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
9787
10262
  }
9788
10263
  function isGitRepo(root) {
9789
10264
  try {
9790
- return git3(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
10265
+ return git4(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
9791
10266
  } catch {
9792
10267
  return false;
9793
10268
  }
9794
10269
  }
9795
10270
  function workingTreeFiles(root) {
9796
10271
  const out = /* @__PURE__ */ new Map();
9797
- const list2 = isGitRepo(root) ? git3(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
9798
- 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) => {
9799
10274
  if (e.name === ".git" || e.name === "node_modules") return [];
9800
10275
  const rel = base ? `${base}/${e.name}` : e.name;
9801
- 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] : [];
9802
10277
  });
9803
10278
  })(root);
9804
10279
  for (const rel of list2) {
9805
- const abs = path22.join(root, rel);
10280
+ const abs = path24.join(root, rel);
9806
10281
  let buf;
9807
10282
  try {
9808
- buf = fs27.readFileSync(abs);
10283
+ buf = fs29.readFileSync(abs);
9809
10284
  } catch {
9810
10285
  continue;
9811
10286
  }
9812
10287
  if (!isText(buf)) continue;
9813
- const exec = (fs27.statSync(abs).mode & 73) !== 0;
10288
+ const exec = (fs29.statSync(abs).mode & 73) !== 0;
9814
10289
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
9815
10290
  }
9816
10291
  return out;
9817
10292
  }
9818
10293
  function commitsOf(root, since) {
9819
10294
  const range = since ? [`${since}..HEAD`] : ["HEAD"];
9820
- 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) => {
9821
10296
  const [sha2, author, at, subject] = l.split("");
9822
10297
  return { sha: sha2, author, at, subject: subject ?? "" };
9823
10298
  });
@@ -9830,7 +10305,7 @@ function readAt(root, sha2, rel, mode) {
9830
10305
  }
9831
10306
  function treeAt(root, sha2) {
9832
10307
  const out = /* @__PURE__ */ new Map();
9833
- 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)) {
9834
10309
  const m = /^(\d+) blob [0-9a-f]+\t(.+)$/.exec(line);
9835
10310
  if (!m) continue;
9836
10311
  const f = readAt(root, sha2, m[2], m[1]);
@@ -9844,7 +10319,7 @@ function diffDocs(prev, next) {
9844
10319
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
9845
10320
  }
9846
10321
  function readSkillSystem(root, opts) {
9847
- const repoName2 = path22.basename(path22.resolve(root));
10322
+ const repoName2 = path24.basename(path24.resolve(root));
9848
10323
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
9849
10324
  const tree = build(workingTreeFiles(root));
9850
10325
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -9856,7 +10331,7 @@ function readSkillSystem(root, opts) {
9856
10331
  commits.forEach((c, i) => {
9857
10332
  if (!prevSha) files = treeAt(root, c.sha);
9858
10333
  else {
9859
- 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);
9860
10335
  for (let k = 0; k + 1 < changes.length; k += 2) {
9861
10336
  const m = /^:\d+ (\d+) [0-9a-f]+ [0-9a-f]+ ([A-Z])/.exec(changes[k]);
9862
10337
  const rel = changes[k + 1];
@@ -9921,7 +10396,7 @@ var init_importer = __esm({
9921
10396
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9922
10397
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
9923
10398
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
9924
- 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);
9925
10400
  fingerprint = (d) => JSON.stringify(d);
9926
10401
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
9927
10402
 
@@ -9957,8 +10432,8 @@ __export(skills_exports, {
9957
10432
  cmdSkill: () => cmdSkill,
9958
10433
  parsePin: () => parsePin
9959
10434
  });
9960
- import fs28 from "node:fs";
9961
- import path23 from "node:path";
10435
+ import fs30 from "node:fs";
10436
+ import path25 from "node:path";
9962
10437
  function requireOnline(store, what) {
9963
10438
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
9964
10439
  }
@@ -9967,9 +10442,9 @@ async function cmdImport2(store, args, cwd, out, err) {
9967
10442
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
9968
10443
  return 1;
9969
10444
  }
9970
- const root = path23.resolve(cwd, args._[2]);
9971
- if (!fs28.existsSync(root)) throw new Error(`${root} does not exist`);
9972
- 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);
9973
10448
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
9974
10449
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
9975
10450
  const kinds = /* @__PURE__ */ new Map();
@@ -10004,7 +10479,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10004
10479
  const propose = args.flags["propose-rewrites"];
10005
10480
  if (propose) {
10006
10481
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
10007
- 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);
10008
10483
  }
10009
10484
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10010
10485
  return 0;
@@ -10144,7 +10619,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10144
10619
  }
10145
10620
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10146
10621
  if (graph) {
10147
- fs28.writeFileSync(path23.resolve(cwd, graph), renderSkillGraph(report));
10622
+ fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
10148
10623
  out(`graph written to ${graph}
10149
10624
  `);
10150
10625
  }
@@ -10211,12 +10686,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10211
10686
  }
10212
10687
  case "status": {
10213
10688
  const d = deployedState(config);
10214
- const p = deployPaths();
10689
+ const p = deployPaths(process.env, config.orgId);
10215
10690
  const pending = d.docs.filter((x) => x.pending);
10216
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
10217
10692
  `);
10218
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10219
- 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)"}
10220
10695
  files: ${p.library}
10221
10696
  `);
10222
10697
  for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
@@ -10224,16 +10699,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
10224
10699
  return 0;
10225
10700
  }
10226
10701
  case "workspace": {
10227
- const target = args._[2];
10228
- if (bool4(args.flags.clear)) setDeployWorkspace(null);
10229
- else if (target) setDeployWorkspace(target);
10230
- const ws = deployWorkspace();
10231
- const p = deployPaths();
10232
- out(ws ? `workspace: ${ws}
10233
- ` : "workspace: none (skills and instructions are user-wide)\n");
10234
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10235
- instructions: ${p.claudeMd}, ${p.agentsMd}
10236
- `);
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");
10237
10703
  return 0;
10238
10704
  }
10239
10705
  case "pin": {
@@ -10243,9 +10709,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10243
10709
  `;
10244
10710
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10245
10711
  if (file) {
10246
- const abs = path23.resolve(cwd, file);
10247
- fs28.mkdirSync(path23.dirname(abs), { recursive: true });
10248
- fs28.writeFileSync(abs, line);
10712
+ const abs = path25.resolve(cwd, file);
10713
+ fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10714
+ fs30.writeFileSync(abs, line);
10249
10715
  out(`${file}: ${line}`);
10250
10716
  } else out(line);
10251
10717
  return 0;
@@ -10255,12 +10721,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10255
10721
  const to = str6(args.flags.to);
10256
10722
  if (!to) throw new Error("--to <dir> is required");
10257
10723
  const pinFile = str6(args.flags.pin);
10258
- 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] ?? "");
10259
10725
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10260
10726
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10261
10727
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
10262
- const dir = path23.resolve(cwd, to);
10263
- 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)`);
10264
10730
  const pack = str6(args.flags.pack);
10265
10731
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
10266
10732
  const res = materialize(docs, dir);
@@ -10299,27 +10765,27 @@ __export(reflect_exports, {
10299
10765
  buildLesson: () => buildLesson,
10300
10766
  runReflection: () => runReflection
10301
10767
  });
10302
- import fs29 from "node:fs";
10303
- import path24 from "node:path";
10304
- 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";
10305
10771
  async function runReflection(store, opts) {
10306
10772
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
10307
- 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`);
10308
10774
  const deadline = Date.now() + 66e4;
10309
10775
  for (; ; ) {
10310
10776
  try {
10311
- fs29.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10777
+ fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10312
10778
  break;
10313
10779
  } catch (error) {
10314
10780
  if (error.code !== "EEXIST") throw error;
10315
10781
  try {
10316
- const owner = Number(fs29.readFileSync(lock, "utf8"));
10782
+ const owner = Number(fs31.readFileSync(lock, "utf8"));
10317
10783
  if (Number.isInteger(owner) && owner > 0) {
10318
10784
  try {
10319
10785
  process.kill(owner, 0);
10320
10786
  } catch (error2) {
10321
10787
  if (error2.code === "ESRCH") {
10322
- fs29.unlinkSync(lock);
10788
+ fs31.unlinkSync(lock);
10323
10789
  continue;
10324
10790
  }
10325
10791
  }
@@ -10334,7 +10800,7 @@ async function runReflection(store, opts) {
10334
10800
  return await reflectSession(store, opts);
10335
10801
  } finally {
10336
10802
  try {
10337
- fs29.unlinkSync(lock);
10803
+ fs31.unlinkSync(lock);
10338
10804
  } catch {
10339
10805
  }
10340
10806
  }
@@ -10676,8 +11142,8 @@ __export(cli_exports, {
10676
11142
  parseArgs: () => parseArgs,
10677
11143
  version: () => version
10678
11144
  });
10679
- import fs30 from "node:fs";
10680
- import path25 from "node:path";
11145
+ import fs32 from "node:fs";
11146
+ import path27 from "node:path";
10681
11147
  function parseArgs(argv) {
10682
11148
  const out = { _: [], flags: {} };
10683
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"]);
@@ -10777,7 +11243,7 @@ async function main(argv, opts = {}) {
10777
11243
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
10778
11244
  const agent = harness === "claude-code" ? "claude" : harness;
10779
11245
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
10780
- const prior = new Store(path25.join(cwd, ".bunshin"));
11246
+ const prior = new Store(path27.join(cwd, ".bunshin"));
10781
11247
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
10782
11248
  const apiUrl = str7(args.flags["api-url"]);
10783
11249
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -10789,11 +11255,30 @@ async function main(argv, opts = {}) {
10789
11255
  process.stdin.on("end", () => resolve(value.trim()));
10790
11256
  process.stdin.on("error", reject);
10791
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);
10792
11263
  out("Connecting to Bunshin\u2026\n");
10793
- 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
+ }
10794
11277
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
10795
11278
  `);
10796
- 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}
10797
11282
  Key saved privately outside the project.
10798
11283
  `);
10799
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.`);
@@ -10806,6 +11291,7 @@ Key saved privately outside the project.
10806
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");
10807
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");
10808
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");
10809
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)" : ""}.
10810
11296
  `);
10811
11297
  else out("Hooks are disabled. Run setup again without --no-hooks to configure them.\n");
@@ -10821,7 +11307,7 @@ Key saved privately outside the project.
10821
11307
  if (cmd === "mcp") {
10822
11308
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
10823
11309
  const root = str7(args.flags.root);
10824
- 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() });
10825
11311
  return 0;
10826
11312
  }
10827
11313
  if (cmd === "demo") {
@@ -10848,6 +11334,14 @@ Key saved privately outside the project.
10848
11334
  }
10849
11335
  if (skipped.length && cmd === "status") err(`! ${untrustedNote(skipped[0])}
10850
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
+ }
10851
11345
  try {
10852
11346
  switch (cmd) {
10853
11347
  case "status":
@@ -10899,6 +11393,9 @@ Key saved privately outside the project.
10899
11393
  return await (await Promise.resolve().then(() => (init_skills(), skills_exports))).cmdLibrary(store, args, cwd, out, err);
10900
11394
  case "ui":
10901
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);
10902
11399
  default:
10903
11400
  err(`bunshin: unknown command "${cmd}"
10904
11401
 
@@ -10914,7 +11411,7 @@ ${HELP2}`);
10914
11411
  function openStore(args, cwd) {
10915
11412
  const root = str7(args.flags.root);
10916
11413
  if (root) {
10917
- const store = new Store(path25.resolve(root));
11414
+ const store = new Store(path27.resolve(root));
10918
11415
  store.ensureLayout();
10919
11416
  return store;
10920
11417
  }
@@ -10930,7 +11427,7 @@ async function cmdInit(args, cwd, out, err) {
10930
11427
  orgId: str7(args.flags["org-id"]),
10931
11428
  userId: str7(args.flags["user-id"]),
10932
11429
  apiUrl: str7(args.flags["api-url"]),
10933
- 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"]),
10934
11431
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
10935
11432
  harness,
10936
11433
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -10973,10 +11470,10 @@ function cmdStatus(store, args, out, v2) {
10973
11470
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
10974
11471
  const metrics = store.readMetrics(void 0);
10975
11472
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
10976
- 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")] : [];
10977
11474
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
10978
11475
  try {
10979
- return fs30.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11476
+ return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
10980
11477
  } catch {
10981
11478
  return false;
10982
11479
  }
@@ -10994,7 +11491,7 @@ function cmdStatus(store, args, out, v2) {
10994
11491
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
10995
11492
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
10996
11493
  hooks: { [config.harness]: hooks },
10997
- hookFiles: hookFiles.filter((file) => fs30.existsSync(file)),
11494
+ hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
10998
11495
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
10999
11496
  v2
11000
11497
  };
@@ -11010,6 +11507,8 @@ function cmdStatus(store, args, out, v2) {
11010
11507
  out(`user ${config.userId} \u2014 ${config.role} @ ${config.orgUnit} (org ${config.orgId})
11011
11508
  `);
11012
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)"}
11013
11512
  `);
11014
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}` : ""}
11015
11514
  `);
@@ -11035,7 +11534,7 @@ function cmdStatus(store, args, out, v2) {
11035
11534
  }
11036
11535
  function hookCount(file) {
11037
11536
  try {
11038
- const raw = JSON.parse(fs30.readFileSync(file, "utf8"));
11537
+ const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
11039
11538
  let n = 0;
11040
11539
  for (const groups of Object.values(raw.hooks ?? {})) {
11041
11540
  for (const g of groups ?? []) {
@@ -11155,7 +11654,7 @@ function cmdLessonAdd(store, args, out, err) {
11155
11654
  let content = str7(args.flags.content) ?? "";
11156
11655
  if (!content || content === "-" || args._.includes("-")) {
11157
11656
  try {
11158
- content = fs30.readFileSync(0, "utf8");
11657
+ content = fs32.readFileSync(0, "utf8");
11159
11658
  } catch {
11160
11659
  }
11161
11660
  }
@@ -11221,7 +11720,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11221
11720
  }
11222
11721
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11223
11722
  const result = await runReflection2(store, {
11224
- sessionId: str7(args.flags.session) ?? path25.basename(transcript).replace(/\.jsonl?$/, ""),
11723
+ sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
11225
11724
  transcriptPath: transcript,
11226
11725
  cwd,
11227
11726
  force: bool5(args.flags.force)
@@ -11346,6 +11845,10 @@ Usage: bunshin <command> [options]
11346
11845
  setup --harness codex|claude-code|pi connect this project to your hosted workspace
11347
11846
  --api-url <url> --no-hooks paste the key at the hidden prompt; profile is read from Bunshin
11348
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)
11349
11852
  setup --robot --robot-id <id> --arm so101 --gripper <g> --mount <m> --cameras scene,wrist
11350
11853
  --domain <d> --site <s> --cell <c> [--depth] [--api-url <url>] [--harness claude-code]
11351
11854
  register this machine as a robot context (x-bunshin-context)
@@ -11355,6 +11858,8 @@ Usage: bunshin <command> [options]
11355
11858
  --harness claude-code|codex|pi|other --no-hooks --remove-hooks
11356
11859
 
11357
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)
11358
11863
  recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
11359
11864
  pull candidates from the library (offline: local view)
11360
11865
  load <id> [--anchor <heading>] [--json]
@@ -11417,7 +11922,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
11417
11922
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
11418
11923
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
11419
11924
  `;
11420
- 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.`;
11421
11926
  }
11422
11927
  });
11423
11928