@bunshin-cc/bunshin 0.6.5 → 0.8.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 +1390 -761
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1074,6 +1074,95 @@ var init_permissions = __esm({
1074
1074
  }
1075
1075
  });
1076
1076
 
1077
+ // ../core/src/v2/sources.ts
1078
+ function sourceId(kind, name) {
1079
+ return `${kind}:${name.trim()}`;
1080
+ }
1081
+ function parseSource(id) {
1082
+ if (!id) return null;
1083
+ const i = id.indexOf(":");
1084
+ if (i < 0) return null;
1085
+ const kind = id.slice(0, i);
1086
+ const name = id.slice(i + 1);
1087
+ if (!SOURCE_KINDS.includes(kind) || !NAME.test(name) || name.trim() !== name) return null;
1088
+ return { kind, name };
1089
+ }
1090
+ function sourceName(id) {
1091
+ return parseSource(id)?.name ?? id;
1092
+ }
1093
+ var SOURCE_KINDS, NAME;
1094
+ var init_sources = __esm({
1095
+ "../core/src/v2/sources.ts"() {
1096
+ "use strict";
1097
+ SOURCE_KINDS = ["repo", "folder", "robot"];
1098
+ NAME = /^[^\0-\x1f\x7f]{1,280}$/;
1099
+ }
1100
+ });
1101
+
1102
+ // ../core/src/v2/scopes.ts
1103
+ function emptyTree() {
1104
+ return { version: 0, groups: [], sources: {} };
1105
+ }
1106
+ function isScopeLayer(layer) {
1107
+ const k = layerKind(layer);
1108
+ return k === "group" || k === "source";
1109
+ }
1110
+ function groupChain(tree, groupId) {
1111
+ const byId = new Map(tree.groups.map((g) => [g.id, g]));
1112
+ const out = [];
1113
+ const seen = /* @__PURE__ */ new Set();
1114
+ let cur = groupId ? byId.get(groupId) : void 0;
1115
+ while (cur && !seen.has(cur.id)) {
1116
+ seen.add(cur.id);
1117
+ out.unshift(cur.id);
1118
+ cur = cur.parent ? byId.get(cur.parent) : void 0;
1119
+ }
1120
+ return out;
1121
+ }
1122
+ function scopePath(tree, source) {
1123
+ if (!source || !parseSource(source)) return [];
1124
+ const placed = tree?.sources[source]?.group ?? null;
1125
+ return [...(tree ? groupChain(tree, placed) : []).map(groupLayer), sourceLayer(source)];
1126
+ }
1127
+ function inBoundary(section, reader) {
1128
+ return !isScopeLayer(section.layer) || reader.layers.includes(section.layer);
1129
+ }
1130
+ function sourcesIn(tree, layer) {
1131
+ if (layer === "org") return Object.keys(tree.sources);
1132
+ if (layerKind(layer) === "source") return [layerValue(layer)];
1133
+ if (layerKind(layer) !== "group") return [];
1134
+ const gid = layerValue(layer);
1135
+ return Object.keys(tree.sources).filter((sid) => groupChain(tree, tree.sources[sid].group).includes(gid));
1136
+ }
1137
+ function scopeLabel(layer, tree) {
1138
+ const k = layerKind(layer);
1139
+ if (layer === "org") return "everywhere";
1140
+ if (k === "source") {
1141
+ const sid = layerValue(layer);
1142
+ return `${parseSource(sid)?.kind === "robot" ? "robot" : "project"} ${sourceName(sid)}`;
1143
+ }
1144
+ if (k === "group") return `group ${tree?.groups.find((g) => g.id === layerValue(layer))?.name ?? layerValue(layer)}`;
1145
+ if (k === "user") return "personal";
1146
+ return layer;
1147
+ }
1148
+ function scopeChoices(tree, source) {
1149
+ const t = tree ?? emptyTree();
1150
+ return scopePath(t, source).map((layer) => {
1151
+ const n = sourcesIn(t, layer).length;
1152
+ return { layer, label: layerKind(layer) === "source" ? `this ${scopeLabel(layer, t)} only` : `${scopeLabel(layer, t)} (${n} project${n === 1 ? "" : "s"})` };
1153
+ });
1154
+ }
1155
+ var groupLayer, sourceLayer;
1156
+ var init_scopes = __esm({
1157
+ "../core/src/v2/scopes.ts"() {
1158
+ "use strict";
1159
+ init_layers();
1160
+ init_sources();
1161
+ groupLayer = (id) => `group:${id}`;
1162
+ sourceLayer = (id) => `source:${id}`;
1163
+ }
1164
+ });
1165
+
1077
1166
  // ../core/src/v2/layers.ts
1078
1167
  function layerKind(layer) {
1079
1168
  const kind = layer.split(":")[0];
@@ -1091,11 +1180,22 @@ function layersFor(ctx) {
1091
1180
  const dept = department(team);
1092
1181
  return ["org", ...dept ? [`dept:${dept}`] : [], ...team ? [`team:${team}`] : [], `user:${ctx.userId}`];
1093
1182
  }
1094
- function readerFor(ctx, ownerClearance) {
1183
+ function isLocalLayer(layer) {
1184
+ const kind = layerKind(layer);
1185
+ return kind === "user" || kind === "robot";
1186
+ }
1187
+ function withScope(layers, scope) {
1188
+ const path28 = scopePath(scope?.tree, scope?.source);
1189
+ if (!path28.length) return layers;
1190
+ const i = layers.findIndex((l) => isLocalLayer(l));
1191
+ return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
1192
+ }
1193
+ function readerFor(ctx, ownerClearance, scope) {
1194
+ const source = scope?.source ? { source: scope.source } : {};
1095
1195
  if (ctx.kind === "robot") {
1096
1196
  const rank = (c) => c === "private" ? 0 : CLEARANCE_RANK[c];
1097
1197
  const clearance = ownerClearance && rank(ownerClearance) < CLEARANCE_RANK.org ? ownerClearance : "org";
1098
- return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: layersFor(ctx), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {} };
1198
+ return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: withScope(layersFor(ctx), scope), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {}, ...source };
1099
1199
  }
1100
1200
  return {
1101
1201
  userId: ctx.userId,
@@ -1104,7 +1204,8 @@ function readerFor(ctx, ownerClearance) {
1104
1204
  team: ctx.person?.team ?? "",
1105
1205
  clearance: ctx.person?.clearance ?? "team",
1106
1206
  contextId: ctx.id,
1107
- layers: layersFor(ctx)
1207
+ layers: withScope(layersFor(ctx), scope),
1208
+ ...source
1108
1209
  };
1109
1210
  }
1110
1211
  function explainSectionAccess(section, reader) {
@@ -1134,6 +1235,15 @@ function scopeAffinity(section, reader) {
1134
1235
  }
1135
1236
  function baseAffinity(section, reader) {
1136
1237
  const kind = layerKind(section.layer);
1238
+ if (kind === "group" || kind === "source") {
1239
+ const i = reader.layers.indexOf(section.layer);
1240
+ if (i < 0) return 0.05;
1241
+ const path28 = reader.layers.filter((l) => {
1242
+ const k = layerKind(l);
1243
+ return k === "group" || k === "source";
1244
+ });
1245
+ return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
1246
+ }
1137
1247
  if (reader.layers.includes(section.layer)) {
1138
1248
  const order = reader.role === "robot" ? FLEET_LAYER_ORDER : COMPANY_LAYER_ORDER;
1139
1249
  return 0.5 + 0.5 * (order.indexOf(kind) / Math.max(1, order.length - 1));
@@ -1159,6 +1269,7 @@ var init_layers = __esm({
1159
1269
  "use strict";
1160
1270
  init_permissions();
1161
1271
  init_text();
1272
+ init_scopes();
1162
1273
  COMPANY_LAYER_ORDER = ["org", "dept", "team", "user"];
1163
1274
  FLEET_LAYER_ORDER = ["fleet", "arm", "mount", "robot"];
1164
1275
  }
@@ -1344,17 +1455,17 @@ var init_document = __esm({
1344
1455
  });
1345
1456
 
1346
1457
  // ../core/src/v2/sections/markdown.ts
1347
- function repoSectionId(repo, path26, anchor = "") {
1348
- return anchor ? `repo:${repo}/${path26}#${anchor}` : `repo:${repo}/${path26}`;
1458
+ function repoSectionId(repo, path28, anchor = "") {
1459
+ return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
1349
1460
  }
1350
- function docKind(path26) {
1351
- const base = path26.split("/").pop()?.toLowerCase() ?? "";
1461
+ function docKind(path28) {
1462
+ const base = path28.split("/").pop()?.toLowerCase() ?? "";
1352
1463
  if (base === "testing.md") return "verification";
1353
1464
  if (base === "skill.md") return "skill";
1354
1465
  return "spec";
1355
1466
  }
1356
- function defaultPaths(path26) {
1357
- const dir = path26.split("/").slice(0, -1).join("/");
1467
+ function defaultPaths(path28) {
1468
+ const dir = path28.split("/").slice(0, -1).join("/");
1358
1469
  if (!dir || dir.startsWith(".claude/") || dir.startsWith(".codex/")) return [];
1359
1470
  return [`${dir}/**`];
1360
1471
  }
@@ -1499,11 +1610,11 @@ function lintSkills(sections) {
1499
1610
  }
1500
1611
  const state = /* @__PURE__ */ new Map();
1501
1612
  const cycles = /* @__PURE__ */ new Set();
1502
- const dfs = (n, path26) => {
1613
+ const dfs = (n, path28) => {
1503
1614
  state.set(n, 1);
1504
1615
  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]);
1616
+ if (state.get(d) === 1) cycles.add([...path28.slice(path28.indexOf(d)), d].join(" -> "));
1617
+ else if (!state.get(d)) dfs(d, [...path28, d]);
1507
1618
  }
1508
1619
  state.set(n, 2);
1509
1620
  };
@@ -1590,8 +1701,8 @@ function parsePythonSkills(text2) {
1590
1701
  }
1591
1702
  return { skills, constants };
1592
1703
  }
1593
- function harnessSectionId(path26, symbol) {
1594
- return `harness:${path26}#${symbol}`;
1704
+ function harnessSectionId(path28, symbol) {
1705
+ return `harness:${path28}#${symbol}`;
1595
1706
  }
1596
1707
  var DEF_RE;
1597
1708
  var init_python = __esm({
@@ -1717,8 +1828,8 @@ function globToRegExp(glob) {
1717
1828
  globCache.set(glob, out);
1718
1829
  return out;
1719
1830
  }
1720
- function matchesGlob(path26, glob) {
1721
- const p = path26.replace(/^\.\//, "").replace(/\\/g, "/");
1831
+ function matchesGlob(path28, glob) {
1832
+ const p = path28.replace(/^\.\//, "").replace(/\\/g, "/");
1722
1833
  return globToRegExp(glob.replace(/^\.\//, "")).test(p);
1723
1834
  }
1724
1835
  var DEFAULT_EMBEDDERS, globCache;
@@ -1748,8 +1859,8 @@ function servable(sections, reader) {
1748
1859
  }
1749
1860
  return [...out.values()];
1750
1861
  }
1751
- function recallable(sections, reader) {
1752
- return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
1862
+ function recallable(sections, reader, opts = {}) {
1863
+ return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect" && (opts.across || inBoundary(s, reader)));
1753
1864
  }
1754
1865
  function passagesOfSection(s) {
1755
1866
  const hit = passageCache.get(s);
@@ -1797,6 +1908,7 @@ function passageDense(embedder, pool, q) {
1797
1908
  function sourceLabel(s) {
1798
1909
  if (s.storage.type === "repo") return `repo:${s.storage.repo}/${s.storage.path}`;
1799
1910
  if (s.storage.type === "harness") return `harness:${s.storage.path}`;
1911
+ if (isScopeLayer(s.layer)) return s.layer;
1800
1912
  return s.owner.team || s.layer;
1801
1913
  }
1802
1914
  function push(sections, reader, query) {
@@ -1809,10 +1921,10 @@ function push(sections, reader, query) {
1809
1921
  for (const s of recallable(sections, reader).filter((x) => reachesReader(x, reader))) {
1810
1922
  const triggers = [...(s.triggers ?? []).filter((t) => t.anchor), { anchor: "", paths: s.paths, entities: s.entities, skills: s.skills }];
1811
1923
  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}` : "";
1924
+ const path28 = paths.find((p) => t.paths.some((g) => matchesGlob(p, g)));
1925
+ const entity = path28 ? void 0 : t.entities.find((e) => entities.has(e) || text2 && text2.includes(e));
1926
+ const skill = path28 || entity ? void 0 : skills.find((k) => t.skills.includes(k));
1927
+ const trigger = path28 ? `path:${path28}` : entity ? `entity:${entity}` : skill ? `skill:${skill}` : "";
1816
1928
  if (trigger) {
1817
1929
  hits.set(s.id, { section: s, trigger, anchor: t.anchor });
1818
1930
  break;
@@ -1837,7 +1949,7 @@ function maxSim(embedder, q, lists) {
1837
1949
  function pull(sections, reader, query, opts = {}) {
1838
1950
  const embedders = opts.embedders ?? DEFAULT_EMBEDDERS;
1839
1951
  const exclude = new Set(opts.exclude ?? []);
1840
- const pool = recallable(sections, reader).filter((s) => !exclude.has(s.id));
1952
+ const pool = recallable(sections, reader, opts).filter((s) => !exclude.has(s.id));
1841
1953
  if (!pool.length) return [];
1842
1954
  const limit = Math.max(1, Math.min(20, query.limit ?? 10));
1843
1955
  const signals = [];
@@ -1878,6 +1990,7 @@ function pull(sections, reader, query, opts = {}) {
1878
1990
  kind: section.kind,
1879
1991
  layer: section.layer,
1880
1992
  source: sourceLabel(section),
1993
+ ...isScopeLayer(section.layer) && !reader.layers.includes(section.layer) ? { outside: true } : {},
1881
1994
  score: Math.round(score2 * 1e3) / 1e3,
1882
1995
  reasons,
1883
1996
  ...part.get(section.id) ? { part: part.get(section.id) } : {},
@@ -1946,9 +2059,9 @@ function renderSection(s, trigger, opts = {}) {
1946
2059
  `_Load the whole document, or one part, with bunshin_load {"section_id": "${s.id}"${part ? `, "anchor": "${opts.anchor}"` : ""}} (or \`bunshin load ${s.id}${part ? ` --anchor ${opts.anchor}` : ""}\`)._`
1947
2060
  ].join("\n");
1948
2061
  }
1949
- function renderCandidates(c) {
2062
+ function renderCandidates(c, tree) {
1950
2063
  if (!c.length) return "No sections matched. Proceed with general knowledge; nothing here is required.";
1951
- return c.map((x, i) => `${i + 1}. [${x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""})
2064
+ return c.map((x, i) => `${i + 1}. [${isScopeLayer(x.source) ? scopeLabel(x.source, tree) : x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""}${x.outside ? "; borrowed from outside this project's scope" : ""})
1952
2065
  when: ${x.when}`).join("\n");
1953
2066
  }
1954
2067
  var PASSAGE_CHARS, passageCache, words;
@@ -1957,6 +2070,7 @@ var init_engine = __esm({
1957
2070
  "use strict";
1958
2071
  init_text();
1959
2072
  init_layers();
2073
+ init_scopes();
1960
2074
  init_document();
1961
2075
  init_text_index();
1962
2076
  PASSAGE_CHARS = 4e3;
@@ -1968,7 +2082,7 @@ var init_engine = __esm({
1968
2082
  // ../core/src/v2/guidelines.ts
1969
2083
  function reflectionGuidelines(sections, reader, opts = {}) {
1970
2084
  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 }));
2085
+ return pool.filter((s) => s.delivery === "reflect" && s.status === "active" && (!reader || inBoundary(s, reader))).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
2086
  }
1973
2087
  function guidelineRules(body) {
1974
2088
  const at = body.search(/^## /m);
@@ -2004,8 +2118,9 @@ var init_guidelines = __esm({
2004
2118
  init_types();
2005
2119
  init_layers();
2006
2120
  init_engine();
2121
+ init_scopes();
2007
2122
  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;
2123
+ generality = (l) => ({ org: 0, dept: 1, fleet: 1, group: 2, team: 2, arm: 3, mount: 3, source: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
2009
2124
  }
2010
2125
  });
2011
2126
 
@@ -2060,17 +2175,6 @@ function identLike(rest) {
2060
2175
  return humps.length > 0 && m[1].length / humps.length >= 3;
2061
2176
  });
2062
2177
  }
2063
- function luhnOk(digits) {
2064
- let total = 0;
2065
- let alt = false;
2066
- for (let i = digits.length - 1; i >= 0; i--) {
2067
- let d = digits.charCodeAt(i) - 48;
2068
- if (alt && (d *= 2) > 9) d -= 9;
2069
- total += d;
2070
- alt = !alt;
2071
- }
2072
- return total % 10 === 0;
2073
- }
2074
2178
  function issuerOk(d) {
2075
2179
  const n = d.length;
2076
2180
  if (d[0] === "4") return n === 13 || n === 16 || n === 19;
@@ -2082,23 +2186,44 @@ function issuerOk(d) {
2082
2186
  if (/^(?:50|5[6-8]|6\d)/.test(d)) return n >= 12 && n <= 19;
2083
2187
  return false;
2084
2188
  }
2189
+ function luhnDigits(d, n) {
2190
+ let total = 0;
2191
+ let alt = false;
2192
+ for (let i = n - 1; i >= 0; i--) {
2193
+ let x = d[i];
2194
+ if (alt && (x *= 2) > 9) x -= 9;
2195
+ total += x;
2196
+ alt = !alt;
2197
+ }
2198
+ return total % 10 === 0;
2199
+ }
2085
2200
  function cardPrefix(s) {
2086
- const digits = s.replace(/\D/g, "");
2087
- if (/^3[1-3]|^2[019]/.test(digits)) return 0;
2088
- const sep = s.search(/[ -]/);
2089
- if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
2201
+ const d = CARD_DIGITS;
2090
2202
  const ends = [];
2091
- let n = 0;
2203
+ const counts = [];
2204
+ let digits = 0;
2205
+ let sep = -1;
2092
2206
  for (let i = 0, start = 0; i <= s.length; i++) {
2093
- if (i < s.length && s.charCodeAt(i) >= 48 && s.charCodeAt(i) <= 57) continue;
2207
+ const c = i < s.length ? s.charCodeAt(i) : -1;
2208
+ if (c >= 48 && c <= 57) {
2209
+ if (digits < d.length) d[digits] = c - 48;
2210
+ digits++;
2211
+ continue;
2212
+ }
2213
+ if (sep < 0 && c !== -1) sep = i;
2094
2214
  if (ends.length && i - start < 3) break;
2095
- n += i - start;
2096
- ends.push([i, n]);
2215
+ ends.push(i);
2216
+ counts.push(digits);
2097
2217
  start = i + 1;
2098
2218
  }
2219
+ if (d[0] === 3 && d[1] >= 1 && d[1] <= 3 || d[0] === 2 && (d[1] === 0 || d[1] === 1 || d[1] === 9)) return 0;
2220
+ if (sep > 0 && sep !== 4 && !(sep >= 13 && sep <= 19)) return 0;
2099
2221
  for (let k = ends.length - 1; k >= 0; k--) {
2100
- const [end, count] = ends[k];
2101
- if (count >= 13 && count <= 19 && luhnOk(digits.slice(0, count)) && issuerOk(digits.slice(0, count))) return end;
2222
+ const count = counts[k];
2223
+ if (count < 13 || count > 19 || !luhnDigits(d, count)) continue;
2224
+ let str8 = "";
2225
+ for (let j = 0; j < count; j++) str8 += String.fromCharCode(48 + d[j]);
2226
+ if (issuerOk(str8)) return ends[k];
2102
2227
  }
2103
2228
  return 0;
2104
2229
  }
@@ -2181,7 +2306,7 @@ function redactDeep(value, opts = {}) {
2181
2306
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
2182
2307
  return value;
2183
2308
  }
2184
- var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
2309
+ 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, NAME2, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
2185
2310
  var init_redact = __esm({
2186
2311
  "../core/src/redact.ts"() {
2187
2312
  "use strict";
@@ -2257,9 +2382,10 @@ var init_redact = __esm({
2257
2382
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2258
2383
  PATH_RE = /^(?:~|\.{1,2})?\/[\w.@%+~/-]*$|^[A-Za-z]:\\/;
2259
2384
  credentialish = (v) => /[\d\W_]/.test(v) || v.length >= 16 && /[a-z]/.test(v) && /[A-Z]/.test(v);
2385
+ CARD_DIGITS = new Uint8Array(64);
2260
2386
  ASSET_EXT = /^\.(?:m?js|cjs|jsx|tsx?|css|scss|map|json|html?|wasm|woff2?|ttf|otf|eot|png|jpe?g|gif|svg|webp|avif|ico|bmp|mp[34]|webm|wav|ogg|pdf|txt|md|zip|gz|br|tgz|tar|whl|jar|bin|dat|lock|log|ya?ml|xml|csv)\b/;
2261
2387
  NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
2262
- 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}`;
2388
+ NAME2 = String.raw`(?=[A-Za-z_$])(?=[\w.$-]{0,64}?(?:key|token|secret|pass|pwd|credential|auth|cookie|session|creds))[A-Za-z_$][\w.$-]{0,127}`;
2263
2389
  Q = String.raw`(?:\\\\){0,8}\\?["'\x60]`;
2264
2390
  VALUE = String.raw`(?:(${Q})(?!\s)(.{0,4096}?)\4|(${Q})?([^\s"'\x60\\,]{1,4096}))`;
2265
2391
  ARGS = String.raw`[^\n|;&]{0,256}?`;
@@ -2345,7 +2471,7 @@ var init_redact = __esm({
2345
2471
  },
2346
2472
  {
2347
2473
  kind: "assigned-secret",
2348
- re: new RegExp(String.raw`(?:(?<![\w.$-])|(?=["'\x60\\]))((?:\\\\){0,8}\\?["'\x60]?)(${NAME})\1(\??\s{0,64}(?::=|=>|==?|:(?=[\s"'\x60\\]|$))[ \t]{0,64})${VALUE}`, "gim"),
2474
+ re: new RegExp(String.raw`(?:(?<![\w.$-])|(?=["'\x60\\]))((?:\\\\){0,8}\\?["'\x60]?)(${NAME2})\1(\??\s{0,64}(?::=|=>|==?|:(?=[\s"'\x60\\]|$))[ \t]{0,64})${VALUE}`, "gim"),
2349
2475
  retryFrom: (m) => m[1].length * 2 + m[2].length + m[3].length,
2350
2476
  replace: (m, R, at, all) => {
2351
2477
  const [whole, , name, , q1, inQuotes, q2, bare] = m;
@@ -2385,7 +2511,7 @@ var init_redact = __esm({
2385
2511
  // YAML block scalar: `TOKEN: |` and the indented lines under it.
2386
2512
  {
2387
2513
  kind: "assigned-secret",
2388
- re: new RegExp(String.raw`^([ \t]{0,64})((?:-[ \t]{1,16})?(["']?)(${NAME})\3[ \t]{0,16}:[ \t]{0,16}[|>][+-]?\d?[ \t]{0,16})((?:\n(?:[ \t]{0,64}\n){0,8}\1[ \t]{1,64}\S[^\n]{0,4096}){1,500})`, "gim"),
2514
+ re: new RegExp(String.raw`^([ \t]{0,64})((?:-[ \t]{1,16})?(["']?)(${NAME2})\3[ \t]{0,16}:[ \t]{0,16}[|>][+-]?\d?[ \t]{0,16})((?:\n(?:[ \t]{0,64}\n){0,8}\1[ \t]{1,64}\S[^\n]{0,4096}){1,500})`, "gim"),
2389
2515
  replace: (m, R) => {
2390
2516
  const [, indent, head, , name, body] = m;
2391
2517
  if (!secretName(name)) return null;
@@ -2610,7 +2736,7 @@ ${input.digestNote ? `${input.digestNote}
2610
2736
 
2611
2737
  ` : ""}${input.transcriptDigest}
2612
2738
  ` : ""}
2613
- ${input.focus ? `## What to capture
2739
+ ${input.feedback ? feedbackSection(input.feedback) : ""}${input.focus ? `## What to capture
2614
2740
 
2615
2741
  ${input.focus}
2616
2742
  ` : ""}${guidelinesSection(input.guidelines, { forReflection: true })}
@@ -2657,7 +2783,15 @@ ${input.pendingNew.map((p) => `- ${p}`).join("\n")}
2657
2783
  Already captured this session (do not repeat): ${input.captured.length ? input.captured.join(", ") : "none"}
2658
2784
 
2659
2785
  Layers you may target: ${input.layers.join(", ")}.
2660
- ${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
2786
+ ${input.scopes?.length ? `
2787
+ Where a lesson applies (put each at the narrowest scope it holds for; when unsure, the narrowest):
2788
+ ${[...input.scopes].reverse().map((x) => `- ${x.layer}: ${x.label}`).join("\n")}
2789
+ - org: everywhere in this workspace
2790
+ A fact, command or habit of this project \u2192 this project. True of every project in a group alike \u2192
2791
+ that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
2792
+ are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
2793
+ "layer" in the target of a new document (an edit keeps the document's own scope).
2794
+ ` : ""}${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
2661
2795
  not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
2662
2796
  A lesson for anyone doing the task belongs in the task's document instead.
2663
2797
  ` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
@@ -2690,6 +2824,34 @@ At most ${max} suggestions. Schema:
2690
2824
  "rationale": string, "urgency": "normal"|"urgent",
2691
2825
  "signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
2692
2826
  }
2827
+ function feedbackSection(f) {
2828
+ return `
2829
+ ## Feedback from the person (human steering)
2830
+
2831
+ ${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:
2832
+
2833
+ <feedback>
2834
+ ${f.text.trim()}
2835
+ </feedback>
2836
+ ${f.lesson ? `
2837
+ The lesson it is about:
2838
+
2839
+ <lesson>
2840
+ ${f.lesson.trim()}
2841
+ </lesson>
2842
+ ` : ""}
2843
+ Treat it as a correction from a person (signal "steering"): the strongest evidence there is. Act on it:
2844
+ - If it says the session taught something that was missed, capture that lesson in the right
2845
+ 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."}
2846
+ - If it corrects a lesson (too specific, wrong, in the wrong place), propose the corrected lesson
2847
+ (op "replace" if it already landed in a document, otherwise a new one) and name the lesson it
2848
+ corrects in the rationale. Lessons listed as already taken may be corrected this way.
2849
+ - If it says how lessons should be captured or written in general, propose a rule for the
2850
+ reflection guidelines, merged with the rules already there: never repeat one; refine an existing
2851
+ rule with op "replace" when the feedback sharpens it.
2852
+ Answer "nothing_new" only if everything it asks for is already in the library or proposed.
2853
+ `;
2854
+ }
2693
2855
  function documentsSection(docs) {
2694
2856
  if (!docs?.length) return "";
2695
2857
  const body = docs.map((d) => `### ${d.id}: ${d.title} (${d.why})
@@ -2913,6 +3075,8 @@ var init_reconcile = __esm({
2913
3075
  init_document();
2914
3076
  init_markdown();
2915
3077
  init_text_index();
3078
+ init_scopes();
3079
+ init_sources();
2916
3080
  init_types();
2917
3081
  }
2918
3082
  });
@@ -2954,6 +3118,17 @@ var init_profile = __esm({
2954
3118
  }
2955
3119
  });
2956
3120
 
3121
+ // ../core/src/v2/generalize.ts
3122
+ var init_generalize = __esm({
3123
+ "../core/src/v2/generalize.ts"() {
3124
+ "use strict";
3125
+ init_layers();
3126
+ init_guidelines();
3127
+ init_reflect();
3128
+ init_scopes();
3129
+ }
3130
+ });
3131
+
2957
3132
  // ../core/src/v2/index.ts
2958
3133
  var init_v2 = __esm({
2959
3134
  "../core/src/v2/index.ts"() {
@@ -2975,6 +3150,9 @@ var init_v2 = __esm({
2975
3150
  init_research();
2976
3151
  init_profile();
2977
3152
  init_guidelines();
3153
+ init_sources();
3154
+ init_scopes();
3155
+ init_generalize();
2978
3156
  }
2979
3157
  });
2980
3158
 
@@ -3073,6 +3251,284 @@ var init_api = __esm({
3073
3251
  }
3074
3252
  });
3075
3253
 
3254
+ // src/v2/repo.ts
3255
+ import fs6 from "node:fs";
3256
+ import path5 from "node:path";
3257
+ import { execFileSync } from "node:child_process";
3258
+ function git(cwd, args, timeout = 1500) {
3259
+ try {
3260
+ return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
3261
+ } catch {
3262
+ return null;
3263
+ }
3264
+ }
3265
+ function repoRoot(cwd, storeRoot) {
3266
+ const key = `${cwd}
3267
+ ${storeRoot ?? ""}`;
3268
+ const hit = rootCache.get(key);
3269
+ if (hit) return hit;
3270
+ const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
3271
+ let root = top || "";
3272
+ if (!root && storeRoot && path5.basename(storeRoot) === ".bunshin") {
3273
+ const parent = path5.dirname(storeRoot);
3274
+ if (path5.resolve(cwd).startsWith(path5.resolve(parent))) root = parent;
3275
+ }
3276
+ root = root || path5.resolve(cwd);
3277
+ rootCache.set(key, root);
3278
+ return root;
3279
+ }
3280
+ function parseRemote(url) {
3281
+ const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
3282
+ if (!clean2) return null;
3283
+ const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
3284
+ const pathPart = scp ? scp[1] : (() => {
3285
+ try {
3286
+ return new URL(clean2).pathname;
3287
+ } catch {
3288
+ return clean2;
3289
+ }
3290
+ })();
3291
+ const segs = pathPart.split("/").filter(Boolean);
3292
+ if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
3293
+ return segs[0] ?? null;
3294
+ }
3295
+ function repoName(root) {
3296
+ const url = git(root, ["remote", "get-url", "origin"]);
3297
+ return url && parseRemote(url) || path5.basename(root);
3298
+ }
3299
+ function repoCommit(root) {
3300
+ return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
3301
+ }
3302
+ function listDocs(root) {
3303
+ const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
3304
+ if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs6.existsSync(path5.join(root, rel)) && !deployedSkill(root, rel)).sort();
3305
+ const out = [];
3306
+ let visited = 0;
3307
+ const walk = (dir, rel, depth) => {
3308
+ if (depth > 8 || visited++ > 2e4) return;
3309
+ let entries;
3310
+ try {
3311
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
3312
+ } catch {
3313
+ return;
3314
+ }
3315
+ for (const e of entries) {
3316
+ if (e.isDirectory()) {
3317
+ if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir, e.name, ".git"))) walk(path5.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
3318
+ } else if (e.isFile()) {
3319
+ const r = rel ? `${rel}/${e.name}` : e.name;
3320
+ if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
3321
+ }
3322
+ }
3323
+ };
3324
+ walk(root, "", 0);
3325
+ return out.sort();
3326
+ }
3327
+ function indexWorkingTree(v2, config, cwd, opts = {}) {
3328
+ const root = repoRoot(cwd, v2.store.root);
3329
+ const now = opts.now ?? Date.now();
3330
+ const cached = v2.readRepoIndex(root);
3331
+ const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
3332
+ const sameRepo = cached && cached.ownerTeam === ownerTeam;
3333
+ const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
3334
+ const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
3335
+ const paths = fresh ? cached.paths : listDocs(root);
3336
+ let changed = !fresh || !sameRepo || cached.repo !== repo;
3337
+ const files = {};
3338
+ for (const rel of paths) {
3339
+ const abs = path5.join(root, rel);
3340
+ let stat;
3341
+ try {
3342
+ stat = fs6.statSync(abs);
3343
+ } catch {
3344
+ changed = true;
3345
+ continue;
3346
+ }
3347
+ const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
3348
+ if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
3349
+ files[rel] = prev;
3350
+ continue;
3351
+ }
3352
+ changed = true;
3353
+ try {
3354
+ const text2 = fs6.readFileSync(abs, "utf8");
3355
+ files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
3356
+ } catch {
3357
+ }
3358
+ }
3359
+ const index = {
3360
+ repo,
3361
+ ownerTeam,
3362
+ listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
3363
+ paths,
3364
+ files
3365
+ };
3366
+ if (changed) {
3367
+ try {
3368
+ v2.writeRepoIndex(root, index);
3369
+ } catch {
3370
+ }
3371
+ }
3372
+ return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
3373
+ }
3374
+ function relativeToRoot(root, file, cwd = root) {
3375
+ if (!file) return null;
3376
+ const abs = path5.isAbsolute(file) ? file : path5.resolve(cwd, file);
3377
+ let real = abs;
3378
+ let realRoot = root;
3379
+ try {
3380
+ realRoot = fs6.realpathSync(root);
3381
+ } catch {
3382
+ }
3383
+ try {
3384
+ real = fs6.realpathSync(abs);
3385
+ } catch {
3386
+ try {
3387
+ real = path5.join(fs6.realpathSync(path5.dirname(abs)), path5.basename(abs));
3388
+ } catch {
3389
+ }
3390
+ }
3391
+ for (const [r, f] of [
3392
+ [realRoot, real],
3393
+ [root, abs]
3394
+ ]) {
3395
+ const rel = path5.relative(r, f);
3396
+ if (rel && !rel.startsWith("..") && !path5.isAbsolute(rel)) return rel.split(path5.sep).join("/");
3397
+ }
3398
+ return null;
3399
+ }
3400
+ var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
3401
+ var init_repo = __esm({
3402
+ "src/v2/repo.ts"() {
3403
+ "use strict";
3404
+ init_v22();
3405
+ DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
3406
+ SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
3407
+ LIST_TTL_MS = 6e4;
3408
+ rootCache = /* @__PURE__ */ new Map();
3409
+ isDoc = (rel) => {
3410
+ const parts = rel.split("/");
3411
+ if (parts.some((p) => SKIP_DIRS.has(p))) return false;
3412
+ return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
3413
+ };
3414
+ deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs6.existsSync(path5.join(root, path5.posix.dirname(rel), ".bunshin.json"));
3415
+ }
3416
+ });
3417
+
3418
+ // src/v2/source.ts
3419
+ var source_exports = {};
3420
+ __export(source_exports, {
3421
+ cwdInPool: () => cwdInPool,
3422
+ inPool: () => inPool,
3423
+ listFolders: () => listFolders,
3424
+ projectRootFor: () => projectRootFor,
3425
+ select: () => select,
3426
+ sourceFor: () => sourceFor,
3427
+ storeHome: () => storeHome,
3428
+ topFolder: () => topFolder
3429
+ });
3430
+ import fs7 from "node:fs";
3431
+ import path6 from "node:path";
3432
+ import { execFileSync as execFileSync2 } from "node:child_process";
3433
+ function storeHome(storeRoot) {
3434
+ return path6.dirname(path6.resolve(storeRoot));
3435
+ }
3436
+ function topFolder(cwd, storeRoot) {
3437
+ const rel = path6.relative(storeHome(storeRoot), path6.resolve(cwd));
3438
+ if (!rel || rel.startsWith("..") || path6.isAbsolute(rel)) return "";
3439
+ return rel.split(path6.sep)[0] ?? "";
3440
+ }
3441
+ function inPool(selection, folder) {
3442
+ if (!folder || !selection) return true;
3443
+ if (selection.exclude?.includes(folder)) return false;
3444
+ return selection.only ? selection.only.includes(folder) : true;
3445
+ }
3446
+ function cwdInPool(config, cwd, storeRoot) {
3447
+ return inPool(config.sources, topFolder(cwd, storeRoot));
3448
+ }
3449
+ function git2(cwd, args) {
3450
+ try {
3451
+ return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
3452
+ } catch {
3453
+ return null;
3454
+ }
3455
+ }
3456
+ function sourceFor(cwd, storeRoot, ctx) {
3457
+ if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
3458
+ const key = `${path6.resolve(cwd)}
3459
+ ${storeRoot}`;
3460
+ if (sourceCache.has(key)) return sourceCache.get(key);
3461
+ const found = findSource(cwd, storeRoot);
3462
+ sourceCache.set(key, found);
3463
+ return found;
3464
+ }
3465
+ function findSource(cwd, storeRoot) {
3466
+ const home = storeHome(storeRoot);
3467
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
3468
+ if (top) {
3469
+ const remote = git2(top, ["remote", "get-url", "origin"]);
3470
+ const named = remote ? parseRemote(remote) : null;
3471
+ if (named) return sourceId("repo", named);
3472
+ const rel = path6.relative(home, top);
3473
+ return sourceId("folder", rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel.split(path6.sep).join("/") : path6.basename(top));
3474
+ }
3475
+ const folder = topFolder(cwd, storeRoot);
3476
+ return folder ? sourceId("folder", folder) : void 0;
3477
+ }
3478
+ function projectRootFor(cwd, storeRoot) {
3479
+ const home = storeHome(storeRoot);
3480
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
3481
+ if (top) {
3482
+ const rel = path6.relative(home, top);
3483
+ if (!rel.startsWith("..") && !path6.isAbsolute(rel)) return top;
3484
+ }
3485
+ const folder = topFolder(cwd, storeRoot);
3486
+ return folder ? path6.join(home, folder) : home;
3487
+ }
3488
+ function listFolders(storeRoot, selection, opts = {}) {
3489
+ const home = storeHome(storeRoot);
3490
+ let names = [];
3491
+ try {
3492
+ names = fs7.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
3493
+ } catch {
3494
+ }
3495
+ return names.map((folder) => ({
3496
+ folder,
3497
+ included: inPool(selection, folder),
3498
+ ...opts.sources ? { source: sourceFor(path6.join(home, folder), storeRoot) } : {}
3499
+ }));
3500
+ }
3501
+ function select(current, action, folders) {
3502
+ const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
3503
+ const only = current?.only;
3504
+ const exclude = current?.exclude ?? [];
3505
+ let next;
3506
+ switch (action) {
3507
+ case "all":
3508
+ return void 0;
3509
+ case "only":
3510
+ next = { only: uniq(folders) };
3511
+ break;
3512
+ case "include":
3513
+ next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
3514
+ break;
3515
+ case "exclude":
3516
+ next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
3517
+ break;
3518
+ }
3519
+ if (!next.exclude?.length) delete next.exclude;
3520
+ return next.only || next.exclude ? next : void 0;
3521
+ }
3522
+ var sourceCache;
3523
+ var init_source = __esm({
3524
+ "src/v2/source.ts"() {
3525
+ "use strict";
3526
+ init_v22();
3527
+ init_repo();
3528
+ sourceCache = /* @__PURE__ */ new Map();
3529
+ }
3530
+ });
3531
+
3076
3532
  // ../core/src/types.ts
3077
3533
  var init_types2 = __esm({
3078
3534
  "../core/src/types.ts"() {
@@ -3593,9 +4049,9 @@ var init_src = __esm({
3593
4049
  });
3594
4050
 
3595
4051
  // src/v2/store.ts
3596
- import fs6 from "node:fs";
3597
- import path5 from "node:path";
3598
- import { randomBytes as randomBytes3 } from "node:crypto";
4052
+ import fs8 from "node:fs";
4053
+ import path7 from "node:path";
4054
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
3599
4055
  function hexId(prefix) {
3600
4056
  return `${prefix}_${randomBytes3(6).toString("hex")}`;
3601
4057
  }
@@ -3617,7 +4073,7 @@ function normalizeSession(id, raw) {
3617
4073
  hinted: list2(raw.hinted)
3618
4074
  };
3619
4075
  }
3620
- var SAFE_ID, fileSafe, V2Store, addUnique;
4076
+ var SAFE_ID, fileSafe, V2Store, rootKey, addUnique;
3621
4077
  var init_store2 = __esm({
3622
4078
  "src/v2/store.ts"() {
3623
4079
  "use strict";
@@ -3628,12 +4084,12 @@ var init_store2 = __esm({
3628
4084
  V2Store = class {
3629
4085
  constructor(store) {
3630
4086
  this.store = store;
3631
- this.dir = path5.join(store.root, "v2");
4087
+ this.dir = path7.join(store.root, "v2");
3632
4088
  }
3633
4089
  store;
3634
4090
  dir;
3635
4091
  p(...parts) {
3636
- return path5.join(this.dir, ...parts);
4092
+ return path7.join(this.dir, ...parts);
3637
4093
  }
3638
4094
  // ── context / bundle / params ──
3639
4095
  readContext() {
@@ -3714,9 +4170,9 @@ var init_store2 = __esm({
3714
4170
  const dir = this.p("sessions");
3715
4171
  if (!fileExists(dir)) return [];
3716
4172
  const out = [];
3717
- for (const f of fs6.readdirSync(dir)) {
4173
+ for (const f of fs8.readdirSync(dir)) {
3718
4174
  if (!f.endsWith(".json") || f === "current.json") continue;
3719
- const raw = readJson(path5.join(dir, f), null);
4175
+ const raw = readJson(path7.join(dir, f), null);
3720
4176
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
3721
4177
  }
3722
4178
  return out;
@@ -3744,7 +4200,7 @@ var init_store2 = __esm({
3744
4200
  let at = raw.at;
3745
4201
  if (!at) {
3746
4202
  try {
3747
- at = fs6.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
4203
+ at = fs8.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
3748
4204
  } catch {
3749
4205
  at = (/* @__PURE__ */ new Date()).toISOString();
3750
4206
  }
@@ -3752,11 +4208,12 @@ var init_store2 = __esm({
3752
4208
  return { status: raw.status, source: raw.source || "robot", ...raw.metrics ? { metrics: raw.metrics } : {}, at };
3753
4209
  }
3754
4210
  // ── repo index / sync state ──
3755
- readRepoIndex() {
3756
- return readJson(this.p("repo-index.json"), null);
4211
+ /** One index per working tree: a store covering a folder of repositories keeps each one's. */
4212
+ readRepoIndex(root) {
4213
+ return readJson(this.p("repo-index", `${rootKey(root)}.json`), null);
3757
4214
  }
3758
- writeRepoIndex(index) {
3759
- writeJsonAtomic(this.p("repo-index.json"), index);
4215
+ writeRepoIndex(root, index) {
4216
+ writeJsonAtomic(this.p("repo-index", `${rootKey(root)}.json`), index);
3760
4217
  }
3761
4218
  readSyncState() {
3762
4219
  return readJson(this.p("sync.json"), {});
@@ -3770,13 +4227,13 @@ var init_store2 = __esm({
3770
4227
  /** Exclusive lock file; returns a release function, or null when another process holds it. */
3771
4228
  tryLock(name, staleMs = 10 * 6e4) {
3772
4229
  const file = this.p("locks", `${fileSafe(name)}.lock`);
3773
- ensureDir(path5.dirname(file));
4230
+ ensureDir(path7.dirname(file));
3774
4231
  for (let attempt = 0; attempt < 2; attempt++) {
3775
4232
  try {
3776
- fs6.writeFileSync(file, String(process.pid), { flag: "wx" });
4233
+ fs8.writeFileSync(file, String(process.pid), { flag: "wx" });
3777
4234
  return () => {
3778
4235
  try {
3779
- fs6.unlinkSync(file);
4236
+ fs8.unlinkSync(file);
3780
4237
  } catch {
3781
4238
  }
3782
4239
  };
@@ -3784,8 +4241,8 @@ var init_store2 = __esm({
3784
4241
  if (err.code !== "EEXIST") return null;
3785
4242
  let stale = false;
3786
4243
  try {
3787
- const owner = Number(fs6.readFileSync(file, "utf8"));
3788
- const age = Date.now() - fs6.statSync(file).mtimeMs;
4244
+ const owner = Number(fs8.readFileSync(file, "utf8"));
4245
+ const age = Date.now() - fs8.statSync(file).mtimeMs;
3789
4246
  if (age > staleMs) stale = true;
3790
4247
  else if (Number.isInteger(owner) && owner > 0) {
3791
4248
  try {
@@ -3799,7 +4256,7 @@ var init_store2 = __esm({
3799
4256
  }
3800
4257
  if (!stale) return null;
3801
4258
  try {
3802
- fs6.unlinkSync(file);
4259
+ fs8.unlinkSync(file);
3803
4260
  } catch {
3804
4261
  }
3805
4262
  }
@@ -3816,14 +4273,15 @@ var init_store2 = __esm({
3816
4273
  const dir = this.p(name);
3817
4274
  if (!fileExists(dir)) return [];
3818
4275
  const out = [];
3819
- for (const f of fs6.readdirSync(dir)) {
4276
+ for (const f of fs8.readdirSync(dir)) {
3820
4277
  if (!f.endsWith(".json")) continue;
3821
- const v = readJson(path5.join(dir, f), null);
4278
+ const v = readJson(path7.join(dir, f), null);
3822
4279
  if (v) out.push(v);
3823
4280
  }
3824
4281
  return out;
3825
4282
  }
3826
4283
  };
4284
+ rootKey = (root) => createHash2("sha256").update(path7.resolve(root)).digest("hex").slice(0, 16);
3827
4285
  addUnique = (list2, ...ids) => {
3828
4286
  for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
3829
4287
  };
@@ -3831,63 +4289,49 @@ var init_store2 = __esm({
3831
4289
  });
3832
4290
 
3833
4291
  // src/v2/deploy.ts
3834
- import { createHash as createHash2 } from "node:crypto";
3835
- import fs7 from "node:fs";
4292
+ import { execFileSync as execFileSync3 } from "node:child_process";
4293
+ import { createHash as createHash3 } from "node:crypto";
4294
+ import fs9 from "node:fs";
3836
4295
  import os2 from "node:os";
3837
- import path6 from "node:path";
4296
+ import path8 from "node:path";
3838
4297
  function deployWorkspace(env = process.env) {
3839
- if (env.BUNSHIN_DEPLOY_WORKSPACE) return path6.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
4298
+ if (env.BUNSHIN_DEPLOY_WORKSPACE) return path8.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
3840
4299
  try {
3841
- const ws = JSON.parse(fs7.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
4300
+ const ws = JSON.parse(fs9.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
3842
4301
  return typeof ws === "string" && ws ? ws : null;
3843
4302
  } catch {
3844
4303
  return null;
3845
4304
  }
3846
4305
  }
3847
- function deployPaths(env = process.env) {
4306
+ function deployPaths(env = process.env, orgId) {
3848
4307
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3849
4308
  const ws = deployWorkspace(env);
4309
+ const state = path8.join(home, ".bunshin", "deploy");
4310
+ const shared = { claude: path8.join(ws ?? home, ".claude", "skills"), codex: path8.join(ws ?? home, ".codex", "skills") };
4311
+ const org = orgId ? path8.join(state, orgKey(orgId)) : null;
3850
4312
  return {
3851
4313
  home,
3852
- claudeSkills: path6.join(ws ?? home, ".claude", "skills"),
3853
- codexSkills: path6.join(ws ?? home, ".codex", "skills"),
3854
- claudeMd: ws ? path6.join(ws, "CLAUDE.md") : path6.join(home, ".claude", "CLAUDE.md"),
3855
- agentsMd: ws ? path6.join(ws, "AGENTS.md") : path6.join(home, ".codex", "AGENTS.md"),
3856
- library: path6.join(home, ".bunshin", "library"),
3857
- bin: path6.join(home, ".bunshin", "bin"),
3858
- state: path6.join(home, ".bunshin", "deploy")
4314
+ claudeSkills: org ? path8.join(org, "claude-skills") : shared.claude,
4315
+ codexSkills: org ? path8.join(org, "codex-skills") : shared.codex,
4316
+ ...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path8.join(home, ".claude", "skills"), path8.join(home, ".codex", "skills"), path8.join(home, ".bunshin", "library")])] } : {},
4317
+ claudeMd: ws ? path8.join(ws, "CLAUDE.md") : path8.join(home, ".claude", "CLAUDE.md"),
4318
+ agentsMd: ws ? path8.join(ws, "AGENTS.md") : path8.join(home, ".codex", "AGENTS.md"),
4319
+ library: org ? path8.join(org, "library") : path8.join(home, ".bunshin", "library"),
4320
+ // Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
4321
+ bin: path8.join(home, ".bunshin", "bin"),
4322
+ state
3859
4323
  };
3860
4324
  }
3861
- function setDeployWorkspace(dir, env = process.env) {
3862
- const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3863
- const ws = dir ? path6.resolve(dir.replace(/^~(?=$|\/)/, home)) : null;
3864
- if (ws && !fs7.statSync(ws, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${ws} is not a folder`);
3865
- const stateDir = path6.join(home, ".bunshin", "deploy");
3866
- const deployed = fs7.existsSync(stateDir) && fs7.readdirSync(stateDir).some((f) => {
3867
- if (f === "settings.json" || !f.endsWith(".json")) return false;
3868
- try {
3869
- return Object.keys(JSON.parse(fs7.readFileSync(path6.join(stateDir, f), "utf8")).docs ?? {}).length > 0;
3870
- } catch {
3871
- return false;
3872
- }
3873
- });
3874
- if (deployed && ws !== deployWorkspace({ ...env, BUNSHIN_DEPLOY_WORKSPACE: "" })) {
3875
- throw new Error("The library is already deployed to the current location. Set the workspace before the first sync on this machine.");
3876
- }
3877
- fs7.mkdirSync(path6.dirname(settingsFile(home)), { recursive: true });
3878
- fs7.writeFileSync(settingsFile(home), JSON.stringify(ws ? { workspace: ws } : {}, null, 2) + "\n");
3879
- return ws;
3880
- }
3881
4325
  function statePath(p, orgId) {
3882
- return path6.join(p.state, `${orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org"}.json`);
4326
+ return path8.join(p.state, `${orgKey(orgId)}.json`);
3883
4327
  }
3884
4328
  function readState(p, orgId) {
3885
4329
  const s = readJson(statePath(p, orgId), null);
3886
- return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
4330
+ return { orgId, since: s?.since ?? (/* @__PURE__ */ new Date()).toISOString(), docs: s?.docs ?? {}, created: s?.created ?? {}, links: s?.links ?? [], ...s?.linked?.length ? { linked: s.linked } : {}, ...s?.libraryVersion ? { libraryVersion: s.libraryVersion } : {} };
3887
4331
  }
3888
4332
  function readText(file) {
3889
4333
  try {
3890
- return fs7.readFileSync(file, "utf8");
4334
+ return fs9.readFileSync(file, "utf8");
3891
4335
  } catch {
3892
4336
  return null;
3893
4337
  }
@@ -3895,13 +4339,13 @@ function readText(file) {
3895
4339
  function writeFile(file, content, mode) {
3896
4340
  writeTextAtomic(file, content);
3897
4341
  try {
3898
- fs7.chmodSync(file, mode === "x" ? 493 : 420);
4342
+ fs9.chmodSync(file, mode === "x" ? 493 : 420);
3899
4343
  } catch {
3900
4344
  }
3901
4345
  }
3902
4346
  function removeFile(file) {
3903
4347
  try {
3904
- fs7.unlinkSync(file);
4348
+ fs9.unlinkSync(file);
3905
4349
  } catch {
3906
4350
  }
3907
4351
  }
@@ -3909,33 +4353,33 @@ function pruneDirs(dir, stop) {
3909
4353
  let d = dir;
3910
4354
  while (d.startsWith(stop) && d !== stop) {
3911
4355
  try {
3912
- if (fs7.readdirSync(d).length) return;
3913
- fs7.rmdirSync(d);
4356
+ if (fs9.readdirSync(d).length) return;
4357
+ fs9.rmdirSync(d);
3914
4358
  } catch {
3915
4359
  return;
3916
4360
  }
3917
- d = path6.dirname(d);
4361
+ d = path8.dirname(d);
3918
4362
  }
3919
4363
  }
3920
4364
  function listFiles(dir, base = dir) {
3921
4365
  let entries;
3922
4366
  try {
3923
- entries = fs7.readdirSync(dir, { withFileTypes: true });
4367
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
3924
4368
  } catch {
3925
4369
  return [];
3926
4370
  }
3927
4371
  const out = [];
3928
4372
  for (const e of entries) {
3929
- const abs = path6.join(dir, e.name);
4373
+ const abs = path8.join(dir, e.name);
3930
4374
  if (e.isDirectory()) out.push(...listFiles(abs, base));
3931
- else if (e.isFile()) out.push(path6.relative(base, abs).split(path6.sep).join("/"));
4375
+ else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
3932
4376
  }
3933
4377
  return out.sort();
3934
4378
  }
3935
4379
  function childDir(root, name) {
3936
4380
  if (!name || !SAFE_NAME.test(name)) return null;
3937
- const dir = path6.join(root, name);
3938
- return path6.dirname(path6.resolve(dir)) === path6.resolve(root) && insideRoot(root, dir) ? dir : null;
4381
+ const dir = path8.join(root, name);
4382
+ return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
3939
4383
  }
3940
4384
  function planDeploy(bundle, p, harnesses) {
3941
4385
  const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
@@ -4042,7 +4486,7 @@ function removeBlock(file) {
4042
4486
  if (start < 0 || end < start) return false;
4043
4487
  const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
4044
4488
  if (rest) writeTextAtomic(file, rest + "\n");
4045
- else fs7.unlinkSync(file);
4489
+ else fs9.unlinkSync(file);
4046
4490
  return true;
4047
4491
  }
4048
4492
  function writeBlock(file, block2) {
@@ -4063,7 +4507,7 @@ function skillIndex(skills, p) {
4063
4507
  "",
4064
4508
  "When a task matches a skill below, read its SKILL.md and follow it before doing the work by hand.",
4065
4509
  "",
4066
- ...skills.map((s) => `- **${s.skill.name}** (\`${path6.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
4510
+ ...skills.map((s) => `- **${s.skill.name}** (\`${path8.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
4067
4511
  ];
4068
4512
  return lines.join("\n");
4069
4513
  }
@@ -4112,7 +4556,7 @@ function captureDoc(c, doc, out) {
4112
4556
  for (const dir of doc.dirs ?? []) {
4113
4557
  for (const rel of listFiles(dir)) {
4114
4558
  if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
4115
- const abs = path6.join(dir, rel);
4559
+ const abs = path8.join(dir, rel);
4116
4560
  const content = readText(abs);
4117
4561
  if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
4118
4562
  byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
@@ -4151,18 +4595,18 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4151
4595
  for (const root of roots) {
4152
4596
  let names = [];
4153
4597
  try {
4154
- names = fs7.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
4598
+ names = fs9.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
4155
4599
  } catch {
4156
4600
  continue;
4157
4601
  }
4158
4602
  for (const name of names) {
4159
- const dir = path6.join(root, name);
4603
+ const dir = path8.join(root, name);
4160
4604
  if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
4161
- const skillMd = readText(path6.join(dir, "SKILL.md"));
4605
+ const skillMd = readText(path8.join(dir, "SKILL.md"));
4162
4606
  if (skillMd === null) continue;
4163
4607
  let born = 0;
4164
4608
  try {
4165
- const st = fs7.statSync(path6.join(dir, "SKILL.md"));
4609
+ const st = fs9.statSync(path8.join(dir, "SKILL.md"));
4166
4610
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4167
4611
  } catch {
4168
4612
  continue;
@@ -4171,8 +4615,8 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4171
4615
  const files = {};
4172
4616
  for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
4173
4617
  if (rel === "SKILL.md" || rel === MANIFEST) continue;
4174
- const content = readText(path6.join(dir, rel));
4175
- if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path6.join(dir, rel)) ? { mode: "x" } : {} };
4618
+ const content = readText(path8.join(dir, rel));
4619
+ if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir, rel)) ? { mode: "x" } : {} };
4176
4620
  }
4177
4621
  const hash = sha(JSON.stringify([skillMd, files]));
4178
4622
  if (state.created[name]?.hash === hash) continue;
@@ -4205,7 +4649,7 @@ function captureInto(v2, config, ctx, state, p, harnesses, now, incoming, captur
4205
4649
  return captured.map((s) => ({ suggestionId: s.id, op: s.op.op, ...s.target.sectionId ? { sectionId: s.target.sectionId } : {}, name: s.summary ?? s.op.op }));
4206
4650
  }
4207
4651
  function captureEdits(v2, config, ctx, opts = {}) {
4208
- const p = opts.paths ?? deployPaths();
4652
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4209
4653
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4210
4654
  const release = lockDeploy(p);
4211
4655
  if (!release) return [];
@@ -4220,14 +4664,14 @@ function captureEdits(v2, config, ctx, opts = {}) {
4220
4664
  }
4221
4665
  function lockDeploy(p) {
4222
4666
  ensureDir(p.state);
4223
- const file = path6.join(p.state, ".lock");
4667
+ const file = path8.join(p.state, ".lock");
4224
4668
  for (let i = 0; i < 2; i++) {
4225
4669
  try {
4226
- fs7.writeFileSync(file, String(process.pid), { flag: "wx" });
4670
+ fs9.writeFileSync(file, String(process.pid), { flag: "wx" });
4227
4671
  return () => removeFile(file);
4228
4672
  } catch {
4229
4673
  try {
4230
- const age = Date.now() - fs7.statSync(file).mtimeMs;
4674
+ const age = Date.now() - fs9.statSync(file).mtimeMs;
4231
4675
  const pid = Number(readText(file));
4232
4676
  let alive = age < 12e4;
4233
4677
  if (alive && pid > 0) {
@@ -4245,11 +4689,14 @@ function lockDeploy(p) {
4245
4689
  }
4246
4690
  return null;
4247
4691
  }
4692
+ function linkHarnesses(config) {
4693
+ return [.../* @__PURE__ */ new Set([config.harness, ...config.deploy?.harnesses ?? []])].filter((h) => h === "claude-code" || h === "codex");
4694
+ }
4248
4695
  function deployEnabled(config, ctx) {
4249
4696
  return ctx.kind === "person" && config.deploy?.enabled !== false;
4250
4697
  }
4251
4698
  function deployLibrary(v2, config, ctx, bundle, opts = {}) {
4252
- const p = opts.paths ?? deployPaths();
4699
+ const p = opts.paths ?? deployPaths(process.env, config.orgId);
4253
4700
  const harnesses = new Set(opts.harnesses ?? config.deploy?.harnesses ?? ["claude-code", "codex"]);
4254
4701
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4255
4702
  const release = lockDeploy(p);
@@ -4266,6 +4713,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4266
4713
  const plan = planDeploy(bundle, p, harnesses);
4267
4714
  report.warnings.push(...plan.warnings);
4268
4715
  const owned = (abs) => [p.claudeSkills, p.codexSkills, p.library].some((r) => insideRoot(r, abs));
4716
+ const removable = (abs) => owned(abs) || (p.legacy ?? []).some((r) => insideRoot(r, abs));
4269
4717
  if (opts.capture !== false) report.captured = captureInto(v2, config, ctx, state, p, harnesses, now, plan.skills.map((x) => x.skill.name), opts.captureNewSkills);
4270
4718
  const decided = new Map(bundle.decisions.map((d) => [d.suggestionId, d.decision.status]));
4271
4719
  const localStatus = (id) => decided.get(id) ?? v2.readSuggestion(id)?.status;
@@ -4290,7 +4738,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4290
4738
  const foreign = /* @__PURE__ */ new Set();
4291
4739
  if (d.kind === "skill" && !prev) {
4292
4740
  for (const dir of d.dirs ?? []) {
4293
- const cur = readText(path6.join(dir, "SKILL.md"));
4741
+ const cur = readText(path8.join(dir, "SKILL.md"));
4294
4742
  if (cur === null || cur === d.section.body || state.created[d.name]) continue;
4295
4743
  foreign.add(dir);
4296
4744
  report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
@@ -4302,7 +4750,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4302
4750
  targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
4303
4751
  continue;
4304
4752
  }
4305
- if ([...foreign].some((dir) => t.abs.startsWith(dir + path6.sep))) continue;
4753
+ if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
4306
4754
  if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
4307
4755
  report.warnings.push(`${t.abs} not written: it leads outside its folder`);
4308
4756
  continue;
@@ -4311,10 +4759,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4311
4759
  const existing = readText(t.abs);
4312
4760
  try {
4313
4761
  if (existing !== content) {
4314
- ensureDir(path6.dirname(t.abs));
4762
+ ensureDir(path8.dirname(t.abs));
4315
4763
  writeFile(t.abs, content, t.mode);
4316
4764
  report.written++;
4317
- } else if (t.mode === "x" && !isExecutable(t.abs)) fs7.chmodSync(t.abs, 493);
4765
+ } else if (t.mode === "x" && !isExecutable(t.abs)) fs9.chmodSync(t.abs, 493);
4318
4766
  } catch (err) {
4319
4767
  report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
4320
4768
  continue;
@@ -4323,18 +4771,23 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4323
4771
  prevByAbs.delete(`${t.abs}#${t.role}`);
4324
4772
  }
4325
4773
  for (const old of prevByAbs.values()) {
4326
- if (old.role === "block" || d.contents.has(old.abs) || !owned(old.abs)) continue;
4774
+ if (old.role === "block" || d.contents.has(old.abs) || !removable(old.abs)) continue;
4327
4775
  const cur = readText(old.abs);
4328
4776
  if (cur !== null && sha(cur) === old.sha) {
4329
4777
  removeFile(old.abs);
4330
- pruneDirs(path6.dirname(old.abs), d.kind === "skill" ? path6.dirname(path6.dirname(old.abs)) : p.library);
4778
+ pruneDirs(path8.dirname(old.abs), d.kind === "skill" ? path8.dirname(path8.dirname(old.abs)) : p.library);
4331
4779
  report.removed++;
4332
4780
  }
4333
4781
  }
4334
4782
  if (d.kind === "skill") {
4783
+ for (const dir of prev?.dirs ?? []) {
4784
+ if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4785
+ removeFile(path8.join(dir, MANIFEST));
4786
+ pruneDirs(dir, path8.dirname(dir));
4787
+ }
4335
4788
  for (const dir of d.dirs ?? []) {
4336
4789
  const manifest = safeJoin(dir, MANIFEST);
4337
- if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
4790
+ if (foreign.has(dir) || !manifest || !fs9.existsSync(path8.join(dir, "SKILL.md"))) continue;
4338
4791
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4339
4792
  }
4340
4793
  report.skills++;
@@ -4350,7 +4803,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4350
4803
  continue;
4351
4804
  }
4352
4805
  for (const t of doc.targets) {
4353
- if (t.role === "block" || !owned(t.abs)) continue;
4806
+ if (t.role === "block" || !removable(t.abs)) continue;
4354
4807
  const cur = readText(t.abs);
4355
4808
  if (cur !== null && sha(cur) === t.sha) {
4356
4809
  removeFile(t.abs);
@@ -4358,13 +4811,13 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4358
4811
  }
4359
4812
  }
4360
4813
  for (const dir of doc.dirs ?? []) {
4361
- if (!owned(dir)) continue;
4362
- removeFile(path6.join(dir, MANIFEST));
4363
- pruneDirs(dir, path6.dirname(dir));
4364
- if (fs7.existsSync(dir)) {
4814
+ if (!removable(dir)) continue;
4815
+ removeFile(path8.join(dir, MANIFEST));
4816
+ pruneDirs(dir, path8.dirname(dir));
4817
+ if (fs9.existsSync(dir)) {
4365
4818
  const left = listFiles(dir);
4366
4819
  if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4367
- else pruneDirs(dir, path6.dirname(dir));
4820
+ else pruneDirs(dir, path8.dirname(dir));
4368
4821
  }
4369
4822
  }
4370
4823
  }
@@ -4380,17 +4833,17 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4380
4833
  if (d.kind === "skill") continue;
4381
4834
  for (const t of d.targets) {
4382
4835
  if (t.role === "block" || t.mode !== "x" || !/(^|\/)bin\/[^/]+$/.test(t.rel) || !owned(t.abs)) continue;
4383
- const link = path6.join(p.bin, path6.basename(t.abs));
4836
+ const link = path8.join(p.bin, path8.basename(t.abs));
4384
4837
  ensureDir(p.bin);
4385
4838
  try {
4386
- const cur = fs7.readlinkSync(link);
4839
+ const cur = fs9.readlinkSync(link);
4387
4840
  if (cur !== t.abs) {
4388
- fs7.unlinkSync(link);
4389
- fs7.symlinkSync(t.abs, link);
4841
+ fs9.unlinkSync(link);
4842
+ fs9.symlinkSync(t.abs, link);
4390
4843
  }
4391
4844
  } catch {
4392
4845
  try {
4393
- if (!fs7.existsSync(link)) fs7.symlinkSync(t.abs, link);
4846
+ if (!fs9.existsSync(link)) fs9.symlinkSync(t.abs, link);
4394
4847
  } catch {
4395
4848
  }
4396
4849
  }
@@ -4401,15 +4854,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4401
4854
  if (harnesses.has("claude-code")) {
4402
4855
  for (const pack of packs) {
4403
4856
  const dir = childDir(p.library, pack);
4404
- if (!dir || !insideRoot(dir, path6.join(dir, ".claude", "x"))) continue;
4405
- const link = path6.join(dir, ".claude", "skills");
4857
+ if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
4858
+ const link = path8.join(dir, ".claude", "skills");
4406
4859
  try {
4407
- fs7.lstatSync(link);
4860
+ fs9.lstatSync(link);
4861
+ if (fs9.readlinkSync(link) !== p.claudeSkills) {
4862
+ fs9.unlinkSync(link);
4863
+ fs9.symlinkSync(p.claudeSkills, link);
4864
+ }
4408
4865
  } catch {
4409
- ensureDir(path6.dirname(link));
4866
+ ensureDir(path8.dirname(link));
4410
4867
  ensureDir(p.claudeSkills);
4411
4868
  try {
4412
- fs7.symlinkSync(p.claudeSkills, link);
4869
+ fs9.symlinkSync(p.claudeSkills, link);
4413
4870
  } catch {
4414
4871
  }
4415
4872
  }
@@ -4418,32 +4875,53 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4418
4875
  for (const old of state.links) {
4419
4876
  if (links.has(old)) continue;
4420
4877
  try {
4421
- const target = fs7.readlinkSync(old);
4422
- if (!fs7.existsSync(target)) fs7.unlinkSync(old);
4878
+ const target = fs9.readlinkSync(old);
4879
+ if (!fs9.existsSync(target)) fs9.unlinkSync(old);
4423
4880
  } catch {
4424
4881
  }
4425
4882
  }
4426
- const saved = { ...state, docs: next, links: [...links], ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
4883
+ let linked = state.linked ?? [];
4884
+ if (p.legacy) {
4885
+ const deployed = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length);
4886
+ const layerOf = new Map(plan.skills.map((x) => [x.skill.name, x.layer]));
4887
+ const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path8.resolve(opts.linkRoot)] : []])];
4888
+ linked = [];
4889
+ for (const root of roots) {
4890
+ let source;
4891
+ try {
4892
+ source = sourceFor(root, v2.store.root, ctx);
4893
+ } catch {
4894
+ source = void 0;
4895
+ }
4896
+ const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle.scopes ? { tree: bundle.scopes } : {} });
4897
+ const names = deployed.filter((d) => inBoundary({ layer: layerOf.get(d.name) ?? "org" }, reader)).map((d) => d.name);
4898
+ const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
4899
+ report.warnings.push(...res.warnings);
4900
+ if (res.kept) linked.push(root);
4901
+ }
4902
+ }
4903
+ const saved = { ...state, docs: next, links: [...links], ...linked.length ? { linked } : {}, ...bundle.libraryVersion ? { libraryVersion: bundle.libraryVersion } : {} };
4904
+ if (!linked.length) delete saved.linked;
4427
4905
  writeJsonAtomic(statePath(p, config.orgId), saved);
4428
4906
  report.libraryVersion = bundle.libraryVersion;
4429
4907
  return report;
4430
4908
  }
4431
- function deployedState(config, paths = deployPaths()) {
4909
+ function deployedState(config, paths = deployPaths(process.env, config.orgId)) {
4432
4910
  const s = readState(paths, config.orgId);
4433
- return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, docs: Object.values(s.docs) };
4911
+ return { ...s.libraryVersion ? { libraryVersion: s.libraryVersion } : {}, linked: s.linked ?? [], docs: Object.values(s.docs) };
4434
4912
  }
4435
4913
  function materialize(sections, dir) {
4436
4914
  let files = 0;
4437
4915
  const put = (rel, content, mode) => {
4438
4916
  const abs = safeJoin(dir, rel);
4439
4917
  if (!abs) return;
4440
- ensureDir(path6.dirname(abs));
4918
+ ensureDir(path8.dirname(abs));
4441
4919
  writeFile(abs, content, mode);
4442
4920
  files++;
4443
4921
  };
4444
4922
  for (const s of sections.filter((x) => x.status === "active" || x.status === "stale")) {
4445
4923
  if (s.skill) {
4446
- const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path6.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
4924
+ const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path8.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
4447
4925
  put(`${base}/SKILL.md`, s.body);
4448
4926
  for (const [rel, f] of Object.entries(s.skill.files ?? {})) put(`${base}/${rel}`, f.content, f.mode);
4449
4927
  continue;
@@ -4453,22 +4931,106 @@ function materialize(sections, dir) {
4453
4931
  }
4454
4932
  return { files };
4455
4933
  }
4456
- var settingsFile, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS;
4934
+ function linkSkills(p, root, harnesses, names) {
4935
+ const out = { linked: 0, removed: 0, kept: false, warnings: [] };
4936
+ const abs = path8.resolve(root);
4937
+ if (abs === path8.resolve(p.home) || abs === path8.resolve(os2.homedir()) || !fs9.existsSync(abs)) return out;
4938
+ const managed = (target) => target.startsWith(path8.resolve(p.state) + path8.sep);
4939
+ const excluded = [];
4940
+ for (const h of harnesses) {
4941
+ const spec = LINK_DIRS[h];
4942
+ if (!spec) continue;
4943
+ const dir = path8.join(abs, spec.rel);
4944
+ const canonical = path8.resolve(p[spec.key]);
4945
+ const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
4946
+ let entries = [];
4947
+ try {
4948
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
4949
+ } catch {
4950
+ }
4951
+ for (const e of entries) {
4952
+ if (!e.isSymbolicLink()) continue;
4953
+ const link = path8.join(dir, e.name);
4954
+ let target = "";
4955
+ try {
4956
+ target = path8.resolve(dir, fs9.readlinkSync(link));
4957
+ } catch {
4958
+ continue;
4959
+ }
4960
+ if (!managed(target)) continue;
4961
+ if (want.has(e.name) && target === path8.join(canonical, e.name)) {
4962
+ want.delete(e.name);
4963
+ excluded.push(path8.join(spec.rel, e.name));
4964
+ continue;
4965
+ }
4966
+ removeFile(link);
4967
+ out.removed++;
4968
+ }
4969
+ for (const name of want) {
4970
+ const link = path8.join(dir, name);
4971
+ try {
4972
+ fs9.lstatSync(link);
4973
+ out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
4974
+ continue;
4975
+ } catch {
4976
+ }
4977
+ try {
4978
+ ensureDir(dir);
4979
+ fs9.symlinkSync(path8.join(canonical, name), link, "dir");
4980
+ excluded.push(path8.join(spec.rel, name));
4981
+ out.linked++;
4982
+ } catch (err) {
4983
+ out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
4984
+ }
4985
+ }
4986
+ pruneDirs(dir, abs);
4987
+ }
4988
+ out.kept = excluded.length > 0;
4989
+ gitExclude(abs, excluded);
4990
+ return out;
4991
+ }
4992
+ function gitExclude(root, rels) {
4993
+ if (!fs9.existsSync(path8.join(root, ".git"))) return;
4994
+ let file;
4995
+ try {
4996
+ file = path8.resolve(root, execFileSync3("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
4997
+ } catch {
4998
+ return;
4999
+ }
5000
+ const cur = readText(file) ?? "";
5001
+ const start = cur.indexOf(EXCLUDE_BEGIN);
5002
+ const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
5003
+ const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
5004
+ const block2 = rels.length ? `${EXCLUDE_BEGIN}
5005
+ ${[...new Set(rels)].sort().map((r) => `/${r.split(path8.sep).join("/")}`).join("\n")}
5006
+ ${EXCLUDE_END}
5007
+ ` : "";
5008
+ const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
5009
+ if (next === cur) return;
5010
+ try {
5011
+ ensureDir(path8.dirname(file));
5012
+ fs9.writeFileSync(file, next);
5013
+ } catch {
5014
+ }
5015
+ }
5016
+ var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
4457
5017
  var init_deploy = __esm({
4458
5018
  "src/v2/deploy.ts"() {
4459
5019
  "use strict";
4460
5020
  init_v22();
5021
+ init_source();
4461
5022
  init_util();
4462
5023
  init_store2();
4463
- settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
4464
- sha = (text2) => createHash2("sha256").update(text2).digest("hex");
5024
+ settingsFile = (home) => path8.join(home, ".bunshin", "deploy", "settings.json");
5025
+ orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
5026
+ sha = (text2) => createHash3("sha256").update(text2).digest("hex");
4465
5027
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
4466
5028
  BLOCK_END = "<!-- bunshin:end -->";
4467
5029
  MANIFEST = ".bunshin.json";
4468
5030
  MAX_FILE = 2 * 1024 * 1024;
4469
5031
  isExecutable = (file) => {
4470
5032
  try {
4471
- return (fs7.statSync(file).mode & 73) !== 0;
5033
+ return (fs9.statSync(file).mode & 73) !== 0;
4472
5034
  } catch {
4473
5035
  return false;
4474
5036
  }
@@ -4476,9 +5038,15 @@ var init_deploy = __esm({
4476
5038
  safeJoin = containedPath;
4477
5039
  SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
4478
5040
  neutralizeMarkers = (text2) => text2.replace(/<!--(?=\s*bunshin:)/gi, "<!-\u200B-");
4479
- LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
5041
+ LAYER_RANK = { org: 0, dept: 1, group: 2, team: 2, source: 3, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
4480
5042
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
4481
5043
  BORN_SLACK_MS = 2e3;
5044
+ LINK_DIRS = {
5045
+ "claude-code": { rel: path8.join(".claude", "skills"), key: "claudeSkills" },
5046
+ codex: { rel: path8.join(".codex", "skills"), key: "codexSkills" }
5047
+ };
5048
+ EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
5049
+ EXCLUDE_END = "# <<< bunshin";
4482
5050
  }
4483
5051
  });
4484
5052
 
@@ -4587,11 +5155,11 @@ var init_live = __esm({
4587
5155
  });
4588
5156
 
4589
5157
  // src/transcript.ts
4590
- import fs8 from "node:fs";
5158
+ import fs10 from "node:fs";
4591
5159
  function parseTranscript(file) {
4592
5160
  let raw = "";
4593
5161
  try {
4594
- raw = fs8.readFileSync(file, "utf8");
5162
+ raw = fs10.readFileSync(file, "utf8");
4595
5163
  } catch {
4596
5164
  return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
4597
5165
  }
@@ -4729,245 +5297,81 @@ function summarizeInput(input) {
4729
5297
  function bounded(text2, maxChars) {
4730
5298
  if (text2.length <= maxChars) return text2;
4731
5299
  const marker = "\n[... middle omitted ...]\n";
4732
- if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
4733
- const head = Math.floor((maxChars - marker.length) / 2);
4734
- return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
4735
- }
4736
- function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
4737
- const parts = [];
4738
- for (const t of facts.turns.slice(from)) {
4739
- const text2 = redact2(t.text);
4740
- if (t.role === "user") parts.push(`[USER]
4741
- ${bounded(text2, maxChars)}`);
4742
- else if (t.role === "assistant") parts.push(`[ASSISTANT]
4743
- ${bounded(text2, maxChars)}`);
4744
- else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4745
- ${bounded(text2, toolResultChars)}`);
4746
- else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
4747
- }
4748
- return bounded(parts.join("\n\n"), maxChars);
4749
- }
4750
- function fullTrace(facts, { from = 0, to } = {}) {
4751
- return facts.turns.slice(from, to).map((t) => {
4752
- if (t.role === "user") return `[USER]
4753
- ${redact2(t.text)}`;
4754
- if (t.role === "assistant") return `[ASSISTANT]
4755
- ${redact2(t.text)}`;
4756
- if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4757
- ${redact2(t.text)}`;
4758
- const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
4759
- return `[TOOL ${t.tool}]
4760
- ${redact2(input)}`;
4761
- });
4762
- }
4763
- function traceChunks(turns, maxBytes = 1e6) {
4764
- const out = [];
4765
- let cur = "";
4766
- let size = 0;
4767
- turns.forEach((turn, i) => {
4768
- let piece = (i ? "\n\n" : "") + turn;
4769
- let bytes = jsonBytes(piece);
4770
- if (size + bytes <= maxBytes) {
4771
- cur += piece;
4772
- size += bytes;
4773
- return;
4774
- }
4775
- if (cur) out.push(cur);
4776
- while (bytes > maxBytes) {
4777
- let n = Math.min(piece.length, maxBytes);
4778
- for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
4779
- const code2 = piece.charCodeAt(n - 1);
4780
- if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
4781
- const head = piece.slice(0, n);
4782
- out.push(head);
4783
- piece = piece.slice(n);
4784
- bytes -= jsonBytes(head);
4785
- }
4786
- cur = piece;
4787
- size = bytes;
4788
- });
4789
- if (cur) out.push(cur);
4790
- return out;
4791
- }
4792
- function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
4793
- const facts = parseTranscript(file);
4794
- const visible = facts.turns.filter((t) => t.role !== "tool");
4795
- const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
4796
- const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
4797
- return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
4798
- }
4799
- var BANNER, jsonBytes;
4800
- var init_transcript = __esm({
4801
- "src/transcript.ts"() {
4802
- "use strict";
4803
- init_redact2();
4804
- init_util();
4805
- BANNER = "## Recalled lessons (Bunshin)";
4806
- jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
4807
- }
4808
- });
4809
-
4810
- // src/v2/repo.ts
4811
- import fs9 from "node:fs";
4812
- import path7 from "node:path";
4813
- import { execFileSync } from "node:child_process";
4814
- function git(cwd, args, timeout = 1500) {
4815
- try {
4816
- return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
4817
- } catch {
4818
- return null;
4819
- }
4820
- }
4821
- function repoRoot(cwd, storeRoot) {
4822
- const key = `${cwd}
4823
- ${storeRoot ?? ""}`;
4824
- const hit = rootCache.get(key);
4825
- if (hit) return hit;
4826
- const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
4827
- let root = top || "";
4828
- if (!root && storeRoot && path7.basename(storeRoot) === ".bunshin") {
4829
- const parent = path7.dirname(storeRoot);
4830
- if (path7.resolve(cwd).startsWith(path7.resolve(parent))) root = parent;
4831
- }
4832
- root = root || path7.resolve(cwd);
4833
- rootCache.set(key, root);
4834
- return root;
4835
- }
4836
- function parseRemote(url) {
4837
- const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
4838
- if (!clean2) return null;
4839
- const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
4840
- const pathPart = scp ? scp[1] : (() => {
4841
- try {
4842
- return new URL(clean2).pathname;
4843
- } catch {
4844
- return clean2;
4845
- }
4846
- })();
4847
- const segs = pathPart.split("/").filter(Boolean);
4848
- if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
4849
- return segs[0] ?? null;
4850
- }
4851
- function repoName(root) {
4852
- const url = git(root, ["remote", "get-url", "origin"]);
4853
- return url && parseRemote(url) || path7.basename(root);
4854
- }
4855
- function repoCommit(root) {
4856
- return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
4857
- }
4858
- function listDocs(root) {
4859
- const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
4860
- if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs9.existsSync(path7.join(root, rel)) && !deployedSkill(root, rel)).sort();
4861
- const out = [];
4862
- let visited = 0;
4863
- const walk = (dir, rel, depth) => {
4864
- if (depth > 8 || visited++ > 2e4) return;
4865
- let entries;
4866
- try {
4867
- entries = fs9.readdirSync(dir, { withFileTypes: true });
4868
- } catch {
4869
- return;
4870
- }
4871
- for (const e of entries) {
4872
- if (e.isDirectory()) {
4873
- if (!SKIP_DIRS.has(e.name) && !fs9.existsSync(path7.join(dir, e.name, ".git"))) walk(path7.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
4874
- } else if (e.isFile()) {
4875
- const r = rel ? `${rel}/${e.name}` : e.name;
4876
- if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
4877
- }
4878
- }
4879
- };
4880
- walk(root, "", 0);
4881
- return out.sort();
4882
- }
4883
- function indexWorkingTree(v2, config, cwd, opts = {}) {
4884
- const root = repoRoot(cwd, v2.store.root);
4885
- const now = opts.now ?? Date.now();
4886
- const cached = v2.readRepoIndex();
4887
- const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
4888
- const sameRepo = cached && cached.ownerTeam === ownerTeam;
4889
- const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
4890
- const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
4891
- const paths = fresh ? cached.paths : listDocs(root);
4892
- let changed = !fresh || !sameRepo || cached.repo !== repo;
4893
- const files = {};
4894
- for (const rel of paths) {
4895
- const abs = path7.join(root, rel);
4896
- let stat;
4897
- try {
4898
- stat = fs9.statSync(abs);
4899
- } catch {
4900
- changed = true;
4901
- continue;
4902
- }
4903
- const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
4904
- if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
4905
- files[rel] = prev;
4906
- continue;
4907
- }
4908
- changed = true;
4909
- try {
4910
- const text2 = fs9.readFileSync(abs, "utf8");
4911
- files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
4912
- } catch {
4913
- }
4914
- }
4915
- const index = {
4916
- repo,
4917
- ownerTeam,
4918
- listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
4919
- paths,
4920
- files
4921
- };
4922
- if (changed) {
4923
- try {
4924
- v2.writeRepoIndex(index);
4925
- } catch {
4926
- }
4927
- }
4928
- return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
5300
+ if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
5301
+ const head = Math.floor((maxChars - marker.length) / 2);
5302
+ return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
4929
5303
  }
4930
- function relativeToRoot(root, file, cwd = root) {
4931
- if (!file) return null;
4932
- const abs = path7.isAbsolute(file) ? file : path7.resolve(cwd, file);
4933
- let real = abs;
4934
- let realRoot = root;
4935
- try {
4936
- realRoot = fs9.realpathSync(root);
4937
- } catch {
5304
+ function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
5305
+ const parts = [];
5306
+ for (const t of facts.turns.slice(from)) {
5307
+ const text2 = redact2(t.text);
5308
+ if (t.role === "user") parts.push(`[USER]
5309
+ ${bounded(text2, maxChars)}`);
5310
+ else if (t.role === "assistant") parts.push(`[ASSISTANT]
5311
+ ${bounded(text2, maxChars)}`);
5312
+ else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
5313
+ ${bounded(text2, toolResultChars)}`);
5314
+ else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
4938
5315
  }
4939
- try {
4940
- real = fs9.realpathSync(abs);
4941
- } catch {
4942
- try {
4943
- real = path7.join(fs9.realpathSync(path7.dirname(abs)), path7.basename(abs));
4944
- } catch {
5316
+ return bounded(parts.join("\n\n"), maxChars);
5317
+ }
5318
+ function fullTrace(facts, { from = 0, to } = {}) {
5319
+ return facts.turns.slice(from, to).map((t) => {
5320
+ if (t.role === "user") return `[USER]
5321
+ ${redact2(t.text)}`;
5322
+ if (t.role === "assistant") return `[ASSISTANT]
5323
+ ${redact2(t.text)}`;
5324
+ if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
5325
+ ${redact2(t.text)}`;
5326
+ const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
5327
+ return `[TOOL ${t.tool}]
5328
+ ${redact2(input)}`;
5329
+ });
5330
+ }
5331
+ function traceChunks(turns, maxBytes = 1e6) {
5332
+ const out = [];
5333
+ let cur = "";
5334
+ let size = 0;
5335
+ turns.forEach((turn, i) => {
5336
+ let piece = (i ? "\n\n" : "") + turn;
5337
+ let bytes = jsonBytes(piece);
5338
+ if (size + bytes <= maxBytes) {
5339
+ cur += piece;
5340
+ size += bytes;
5341
+ return;
4945
5342
  }
4946
- }
4947
- for (const [r, f] of [
4948
- [realRoot, real],
4949
- [root, abs]
4950
- ]) {
4951
- const rel = path7.relative(r, f);
4952
- if (rel && !rel.startsWith("..") && !path7.isAbsolute(rel)) return rel.split(path7.sep).join("/");
4953
- }
4954
- return null;
5343
+ if (cur) out.push(cur);
5344
+ while (bytes > maxBytes) {
5345
+ let n = Math.min(piece.length, maxBytes);
5346
+ for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
5347
+ const code2 = piece.charCodeAt(n - 1);
5348
+ if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
5349
+ const head = piece.slice(0, n);
5350
+ out.push(head);
5351
+ piece = piece.slice(n);
5352
+ bytes -= jsonBytes(head);
5353
+ }
5354
+ cur = piece;
5355
+ size = bytes;
5356
+ });
5357
+ if (cur) out.push(cur);
5358
+ return out;
4955
5359
  }
4956
- var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
4957
- var init_repo = __esm({
4958
- "src/v2/repo.ts"() {
5360
+ function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
5361
+ const facts = parseTranscript(file);
5362
+ const visible = facts.turns.filter((t) => t.role !== "tool");
5363
+ const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
5364
+ const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
5365
+ return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
5366
+ }
5367
+ var BANNER, jsonBytes;
5368
+ var init_transcript = __esm({
5369
+ "src/transcript.ts"() {
4959
5370
  "use strict";
4960
- init_v22();
4961
- DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
4962
- SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
4963
- LIST_TTL_MS = 6e4;
4964
- rootCache = /* @__PURE__ */ new Map();
4965
- isDoc = (rel) => {
4966
- const parts = rel.split("/");
4967
- if (parts.some((p) => SKIP_DIRS.has(p))) return false;
4968
- return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
4969
- };
4970
- deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs9.existsSync(path7.join(root, path7.posix.dirname(rel), ".bunshin.json"));
5371
+ init_redact2();
5372
+ init_util();
5373
+ BANNER = "## Recalled lessons (Bunshin)";
5374
+ jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
4971
5375
  }
4972
5376
  });
4973
5377
 
@@ -5036,8 +5440,14 @@ function applyOverlays(sections, overlays, ctx) {
5036
5440
  }
5037
5441
  function localView(v2, config, cwd, opts = {}) {
5038
5442
  const ctx = contextFor(v2, config);
5039
- const reader = readerFor(ctx);
5040
5443
  const bundle = v2.readBundle();
5444
+ let source;
5445
+ try {
5446
+ source = opts.source !== void 0 ? opts.source || void 0 : sourceFor(cwd, v2.store.root, ctx);
5447
+ } catch {
5448
+ source = void 0;
5449
+ }
5450
+ const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle?.scopes ? { tree: bundle.scopes } : {} });
5041
5451
  const byId = /* @__PURE__ */ new Map();
5042
5452
  for (const s of bundle?.sections ?? []) byId.set(s.id, s);
5043
5453
  let root = cwd;
@@ -5055,7 +5465,7 @@ function localView(v2, config, cwd, opts = {}) {
5055
5465
  }
5056
5466
  }
5057
5467
  applyOverlays(byId, v2.readOverlays(), ctx);
5058
- return { ctx, reader, layers: layersFor(ctx), bundle, sections: [...byId.values()], byId, root, repo };
5468
+ return { ctx, reader, ...source ? { source } : {}, layers: reader.layers, bundle, sections: [...byId.values()], byId, root, repo };
5059
5469
  }
5060
5470
  var ACTIVE_OVERLAY;
5061
5471
  var init_view = __esm({
@@ -5063,12 +5473,13 @@ var init_view = __esm({
5063
5473
  "use strict";
5064
5474
  init_v22();
5065
5475
  init_repo();
5476
+ init_source();
5066
5477
  ACTIVE_OVERLAY = /* @__PURE__ */ new Set(["proposed", "trial", "submitted", "kept-local", "needs-review"]);
5067
5478
  }
5068
5479
  });
5069
5480
 
5070
5481
  // src/v2/apply.ts
5071
- import fs10 from "node:fs";
5482
+ import fs11 from "node:fs";
5072
5483
  function appendedText(op) {
5073
5484
  return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
5074
5485
  }
@@ -5123,12 +5534,12 @@ function applyLocally(v2, view, s) {
5123
5534
  if (section.storage.type === "repo") {
5124
5535
  const file = containedPath(view.root, section.storage.path);
5125
5536
  const ours = !view.repo || section.storage.repo === view.repo;
5126
- if (ours && file && fs10.existsSync(file)) {
5127
- const before = fs10.readFileSync(file, "utf8");
5537
+ if (ours && file && fs11.existsSync(file)) {
5538
+ const before = fs11.readFileSync(file, "utf8");
5128
5539
  const res2 = applyOpToMarkdown(before, section.storage.anchor, op);
5129
5540
  if (res2.ok) {
5130
5541
  const changed = res2.text !== before;
5131
- if (changed) fs10.writeFileSync(file, res2.text);
5542
+ if (changed) fs11.writeFileSync(file, res2.text);
5132
5543
  const overlay2 = { ...base, kind: "working-tree", sectionId: section.id, file: section.storage.path, anchor: section.storage.anchor, changed };
5133
5544
  upsertOverlay(v2, overlay2);
5134
5545
  return { applied: true, target: describeTarget(s, view), overlay: overlay2 };
@@ -5149,7 +5560,7 @@ function revertWorkingTree(root, o) {
5149
5560
  if (!file) return { reverted: false, reason: "path outside the repository" };
5150
5561
  let text2;
5151
5562
  try {
5152
- text2 = fs10.readFileSync(file, "utf8");
5563
+ text2 = fs11.readFileSync(file, "utf8");
5153
5564
  } catch {
5154
5565
  return { reverted: false, reason: "file missing" };
5155
5566
  }
@@ -5169,7 +5580,7 @@ ${appendedText(o.op)}`;
5169
5580
  if (once(text2, o.op.text) >= 0 && !text2.includes(o.op.find)) next = text2.replace(o.op.text, o.op.find);
5170
5581
  }
5171
5582
  if (next === null) return { reverted: false, reason: "file changed since the trial edit" };
5172
- fs10.writeFileSync(file, next);
5583
+ fs11.writeFileSync(file, next);
5173
5584
  return { reverted: true };
5174
5585
  }
5175
5586
  function revertParam(v2, o) {
@@ -5242,7 +5653,7 @@ var init_apply = __esm({
5242
5653
  });
5243
5654
 
5244
5655
  // src/v2/signals.ts
5245
- import fs11 from "node:fs";
5656
+ import fs12 from "node:fs";
5246
5657
  function isCorrection(text2) {
5247
5658
  const t = text2.trim().slice(0, 600);
5248
5659
  return !!t && CORRECTION_RE.test(t);
@@ -5264,7 +5675,7 @@ function scanTranscript(file) {
5264
5675
  if (!file) return empty;
5265
5676
  let raw;
5266
5677
  try {
5267
- raw = fs11.readFileSync(file, "utf8");
5678
+ raw = fs12.readFileSync(file, "utf8");
5268
5679
  } catch {
5269
5680
  return empty;
5270
5681
  }
@@ -5348,8 +5759,8 @@ var init_signals = __esm({
5348
5759
  });
5349
5760
 
5350
5761
  // src/v2/ui.ts
5351
- import fs12 from "node:fs";
5352
- import path8 from "node:path";
5762
+ import fs13 from "node:fs";
5763
+ import path9 from "node:path";
5353
5764
  function feedPath(v2, sessionId) {
5354
5765
  return v2.p("ui", `${safe(sessionId)}.json`);
5355
5766
  }
@@ -5365,7 +5776,7 @@ function emptyFeed(v2, sessionId, now) {
5365
5776
  }
5366
5777
  function cliArgv() {
5367
5778
  const entry = process.argv[1];
5368
- return entry ? [process.execPath, path8.resolve(entry)] : ["bunshin"];
5779
+ return entry ? [process.execPath, path9.resolve(entry)] : ["bunshin"];
5369
5780
  }
5370
5781
  function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toISOString()) {
5371
5782
  if (!sessionId || sessionId === "no-session") return null;
@@ -5380,7 +5791,7 @@ function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toI
5380
5791
  f.cli = cliArgv();
5381
5792
  f.served = f.served.slice(0, MAX_SERVED);
5382
5793
  f.events = f.events.slice(-MAX_EVENTS);
5383
- fs12.mkdirSync(path8.dirname(feedPath(v2, sessionId)), { recursive: true });
5794
+ fs13.mkdirSync(path9.dirname(feedPath(v2, sessionId)), { recursive: true });
5384
5795
  writeJsonAtomic(feedPath(v2, sessionId), f);
5385
5796
  return f;
5386
5797
  } catch {
@@ -5481,16 +5892,16 @@ function latestFeed(v2) {
5481
5892
  const dir = v2.p("ui");
5482
5893
  let best = null;
5483
5894
  try {
5484
- for (const f2 of fs12.readdirSync(dir)) {
5895
+ for (const f2 of fs13.readdirSync(dir)) {
5485
5896
  if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
5486
- const m = fs12.statSync(path8.join(dir, f2)).mtimeMs;
5897
+ const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
5487
5898
  if (!best || m > best.m) best = { f: f2, m };
5488
5899
  }
5489
5900
  } catch {
5490
5901
  return null;
5491
5902
  }
5492
5903
  if (!best) return null;
5493
- const f = readJson(path8.join(dir, best.f), null);
5904
+ const f = readJson(path9.join(dir, best.f), null);
5494
5905
  return f && f.v === UI_FEED_VERSION ? f : null;
5495
5906
  }
5496
5907
  var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
@@ -5515,8 +5926,8 @@ __export(sync_exports, {
5515
5926
  syncV2: () => syncV2,
5516
5927
  uploadIndex: () => uploadIndex
5517
5928
  });
5518
- import fs13 from "node:fs";
5519
- import path9 from "node:path";
5929
+ import fs14 from "node:fs";
5930
+ import path10 from "node:path";
5520
5931
  function episodePayload(e) {
5521
5932
  const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, transcriptPath: _tp, ...rest } = e;
5522
5933
  void _o, _u, _s, _rs, _rl, _tp;
@@ -5550,7 +5961,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5550
5961
  if (e.upload === "header-only" || e.cliVersion || !e.transcript) continue;
5551
5962
  if (now - Date.parse(e.endedAt || e.startedAt) > days * 864e5) continue;
5552
5963
  const file = e.transcriptPath ?? v2.readSession(e.sessionId)?.transcriptPath;
5553
- if (!file || !fs13.existsSync(file)) {
5964
+ if (!file || !fs14.existsSync(file)) {
5554
5965
  missing++;
5555
5966
  continue;
5556
5967
  }
@@ -5565,7 +5976,7 @@ function markForRetrace(v2, { days = 30, now = Date.now() } = {}) {
5565
5976
  async function syncV2(store, opts = {}) {
5566
5977
  const v2 = new V2Store(store);
5567
5978
  const config = store.config();
5568
- const cwd = opts.cwd ?? path9.dirname(store.root);
5979
+ const cwd = opts.cwd ?? path10.dirname(store.root);
5569
5980
  const report = { offline: !canCall(config), uploadedEpisodes: 0, deferredTraces: 0, uploadedSuggestions: 0, trialUpdates: 0, bundle: v2.readBundle() ? "cached" : "none", settled: [], errors: [] };
5570
5981
  const deadline = Date.now() + (opts.budgetMs ?? 24e4);
5571
5982
  if (report.offline) {
@@ -5681,7 +6092,8 @@ async function syncV2(store, opts = {}) {
5681
6092
  const bundle = v2.readBundle();
5682
6093
  if (bundle) {
5683
6094
  try {
5684
- report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false });
6095
+ const linkRoot = cwd && cwdInPool(config, cwd, store.root) ? projectRootFor(cwd, store.root) : void 0;
6096
+ report.deploy = deployLibrary(v2, config, ctx, bundle, { capture: false, ...linkRoot ? { linkRoot } : {}, linkHarnesses: linkHarnesses(config) });
5685
6097
  } catch (err) {
5686
6098
  report.errors.push(`deploy: ${err instanceof Error ? err.message : String(err)}`);
5687
6099
  }
@@ -5711,7 +6123,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
5711
6123
  const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
5712
6124
  const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
5713
6125
  try {
5714
- return [{ path: rel, text: fs13.readFileSync(path9.join(root, rel), "utf8") }];
6126
+ return [{ path: rel, text: fs14.readFileSync(path10.join(root, rel), "utf8") }];
5715
6127
  } catch {
5716
6128
  return [];
5717
6129
  }
@@ -5729,6 +6141,7 @@ var init_sync = __esm({
5729
6141
  init_api();
5730
6142
  init_apply();
5731
6143
  init_deploy();
6144
+ init_source();
5732
6145
  init_repo();
5733
6146
  init_store2();
5734
6147
  init_view();
@@ -5739,10 +6152,10 @@ var init_sync = __esm({
5739
6152
 
5740
6153
  // src/v2/reflect.ts
5741
6154
  import { spawn as spawn3 } from "node:child_process";
5742
- import { createHash as createHash3 } from "node:crypto";
5743
- import fs14 from "node:fs";
6155
+ import { createHash as createHash4 } from "node:crypto";
6156
+ import fs15 from "node:fs";
5744
6157
  function episodeIdFor(contextId, sessionId) {
5745
- return `e_${createHash3("sha256").update(`${contextId}
6158
+ return `e_${createHash4("sha256").update(`${contextId}
5746
6159
  ${sessionId}`).digest("hex").slice(0, 16)}`;
5747
6160
  }
5748
6161
  function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT_MS } = {}) {
@@ -5775,7 +6188,7 @@ function runClaudeFork(sessionId, prompt, { cwd, model, timeoutMs = FORK_TIMEOUT
5775
6188
  };
5776
6189
  let child;
5777
6190
  try {
5778
- child = spawn3(bin, args, { cwd: cwd && fs14.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
6191
+ child = spawn3(bin, args, { cwd: cwd && fs15.existsSync(cwd) ? cwd : void 0, env: modelEnv("claude-cli"), stdio: ["ignore", "pipe", "pipe"] });
5779
6192
  } catch (e) {
5780
6193
  resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
5781
6194
  return;
@@ -5820,7 +6233,7 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
5820
6233
  return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
5821
6234
  }
5822
6235
  function humanTurnsOf(transcriptPath) {
5823
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return [];
6236
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return [];
5824
6237
  try {
5825
6238
  return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
5826
6239
  } catch {
@@ -5828,7 +6241,7 @@ function humanTurnsOf(transcriptPath) {
5828
6241
  }
5829
6242
  }
5830
6243
  function readFacts(transcriptPath) {
5831
- if (!transcriptPath || !fs14.existsSync(transcriptPath)) return null;
6244
+ if (!transcriptPath || !fs15.existsSync(transcriptPath)) return null;
5832
6245
  try {
5833
6246
  return parseTranscript(transcriptPath);
5834
6247
  } catch {
@@ -5855,7 +6268,7 @@ async function runReflectionV2(store, opts) {
5855
6268
  if (!release) return { ...result, skipped: "a reflection of this session is already running" };
5856
6269
  try {
5857
6270
  const session = v2.session(opts.sessionId, opts.cwd ?? process.cwd());
5858
- const view = localView(v2, config, session.cwd || opts.cwd || process.cwd());
6271
+ const view = localView(v2, config, session.cwd || opts.cwd || process.cwd(), session.source !== void 0 ? { source: session.source } : {});
5859
6272
  const origin = opts.origin ?? (opts.focus !== void 0 ? "learn" : "session-end");
5860
6273
  const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
5861
6274
  const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
@@ -5872,7 +6285,7 @@ async function runReflectionV2(store, opts) {
5872
6285
  const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
5873
6286
  const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
5874
6287
  const guidelines = reflectionGuidelines(view.sections, view.reader);
5875
- const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
6288
+ const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, scopes: scopeChoices(view.bundle?.scopes, view.source), ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
5876
6289
  let raw = null;
5877
6290
  if (harness === "claude-code") {
5878
6291
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
@@ -5891,7 +6304,7 @@ async function runReflectionV2(store, opts) {
5891
6304
  result.method = "fork";
5892
6305
  } else result.error = fork.error;
5893
6306
  }
5894
- if (result.method === "none" && session.transcriptPath && fs14.existsSync(session.transcriptPath)) {
6307
+ if (result.method === "none" && session.transcriptPath && fs15.existsSync(session.transcriptPath)) {
5895
6308
  const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
5896
6309
  const res = await askJson(config, {
5897
6310
  system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
@@ -5912,8 +6325,10 @@ async function runReflectionV2(store, opts) {
5912
6325
  result.nothingNew = result.method !== "none" && parsed.length === 0;
5913
6326
  const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
5914
6327
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6328
+ const source = view.source;
5915
6329
  for (const p of parsed) {
5916
6330
  const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
6331
+ if (source) s.source = source;
5917
6332
  s.summary = summarize(s.op);
5918
6333
  if (apply) {
5919
6334
  const outcome = applyLocally(v2, view, s);
@@ -6014,51 +6429,51 @@ __export(uicmd_exports, {
6014
6429
  modSource: () => modSource
6015
6430
  });
6016
6431
  import { spawnSync } from "node:child_process";
6017
- import fs15 from "node:fs";
6432
+ import fs16 from "node:fs";
6018
6433
  import os3 from "node:os";
6019
- import path10 from "node:path";
6434
+ import path11 from "node:path";
6020
6435
  import { fileURLToPath } from "node:url";
6021
6436
  function modHome(home = process.env.BUNSHIN_DEPLOY_HOME || os3.homedir()) {
6022
- return path10.join(home, ".bunshin", "claude-code-ui");
6437
+ return path11.join(home, ".bunshin", "claude-code-ui");
6023
6438
  }
6024
6439
  function modSource() {
6025
- const here = path10.dirname(fileURLToPath(import.meta.url));
6026
- for (const dir of [path10.join(here, "claude-code-ui"), path10.join(here, "..", "claude-code-ui"), path10.resolve(here, "../../../../adapters/claude-code-ui")]) {
6027
- if (fs15.existsSync(path10.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6440
+ const here = path11.dirname(fileURLToPath(import.meta.url));
6441
+ for (const dir of [path11.join(here, "claude-code-ui"), path11.join(here, "..", "claude-code-ui"), path11.resolve(here, "../../../../adapters/claude-code-ui")]) {
6442
+ if (fs16.existsSync(path11.join(dir, ".claude-plugin", "plugin.json"))) return dir;
6028
6443
  }
6029
6444
  return null;
6030
6445
  }
6031
6446
  function installedModVersion(home) {
6032
6447
  try {
6033
- return JSON.parse(fs15.readFileSync(path10.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6448
+ return JSON.parse(fs16.readFileSync(path11.join(modHome(home), MOD_PLUGIN, ".claude-plugin", "plugin.json"), "utf8")).version ?? null;
6034
6449
  } catch {
6035
6450
  return null;
6036
6451
  }
6037
6452
  }
6038
6453
  function copyDir(from, to) {
6039
- fs15.mkdirSync(to, { recursive: true });
6040
- for (const e of fs15.readdirSync(from, { withFileTypes: true })) {
6454
+ fs16.mkdirSync(to, { recursive: true });
6455
+ for (const e of fs16.readdirSync(from, { withFileTypes: true })) {
6041
6456
  if (e.name === "types" && from.endsWith(".claude-plugin")) continue;
6042
6457
  if (e.name === "node_modules") continue;
6043
- const a = path10.join(from, e.name);
6044
- const b = path10.join(to, e.name);
6458
+ const a = path11.join(from, e.name);
6459
+ const b = path11.join(to, e.name);
6045
6460
  if (e.isDirectory()) copyDir(a, b);
6046
- else fs15.copyFileSync(a, b);
6461
+ else fs16.copyFileSync(a, b);
6047
6462
  }
6048
6463
  }
6049
6464
  function installMod(version2, opts = {}) {
6050
6465
  const src = modSource();
6051
6466
  if (!src) throw new Error("the bunshin-ui mod is not in this build of the CLI");
6052
6467
  const home = modHome(opts.home);
6053
- const dir = path10.join(home, MOD_PLUGIN);
6054
- fs15.rmSync(dir, { recursive: true, force: true });
6468
+ const dir = path11.join(home, MOD_PLUGIN);
6469
+ fs16.rmSync(dir, { recursive: true, force: true });
6055
6470
  copyDir(src, dir);
6056
- const manifest = path10.join(dir, ".claude-plugin", "plugin.json");
6057
- const m = JSON.parse(fs15.readFileSync(manifest, "utf8"));
6058
- fs15.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6059
- fs15.mkdirSync(path10.join(home, ".claude-plugin"), { recursive: true });
6060
- fs15.writeFileSync(
6061
- path10.join(home, ".claude-plugin", "marketplace.json"),
6471
+ const manifest = path11.join(dir, ".claude-plugin", "plugin.json");
6472
+ const m = JSON.parse(fs16.readFileSync(manifest, "utf8"));
6473
+ fs16.writeFileSync(manifest, JSON.stringify({ ...m, version: version2 }, null, 2) + "\n");
6474
+ fs16.mkdirSync(path11.join(home, ".claude-plugin"), { recursive: true });
6475
+ fs16.writeFileSync(
6476
+ path11.join(home, ".claude-plugin", "marketplace.json"),
6062
6477
  JSON.stringify({ name: MOD_MARKETPLACE, owner: { name: "Bunshin" }, plugins: [{ name: MOD_PLUGIN, source: `./${MOD_PLUGIN}`, description: m.description }] }, null, 2) + "\n"
6063
6478
  );
6064
6479
  const run2 = opts.run ?? spawnSync;
@@ -6093,7 +6508,7 @@ async function cmdUi(store, args, out, err, version2) {
6093
6508
  `);
6094
6509
  for (const s of res.steps) out(` ${s.ok ? "\u2713" : "\xB7"} ${s.command}${s.ok ? "" : `: ${s.output.split("\n")[0] ?? ""}`}
6095
6510
  `);
6096
- out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path10.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6511
+ out(installed ? "The mod loads in new Claude Code sessions (and after /reload-plugins): a Bunshin band above the prompt, /bunshin, toasts.\nUntil it runs, Bunshin prints one short line per event instead.\n" : "Could not install with `claude plugin`; install it yourself:\n claude plugin marketplace add " + path11.dirname(res.dir) + "\n claude plugin install bunshin-ui@bunshin-local --scope user\n");
6097
6512
  }
6098
6513
  return installed || !failed.length ? 0 : 1;
6099
6514
  }
@@ -6174,19 +6589,19 @@ __export(update_exports, {
6174
6589
  updateCheckDue: () => updateCheckDue,
6175
6590
  updateNotice: () => updateNotice
6176
6591
  });
6177
- import fs16 from "node:fs";
6178
- import path11 from "node:path";
6592
+ import fs17 from "node:fs";
6593
+ import path12 from "node:path";
6179
6594
  function readState2() {
6180
6595
  try {
6181
- return JSON.parse(fs16.readFileSync(stateFile(), "utf8"));
6596
+ return JSON.parse(fs17.readFileSync(stateFile(), "utf8"));
6182
6597
  } catch {
6183
6598
  return {};
6184
6599
  }
6185
6600
  }
6186
6601
  function writeState(s) {
6187
6602
  try {
6188
- fs16.mkdirSync(path11.dirname(stateFile()), { recursive: true });
6189
- fs16.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6603
+ fs17.mkdirSync(path12.dirname(stateFile()), { recursive: true });
6604
+ fs17.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + "\n");
6190
6605
  } catch {
6191
6606
  }
6192
6607
  }
@@ -6236,14 +6651,14 @@ var init_update = __esm({
6236
6651
  PKG = "@bunshin-cc/bunshin";
6237
6652
  LATEST_URL = `https://registry.npmjs.org/${PKG.replace("/", "%2f")}/latest`;
6238
6653
  DAY = 864e5;
6239
- stateFile = () => path11.join(homeDir(), ".bunshin", "update.json");
6654
+ stateFile = () => path12.join(homeDir(), ".bunshin", "update.json");
6240
6655
  disabled = () => process.env.BUNSHIN_NO_UPDATE_CHECK === "1" || process.env.BUNSHIN_NO_UPDATE_CHECK === "true";
6241
6656
  }
6242
6657
  });
6243
6658
 
6244
6659
  // src/v2/hooks.ts
6245
- import fs17 from "node:fs";
6246
- import path12 from "node:path";
6660
+ import fs18 from "node:fs";
6661
+ import path13 from "node:path";
6247
6662
  function inContext(s) {
6248
6663
  return s.inContext ?? [...s.loaded, ...s.pushed];
6249
6664
  }
@@ -6258,6 +6673,12 @@ function touch(v2, ctx, patch = () => {
6258
6673
  const s = v2.updateSession(id, ctx.cwd, (s2) => {
6259
6674
  s2.cwd = ctx.cwd || s2.cwd;
6260
6675
  s2.harness = ctx.store.config().harness;
6676
+ if (s2.source === void 0) {
6677
+ try {
6678
+ s2.source = sourceFor(s2.cwd, ctx.store.root, contextFor(v2, ctx.store.config())) ?? "";
6679
+ } catch {
6680
+ }
6681
+ }
6261
6682
  if (ctx.input.transcript_path) s2.transcriptPath = ctx.input.transcript_path;
6262
6683
  patch(s2);
6263
6684
  });
@@ -6304,10 +6725,10 @@ async function keepModCurrent(ctx, config) {
6304
6725
  const { version: version2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
6305
6726
  const current = version2();
6306
6727
  if (!modSource2() || installedModVersion2() === current) return;
6307
- const marker = path12.join(modHome2(), `.attempted-${current}`);
6308
- if (fs17.existsSync(marker)) return;
6309
- fs17.mkdirSync(modHome2(), { recursive: true });
6310
- fs17.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6728
+ const marker = path13.join(modHome2(), `.attempted-${current}`);
6729
+ if (fs18.existsSync(marker)) return;
6730
+ fs18.mkdirSync(modHome2(), { recursive: true });
6731
+ fs18.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
6311
6732
  ctx.detach(["--root", ctx.store.root, "ui", "install", "--quiet"], { cwd: ctx.cwd });
6312
6733
  } catch {
6313
6734
  }
@@ -6410,8 +6831,8 @@ function liveUpdate(v2, ctx, session) {
6410
6831
  return "";
6411
6832
  }
6412
6833
  const config = ctx.store.config();
6413
- const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
6414
- const paths = deployed ? deployPaths() : null;
6834
+ const deployed = deployEnabled(config, contextFor(v2, config));
6835
+ const paths = deployed ? deployPaths(process.env, config.orgId) : null;
6415
6836
  const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
6416
6837
  const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
6417
6838
  const { text: text2, next } = changesSince(session.live, bundle, skillPath);
@@ -6436,13 +6857,13 @@ function skillHint(sec, others) {
6436
6857
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6437
6858
  return `Bunshin: the \`${sec.skill.name}\` skill may apply: ${sec.skill.description.replace(/\s+/g, " ").trim()} If it does, invoke it (Skill tool, or \`/${sec.skill.name}\`) before doing this by hand.${also}`;
6438
6859
  }
6439
- async function remoteAuto(ctx, prompt, exclude) {
6860
+ async function remoteAuto(ctx, prompt, exclude, source) {
6440
6861
  const config = ctx.store.config();
6441
6862
  if (!canCall(config) || !config.recall.useMothership) return null;
6442
6863
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6443
6864
  if (budget < 300) return null;
6444
6865
  try {
6445
- const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude }, { timeoutMs: budget });
6866
+ const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
6446
6867
  return res.available ? res : null;
6447
6868
  } catch {
6448
6869
  return null;
@@ -6477,7 +6898,7 @@ async function onPrompt(ctx) {
6477
6898
  };
6478
6899
  if (!prompt.trim()) return withLive({});
6479
6900
  const config = ctx.store.config();
6480
- const view = localView(v2, config, ctx.cwd);
6901
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6481
6902
  const injected = inContext(session);
6482
6903
  const named = invokedSkill(prompt, view.sections);
6483
6904
  if (named) {
@@ -6497,7 +6918,7 @@ async function onPrompt(ctx) {
6497
6918
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6498
6919
  }
6499
6920
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6500
- const remote = await remoteAuto(ctx, prompt, injected);
6921
+ const remote = await remoteAuto(ctx, prompt, injected, session.source);
6501
6922
  if (remote) {
6502
6923
  const local = (id) => view.sections.find((s) => s.id === id);
6503
6924
  if (remote.section) {
@@ -6565,7 +6986,7 @@ function onPreToolUse(ctx) {
6565
6986
  const raw = toolPaths(ctx.input.tool_input);
6566
6987
  if (!raw.length) return withLive({});
6567
6988
  const config = ctx.store.config();
6568
- const view = localView(v2, config, ctx.cwd);
6989
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6569
6990
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
6570
6991
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
6571
6992
  if (!paths.length && !text2) return withLive({});
@@ -6649,6 +7070,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6649
7070
  const v2 = new V2Store(store);
6650
7071
  const config = store.config();
6651
7072
  const view = localView(v2, config, session.cwd, { index: false });
7073
+ const source = sessionSource(store, session, view.ctx);
6652
7074
  const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
6653
7075
  const outcome = v2.readOutcome(session.sessionId);
6654
7076
  const turns = scan.facts.turns.length;
@@ -6688,6 +7110,8 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6688
7110
  ...outcome ? { outcome } : {},
6689
7111
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
6690
7112
  cliVersion: version(),
7113
+ ...source ? { source } : {},
7114
+ ...session.borrowed?.length ? { borrowed: session.borrowed } : {},
6691
7115
  uploaded: false,
6692
7116
  ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
6693
7117
  ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
@@ -6695,6 +7119,14 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6695
7119
  };
6696
7120
  return { episode, gate, scan };
6697
7121
  }
7122
+ function sessionSource(store, session, ctx) {
7123
+ if (session.source !== void 0) return session.source || void 0;
7124
+ try {
7125
+ return sourceFor(session.cwd, store.root, ctx);
7126
+ } catch {
7127
+ return void 0;
7128
+ }
7129
+ }
6698
7130
  function checkpointDue(session, scan, now) {
6699
7131
  const since = Date.parse(session.checkpointAt ?? session.startedAt);
6700
7132
  if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
@@ -6809,6 +7241,7 @@ var init_hooks = __esm({
6809
7241
  init_signals();
6810
7242
  init_store2();
6811
7243
  init_ui();
7244
+ init_source();
6812
7245
  init_view();
6813
7246
  NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
6814
7247
  NUDGE_INTERVAL_MS = 60 * 6e4;
@@ -6823,6 +7256,8 @@ var init_hooks = __esm({
6823
7256
  });
6824
7257
 
6825
7258
  // src/hooks.ts
7259
+ import fs19 from "node:fs";
7260
+ import path14 from "node:path";
6826
7261
  function normalizeEvent(raw) {
6827
7262
  return EVENT_NAMES[String(raw ?? "").trim().toLowerCase().replace(/_/g, "-")] ?? null;
6828
7263
  }
@@ -6866,6 +7301,8 @@ async function handleHook(rawEvent, input, opts = {}) {
6866
7301
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
6867
7302
  if (!store) return {};
6868
7303
  const config = store.config();
7304
+ if (!cwdInPool(config, cwd, store.root)) return {};
7305
+ if (deferToProject(cwd)) return {};
6869
7306
  const budgetMs = Math.min(opts.budgetMs ?? config.recall.hookBudgetMs ?? HARD_BUDGET_MS, HARD_BUDGET_MS);
6870
7307
  const deadline = Date.now() + budgetMs;
6871
7308
  try {
@@ -6886,6 +7323,24 @@ async function handleHook(rawEvent, input, opts = {}) {
6886
7323
  return {};
6887
7324
  }
6888
7325
  }
7326
+ function deferToProject(cwd, env = process.env) {
7327
+ if (env.BUNSHIN_HOOK_SCOPE !== "user") return false;
7328
+ let dir = path14.resolve(cwd);
7329
+ for (let i = 0; i < 40; i++) {
7330
+ for (const f of [".claude/settings.local.json", ".claude/settings.json", ".codex/hooks.json"]) {
7331
+ try {
7332
+ const text2 = fs19.readFileSync(path14.join(dir, f), "utf8");
7333
+ if (/\bbunshin\b[^"]*\bhook\b/.test(text2) && !/BUNSHIN_HOOK_SCOPE=user/.test(text2)) return true;
7334
+ } catch {
7335
+ }
7336
+ }
7337
+ if (fs19.existsSync(path14.join(dir, ".git"))) return false;
7338
+ const up = path14.dirname(dir);
7339
+ if (up === dir) return false;
7340
+ dir = up;
7341
+ }
7342
+ return false;
7343
+ }
6889
7344
  async function hookCommand(rawEvent, opts = {}) {
6890
7345
  const write = opts.write ?? ((s) => process.stdout.write(s));
6891
7346
  let output = {};
@@ -6908,6 +7363,7 @@ var init_hooks2 = __esm({
6908
7363
  init_detach();
6909
7364
  init_store();
6910
7365
  init_hooks();
7366
+ init_source();
6911
7367
  HARD_BUDGET_MS = 4e3;
6912
7368
  EVENT_NAMES = {
6913
7369
  "session-start": "session-start",
@@ -6927,32 +7383,32 @@ var init_hooks2 = __esm({
6927
7383
  });
6928
7384
 
6929
7385
  // src/project-hooks.ts
6930
- import fs18 from "node:fs";
6931
- import path13 from "node:path";
6932
- import { execFileSync as execFileSync2 } from "node:child_process";
7386
+ import fs20 from "node:fs";
7387
+ import path15 from "node:path";
7388
+ import { execFileSync as execFileSync4 } from "node:child_process";
6933
7389
  function codexProjectHooks(cwd) {
6934
- const legacyFile = path13.join(cwd, ".codex", "hooks.json");
6935
- const git4 = (...args) => execFileSync2("git", ["-C", cwd, "rev-parse", ...args], {
7390
+ const legacyFile = path15.join(cwd, ".codex", "hooks.json");
7391
+ const git5 = (...args) => execFileSync4("git", ["-C", cwd, "rev-parse", ...args], {
6936
7392
  encoding: "utf8",
6937
7393
  stdio: ["ignore", "pipe", "ignore"],
6938
7394
  timeout: 2e3
6939
7395
  }).trim();
6940
7396
  let root, common, directory;
6941
7397
  try {
6942
- root = git4("--show-toplevel");
6943
- common = git4("--path-format=absolute", "--git-common-dir");
6944
- directory = git4("--absolute-git-dir");
7398
+ root = git5("--show-toplevel");
7399
+ common = git5("--path-format=absolute", "--git-common-dir");
7400
+ directory = git5("--absolute-git-dir");
6945
7401
  } catch {
6946
7402
  return { file: legacyFile, shared: false, legacyFile };
6947
7403
  }
6948
- const linked = fs18.realpathSync(common) !== fs18.realpathSync(directory);
7404
+ const linked = fs20.realpathSync(common) !== fs20.realpathSync(directory);
6949
7405
  if (linked) {
6950
- if (path13.basename(common) !== ".git" || !fs18.statSync(common).isDirectory()) {
7406
+ if (path15.basename(common) !== ".git" || !fs20.statSync(common).isDirectory()) {
6951
7407
  throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
6952
7408
  }
6953
- root = path13.dirname(common);
7409
+ root = path15.dirname(common);
6954
7410
  }
6955
- const file = fs18.realpathSync(root) === fs18.realpathSync(cwd) ? legacyFile : path13.join(root, ".codex", "hooks.json");
7411
+ const file = fs20.realpathSync(root) === fs20.realpathSync(cwd) ? legacyFile : path15.join(root, ".codex", "hooks.json");
6956
7412
  return { file, shared: linked, legacyFile };
6957
7413
  }
6958
7414
  var init_project_hooks = __esm({
@@ -6964,6 +7420,7 @@ var init_project_hooks = __esm({
6964
7420
  // src/init.ts
6965
7421
  var init_exports = {};
6966
7422
  __export(init_exports, {
7423
+ BORROW_COMMAND: () => BORROW_COMMAND,
6967
7424
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
6968
7425
  HOOK_EVENTS: () => HOOK_EVENTS,
6969
7426
  LEARN_COMMAND: () => LEARN_COMMAND,
@@ -6975,6 +7432,7 @@ __export(init_exports, {
6975
7432
  installLearnCommand: () => installLearnCommand,
6976
7433
  installMcpConfig: () => installMcpConfig,
6977
7434
  installPiExtension: () => installPiExtension,
7435
+ installUserMcp: () => installUserMcp,
6978
7436
  isBunshinEntry: () => isBunshinEntry,
6979
7437
  mcpLauncher: () => mcpLauncher,
6980
7438
  mergeClaudeSettings: () => mergeClaudeSettings,
@@ -6982,8 +7440,9 @@ __export(init_exports, {
6982
7440
  removeBunshinHooks: () => removeBunshinHooks,
6983
7441
  runInit: () => runInit
6984
7442
  });
6985
- import fs19 from "node:fs";
6986
- import path14 from "node:path";
7443
+ import { execFileSync as execFileSync5 } from "node:child_process";
7444
+ import fs21 from "node:fs";
7445
+ import path16 from "node:path";
6987
7446
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
6988
7447
  function isBunshinEntry(entry) {
6989
7448
  return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
@@ -7008,13 +7467,13 @@ function removeBunshinHooks(settings) {
7008
7467
  else delete out.hooks;
7009
7468
  return out;
7010
7469
  }
7011
- function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code" } = {}) {
7470
+ function mergeClaudeSettings(settings, { bin = "bunshin", harness = "claude-code", user = false } = {}) {
7012
7471
  const base = removeBunshinHooks(settings);
7013
7472
  const hooks = { ...base.hooks ?? {} };
7014
7473
  for (const { event, arg, timeout, matcher } of harness === "codex" ? CODEX_HOOK_EVENTS : HOOK_EVENTS) {
7015
7474
  const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
7016
7475
  const hook = bunshinHookEntry(arg, timeout, bin);
7017
- hook.command = `BUNSHIN_HARNESS=${harness} ${hook.command}`;
7476
+ hook.command = `${user ? "BUNSHIN_HOOK_SCOPE=user " : ""}BUNSHIN_HARNESS=${harness} ${hook.command}`;
7018
7477
  if (harness === "codex") delete hook._bunshin;
7019
7478
  groups.push(matcher ? { matcher, hooks: [hook] } : { hooks: [hook] });
7020
7479
  hooks[event] = groups;
@@ -7028,16 +7487,16 @@ function countBunshinHooks(settings) {
7028
7487
  );
7029
7488
  }
7030
7489
  function claudeSettingsPath() {
7031
- return path14.join(homeDir(), ".claude", "settings.json");
7490
+ return path16.join(homeDir(), ".claude", "settings.json");
7032
7491
  }
7033
7492
  function installPiExtension(cwd, remove = false, scope = "repo") {
7034
- const file = scope === "global" ? path14.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path14.join(cwd, ".pi/extensions/bunshin.js");
7493
+ const file = scope === "global" ? path16.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path16.join(cwd, ".pi/extensions/bunshin.js");
7035
7494
  const marker = "// Managed by Bunshin setup.";
7036
7495
  try {
7037
- const previous = fileExists(file) ? fs19.readFileSync(file, "utf8") : "";
7496
+ const previous = fileExists(file) ? fs21.readFileSync(file, "utf8") : "";
7038
7497
  if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
7039
7498
  if (remove) {
7040
- if (previous) fs19.unlinkSync(file);
7499
+ if (previous) fs21.unlinkSync(file);
7041
7500
  return { file, installed: 0, changed: !!previous };
7042
7501
  }
7043
7502
  const extension = fileURLToPath2(new URL("./pi-extension.js", import.meta.url));
@@ -7046,24 +7505,24 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
7046
7505
  export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
7047
7506
  `;
7048
7507
  if (text2 !== previous) {
7049
- ensureDir(path14.dirname(file));
7050
- fs19.writeFileSync(file, text2);
7508
+ ensureDir(path16.dirname(file));
7509
+ fs21.writeFileSync(file, text2);
7051
7510
  }
7052
7511
  return { file, installed: 1, changed: text2 !== previous };
7053
7512
  } catch (error) {
7054
7513
  return { file, installed: 0, changed: false, error: error instanceof Error ? error.message : String(error) };
7055
7514
  }
7056
7515
  }
7057
- function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
7516
+ function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code", user = false } = {}) {
7058
7517
  try {
7059
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7060
- const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
7518
+ const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7519
+ const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness, user });
7061
7520
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7062
7521
  if (changed) {
7063
- ensureDir(path14.dirname(file));
7522
+ ensureDir(path16.dirname(file));
7064
7523
  if (fileExists(file)) {
7065
7524
  try {
7066
- fs19.copyFileSync(file, `${file}.bunshin-backup`);
7525
+ fs21.copyFileSync(file, `${file}.bunshin-backup`);
7067
7526
  } catch {
7068
7527
  }
7069
7528
  }
@@ -7078,23 +7537,23 @@ function mcpLauncher(bin) {
7078
7537
  if (!bin || /^[\w./@+-]+$/.test(bin)) return { command: bin || "bunshin", args: ["mcp"] };
7079
7538
  let script = process.argv[1] ?? "";
7080
7539
  try {
7081
- script = fs19.realpathSync(script);
7540
+ script = fs21.realpathSync(script);
7082
7541
  } catch {
7083
7542
  }
7084
7543
  return { command: process.execPath, args: [script, "mcp"] };
7085
7544
  }
7086
- function installLearnCommand({ file, remove = false }) {
7545
+ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7087
7546
  try {
7088
- const current = fileExists(file) ? fs19.readFileSync(file, "utf8") : null;
7089
- const ours = current === null || current === LEARN_COMMAND;
7547
+ const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7548
+ const ours = current === null || current === content;
7090
7549
  if (remove) {
7091
- if (current !== null && ours) fs19.unlinkSync(file);
7550
+ if (current !== null && ours) fs21.unlinkSync(file);
7092
7551
  return { file, installed: 0, changed: current !== null && ours };
7093
7552
  }
7094
- if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
7553
+ if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
7095
7554
  if (current === null) {
7096
- fs19.mkdirSync(path14.dirname(file), { recursive: true });
7097
- fs19.writeFileSync(file, LEARN_COMMAND);
7555
+ fs21.mkdirSync(path16.dirname(file), { recursive: true });
7556
+ fs21.writeFileSync(file, content);
7098
7557
  }
7099
7558
  return { file, installed: 1, changed: current === null };
7100
7559
  } catch (err) {
@@ -7103,7 +7562,7 @@ function installLearnCommand({ file, remove = false }) {
7103
7562
  }
7104
7563
  function installMcpConfig({ file, launcher, remove = false }) {
7105
7564
  try {
7106
- const before = fileExists(file) ? JSON.parse(fs19.readFileSync(file, "utf8")) : {};
7565
+ const before = fileExists(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : {};
7107
7566
  const servers = { ...before.mcpServers ?? {} };
7108
7567
  if (remove) delete servers.bunshin;
7109
7568
  else servers.bunshin = { type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } };
@@ -7111,7 +7570,7 @@ function installMcpConfig({ file, launcher, remove = false }) {
7111
7570
  if (!Object.keys(servers).length) delete after.mcpServers;
7112
7571
  const changed = JSON.stringify(before) !== JSON.stringify(after);
7113
7572
  if (changed) {
7114
- if (!Object.keys(after).length && fileExists(file)) fs19.unlinkSync(file);
7573
+ if (!Object.keys(after).length && fileExists(file)) fs21.unlinkSync(file);
7115
7574
  else if (Object.keys(after).length) writeJsonAtomic(file, after);
7116
7575
  }
7117
7576
  return { file, installed: servers.bunshin ? 1 : 0, changed };
@@ -7119,25 +7578,49 @@ function installMcpConfig({ file, launcher, remove = false }) {
7119
7578
  return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
7120
7579
  }
7121
7580
  }
7581
+ function installUserMcp({ launcher, remove = false, claude = "claude" }) {
7582
+ const file = path16.join(homeDir(), ".claude.json");
7583
+ const run2 = (args) => execFileSync5(claude, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 2e4, env: { ...process.env, BUNSHIN_CHILD: "1" } }).toString();
7584
+ try {
7585
+ let had = false;
7586
+ try {
7587
+ run2(["mcp", "get", "bunshin"]);
7588
+ had = true;
7589
+ } catch {
7590
+ }
7591
+ if (had) {
7592
+ try {
7593
+ run2(["mcp", "remove", "bunshin", "-s", "user"]);
7594
+ } catch {
7595
+ }
7596
+ }
7597
+ if (remove) return { file, installed: 0, changed: had };
7598
+ run2(["mcp", "add-json", "bunshin", JSON.stringify({ type: "stdio", command: launcher.command, args: launcher.args, env: { BUNSHIN_HARNESS: "claude-code" } }), "-s", "user"]);
7599
+ return { file, installed: 1, changed: true };
7600
+ } catch (err) {
7601
+ return { file, installed: 0, changed: false, error: err instanceof Error ? err.message.split("\n")[0] : String(err) };
7602
+ }
7603
+ }
7122
7604
  function runInit(opts = {}) {
7123
7605
  const cwd = opts.cwd ?? process.cwd();
7124
7606
  const warnings = [];
7125
7607
  const scope = opts.scope ?? "repo";
7126
- const root = scope === "global" ? path14.join(homeDir(), ".bunshin") : path14.join(cwd, ".bunshin");
7127
- if (fs19.existsSync(root) && !new Store(root).trusted && fs19.readdirSync(root).length) {
7608
+ const root = scope === "global" ? path16.join(homeDir(), ".bunshin") : path16.join(cwd, ".bunshin");
7609
+ if (fs21.existsSync(root) && !new Store(root).trusted && fs21.readdirSync(root).length) {
7128
7610
  const aside = `${root}.untrusted-${Date.now()}`;
7129
- fs19.renameSync(root, aside);
7611
+ fs21.renameSync(root, aside);
7130
7612
  warnings.push(`moved ${root} aside to ${aside}: this user did not create it`);
7131
7613
  }
7132
- const existed = fileExists(path14.join(root, "config.json"));
7614
+ const existed = fileExists(path16.join(root, "config.json"));
7133
7615
  const store = new Store(root);
7134
7616
  store.ensureLayout();
7135
- fs19.chmodSync(root, 448);
7136
- if (scope === "repo") {
7137
- const ignore = path14.join(cwd, ".gitignore");
7138
- const current = fileExists(ignore) ? fs19.readFileSync(ignore, "utf8") : "";
7617
+ fs21.chmodSync(root, 448);
7618
+ const hookScope = opts.hooks ?? (scope === "global" ? "user" : (existed && store.trusted ? store.config().hooks : void 0) ?? "project");
7619
+ if (scope === "repo" && (hookScope === "project" || fileExists(path16.join(cwd, ".git")))) {
7620
+ const ignore = path16.join(cwd, ".gitignore");
7621
+ const current = fileExists(ignore) ? fs21.readFileSync(ignore, "utf8") : "";
7139
7622
  if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
7140
- fs19.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7623
+ fs21.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
7141
7624
  `);
7142
7625
  }
7143
7626
  }
@@ -7145,6 +7628,7 @@ function runInit(opts = {}) {
7145
7628
  const config = {
7146
7629
  ...previous,
7147
7630
  scope,
7631
+ hooks: hookScope,
7148
7632
  orgId: opts.orgId ?? previous.orgId,
7149
7633
  userId: opts.userId ?? previous.userId,
7150
7634
  role: opts.role ?? previous.role,
@@ -7161,18 +7645,22 @@ function runInit(opts = {}) {
7161
7645
  if (!fileExists(store.indexFile)) store.rebuildIndex();
7162
7646
  let hooks;
7163
7647
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "pi") {
7164
- hooks = installPiExtension(cwd, opts.removeHooks, scope);
7648
+ hooks = installPiExtension(cwd, opts.removeHooks, hookScope === "user" ? "global" : "repo");
7165
7649
  if (hooks.error) warnings.push(hooks.error);
7166
7650
  }
7167
7651
  if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
7168
- const projectHooks = config.harness === "codex" && scope === "repo" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7169
- const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path14.join(process.env.CODEX_HOME ?? path14.join(homeDir(), ".codex"), "hooks.json") : scope === "repo" ? path14.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
7170
- hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness });
7652
+ const projectHooks = config.harness === "codex" && hookScope === "project" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
7653
+ 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());
7654
+ hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness, user: hookScope === "user" && !opts.settingsFile });
7655
+ if (!opts.removeHooks && !opts.settingsFile && hookScope === "user" && config.harness === "claude-code") {
7656
+ const local = path16.join(cwd, ".claude", "settings.local.json");
7657
+ if (fileExists(local) && path16.resolve(local) !== path16.resolve(file)) installClaudeHooks({ file: local, remove: true });
7658
+ }
7171
7659
  if (projectHooks?.shared) {
7172
7660
  hooks.sharedWithWorktrees = true;
7173
- if (!hooks.error && !opts.removeHooks) ensureDir(path14.dirname(projectHooks.legacyFile));
7661
+ if (!hooks.error && !opts.removeHooks) ensureDir(path16.dirname(projectHooks.legacyFile));
7174
7662
  }
7175
- if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs19.realpathSync(projectHooks.file) !== fs19.realpathSync(projectHooks.legacyFile)) {
7663
+ if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs21.realpathSync(projectHooks.file) !== fs21.realpathSync(projectHooks.legacyFile)) {
7176
7664
  const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
7177
7665
  if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
7178
7666
  }
@@ -7180,17 +7668,27 @@ function runInit(opts = {}) {
7180
7668
  }
7181
7669
  let mcp;
7182
7670
  let learnCommand;
7183
- if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code" && scope === "repo") {
7184
- mcp = installMcpConfig({ file: path14.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
7671
+ if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code") {
7672
+ const user = hookScope === "user";
7673
+ mcp = user ? installUserMcp({ launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks }) : installMcpConfig({ file: path16.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
7185
7674
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
7186
- learnCommand = installLearnCommand({ file: path14.join(cwd, ".claude", "commands", "learn.md"), remove: opts.removeHooks });
7675
+ learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7187
7676
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7188
7677
  }
7678
+ let borrowCommand;
7679
+ if (opts.removeHooks || !opts.noHooks) {
7680
+ const user = hookScope === "user";
7681
+ const file = config.harness === "claude-code" ? path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path16.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path16.join(cwd, ".pi", "prompts", "borrow.md") : null;
7682
+ if (file) {
7683
+ borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
7684
+ if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
7685
+ }
7686
+ }
7189
7687
  if (!resolveBackend(config)) {
7190
7688
  warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
7191
7689
  }
7192
7690
  store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
7193
- return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, warnings };
7691
+ return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, ...borrowCommand ? { borrowCommand } : {}, warnings };
7194
7692
  }
7195
7693
  function otherHarnessInstructions() {
7196
7694
  return [
@@ -7206,7 +7704,7 @@ function otherHarnessInstructions() {
7206
7704
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7207
7705
  ].join("\n");
7208
7706
  }
7209
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
7707
+ var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
7210
7708
  var init_init = __esm({
7211
7709
  "src/init.ts"() {
7212
7710
  "use strict";
@@ -7232,6 +7730,7 @@ var init_init = __esm({
7232
7730
  { event: "SessionEnd", arg: "session-end", timeout: 3 }
7233
7731
  ];
7234
7732
  LEARN_COMMAND = '---\ndescription: Capture what this session learned into the Bunshin library now (applied locally as a trial, submitted for the nightly review)\nargument-hint: "[what to capture, e.g. \\"the ledger must be reconciled before closing\\"]"\nallowed-tools: Bash(bunshin learn:*)\n---\n\nCapture knowledge from this session into Bunshin.\n\nRun exactly this with the Bash tool (it takes up to two minutes; use a timeout of 180000 ms):\n\n```bash\nbunshin learn "$ARGUMENTS"\n```\n\n`bunshin learn` forks this conversation (`claude -p --resume <this session> --fork-session`),\nso the reflection sees everything you saw without re-sending the transcript. It applies what it\nfinds locally as a trial and queues it for the nightly review:\n\n- edits to this repo\'s SPEC.md / TESTING.md / SKILL.md land in the working tree (they show up\n in `git diff`);\n- knowledge with no repo home becomes a local overlay on the Bunshin library;\n- per-machine values stay private.\n\nRelay its output to the user as it is. Each line reads\n`captured: <summary> \u2192 <section or file#anchor>`. If it prints `nothing new to capture.`, say so\nin one line; that is the normal outcome. If it reports an error, show it and stop; do not retry\nin a loop, and do not edit the library files by hand.\n\nIf the user named nothing specific, run it without arguments. Do not continue with other work in\nthe same reply unless the user asked you to.\n';
7733
+ BORROW_COMMAND = '---\ndescription: Bring in a lesson or skill learned elsewhere in this workspace (another project or group) when you know it will help here\nargument-hint: "[what to look for, e.g. \\"the release checklist from the api repo\\"]"\nallowed-tools: Bash(bunshin recall:*), Bash(bunshin load:*)\n---\n\nThe person wants something learned outside this project\'s scope. Search the whole workspace for it\n(run with the Bash tool):\n\n```bash\nbunshin recall "$ARGUMENTS" --hypothetical "$ARGUMENTS" --across\n```\n\nShow the person the numbered results as printed (each says where it was learned). Load the one\nthey pick, or the one that plainly matches, with `bunshin load <id>`, then use it as prior\nknowledge for the task. Loading it records that it was borrowed into this project: the nightly\ngeneralization reads that as evidence the lesson applies beyond where it was learned.\n\nIf nothing matches, say so in one line and carry on.\n';
7235
7734
  }
7236
7735
  });
7237
7736
 
@@ -7253,19 +7752,19 @@ function mark(v2, sessionId, cwd, field, ids) {
7253
7752
  } catch {
7254
7753
  }
7255
7754
  }
7256
- async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3 }) {
7755
+ async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3, across = false }) {
7257
7756
  const v2 = new V2Store(store);
7258
7757
  const config = store.config();
7259
7758
  const sid = sessionId ?? sessionIdFromEnv(v2);
7260
7759
  const session = sid ? v2.readSession(sid) : null;
7261
7760
  const exclude = session ? [...session.loaded] : [];
7262
7761
  const limit = Math.max(1, Math.min(10, query.limit ?? 5));
7263
- const view = localView(v2, config, cwd);
7264
- const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude });
7762
+ const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
7763
+ const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude, across });
7265
7764
  let out;
7266
7765
  if (canCall(config)) {
7267
7766
  try {
7268
- const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude }, { timeoutMs });
7767
+ const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude, ...view.source ? { source: view.source } : {}, ...across ? { across: true } : {} }, { timeoutMs });
7269
7768
  const remote = Array.isArray(res.candidates) ? res.candidates : [];
7270
7769
  const extra = local().filter((c) => c.id.startsWith("local:") && !remote.some((r) => r.id === c.id));
7271
7770
  out = { candidates: [...remote, ...extra].slice(0, limit), source: "mothership" };
@@ -7280,7 +7779,8 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7280
7779
  const v2 = new V2Store(store);
7281
7780
  const config = store.config();
7282
7781
  const sid = sessionId ?? sessionIdFromEnv(v2);
7283
- const view = localView(v2, config, cwd);
7782
+ const session = sid ? v2.readSession(sid) : null;
7783
+ const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
7284
7784
  let section = view.byId.get(id);
7285
7785
  let source = "local";
7286
7786
  if (!section && canCall(config)) {
@@ -7293,8 +7793,17 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7293
7793
  }
7294
7794
  if (!section) return null;
7295
7795
  mark(v2, sid, cwd, "loaded", [section.id]);
7796
+ const borrowed = !inBoundary(section, view.reader);
7797
+ if (borrowed && sid) {
7798
+ try {
7799
+ v2.updateSession(sid, cwd, (s) => {
7800
+ s.borrowed = [...(s.borrowed ?? []).filter((b) => b.sectionId !== section.id), { sectionId: section.id, layer: section.layer, at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-50);
7801
+ });
7802
+ } catch {
7803
+ }
7804
+ }
7296
7805
  const part = anchor?.replace(/^[§#]/, "");
7297
- return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source };
7806
+ return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source, ...borrowed ? { borrowed: true } : {} };
7298
7807
  }
7299
7808
  var init_library = __esm({
7300
7809
  "src/v2/library.ts"() {
@@ -7318,6 +7827,7 @@ __export(commands_exports, {
7318
7827
  cmdReflectFork: () => cmdReflectFork,
7319
7828
  cmdReview: () => cmdReview,
7320
7829
  cmdReviewDecide: () => cmdReviewDecide,
7830
+ cmdSources: () => cmdSources,
7321
7831
  cmdSuggest: () => cmdSuggest,
7322
7832
  cmdSync: () => cmdSync,
7323
7833
  robotAttrsFromFlags: () => robotAttrsFromFlags,
@@ -7325,9 +7835,9 @@ __export(commands_exports, {
7325
7835
  setupRobot: () => setupRobot,
7326
7836
  v2Status: () => v2Status
7327
7837
  });
7328
- import fs20 from "node:fs";
7329
- import path15 from "node:path";
7330
- import { createHash as createHash4, randomBytes as randomBytes4 } from "node:crypto";
7838
+ import fs22 from "node:fs";
7839
+ import path17 from "node:path";
7840
+ import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
7331
7841
  function cmdReview(store, args, cwd, out, err) {
7332
7842
  const v2 = new V2Store(store);
7333
7843
  const sub = args._[1] ?? "list";
@@ -7427,18 +7937,19 @@ async function cmdReviewDecide(store, args, out, err) {
7427
7937
  async function cmdRecall(store, args, cwd, out, err) {
7428
7938
  const abstract = args._.slice(1).join(" ").trim() || str3(args.flags.abstract) || "";
7429
7939
  if (!abstract) {
7430
- err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--json]\n');
7940
+ err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--across] [--json]\n');
7431
7941
  return 1;
7432
7942
  }
7433
7943
  const limit = Number(str3(args.flags.limit)) || void 0;
7434
- const res = await recallPull(store, { taskAbstract: abstract, hypotheticalLesson: str3(args.flags.hypothetical) ?? abstract, identifiers: list(args.flags.id ?? args.flags.ids), ...limit ? { limit } : {} }, { cwd, sessionId: str3(args.flags.session) });
7944
+ const across = bool2(args.flags.across);
7945
+ const res = await recallPull(store, { taskAbstract: abstract, hypotheticalLesson: str3(args.flags.hypothetical) ?? abstract, identifiers: list(args.flags.id ?? args.flags.ids), ...limit ? { limit } : {} }, { cwd, sessionId: str3(args.flags.session), across });
7435
7946
  if (bool2(args.flags.json)) {
7436
7947
  out(JSON.stringify(res, null, 2) + "\n");
7437
7948
  return 0;
7438
7949
  }
7439
- out(renderCandidates(res.candidates) + "\n");
7950
+ out(renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes) + "\n");
7440
7951
  if (res.candidates.length) out(`
7441
- (${res.source}) Load one with: bunshin load <id>
7952
+ (${res.source}) Load one with: bunshin load <id>${across && res.candidates.some((c) => c.outside) ? " (a lesson from outside this project's scope is recorded as borrowed: evidence it applies here too)" : ""}
7442
7953
  `);
7443
7954
  return 0;
7444
7955
  }
@@ -7455,7 +7966,7 @@ async function cmdLoad(store, args, cwd, out, err) {
7455
7966
  `);
7456
7967
  return 1;
7457
7968
  }
7458
- out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source }, null, 2) + "\n" : res.text + "\n");
7969
+ out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source, ...res.borrowed ? { borrowed: true } : {} }, null, 2) + "\n" : res.text + "\n");
7459
7970
  return 0;
7460
7971
  }
7461
7972
  async function cmdLearn(store, args, cwd, out, err) {
@@ -7496,11 +8007,11 @@ function cmdSuggest(store, args, cwd, out, err) {
7496
8007
  err('bunshin suggest: usage: bunshin suggest --code <harness-relative path> [--file <local file>] [--rationale "<text>"]\n');
7497
8008
  return 1;
7498
8009
  }
7499
- const harnessDir = path15.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
7500
- const file = str3(args.flags.file) ? path15.resolve(cwd, str3(args.flags.file)) : path15.join(harnessDir, rel);
8010
+ const harnessDir = path17.resolve(cwd, str3(args.flags["harness-dir"]) ?? ".");
8011
+ const file = str3(args.flags.file) ? path17.resolve(cwd, str3(args.flags.file)) : path17.join(harnessDir, rel);
7501
8012
  let content;
7502
8013
  try {
7503
- content = fs20.readFileSync(file, "utf8");
8014
+ content = fs22.readFileSync(file, "utf8");
7504
8015
  } catch {
7505
8016
  err(`bunshin suggest: cannot read ${file}
7506
8017
  `);
@@ -7621,15 +8132,15 @@ async function cmdQuestions(store, args, out, err) {
7621
8132
  return 0;
7622
8133
  }
7623
8134
  function saveCredential(storeRoot, apiUrl, apiKey, id) {
7624
- const dir = path15.join(homeDir(), ".config/bunshin/credentials");
7625
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
7626
- fs20.chmodSync(dir, 448);
7627
- const file = path15.join(dir, `${createHash4("sha256").update(`${storeRoot}
8135
+ const dir = path17.join(homeDir(), ".config/bunshin/credentials");
8136
+ fs22.mkdirSync(dir, { recursive: true, mode: 448 });
8137
+ fs22.chmodSync(dir, 448);
8138
+ const file = path17.join(dir, `${createHash5("sha256").update(`${storeRoot}
7628
8139
  ${apiUrl}
7629
8140
  ${id}`).digest("hex")}.json`);
7630
8141
  const tmp = `${file}.${randomBytes4(4).toString("hex")}.tmp`;
7631
- fs20.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7632
- fs20.renameSync(tmp, file);
8142
+ fs22.writeFileSync(tmp, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8143
+ fs22.renameSync(tmp, file);
7633
8144
  return file;
7634
8145
  }
7635
8146
  async function setupRobot(opts) {
@@ -7637,10 +8148,10 @@ async function setupRobot(opts) {
7637
8148
  const existing = Store.discover(opts.cwd);
7638
8149
  let store;
7639
8150
  const harness = opts.harness ?? "other";
7640
- if (existing && path15.resolve(path15.dirname(existing.root)) === path15.resolve(opts.cwd) && harness === "other") store = existing;
8151
+ if (existing && path17.resolve(path17.dirname(existing.root)) === path17.resolve(opts.cwd) && harness === "other") store = existing;
7641
8152
  else {
7642
8153
  runInit({ cwd: opts.cwd, harness, noHooks: harness === "other", ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
7643
- store = new Store(path15.join(opts.cwd, ".bunshin"));
8154
+ store = new Store(path17.join(opts.cwd, ".bunshin"));
7644
8155
  }
7645
8156
  let config = store.config();
7646
8157
  const apiUrl = (opts.apiUrl ?? config.apiUrl ?? "").replace(/\/$/, "");
@@ -7703,8 +8214,10 @@ function v2Status(store, cwd) {
7703
8214
  }
7704
8215
  const view = localView(v2, config, cwd, { index: false });
7705
8216
  const sync = v2.readSyncState();
8217
+ const pooled = cwdInPool(config, cwd, store.root);
7706
8218
  return {
7707
8219
  context: { id: view.ctx.id, kind: view.ctx.kind, layers: view.layers },
8220
+ source: { id: pooled ? sourceFor(cwd, store.root, view.ctx) ?? null : null, inPool: pooled, folder: topFolder(cwd, store.root) || null },
7708
8221
  bundle: bundle ? { sections: bundle.sections.length, generatedAt: bundle.generatedAt, release: bundle.release?.version ?? null } : null,
7709
8222
  repo: { name: repo, sections: repoSections },
7710
8223
  overlays: v2.readOverlays().length,
@@ -7715,6 +8228,46 @@ function v2Status(store, cwd) {
7715
8228
  session: v2.readCurrent()?.sessionId ?? null
7716
8229
  };
7717
8230
  }
8231
+ function cmdSources(store, args, cwd, out, err) {
8232
+ const config = store.config();
8233
+ const action = args._[1];
8234
+ const names = args._.slice(2).flatMap((x) => x.split(",")).map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
8235
+ if (action) {
8236
+ if (action !== "all" && action !== "only" && action !== "include" && action !== "exclude") {
8237
+ err("usage: bunshin sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>]\n");
8238
+ return 1;
8239
+ }
8240
+ if (action !== "all" && !names.length) {
8241
+ err(`bunshin sources ${action}: name at least one folder
8242
+ `);
8243
+ return 1;
8244
+ }
8245
+ const known = new Set(listFolders(store.root, void 0).map((f) => f.folder));
8246
+ const unknown = names.filter((n) => !known.has(n));
8247
+ if (unknown.length) {
8248
+ err(`bunshin sources: not a folder in ${storeHome(store.root)}: ${unknown.join(", ")}
8249
+ `);
8250
+ return 1;
8251
+ }
8252
+ const next = select(config.sources, action, names);
8253
+ const { sources: _drop, ...rest } = config;
8254
+ void _drop;
8255
+ store.saveConfig(next ? { ...rest, sources: next } : rest);
8256
+ }
8257
+ const selection = store.config().sources;
8258
+ const folders = listFolders(store.root, selection, { sources: true });
8259
+ if (bool2(args.flags.json)) {
8260
+ out(JSON.stringify({ home: storeHome(store.root), selection: selection ?? null, folders }, null, 2) + "\n");
8261
+ return 0;
8262
+ }
8263
+ const here = topFolder(cwd, store.root);
8264
+ 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)"}
8265
+ `);
8266
+ if (!folders.length) out(" (no folders here yet)\n");
8267
+ for (const f of folders) out(` ${f.included ? "in " : "out"} ${f.folder}${f.source ? ` (${f.source})` : ""}${f.folder === here ? " \u2190 here" : ""}
8268
+ `);
8269
+ return 0;
8270
+ }
7718
8271
  var str3, bool2, list;
7719
8272
  var init_commands = __esm({
7720
8273
  "src/v2/commands.ts"() {
@@ -7731,6 +8284,7 @@ var init_commands = __esm({
7731
8284
  init_repo();
7732
8285
  init_store2();
7733
8286
  init_sync();
8287
+ init_source();
7734
8288
  init_view();
7735
8289
  str3 = (v) => typeof v === "string" ? v : void 0;
7736
8290
  bool2 = (v) => v === true || v === "true";
@@ -7743,6 +8297,7 @@ var setup_exports = {};
7743
8297
  __export(setup_exports, {
7744
8298
  HOSTED_API: () => HOSTED_API,
7745
8299
  apiAddress: () => apiAddress,
8300
+ askSources: () => askSources,
7746
8301
  connectionProfile: () => connectionProfile,
7747
8302
  executable: () => executable,
7748
8303
  hookBinary: () => hookBinary,
@@ -7750,16 +8305,16 @@ __export(setup_exports, {
7750
8305
  setupProject: () => setupProject,
7751
8306
  shellQuote: () => shellQuote
7752
8307
  });
7753
- import fs21 from "node:fs";
7754
- import path16 from "node:path";
7755
- import { createHash as createHash5, randomBytes as randomBytes5 } from "node:crypto";
8308
+ import fs23 from "node:fs";
8309
+ import path18 from "node:path";
8310
+ import { createHash as createHash6, randomBytes as randomBytes5 } from "node:crypto";
7756
8311
  function executable(name) {
7757
- const directories = [...(process.env.PATH ?? "").split(path16.delimiter), path16.join(homeDir(), ".local/bin")];
8312
+ const directories = [...(process.env.PATH ?? "").split(path18.delimiter), path18.join(homeDir(), ".local/bin")];
7758
8313
  for (const directory of directories) {
7759
- const file = path16.resolve(directory, name);
8314
+ const file = path18.resolve(directory, name);
7760
8315
  try {
7761
- fs21.accessSync(file, fs21.constants.X_OK);
7762
- if (fs21.statSync(file).isFile()) return file;
8316
+ fs23.accessSync(file, fs23.constants.X_OK);
8317
+ if (fs23.statSync(file).isFile()) return file;
7763
8318
  } catch {
7764
8319
  }
7765
8320
  }
@@ -7767,8 +8322,8 @@ function executable(name) {
7767
8322
  }
7768
8323
  function hookBinary(harness) {
7769
8324
  const agent = executable(harness === "claude-code" ? "claude" : harness);
7770
- const directories = [.../* @__PURE__ */ new Set([path16.dirname(process.execPath), path16.join(homeDir(), ".local/bin"), ...agent ? [path16.dirname(agent)] : []])];
7771
- return `PATH=${shellQuote(directories.join(path16.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs21.realpathSync(process.argv[1]))}`;
8325
+ const directories = [.../* @__PURE__ */ new Set([path18.dirname(process.execPath), path18.join(homeDir(), ".local/bin"), ...agent ? [path18.dirname(agent)] : []])];
8326
+ return `PATH=${shellQuote(directories.join(path18.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs23.realpathSync(process.argv[1]))}`;
7772
8327
  }
7773
8328
  function apiAddress(raw) {
7774
8329
  const url = new URL(raw);
@@ -7837,33 +8392,37 @@ async function setupProject(opts) {
7837
8392
  const apiKey = opts.apiKey.trim();
7838
8393
  if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
7839
8394
  const profile = await connectionProfile(apiUrl, apiKey);
7840
- const root = path16.join(opts.cwd, ".bunshin");
8395
+ const root = path18.join(opts.cwd, ".bunshin");
7841
8396
  const store = new Store(root);
7842
8397
  const previous = store.config();
7843
8398
  if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
7844
8399
  throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
7845
8400
  }
7846
- const credentialDirectory = path16.join(homeDir(), ".config/bunshin/credentials");
7847
- fs21.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
7848
- fs21.chmodSync(credentialDirectory, 448);
7849
- const name = createHash5("sha256").update(`${root}
8401
+ const credentialDirectory = path18.join(homeDir(), ".config/bunshin/credentials");
8402
+ fs23.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
8403
+ fs23.chmodSync(credentialDirectory, 448);
8404
+ const name = createHash6("sha256").update(`${root}
7850
8405
  ${apiUrl}
7851
8406
  ${profile.orgId}
7852
8407
  ${profile.userId}`).digest("hex");
7853
- const credentialFile = path16.join(credentialDirectory, `${name}.json`);
8408
+ const credentialFile = path18.join(credentialDirectory, `${name}.json`);
7854
8409
  const temporary = `${credentialFile}.${randomBytes5(4).toString("hex")}.tmp`;
7855
- fs21.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
7856
- fs21.renameSync(temporary, credentialFile);
8410
+ fs23.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
8411
+ fs23.renameSync(temporary, credentialFile);
7857
8412
  const result = runInit({
7858
8413
  cwd: opts.cwd,
7859
8414
  harness: opts.harness,
7860
8415
  apiUrl,
7861
8416
  ...profile,
7862
8417
  noHooks: opts.noHooks,
7863
- bin: opts.bin ?? hookBinary(opts.harness)
8418
+ bin: opts.bin ?? hookBinary(opts.harness),
8419
+ ...opts.hooks ? { hooks: opts.hooks } : {}
7864
8420
  });
8421
+ const { sources: kept, ...base } = result.config;
8422
+ const sources = opts.sources === void 0 ? kept : opts.sources ?? void 0;
7865
8423
  store.saveConfig({
7866
- ...result.config,
8424
+ ...base,
8425
+ ...sources ? { sources } : {},
7867
8426
  ...profile,
7868
8427
  apiKey: void 0,
7869
8428
  credentialFile,
@@ -7874,6 +8433,39 @@ ${profile.userId}`).digest("hex");
7874
8433
  trustStore(root);
7875
8434
  return { ...result, profile, credentialFile };
7876
8435
  }
8436
+ async function askSources(store, out) {
8437
+ const { listFolders: listFolders2, select: select2 } = await Promise.resolve().then(() => (init_source(), source_exports));
8438
+ const config = store.config();
8439
+ const folders = listFolders2(store.root, void 0);
8440
+ if (folders.length < 2) return;
8441
+ const readline2 = await import("node:readline/promises");
8442
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
8443
+ try {
8444
+ out(`This folder holds ${folders.length} folders:
8445
+ `);
8446
+ folders.forEach((f, i) => out(` ${String(i + 1).padStart(2)}. ${f.folder}
8447
+ `));
8448
+ const answer = (await rl.question("Learn from [a]ll of them, all [e]xcept some, or [o]nly some? [a] ")).trim().toLowerCase();
8449
+ const mode = answer.startsWith("e") || answer.startsWith("x") ? "exclude" : answer.startsWith("o") || answer.startsWith("p") ? "only" : "all";
8450
+ const { sources: _drop, ...rest } = config;
8451
+ void _drop;
8452
+ if (mode === "all") {
8453
+ store.saveConfig(rest);
8454
+ out("Learning from every folder here; new folders join automatically.\n");
8455
+ return;
8456
+ }
8457
+ 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): ");
8458
+ 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))];
8459
+ const next = select2(void 0, mode, picked);
8460
+ store.saveConfig(next ? { ...rest, sources: next } : rest);
8461
+ if (mode === "exclude") out(picked.length ? `Learning from every folder but ${picked.join(", ")}; new folders join automatically.
8462
+ ` : "Learning from every folder here; new folders join automatically.\n");
8463
+ else out(picked.length ? `Learning from ${picked.join(", ")} only; new folders stay out until you add them (bunshin sources include <folder>).
8464
+ ` : "Nothing here takes part until you add a folder (bunshin sources include <folder>).\n");
8465
+ } finally {
8466
+ rl.close();
8467
+ }
8468
+ }
7877
8469
  var shellQuote;
7878
8470
  var init_setup = __esm({
7879
8471
  "src/setup.ts"() {
@@ -7924,6 +8516,7 @@ async function handleMcp(msg, opts) {
7924
8516
  const cwd = opts.cwd ?? process.cwd();
7925
8517
  const store = opts.store !== void 0 ? opts.store : Store.discover(cwd);
7926
8518
  if (!store) return reply(text("Bunshin is not set up in this project (run `bunshin setup`). Proceed with general knowledge.", true));
8519
+ if (!cwdInPool(store.config(), cwd, store.root)) return reply(text("This folder is not part of Bunshin's learning pool (`bunshin sources`). Proceed with general knowledge.", true));
7927
8520
  try {
7928
8521
  if (name === "bunshin_recall") {
7929
8522
  const taskAbstract = typeof args.task_abstract === "string" ? args.task_abstract : "";
@@ -7931,8 +8524,8 @@ async function handleMcp(msg, opts) {
7931
8524
  if (!taskAbstract.trim()) return reply(text("task_abstract is required: describe the task abstractly.", true));
7932
8525
  const identifiers = Array.isArray(args.identifiers) ? args.identifiers.filter((x) => typeof x === "string") : [];
7933
8526
  const limit = typeof args.limit === "number" ? args.limit : void 0;
7934
- const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd });
7935
- const body = renderCandidates(res.candidates);
8527
+ const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd, across: args.across === true });
8528
+ const body = renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes);
7936
8529
  return reply(text(res.candidates.length ? `${body}
7937
8530
 
7938
8531
  Load the ones that would change what you do with bunshin_load (0-3).` : body));
@@ -7987,7 +8580,9 @@ var init_mcp = __esm({
7987
8580
  "src/v2/mcp.ts"() {
7988
8581
  "use strict";
7989
8582
  init_v22();
8583
+ init_source();
7990
8584
  init_store();
8585
+ init_store2();
7991
8586
  init_library();
7992
8587
  DEFAULT_PROTOCOL = "2025-06-18";
7993
8588
  MCP_TOOLS = [
@@ -8001,7 +8596,8 @@ var init_mcp = __esm({
8001
8596
  task_abstract: { type: "string", description: "The task in abstract, structural terms (what kind of problem this is), not a restatement of its topic." },
8002
8597
  hypothetical_lesson: { type: "string", description: "The lesson you would want to find, written as if it existed." },
8003
8598
  identifiers: { type: "array", items: { type: "string" }, description: "Concrete identifiers involved: function names, error codes, file paths, product or system names. May be empty." },
8004
- limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." }
8599
+ limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." },
8600
+ across: { type: "boolean", description: "Only when the person asks for something learned elsewhere (another project or group of this workspace): search across the project's boundary. Default false." }
8005
8601
  },
8006
8602
  required: ["task_abstract", "hypothetical_lesson", "identifiers"],
8007
8603
  additionalProperties: false
@@ -8164,9 +8760,9 @@ __export(sync_exports2, {
8164
8760
  resolveConflict: () => resolveConflict,
8165
8761
  runSync: () => runSync
8166
8762
  });
8167
- import { createHash as createHash6 } from "node:crypto";
8168
- import path17 from "node:path";
8169
- import fs22 from "node:fs";
8763
+ import { createHash as createHash7 } from "node:crypto";
8764
+ import path19 from "node:path";
8765
+ import fs24 from "node:fs";
8170
8766
  async function api(config, method, route, body, timeoutMs = 1e4) {
8171
8767
  const ctrl = new AbortController();
8172
8768
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -8204,22 +8800,22 @@ async function mothershipLookup(config, ids, timeoutMs = 8e3) {
8204
8800
  }
8205
8801
  }
8206
8802
  async function runSync(store) {
8207
- const lock = path17.join(store.root, "sync.lock");
8803
+ const lock = path19.join(store.root, "sync.lock");
8208
8804
  const deadline = Date.now() + 12e4;
8209
8805
  for (; ; ) {
8210
8806
  try {
8211
- fs22.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8807
+ fs24.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8212
8808
  break;
8213
8809
  } catch (error) {
8214
8810
  if (error.code !== "EEXIST") throw error;
8215
8811
  try {
8216
- const owner = Number(fs22.readFileSync(lock, "utf8"));
8812
+ const owner = Number(fs24.readFileSync(lock, "utf8"));
8217
8813
  if (Number.isInteger(owner) && owner > 0) {
8218
8814
  try {
8219
8815
  process.kill(owner, 0);
8220
8816
  } catch (error2) {
8221
8817
  if (error2.code === "ESRCH") {
8222
- fs22.unlinkSync(lock);
8818
+ fs24.unlinkSync(lock);
8223
8819
  continue;
8224
8820
  }
8225
8821
  }
@@ -8238,7 +8834,7 @@ async function runSync(store) {
8238
8834
  return await syncStore(store);
8239
8835
  } finally {
8240
8836
  try {
8241
- fs22.unlinkSync(lock);
8837
+ fs24.unlinkSync(lock);
8242
8838
  } catch {
8243
8839
  }
8244
8840
  }
@@ -8266,9 +8862,9 @@ async function syncStore(store) {
8266
8862
  result.errors.push(String(e instanceof Error ? e.message : e));
8267
8863
  }
8268
8864
  }
8269
- const acknowledgementsFile = path17.join(store.root, "synced-traces.json");
8865
+ const acknowledgementsFile = path19.join(store.root, "synced-traces.json");
8270
8866
  const acknowledgements = readJson(acknowledgementsFile, {});
8271
- const fingerprint2 = (trace) => createHash6("sha256").update(JSON.stringify(trace)).digest("hex");
8867
+ const fingerprint2 = (trace) => createHash7("sha256").update(JSON.stringify(trace)).digest("hex");
8272
8868
  const traces = store.allTraces().filter((t) => acknowledgements[t.id] !== fingerprint2(t)).slice(0, 50);
8273
8869
  for (const t of traces) {
8274
8870
  try {
@@ -8521,11 +9117,11 @@ __export(demo_exports, {
8521
9117
  runDemo: () => runDemo
8522
9118
  });
8523
9119
  import assert from "node:assert/strict";
8524
- import fs23 from "node:fs";
9120
+ import fs25 from "node:fs";
8525
9121
  import os4 from "node:os";
8526
- import path18 from "node:path";
9122
+ import path20 from "node:path";
8527
9123
  async function runDemo(write) {
8528
- const root = fs23.mkdtempSync(path18.join(os4.tmpdir(), "bunshin-demo-"));
9124
+ const root = fs25.mkdtempSync(path20.join(os4.tmpdir(), "bunshin-demo-"));
8529
9125
  try {
8530
9126
  const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
8531
9127
  const { lessons, constraint, workflow } = aprScenario();
@@ -8542,7 +9138,7 @@ Task: ${APR_TASK}
8542
9138
  write(result.text);
8543
9139
  write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
8544
9140
  } finally {
8545
- fs23.rmSync(root, { recursive: true, force: true });
9141
+ fs25.rmSync(root, { recursive: true, force: true });
8546
9142
  }
8547
9143
  }
8548
9144
  var init_demo = __esm({
@@ -8555,21 +9151,21 @@ var init_demo = __esm({
8555
9151
  });
8556
9152
 
8557
9153
  // src/v2/gitremote.ts
8558
- import fs24 from "node:fs";
8559
- import path19 from "node:path";
8560
- import { createHash as createHash7 } from "node:crypto";
9154
+ import fs26 from "node:fs";
9155
+ import path21 from "node:path";
9156
+ import { createHash as createHash8 } from "node:crypto";
8561
9157
  function saveGitCredential(apiUrl, apiKey) {
8562
9158
  const file = gitCredentialFile();
8563
- fs24.mkdirSync(path19.dirname(file), { recursive: true, mode: 448 });
9159
+ fs26.mkdirSync(path21.dirname(file), { recursive: true, mode: 448 });
8564
9160
  let all = {};
8565
9161
  try {
8566
- all = JSON.parse(fs24.readFileSync(file, "utf8"));
9162
+ all = JSON.parse(fs26.readFileSync(file, "utf8"));
8567
9163
  } catch {
8568
9164
  }
8569
9165
  all[apiUrl.replace(/\/$/, "")] = apiKey;
8570
9166
  const tmp = `${file}.${process.pid}.tmp`;
8571
- fs24.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8572
- fs24.renameSync(tmp, file);
9167
+ fs26.writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
9168
+ fs26.renameSync(tmp, file);
8573
9169
  }
8574
9170
  function resolveGitKey(apiUrl) {
8575
9171
  const base = apiUrl.replace(/\/$/, "");
@@ -8577,20 +9173,20 @@ function resolveGitKey(apiUrl) {
8577
9173
  const origin = urlOrigin(base);
8578
9174
  if (process.env.BUNSHIN_API_KEY && origin && trustedOrigins().has(origin)) return process.env.BUNSHIN_API_KEY;
8579
9175
  try {
8580
- const all = JSON.parse(fs24.readFileSync(gitCredentialFile(), "utf8"));
9176
+ const all = JSON.parse(fs26.readFileSync(gitCredentialFile(), "utf8"));
8581
9177
  if (all[base]) return all[base];
8582
9178
  } catch {
8583
9179
  }
8584
- const dir = path19.join(homeDir(), ".config/bunshin/credentials");
9180
+ const dir = path21.join(homeDir(), ".config/bunshin/credentials");
8585
9181
  let files = [];
8586
9182
  try {
8587
- files = fs24.readdirSync(dir).filter((f) => f.endsWith(".json"));
9183
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".json"));
8588
9184
  } catch {
8589
9185
  return void 0;
8590
9186
  }
8591
9187
  for (const f of files.sort()) {
8592
9188
  try {
8593
- const c = JSON.parse(fs24.readFileSync(path19.join(dir, f), "utf8"));
9189
+ const c = JSON.parse(fs26.readFileSync(path21.join(dir, f), "utf8"));
8594
9190
  if (c.apiKey && c.apiUrl?.replace(/\/$/, "") === base) return c.apiKey;
8595
9191
  } catch {
8596
9192
  }
@@ -8598,7 +9194,7 @@ function resolveGitKey(apiUrl) {
8598
9194
  return void 0;
8599
9195
  }
8600
9196
  function refsDigest(refs) {
8601
- return createHash7("sha256").update(
9197
+ return createHash8("sha256").update(
8602
9198
  [...refs].sort((a, b) => a.ref.localeCompare(b.ref)).map((r) => `${r.sha} ${r.ref}`).join("\n")
8603
9199
  ).digest("hex").slice(0, 12);
8604
9200
  }
@@ -8611,7 +9207,7 @@ var init_gitremote = __esm({
8611
9207
  ZERO_SHA = "0".repeat(40);
8612
9208
  REMOTE_PATH = "/v1/git/repos/";
8613
9209
  remoteUrl = (apiUrl, repo) => `bunshin::${apiUrl.replace(/\/$/, "")}${REMOTE_PATH}${repo}`;
8614
- gitCredentialFile = () => path19.join(homeDir(), ".config/bunshin/git-credentials.json");
9210
+ gitCredentialFile = () => path21.join(homeDir(), ".config/bunshin/git-credentials.json");
8615
9211
  GitApiError = class extends Error {
8616
9212
  constructor(status, message, body = null) {
8617
9213
  super(message);
@@ -8703,19 +9299,19 @@ __export(gitrepo_exports, {
8703
9299
  githubReferences: () => githubReferences,
8704
9300
  repoFromUrl: () => repoFromUrl
8705
9301
  });
8706
- import fs25 from "node:fs";
8707
- import path20 from "node:path";
9302
+ import fs27 from "node:fs";
9303
+ import path22 from "node:path";
8708
9304
  import { spawnSync as spawnSync2 } from "node:child_process";
8709
- function git2(cwd, args, opts = {}) {
9305
+ function git3(cwd, args, opts = {}) {
8710
9306
  const r = spawnSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024 });
8711
9307
  const res = { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "" };
8712
9308
  if (!res.ok && !opts.allowFail) throw new Error(`git ${args.join(" ")} failed: ${(res.err || res.out).trim()}`);
8713
9309
  return res;
8714
9310
  }
8715
9311
  function onPath(name) {
8716
- for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
9312
+ for (const dir of (process.env.PATH ?? "").split(path22.delimiter)) {
8717
9313
  try {
8718
- fs25.accessSync(path20.join(dir, name), fs25.constants.X_OK);
9314
+ fs27.accessSync(path22.join(dir, name), fs27.constants.X_OK);
8719
9315
  return true;
8720
9316
  } catch {
8721
9317
  }
@@ -8767,7 +9363,7 @@ async function connect(args, cwd, prompt = true, reuseKey = true) {
8767
9363
  return client;
8768
9364
  }
8769
9365
  function toplevel(dir) {
8770
- const r = git2(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
9366
+ const r = git3(dir, ["rev-parse", "--show-toplevel"], { allowFail: true });
8771
9367
  if (!r.ok) throw new Error(`${dir} is not inside a git repository`);
8772
9368
  return r.out.trim();
8773
9369
  }
@@ -8775,7 +9371,7 @@ function githubReferences(root, repo) {
8775
9371
  const [owner, name] = repo.split("/");
8776
9372
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8777
9373
  const pattern = `github\\.com[:/]${esc(owner)}/${esc(name)}|--repo[ =]${esc(owner)}/${esc(name)}|gh repo clone ${esc(owner)}/${esc(name)}`;
8778
- const r = git2(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
9374
+ const r = git3(root, ["grep", "-n", "-I", "-E", pattern, "HEAD", "--"], { allowFail: true });
8779
9375
  return r.out.split("\n").filter(Boolean).map((l) => l.replace(/^HEAD:/, ""));
8780
9376
  }
8781
9377
  function switchOrigin(root, url, out) {
@@ -8784,41 +9380,41 @@ function switchOrigin(root, url, out) {
8784
9380
  let keptAs = null;
8785
9381
  if (old && old !== url) {
8786
9382
  keptAs = rs.has("github") ? `github-${Date.now()}` : "github";
8787
- git2(root, ["remote", "rename", "origin", keptAs]);
9383
+ git3(root, ["remote", "rename", "origin", keptAs]);
8788
9384
  }
8789
- if (rs.has("bunshin") && rs.get("bunshin") === url) git2(root, ["remote", "rename", "bunshin", "origin"]);
8790
- else if (old !== url) git2(root, ["remote", "add", "origin", url]);
8791
- git2(root, ["fetch", "--quiet", "--tags", "origin"]);
8792
- git2(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
9385
+ if (rs.has("bunshin") && rs.get("bunshin") === url) git3(root, ["remote", "rename", "bunshin", "origin"]);
9386
+ else if (old !== url) git3(root, ["remote", "add", "origin", url]);
9387
+ git3(root, ["fetch", "--quiet", "--tags", "origin"]);
9388
+ git3(root, ["remote", "set-head", "origin", "--auto"], { allowFail: true });
8793
9389
  const originRefs = refMap(root, "refs/remotes/origin");
8794
- for (const b of git2(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
8795
- if (originRefs.has(`refs/remotes/origin/${b}`)) git2(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
9390
+ for (const b of git3(root, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).out.split("\n").filter(Boolean)) {
9391
+ if (originRefs.has(`refs/remotes/origin/${b}`)) git3(root, ["branch", `--set-upstream-to=origin/${b}`, b], { allowFail: true });
8796
9392
  }
8797
9393
  out(`origin \u2192 ${url}${keptAs ? ` (the old origin is now the remote "${keptAs}")` : ""}
8798
9394
  `);
8799
9395
  }
8800
9396
  async function cmdImport(args, cwd, out, err) {
8801
9397
  requireHelper();
8802
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
8803
- if (git2(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
9398
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? args._[1] ?? cwd));
9399
+ if (git3(root, ["rev-parse", "--is-shallow-repository"]).out.trim() === "true") throw new Error("this clone is shallow; fetch the full history first: git fetch --unshallow");
8804
9400
  const rs = remotes(root);
8805
9401
  const originUrl = rs.get("origin");
8806
- const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path20.basename(path20.dirname(root))}/${path20.basename(root)}`;
9402
+ const name = str4(args.flags.name) ?? (originUrl && !originUrl.startsWith("bunshin::") ? repoFromUrl(originUrl) : null) ?? `${path22.basename(path22.dirname(root))}/${path22.basename(root)}`;
8807
9403
  const client = await connect(args, cwd);
8808
9404
  const url = remoteUrl(client.apiUrl, name);
8809
9405
  const fromOrigin = !!originUrl && !originUrl.startsWith("bunshin::") && !bool3(args.flags.local);
8810
9406
  if (fromOrigin) {
8811
9407
  out(`Fetching ${originUrl} \u2026
8812
9408
  `);
8813
- const f = git2(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
9409
+ const f = git3(root, ["fetch", "--quiet", "--prune", "--tags", "origin"], { allowFail: true });
8814
9410
  if (!f.ok) err(`! could not fetch origin (${f.err.trim().split("\n")[0]}); importing what this clone already has from it
8815
9411
  `);
8816
9412
  }
8817
9413
  const branches = fromOrigin ? [...refMap(root, "refs/remotes/origin").entries()].filter(([ref]) => ref !== "refs/remotes/origin/HEAD").map(([ref, sha2]) => ({ src: ref, dst: ref.replace("refs/remotes/origin/", "refs/heads/"), sha: sha2 })) : [...refMap(root, "refs/heads").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
8818
9414
  const tags = [...refMap(root, "refs/tags").entries()].map(([ref, sha2]) => ({ src: ref, dst: ref, sha: sha2 }));
8819
9415
  if (!branches.length) throw new Error("nothing to import: no branches found");
8820
- const originHead = git2(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
8821
- const current = git2(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
9416
+ const originHead = git3(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true }).out.trim().replace("refs/remotes/origin/", "");
9417
+ const current = git3(root, ["symbolic-ref", "--quiet", "--short", "HEAD"], { allowFail: true }).out.trim();
8822
9418
  const defaultBranch = str4(args.flags["default-branch"]) ?? (fromOrigin && originHead ? originHead : current || "main");
8823
9419
  if (!branches.some((b) => b.dst === `refs/heads/${defaultBranch}`)) throw new Error(`default branch ${defaultBranch} is not among the imported branches`);
8824
9420
  if (fromOrigin) {
@@ -8828,7 +9424,7 @@ async function cmdImport(args, cwd, out, err) {
8828
9424
  if (!remote) err(`! local branch ${b} is not on the origin and was not imported (push it to Bunshin afterwards if you need it)
8829
9425
  `);
8830
9426
  else if (remote !== sha2) {
8831
- const ahead = git2(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
9427
+ const ahead = git3(root, ["rev-list", "--count", `${remote}..${sha2}`], { allowFail: true }).out.trim();
8832
9428
  if (ahead && ahead !== "0") err(`! ${b} has ${ahead} local commit${ahead === "1" ? "" : "s"} not on the origin; after the switch, \`git push\` sends them to Bunshin
8833
9429
  `);
8834
9430
  }
@@ -8851,13 +9447,13 @@ async function cmdImport(args, cwd, out, err) {
8851
9447
  out(`${name} exists and is empty; importing into it
8852
9448
  `);
8853
9449
  }
8854
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8855
- else git2(root, ["remote", "add", "bunshin", url]);
9450
+ if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
9451
+ else git3(root, ["remote", "add", "bunshin", url]);
8856
9452
  const specs = [...branches, ...tags].map((r) => `${r.src}:${r.dst}`);
8857
- const commits = git2(root, ["rev-list", "--count", "--all"]).out.trim();
9453
+ const commits = git3(root, ["rev-list", "--count", "--all"]).out.trim();
8858
9454
  out(`Pushing ${branches.length} branch${branches.length === 1 ? "" : "es"}, ${tags.length} tag${tags.length === 1 ? "" : "s"} (${commits} commits) \u2026
8859
9455
  `);
8860
- const push2 = git2(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
9456
+ const push2 = git3(root, ["push", "--quiet", "bunshin", ...specs], { allowFail: true });
8861
9457
  if (!push2.ok) throw new Error(`push to Bunshin failed: ${push2.err.trim()}`);
8862
9458
  const info = await client.info(name);
8863
9459
  const want = [...branches, ...tags].map((r) => ({ ref: r.dst, sha: r.sha }));
@@ -8879,7 +9475,7 @@ ${refsToGithub.length} line${refsToGithub.length === 1 ? "" : "s"} in the reposi
8879
9475
  if (refsToGithub.length > 40) out(` \u2026 and ${refsToGithub.length - 40} more (git grep for ${name})
8880
9476
  `);
8881
9477
  }
8882
- if (fs25.existsSync(path20.join(root, ".github"))) out(`
9478
+ if (fs27.existsSync(path22.join(root, ".github"))) out(`
8883
9479
  Note: .github/ (Actions, CODEOWNERS, templates) only works on GitHub; Bunshin does not run it.
8884
9480
  `);
8885
9481
  out(`
@@ -8890,7 +9486,7 @@ New machines clone with: bunshin repo clone ${name}
8890
9486
  }
8891
9487
  async function cmdAttach(args, cwd, out, err) {
8892
9488
  requireHelper();
8893
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9489
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8894
9490
  const rs = remotes(root);
8895
9491
  const originUrl = rs.get("origin");
8896
9492
  const name = str4(args.flags.name) ?? args._[1] ?? (originUrl ? repoFromUrl(originUrl) : null);
@@ -8904,9 +9500,9 @@ async function cmdAttach(args, cwd, out, err) {
8904
9500
  }
8905
9501
  const info = await client.info(name);
8906
9502
  if (!info.refs.length) throw new Error(`${name} in Bunshin is empty; import it first (bunshin repo import)`);
8907
- if (rs.has("bunshin")) git2(root, ["remote", "set-url", "bunshin", url]);
8908
- else git2(root, ["remote", "add", "bunshin", url]);
8909
- git2(root, ["fetch", "--quiet", "--tags", "bunshin"]);
9503
+ if (rs.has("bunshin")) git3(root, ["remote", "set-url", "bunshin", url]);
9504
+ else git3(root, ["remote", "add", "bunshin", url]);
9505
+ git3(root, ["fetch", "--quiet", "--tags", "bunshin"]);
8910
9506
  if (originUrl) {
8911
9507
  const behind = [];
8912
9508
  for (const [ref, sha2] of refMap(root, "refs/remotes/origin")) {
@@ -8914,7 +9510,7 @@ async function cmdAttach(args, cwd, out, err) {
8914
9510
  const b = ref.replace("refs/remotes/origin/", "");
8915
9511
  const theirs = info.refs.find((r) => r.ref === `refs/heads/${b}`)?.sha;
8916
9512
  if (!theirs) behind.push(`${b} (missing in Bunshin)`);
8917
- else if (theirs !== sha2 && !git2(root, ["merge-base", "--is-ancestor", sha2, theirs], { allowFail: true }).ok) behind.push(`${b} (the old origin has commits Bunshin lacks)`);
9513
+ else if (theirs !== sha2 && !git3(root, ["merge-base", "--is-ancestor", sha2, theirs], { allowFail: true }).ok) behind.push(`${b} (the old origin has commits Bunshin lacks)`);
8918
9514
  }
8919
9515
  if (behind.length && !bool3(args.flags.force)) {
8920
9516
  err(`The old origin has work that is not in Bunshin:
@@ -8925,7 +9521,7 @@ Push it to Bunshin first (git push bunshin <branch>), or re-run with --force.
8925
9521
  }
8926
9522
  }
8927
9523
  switchOrigin(root, url, out);
8928
- const status = git2(root, ["status", "-sb"]).out.split("\n")[0];
9524
+ const status = git3(root, ["status", "-sb"]).out.split("\n")[0];
8929
9525
  out(`${status}
8930
9526
  `);
8931
9527
  return 0;
@@ -8938,12 +9534,12 @@ async function cmdClone(args, cwd, out) {
8938
9534
  const dir = args._[2] ?? name.split("/")[1];
8939
9535
  const r = spawnSync2("git", ["clone", remoteUrl(client.apiUrl, name), dir], { cwd, stdio: "inherit" });
8940
9536
  if (r.status !== 0) return r.status ?? 1;
8941
- out(`Cloned ${name} into ${path20.resolve(cwd, dir)}
9537
+ out(`Cloned ${name} into ${path22.resolve(cwd, dir)}
8942
9538
  `);
8943
9539
  return 0;
8944
9540
  }
8945
9541
  async function cmdVerify(args, cwd, out, err) {
8946
- const root = toplevel(path20.resolve(str4(args.flags.path) ?? cwd));
9542
+ const root = toplevel(path22.resolve(str4(args.flags.path) ?? cwd));
8947
9543
  const rs = remotes(root);
8948
9544
  const originUrl = rs.get("origin") ?? "";
8949
9545
  if (!originUrl.startsWith("bunshin::")) throw new Error("origin is not a Bunshin repository (run bunshin repo attach)");
@@ -8964,7 +9560,7 @@ async function cmdVerify(args, cwd, out, err) {
8964
9560
  `);
8965
9561
  }
8966
9562
  }
8967
- const fsck = git2(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
9563
+ const fsck = git3(root, ["fsck", "--connectivity-only", "--no-dangling"], { allowFail: true });
8968
9564
  if (!fsck.ok) {
8969
9565
  problems++;
8970
9566
  err(`! git fsck: ${fsck.err.trim().split("\n")[0]}
@@ -9054,12 +9650,12 @@ var init_gitrepo = __esm({
9054
9650
  bool3 = (v) => v === true || v === "true";
9055
9651
  remotes = (root) => {
9056
9652
  const m = /* @__PURE__ */ new Map();
9057
- for (const name of git2(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git2(root, ["remote", "get-url", name]).out.trim());
9653
+ for (const name of git3(root, ["remote"]).out.split("\n").filter(Boolean)) m.set(name, git3(root, ["remote", "get-url", name]).out.trim());
9058
9654
  return m;
9059
9655
  };
9060
9656
  refMap = (root, prefix) => {
9061
9657
  const m = /* @__PURE__ */ new Map();
9062
- for (const line of git2(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
9658
+ for (const line of git3(root, ["for-each-ref", "--format=%(objectname) %(refname)", prefix]).out.split("\n").filter(Boolean)) {
9063
9659
  const [sha2, ref] = line.split(" ");
9064
9660
  m.set(ref, sha2);
9065
9661
  }
@@ -9106,22 +9702,22 @@ __export(runner_exports, {
9106
9702
  setupText: () => setupText
9107
9703
  });
9108
9704
  import { spawnSync as spawnSync3 } from "node:child_process";
9109
- import fs26 from "node:fs";
9705
+ import fs28 from "node:fs";
9110
9706
  import os5 from "node:os";
9111
- import path21 from "node:path";
9707
+ import path23 from "node:path";
9112
9708
  function runnerHome(home = process.env.BUNSHIN_RUNNER_HOME || os5.homedir()) {
9113
- return path21.join(home, ".bunshin", "runner");
9709
+ return path23.join(home, ".bunshin", "runner");
9114
9710
  }
9115
9711
  function readInstalled(home) {
9116
9712
  try {
9117
- return JSON.parse(fs26.readFileSync(path21.join(runnerHome(home), "config.json"), "utf8"));
9713
+ return JSON.parse(fs28.readFileSync(path23.join(runnerHome(home), "config.json"), "utf8"));
9118
9714
  } catch {
9119
9715
  return null;
9120
9716
  }
9121
9717
  }
9122
9718
  function connection(args, cwd) {
9123
9719
  const saved = readInstalled();
9124
- const store = saved && path21.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9720
+ const store = saved && path23.resolve(cwd).startsWith(runnerHome()) ? null : Store.discover(cwd);
9125
9721
  const config = store?.config();
9126
9722
  const key = str5(args.flags.key) || process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || saved?.key || "";
9127
9723
  const apiUrl = (str5(args.flags["api-url"]) || process.env.BUNSHIN_API_URL || config?.apiUrl || saved?.apiUrl || DEFAULT_API).replace(/\/$/, "");
@@ -9181,9 +9777,9 @@ async function runOnce(c, args, out, err) {
9181
9777
  const [job] = await lease(c, args);
9182
9778
  if (!job) break;
9183
9779
  const started = Date.now();
9184
- const scratch = fs26.mkdtempSync(path21.join(os5.tmpdir(), "bunshin-job-"));
9780
+ const scratch = fs28.mkdtempSync(path23.join(os5.tmpdir(), "bunshin-job-"));
9185
9781
  const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3, cwd: scratch }).finally(
9186
- () => fs26.rmSync(scratch, { recursive: true, force: true })
9782
+ () => fs28.rmSync(scratch, { recursive: true, force: true })
9187
9783
  );
9188
9784
  const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
9189
9785
  if (!res.ok) {
@@ -9210,7 +9806,7 @@ function effortOf(job) {
9210
9806
  const tokens = Math.round(chars / 4);
9211
9807
  const minutes = Math.max(2, Math.round(2 + tokens / 15e3));
9212
9808
  const proposals = (job.prompt.match(/^### g_\w+/gm) ?? []).length;
9213
- const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
9809
+ const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : job.kind === "generalize" ? `${(job.prompt.match(/^<document /gm) ?? []).length} documents from sibling scopes: write once at the parent what at least two of them share, and trim it out of them; "nothing_shared" is a normal answer` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
9214
9810
  return `${chars.toLocaleString("en")} chars (~${tokens.toLocaleString("en")} tokens); about ${minutes} min. ${what}.`;
9215
9811
  }
9216
9812
  function jobIndex(text2) {
@@ -9273,24 +9869,24 @@ function renderCheck(id, c) {
9273
9869
  }
9274
9870
  function logAnswer(entry) {
9275
9871
  try {
9276
- fs26.mkdirSync(runnerHome(), { recursive: true });
9277
- fs26.appendFileSync(path21.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9872
+ fs28.mkdirSync(runnerHome(), { recursive: true });
9873
+ fs28.appendFileSync(path23.join(runnerHome(), "log.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
9278
9874
  `);
9279
9875
  } catch {
9280
9876
  }
9281
9877
  }
9282
9878
  function which(name) {
9283
- for (const d of [...(process.env.PATH || "").split(path21.delimiter), path21.join(os5.homedir(), ".local", "bin")]) {
9284
- if (d && fs26.existsSync(path21.join(d, name))) return path21.join(d, name);
9879
+ for (const d of [...(process.env.PATH || "").split(path23.delimiter), path23.join(os5.homedir(), ".local", "bin")]) {
9880
+ if (d && fs28.existsSync(path23.join(d, name))) return path23.join(d, name);
9285
9881
  }
9286
9882
  return null;
9287
9883
  }
9288
9884
  function scheduledCommand(backend) {
9289
- const node = path21.dirname(process.execPath);
9290
- const npx = fs26.existsSync(path21.join(node, "npx")) ? path21.join(node, "npx") : which("npx") ?? "npx";
9885
+ const node = path23.dirname(process.execPath);
9886
+ const npx = fs28.existsSync(path23.join(node, "npx")) ? path23.join(node, "npx") : which("npx") ?? "npx";
9291
9887
  const bin = BIN_OF[backend] ? which(BIN_OF[backend]) : null;
9292
- const dirs = [.../* @__PURE__ */ new Set([node, ...bin ? [path21.dirname(bin)] : [], path21.join(os5.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])];
9293
- return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path21.delimiter) };
9888
+ 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"])];
9889
+ return { argv: [npx, "-y", PKG2, "runner", "--once"], path: dirs.join(path23.delimiter) };
9294
9890
  }
9295
9891
  function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9296
9892
  const cmd = scheduledCommand(backend);
@@ -9305,8 +9901,8 @@ function launchdPlist(backend, everyMinutes, home = runnerHome()) {
9305
9901
  <dict><key>PATH</key><string>${xml(cmd.path)}</string></dict>
9306
9902
  <key>WorkingDirectory</key><string>${xml(home)}</string>
9307
9903
  <key>StartInterval</key><integer>${everyMinutes * 60}</integer>
9308
- <key>StandardOutPath</key><string>${xml(path21.join(home, "runner.log"))}</string>
9309
- <key>StandardErrorPath</key><string>${xml(path21.join(home, "runner.log"))}</string>
9904
+ <key>StandardOutPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9905
+ <key>StandardErrorPath</key><string>${xml(path23.join(home, "runner.log"))}</string>
9310
9906
  <key>ProcessType</key><string>Background</string>
9311
9907
  <key>LowPriorityIO</key><true/>
9312
9908
  </dict>
@@ -9317,19 +9913,19 @@ function cronLine(backend, everyMinutes, home = runnerHome()) {
9317
9913
  const cmd = scheduledCommand(backend);
9318
9914
  const when = everyMinutes >= 60 ? `17 */${Math.max(1, Math.round(everyMinutes / 60))} * * *` : `*/${everyMinutes} * * * *`;
9319
9915
  const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
9320
- return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path21.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
9916
+ return `${when} cd ${q(home)} && PATH=${q(cmd.path)} ${cmd.argv.map(q).join(" ")} >> ${q(path23.join(home, "runner.log"))} 2>&1 ${CRON_MARK}`;
9321
9917
  }
9322
9918
  function launchAgentsDir(opts) {
9323
- return opts.launchAgents ?? path21.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
9919
+ return opts.launchAgents ?? path23.join(process.env.BUNSHIN_RUNNER_HOME || os5.homedir(), "Library", "LaunchAgents");
9324
9920
  }
9325
9921
  function installSchedule(backend, everyMinutes, opts = {}) {
9326
9922
  const platform = opts.platform ?? process.platform;
9327
9923
  const run2 = opts.run ?? realRun;
9328
9924
  if (platform === "darwin") {
9329
9925
  const dir = launchAgentsDir(opts);
9330
- const plist = path21.join(dir, `${LAUNCHD_LABEL}.plist`);
9331
- fs26.mkdirSync(dir, { recursive: true });
9332
- fs26.writeFileSync(plist, launchdPlist(backend, everyMinutes));
9926
+ const plist = path23.join(dir, `${LAUNCHD_LABEL}.plist`);
9927
+ fs28.mkdirSync(dir, { recursive: true });
9928
+ fs28.writeFileSync(plist, launchdPlist(backend, everyMinutes));
9333
9929
  const domain = `gui/${process.getuid?.() ?? 501}`;
9334
9930
  run2("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
9335
9931
  const r = run2("launchctl", ["bootstrap", domain, plist]);
@@ -9349,10 +9945,10 @@ function removeSchedule(opts = {}) {
9349
9945
  const platform = opts.platform ?? process.platform;
9350
9946
  const run2 = opts.run ?? realRun;
9351
9947
  if (platform === "darwin") {
9352
- const plist = path21.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9948
+ const plist = path23.join(launchAgentsDir(opts), `${LAUNCHD_LABEL}.plist`);
9353
9949
  run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCHD_LABEL}`]);
9354
- if (!fs26.existsSync(plist)) return "no schedule";
9355
- fs26.rmSync(plist, { force: true });
9950
+ if (!fs28.existsSync(plist)) return "no schedule";
9951
+ fs28.rmSync(plist, { force: true });
9356
9952
  return `removed ${plist}`;
9357
9953
  }
9358
9954
  const current = run2("crontab", ["-l"]);
@@ -9379,10 +9975,10 @@ async function install(args, cwd, out, err, opts = {}) {
9379
9975
  }
9380
9976
  const every = Math.max(15, Number(str5(args.flags.every) ?? 60) || 60);
9381
9977
  const home = runnerHome();
9382
- fs26.mkdirSync(home, { recursive: true, mode: 448 });
9978
+ fs28.mkdirSync(home, { recursive: true, mode: 448 });
9383
9979
  const saved = { apiUrl: c.apiUrl, key: c.key, backend, ...config.model && str5(args.flags.model) ? { model: config.model } : {}, name: c.name, every };
9384
- fs26.writeFileSync(path21.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
9385
- fs26.chmodSync(path21.join(home, "config.json"), 384);
9980
+ fs28.writeFileSync(path23.join(home, "config.json"), JSON.stringify(saved, null, 2) + "\n", { mode: 384 });
9981
+ fs28.chmodSync(path23.join(home, "config.json"), 384);
9386
9982
  let schedule;
9387
9983
  try {
9388
9984
  schedule = installSchedule(backend, every, opts);
@@ -9395,7 +9991,7 @@ Run it yourself: npx -y ${PKG2} runner --once
9395
9991
  out(`\u2713 Runner "${c.name}" answers ${c.apiUrl}'s model jobs with ${BACKEND_NAME[backend] ?? backend} on this computer.
9396
9992
  `);
9397
9993
  out(schedule.kind === "none" ? ` No scheduler here: run \`npx -y ${PKG2} runner --once\` on a schedule yourself.
9398
- ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path21.join(home, "runner.log")}
9994
+ ` : ` Every ${every} minutes while this computer is on (${schedule.kind}). Log: ${path23.join(home, "runner.log")}
9399
9995
  `);
9400
9996
  out(` Stop: npx -y ${PKG2} runner uninstall
9401
9997
  `);
@@ -9412,9 +10008,9 @@ Answering what waits now\u2026
9412
10008
  function uninstall(out, opts = {}) {
9413
10009
  out(`${removeSchedule(opts)}
9414
10010
  `);
9415
- const file = path21.join(runnerHome(), "config.json");
9416
- if (fs26.existsSync(file)) {
9417
- fs26.rmSync(file, { force: true });
10011
+ const file = path23.join(runnerHome(), "config.json");
10012
+ if (fs28.existsSync(file)) {
10013
+ fs28.rmSync(file, { force: true });
9418
10014
  out(`removed ${file} (revoke the key in the console: Agent keys)
9419
10015
  `);
9420
10016
  }
@@ -9594,7 +10190,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9594
10190
  out(text2);
9595
10191
  return 0;
9596
10192
  }
9597
- fs26.writeFileSync(file, text2, { mode: 384 });
10193
+ fs28.writeFileSync(file, text2, { mode: 384 });
9598
10194
  const left = Math.round((Date.parse(job.leaseUntil) - Date.now()) / 6e4);
9599
10195
  out(
9600
10196
  [
@@ -9612,7 +10208,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9612
10208
  const id = args._[2];
9613
10209
  const file = args._[3] ?? "-";
9614
10210
  if (!id) throw new Error("usage: bunshin runner validate <job id> <file|->");
9615
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10211
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9616
10212
  const r = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4);
9617
10213
  out(renderCheck(id, r.body));
9618
10214
  return r.body.ok ? 0 : 1;
@@ -9621,7 +10217,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9621
10217
  const id = args._[2];
9622
10218
  const file = args._[3] ?? "-";
9623
10219
  if (!id) throw new Error("usage: bunshin runner submit <job id> <file|-> [--force]");
9624
- const text2 = file === "-" ? fs26.readFileSync(0, "utf8") : fs26.readFileSync(file, "utf8");
10220
+ const text2 = file === "-" ? fs28.readFileSync(0, "utf8") : fs28.readFileSync(file, "utf8");
9625
10221
  const check = await call(c, "POST", `/jobs/${id}/check`, { answer: text2, runner: c.name }, 12e4).catch(() => null);
9626
10222
  if (check?.body && (check.status < 400 || check.body.problems)) {
9627
10223
  out(renderCheck(id, check.body));
@@ -9645,7 +10241,7 @@ async function cmdRunner(args, cwd, out, err, opts = {}) {
9645
10241
  const since = Date.parse(str5(args.flags.since) ?? "") || Date.now() - 12 * 36e5;
9646
10242
  const entries = (() => {
9647
10243
  try {
9648
- return fs26.readFileSync(path21.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
10244
+ return fs28.readFileSync(path23.join(runnerHome(), "log.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => Date.parse(e.at) >= since);
9649
10245
  } catch {
9650
10246
  return [];
9651
10247
  }
@@ -9698,7 +10294,7 @@ var init_runner = __esm({
9698
10294
  init_trust();
9699
10295
  init_version();
9700
10296
  DEFAULT_API = "https://api.bunshin.cc";
9701
- KINDS2 = ["reflect", "merge", "create"];
10297
+ KINDS2 = ["reflect", "merge", "create", "generalize"];
9702
10298
  str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
9703
10299
  LAUNCHD_LABEL = "cc.bunshin.runner";
9704
10300
  CRON_MARK = "# bunshin-runner";
@@ -9714,9 +10310,9 @@ var init_runner = __esm({
9714
10310
  });
9715
10311
 
9716
10312
  // src/v2/importer.ts
9717
- import { execFileSync as execFileSync3 } from "node:child_process";
9718
- import fs27 from "node:fs";
9719
- import path22 from "node:path";
10313
+ import { execFileSync as execFileSync6 } from "node:child_process";
10314
+ import fs29 from "node:fs";
10315
+ import path24 from "node:path";
9720
10316
  function rewriteRepoPaths(text2, repoName2, pack) {
9721
10317
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
9722
10318
  return text2.replace(re, `~/.bunshin/library/${pack}`);
@@ -9755,7 +10351,7 @@ function buildDocs(files, opts) {
9755
10351
  const main2 = `${dir}/SKILL.md`;
9756
10352
  const text2 = rw(files.get(main2).content);
9757
10353
  const fm = frontmatterDescription(text2);
9758
- const name = path22.posix.basename(dir);
10354
+ const name = path24.posix.basename(dir);
9759
10355
  const bundle = {};
9760
10356
  for (const p of all.filter((x) => x.startsWith(`${dir}/`))) {
9761
10357
  claimed.add(p);
@@ -9837,45 +10433,45 @@ ${Object.keys(loose).map((p) => `- \`${p}\``).join("\n")}
9837
10433
  }
9838
10434
  return docs.sort((a, b) => a.path.localeCompare(b.path));
9839
10435
  }
9840
- function git3(root, args) {
9841
- return execFileSync3("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
10436
+ function git4(root, args) {
10437
+ return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
9842
10438
  }
9843
10439
  function gitBuf(root, args) {
9844
- return execFileSync3("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
10440
+ return execFileSync6("git", ["-C", root, ...args], { maxBuffer: 256 * 1024 * 1024 });
9845
10441
  }
9846
10442
  function isGitRepo(root) {
9847
10443
  try {
9848
- return git3(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
10444
+ return git4(root, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
9849
10445
  } catch {
9850
10446
  return false;
9851
10447
  }
9852
10448
  }
9853
10449
  function workingTreeFiles(root) {
9854
10450
  const out = /* @__PURE__ */ new Map();
9855
- const list2 = isGitRepo(root) ? git3(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
9856
- return fs27.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
10451
+ const list2 = isGitRepo(root) ? git4(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
10452
+ return fs29.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
9857
10453
  if (e.name === ".git" || e.name === "node_modules") return [];
9858
10454
  const rel = base ? `${base}/${e.name}` : e.name;
9859
- return e.isDirectory() ? walk(path22.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
10455
+ return e.isDirectory() ? walk(path24.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
9860
10456
  });
9861
10457
  })(root);
9862
10458
  for (const rel of list2) {
9863
- const abs = path22.join(root, rel);
10459
+ const abs = path24.join(root, rel);
9864
10460
  let buf;
9865
10461
  try {
9866
- buf = fs27.readFileSync(abs);
10462
+ buf = fs29.readFileSync(abs);
9867
10463
  } catch {
9868
10464
  continue;
9869
10465
  }
9870
10466
  if (!isText(buf)) continue;
9871
- const exec = (fs27.statSync(abs).mode & 73) !== 0;
10467
+ const exec = (fs29.statSync(abs).mode & 73) !== 0;
9872
10468
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
9873
10469
  }
9874
10470
  return out;
9875
10471
  }
9876
10472
  function commitsOf(root, since) {
9877
10473
  const range = since ? [`${since}..HEAD`] : ["HEAD"];
9878
- return git3(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
10474
+ return git4(root, ["log", "--reverse", "--topo-order", "--format=%H%x1f%an%x1f%aI%x1f%s", ...range]).split("\n").filter(Boolean).map((l) => {
9879
10475
  const [sha2, author, at, subject] = l.split("");
9880
10476
  return { sha: sha2, author, at, subject: subject ?? "" };
9881
10477
  });
@@ -9888,7 +10484,7 @@ function readAt(root, sha2, rel, mode) {
9888
10484
  }
9889
10485
  function treeAt(root, sha2) {
9890
10486
  const out = /* @__PURE__ */ new Map();
9891
- for (const line of git3(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
10487
+ for (const line of git4(root, ["ls-tree", "-r", "-z", sha2]).split("\0").filter(Boolean)) {
9892
10488
  const m = /^(\d+) blob [0-9a-f]+\t(.+)$/.exec(line);
9893
10489
  if (!m) continue;
9894
10490
  const f = readAt(root, sha2, m[2], m[1]);
@@ -9902,7 +10498,7 @@ function diffDocs(prev, next) {
9902
10498
  return { docs: next.filter((d) => before.get(d.path) !== fingerprint(d)), removed: [...before.keys()].filter((p) => !after.has(p)) };
9903
10499
  }
9904
10500
  function readSkillSystem(root, opts) {
9905
- const repoName2 = path22.basename(path22.resolve(root));
10501
+ const repoName2 = path24.basename(path24.resolve(root));
9906
10502
  const build = (files2) => buildDocs(files2, { pack: opts.pack, repoName: repoName2, ...opts.rewrite === false ? { rewrite: false } : {} });
9907
10503
  const tree = build(workingTreeFiles(root));
9908
10504
  if (!opts.history || !isGitRepo(root)) return { versions: [{ summary: `import ${opts.pack}`, docs: tree, removed: [], complete: true }], docs: tree };
@@ -9914,7 +10510,7 @@ function readSkillSystem(root, opts) {
9914
10510
  commits.forEach((c, i) => {
9915
10511
  if (!prevSha) files = treeAt(root, c.sha);
9916
10512
  else {
9917
- const changes = git3(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
10513
+ const changes = git4(root, ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id", prevSha, c.sha]).split("\0").filter(Boolean);
9918
10514
  for (let k = 0; k + 1 < changes.length; k += 2) {
9919
10515
  const m = /^:\d+ (\d+) [0-9a-f]+ [0-9a-f]+ ([A-Z])/.exec(changes[k]);
9920
10516
  const rel = changes[k + 1];
@@ -9979,7 +10575,7 @@ var init_importer = __esm({
9979
10575
  escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9980
10576
  looksGenerated = (text2) => /generated by|do not edit( by hand)?/i.test(text2.slice(0, 600));
9981
10577
  isStopHook = (text2) => /"decision"\s*:\s*"block"|decision.{0,4}block/.test(text2) && /REASON=/.test(text2);
9982
- isUpdateHook = (p, text2) => /auto-?update/i.test(path22.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
10578
+ isUpdateHook = (p, text2) => /auto-?update/i.test(path24.basename(p)) || /git (pull|fetch)/.test(text2) && /SessionStart|session start/i.test(text2);
9983
10579
  fingerprint = (d) => JSON.stringify(d);
9984
10580
  REWRITE_SYSTEM = `You adapt a company's skill documents after its skill system moved into Bunshin.
9985
10581
 
@@ -10015,8 +10611,8 @@ __export(skills_exports, {
10015
10611
  cmdSkill: () => cmdSkill,
10016
10612
  parsePin: () => parsePin
10017
10613
  });
10018
- import fs28 from "node:fs";
10019
- import path23 from "node:path";
10614
+ import fs30 from "node:fs";
10615
+ import path25 from "node:path";
10020
10616
  function requireOnline(store, what) {
10021
10617
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
10022
10618
  }
@@ -10025,9 +10621,9 @@ async function cmdImport2(store, args, cwd, out, err) {
10025
10621
  err("usage: bunshin import skills <dir> [--pack <name>] [--layer org|team:<unit>] [--owner-team <unit>] [--no-history] [--dry-run] [--propose-rewrites]\n");
10026
10622
  return 1;
10027
10623
  }
10028
- const root = path23.resolve(cwd, args._[2]);
10029
- if (!fs28.existsSync(root)) throw new Error(`${root} does not exist`);
10030
- const pack = str6(args.flags.pack) ?? path23.basename(root);
10624
+ const root = path25.resolve(cwd, args._[2]);
10625
+ if (!fs30.existsSync(root)) throw new Error(`${root} does not exist`);
10626
+ const pack = str6(args.flags.pack) ?? path25.basename(root);
10031
10627
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
10032
10628
  const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
10033
10629
  const kinds = /* @__PURE__ */ new Map();
@@ -10062,7 +10658,7 @@ async function cmdImport2(store, args, cwd, out, err) {
10062
10658
  const propose = args.flags["propose-rewrites"];
10063
10659
  if (propose) {
10064
10660
  const only = typeof propose === "string" && propose !== "true" ? propose.split(",").map((x) => x.trim()).filter(Boolean) : [];
10065
- await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path23.basename(root), out, err);
10661
+ await proposeRewrites(store, read.docs.filter((d) => !only.length || only.some((o) => d.path.includes(o))), pack, path25.basename(root), out, err);
10066
10662
  }
10067
10663
  out("Run `bunshin sync` on each machine (it runs at session start) to install the skills, instructions and files.\n");
10068
10664
  return 0;
@@ -10202,7 +10798,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
10202
10798
  }
10203
10799
  const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
10204
10800
  if (graph) {
10205
- fs28.writeFileSync(path23.resolve(cwd, graph), renderSkillGraph(report));
10801
+ fs30.writeFileSync(path25.resolve(cwd, graph), renderSkillGraph(report));
10206
10802
  out(`graph written to ${graph}
10207
10803
  `);
10208
10804
  }
@@ -10269,12 +10865,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10269
10865
  }
10270
10866
  case "status": {
10271
10867
  const d = deployedState(config);
10272
- const p = deployPaths();
10868
+ const p = deployPaths(process.env, config.orgId);
10273
10869
  const pending = d.docs.filter((x) => x.pending);
10274
10870
  out(`deployed: ${d.libraryVersion ? `lib@${d.libraryVersion}` : "nothing yet"}; ${d.docs.filter((x) => x.kind === "skill").length} skills, ${d.docs.filter((x) => x.kind === "instructions").length} instruction documents, ${d.docs.filter((x) => x.kind === "pack").length} pack documents
10275
10871
  `);
10276
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10277
- instructions: ${p.claudeMd}, ${p.agentsMd}
10872
+ out(` skills: ${p.claudeSkills}, ${p.codexSkills} (this workspace's own copy)
10873
+ linked into: ${d.linked.length ? d.linked.join(", ") : "no project yet (each project gets links when a session runs there)"}
10278
10874
  files: ${p.library}
10279
10875
  `);
10280
10876
  for (const x of pending) out(` edited here, awaiting the nightly: ${x.name}
@@ -10282,16 +10878,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
10282
10878
  return 0;
10283
10879
  }
10284
10880
  case "workspace": {
10285
- const target = args._[2];
10286
- if (bool4(args.flags.clear)) setDeployWorkspace(null);
10287
- else if (target) setDeployWorkspace(target);
10288
- const ws = deployWorkspace();
10289
- const p = deployPaths();
10290
- out(ws ? `workspace: ${ws}
10291
- ` : "workspace: none (skills and instructions are user-wide)\n");
10292
- out(` skills: ${p.claudeSkills}, ${p.codexSkills}
10293
- instructions: ${p.claudeMd}, ${p.agentsMd}
10294
- `);
10881
+ out("Skills no longer go to one shared folder: each workspace keeps its own copy and links it into the projects its sessions run in, so they never reach another workspace's projects. See `bunshin library status`.\n");
10295
10882
  return 0;
10296
10883
  }
10297
10884
  case "pin": {
@@ -10301,9 +10888,9 @@ async function cmdLibrary(store, args, cwd, out, err) {
10301
10888
  `;
10302
10889
  const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
10303
10890
  if (file) {
10304
- const abs = path23.resolve(cwd, file);
10305
- fs28.mkdirSync(path23.dirname(abs), { recursive: true });
10306
- fs28.writeFileSync(abs, line);
10891
+ const abs = path25.resolve(cwd, file);
10892
+ fs30.mkdirSync(path25.dirname(abs), { recursive: true });
10893
+ fs30.writeFileSync(abs, line);
10307
10894
  out(`${file}: ${line}`);
10308
10895
  } else out(line);
10309
10896
  return 0;
@@ -10313,12 +10900,12 @@ async function cmdLibrary(store, args, cwd, out, err) {
10313
10900
  const to = str6(args.flags.to);
10314
10901
  if (!to) throw new Error("--to <dir> is required");
10315
10902
  const pinFile = str6(args.flags.pin);
10316
- const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs28.readFileSync(path23.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10903
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs30.readFileSync(path25.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
10317
10904
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
10318
10905
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
10319
10906
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
10320
- const dir = path23.resolve(cwd, to);
10321
- if (fs28.existsSync(dir) && fs28.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
10907
+ const dir = path25.resolve(cwd, to);
10908
+ if (fs30.existsSync(dir) && fs30.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
10322
10909
  const pack = str6(args.flags.pack);
10323
10910
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
10324
10911
  const res = materialize(docs, dir);
@@ -10357,27 +10944,27 @@ __export(reflect_exports, {
10357
10944
  buildLesson: () => buildLesson,
10358
10945
  runReflection: () => runReflection
10359
10946
  });
10360
- import fs29 from "node:fs";
10361
- import path24 from "node:path";
10362
- import { createHash as createHash8 } from "node:crypto";
10947
+ import fs31 from "node:fs";
10948
+ import path26 from "node:path";
10949
+ import { createHash as createHash9 } from "node:crypto";
10363
10950
  async function runReflection(store, opts) {
10364
10951
  const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
10365
- const lock = path24.join(store.root, `reflect-${createHash8("sha256").update(opts.sessionId).digest("hex")}.lock`);
10952
+ const lock = path26.join(store.root, `reflect-${createHash9("sha256").update(opts.sessionId).digest("hex")}.lock`);
10366
10953
  const deadline = Date.now() + 66e4;
10367
10954
  for (; ; ) {
10368
10955
  try {
10369
- fs29.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10956
+ fs31.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
10370
10957
  break;
10371
10958
  } catch (error) {
10372
10959
  if (error.code !== "EEXIST") throw error;
10373
10960
  try {
10374
- const owner = Number(fs29.readFileSync(lock, "utf8"));
10961
+ const owner = Number(fs31.readFileSync(lock, "utf8"));
10375
10962
  if (Number.isInteger(owner) && owner > 0) {
10376
10963
  try {
10377
10964
  process.kill(owner, 0);
10378
10965
  } catch (error2) {
10379
10966
  if (error2.code === "ESRCH") {
10380
- fs29.unlinkSync(lock);
10967
+ fs31.unlinkSync(lock);
10381
10968
  continue;
10382
10969
  }
10383
10970
  }
@@ -10392,7 +10979,7 @@ async function runReflection(store, opts) {
10392
10979
  return await reflectSession(store, opts);
10393
10980
  } finally {
10394
10981
  try {
10395
- fs29.unlinkSync(lock);
10982
+ fs31.unlinkSync(lock);
10396
10983
  } catch {
10397
10984
  }
10398
10985
  }
@@ -10734,11 +11321,11 @@ __export(cli_exports, {
10734
11321
  parseArgs: () => parseArgs,
10735
11322
  version: () => version
10736
11323
  });
10737
- import fs30 from "node:fs";
10738
- import path25 from "node:path";
11324
+ import fs32 from "node:fs";
11325
+ import path27 from "node:path";
10739
11326
  function parseArgs(argv) {
10740
11327
  const out = { _: [], flags: {} };
10741
- 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"]);
11328
+ const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace", "across"]);
10742
11329
  for (let i = 0; i < argv.length; i++) {
10743
11330
  const a = argv[i];
10744
11331
  if (a === "--") {
@@ -10835,7 +11422,7 @@ async function main(argv, opts = {}) {
10835
11422
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
10836
11423
  const agent = harness === "claude-code" ? "claude" : harness;
10837
11424
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
10838
- const prior = new Store(path25.join(cwd, ".bunshin"));
11425
+ const prior = new Store(path27.join(cwd, ".bunshin"));
10839
11426
  const existing = prior.trusted ? prior.config() : { apiUrl: "", apiKey: void 0 };
10840
11427
  const apiUrl = str7(args.flags["api-url"]);
10841
11428
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
@@ -10847,23 +11434,45 @@ async function main(argv, opts = {}) {
10847
11434
  process.stdin.on("end", () => resolve(value.trim()));
10848
11435
  process.stdin.on("error", reject);
10849
11436
  }) : process.env.BUNSHIN_API_KEY || (!apiUrl || apiUrl === existing.apiUrl) && existing.apiKey || await readKey2();
11437
+ const hooksFlag = str7(args.flags.hooks);
11438
+ if (hooksFlag && hooksFlag !== "user" && hooksFlag !== "project") throw new Error("--hooks must be user (every session under this folder) or project (this folder only)");
11439
+ const only = str7(args.flags.only);
11440
+ const exclude = str7(args.flags.exclude);
11441
+ const listed = (v) => v.split(",").map((x) => x.trim().replace(/\/+$/, "")).filter(Boolean);
10850
11442
  out("Connecting to Bunshin\u2026\n");
10851
- const result = await setupProject2({ cwd, harness, apiKey: key, apiUrl, noHooks: bool5(args.flags["no-hooks"]) });
11443
+ const result = await setupProject2({
11444
+ cwd,
11445
+ harness,
11446
+ apiKey: key,
11447
+ apiUrl,
11448
+ noHooks: bool5(args.flags["no-hooks"]),
11449
+ ...hooksFlag ? { hooks: hooksFlag } : {},
11450
+ ...bool5(args.flags.all) ? { sources: null } : only || exclude ? { sources: { ...only ? { only: listed(only) } : {}, ...exclude ? { exclude: listed(exclude) } : {} } } : {}
11451
+ });
11452
+ if (result.config.hooks === "user" && !only && !exclude && !bool5(args.flags.all) && process.stdin.isTTY && !bool5(args.flags["key-stdin"])) {
11453
+ const { askSources: askSources2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
11454
+ await askSources2(new Store(path27.join(cwd, ".bunshin")), out);
11455
+ }
10852
11456
  out(`Connected as ${result.profile.role}${result.profile.orgUnit ? ` in ${result.profile.orgUnit}` : " (company-wide)"}.
10853
11457
  `);
10854
- out(`Project: ${cwd}
11458
+ out(result.config.hooks === "user" ? `Folder: ${cwd} (every session in it or below it uses this workspace)
11459
+ Key saved privately outside the folder.
11460
+ ` : `Project: ${cwd}
10855
11461
  Key saved privately outside the project.
10856
11462
  `);
10857
11463
  if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
10858
11464
  if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
10859
11465
  `);
10860
11466
  if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
11467
+ `);
11468
+ if (result.borrowCommand?.installed) out(`/borrow installed in ${result.borrowCommand.file} (bring in a lesson learned in another project or group)
10861
11469
  `);
10862
11470
  else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
10863
11471
  `);
10864
11472
  if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
10865
11473
  if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the five Bunshin hooks.\n");
10866
11474
  else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
11475
+ else if (result.hooks && result.config.hooks === "user") out("Open a new Claude Code session in any project under this folder to load the hooks. Sessions elsewhere are untouched.\nChange which folders take part with: bunshin sources\n");
10867
11476
  else if (result.hooks) out(`Open a new Claude Code session in this project to load the hooks${result.mcp?.installed ? " and approve the bunshin MCP server (.mcp.json)" : ""}.
10868
11477
  `);
10869
11478
  else out("Hooks are disabled. Run setup again without --no-hooks to configure them.\n");
@@ -10879,7 +11488,7 @@ Key saved privately outside the project.
10879
11488
  if (cmd === "mcp") {
10880
11489
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
10881
11490
  const root = str7(args.flags.root);
10882
- await runMcpServer2({ cwd, ...root ? { store: new Store(path25.resolve(root)) } : {}, version: version() });
11491
+ await runMcpServer2({ cwd, ...root ? { store: new Store(path27.resolve(root)) } : {}, version: version() });
10883
11492
  return 0;
10884
11493
  }
10885
11494
  if (cmd === "demo") {
@@ -10906,6 +11515,14 @@ Key saved privately outside the project.
10906
11515
  }
10907
11516
  if (skipped.length && cmd === "status") err(`! ${untrustedNote(skipped[0])}
10908
11517
  `);
11518
+ if (["learn", "reflect", "recall", "load", "suggest"].includes(cmd) && !str7(args.flags.root)) {
11519
+ const { cwdInPool: cwdInPool2, topFolder: topFolder2 } = await Promise.resolve().then(() => (init_source(), source_exports));
11520
+ if (!cwdInPool2(store.config(), cwd, store.root)) {
11521
+ 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)}\`.
11522
+ `);
11523
+ return 1;
11524
+ }
11525
+ }
10909
11526
  try {
10910
11527
  switch (cmd) {
10911
11528
  case "status":
@@ -10957,6 +11574,9 @@ Key saved privately outside the project.
10957
11574
  return await (await Promise.resolve().then(() => (init_skills(), skills_exports))).cmdLibrary(store, args, cwd, out, err);
10958
11575
  case "ui":
10959
11576
  return await (await Promise.resolve().then(() => (init_uicmd(), uicmd_exports))).cmdUi(store, args, out, err, version());
11577
+ case "sources":
11578
+ case "source":
11579
+ return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdSources(store, args, cwd, out, err);
10960
11580
  default:
10961
11581
  err(`bunshin: unknown command "${cmd}"
10962
11582
 
@@ -10972,7 +11592,7 @@ ${HELP2}`);
10972
11592
  function openStore(args, cwd) {
10973
11593
  const root = str7(args.flags.root);
10974
11594
  if (root) {
10975
- const store = new Store(path25.resolve(root));
11595
+ const store = new Store(path27.resolve(root));
10976
11596
  store.ensureLayout();
10977
11597
  return store;
10978
11598
  }
@@ -10988,7 +11608,7 @@ async function cmdInit(args, cwd, out, err) {
10988
11608
  orgId: str7(args.flags["org-id"]),
10989
11609
  userId: str7(args.flags["user-id"]),
10990
11610
  apiUrl: str7(args.flags["api-url"]),
10991
- apiKey: bool5(args.flags["key-stdin"]) ? fs30.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
11611
+ apiKey: bool5(args.flags["key-stdin"]) ? fs32.readFileSync(0, "utf8").trim() || void 0 : str7(args.flags["api-key"]),
10992
11612
  scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
10993
11613
  harness,
10994
11614
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
@@ -11031,10 +11651,10 @@ function cmdStatus(store, args, out, v2) {
11031
11651
  const conflicts = store.listConflicts().filter((c) => c.status === "open");
11032
11652
  const metrics = store.readMetrics(void 0);
11033
11653
  const last50 = metrics?.windows?.find((w) => w.name === "last50");
11034
- const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path25.join(process.env.CODEX_HOME ?? path25.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path25.dirname(store.root)).file])] : config.harness === "claude-code" ? [path25.join(homeDir(), ".claude", "settings.json"), path25.join(path25.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path25.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path25.join(path25.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
11654
+ const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path27.join(process.env.CODEX_HOME ?? path27.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path27.dirname(store.root)).file])] : config.harness === "claude-code" ? [path27.join(homeDir(), ".claude", "settings.json"), path27.join(path27.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path27.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path27.join(path27.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
11035
11655
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
11036
11656
  try {
11037
- return fs30.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11657
+ return fs32.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
11038
11658
  } catch {
11039
11659
  return false;
11040
11660
  }
@@ -11052,7 +11672,7 @@ function cmdStatus(store, args, out, v2) {
11052
11672
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
11053
11673
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
11054
11674
  hooks: { [config.harness]: hooks },
11055
- hookFiles: hookFiles.filter((file) => fs30.existsSync(file)),
11675
+ hookFiles: hookFiles.filter((file) => fs32.existsSync(file)),
11056
11676
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
11057
11677
  v2
11058
11678
  };
@@ -11068,6 +11688,8 @@ function cmdStatus(store, args, out, v2) {
11068
11688
  out(`user ${config.userId} \u2014 ${config.role} @ ${config.orgUnit} (org ${config.orgId})
11069
11689
  `);
11070
11690
  out(`context ${v2.context.id} (${v2.context.kind}; layers ${v2.context.layers.join(", ")})
11691
+ `);
11692
+ if (v2.source) out(`source ${!v2.source.inPool ? `${v2.source.folder} is outside the learning pool (\`bunshin sources\`)` : v2.source.id ?? "none (the store's own folder)"}
11071
11693
  `);
11072
11694
  out(`library ${v2.bundle ? `${v2.bundle.sections} bundle section(s), generated ${v2.bundle.generatedAt}` : "no bundle yet"}; ${v2.repo.sections} working-tree section(s)${v2.repo.name ? ` in ${v2.repo.name}` : ""}
11073
11695
  `);
@@ -11093,7 +11715,7 @@ function cmdStatus(store, args, out, v2) {
11093
11715
  }
11094
11716
  function hookCount(file) {
11095
11717
  try {
11096
- const raw = JSON.parse(fs30.readFileSync(file, "utf8"));
11718
+ const raw = JSON.parse(fs32.readFileSync(file, "utf8"));
11097
11719
  let n = 0;
11098
11720
  for (const groups of Object.values(raw.hooks ?? {})) {
11099
11721
  for (const g of groups ?? []) {
@@ -11213,7 +11835,7 @@ function cmdLessonAdd(store, args, out, err) {
11213
11835
  let content = str7(args.flags.content) ?? "";
11214
11836
  if (!content || content === "-" || args._.includes("-")) {
11215
11837
  try {
11216
- content = fs30.readFileSync(0, "utf8");
11838
+ content = fs32.readFileSync(0, "utf8");
11217
11839
  } catch {
11218
11840
  }
11219
11841
  }
@@ -11279,7 +11901,7 @@ async function cmdReflect(store, args, cwd, out, err) {
11279
11901
  }
11280
11902
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
11281
11903
  const result = await runReflection2(store, {
11282
- sessionId: str7(args.flags.session) ?? path25.basename(transcript).replace(/\.jsonl?$/, ""),
11904
+ sessionId: str7(args.flags.session) ?? path27.basename(transcript).replace(/\.jsonl?$/, ""),
11283
11905
  transcriptPath: transcript,
11284
11906
  cwd,
11285
11907
  force: bool5(args.flags.force)
@@ -11404,6 +12026,10 @@ Usage: bunshin <command> [options]
11404
12026
  setup --harness codex|claude-code|pi connect this project to your hosted workspace
11405
12027
  --api-url <url> --no-hooks paste the key at the hidden prompt; profile is read from Bunshin
11406
12028
  --key-stdin read the key from stdin (for automated setup)
12029
+ --hooks user a folder of projects: hooks in your user settings, so every
12030
+ session under this folder uses its workspace (default: project)
12031
+ --only a,b | --exclude c | --all
12032
+ which of the folder's projects take part (asked when omitted)
11407
12033
  setup --robot --robot-id <id> --arm so101 --gripper <g> --mount <m> --cameras scene,wrist
11408
12034
  --domain <d> --site <s> --cell <c> [--depth] [--api-url <url>] [--harness claude-code]
11409
12035
  register this machine as a robot context (x-bunshin-context)
@@ -11413,10 +12039,14 @@ Usage: bunshin <command> [options]
11413
12039
  --harness claude-code|codex|pi|other --no-hooks --remove-hooks
11414
12040
 
11415
12041
  status [--json] store, context, library, outbox, hooks and sync health
11416
- recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
11417
- pull candidates from the library (offline: local view)
12042
+ sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
12043
+ which folders under this store's folder take part (learning pool)
12044
+ recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--across] [--json]
12045
+ pull candidates from this project's scopes (offline: local view);
12046
+ --across borrows from anywhere in the workspace (/borrow)
11418
12047
  load <id> [--anchor <heading>] [--json]
11419
- print one document (or one part of it) and mark it loaded
12048
+ print one document (or one part of it) and mark it loaded; a
12049
+ lesson from outside this project's scopes is recorded as borrowed
11420
12050
  learn [focus...] capture what this session learned now (fork reflection),
11421
12051
  apply it locally as a trial and submit it
11422
12052
  review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
@@ -11436,9 +12066,8 @@ Usage: bunshin <command> [options]
11436
12066
  the deployed skills; push proposes local edits / a new skill now
11437
12067
  library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
11438
12068
  library versions (lib@N), history, and exact reproductions
11439
- library workspace [<dir>|--clear]
11440
- deploy skills and instructions into a workspace folder (e.g.
11441
- ~/Documents/dev) instead of ~/.claude and ~/.codex; set it before the first sync
12069
+ library workspace (retired: each workspace keeps its own skills and links them into
12070
+ the projects its sessions run in; see library status)
11442
12071
  ui [--session <id>] [--json] [--watch] what Bunshin did this session, as the person sees it
11443
12072
  ui status | install | uninstall the Claude Code mod (band above the prompt, /bunshin, toasts)
11444
12073
  ui enable|disable --harness <h> [--mode auto|mod|widget|lines|off] how a harness shows it
@@ -11449,9 +12078,9 @@ Usage: bunshin <command> [options]
11449
12078
  repo import|attach|clone|verify|list|url|login
11450
12079
  host a git repository in Bunshin (full history; git push/pull
11451
12080
  keep working through git-remote-bunshin). bunshin repo help
11452
- runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create]
12081
+ runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create,generalize]
11453
12082
  answer the company's queued model work (cloud reflection,
11454
- reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
12083
+ reconciliation, generalization) with this machine's model. BUNSHIN_RUNNER_KEY
11455
12084
  runner next | submit <job> <file|-> | fail <job> --error <why>
11456
12085
  agent mode, for a Claude Code routine or Codex cloud task
11457
12086
  runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall
@@ -11475,7 +12104,7 @@ Global: --root <dir> (use this .bunshin store) --version --help
11475
12104
  Env: BUNSHIN_API_KEY, BUNSHIN_HARNESS, BUNSHIN_DISABLE, BUNSHIN_CHILD, BUNSHIN_CLAUDE_BIN,
11476
12105
  CLAUDE_CODE_SESSION_ID (learn / mcp session), ANTHROPIC_API_KEY (non-fork reflection)
11477
12106
  `;
11478
- untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path25.dirname(root)} to trust it.`;
12107
+ untrustedNote = (root) => `${root} was not created by you (e.g. it is committed to the repository), so Bunshin ignores it. If it is yours, run \`bunshin setup\` (or \`bunshin init\`) in ${path27.dirname(root)} to trust it.`;
11479
12108
  }
11480
12109
  });
11481
12110