@bunshin-cc/bunshin 0.7.0 → 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 (2) hide show
  1. package/dist/bin.js +811 -629
  2. 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
  }
@@ -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) {
@@ -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
 
@@ -2191,7 +2306,7 @@ function redactDeep(value, opts = {}) {
2191
2306
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
2192
2307
  return value;
2193
2308
  }
2194
- var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, CARD_DIGITS, ASSET_EXT, NOREPLY_RE, NAME, Q, VALUE, ARGS, STRONG_LAST, KEY_FILE, plainValue, PATTERNS;
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;
2195
2310
  var init_redact = __esm({
2196
2311
  "../core/src/redact.ts"() {
2197
2312
  "use strict";
@@ -2270,7 +2385,7 @@ var init_redact = __esm({
2270
2385
  CARD_DIGITS = new Uint8Array(64);
2271
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/;
2272
2387
  NOREPLY_RE = /^(?:no[._-]?reply|donot[._-]?reply|do[._-]?not[._-]?reply|noreply\+?.*|git|hg)$/i;
2273
- NAME = String.raw`(?=[A-Za-z_$])(?=[\w.$-]{0,64}?(?:key|token|secret|pass|pwd|credential|auth|cookie|session|creds))[A-Za-z_$][\w.$-]{0,127}`;
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}`;
2274
2389
  Q = String.raw`(?:\\\\){0,8}\\?["'\x60]`;
2275
2390
  VALUE = String.raw`(?:(${Q})(?!\s)(.{0,4096}?)\4|(${Q})?([^\s"'\x60\\,]{1,4096}))`;
2276
2391
  ARGS = String.raw`[^\n|;&]{0,256}?`;
@@ -2356,7 +2471,7 @@ var init_redact = __esm({
2356
2471
  },
2357
2472
  {
2358
2473
  kind: "assigned-secret",
2359
- 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"),
2360
2475
  retryFrom: (m) => m[1].length * 2 + m[2].length + m[3].length,
2361
2476
  replace: (m, R, at, all) => {
2362
2477
  const [whole, , name, , q1, inQuotes, q2, bare] = m;
@@ -2396,7 +2511,7 @@ var init_redact = __esm({
2396
2511
  // YAML block scalar: `TOKEN: |` and the indented lines under it.
2397
2512
  {
2398
2513
  kind: "assigned-secret",
2399
- 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"),
2400
2515
  replace: (m, R) => {
2401
2516
  const [, indent, head, , name, body] = m;
2402
2517
  if (!secretName(name)) return null;
@@ -2668,7 +2783,15 @@ ${input.pendingNew.map((p) => `- ${p}`).join("\n")}
2668
2783
  Already captured this session (do not repeat): ${input.captured.length ? input.captured.join(", ") : "none"}
2669
2784
 
2670
2785
  Layers you may target: ${input.layers.join(", ")}.
2671
- ${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,
2672
2795
  not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
2673
2796
  A lesson for anyone doing the task belongs in the task's document instead.
2674
2797
  ` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
@@ -2952,6 +3075,8 @@ var init_reconcile = __esm({
2952
3075
  init_document();
2953
3076
  init_markdown();
2954
3077
  init_text_index();
3078
+ init_scopes();
3079
+ init_sources();
2955
3080
  init_types();
2956
3081
  }
2957
3082
  });
@@ -2993,13 +3118,14 @@ var init_profile = __esm({
2993
3118
  }
2994
3119
  });
2995
3120
 
2996
- // ../core/src/v2/sources.ts
2997
- function sourceId(kind, name) {
2998
- return `${kind}:${name.trim()}`;
2999
- }
3000
- var init_sources = __esm({
3001
- "../core/src/v2/sources.ts"() {
3121
+ // ../core/src/v2/generalize.ts
3122
+ var init_generalize = __esm({
3123
+ "../core/src/v2/generalize.ts"() {
3002
3124
  "use strict";
3125
+ init_layers();
3126
+ init_guidelines();
3127
+ init_reflect();
3128
+ init_scopes();
3003
3129
  }
3004
3130
  });
3005
3131
 
@@ -3025,6 +3151,8 @@ var init_v2 = __esm({
3025
3151
  init_profile();
3026
3152
  init_guidelines();
3027
3153
  init_sources();
3154
+ init_scopes();
3155
+ init_generalize();
3028
3156
  }
3029
3157
  });
3030
3158
 
@@ -3123,77 +3251,355 @@ var init_api = __esm({
3123
3251
  }
3124
3252
  });
3125
3253
 
3126
- // ../core/src/types.ts
3127
- var init_types2 = __esm({
3128
- "../core/src/types.ts"() {
3129
- "use strict";
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;
3130
3263
  }
3131
- });
3132
-
3133
- // ../core/src/gate.ts
3134
- var CURRENT_CHAT_PREFIX, ANAPHORIC_QUESTION;
3135
- var init_gate = __esm({
3136
- "../core/src/gate.ts"() {
3137
- "use strict";
3138
- CURRENT_CHAT_PREFIX = "(?:(?:so\\s+)?(?:in|within)\\s+(?:this|the)\\s+(?:chat|conversation|session)\\s*,?\\s*)?";
3139
- ANAPHORIC_QUESTION = new RegExp(
3140
- "^" + CURRENT_CHAT_PREFIX + "(?:(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\s+.*\\b(?:this|that|it|these|those|they)\\b.*|(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\b.*\\b(?:also|too|as\\s+well)\\b.*|(?:what|why|how|where|when)\\s+(?:does|do|did|is|are|was|were)\\s+(?:this|that|it|these|those|they)\\b.*|(?:can|could|would)\\s+you\\s+(?:explain|clarify|show)\\s+(?:this|that|it)\\b.*)$",
3141
- "i"
3142
- );
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;
3143
3275
  }
3144
- });
3145
-
3146
- // ../core/src/metrics.ts
3147
- var init_metrics = __esm({
3148
- "../core/src/metrics.ts"() {
3149
- "use strict";
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
+ }
3150
3358
  }
3151
- });
3152
-
3153
- // ../core/src/lesson/id.ts
3154
- var init_id = __esm({
3155
- "../core/src/lesson/id.ts"() {
3156
- "use strict";
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
+ }
3157
3371
  }
3158
- });
3159
-
3160
- // ../core/src/lesson/markdown.ts
3161
- var init_markdown2 = __esm({
3162
- "../core/src/lesson/markdown.ts"() {
3163
- "use strict";
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 {
3164
3382
  }
3165
- });
3166
-
3167
- // ../core/src/lesson/index.ts
3168
- var init_lesson = __esm({
3169
- "../core/src/lesson/index.ts"() {
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"() {
3170
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"));
3171
3415
  }
3172
3416
  });
3173
3417
 
3174
- // ../core/src/serve/score.ts
3175
- function classifyTaskMode(task) {
3176
- const t = (task || "").trim();
3177
- if (INVESTIGATE_RE.test(t) && !BUILD_RE.test(t)) return "investigate";
3178
- if (BUILD_RE.test(t)) return "build";
3179
- if (CONVERSE_RE.test(t) || t.endsWith("?")) return "converse";
3180
- return "build";
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));
3181
3435
  }
3182
- function lessonText(lesson) {
3183
- const extra = lesson.kind === "workflow" ? [...lesson.preconditions, ...lesson.steps.map((s) => `${s.action} ${s.detail ?? ""} ${s.verify ?? ""}`), lesson.outcome].join(" ") : "";
3184
- return `${lesson.title}
3185
- ${lesson.gist}
3186
- ${lesson.tags.join(" ")}
3187
- ${lesson.content}
3188
- ${extra}`;
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] ?? "";
3189
3440
  }
3190
- function score(lesson, request, profile, textRelevance, opts = {}) {
3191
- const w = { ...DEFAULT_WEIGHTS, ...opts.weights ?? {} };
3192
- const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
3193
- const reasons = [];
3194
- const mode = classifyTaskMode(request.task);
3195
- if (lesson.status === "retired" || lesson.status === "superseded") {
3196
- return { score: -1, reasons: [`status:${lesson.status} excluded`], excluded: true, mode };
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
+
3532
+ // ../core/src/types.ts
3533
+ var init_types2 = __esm({
3534
+ "../core/src/types.ts"() {
3535
+ "use strict";
3536
+ }
3537
+ });
3538
+
3539
+ // ../core/src/gate.ts
3540
+ var CURRENT_CHAT_PREFIX, ANAPHORIC_QUESTION;
3541
+ var init_gate = __esm({
3542
+ "../core/src/gate.ts"() {
3543
+ "use strict";
3544
+ CURRENT_CHAT_PREFIX = "(?:(?:so\\s+)?(?:in|within)\\s+(?:this|the)\\s+(?:chat|conversation|session)\\s*,?\\s*)?";
3545
+ ANAPHORIC_QUESTION = new RegExp(
3546
+ "^" + CURRENT_CHAT_PREFIX + "(?:(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\s+.*\\b(?:this|that|it|these|those|they)\\b.*|(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\b.*\\b(?:also|too|as\\s+well)\\b.*|(?:what|why|how|where|when)\\s+(?:does|do|did|is|are|was|were)\\s+(?:this|that|it|these|those|they)\\b.*|(?:can|could|would)\\s+you\\s+(?:explain|clarify|show)\\s+(?:this|that|it)\\b.*)$",
3547
+ "i"
3548
+ );
3549
+ }
3550
+ });
3551
+
3552
+ // ../core/src/metrics.ts
3553
+ var init_metrics = __esm({
3554
+ "../core/src/metrics.ts"() {
3555
+ "use strict";
3556
+ }
3557
+ });
3558
+
3559
+ // ../core/src/lesson/id.ts
3560
+ var init_id = __esm({
3561
+ "../core/src/lesson/id.ts"() {
3562
+ "use strict";
3563
+ }
3564
+ });
3565
+
3566
+ // ../core/src/lesson/markdown.ts
3567
+ var init_markdown2 = __esm({
3568
+ "../core/src/lesson/markdown.ts"() {
3569
+ "use strict";
3570
+ }
3571
+ });
3572
+
3573
+ // ../core/src/lesson/index.ts
3574
+ var init_lesson = __esm({
3575
+ "../core/src/lesson/index.ts"() {
3576
+ "use strict";
3577
+ }
3578
+ });
3579
+
3580
+ // ../core/src/serve/score.ts
3581
+ function classifyTaskMode(task) {
3582
+ const t = (task || "").trim();
3583
+ if (INVESTIGATE_RE.test(t) && !BUILD_RE.test(t)) return "investigate";
3584
+ if (BUILD_RE.test(t)) return "build";
3585
+ if (CONVERSE_RE.test(t) || t.endsWith("?")) return "converse";
3586
+ return "build";
3587
+ }
3588
+ function lessonText(lesson) {
3589
+ const extra = lesson.kind === "workflow" ? [...lesson.preconditions, ...lesson.steps.map((s) => `${s.action} ${s.detail ?? ""} ${s.verify ?? ""}`), lesson.outcome].join(" ") : "";
3590
+ return `${lesson.title}
3591
+ ${lesson.gist}
3592
+ ${lesson.tags.join(" ")}
3593
+ ${lesson.content}
3594
+ ${extra}`;
3595
+ }
3596
+ function score(lesson, request, profile, textRelevance, opts = {}) {
3597
+ const w = { ...DEFAULT_WEIGHTS, ...opts.weights ?? {} };
3598
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
3599
+ const reasons = [];
3600
+ const mode = classifyTaskMode(request.task);
3601
+ if (lesson.status === "retired" || lesson.status === "superseded") {
3602
+ return { score: -1, reasons: [`status:${lesson.status} excluded`], excluded: true, mode };
3197
3603
  }
3198
3604
  let s = 0;
3199
3605
  const rel = clamp(textRelevance);
@@ -3643,8 +4049,8 @@ var init_src = __esm({
3643
4049
  });
3644
4050
 
3645
4051
  // src/v2/store.ts
3646
- import fs6 from "node:fs";
3647
- import path5 from "node:path";
4052
+ import fs8 from "node:fs";
4053
+ import path7 from "node:path";
3648
4054
  import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
3649
4055
  function hexId(prefix) {
3650
4056
  return `${prefix}_${randomBytes3(6).toString("hex")}`;
@@ -3678,12 +4084,12 @@ var init_store2 = __esm({
3678
4084
  V2Store = class {
3679
4085
  constructor(store) {
3680
4086
  this.store = store;
3681
- this.dir = path5.join(store.root, "v2");
4087
+ this.dir = path7.join(store.root, "v2");
3682
4088
  }
3683
4089
  store;
3684
4090
  dir;
3685
4091
  p(...parts) {
3686
- return path5.join(this.dir, ...parts);
4092
+ return path7.join(this.dir, ...parts);
3687
4093
  }
3688
4094
  // ── context / bundle / params ──
3689
4095
  readContext() {
@@ -3764,9 +4170,9 @@ var init_store2 = __esm({
3764
4170
  const dir = this.p("sessions");
3765
4171
  if (!fileExists(dir)) return [];
3766
4172
  const out = [];
3767
- for (const f of fs6.readdirSync(dir)) {
4173
+ for (const f of fs8.readdirSync(dir)) {
3768
4174
  if (!f.endsWith(".json") || f === "current.json") continue;
3769
- const raw = readJson(path5.join(dir, f), null);
4175
+ const raw = readJson(path7.join(dir, f), null);
3770
4176
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
3771
4177
  }
3772
4178
  return out;
@@ -3794,7 +4200,7 @@ var init_store2 = __esm({
3794
4200
  let at = raw.at;
3795
4201
  if (!at) {
3796
4202
  try {
3797
- at = fs6.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
4203
+ at = fs8.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
3798
4204
  } catch {
3799
4205
  at = (/* @__PURE__ */ new Date()).toISOString();
3800
4206
  }
@@ -3821,13 +4227,13 @@ var init_store2 = __esm({
3821
4227
  /** Exclusive lock file; returns a release function, or null when another process holds it. */
3822
4228
  tryLock(name, staleMs = 10 * 6e4) {
3823
4229
  const file = this.p("locks", `${fileSafe(name)}.lock`);
3824
- ensureDir(path5.dirname(file));
4230
+ ensureDir(path7.dirname(file));
3825
4231
  for (let attempt = 0; attempt < 2; attempt++) {
3826
4232
  try {
3827
- fs6.writeFileSync(file, String(process.pid), { flag: "wx" });
4233
+ fs8.writeFileSync(file, String(process.pid), { flag: "wx" });
3828
4234
  return () => {
3829
4235
  try {
3830
- fs6.unlinkSync(file);
4236
+ fs8.unlinkSync(file);
3831
4237
  } catch {
3832
4238
  }
3833
4239
  };
@@ -3835,8 +4241,8 @@ var init_store2 = __esm({
3835
4241
  if (err.code !== "EEXIST") return null;
3836
4242
  let stale = false;
3837
4243
  try {
3838
- const owner = Number(fs6.readFileSync(file, "utf8"));
3839
- 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;
3840
4246
  if (age > staleMs) stale = true;
3841
4247
  else if (Number.isInteger(owner) && owner > 0) {
3842
4248
  try {
@@ -3850,7 +4256,7 @@ var init_store2 = __esm({
3850
4256
  }
3851
4257
  if (!stale) return null;
3852
4258
  try {
3853
- fs6.unlinkSync(file);
4259
+ fs8.unlinkSync(file);
3854
4260
  } catch {
3855
4261
  }
3856
4262
  }
@@ -3867,15 +4273,15 @@ var init_store2 = __esm({
3867
4273
  const dir = this.p(name);
3868
4274
  if (!fileExists(dir)) return [];
3869
4275
  const out = [];
3870
- for (const f of fs6.readdirSync(dir)) {
4276
+ for (const f of fs8.readdirSync(dir)) {
3871
4277
  if (!f.endsWith(".json")) continue;
3872
- const v = readJson(path5.join(dir, f), null);
4278
+ const v = readJson(path7.join(dir, f), null);
3873
4279
  if (v) out.push(v);
3874
4280
  }
3875
4281
  return out;
3876
4282
  }
3877
4283
  };
3878
- rootKey = (root) => createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 16);
4284
+ rootKey = (root) => createHash2("sha256").update(path7.resolve(root)).digest("hex").slice(0, 16);
3879
4285
  addUnique = (list2, ...ids) => {
3880
4286
  for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
3881
4287
  };
@@ -3883,15 +4289,15 @@ var init_store2 = __esm({
3883
4289
  });
3884
4290
 
3885
4291
  // src/v2/deploy.ts
3886
- import { execFileSync } from "node:child_process";
4292
+ import { execFileSync as execFileSync3 } from "node:child_process";
3887
4293
  import { createHash as createHash3 } from "node:crypto";
3888
- import fs7 from "node:fs";
4294
+ import fs9 from "node:fs";
3889
4295
  import os2 from "node:os";
3890
- import path6 from "node:path";
4296
+ import path8 from "node:path";
3891
4297
  function deployWorkspace(env = process.env) {
3892
- 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);
3893
4299
  try {
3894
- 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;
3895
4301
  return typeof ws === "string" && ws ? ws : null;
3896
4302
  } catch {
3897
4303
  return null;
@@ -3900,24 +4306,24 @@ function deployWorkspace(env = process.env) {
3900
4306
  function deployPaths(env = process.env, orgId) {
3901
4307
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3902
4308
  const ws = deployWorkspace(env);
3903
- const state = path6.join(home, ".bunshin", "deploy");
3904
- const shared = { claude: path6.join(ws ?? home, ".claude", "skills"), codex: path6.join(ws ?? home, ".codex", "skills") };
3905
- const org = orgId ? path6.join(state, orgKey(orgId)) : null;
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;
3906
4312
  return {
3907
4313
  home,
3908
- claudeSkills: org ? path6.join(org, "claude-skills") : shared.claude,
3909
- codexSkills: org ? path6.join(org, "codex-skills") : shared.codex,
3910
- ...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path6.join(home, ".claude", "skills"), path6.join(home, ".codex", "skills"), path6.join(home, ".bunshin", "library")])] } : {},
3911
- claudeMd: ws ? path6.join(ws, "CLAUDE.md") : path6.join(home, ".claude", "CLAUDE.md"),
3912
- agentsMd: ws ? path6.join(ws, "AGENTS.md") : path6.join(home, ".codex", "AGENTS.md"),
3913
- library: org ? path6.join(org, "library") : path6.join(home, ".bunshin", "library"),
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"),
3914
4320
  // Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
3915
- bin: path6.join(home, ".bunshin", "bin"),
4321
+ bin: path8.join(home, ".bunshin", "bin"),
3916
4322
  state
3917
4323
  };
3918
4324
  }
3919
4325
  function statePath(p, orgId) {
3920
- return path6.join(p.state, `${orgKey(orgId)}.json`);
4326
+ return path8.join(p.state, `${orgKey(orgId)}.json`);
3921
4327
  }
3922
4328
  function readState(p, orgId) {
3923
4329
  const s = readJson(statePath(p, orgId), null);
@@ -3925,7 +4331,7 @@ function readState(p, orgId) {
3925
4331
  }
3926
4332
  function readText(file) {
3927
4333
  try {
3928
- return fs7.readFileSync(file, "utf8");
4334
+ return fs9.readFileSync(file, "utf8");
3929
4335
  } catch {
3930
4336
  return null;
3931
4337
  }
@@ -3933,13 +4339,13 @@ function readText(file) {
3933
4339
  function writeFile(file, content, mode) {
3934
4340
  writeTextAtomic(file, content);
3935
4341
  try {
3936
- fs7.chmodSync(file, mode === "x" ? 493 : 420);
4342
+ fs9.chmodSync(file, mode === "x" ? 493 : 420);
3937
4343
  } catch {
3938
4344
  }
3939
4345
  }
3940
4346
  function removeFile(file) {
3941
4347
  try {
3942
- fs7.unlinkSync(file);
4348
+ fs9.unlinkSync(file);
3943
4349
  } catch {
3944
4350
  }
3945
4351
  }
@@ -3947,33 +4353,33 @@ function pruneDirs(dir, stop) {
3947
4353
  let d = dir;
3948
4354
  while (d.startsWith(stop) && d !== stop) {
3949
4355
  try {
3950
- if (fs7.readdirSync(d).length) return;
3951
- fs7.rmdirSync(d);
4356
+ if (fs9.readdirSync(d).length) return;
4357
+ fs9.rmdirSync(d);
3952
4358
  } catch {
3953
4359
  return;
3954
4360
  }
3955
- d = path6.dirname(d);
4361
+ d = path8.dirname(d);
3956
4362
  }
3957
4363
  }
3958
4364
  function listFiles(dir, base = dir) {
3959
4365
  let entries;
3960
4366
  try {
3961
- entries = fs7.readdirSync(dir, { withFileTypes: true });
4367
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
3962
4368
  } catch {
3963
4369
  return [];
3964
4370
  }
3965
4371
  const out = [];
3966
4372
  for (const e of entries) {
3967
- const abs = path6.join(dir, e.name);
4373
+ const abs = path8.join(dir, e.name);
3968
4374
  if (e.isDirectory()) out.push(...listFiles(abs, base));
3969
- 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("/"));
3970
4376
  }
3971
4377
  return out.sort();
3972
4378
  }
3973
4379
  function childDir(root, name) {
3974
4380
  if (!name || !SAFE_NAME.test(name)) return null;
3975
- const dir = path6.join(root, name);
3976
- 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;
3977
4383
  }
3978
4384
  function planDeploy(bundle, p, harnesses) {
3979
4385
  const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
@@ -4080,7 +4486,7 @@ function removeBlock(file) {
4080
4486
  if (start < 0 || end < start) return false;
4081
4487
  const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
4082
4488
  if (rest) writeTextAtomic(file, rest + "\n");
4083
- else fs7.unlinkSync(file);
4489
+ else fs9.unlinkSync(file);
4084
4490
  return true;
4085
4491
  }
4086
4492
  function writeBlock(file, block2) {
@@ -4101,7 +4507,7 @@ function skillIndex(skills, p) {
4101
4507
  "",
4102
4508
  "When a task matches a skill below, read its SKILL.md and follow it before doing the work by hand.",
4103
4509
  "",
4104
- ...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()}`)
4105
4511
  ];
4106
4512
  return lines.join("\n");
4107
4513
  }
@@ -4150,7 +4556,7 @@ function captureDoc(c, doc, out) {
4150
4556
  for (const dir of doc.dirs ?? []) {
4151
4557
  for (const rel of listFiles(dir)) {
4152
4558
  if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
4153
- const abs = path6.join(dir, rel);
4559
+ const abs = path8.join(dir, rel);
4154
4560
  const content = readText(abs);
4155
4561
  if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
4156
4562
  byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
@@ -4189,18 +4595,18 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4189
4595
  for (const root of roots) {
4190
4596
  let names = [];
4191
4597
  try {
4192
- 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);
4193
4599
  } catch {
4194
4600
  continue;
4195
4601
  }
4196
4602
  for (const name of names) {
4197
- const dir = path6.join(root, name);
4603
+ const dir = path8.join(root, name);
4198
4604
  if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
4199
- const skillMd = readText(path6.join(dir, "SKILL.md"));
4605
+ const skillMd = readText(path8.join(dir, "SKILL.md"));
4200
4606
  if (skillMd === null) continue;
4201
4607
  let born = 0;
4202
4608
  try {
4203
- const st = fs7.statSync(path6.join(dir, "SKILL.md"));
4609
+ const st = fs9.statSync(path8.join(dir, "SKILL.md"));
4204
4610
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4205
4611
  } catch {
4206
4612
  continue;
@@ -4209,8 +4615,8 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4209
4615
  const files = {};
4210
4616
  for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
4211
4617
  if (rel === "SKILL.md" || rel === MANIFEST) continue;
4212
- const content = readText(path6.join(dir, rel));
4213
- 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" } : {} };
4214
4620
  }
4215
4621
  const hash = sha(JSON.stringify([skillMd, files]));
4216
4622
  if (state.created[name]?.hash === hash) continue;
@@ -4258,14 +4664,14 @@ function captureEdits(v2, config, ctx, opts = {}) {
4258
4664
  }
4259
4665
  function lockDeploy(p) {
4260
4666
  ensureDir(p.state);
4261
- const file = path6.join(p.state, ".lock");
4667
+ const file = path8.join(p.state, ".lock");
4262
4668
  for (let i = 0; i < 2; i++) {
4263
4669
  try {
4264
- fs7.writeFileSync(file, String(process.pid), { flag: "wx" });
4670
+ fs9.writeFileSync(file, String(process.pid), { flag: "wx" });
4265
4671
  return () => removeFile(file);
4266
4672
  } catch {
4267
4673
  try {
4268
- const age = Date.now() - fs7.statSync(file).mtimeMs;
4674
+ const age = Date.now() - fs9.statSync(file).mtimeMs;
4269
4675
  const pid = Number(readText(file));
4270
4676
  let alive = age < 12e4;
4271
4677
  if (alive && pid > 0) {
@@ -4332,7 +4738,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4332
4738
  const foreign = /* @__PURE__ */ new Set();
4333
4739
  if (d.kind === "skill" && !prev) {
4334
4740
  for (const dir of d.dirs ?? []) {
4335
- const cur = readText(path6.join(dir, "SKILL.md"));
4741
+ const cur = readText(path8.join(dir, "SKILL.md"));
4336
4742
  if (cur === null || cur === d.section.body || state.created[d.name]) continue;
4337
4743
  foreign.add(dir);
4338
4744
  report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
@@ -4344,7 +4750,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4344
4750
  targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
4345
4751
  continue;
4346
4752
  }
4347
- if ([...foreign].some((dir) => t.abs.startsWith(dir + path6.sep))) continue;
4753
+ if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
4348
4754
  if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
4349
4755
  report.warnings.push(`${t.abs} not written: it leads outside its folder`);
4350
4756
  continue;
@@ -4353,10 +4759,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4353
4759
  const existing = readText(t.abs);
4354
4760
  try {
4355
4761
  if (existing !== content) {
4356
- ensureDir(path6.dirname(t.abs));
4762
+ ensureDir(path8.dirname(t.abs));
4357
4763
  writeFile(t.abs, content, t.mode);
4358
4764
  report.written++;
4359
- } 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);
4360
4766
  } catch (err) {
4361
4767
  report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
4362
4768
  continue;
@@ -4369,19 +4775,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4369
4775
  const cur = readText(old.abs);
4370
4776
  if (cur !== null && sha(cur) === old.sha) {
4371
4777
  removeFile(old.abs);
4372
- 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);
4373
4779
  report.removed++;
4374
4780
  }
4375
4781
  }
4376
4782
  if (d.kind === "skill") {
4377
4783
  for (const dir of prev?.dirs ?? []) {
4378
4784
  if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4379
- removeFile(path6.join(dir, MANIFEST));
4380
- pruneDirs(dir, path6.dirname(dir));
4785
+ removeFile(path8.join(dir, MANIFEST));
4786
+ pruneDirs(dir, path8.dirname(dir));
4381
4787
  }
4382
4788
  for (const dir of d.dirs ?? []) {
4383
4789
  const manifest = safeJoin(dir, MANIFEST);
4384
- 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;
4385
4791
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4386
4792
  }
4387
4793
  report.skills++;
@@ -4406,12 +4812,12 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4406
4812
  }
4407
4813
  for (const dir of doc.dirs ?? []) {
4408
4814
  if (!removable(dir)) continue;
4409
- removeFile(path6.join(dir, MANIFEST));
4410
- pruneDirs(dir, path6.dirname(dir));
4411
- if (fs7.existsSync(dir)) {
4815
+ removeFile(path8.join(dir, MANIFEST));
4816
+ pruneDirs(dir, path8.dirname(dir));
4817
+ if (fs9.existsSync(dir)) {
4412
4818
  const left = listFiles(dir);
4413
4819
  if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4414
- else pruneDirs(dir, path6.dirname(dir));
4820
+ else pruneDirs(dir, path8.dirname(dir));
4415
4821
  }
4416
4822
  }
4417
4823
  }
@@ -4427,17 +4833,17 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4427
4833
  if (d.kind === "skill") continue;
4428
4834
  for (const t of d.targets) {
4429
4835
  if (t.role === "block" || t.mode !== "x" || !/(^|\/)bin\/[^/]+$/.test(t.rel) || !owned(t.abs)) continue;
4430
- const link = path6.join(p.bin, path6.basename(t.abs));
4836
+ const link = path8.join(p.bin, path8.basename(t.abs));
4431
4837
  ensureDir(p.bin);
4432
4838
  try {
4433
- const cur = fs7.readlinkSync(link);
4839
+ const cur = fs9.readlinkSync(link);
4434
4840
  if (cur !== t.abs) {
4435
- fs7.unlinkSync(link);
4436
- fs7.symlinkSync(t.abs, link);
4841
+ fs9.unlinkSync(link);
4842
+ fs9.symlinkSync(t.abs, link);
4437
4843
  }
4438
4844
  } catch {
4439
4845
  try {
4440
- if (!fs7.existsSync(link)) fs7.symlinkSync(t.abs, link);
4846
+ if (!fs9.existsSync(link)) fs9.symlinkSync(t.abs, link);
4441
4847
  } catch {
4442
4848
  }
4443
4849
  }
@@ -4448,19 +4854,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4448
4854
  if (harnesses.has("claude-code")) {
4449
4855
  for (const pack of packs) {
4450
4856
  const dir = childDir(p.library, pack);
4451
- if (!dir || !insideRoot(dir, path6.join(dir, ".claude", "x"))) continue;
4452
- 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");
4453
4859
  try {
4454
- fs7.lstatSync(link);
4455
- if (fs7.readlinkSync(link) !== p.claudeSkills) {
4456
- fs7.unlinkSync(link);
4457
- fs7.symlinkSync(p.claudeSkills, link);
4860
+ fs9.lstatSync(link);
4861
+ if (fs9.readlinkSync(link) !== p.claudeSkills) {
4862
+ fs9.unlinkSync(link);
4863
+ fs9.symlinkSync(p.claudeSkills, link);
4458
4864
  }
4459
4865
  } catch {
4460
- ensureDir(path6.dirname(link));
4866
+ ensureDir(path8.dirname(link));
4461
4867
  ensureDir(p.claudeSkills);
4462
4868
  try {
4463
- fs7.symlinkSync(p.claudeSkills, link);
4869
+ fs9.symlinkSync(p.claudeSkills, link);
4464
4870
  } catch {
4465
4871
  }
4466
4872
  }
@@ -4469,17 +4875,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4469
4875
  for (const old of state.links) {
4470
4876
  if (links.has(old)) continue;
4471
4877
  try {
4472
- const target = fs7.readlinkSync(old);
4473
- if (!fs7.existsSync(target)) fs7.unlinkSync(old);
4878
+ const target = fs9.readlinkSync(old);
4879
+ if (!fs9.existsSync(target)) fs9.unlinkSync(old);
4474
4880
  } catch {
4475
4881
  }
4476
4882
  }
4477
4883
  let linked = state.linked ?? [];
4478
4884
  if (p.legacy) {
4479
- const names = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length).map((d) => d.name);
4480
- const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path6.resolve(opts.linkRoot)] : []])];
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)] : []])];
4481
4888
  linked = [];
4482
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);
4483
4898
  const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
4484
4899
  report.warnings.push(...res.warnings);
4485
4900
  if (res.kept) linked.push(root);
@@ -4500,13 +4915,13 @@ function materialize(sections, dir) {
4500
4915
  const put = (rel, content, mode) => {
4501
4916
  const abs = safeJoin(dir, rel);
4502
4917
  if (!abs) return;
4503
- ensureDir(path6.dirname(abs));
4918
+ ensureDir(path8.dirname(abs));
4504
4919
  writeFile(abs, content, mode);
4505
4920
  files++;
4506
4921
  };
4507
4922
  for (const s of sections.filter((x) => x.status === "active" || x.status === "stale")) {
4508
4923
  if (s.skill) {
4509
- 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}`;
4510
4925
  put(`${base}/SKILL.md`, s.body);
4511
4926
  for (const [rel, f] of Object.entries(s.skill.files ?? {})) put(`${base}/${rel}`, f.content, f.mode);
4512
4927
  continue;
@@ -4518,51 +4933,51 @@ function materialize(sections, dir) {
4518
4933
  }
4519
4934
  function linkSkills(p, root, harnesses, names) {
4520
4935
  const out = { linked: 0, removed: 0, kept: false, warnings: [] };
4521
- const abs = path6.resolve(root);
4522
- if (abs === path6.resolve(p.home) || abs === path6.resolve(os2.homedir()) || !fs7.existsSync(abs)) return out;
4523
- const managed = (target) => target.startsWith(path6.resolve(p.state) + path6.sep);
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);
4524
4939
  const excluded = [];
4525
4940
  for (const h of harnesses) {
4526
4941
  const spec = LINK_DIRS[h];
4527
4942
  if (!spec) continue;
4528
- const dir = path6.join(abs, spec.rel);
4529
- const canonical = path6.resolve(p[spec.key]);
4530
- const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs7.existsSync(path6.join(canonical, n, "SKILL.md"))));
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"))));
4531
4946
  let entries = [];
4532
4947
  try {
4533
- entries = fs7.readdirSync(dir, { withFileTypes: true });
4948
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
4534
4949
  } catch {
4535
4950
  }
4536
4951
  for (const e of entries) {
4537
4952
  if (!e.isSymbolicLink()) continue;
4538
- const link = path6.join(dir, e.name);
4953
+ const link = path8.join(dir, e.name);
4539
4954
  let target = "";
4540
4955
  try {
4541
- target = path6.resolve(dir, fs7.readlinkSync(link));
4956
+ target = path8.resolve(dir, fs9.readlinkSync(link));
4542
4957
  } catch {
4543
4958
  continue;
4544
4959
  }
4545
4960
  if (!managed(target)) continue;
4546
- if (want.has(e.name) && target === path6.join(canonical, e.name)) {
4961
+ if (want.has(e.name) && target === path8.join(canonical, e.name)) {
4547
4962
  want.delete(e.name);
4548
- excluded.push(path6.join(spec.rel, e.name));
4963
+ excluded.push(path8.join(spec.rel, e.name));
4549
4964
  continue;
4550
4965
  }
4551
4966
  removeFile(link);
4552
4967
  out.removed++;
4553
4968
  }
4554
4969
  for (const name of want) {
4555
- const link = path6.join(dir, name);
4970
+ const link = path8.join(dir, name);
4556
4971
  try {
4557
- fs7.lstatSync(link);
4972
+ fs9.lstatSync(link);
4558
4973
  out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
4559
4974
  continue;
4560
4975
  } catch {
4561
4976
  }
4562
4977
  try {
4563
4978
  ensureDir(dir);
4564
- fs7.symlinkSync(path6.join(canonical, name), link, "dir");
4565
- excluded.push(path6.join(spec.rel, name));
4979
+ fs9.symlinkSync(path8.join(canonical, name), link, "dir");
4980
+ excluded.push(path8.join(spec.rel, name));
4566
4981
  out.linked++;
4567
4982
  } catch (err) {
4568
4983
  out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
@@ -4575,10 +4990,10 @@ function linkSkills(p, root, harnesses, names) {
4575
4990
  return out;
4576
4991
  }
4577
4992
  function gitExclude(root, rels) {
4578
- if (!fs7.existsSync(path6.join(root, ".git"))) return;
4993
+ if (!fs9.existsSync(path8.join(root, ".git"))) return;
4579
4994
  let file;
4580
4995
  try {
4581
- file = path6.resolve(root, execFileSync("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
4996
+ file = path8.resolve(root, execFileSync3("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
4582
4997
  } catch {
4583
4998
  return;
4584
4999
  }
@@ -4587,14 +5002,14 @@ function gitExclude(root, rels) {
4587
5002
  const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
4588
5003
  const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
4589
5004
  const block2 = rels.length ? `${EXCLUDE_BEGIN}
4590
- ${[...new Set(rels)].sort().map((r) => `/${r.split(path6.sep).join("/")}`).join("\n")}
5005
+ ${[...new Set(rels)].sort().map((r) => `/${r.split(path8.sep).join("/")}`).join("\n")}
4591
5006
  ${EXCLUDE_END}
4592
5007
  ` : "";
4593
5008
  const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
4594
5009
  if (next === cur) return;
4595
5010
  try {
4596
- ensureDir(path6.dirname(file));
4597
- fs7.writeFileSync(file, next);
5011
+ ensureDir(path8.dirname(file));
5012
+ fs9.writeFileSync(file, next);
4598
5013
  } catch {
4599
5014
  }
4600
5015
  }
@@ -4603,9 +5018,10 @@ var init_deploy = __esm({
4603
5018
  "src/v2/deploy.ts"() {
4604
5019
  "use strict";
4605
5020
  init_v22();
5021
+ init_source();
4606
5022
  init_util();
4607
5023
  init_store2();
4608
- settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
5024
+ settingsFile = (home) => path8.join(home, ".bunshin", "deploy", "settings.json");
4609
5025
  orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
4610
5026
  sha = (text2) => createHash3("sha256").update(text2).digest("hex");
4611
5027
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
@@ -4614,7 +5030,7 @@ var init_deploy = __esm({
4614
5030
  MAX_FILE = 2 * 1024 * 1024;
4615
5031
  isExecutable = (file) => {
4616
5032
  try {
4617
- return (fs7.statSync(file).mode & 73) !== 0;
5033
+ return (fs9.statSync(file).mode & 73) !== 0;
4618
5034
  } catch {
4619
5035
  return false;
4620
5036
  }
@@ -4622,12 +5038,12 @@ var init_deploy = __esm({
4622
5038
  safeJoin = containedPath;
4623
5039
  SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
4624
5040
  neutralizeMarkers = (text2) => text2.replace(/<!--(?=\s*bunshin:)/gi, "<!-\u200B-");
4625
- 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 };
4626
5042
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
4627
5043
  BORN_SLACK_MS = 2e3;
4628
5044
  LINK_DIRS = {
4629
- "claude-code": { rel: path6.join(".claude", "skills"), key: "claudeSkills" },
4630
- codex: { rel: path6.join(".codex", "skills"), key: "codexSkills" }
5045
+ "claude-code": { rel: path8.join(".claude", "skills"), key: "claudeSkills" },
5046
+ codex: { rel: path8.join(".codex", "skills"), key: "codexSkills" }
4631
5047
  };
4632
5048
  EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
4633
5049
  EXCLUDE_END = "# <<< bunshin";
@@ -4739,11 +5155,11 @@ var init_live = __esm({
4739
5155
  });
4740
5156
 
4741
5157
  // src/transcript.ts
4742
- import fs8 from "node:fs";
5158
+ import fs10 from "node:fs";
4743
5159
  function parseTranscript(file) {
4744
5160
  let raw = "";
4745
5161
  try {
4746
- raw = fs8.readFileSync(file, "utf8");
5162
+ raw = fs10.readFileSync(file, "utf8");
4747
5163
  } catch {
4748
5164
  return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
4749
5165
  }
@@ -4836,290 +5252,126 @@ function blockText(content) {
4836
5252
  if (Array.isArray(content)) {
4837
5253
  return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
4838
5254
  }
4839
- return content == null ? "" : JSON.stringify(content);
4840
- }
4841
- function codexToolResults(output, depth = 0) {
4842
- const raw = blockText(output);
4843
- let envelope = output;
4844
- if (typeof output === "string") {
4845
- try {
4846
- envelope = JSON.parse(output);
4847
- } catch {
4848
- }
4849
- }
4850
- if (depth > 6) return [{ text: raw, pending: true }];
4851
- if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
4852
- if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
4853
- const result = envelope;
4854
- if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
4855
- const pending = !Number.isInteger(result.exit_code);
4856
- return [{
4857
- text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
4858
- ${result.output}`,
4859
- isError: !pending && result.exit_code !== 0,
4860
- pending
4861
- }];
4862
- }
4863
- if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
4864
- if (result.status === "rejected") return [{ text: raw, isError: true }];
4865
- }
4866
- return [{
4867
- text: raw,
4868
- isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
4869
- pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
4870
- }];
4871
- }
4872
- function summarizeInput(input) {
4873
- if (input == null) return "";
4874
- if (typeof input !== "object") return String(input);
4875
- const obj = input;
4876
- for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
4877
- if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
4878
- }
4879
- return JSON.stringify(obj);
4880
- }
4881
- function bounded(text2, maxChars) {
4882
- if (text2.length <= maxChars) return text2;
4883
- const marker = "\n[... middle omitted ...]\n";
4884
- if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
4885
- const head = Math.floor((maxChars - marker.length) / 2);
4886
- return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
4887
- }
4888
- function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
4889
- const parts = [];
4890
- for (const t of facts.turns.slice(from)) {
4891
- const text2 = redact2(t.text);
4892
- if (t.role === "user") parts.push(`[USER]
4893
- ${bounded(text2, maxChars)}`);
4894
- else if (t.role === "assistant") parts.push(`[ASSISTANT]
4895
- ${bounded(text2, maxChars)}`);
4896
- else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4897
- ${bounded(text2, toolResultChars)}`);
4898
- else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
4899
- }
4900
- return bounded(parts.join("\n\n"), maxChars);
4901
- }
4902
- function fullTrace(facts, { from = 0, to } = {}) {
4903
- return facts.turns.slice(from, to).map((t) => {
4904
- if (t.role === "user") return `[USER]
4905
- ${redact2(t.text)}`;
4906
- if (t.role === "assistant") return `[ASSISTANT]
4907
- ${redact2(t.text)}`;
4908
- if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
4909
- ${redact2(t.text)}`;
4910
- const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
4911
- return `[TOOL ${t.tool}]
4912
- ${redact2(input)}`;
4913
- });
4914
- }
4915
- function traceChunks(turns, maxBytes = 1e6) {
4916
- const out = [];
4917
- let cur = "";
4918
- let size = 0;
4919
- turns.forEach((turn, i) => {
4920
- let piece = (i ? "\n\n" : "") + turn;
4921
- let bytes = jsonBytes(piece);
4922
- if (size + bytes <= maxBytes) {
4923
- cur += piece;
4924
- size += bytes;
4925
- return;
4926
- }
4927
- if (cur) out.push(cur);
4928
- while (bytes > maxBytes) {
4929
- let n = Math.min(piece.length, maxBytes);
4930
- 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);
4931
- const code2 = piece.charCodeAt(n - 1);
4932
- if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
4933
- const head = piece.slice(0, n);
4934
- out.push(head);
4935
- piece = piece.slice(n);
4936
- bytes -= jsonBytes(head);
4937
- }
4938
- cur = piece;
4939
- size = bytes;
4940
- });
4941
- if (cur) out.push(cur);
4942
- return out;
4943
- }
4944
- function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
4945
- const facts = parseTranscript(file);
4946
- const visible = facts.turns.filter((t) => t.role !== "tool");
4947
- const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
4948
- const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
4949
- return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
4950
- }
4951
- var BANNER, jsonBytes;
4952
- var init_transcript = __esm({
4953
- "src/transcript.ts"() {
4954
- "use strict";
4955
- init_redact2();
4956
- init_util();
4957
- BANNER = "## Recalled lessons (Bunshin)";
4958
- jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
4959
- }
4960
- });
4961
-
4962
- // src/v2/repo.ts
4963
- import fs9 from "node:fs";
4964
- import path7 from "node:path";
4965
- import { execFileSync as execFileSync2 } from "node:child_process";
4966
- function git(cwd, args, timeout = 1500) {
4967
- try {
4968
- return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
4969
- } catch {
4970
- return null;
4971
- }
4972
- }
4973
- function repoRoot(cwd, storeRoot) {
4974
- const key = `${cwd}
4975
- ${storeRoot ?? ""}`;
4976
- const hit = rootCache.get(key);
4977
- if (hit) return hit;
4978
- const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
4979
- let root = top || "";
4980
- if (!root && storeRoot && path7.basename(storeRoot) === ".bunshin") {
4981
- const parent = path7.dirname(storeRoot);
4982
- if (path7.resolve(cwd).startsWith(path7.resolve(parent))) root = parent;
4983
- }
4984
- root = root || path7.resolve(cwd);
4985
- rootCache.set(key, root);
4986
- return root;
4987
- }
4988
- function parseRemote(url) {
4989
- const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
4990
- if (!clean2) return null;
4991
- const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
4992
- const pathPart = scp ? scp[1] : (() => {
4993
- try {
4994
- return new URL(clean2).pathname;
4995
- } catch {
4996
- return clean2;
4997
- }
4998
- })();
4999
- const segs = pathPart.split("/").filter(Boolean);
5000
- if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
5001
- return segs[0] ?? null;
5002
- }
5003
- function repoName(root) {
5004
- const url = git(root, ["remote", "get-url", "origin"]);
5005
- return url && parseRemote(url) || path7.basename(root);
5006
- }
5007
- function repoCommit(root) {
5008
- return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
5009
- }
5010
- function listDocs(root) {
5011
- const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
5012
- 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();
5013
- const out = [];
5014
- let visited = 0;
5015
- const walk = (dir, rel, depth) => {
5016
- if (depth > 8 || visited++ > 2e4) return;
5017
- let entries;
5018
- try {
5019
- entries = fs9.readdirSync(dir, { withFileTypes: true });
5020
- } catch {
5021
- return;
5022
- }
5023
- for (const e of entries) {
5024
- if (e.isDirectory()) {
5025
- 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);
5026
- } else if (e.isFile()) {
5027
- const r = rel ? `${rel}/${e.name}` : e.name;
5028
- if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
5029
- }
5030
- }
5031
- };
5032
- walk(root, "", 0);
5033
- return out.sort();
5034
- }
5035
- function indexWorkingTree(v2, config, cwd, opts = {}) {
5036
- const root = repoRoot(cwd, v2.store.root);
5037
- const now = opts.now ?? Date.now();
5038
- const cached = v2.readRepoIndex(root);
5039
- const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
5040
- const sameRepo = cached && cached.ownerTeam === ownerTeam;
5041
- const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
5042
- const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
5043
- const paths = fresh ? cached.paths : listDocs(root);
5044
- let changed = !fresh || !sameRepo || cached.repo !== repo;
5045
- const files = {};
5046
- for (const rel of paths) {
5047
- const abs = path7.join(root, rel);
5048
- let stat;
5049
- try {
5050
- stat = fs9.statSync(abs);
5051
- } catch {
5052
- changed = true;
5053
- continue;
5054
- }
5055
- const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
5056
- if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
5057
- files[rel] = prev;
5058
- continue;
5059
- }
5060
- changed = true;
5061
- try {
5062
- const text2 = fs9.readFileSync(abs, "utf8");
5063
- files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
5064
- } catch {
5065
- }
5066
- }
5067
- const index = {
5068
- repo,
5069
- ownerTeam,
5070
- listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
5071
- paths,
5072
- files
5073
- };
5074
- if (changed) {
5075
- try {
5076
- v2.writeRepoIndex(root, index);
5077
- } catch {
5078
- }
5079
- }
5080
- return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
5255
+ return content == null ? "" : JSON.stringify(content);
5081
5256
  }
5082
- function relativeToRoot(root, file, cwd = root) {
5083
- if (!file) return null;
5084
- const abs = path7.isAbsolute(file) ? file : path7.resolve(cwd, file);
5085
- let real = abs;
5086
- let realRoot = root;
5087
- try {
5088
- realRoot = fs9.realpathSync(root);
5089
- } catch {
5090
- }
5091
- try {
5092
- real = fs9.realpathSync(abs);
5093
- } catch {
5257
+ function codexToolResults(output, depth = 0) {
5258
+ const raw = blockText(output);
5259
+ let envelope = output;
5260
+ if (typeof output === "string") {
5094
5261
  try {
5095
- real = path7.join(fs9.realpathSync(path7.dirname(abs)), path7.basename(abs));
5262
+ envelope = JSON.parse(output);
5096
5263
  } catch {
5097
5264
  }
5098
5265
  }
5099
- for (const [r, f] of [
5100
- [realRoot, real],
5101
- [root, abs]
5102
- ]) {
5103
- const rel = path7.relative(r, f);
5104
- if (rel && !rel.startsWith("..") && !path7.isAbsolute(rel)) return rel.split(path7.sep).join("/");
5266
+ if (depth > 6) return [{ text: raw, pending: true }];
5267
+ if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
5268
+ if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
5269
+ const result = envelope;
5270
+ if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
5271
+ const pending = !Number.isInteger(result.exit_code);
5272
+ return [{
5273
+ text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
5274
+ ${result.output}`,
5275
+ isError: !pending && result.exit_code !== 0,
5276
+ pending
5277
+ }];
5278
+ }
5279
+ if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
5280
+ if (result.status === "rejected") return [{ text: raw, isError: true }];
5105
5281
  }
5106
- return null;
5282
+ return [{
5283
+ text: raw,
5284
+ isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
5285
+ pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
5286
+ }];
5107
5287
  }
5108
- var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
5109
- var init_repo = __esm({
5110
- "src/v2/repo.ts"() {
5288
+ function summarizeInput(input) {
5289
+ if (input == null) return "";
5290
+ if (typeof input !== "object") return String(input);
5291
+ const obj = input;
5292
+ for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
5293
+ if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
5294
+ }
5295
+ return JSON.stringify(obj);
5296
+ }
5297
+ function bounded(text2, maxChars) {
5298
+ if (text2.length <= maxChars) return text2;
5299
+ const marker = "\n[... middle omitted ...]\n";
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));
5303
+ }
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)}`);
5315
+ }
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;
5342
+ }
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;
5359
+ }
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"() {
5111
5370
  "use strict";
5112
- init_v22();
5113
- DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
5114
- SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
5115
- LIST_TTL_MS = 6e4;
5116
- rootCache = /* @__PURE__ */ new Map();
5117
- isDoc = (rel) => {
5118
- const parts = rel.split("/");
5119
- if (parts.some((p) => SKIP_DIRS.has(p))) return false;
5120
- return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
5121
- };
5122
- 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;
5123
5375
  }
5124
5376
  });
5125
5377
 
@@ -5188,8 +5440,14 @@ function applyOverlays(sections, overlays, ctx) {
5188
5440
  }
5189
5441
  function localView(v2, config, cwd, opts = {}) {
5190
5442
  const ctx = contextFor(v2, config);
5191
- const reader = readerFor(ctx);
5192
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 } : {} });
5193
5451
  const byId = /* @__PURE__ */ new Map();
5194
5452
  for (const s of bundle?.sections ?? []) byId.set(s.id, s);
5195
5453
  let root = cwd;
@@ -5207,7 +5465,7 @@ function localView(v2, config, cwd, opts = {}) {
5207
5465
  }
5208
5466
  }
5209
5467
  applyOverlays(byId, v2.readOverlays(), ctx);
5210
- 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 };
5211
5469
  }
5212
5470
  var ACTIVE_OVERLAY;
5213
5471
  var init_view = __esm({
@@ -5215,12 +5473,13 @@ var init_view = __esm({
5215
5473
  "use strict";
5216
5474
  init_v22();
5217
5475
  init_repo();
5476
+ init_source();
5218
5477
  ACTIVE_OVERLAY = /* @__PURE__ */ new Set(["proposed", "trial", "submitted", "kept-local", "needs-review"]);
5219
5478
  }
5220
5479
  });
5221
5480
 
5222
5481
  // src/v2/apply.ts
5223
- import fs10 from "node:fs";
5482
+ import fs11 from "node:fs";
5224
5483
  function appendedText(op) {
5225
5484
  return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
5226
5485
  }
@@ -5275,12 +5534,12 @@ function applyLocally(v2, view, s) {
5275
5534
  if (section.storage.type === "repo") {
5276
5535
  const file = containedPath(view.root, section.storage.path);
5277
5536
  const ours = !view.repo || section.storage.repo === view.repo;
5278
- if (ours && file && fs10.existsSync(file)) {
5279
- const before = fs10.readFileSync(file, "utf8");
5537
+ if (ours && file && fs11.existsSync(file)) {
5538
+ const before = fs11.readFileSync(file, "utf8");
5280
5539
  const res2 = applyOpToMarkdown(before, section.storage.anchor, op);
5281
5540
  if (res2.ok) {
5282
5541
  const changed = res2.text !== before;
5283
- if (changed) fs10.writeFileSync(file, res2.text);
5542
+ if (changed) fs11.writeFileSync(file, res2.text);
5284
5543
  const overlay2 = { ...base, kind: "working-tree", sectionId: section.id, file: section.storage.path, anchor: section.storage.anchor, changed };
5285
5544
  upsertOverlay(v2, overlay2);
5286
5545
  return { applied: true, target: describeTarget(s, view), overlay: overlay2 };
@@ -5301,7 +5560,7 @@ function revertWorkingTree(root, o) {
5301
5560
  if (!file) return { reverted: false, reason: "path outside the repository" };
5302
5561
  let text2;
5303
5562
  try {
5304
- text2 = fs10.readFileSync(file, "utf8");
5563
+ text2 = fs11.readFileSync(file, "utf8");
5305
5564
  } catch {
5306
5565
  return { reverted: false, reason: "file missing" };
5307
5566
  }
@@ -5321,7 +5580,7 @@ ${appendedText(o.op)}`;
5321
5580
  if (once(text2, o.op.text) >= 0 && !text2.includes(o.op.find)) next = text2.replace(o.op.text, o.op.find);
5322
5581
  }
5323
5582
  if (next === null) return { reverted: false, reason: "file changed since the trial edit" };
5324
- fs10.writeFileSync(file, next);
5583
+ fs11.writeFileSync(file, next);
5325
5584
  return { reverted: true };
5326
5585
  }
5327
5586
  function revertParam(v2, o) {
@@ -5394,7 +5653,7 @@ var init_apply = __esm({
5394
5653
  });
5395
5654
 
5396
5655
  // src/v2/signals.ts
5397
- import fs11 from "node:fs";
5656
+ import fs12 from "node:fs";
5398
5657
  function isCorrection(text2) {
5399
5658
  const t = text2.trim().slice(0, 600);
5400
5659
  return !!t && CORRECTION_RE.test(t);
@@ -5416,7 +5675,7 @@ function scanTranscript(file) {
5416
5675
  if (!file) return empty;
5417
5676
  let raw;
5418
5677
  try {
5419
- raw = fs11.readFileSync(file, "utf8");
5678
+ raw = fs12.readFileSync(file, "utf8");
5420
5679
  } catch {
5421
5680
  return empty;
5422
5681
  }
@@ -5500,8 +5759,8 @@ var init_signals = __esm({
5500
5759
  });
5501
5760
 
5502
5761
  // src/v2/ui.ts
5503
- import fs12 from "node:fs";
5504
- import path8 from "node:path";
5762
+ import fs13 from "node:fs";
5763
+ import path9 from "node:path";
5505
5764
  function feedPath(v2, sessionId) {
5506
5765
  return v2.p("ui", `${safe(sessionId)}.json`);
5507
5766
  }
@@ -5517,7 +5776,7 @@ function emptyFeed(v2, sessionId, now) {
5517
5776
  }
5518
5777
  function cliArgv() {
5519
5778
  const entry = process.argv[1];
5520
- return entry ? [process.execPath, path8.resolve(entry)] : ["bunshin"];
5779
+ return entry ? [process.execPath, path9.resolve(entry)] : ["bunshin"];
5521
5780
  }
5522
5781
  function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toISOString()) {
5523
5782
  if (!sessionId || sessionId === "no-session") return null;
@@ -5532,7 +5791,7 @@ function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toI
5532
5791
  f.cli = cliArgv();
5533
5792
  f.served = f.served.slice(0, MAX_SERVED);
5534
5793
  f.events = f.events.slice(-MAX_EVENTS);
5535
- fs12.mkdirSync(path8.dirname(feedPath(v2, sessionId)), { recursive: true });
5794
+ fs13.mkdirSync(path9.dirname(feedPath(v2, sessionId)), { recursive: true });
5536
5795
  writeJsonAtomic(feedPath(v2, sessionId), f);
5537
5796
  return f;
5538
5797
  } catch {
@@ -5633,16 +5892,16 @@ function latestFeed(v2) {
5633
5892
  const dir = v2.p("ui");
5634
5893
  let best = null;
5635
5894
  try {
5636
- for (const f2 of fs12.readdirSync(dir)) {
5895
+ for (const f2 of fs13.readdirSync(dir)) {
5637
5896
  if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
5638
- const m = fs12.statSync(path8.join(dir, f2)).mtimeMs;
5897
+ const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
5639
5898
  if (!best || m > best.m) best = { f: f2, m };
5640
5899
  }
5641
5900
  } catch {
5642
5901
  return null;
5643
5902
  }
5644
5903
  if (!best) return null;
5645
- const f = readJson(path8.join(dir, best.f), null);
5904
+ const f = readJson(path9.join(dir, best.f), null);
5646
5905
  return f && f.v === UI_FEED_VERSION ? f : null;
5647
5906
  }
5648
5907
  var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
@@ -5658,110 +5917,6 @@ var init_ui = __esm({
5658
5917
  }
5659
5918
  });
5660
5919
 
5661
- // src/v2/source.ts
5662
- var source_exports = {};
5663
- __export(source_exports, {
5664
- cwdInPool: () => cwdInPool,
5665
- inPool: () => inPool,
5666
- listFolders: () => listFolders,
5667
- projectRootFor: () => projectRootFor,
5668
- select: () => select,
5669
- sourceFor: () => sourceFor,
5670
- storeHome: () => storeHome,
5671
- topFolder: () => topFolder
5672
- });
5673
- import fs13 from "node:fs";
5674
- import path9 from "node:path";
5675
- import { execFileSync as execFileSync3 } from "node:child_process";
5676
- function storeHome(storeRoot) {
5677
- return path9.dirname(path9.resolve(storeRoot));
5678
- }
5679
- function topFolder(cwd, storeRoot) {
5680
- const rel = path9.relative(storeHome(storeRoot), path9.resolve(cwd));
5681
- if (!rel || rel.startsWith("..") || path9.isAbsolute(rel)) return "";
5682
- return rel.split(path9.sep)[0] ?? "";
5683
- }
5684
- function inPool(selection, folder) {
5685
- if (!folder || !selection) return true;
5686
- if (selection.exclude?.includes(folder)) return false;
5687
- return selection.only ? selection.only.includes(folder) : true;
5688
- }
5689
- function cwdInPool(config, cwd, storeRoot) {
5690
- return inPool(config.sources, topFolder(cwd, storeRoot));
5691
- }
5692
- function git2(cwd, args) {
5693
- try {
5694
- return execFileSync3("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
5695
- } catch {
5696
- return null;
5697
- }
5698
- }
5699
- function sourceFor(cwd, storeRoot, ctx) {
5700
- if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
5701
- const home = storeHome(storeRoot);
5702
- const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
5703
- if (top) {
5704
- const remote = git2(top, ["remote", "get-url", "origin"]);
5705
- const named = remote ? parseRemote(remote) : null;
5706
- if (named) return sourceId("repo", named);
5707
- const rel = path9.relative(home, top);
5708
- return sourceId("folder", rel && !rel.startsWith("..") && !path9.isAbsolute(rel) ? rel.split(path9.sep).join("/") : path9.basename(top));
5709
- }
5710
- const folder = topFolder(cwd, storeRoot);
5711
- return folder ? sourceId("folder", folder) : void 0;
5712
- }
5713
- function projectRootFor(cwd, storeRoot) {
5714
- const home = storeHome(storeRoot);
5715
- const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
5716
- if (top) {
5717
- const rel = path9.relative(home, top);
5718
- if (!rel.startsWith("..") && !path9.isAbsolute(rel)) return top;
5719
- }
5720
- const folder = topFolder(cwd, storeRoot);
5721
- return folder ? path9.join(home, folder) : home;
5722
- }
5723
- function listFolders(storeRoot, selection, opts = {}) {
5724
- const home = storeHome(storeRoot);
5725
- let names = [];
5726
- try {
5727
- names = fs13.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
5728
- } catch {
5729
- }
5730
- return names.map((folder) => ({
5731
- folder,
5732
- included: inPool(selection, folder),
5733
- ...opts.sources ? { source: sourceFor(path9.join(home, folder), storeRoot) } : {}
5734
- }));
5735
- }
5736
- function select(current, action, folders) {
5737
- const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
5738
- const only = current?.only;
5739
- const exclude = current?.exclude ?? [];
5740
- let next;
5741
- switch (action) {
5742
- case "all":
5743
- return void 0;
5744
- case "only":
5745
- next = { only: uniq(folders) };
5746
- break;
5747
- case "include":
5748
- next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
5749
- break;
5750
- case "exclude":
5751
- next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
5752
- break;
5753
- }
5754
- if (!next.exclude?.length) delete next.exclude;
5755
- return next.only || next.exclude ? next : void 0;
5756
- }
5757
- var init_source = __esm({
5758
- "src/v2/source.ts"() {
5759
- "use strict";
5760
- init_v22();
5761
- init_repo();
5762
- }
5763
- });
5764
-
5765
5920
  // src/v2/sync.ts
5766
5921
  var sync_exports = {};
5767
5922
  __export(sync_exports, {
@@ -6113,7 +6268,7 @@ async function runReflectionV2(store, opts) {
6113
6268
  if (!release) return { ...result, skipped: "a reflection of this session is already running" };
6114
6269
  try {
6115
6270
  const session = v2.session(opts.sessionId, opts.cwd ?? process.cwd());
6116
- 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 } : {});
6117
6272
  const origin = opts.origin ?? (opts.focus !== void 0 ? "learn" : "session-end");
6118
6273
  const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
6119
6274
  const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
@@ -6130,7 +6285,7 @@ async function runReflectionV2(store, opts) {
6130
6285
  const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
6131
6286
  const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
6132
6287
  const guidelines = reflectionGuidelines(view.sections, view.reader);
6133
- const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
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 } : {} };
6134
6289
  let raw = null;
6135
6290
  if (harness === "claude-code") {
6136
6291
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
@@ -6170,7 +6325,7 @@ async function runReflectionV2(store, opts) {
6170
6325
  result.nothingNew = result.method !== "none" && parsed.length === 0;
6171
6326
  const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
6172
6327
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6173
- const source = session.source !== void 0 ? session.source || void 0 : sourceFor(session.cwd || opts.cwd || process.cwd(), store.root, view.ctx);
6328
+ const source = view.source;
6174
6329
  for (const p of parsed) {
6175
6330
  const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
6176
6331
  if (source) s.source = source;
@@ -6257,7 +6412,6 @@ var init_reflect3 = __esm({
6257
6412
  init_signals();
6258
6413
  init_ui();
6259
6414
  init_view();
6260
- init_source();
6261
6415
  FORK_TIMEOUT_MS = 12e4;
6262
6416
  FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
6263
6417
  }
@@ -6677,7 +6831,7 @@ function liveUpdate(v2, ctx, session) {
6677
6831
  return "";
6678
6832
  }
6679
6833
  const config = ctx.store.config();
6680
- const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
6834
+ const deployed = deployEnabled(config, contextFor(v2, config));
6681
6835
  const paths = deployed ? deployPaths(process.env, config.orgId) : null;
6682
6836
  const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
6683
6837
  const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
@@ -6744,7 +6898,7 @@ async function onPrompt(ctx) {
6744
6898
  };
6745
6899
  if (!prompt.trim()) return withLive({});
6746
6900
  const config = ctx.store.config();
6747
- const view = localView(v2, config, ctx.cwd);
6901
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6748
6902
  const injected = inContext(session);
6749
6903
  const named = invokedSkill(prompt, view.sections);
6750
6904
  if (named) {
@@ -6832,7 +6986,7 @@ function onPreToolUse(ctx) {
6832
6986
  const raw = toolPaths(ctx.input.tool_input);
6833
6987
  if (!raw.length) return withLive({});
6834
6988
  const config = ctx.store.config();
6835
- const view = localView(v2, config, ctx.cwd);
6989
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6836
6990
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
6837
6991
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
6838
6992
  if (!paths.length && !text2) return withLive({});
@@ -6957,6 +7111,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6957
7111
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
6958
7112
  cliVersion: version(),
6959
7113
  ...source ? { source } : {},
7114
+ ...session.borrowed?.length ? { borrowed: session.borrowed } : {},
6960
7115
  uploaded: false,
6961
7116
  ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
6962
7117
  ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
@@ -7265,6 +7420,7 @@ var init_project_hooks = __esm({
7265
7420
  // src/init.ts
7266
7421
  var init_exports = {};
7267
7422
  __export(init_exports, {
7423
+ BORROW_COMMAND: () => BORROW_COMMAND,
7268
7424
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
7269
7425
  HOOK_EVENTS: () => HOOK_EVENTS,
7270
7426
  LEARN_COMMAND: () => LEARN_COMMAND,
@@ -7386,18 +7542,18 @@ function mcpLauncher(bin) {
7386
7542
  }
7387
7543
  return { command: process.execPath, args: [script, "mcp"] };
7388
7544
  }
7389
- function installLearnCommand({ file, remove = false }) {
7545
+ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7390
7546
  try {
7391
7547
  const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7392
- const ours = current === null || current === LEARN_COMMAND;
7548
+ const ours = current === null || current === content;
7393
7549
  if (remove) {
7394
7550
  if (current !== null && ours) fs21.unlinkSync(file);
7395
7551
  return { file, installed: 0, changed: current !== null && ours };
7396
7552
  }
7397
- 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` };
7398
7554
  if (current === null) {
7399
7555
  fs21.mkdirSync(path16.dirname(file), { recursive: true });
7400
- fs21.writeFileSync(file, LEARN_COMMAND);
7556
+ fs21.writeFileSync(file, content);
7401
7557
  }
7402
7558
  return { file, installed: 1, changed: current === null };
7403
7559
  } catch (err) {
@@ -7519,11 +7675,20 @@ function runInit(opts = {}) {
7519
7675
  learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7520
7676
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7521
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
+ }
7522
7687
  if (!resolveBackend(config)) {
7523
7688
  warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
7524
7689
  }
7525
7690
  store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
7526
- 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 };
7527
7692
  }
7528
7693
  function otherHarnessInstructions() {
7529
7694
  return [
@@ -7539,7 +7704,7 @@ function otherHarnessInstructions() {
7539
7704
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7540
7705
  ].join("\n");
7541
7706
  }
7542
- 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;
7543
7708
  var init_init = __esm({
7544
7709
  "src/init.ts"() {
7545
7710
  "use strict";
@@ -7565,6 +7730,7 @@ var init_init = __esm({
7565
7730
  { event: "SessionEnd", arg: "session-end", timeout: 3 }
7566
7731
  ];
7567
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';
7568
7734
  }
7569
7735
  });
7570
7736
 
@@ -7586,19 +7752,19 @@ function mark(v2, sessionId, cwd, field, ids) {
7586
7752
  } catch {
7587
7753
  }
7588
7754
  }
7589
- async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3 }) {
7755
+ async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3, across = false }) {
7590
7756
  const v2 = new V2Store(store);
7591
7757
  const config = store.config();
7592
7758
  const sid = sessionId ?? sessionIdFromEnv(v2);
7593
7759
  const session = sid ? v2.readSession(sid) : null;
7594
7760
  const exclude = session ? [...session.loaded] : [];
7595
7761
  const limit = Math.max(1, Math.min(10, query.limit ?? 5));
7596
- const view = localView(v2, config, cwd);
7597
- 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 });
7598
7764
  let out;
7599
7765
  if (canCall(config)) {
7600
7766
  try {
7601
- 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 });
7602
7768
  const remote = Array.isArray(res.candidates) ? res.candidates : [];
7603
7769
  const extra = local().filter((c) => c.id.startsWith("local:") && !remote.some((r) => r.id === c.id));
7604
7770
  out = { candidates: [...remote, ...extra].slice(0, limit), source: "mothership" };
@@ -7613,7 +7779,8 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7613
7779
  const v2 = new V2Store(store);
7614
7780
  const config = store.config();
7615
7781
  const sid = sessionId ?? sessionIdFromEnv(v2);
7616
- 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 } : {});
7617
7784
  let section = view.byId.get(id);
7618
7785
  let source = "local";
7619
7786
  if (!section && canCall(config)) {
@@ -7626,8 +7793,17 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7626
7793
  }
7627
7794
  if (!section) return null;
7628
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
+ }
7629
7805
  const part = anchor?.replace(/^[§#]/, "");
7630
- 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 } : {} };
7631
7807
  }
7632
7808
  var init_library = __esm({
7633
7809
  "src/v2/library.ts"() {
@@ -7761,18 +7937,19 @@ async function cmdReviewDecide(store, args, out, err) {
7761
7937
  async function cmdRecall(store, args, cwd, out, err) {
7762
7938
  const abstract = args._.slice(1).join(" ").trim() || str3(args.flags.abstract) || "";
7763
7939
  if (!abstract) {
7764
- 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');
7765
7941
  return 1;
7766
7942
  }
7767
7943
  const limit = Number(str3(args.flags.limit)) || void 0;
7768
- 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 });
7769
7946
  if (bool2(args.flags.json)) {
7770
7947
  out(JSON.stringify(res, null, 2) + "\n");
7771
7948
  return 0;
7772
7949
  }
7773
- out(renderCandidates(res.candidates) + "\n");
7950
+ out(renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes) + "\n");
7774
7951
  if (res.candidates.length) out(`
7775
- (${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)" : ""}
7776
7953
  `);
7777
7954
  return 0;
7778
7955
  }
@@ -7789,7 +7966,7 @@ async function cmdLoad(store, args, cwd, out, err) {
7789
7966
  `);
7790
7967
  return 1;
7791
7968
  }
7792
- 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");
7793
7970
  return 0;
7794
7971
  }
7795
7972
  async function cmdLearn(store, args, cwd, out, err) {
@@ -8347,8 +8524,8 @@ async function handleMcp(msg, opts) {
8347
8524
  if (!taskAbstract.trim()) return reply(text("task_abstract is required: describe the task abstractly.", true));
8348
8525
  const identifiers = Array.isArray(args.identifiers) ? args.identifiers.filter((x) => typeof x === "string") : [];
8349
8526
  const limit = typeof args.limit === "number" ? args.limit : void 0;
8350
- const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd });
8351
- 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);
8352
8529
  return reply(text(res.candidates.length ? `${body}
8353
8530
 
8354
8531
  Load the ones that would change what you do with bunshin_load (0-3).` : body));
@@ -8405,6 +8582,7 @@ var init_mcp = __esm({
8405
8582
  init_v22();
8406
8583
  init_source();
8407
8584
  init_store();
8585
+ init_store2();
8408
8586
  init_library();
8409
8587
  DEFAULT_PROTOCOL = "2025-06-18";
8410
8588
  MCP_TOOLS = [
@@ -8418,7 +8596,8 @@ var init_mcp = __esm({
8418
8596
  task_abstract: { type: "string", description: "The task in abstract, structural terms (what kind of problem this is), not a restatement of its topic." },
8419
8597
  hypothetical_lesson: { type: "string", description: "The lesson you would want to find, written as if it existed." },
8420
8598
  identifiers: { type: "array", items: { type: "string" }, description: "Concrete identifiers involved: function names, error codes, file paths, product or system names. May be empty." },
8421
- 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." }
8422
8601
  },
8423
8602
  required: ["task_abstract", "hypothetical_lesson", "identifiers"],
8424
8603
  additionalProperties: false
@@ -9627,7 +9806,7 @@ function effortOf(job) {
9627
9806
  const tokens = Math.round(chars / 4);
9628
9807
  const minutes = Math.max(2, Math.round(2 + tokens / 15e3));
9629
9808
  const proposals = (job.prompt.match(/^### g_\w+/gm) ?? []).length;
9630
- 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`;
9631
9810
  return `${chars.toLocaleString("en")} chars (~${tokens.toLocaleString("en")} tokens); about ${minutes} min. ${what}.`;
9632
9811
  }
9633
9812
  function jobIndex(text2) {
@@ -10115,7 +10294,7 @@ var init_runner = __esm({
10115
10294
  init_trust();
10116
10295
  init_version();
10117
10296
  DEFAULT_API = "https://api.bunshin.cc";
10118
- KINDS2 = ["reflect", "merge", "create"];
10297
+ KINDS2 = ["reflect", "merge", "create", "generalize"];
10119
10298
  str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
10120
10299
  LAUNCHD_LABEL = "cc.bunshin.runner";
10121
10300
  CRON_MARK = "# bunshin-runner";
@@ -11146,7 +11325,7 @@ import fs32 from "node:fs";
11146
11325
  import path27 from "node:path";
11147
11326
  function parseArgs(argv) {
11148
11327
  const out = { _: [], flags: {} };
11149
- const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace"]);
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"]);
11150
11329
  for (let i = 0; i < argv.length; i++) {
11151
11330
  const a = argv[i];
11152
11331
  if (a === "--") {
@@ -11285,6 +11464,8 @@ Key saved privately outside the project.
11285
11464
  if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
11286
11465
  `);
11287
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)
11288
11469
  `);
11289
11470
  else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
11290
11471
  `);
@@ -11860,10 +12041,12 @@ Usage: bunshin <command> [options]
11860
12041
  status [--json] store, context, library, outbox, hooks and sync health
11861
12042
  sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
11862
12043
  which folders under this store's folder take part (learning pool)
11863
- recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
11864
- pull candidates from the library (offline: local view)
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)
11865
12047
  load <id> [--anchor <heading>] [--json]
11866
- 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
11867
12050
  learn [focus...] capture what this session learned now (fork reflection),
11868
12051
  apply it locally as a trial and submit it
11869
12052
  review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
@@ -11883,9 +12066,8 @@ Usage: bunshin <command> [options]
11883
12066
  the deployed skills; push proposes local edits / a new skill now
11884
12067
  library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
11885
12068
  library versions (lib@N), history, and exact reproductions
11886
- library workspace [<dir>|--clear]
11887
- deploy skills and instructions into a workspace folder (e.g.
11888
- ~/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)
11889
12071
  ui [--session <id>] [--json] [--watch] what Bunshin did this session, as the person sees it
11890
12072
  ui status | install | uninstall the Claude Code mod (band above the prompt, /bunshin, toasts)
11891
12073
  ui enable|disable --harness <h> [--mode auto|mod|widget|lines|off] how a harness shows it
@@ -11896,9 +12078,9 @@ Usage: bunshin <command> [options]
11896
12078
  repo import|attach|clone|verify|list|url|login
11897
12079
  host a git repository in Bunshin (full history; git push/pull
11898
12080
  keep working through git-remote-bunshin). bunshin repo help
11899
- 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]
11900
12082
  answer the company's queued model work (cloud reflection,
11901
- reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
12083
+ reconciliation, generalization) with this machine's model. BUNSHIN_RUNNER_KEY
11902
12084
  runner next | submit <job> <file|-> | fail <job> --error <why>
11903
12085
  agent mode, for a Claude Code routine or Codex cloud task
11904
12086
  runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall