@bunshin-cc/bunshin 0.7.0 → 0.8.1

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 +862 -659
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1074,6 +1074,112 @@ 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 parentScope(tree, layer) {
1131
+ const k = layerKind(layer);
1132
+ if (k === "source") {
1133
+ const g = tree.sources[layerValue(layer)]?.group ?? null;
1134
+ return g && tree.groups.some((x) => x.id === g) ? groupLayer(g) : "org";
1135
+ }
1136
+ if (k === "group") {
1137
+ const g = tree.groups.find((x) => x.id === layerValue(layer));
1138
+ return g?.parent && tree.groups.some((x) => x.id === g.parent) ? groupLayer(g.parent) : "org";
1139
+ }
1140
+ return null;
1141
+ }
1142
+ function ancestorScopes(tree, layer) {
1143
+ const out = [];
1144
+ for (let cur = parentScope(tree, layer); cur && !out.includes(cur); cur = parentScope(tree, cur)) out.push(cur);
1145
+ return out;
1146
+ }
1147
+ function sourcesIn(tree, layer) {
1148
+ if (layer === "org") return Object.keys(tree.sources);
1149
+ if (layerKind(layer) === "source") return [layerValue(layer)];
1150
+ if (layerKind(layer) !== "group") return [];
1151
+ const gid = layerValue(layer);
1152
+ return Object.keys(tree.sources).filter((sid) => groupChain(tree, tree.sources[sid].group).includes(gid));
1153
+ }
1154
+ function scopeLabel(layer, tree) {
1155
+ const k = layerKind(layer);
1156
+ if (layer === "org") return "everywhere";
1157
+ if (k === "source") {
1158
+ const sid = layerValue(layer);
1159
+ return `${parseSource(sid)?.kind === "robot" ? "robot" : "project"} ${sourceName(sid)}`;
1160
+ }
1161
+ if (k === "group") return `group ${tree?.groups.find((g) => g.id === layerValue(layer))?.name ?? layerValue(layer)}`;
1162
+ if (k === "user") return "personal";
1163
+ return layer;
1164
+ }
1165
+ function scopeChoices(tree, source) {
1166
+ const t = tree ?? emptyTree();
1167
+ return scopePath(t, source).map((layer) => {
1168
+ const n = sourcesIn(t, layer).length;
1169
+ return { layer, label: layerKind(layer) === "source" ? `this ${scopeLabel(layer, t)} only` : `${scopeLabel(layer, t)} (${n} project${n === 1 ? "" : "s"})` };
1170
+ });
1171
+ }
1172
+ var groupLayer, sourceLayer;
1173
+ var init_scopes = __esm({
1174
+ "../core/src/v2/scopes.ts"() {
1175
+ "use strict";
1176
+ init_layers();
1177
+ init_sources();
1178
+ groupLayer = (id) => `group:${id}`;
1179
+ sourceLayer = (id) => `source:${id}`;
1180
+ }
1181
+ });
1182
+
1077
1183
  // ../core/src/v2/layers.ts
1078
1184
  function layerKind(layer) {
1079
1185
  const kind = layer.split(":")[0];
@@ -1083,19 +1189,29 @@ function layerValue(layer) {
1083
1189
  const i = layer.indexOf(":");
1084
1190
  return i < 0 ? "" : layer.slice(i + 1);
1085
1191
  }
1192
+ function privateLayer(ctx) {
1193
+ return ctx.kind === "robot" ? `robot:${ctx.id}` : `user:${ctx.userId}`;
1194
+ }
1086
1195
  function layersFor(ctx) {
1087
- if (ctx.kind === "robot" && ctx.robot) {
1088
- return ["fleet", `arm:${ctx.robot.armModel}`, `mount:${ctx.robot.mount}`, `robot:${ctx.id}`];
1089
- }
1090
- const team = ctx.person?.team ?? "";
1091
- const dept = department(team);
1092
- return ["org", ...dept ? [`dept:${dept}`] : [], ...team ? [`team:${team}`] : [], `user:${ctx.userId}`];
1196
+ return ["org", privateLayer(ctx)];
1093
1197
  }
1094
- function readerFor(ctx, ownerClearance) {
1198
+ function isLocalLayer(layer) {
1199
+ const kind = layerKind(layer);
1200
+ return kind === "user" || kind === "robot";
1201
+ }
1202
+ function withScope(layers, scope) {
1203
+ const path28 = scopePath(scope?.tree, scope?.source);
1204
+ if (!path28.length) return layers;
1205
+ const i = layers.findIndex((l) => isLocalLayer(l));
1206
+ return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
1207
+ }
1208
+ function readerFor(ctx, ownerClearance, scope) {
1209
+ if (ctx.kind === "robot" && !scope?.source) scope = { ...scope, source: `robot:${ctx.id}` };
1210
+ const source = scope?.source ? { source: scope.source } : {};
1095
1211
  if (ctx.kind === "robot") {
1096
1212
  const rank = (c) => c === "private" ? 0 : CLEARANCE_RANK[c];
1097
1213
  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 } : {} };
1214
+ 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
1215
  }
1100
1216
  return {
1101
1217
  userId: ctx.userId,
@@ -1104,7 +1220,8 @@ function readerFor(ctx, ownerClearance) {
1104
1220
  team: ctx.person?.team ?? "",
1105
1221
  clearance: ctx.person?.clearance ?? "team",
1106
1222
  contextId: ctx.id,
1107
- layers: layersFor(ctx)
1223
+ layers: withScope(layersFor(ctx), scope),
1224
+ ...source
1108
1225
  };
1109
1226
  }
1110
1227
  function explainSectionAccess(section, reader) {
@@ -1112,9 +1229,6 @@ function explainSectionAccess(section, reader) {
1112
1229
  const kind = layerKind(section.layer);
1113
1230
  if (kind === "user" && section.layer !== `user:${reader.userId}`) return { allowed: false, reason: "other-user" };
1114
1231
  if (kind === "robot" && section.layer !== `robot:${reader.contextId}`) return { allowed: false, reason: "other-robot" };
1115
- if ((kind === "arm" || kind === "mount") && reader.role === "robot" && !reader.layers.includes(section.layer)) {
1116
- return { allowed: false, reason: "other-hardware" };
1117
- }
1118
1232
  const access = explainAccess(
1119
1233
  {
1120
1234
  clearance: section.clearance,
@@ -1134,33 +1248,26 @@ function scopeAffinity(section, reader) {
1134
1248
  }
1135
1249
  function baseAffinity(section, reader) {
1136
1250
  const kind = layerKind(section.layer);
1137
- if (reader.layers.includes(section.layer)) {
1138
- const order = reader.role === "robot" ? FLEET_LAYER_ORDER : COMPANY_LAYER_ORDER;
1139
- return 0.5 + 0.5 * (order.indexOf(kind) / Math.max(1, order.length - 1));
1140
- }
1141
- if (kind === "team") {
1142
- const team = layerValue(section.layer);
1143
- if (department(team) === department(reader.team)) return 0.3;
1144
- return 0.05;
1251
+ if (kind === "group" || kind === "source") {
1252
+ if (!reader.layers.includes(section.layer)) return 0.05;
1253
+ const path28 = reader.layers.filter((l) => {
1254
+ const k = layerKind(l);
1255
+ return k === "group" || k === "source";
1256
+ });
1257
+ return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
1145
1258
  }
1146
- if (kind === "dept") return 0.1;
1147
- return 0.2;
1259
+ if (isLocalLayer(section.layer)) return reader.layers.includes(section.layer) ? 1 : 0;
1260
+ return 0.5;
1148
1261
  }
1149
1262
  function reachesReader(section, reader) {
1150
- if (reader.layers.includes(section.layer) || section.kind === "guardrail") return true;
1151
- const kind = layerKind(section.layer);
1152
- if (kind === "org" || kind === "dept" || FLEET_LAYER_ORDER.includes(kind)) return canRead(section, reader);
1153
- if (kind !== "team") return false;
1154
- return [layerValue(section.layer), ...section.allowedTeams ?? []].some((t) => !!t && orgUnitUnder(reader.team, t));
1263
+ return reader.layers.includes(section.layer) && canRead(section, reader);
1155
1264
  }
1156
- var COMPANY_LAYER_ORDER, FLEET_LAYER_ORDER;
1157
1265
  var init_layers = __esm({
1158
1266
  "../core/src/v2/layers.ts"() {
1159
1267
  "use strict";
1160
1268
  init_permissions();
1161
1269
  init_text();
1162
- COMPANY_LAYER_ORDER = ["org", "dept", "team", "user"];
1163
- FLEET_LAYER_ORDER = ["fleet", "arm", "mount", "robot"];
1270
+ init_scopes();
1164
1271
  }
1165
1272
  });
1166
1273
 
@@ -1344,6 +1451,9 @@ var init_document = __esm({
1344
1451
  });
1345
1452
 
1346
1453
  // ../core/src/v2/sections/markdown.ts
1454
+ function repoLayer(repo) {
1455
+ return `source:${repo.includes("/") ? "repo" : "folder"}:${repo}`;
1456
+ }
1347
1457
  function repoSectionId(repo, path28, anchor = "") {
1348
1458
  return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
1349
1459
  }
@@ -1376,7 +1486,7 @@ function parseRepoDoc(doc, opts) {
1376
1486
  id: meta.id ? repoSectionId(doc.repo, doc.path, meta.id) : repoSectionId(doc.repo, doc.path),
1377
1487
  orgId: opts.orgId,
1378
1488
  kind,
1379
- layer: `team:${owner}`,
1489
+ layer: opts.layer ?? repoLayer(doc.repo),
1380
1490
  title,
1381
1491
  body: doc.text,
1382
1492
  when,
@@ -1748,8 +1858,8 @@ function servable(sections, reader) {
1748
1858
  }
1749
1859
  return [...out.values()];
1750
1860
  }
1751
- function recallable(sections, reader) {
1752
- return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
1861
+ function recallable(sections, reader, opts = {}) {
1862
+ return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect" && (opts.across || inBoundary(s, reader)));
1753
1863
  }
1754
1864
  function passagesOfSection(s) {
1755
1865
  const hit = passageCache.get(s);
@@ -1797,6 +1907,7 @@ function passageDense(embedder, pool, q) {
1797
1907
  function sourceLabel(s) {
1798
1908
  if (s.storage.type === "repo") return `repo:${s.storage.repo}/${s.storage.path}`;
1799
1909
  if (s.storage.type === "harness") return `harness:${s.storage.path}`;
1910
+ if (isScopeLayer(s.layer)) return s.layer;
1800
1911
  return s.owner.team || s.layer;
1801
1912
  }
1802
1913
  function push(sections, reader, query) {
@@ -1837,7 +1948,7 @@ function maxSim(embedder, q, lists) {
1837
1948
  function pull(sections, reader, query, opts = {}) {
1838
1949
  const embedders = opts.embedders ?? DEFAULT_EMBEDDERS;
1839
1950
  const exclude = new Set(opts.exclude ?? []);
1840
- const pool = recallable(sections, reader).filter((s) => !exclude.has(s.id));
1951
+ const pool = recallable(sections, reader, opts).filter((s) => !exclude.has(s.id));
1841
1952
  if (!pool.length) return [];
1842
1953
  const limit = Math.max(1, Math.min(20, query.limit ?? 10));
1843
1954
  const signals = [];
@@ -1878,6 +1989,7 @@ function pull(sections, reader, query, opts = {}) {
1878
1989
  kind: section.kind,
1879
1990
  layer: section.layer,
1880
1991
  source: sourceLabel(section),
1992
+ ...isScopeLayer(section.layer) && !reader.layers.includes(section.layer) ? { outside: true } : {},
1881
1993
  score: Math.round(score2 * 1e3) / 1e3,
1882
1994
  reasons,
1883
1995
  ...part.get(section.id) ? { part: part.get(section.id) } : {},
@@ -1946,9 +2058,9 @@ function renderSection(s, trigger, opts = {}) {
1946
2058
  `_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
2059
  ].join("\n");
1948
2060
  }
1949
- function renderCandidates(c) {
2061
+ function renderCandidates(c, tree) {
1950
2062
  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` : ""})
2063
+ 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
2064
  when: ${x.when}`).join("\n");
1953
2065
  }
1954
2066
  var PASSAGE_CHARS, passageCache, words;
@@ -1957,6 +2069,7 @@ var init_engine = __esm({
1957
2069
  "use strict";
1958
2070
  init_text();
1959
2071
  init_layers();
2072
+ init_scopes();
1960
2073
  init_document();
1961
2074
  init_text_index();
1962
2075
  PASSAGE_CHARS = 4e3;
@@ -1967,8 +2080,9 @@ var init_engine = __esm({
1967
2080
 
1968
2081
  // ../core/src/v2/guidelines.ts
1969
2082
  function reflectionGuidelines(sections, reader, opts = {}) {
2083
+ const covering = opts.layer ? /* @__PURE__ */ new Set(["org", opts.layer, ...opts.tree ? ancestorScopes(opts.tree, opts.layer) : []]) : void 0;
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) => !covering || covering.has(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, group: 1, source: 2, robot: 3, user: 3 })[layerKind(l)] ?? 4;
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,25 @@ 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"() {
3124
+ "use strict";
3125
+ init_layers();
3126
+ init_guidelines();
3127
+ init_reflect();
3128
+ init_scopes();
3129
+ }
3130
+ });
3131
+
3132
+ // ../core/src/v2/adopt.ts
3133
+ var init_adopt = __esm({
3134
+ "../core/src/v2/adopt.ts"() {
3002
3135
  "use strict";
3136
+ init_text();
3137
+ init_layers();
3138
+ init_scopes();
3139
+ init_sources();
3003
3140
  }
3004
3141
  });
3005
3142
 
@@ -3025,6 +3162,9 @@ var init_v2 = __esm({
3025
3162
  init_profile();
3026
3163
  init_guidelines();
3027
3164
  init_sources();
3165
+ init_scopes();
3166
+ init_generalize();
3167
+ init_adopt();
3028
3168
  }
3029
3169
  });
3030
3170
 
@@ -3123,72 +3263,354 @@ var init_api = __esm({
3123
3263
  }
3124
3264
  });
3125
3265
 
3126
- // ../core/src/types.ts
3127
- var init_types2 = __esm({
3128
- "../core/src/types.ts"() {
3129
- "use strict";
3266
+ // src/v2/repo.ts
3267
+ import fs6 from "node:fs";
3268
+ import path5 from "node:path";
3269
+ import { execFileSync } from "node:child_process";
3270
+ function git(cwd, args, timeout = 1500) {
3271
+ try {
3272
+ return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
3273
+ } catch {
3274
+ return null;
3130
3275
  }
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
- );
3276
+ }
3277
+ function repoRoot(cwd, storeRoot) {
3278
+ const key = `${cwd}
3279
+ ${storeRoot ?? ""}`;
3280
+ const hit = rootCache.get(key);
3281
+ if (hit) return hit;
3282
+ const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
3283
+ let root = top || "";
3284
+ if (!root && storeRoot && path5.basename(storeRoot) === ".bunshin") {
3285
+ const parent = path5.dirname(storeRoot);
3286
+ if (path5.resolve(cwd).startsWith(path5.resolve(parent))) root = parent;
3143
3287
  }
3144
- });
3145
-
3146
- // ../core/src/metrics.ts
3147
- var init_metrics = __esm({
3148
- "../core/src/metrics.ts"() {
3149
- "use strict";
3288
+ root = root || path5.resolve(cwd);
3289
+ rootCache.set(key, root);
3290
+ return root;
3291
+ }
3292
+ function parseRemote(url) {
3293
+ const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
3294
+ if (!clean2) return null;
3295
+ const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
3296
+ const pathPart = scp ? scp[1] : (() => {
3297
+ try {
3298
+ return new URL(clean2).pathname;
3299
+ } catch {
3300
+ return clean2;
3301
+ }
3302
+ })();
3303
+ const segs = pathPart.split("/").filter(Boolean);
3304
+ if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
3305
+ return segs[0] ?? null;
3306
+ }
3307
+ function repoName(root) {
3308
+ const url = git(root, ["remote", "get-url", "origin"]);
3309
+ return url && parseRemote(url) || path5.basename(root);
3310
+ }
3311
+ function repoCommit(root) {
3312
+ return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
3313
+ }
3314
+ function listDocs(root) {
3315
+ const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
3316
+ 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();
3317
+ const out = [];
3318
+ let visited = 0;
3319
+ const walk = (dir, rel, depth) => {
3320
+ if (depth > 8 || visited++ > 2e4) return;
3321
+ let entries;
3322
+ try {
3323
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
3324
+ } catch {
3325
+ return;
3326
+ }
3327
+ for (const e of entries) {
3328
+ if (e.isDirectory()) {
3329
+ 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);
3330
+ } else if (e.isFile()) {
3331
+ const r = rel ? `${rel}/${e.name}` : e.name;
3332
+ if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
3333
+ }
3334
+ }
3335
+ };
3336
+ walk(root, "", 0);
3337
+ return out.sort();
3338
+ }
3339
+ function indexWorkingTree(v2, config, cwd, opts = {}) {
3340
+ const root = repoRoot(cwd, v2.store.root);
3341
+ const now = opts.now ?? Date.now();
3342
+ const cached = v2.readRepoIndex(root);
3343
+ const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
3344
+ const source = sourceFor(root, v2.store.root);
3345
+ const sameRepo = cached && cached.ownerTeam === ownerTeam && cached.format === INDEX_FORMAT;
3346
+ const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
3347
+ const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
3348
+ const paths = fresh ? cached.paths : listDocs(root);
3349
+ let changed = !fresh || !sameRepo || cached.repo !== repo;
3350
+ const files = {};
3351
+ for (const rel of paths) {
3352
+ const abs = path5.join(root, rel);
3353
+ let stat;
3354
+ try {
3355
+ stat = fs6.statSync(abs);
3356
+ } catch {
3357
+ changed = true;
3358
+ continue;
3359
+ }
3360
+ const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
3361
+ if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
3362
+ files[rel] = prev;
3363
+ continue;
3364
+ }
3365
+ changed = true;
3366
+ try {
3367
+ const text2 = fs6.readFileSync(abs, "utf8");
3368
+ files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam, layer: source ? sourceLayer(source) : "org" }) };
3369
+ } catch {
3370
+ }
3150
3371
  }
3151
- });
3152
-
3153
- // ../core/src/lesson/id.ts
3154
- var init_id = __esm({
3155
- "../core/src/lesson/id.ts"() {
3156
- "use strict";
3372
+ const index = {
3373
+ format: INDEX_FORMAT,
3374
+ repo,
3375
+ ownerTeam,
3376
+ listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
3377
+ paths,
3378
+ files
3379
+ };
3380
+ if (changed) {
3381
+ try {
3382
+ v2.writeRepoIndex(root, index);
3383
+ } catch {
3384
+ }
3157
3385
  }
3158
- });
3159
-
3160
- // ../core/src/lesson/markdown.ts
3161
- var init_markdown2 = __esm({
3162
- "../core/src/lesson/markdown.ts"() {
3163
- "use strict";
3386
+ return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
3387
+ }
3388
+ function relativeToRoot(root, file, cwd = root) {
3389
+ if (!file) return null;
3390
+ const abs = path5.isAbsolute(file) ? file : path5.resolve(cwd, file);
3391
+ let real = abs;
3392
+ let realRoot = root;
3393
+ try {
3394
+ realRoot = fs6.realpathSync(root);
3395
+ } catch {
3164
3396
  }
3165
- });
3166
-
3167
- // ../core/src/lesson/index.ts
3168
- var init_lesson = __esm({
3169
- "../core/src/lesson/index.ts"() {
3397
+ try {
3398
+ real = fs6.realpathSync(abs);
3399
+ } catch {
3400
+ try {
3401
+ real = path5.join(fs6.realpathSync(path5.dirname(abs)), path5.basename(abs));
3402
+ } catch {
3403
+ }
3404
+ }
3405
+ for (const [r, f] of [
3406
+ [realRoot, real],
3407
+ [root, abs]
3408
+ ]) {
3409
+ const rel = path5.relative(r, f);
3410
+ if (rel && !rel.startsWith("..") && !path5.isAbsolute(rel)) return rel.split(path5.sep).join("/");
3411
+ }
3412
+ return null;
3413
+ }
3414
+ var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill, INDEX_FORMAT;
3415
+ var init_repo = __esm({
3416
+ "src/v2/repo.ts"() {
3170
3417
  "use strict";
3418
+ init_v22();
3419
+ init_source();
3420
+ DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
3421
+ SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
3422
+ LIST_TTL_MS = 6e4;
3423
+ rootCache = /* @__PURE__ */ new Map();
3424
+ isDoc = (rel) => {
3425
+ const parts = rel.split("/");
3426
+ if (parts.some((p) => SKIP_DIRS.has(p))) return false;
3427
+ return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
3428
+ };
3429
+ deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs6.existsSync(path5.join(root, path5.posix.dirname(rel), ".bunshin.json"));
3430
+ INDEX_FORMAT = 2;
3171
3431
  }
3172
3432
  });
3173
3433
 
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";
3181
- }
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}`;
3189
- }
3190
- function score(lesson, request, profile, textRelevance, opts = {}) {
3191
- const w = { ...DEFAULT_WEIGHTS, ...opts.weights ?? {} };
3434
+ // src/v2/source.ts
3435
+ var source_exports = {};
3436
+ __export(source_exports, {
3437
+ cwdInPool: () => cwdInPool,
3438
+ inPool: () => inPool,
3439
+ listFolders: () => listFolders,
3440
+ projectRootFor: () => projectRootFor,
3441
+ select: () => select,
3442
+ sourceFor: () => sourceFor,
3443
+ storeHome: () => storeHome,
3444
+ topFolder: () => topFolder
3445
+ });
3446
+ import fs7 from "node:fs";
3447
+ import path6 from "node:path";
3448
+ import { execFileSync as execFileSync2 } from "node:child_process";
3449
+ function storeHome(storeRoot) {
3450
+ return path6.dirname(path6.resolve(storeRoot));
3451
+ }
3452
+ function topFolder(cwd, storeRoot) {
3453
+ const rel = path6.relative(storeHome(storeRoot), path6.resolve(cwd));
3454
+ if (!rel || rel.startsWith("..") || path6.isAbsolute(rel)) return "";
3455
+ return rel.split(path6.sep)[0] ?? "";
3456
+ }
3457
+ function inPool(selection, folder) {
3458
+ if (!folder || !selection) return true;
3459
+ if (selection.exclude?.includes(folder)) return false;
3460
+ return selection.only ? selection.only.includes(folder) : true;
3461
+ }
3462
+ function cwdInPool(config, cwd, storeRoot) {
3463
+ return inPool(config.sources, topFolder(cwd, storeRoot));
3464
+ }
3465
+ function git2(cwd, args) {
3466
+ try {
3467
+ return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
3468
+ } catch {
3469
+ return null;
3470
+ }
3471
+ }
3472
+ function sourceFor(cwd, storeRoot, ctx) {
3473
+ if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
3474
+ const key = `${path6.resolve(cwd)}
3475
+ ${storeRoot}`;
3476
+ if (sourceCache.has(key)) return sourceCache.get(key);
3477
+ const found = findSource(cwd, storeRoot);
3478
+ sourceCache.set(key, found);
3479
+ return found;
3480
+ }
3481
+ function findSource(cwd, storeRoot) {
3482
+ const home = storeHome(storeRoot);
3483
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
3484
+ if (top) {
3485
+ const remote = git2(top, ["remote", "get-url", "origin"]);
3486
+ const named = remote ? parseRemote(remote) : null;
3487
+ if (named) return sourceId("repo", named);
3488
+ const rel = path6.relative(home, top);
3489
+ return sourceId("folder", rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel.split(path6.sep).join("/") : path6.basename(top));
3490
+ }
3491
+ const folder = topFolder(cwd, storeRoot);
3492
+ return folder ? sourceId("folder", folder) : void 0;
3493
+ }
3494
+ function projectRootFor(cwd, storeRoot) {
3495
+ const home = storeHome(storeRoot);
3496
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
3497
+ if (top) {
3498
+ const rel = path6.relative(home, top);
3499
+ if (!rel.startsWith("..") && !path6.isAbsolute(rel)) return top;
3500
+ }
3501
+ const folder = topFolder(cwd, storeRoot);
3502
+ return folder ? path6.join(home, folder) : home;
3503
+ }
3504
+ function listFolders(storeRoot, selection, opts = {}) {
3505
+ const home = storeHome(storeRoot);
3506
+ let names = [];
3507
+ try {
3508
+ names = fs7.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
3509
+ } catch {
3510
+ }
3511
+ return names.map((folder) => ({
3512
+ folder,
3513
+ included: inPool(selection, folder),
3514
+ ...opts.sources ? { source: sourceFor(path6.join(home, folder), storeRoot) } : {}
3515
+ }));
3516
+ }
3517
+ function select(current, action, folders) {
3518
+ const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
3519
+ const only = current?.only;
3520
+ const exclude = current?.exclude ?? [];
3521
+ let next;
3522
+ switch (action) {
3523
+ case "all":
3524
+ return void 0;
3525
+ case "only":
3526
+ next = { only: uniq(folders) };
3527
+ break;
3528
+ case "include":
3529
+ next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
3530
+ break;
3531
+ case "exclude":
3532
+ next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
3533
+ break;
3534
+ }
3535
+ if (!next.exclude?.length) delete next.exclude;
3536
+ return next.only || next.exclude ? next : void 0;
3537
+ }
3538
+ var sourceCache;
3539
+ var init_source = __esm({
3540
+ "src/v2/source.ts"() {
3541
+ "use strict";
3542
+ init_v22();
3543
+ init_repo();
3544
+ sourceCache = /* @__PURE__ */ new Map();
3545
+ }
3546
+ });
3547
+
3548
+ // ../core/src/types.ts
3549
+ var init_types2 = __esm({
3550
+ "../core/src/types.ts"() {
3551
+ "use strict";
3552
+ }
3553
+ });
3554
+
3555
+ // ../core/src/gate.ts
3556
+ var CURRENT_CHAT_PREFIX, ANAPHORIC_QUESTION;
3557
+ var init_gate = __esm({
3558
+ "../core/src/gate.ts"() {
3559
+ "use strict";
3560
+ CURRENT_CHAT_PREFIX = "(?:(?:so\\s+)?(?:in|within)\\s+(?:this|the)\\s+(?:chat|conversation|session)\\s*,?\\s*)?";
3561
+ ANAPHORIC_QUESTION = new RegExp(
3562
+ "^" + 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.*)$",
3563
+ "i"
3564
+ );
3565
+ }
3566
+ });
3567
+
3568
+ // ../core/src/metrics.ts
3569
+ var init_metrics = __esm({
3570
+ "../core/src/metrics.ts"() {
3571
+ "use strict";
3572
+ }
3573
+ });
3574
+
3575
+ // ../core/src/lesson/id.ts
3576
+ var init_id = __esm({
3577
+ "../core/src/lesson/id.ts"() {
3578
+ "use strict";
3579
+ }
3580
+ });
3581
+
3582
+ // ../core/src/lesson/markdown.ts
3583
+ var init_markdown2 = __esm({
3584
+ "../core/src/lesson/markdown.ts"() {
3585
+ "use strict";
3586
+ }
3587
+ });
3588
+
3589
+ // ../core/src/lesson/index.ts
3590
+ var init_lesson = __esm({
3591
+ "../core/src/lesson/index.ts"() {
3592
+ "use strict";
3593
+ }
3594
+ });
3595
+
3596
+ // ../core/src/serve/score.ts
3597
+ function classifyTaskMode(task) {
3598
+ const t = (task || "").trim();
3599
+ if (INVESTIGATE_RE.test(t) && !BUILD_RE.test(t)) return "investigate";
3600
+ if (BUILD_RE.test(t)) return "build";
3601
+ if (CONVERSE_RE.test(t) || t.endsWith("?")) return "converse";
3602
+ return "build";
3603
+ }
3604
+ function lessonText(lesson) {
3605
+ const extra = lesson.kind === "workflow" ? [...lesson.preconditions, ...lesson.steps.map((s) => `${s.action} ${s.detail ?? ""} ${s.verify ?? ""}`), lesson.outcome].join(" ") : "";
3606
+ return `${lesson.title}
3607
+ ${lesson.gist}
3608
+ ${lesson.tags.join(" ")}
3609
+ ${lesson.content}
3610
+ ${extra}`;
3611
+ }
3612
+ function score(lesson, request, profile, textRelevance, opts = {}) {
3613
+ const w = { ...DEFAULT_WEIGHTS, ...opts.weights ?? {} };
3192
3614
  const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
3193
3615
  const reasons = [];
3194
3616
  const mode = classifyTaskMode(request.task);
@@ -3643,8 +4065,8 @@ var init_src = __esm({
3643
4065
  });
3644
4066
 
3645
4067
  // src/v2/store.ts
3646
- import fs6 from "node:fs";
3647
- import path5 from "node:path";
4068
+ import fs8 from "node:fs";
4069
+ import path7 from "node:path";
3648
4070
  import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
3649
4071
  function hexId(prefix) {
3650
4072
  return `${prefix}_${randomBytes3(6).toString("hex")}`;
@@ -3678,12 +4100,12 @@ var init_store2 = __esm({
3678
4100
  V2Store = class {
3679
4101
  constructor(store) {
3680
4102
  this.store = store;
3681
- this.dir = path5.join(store.root, "v2");
4103
+ this.dir = path7.join(store.root, "v2");
3682
4104
  }
3683
4105
  store;
3684
4106
  dir;
3685
4107
  p(...parts) {
3686
- return path5.join(this.dir, ...parts);
4108
+ return path7.join(this.dir, ...parts);
3687
4109
  }
3688
4110
  // ── context / bundle / params ──
3689
4111
  readContext() {
@@ -3764,9 +4186,9 @@ var init_store2 = __esm({
3764
4186
  const dir = this.p("sessions");
3765
4187
  if (!fileExists(dir)) return [];
3766
4188
  const out = [];
3767
- for (const f of fs6.readdirSync(dir)) {
4189
+ for (const f of fs8.readdirSync(dir)) {
3768
4190
  if (!f.endsWith(".json") || f === "current.json") continue;
3769
- const raw = readJson(path5.join(dir, f), null);
4191
+ const raw = readJson(path7.join(dir, f), null);
3770
4192
  if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
3771
4193
  }
3772
4194
  return out;
@@ -3794,7 +4216,7 @@ var init_store2 = __esm({
3794
4216
  let at = raw.at;
3795
4217
  if (!at) {
3796
4218
  try {
3797
- at = fs6.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
4219
+ at = fs8.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
3798
4220
  } catch {
3799
4221
  at = (/* @__PURE__ */ new Date()).toISOString();
3800
4222
  }
@@ -3821,13 +4243,13 @@ var init_store2 = __esm({
3821
4243
  /** Exclusive lock file; returns a release function, or null when another process holds it. */
3822
4244
  tryLock(name, staleMs = 10 * 6e4) {
3823
4245
  const file = this.p("locks", `${fileSafe(name)}.lock`);
3824
- ensureDir(path5.dirname(file));
4246
+ ensureDir(path7.dirname(file));
3825
4247
  for (let attempt = 0; attempt < 2; attempt++) {
3826
4248
  try {
3827
- fs6.writeFileSync(file, String(process.pid), { flag: "wx" });
4249
+ fs8.writeFileSync(file, String(process.pid), { flag: "wx" });
3828
4250
  return () => {
3829
4251
  try {
3830
- fs6.unlinkSync(file);
4252
+ fs8.unlinkSync(file);
3831
4253
  } catch {
3832
4254
  }
3833
4255
  };
@@ -3835,8 +4257,8 @@ var init_store2 = __esm({
3835
4257
  if (err.code !== "EEXIST") return null;
3836
4258
  let stale = false;
3837
4259
  try {
3838
- const owner = Number(fs6.readFileSync(file, "utf8"));
3839
- const age = Date.now() - fs6.statSync(file).mtimeMs;
4260
+ const owner = Number(fs8.readFileSync(file, "utf8"));
4261
+ const age = Date.now() - fs8.statSync(file).mtimeMs;
3840
4262
  if (age > staleMs) stale = true;
3841
4263
  else if (Number.isInteger(owner) && owner > 0) {
3842
4264
  try {
@@ -3850,7 +4272,7 @@ var init_store2 = __esm({
3850
4272
  }
3851
4273
  if (!stale) return null;
3852
4274
  try {
3853
- fs6.unlinkSync(file);
4275
+ fs8.unlinkSync(file);
3854
4276
  } catch {
3855
4277
  }
3856
4278
  }
@@ -3867,15 +4289,15 @@ var init_store2 = __esm({
3867
4289
  const dir = this.p(name);
3868
4290
  if (!fileExists(dir)) return [];
3869
4291
  const out = [];
3870
- for (const f of fs6.readdirSync(dir)) {
4292
+ for (const f of fs8.readdirSync(dir)) {
3871
4293
  if (!f.endsWith(".json")) continue;
3872
- const v = readJson(path5.join(dir, f), null);
4294
+ const v = readJson(path7.join(dir, f), null);
3873
4295
  if (v) out.push(v);
3874
4296
  }
3875
4297
  return out;
3876
4298
  }
3877
4299
  };
3878
- rootKey = (root) => createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 16);
4300
+ rootKey = (root) => createHash2("sha256").update(path7.resolve(root)).digest("hex").slice(0, 16);
3879
4301
  addUnique = (list2, ...ids) => {
3880
4302
  for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
3881
4303
  };
@@ -3883,15 +4305,15 @@ var init_store2 = __esm({
3883
4305
  });
3884
4306
 
3885
4307
  // src/v2/deploy.ts
3886
- import { execFileSync } from "node:child_process";
4308
+ import { execFileSync as execFileSync3 } from "node:child_process";
3887
4309
  import { createHash as createHash3 } from "node:crypto";
3888
- import fs7 from "node:fs";
4310
+ import fs9 from "node:fs";
3889
4311
  import os2 from "node:os";
3890
- import path6 from "node:path";
4312
+ import path8 from "node:path";
3891
4313
  function deployWorkspace(env = process.env) {
3892
- if (env.BUNSHIN_DEPLOY_WORKSPACE) return path6.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
4314
+ if (env.BUNSHIN_DEPLOY_WORKSPACE) return path8.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
3893
4315
  try {
3894
- const ws = JSON.parse(fs7.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
4316
+ const ws = JSON.parse(fs9.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
3895
4317
  return typeof ws === "string" && ws ? ws : null;
3896
4318
  } catch {
3897
4319
  return null;
@@ -3900,24 +4322,24 @@ function deployWorkspace(env = process.env) {
3900
4322
  function deployPaths(env = process.env, orgId) {
3901
4323
  const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
3902
4324
  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;
4325
+ const state = path8.join(home, ".bunshin", "deploy");
4326
+ const shared = { claude: path8.join(ws ?? home, ".claude", "skills"), codex: path8.join(ws ?? home, ".codex", "skills") };
4327
+ const org = orgId ? path8.join(state, orgKey(orgId)) : null;
3906
4328
  return {
3907
4329
  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"),
4330
+ claudeSkills: org ? path8.join(org, "claude-skills") : shared.claude,
4331
+ codexSkills: org ? path8.join(org, "codex-skills") : shared.codex,
4332
+ ...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path8.join(home, ".claude", "skills"), path8.join(home, ".codex", "skills"), path8.join(home, ".bunshin", "library")])] } : {},
4333
+ claudeMd: ws ? path8.join(ws, "CLAUDE.md") : path8.join(home, ".claude", "CLAUDE.md"),
4334
+ agentsMd: ws ? path8.join(ws, "AGENTS.md") : path8.join(home, ".codex", "AGENTS.md"),
4335
+ library: org ? path8.join(org, "library") : path8.join(home, ".bunshin", "library"),
3914
4336
  // Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
3915
- bin: path6.join(home, ".bunshin", "bin"),
4337
+ bin: path8.join(home, ".bunshin", "bin"),
3916
4338
  state
3917
4339
  };
3918
4340
  }
3919
4341
  function statePath(p, orgId) {
3920
- return path6.join(p.state, `${orgKey(orgId)}.json`);
4342
+ return path8.join(p.state, `${orgKey(orgId)}.json`);
3921
4343
  }
3922
4344
  function readState(p, orgId) {
3923
4345
  const s = readJson(statePath(p, orgId), null);
@@ -3925,7 +4347,7 @@ function readState(p, orgId) {
3925
4347
  }
3926
4348
  function readText(file) {
3927
4349
  try {
3928
- return fs7.readFileSync(file, "utf8");
4350
+ return fs9.readFileSync(file, "utf8");
3929
4351
  } catch {
3930
4352
  return null;
3931
4353
  }
@@ -3933,13 +4355,13 @@ function readText(file) {
3933
4355
  function writeFile(file, content, mode) {
3934
4356
  writeTextAtomic(file, content);
3935
4357
  try {
3936
- fs7.chmodSync(file, mode === "x" ? 493 : 420);
4358
+ fs9.chmodSync(file, mode === "x" ? 493 : 420);
3937
4359
  } catch {
3938
4360
  }
3939
4361
  }
3940
4362
  function removeFile(file) {
3941
4363
  try {
3942
- fs7.unlinkSync(file);
4364
+ fs9.unlinkSync(file);
3943
4365
  } catch {
3944
4366
  }
3945
4367
  }
@@ -3947,33 +4369,38 @@ function pruneDirs(dir, stop) {
3947
4369
  let d = dir;
3948
4370
  while (d.startsWith(stop) && d !== stop) {
3949
4371
  try {
3950
- if (fs7.readdirSync(d).length) return;
3951
- fs7.rmdirSync(d);
4372
+ if (fs9.readdirSync(d).length) return;
4373
+ fs9.rmdirSync(d);
3952
4374
  } catch {
3953
4375
  return;
3954
4376
  }
3955
- d = path6.dirname(d);
4377
+ d = path8.dirname(d);
3956
4378
  }
3957
4379
  }
3958
4380
  function listFiles(dir, base = dir) {
3959
4381
  let entries;
3960
4382
  try {
3961
- entries = fs7.readdirSync(dir, { withFileTypes: true });
4383
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
3962
4384
  } catch {
3963
4385
  return [];
3964
4386
  }
3965
4387
  const out = [];
3966
4388
  for (const e of entries) {
3967
- const abs = path6.join(dir, e.name);
4389
+ const abs = path8.join(dir, e.name);
3968
4390
  if (e.isDirectory()) out.push(...listFiles(abs, base));
3969
- else if (e.isFile()) out.push(path6.relative(base, abs).split(path6.sep).join("/"));
4391
+ else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
3970
4392
  }
3971
4393
  return out.sort();
3972
4394
  }
3973
4395
  function childDir(root, name) {
3974
4396
  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;
4397
+ const dir = path8.join(root, name);
4398
+ return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
4399
+ }
4400
+ function layerRank(layer, tree) {
4401
+ const kind = layerKind(layer);
4402
+ if (kind === "group") return 1 + (tree ? groupChain(tree, layerValue(layer)).length : 0);
4403
+ return kind === "source" ? 1e3 : kind === "user" || kind === "robot" ? 2e3 : 0;
3977
4404
  }
3978
4405
  function planDeploy(bundle, p, harnesses) {
3979
4406
  const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
@@ -3985,7 +4412,7 @@ function planDeploy(bundle, p, harnesses) {
3985
4412
  continue;
3986
4413
  }
3987
4414
  const prev = byName.get(s.skill.name);
3988
- if (!prev || (LAYER_RANK[layerKind(s.layer)] ?? 0) > (LAYER_RANK[layerKind(prev.layer)] ?? 0)) byName.set(s.skill.name, s);
4415
+ if (!prev || layerRank(s.layer, bundle.scopes) > layerRank(prev.layer, bundle.scopes)) byName.set(s.skill.name, s);
3989
4416
  }
3990
4417
  const skills = [...byName.values()].sort((a, b) => a.skill.name.localeCompare(b.skill.name));
3991
4418
  const instructions = active.filter((s) => s.delivery === "always" && !s.skill && !s.profileOf).sort((a, b) => (a.packPath ?? a.title).localeCompare(b.packPath ?? b.title));
@@ -4080,7 +4507,7 @@ function removeBlock(file) {
4080
4507
  if (start < 0 || end < start) return false;
4081
4508
  const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
4082
4509
  if (rest) writeTextAtomic(file, rest + "\n");
4083
- else fs7.unlinkSync(file);
4510
+ else fs9.unlinkSync(file);
4084
4511
  return true;
4085
4512
  }
4086
4513
  function writeBlock(file, block2) {
@@ -4101,7 +4528,7 @@ function skillIndex(skills, p) {
4101
4528
  "",
4102
4529
  "When a task matches a skill below, read its SKILL.md and follow it before doing the work by hand.",
4103
4530
  "",
4104
- ...skills.map((s) => `- **${s.skill.name}** (\`${path6.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
4531
+ ...skills.map((s) => `- **${s.skill.name}** (\`${path8.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
4105
4532
  ];
4106
4533
  return lines.join("\n");
4107
4534
  }
@@ -4150,7 +4577,7 @@ function captureDoc(c, doc, out) {
4150
4577
  for (const dir of doc.dirs ?? []) {
4151
4578
  for (const rel of listFiles(dir)) {
4152
4579
  if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
4153
- const abs = path6.join(dir, rel);
4580
+ const abs = path8.join(dir, rel);
4154
4581
  const content = readText(abs);
4155
4582
  if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
4156
4583
  byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
@@ -4189,18 +4616,18 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4189
4616
  for (const root of roots) {
4190
4617
  let names = [];
4191
4618
  try {
4192
- names = fs7.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
4619
+ names = fs9.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
4193
4620
  } catch {
4194
4621
  continue;
4195
4622
  }
4196
4623
  for (const name of names) {
4197
- const dir = path6.join(root, name);
4624
+ const dir = path8.join(root, name);
4198
4625
  if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
4199
- const skillMd = readText(path6.join(dir, "SKILL.md"));
4626
+ const skillMd = readText(path8.join(dir, "SKILL.md"));
4200
4627
  if (skillMd === null) continue;
4201
4628
  let born = 0;
4202
4629
  try {
4203
- const st = fs7.statSync(path6.join(dir, "SKILL.md"));
4630
+ const st = fs9.statSync(path8.join(dir, "SKILL.md"));
4204
4631
  born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
4205
4632
  } catch {
4206
4633
  continue;
@@ -4209,8 +4636,8 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
4209
4636
  const files = {};
4210
4637
  for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
4211
4638
  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" } : {} };
4639
+ const content = readText(path8.join(dir, rel));
4640
+ if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir, rel)) ? { mode: "x" } : {} };
4214
4641
  }
4215
4642
  const hash = sha(JSON.stringify([skillMd, files]));
4216
4643
  if (state.created[name]?.hash === hash) continue;
@@ -4258,14 +4685,14 @@ function captureEdits(v2, config, ctx, opts = {}) {
4258
4685
  }
4259
4686
  function lockDeploy(p) {
4260
4687
  ensureDir(p.state);
4261
- const file = path6.join(p.state, ".lock");
4688
+ const file = path8.join(p.state, ".lock");
4262
4689
  for (let i = 0; i < 2; i++) {
4263
4690
  try {
4264
- fs7.writeFileSync(file, String(process.pid), { flag: "wx" });
4691
+ fs9.writeFileSync(file, String(process.pid), { flag: "wx" });
4265
4692
  return () => removeFile(file);
4266
4693
  } catch {
4267
4694
  try {
4268
- const age = Date.now() - fs7.statSync(file).mtimeMs;
4695
+ const age = Date.now() - fs9.statSync(file).mtimeMs;
4269
4696
  const pid = Number(readText(file));
4270
4697
  let alive = age < 12e4;
4271
4698
  if (alive && pid > 0) {
@@ -4332,7 +4759,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4332
4759
  const foreign = /* @__PURE__ */ new Set();
4333
4760
  if (d.kind === "skill" && !prev) {
4334
4761
  for (const dir of d.dirs ?? []) {
4335
- const cur = readText(path6.join(dir, "SKILL.md"));
4762
+ const cur = readText(path8.join(dir, "SKILL.md"));
4336
4763
  if (cur === null || cur === d.section.body || state.created[d.name]) continue;
4337
4764
  foreign.add(dir);
4338
4765
  report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
@@ -4344,7 +4771,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4344
4771
  targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
4345
4772
  continue;
4346
4773
  }
4347
- if ([...foreign].some((dir) => t.abs.startsWith(dir + path6.sep))) continue;
4774
+ if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
4348
4775
  if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
4349
4776
  report.warnings.push(`${t.abs} not written: it leads outside its folder`);
4350
4777
  continue;
@@ -4353,10 +4780,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4353
4780
  const existing = readText(t.abs);
4354
4781
  try {
4355
4782
  if (existing !== content) {
4356
- ensureDir(path6.dirname(t.abs));
4783
+ ensureDir(path8.dirname(t.abs));
4357
4784
  writeFile(t.abs, content, t.mode);
4358
4785
  report.written++;
4359
- } else if (t.mode === "x" && !isExecutable(t.abs)) fs7.chmodSync(t.abs, 493);
4786
+ } else if (t.mode === "x" && !isExecutable(t.abs)) fs9.chmodSync(t.abs, 493);
4360
4787
  } catch (err) {
4361
4788
  report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
4362
4789
  continue;
@@ -4369,19 +4796,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4369
4796
  const cur = readText(old.abs);
4370
4797
  if (cur !== null && sha(cur) === old.sha) {
4371
4798
  removeFile(old.abs);
4372
- pruneDirs(path6.dirname(old.abs), d.kind === "skill" ? path6.dirname(path6.dirname(old.abs)) : p.library);
4799
+ pruneDirs(path8.dirname(old.abs), d.kind === "skill" ? path8.dirname(path8.dirname(old.abs)) : p.library);
4373
4800
  report.removed++;
4374
4801
  }
4375
4802
  }
4376
4803
  if (d.kind === "skill") {
4377
4804
  for (const dir of prev?.dirs ?? []) {
4378
4805
  if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
4379
- removeFile(path6.join(dir, MANIFEST));
4380
- pruneDirs(dir, path6.dirname(dir));
4806
+ removeFile(path8.join(dir, MANIFEST));
4807
+ pruneDirs(dir, path8.dirname(dir));
4381
4808
  }
4382
4809
  for (const dir of d.dirs ?? []) {
4383
4810
  const manifest = safeJoin(dir, MANIFEST);
4384
- if (foreign.has(dir) || !manifest || !fs7.existsSync(path6.join(dir, "SKILL.md"))) continue;
4811
+ if (foreign.has(dir) || !manifest || !fs9.existsSync(path8.join(dir, "SKILL.md"))) continue;
4385
4812
  writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
4386
4813
  }
4387
4814
  report.skills++;
@@ -4406,12 +4833,12 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4406
4833
  }
4407
4834
  for (const dir of doc.dirs ?? []) {
4408
4835
  if (!removable(dir)) continue;
4409
- removeFile(path6.join(dir, MANIFEST));
4410
- pruneDirs(dir, path6.dirname(dir));
4411
- if (fs7.existsSync(dir)) {
4836
+ removeFile(path8.join(dir, MANIFEST));
4837
+ pruneDirs(dir, path8.dirname(dir));
4838
+ if (fs9.existsSync(dir)) {
4412
4839
  const left = listFiles(dir);
4413
4840
  if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
4414
- else pruneDirs(dir, path6.dirname(dir));
4841
+ else pruneDirs(dir, path8.dirname(dir));
4415
4842
  }
4416
4843
  }
4417
4844
  }
@@ -4427,17 +4854,17 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4427
4854
  if (d.kind === "skill") continue;
4428
4855
  for (const t of d.targets) {
4429
4856
  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));
4857
+ const link = path8.join(p.bin, path8.basename(t.abs));
4431
4858
  ensureDir(p.bin);
4432
4859
  try {
4433
- const cur = fs7.readlinkSync(link);
4860
+ const cur = fs9.readlinkSync(link);
4434
4861
  if (cur !== t.abs) {
4435
- fs7.unlinkSync(link);
4436
- fs7.symlinkSync(t.abs, link);
4862
+ fs9.unlinkSync(link);
4863
+ fs9.symlinkSync(t.abs, link);
4437
4864
  }
4438
4865
  } catch {
4439
4866
  try {
4440
- if (!fs7.existsSync(link)) fs7.symlinkSync(t.abs, link);
4867
+ if (!fs9.existsSync(link)) fs9.symlinkSync(t.abs, link);
4441
4868
  } catch {
4442
4869
  }
4443
4870
  }
@@ -4448,19 +4875,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4448
4875
  if (harnesses.has("claude-code")) {
4449
4876
  for (const pack of packs) {
4450
4877
  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");
4878
+ if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
4879
+ const link = path8.join(dir, ".claude", "skills");
4453
4880
  try {
4454
- fs7.lstatSync(link);
4455
- if (fs7.readlinkSync(link) !== p.claudeSkills) {
4456
- fs7.unlinkSync(link);
4457
- fs7.symlinkSync(p.claudeSkills, link);
4881
+ fs9.lstatSync(link);
4882
+ if (fs9.readlinkSync(link) !== p.claudeSkills) {
4883
+ fs9.unlinkSync(link);
4884
+ fs9.symlinkSync(p.claudeSkills, link);
4458
4885
  }
4459
4886
  } catch {
4460
- ensureDir(path6.dirname(link));
4887
+ ensureDir(path8.dirname(link));
4461
4888
  ensureDir(p.claudeSkills);
4462
4889
  try {
4463
- fs7.symlinkSync(p.claudeSkills, link);
4890
+ fs9.symlinkSync(p.claudeSkills, link);
4464
4891
  } catch {
4465
4892
  }
4466
4893
  }
@@ -4469,17 +4896,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
4469
4896
  for (const old of state.links) {
4470
4897
  if (links.has(old)) continue;
4471
4898
  try {
4472
- const target = fs7.readlinkSync(old);
4473
- if (!fs7.existsSync(target)) fs7.unlinkSync(old);
4899
+ const target = fs9.readlinkSync(old);
4900
+ if (!fs9.existsSync(target)) fs9.unlinkSync(old);
4474
4901
  } catch {
4475
4902
  }
4476
4903
  }
4477
4904
  let linked = state.linked ?? [];
4478
4905
  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)] : []])];
4906
+ const deployed = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length);
4907
+ const layerOf = new Map(plan.skills.map((x) => [x.skill.name, x.layer]));
4908
+ const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path8.resolve(opts.linkRoot)] : []])];
4481
4909
  linked = [];
4482
4910
  for (const root of roots) {
4911
+ let source;
4912
+ try {
4913
+ source = sourceFor(root, v2.store.root, ctx);
4914
+ } catch {
4915
+ source = void 0;
4916
+ }
4917
+ const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle.scopes ? { tree: bundle.scopes } : {} });
4918
+ const names = deployed.filter((d) => inBoundary({ layer: layerOf.get(d.name) ?? "org" }, reader)).map((d) => d.name);
4483
4919
  const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
4484
4920
  report.warnings.push(...res.warnings);
4485
4921
  if (res.kept) linked.push(root);
@@ -4500,13 +4936,13 @@ function materialize(sections, dir) {
4500
4936
  const put = (rel, content, mode) => {
4501
4937
  const abs = safeJoin(dir, rel);
4502
4938
  if (!abs) return;
4503
- ensureDir(path6.dirname(abs));
4939
+ ensureDir(path8.dirname(abs));
4504
4940
  writeFile(abs, content, mode);
4505
4941
  files++;
4506
4942
  };
4507
4943
  for (const s of sections.filter((x) => x.status === "active" || x.status === "stale")) {
4508
4944
  if (s.skill) {
4509
- const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path6.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
4945
+ const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path8.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
4510
4946
  put(`${base}/SKILL.md`, s.body);
4511
4947
  for (const [rel, f] of Object.entries(s.skill.files ?? {})) put(`${base}/${rel}`, f.content, f.mode);
4512
4948
  continue;
@@ -4518,51 +4954,51 @@ function materialize(sections, dir) {
4518
4954
  }
4519
4955
  function linkSkills(p, root, harnesses, names) {
4520
4956
  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);
4957
+ const abs = path8.resolve(root);
4958
+ if (abs === path8.resolve(p.home) || abs === path8.resolve(os2.homedir()) || !fs9.existsSync(abs)) return out;
4959
+ const managed = (target) => target.startsWith(path8.resolve(p.state) + path8.sep);
4524
4960
  const excluded = [];
4525
4961
  for (const h of harnesses) {
4526
4962
  const spec = LINK_DIRS[h];
4527
4963
  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"))));
4964
+ const dir = path8.join(abs, spec.rel);
4965
+ const canonical = path8.resolve(p[spec.key]);
4966
+ const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
4531
4967
  let entries = [];
4532
4968
  try {
4533
- entries = fs7.readdirSync(dir, { withFileTypes: true });
4969
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
4534
4970
  } catch {
4535
4971
  }
4536
4972
  for (const e of entries) {
4537
4973
  if (!e.isSymbolicLink()) continue;
4538
- const link = path6.join(dir, e.name);
4974
+ const link = path8.join(dir, e.name);
4539
4975
  let target = "";
4540
4976
  try {
4541
- target = path6.resolve(dir, fs7.readlinkSync(link));
4977
+ target = path8.resolve(dir, fs9.readlinkSync(link));
4542
4978
  } catch {
4543
4979
  continue;
4544
4980
  }
4545
4981
  if (!managed(target)) continue;
4546
- if (want.has(e.name) && target === path6.join(canonical, e.name)) {
4982
+ if (want.has(e.name) && target === path8.join(canonical, e.name)) {
4547
4983
  want.delete(e.name);
4548
- excluded.push(path6.join(spec.rel, e.name));
4984
+ excluded.push(path8.join(spec.rel, e.name));
4549
4985
  continue;
4550
4986
  }
4551
4987
  removeFile(link);
4552
4988
  out.removed++;
4553
4989
  }
4554
4990
  for (const name of want) {
4555
- const link = path6.join(dir, name);
4991
+ const link = path8.join(dir, name);
4556
4992
  try {
4557
- fs7.lstatSync(link);
4993
+ fs9.lstatSync(link);
4558
4994
  out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
4559
4995
  continue;
4560
4996
  } catch {
4561
4997
  }
4562
4998
  try {
4563
4999
  ensureDir(dir);
4564
- fs7.symlinkSync(path6.join(canonical, name), link, "dir");
4565
- excluded.push(path6.join(spec.rel, name));
5000
+ fs9.symlinkSync(path8.join(canonical, name), link, "dir");
5001
+ excluded.push(path8.join(spec.rel, name));
4566
5002
  out.linked++;
4567
5003
  } catch (err) {
4568
5004
  out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
@@ -4575,10 +5011,10 @@ function linkSkills(p, root, harnesses, names) {
4575
5011
  return out;
4576
5012
  }
4577
5013
  function gitExclude(root, rels) {
4578
- if (!fs7.existsSync(path6.join(root, ".git"))) return;
5014
+ if (!fs9.existsSync(path8.join(root, ".git"))) return;
4579
5015
  let file;
4580
5016
  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());
5017
+ file = path8.resolve(root, execFileSync3("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
4582
5018
  } catch {
4583
5019
  return;
4584
5020
  }
@@ -4587,25 +5023,26 @@ function gitExclude(root, rels) {
4587
5023
  const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
4588
5024
  const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
4589
5025
  const block2 = rels.length ? `${EXCLUDE_BEGIN}
4590
- ${[...new Set(rels)].sort().map((r) => `/${r.split(path6.sep).join("/")}`).join("\n")}
5026
+ ${[...new Set(rels)].sort().map((r) => `/${r.split(path8.sep).join("/")}`).join("\n")}
4591
5027
  ${EXCLUDE_END}
4592
5028
  ` : "";
4593
5029
  const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
4594
5030
  if (next === cur) return;
4595
5031
  try {
4596
- ensureDir(path6.dirname(file));
4597
- fs7.writeFileSync(file, next);
5032
+ ensureDir(path8.dirname(file));
5033
+ fs9.writeFileSync(file, next);
4598
5034
  } catch {
4599
5035
  }
4600
5036
  }
4601
- var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
5037
+ var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
4602
5038
  var init_deploy = __esm({
4603
5039
  "src/v2/deploy.ts"() {
4604
5040
  "use strict";
4605
5041
  init_v22();
5042
+ init_source();
4606
5043
  init_util();
4607
5044
  init_store2();
4608
- settingsFile = (home) => path6.join(home, ".bunshin", "deploy", "settings.json");
5045
+ settingsFile = (home) => path8.join(home, ".bunshin", "deploy", "settings.json");
4609
5046
  orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
4610
5047
  sha = (text2) => createHash3("sha256").update(text2).digest("hex");
4611
5048
  BLOCK_BEGIN = "<!-- bunshin:begin -->";
@@ -4614,7 +5051,7 @@ var init_deploy = __esm({
4614
5051
  MAX_FILE = 2 * 1024 * 1024;
4615
5052
  isExecutable = (file) => {
4616
5053
  try {
4617
- return (fs7.statSync(file).mode & 73) !== 0;
5054
+ return (fs9.statSync(file).mode & 73) !== 0;
4618
5055
  } catch {
4619
5056
  return false;
4620
5057
  }
@@ -4622,12 +5059,11 @@ var init_deploy = __esm({
4622
5059
  safeJoin = containedPath;
4623
5060
  SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
4624
5061
  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 };
4626
5062
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
4627
5063
  BORN_SLACK_MS = 2e3;
4628
5064
  LINK_DIRS = {
4629
- "claude-code": { rel: path6.join(".claude", "skills"), key: "claudeSkills" },
4630
- codex: { rel: path6.join(".codex", "skills"), key: "codexSkills" }
5065
+ "claude-code": { rel: path8.join(".claude", "skills"), key: "claudeSkills" },
5066
+ codex: { rel: path8.join(".codex", "skills"), key: "codexSkills" }
4631
5067
  };
4632
5068
  EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
4633
5069
  EXCLUDE_END = "# <<< bunshin";
@@ -4739,11 +5175,11 @@ var init_live = __esm({
4739
5175
  });
4740
5176
 
4741
5177
  // src/transcript.ts
4742
- import fs8 from "node:fs";
5178
+ import fs10 from "node:fs";
4743
5179
  function parseTranscript(file) {
4744
5180
  let raw = "";
4745
5181
  try {
4746
- raw = fs8.readFileSync(file, "utf8");
5182
+ raw = fs10.readFileSync(file, "utf8");
4747
5183
  } catch {
4748
5184
  return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
4749
5185
  }
@@ -4834,292 +5270,128 @@ function isHumanText(text2) {
4834
5270
  function blockText(content) {
4835
5271
  if (typeof content === "string") return content;
4836
5272
  if (Array.isArray(content)) {
4837
- return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
4838
- }
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
- }
5273
+ return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
5079
5274
  }
5080
- return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
5275
+ return content == null ? "" : JSON.stringify(content);
5081
5276
  }
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 {
5277
+ function codexToolResults(output, depth = 0) {
5278
+ const raw = blockText(output);
5279
+ let envelope = output;
5280
+ if (typeof output === "string") {
5094
5281
  try {
5095
- real = path7.join(fs9.realpathSync(path7.dirname(abs)), path7.basename(abs));
5282
+ envelope = JSON.parse(output);
5096
5283
  } catch {
5097
5284
  }
5098
5285
  }
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("/");
5286
+ if (depth > 6) return [{ text: raw, pending: true }];
5287
+ if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
5288
+ if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
5289
+ const result = envelope;
5290
+ if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
5291
+ const pending = !Number.isInteger(result.exit_code);
5292
+ return [{
5293
+ text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
5294
+ ${result.output}`,
5295
+ isError: !pending && result.exit_code !== 0,
5296
+ pending
5297
+ }];
5298
+ }
5299
+ if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
5300
+ if (result.status === "rejected") return [{ text: raw, isError: true }];
5105
5301
  }
5106
- return null;
5302
+ return [{
5303
+ text: raw,
5304
+ isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
5305
+ pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
5306
+ }];
5107
5307
  }
5108
- var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
5109
- var init_repo = __esm({
5110
- "src/v2/repo.ts"() {
5308
+ function summarizeInput(input) {
5309
+ if (input == null) return "";
5310
+ if (typeof input !== "object") return String(input);
5311
+ const obj = input;
5312
+ for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
5313
+ if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
5314
+ }
5315
+ return JSON.stringify(obj);
5316
+ }
5317
+ function bounded(text2, maxChars) {
5318
+ if (text2.length <= maxChars) return text2;
5319
+ const marker = "\n[... middle omitted ...]\n";
5320
+ if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
5321
+ const head = Math.floor((maxChars - marker.length) / 2);
5322
+ return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
5323
+ }
5324
+ function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
5325
+ const parts = [];
5326
+ for (const t of facts.turns.slice(from)) {
5327
+ const text2 = redact2(t.text);
5328
+ if (t.role === "user") parts.push(`[USER]
5329
+ ${bounded(text2, maxChars)}`);
5330
+ else if (t.role === "assistant") parts.push(`[ASSISTANT]
5331
+ ${bounded(text2, maxChars)}`);
5332
+ else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
5333
+ ${bounded(text2, toolResultChars)}`);
5334
+ else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
5335
+ }
5336
+ return bounded(parts.join("\n\n"), maxChars);
5337
+ }
5338
+ function fullTrace(facts, { from = 0, to } = {}) {
5339
+ return facts.turns.slice(from, to).map((t) => {
5340
+ if (t.role === "user") return `[USER]
5341
+ ${redact2(t.text)}`;
5342
+ if (t.role === "assistant") return `[ASSISTANT]
5343
+ ${redact2(t.text)}`;
5344
+ if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
5345
+ ${redact2(t.text)}`;
5346
+ const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
5347
+ return `[TOOL ${t.tool}]
5348
+ ${redact2(input)}`;
5349
+ });
5350
+ }
5351
+ function traceChunks(turns, maxBytes = 1e6) {
5352
+ const out = [];
5353
+ let cur = "";
5354
+ let size = 0;
5355
+ turns.forEach((turn, i) => {
5356
+ let piece = (i ? "\n\n" : "") + turn;
5357
+ let bytes = jsonBytes(piece);
5358
+ if (size + bytes <= maxBytes) {
5359
+ cur += piece;
5360
+ size += bytes;
5361
+ return;
5362
+ }
5363
+ if (cur) out.push(cur);
5364
+ while (bytes > maxBytes) {
5365
+ let n = Math.min(piece.length, maxBytes);
5366
+ 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);
5367
+ const code2 = piece.charCodeAt(n - 1);
5368
+ if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
5369
+ const head = piece.slice(0, n);
5370
+ out.push(head);
5371
+ piece = piece.slice(n);
5372
+ bytes -= jsonBytes(head);
5373
+ }
5374
+ cur = piece;
5375
+ size = bytes;
5376
+ });
5377
+ if (cur) out.push(cur);
5378
+ return out;
5379
+ }
5380
+ function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
5381
+ const facts = parseTranscript(file);
5382
+ const visible = facts.turns.filter((t) => t.role !== "tool");
5383
+ const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
5384
+ const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
5385
+ return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
5386
+ }
5387
+ var BANNER, jsonBytes;
5388
+ var init_transcript = __esm({
5389
+ "src/transcript.ts"() {
5111
5390
  "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"));
5391
+ init_redact2();
5392
+ init_util();
5393
+ BANNER = "## Recalled lessons (Bunshin)";
5394
+ jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
5123
5395
  }
5124
5396
  });
5125
5397
 
@@ -5188,8 +5460,14 @@ function applyOverlays(sections, overlays, ctx) {
5188
5460
  }
5189
5461
  function localView(v2, config, cwd, opts = {}) {
5190
5462
  const ctx = contextFor(v2, config);
5191
- const reader = readerFor(ctx);
5192
5463
  const bundle = v2.readBundle();
5464
+ let source;
5465
+ try {
5466
+ source = opts.source !== void 0 ? opts.source || void 0 : sourceFor(cwd, v2.store.root, ctx);
5467
+ } catch {
5468
+ source = void 0;
5469
+ }
5470
+ const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle?.scopes ? { tree: bundle.scopes } : {} });
5193
5471
  const byId = /* @__PURE__ */ new Map();
5194
5472
  for (const s of bundle?.sections ?? []) byId.set(s.id, s);
5195
5473
  let root = cwd;
@@ -5207,7 +5485,7 @@ function localView(v2, config, cwd, opts = {}) {
5207
5485
  }
5208
5486
  }
5209
5487
  applyOverlays(byId, v2.readOverlays(), ctx);
5210
- return { ctx, reader, layers: layersFor(ctx), bundle, sections: [...byId.values()], byId, root, repo };
5488
+ return { ctx, reader, ...source ? { source } : {}, layers: reader.layers, bundle, sections: [...byId.values()], byId, root, repo };
5211
5489
  }
5212
5490
  var ACTIVE_OVERLAY;
5213
5491
  var init_view = __esm({
@@ -5215,12 +5493,13 @@ var init_view = __esm({
5215
5493
  "use strict";
5216
5494
  init_v22();
5217
5495
  init_repo();
5496
+ init_source();
5218
5497
  ACTIVE_OVERLAY = /* @__PURE__ */ new Set(["proposed", "trial", "submitted", "kept-local", "needs-review"]);
5219
5498
  }
5220
5499
  });
5221
5500
 
5222
5501
  // src/v2/apply.ts
5223
- import fs10 from "node:fs";
5502
+ import fs11 from "node:fs";
5224
5503
  function appendedText(op) {
5225
5504
  return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
5226
5505
  }
@@ -5275,12 +5554,12 @@ function applyLocally(v2, view, s) {
5275
5554
  if (section.storage.type === "repo") {
5276
5555
  const file = containedPath(view.root, section.storage.path);
5277
5556
  const ours = !view.repo || section.storage.repo === view.repo;
5278
- if (ours && file && fs10.existsSync(file)) {
5279
- const before = fs10.readFileSync(file, "utf8");
5557
+ if (ours && file && fs11.existsSync(file)) {
5558
+ const before = fs11.readFileSync(file, "utf8");
5280
5559
  const res2 = applyOpToMarkdown(before, section.storage.anchor, op);
5281
5560
  if (res2.ok) {
5282
5561
  const changed = res2.text !== before;
5283
- if (changed) fs10.writeFileSync(file, res2.text);
5562
+ if (changed) fs11.writeFileSync(file, res2.text);
5284
5563
  const overlay2 = { ...base, kind: "working-tree", sectionId: section.id, file: section.storage.path, anchor: section.storage.anchor, changed };
5285
5564
  upsertOverlay(v2, overlay2);
5286
5565
  return { applied: true, target: describeTarget(s, view), overlay: overlay2 };
@@ -5301,7 +5580,7 @@ function revertWorkingTree(root, o) {
5301
5580
  if (!file) return { reverted: false, reason: "path outside the repository" };
5302
5581
  let text2;
5303
5582
  try {
5304
- text2 = fs10.readFileSync(file, "utf8");
5583
+ text2 = fs11.readFileSync(file, "utf8");
5305
5584
  } catch {
5306
5585
  return { reverted: false, reason: "file missing" };
5307
5586
  }
@@ -5321,7 +5600,7 @@ ${appendedText(o.op)}`;
5321
5600
  if (once(text2, o.op.text) >= 0 && !text2.includes(o.op.find)) next = text2.replace(o.op.text, o.op.find);
5322
5601
  }
5323
5602
  if (next === null) return { reverted: false, reason: "file changed since the trial edit" };
5324
- fs10.writeFileSync(file, next);
5603
+ fs11.writeFileSync(file, next);
5325
5604
  return { reverted: true };
5326
5605
  }
5327
5606
  function revertParam(v2, o) {
@@ -5394,7 +5673,7 @@ var init_apply = __esm({
5394
5673
  });
5395
5674
 
5396
5675
  // src/v2/signals.ts
5397
- import fs11 from "node:fs";
5676
+ import fs12 from "node:fs";
5398
5677
  function isCorrection(text2) {
5399
5678
  const t = text2.trim().slice(0, 600);
5400
5679
  return !!t && CORRECTION_RE.test(t);
@@ -5416,7 +5695,7 @@ function scanTranscript(file) {
5416
5695
  if (!file) return empty;
5417
5696
  let raw;
5418
5697
  try {
5419
- raw = fs11.readFileSync(file, "utf8");
5698
+ raw = fs12.readFileSync(file, "utf8");
5420
5699
  } catch {
5421
5700
  return empty;
5422
5701
  }
@@ -5500,8 +5779,8 @@ var init_signals = __esm({
5500
5779
  });
5501
5780
 
5502
5781
  // src/v2/ui.ts
5503
- import fs12 from "node:fs";
5504
- import path8 from "node:path";
5782
+ import fs13 from "node:fs";
5783
+ import path9 from "node:path";
5505
5784
  function feedPath(v2, sessionId) {
5506
5785
  return v2.p("ui", `${safe(sessionId)}.json`);
5507
5786
  }
@@ -5517,7 +5796,7 @@ function emptyFeed(v2, sessionId, now) {
5517
5796
  }
5518
5797
  function cliArgv() {
5519
5798
  const entry = process.argv[1];
5520
- return entry ? [process.execPath, path8.resolve(entry)] : ["bunshin"];
5799
+ return entry ? [process.execPath, path9.resolve(entry)] : ["bunshin"];
5521
5800
  }
5522
5801
  function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toISOString()) {
5523
5802
  if (!sessionId || sessionId === "no-session") return null;
@@ -5532,7 +5811,7 @@ function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toI
5532
5811
  f.cli = cliArgv();
5533
5812
  f.served = f.served.slice(0, MAX_SERVED);
5534
5813
  f.events = f.events.slice(-MAX_EVENTS);
5535
- fs12.mkdirSync(path8.dirname(feedPath(v2, sessionId)), { recursive: true });
5814
+ fs13.mkdirSync(path9.dirname(feedPath(v2, sessionId)), { recursive: true });
5536
5815
  writeJsonAtomic(feedPath(v2, sessionId), f);
5537
5816
  return f;
5538
5817
  } catch {
@@ -5633,16 +5912,16 @@ function latestFeed(v2) {
5633
5912
  const dir = v2.p("ui");
5634
5913
  let best = null;
5635
5914
  try {
5636
- for (const f2 of fs12.readdirSync(dir)) {
5915
+ for (const f2 of fs13.readdirSync(dir)) {
5637
5916
  if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
5638
- const m = fs12.statSync(path8.join(dir, f2)).mtimeMs;
5917
+ const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
5639
5918
  if (!best || m > best.m) best = { f: f2, m };
5640
5919
  }
5641
5920
  } catch {
5642
5921
  return null;
5643
5922
  }
5644
5923
  if (!best) return null;
5645
- const f = readJson(path8.join(dir, best.f), null);
5924
+ const f = readJson(path9.join(dir, best.f), null);
5646
5925
  return f && f.v === UI_FEED_VERSION ? f : null;
5647
5926
  }
5648
5927
  var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
@@ -5658,110 +5937,6 @@ var init_ui = __esm({
5658
5937
  }
5659
5938
  });
5660
5939
 
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
5940
  // src/v2/sync.ts
5766
5941
  var sync_exports = {};
5767
5942
  __export(sync_exports, {
@@ -5974,7 +6149,8 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
5974
6149
  }
5975
6150
  });
5976
6151
  const commit = repoCommit(root);
5977
- const res = await v2api(config, "POST", "/sections/index", { repo: index.repo, ...commit ? { commit } : {}, ownerTeam: index.ownerTeam, docs }, { deadline, timeoutMs: 3e4 });
6152
+ const source = sourceFor(root, store.root);
6153
+ const res = await v2api(config, "POST", "/sections/index", { repo: index.repo, ...commit ? { commit } : {}, ...source ? { source } : {}, ownerTeam: index.ownerTeam, docs }, { deadline, timeoutMs: 3e4 });
5978
6154
  return typeof res.indexed === "number" ? res.indexed : docs.length;
5979
6155
  }
5980
6156
  var isLocalId, carriesTrace;
@@ -6113,7 +6289,7 @@ async function runReflectionV2(store, opts) {
6113
6289
  if (!release) return { ...result, skipped: "a reflection of this session is already running" };
6114
6290
  try {
6115
6291
  const session = v2.session(opts.sessionId, opts.cwd ?? process.cwd());
6116
- const view = localView(v2, config, session.cwd || opts.cwd || process.cwd());
6292
+ const view = localView(v2, config, session.cwd || opts.cwd || process.cwd(), session.source !== void 0 ? { source: session.source } : {});
6117
6293
  const origin = opts.origin ?? (opts.focus !== void 0 ? "learn" : "session-end");
6118
6294
  const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
6119
6295
  const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
@@ -6130,7 +6306,7 @@ async function runReflectionV2(store, opts) {
6130
6306
  const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
6131
6307
  const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
6132
6308
  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 } : {} };
6309
+ 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
6310
  let raw = null;
6135
6311
  if (harness === "claude-code") {
6136
6312
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
@@ -6170,7 +6346,7 @@ async function runReflectionV2(store, opts) {
6170
6346
  result.nothingNew = result.method !== "none" && parsed.length === 0;
6171
6347
  const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
6172
6348
  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);
6349
+ const source = view.source;
6174
6350
  for (const p of parsed) {
6175
6351
  const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
6176
6352
  if (source) s.source = source;
@@ -6257,7 +6433,6 @@ var init_reflect3 = __esm({
6257
6433
  init_signals();
6258
6434
  init_ui();
6259
6435
  init_view();
6260
- init_source();
6261
6436
  FORK_TIMEOUT_MS = 12e4;
6262
6437
  FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
6263
6438
  }
@@ -6677,7 +6852,7 @@ function liveUpdate(v2, ctx, session) {
6677
6852
  return "";
6678
6853
  }
6679
6854
  const config = ctx.store.config();
6680
- const deployed = deployEnabled(config, localView(v2, config, ctx.cwd).ctx);
6855
+ const deployed = deployEnabled(config, contextFor(v2, config));
6681
6856
  const paths = deployed ? deployPaths(process.env, config.orgId) : null;
6682
6857
  const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
6683
6858
  const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
@@ -6744,7 +6919,7 @@ async function onPrompt(ctx) {
6744
6919
  };
6745
6920
  if (!prompt.trim()) return withLive({});
6746
6921
  const config = ctx.store.config();
6747
- const view = localView(v2, config, ctx.cwd);
6922
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6748
6923
  const injected = inContext(session);
6749
6924
  const named = invokedSkill(prompt, view.sections);
6750
6925
  if (named) {
@@ -6832,7 +7007,7 @@ function onPreToolUse(ctx) {
6832
7007
  const raw = toolPaths(ctx.input.tool_input);
6833
7008
  if (!raw.length) return withLive({});
6834
7009
  const config = ctx.store.config();
6835
- const view = localView(v2, config, ctx.cwd);
7010
+ const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
6836
7011
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
6837
7012
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
6838
7013
  if (!paths.length && !text2) return withLive({});
@@ -6957,6 +7132,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
6957
7132
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
6958
7133
  cliVersion: version(),
6959
7134
  ...source ? { source } : {},
7135
+ ...session.borrowed?.length ? { borrowed: session.borrowed } : {},
6960
7136
  uploaded: false,
6961
7137
  ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
6962
7138
  ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
@@ -7265,6 +7441,7 @@ var init_project_hooks = __esm({
7265
7441
  // src/init.ts
7266
7442
  var init_exports = {};
7267
7443
  __export(init_exports, {
7444
+ BORROW_COMMAND: () => BORROW_COMMAND,
7268
7445
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
7269
7446
  HOOK_EVENTS: () => HOOK_EVENTS,
7270
7447
  LEARN_COMMAND: () => LEARN_COMMAND,
@@ -7386,18 +7563,18 @@ function mcpLauncher(bin) {
7386
7563
  }
7387
7564
  return { command: process.execPath, args: [script, "mcp"] };
7388
7565
  }
7389
- function installLearnCommand({ file, remove = false }) {
7566
+ function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
7390
7567
  try {
7391
7568
  const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
7392
- const ours = current === null || current === LEARN_COMMAND;
7569
+ const ours = current === null || current === content;
7393
7570
  if (remove) {
7394
7571
  if (current !== null && ours) fs21.unlinkSync(file);
7395
7572
  return { file, installed: 0, changed: current !== null && ours };
7396
7573
  }
7397
- if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
7574
+ if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
7398
7575
  if (current === null) {
7399
7576
  fs21.mkdirSync(path16.dirname(file), { recursive: true });
7400
- fs21.writeFileSync(file, LEARN_COMMAND);
7577
+ fs21.writeFileSync(file, content);
7401
7578
  }
7402
7579
  return { file, installed: 1, changed: current === null };
7403
7580
  } catch (err) {
@@ -7519,11 +7696,20 @@ function runInit(opts = {}) {
7519
7696
  learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
7520
7697
  if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
7521
7698
  }
7699
+ let borrowCommand;
7700
+ if (opts.removeHooks || !opts.noHooks) {
7701
+ const user = hookScope === "user";
7702
+ 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;
7703
+ if (file) {
7704
+ borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
7705
+ if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
7706
+ }
7707
+ }
7522
7708
  if (!resolveBackend(config)) {
7523
7709
  warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
7524
7710
  }
7525
7711
  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 };
7712
+ return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, ...borrowCommand ? { borrowCommand } : {}, warnings };
7527
7713
  }
7528
7714
  function otherHarnessInstructions() {
7529
7715
  return [
@@ -7539,7 +7725,7 @@ function otherHarnessInstructions() {
7539
7725
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
7540
7726
  ].join("\n");
7541
7727
  }
7542
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
7728
+ var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
7543
7729
  var init_init = __esm({
7544
7730
  "src/init.ts"() {
7545
7731
  "use strict";
@@ -7565,6 +7751,7 @@ var init_init = __esm({
7565
7751
  { event: "SessionEnd", arg: "session-end", timeout: 3 }
7566
7752
  ];
7567
7753
  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';
7754
+ 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
7755
  }
7569
7756
  });
7570
7757
 
@@ -7586,19 +7773,19 @@ function mark(v2, sessionId, cwd, field, ids) {
7586
7773
  } catch {
7587
7774
  }
7588
7775
  }
7589
- async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3 }) {
7776
+ async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3, across = false }) {
7590
7777
  const v2 = new V2Store(store);
7591
7778
  const config = store.config();
7592
7779
  const sid = sessionId ?? sessionIdFromEnv(v2);
7593
7780
  const session = sid ? v2.readSession(sid) : null;
7594
7781
  const exclude = session ? [...session.loaded] : [];
7595
7782
  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 });
7783
+ const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
7784
+ const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude, across });
7598
7785
  let out;
7599
7786
  if (canCall(config)) {
7600
7787
  try {
7601
- const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude }, { timeoutMs });
7788
+ const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude, ...view.source ? { source: view.source } : {}, ...across ? { across: true } : {} }, { timeoutMs });
7602
7789
  const remote = Array.isArray(res.candidates) ? res.candidates : [];
7603
7790
  const extra = local().filter((c) => c.id.startsWith("local:") && !remote.some((r) => r.id === c.id));
7604
7791
  out = { candidates: [...remote, ...extra].slice(0, limit), source: "mothership" };
@@ -7613,7 +7800,8 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7613
7800
  const v2 = new V2Store(store);
7614
7801
  const config = store.config();
7615
7802
  const sid = sessionId ?? sessionIdFromEnv(v2);
7616
- const view = localView(v2, config, cwd);
7803
+ const session = sid ? v2.readSession(sid) : null;
7804
+ const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
7617
7805
  let section = view.byId.get(id);
7618
7806
  let source = "local";
7619
7807
  if (!section && canCall(config)) {
@@ -7626,8 +7814,17 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
7626
7814
  }
7627
7815
  if (!section) return null;
7628
7816
  mark(v2, sid, cwd, "loaded", [section.id]);
7817
+ const borrowed = !inBoundary(section, view.reader);
7818
+ if (borrowed && sid) {
7819
+ try {
7820
+ v2.updateSession(sid, cwd, (s) => {
7821
+ s.borrowed = [...(s.borrowed ?? []).filter((b) => b.sectionId !== section.id), { sectionId: section.id, layer: section.layer, at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-50);
7822
+ });
7823
+ } catch {
7824
+ }
7825
+ }
7629
7826
  const part = anchor?.replace(/^[§#]/, "");
7630
- return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source };
7827
+ return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source, ...borrowed ? { borrowed: true } : {} };
7631
7828
  }
7632
7829
  var init_library = __esm({
7633
7830
  "src/v2/library.ts"() {
@@ -7761,18 +7958,19 @@ async function cmdReviewDecide(store, args, out, err) {
7761
7958
  async function cmdRecall(store, args, cwd, out, err) {
7762
7959
  const abstract = args._.slice(1).join(" ").trim() || str3(args.flags.abstract) || "";
7763
7960
  if (!abstract) {
7764
- err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--json]\n');
7961
+ err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--across] [--json]\n');
7765
7962
  return 1;
7766
7963
  }
7767
7964
  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) });
7965
+ const across = bool2(args.flags.across);
7966
+ 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
7967
  if (bool2(args.flags.json)) {
7770
7968
  out(JSON.stringify(res, null, 2) + "\n");
7771
7969
  return 0;
7772
7970
  }
7773
- out(renderCandidates(res.candidates) + "\n");
7971
+ out(renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes) + "\n");
7774
7972
  if (res.candidates.length) out(`
7775
- (${res.source}) Load one with: bunshin load <id>
7973
+ (${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
7974
  `);
7777
7975
  return 0;
7778
7976
  }
@@ -7789,7 +7987,7 @@ async function cmdLoad(store, args, cwd, out, err) {
7789
7987
  `);
7790
7988
  return 1;
7791
7989
  }
7792
- out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source }, null, 2) + "\n" : res.text + "\n");
7990
+ out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source, ...res.borrowed ? { borrowed: true } : {} }, null, 2) + "\n" : res.text + "\n");
7793
7991
  return 0;
7794
7992
  }
7795
7993
  async function cmdLearn(store, args, cwd, out, err) {
@@ -7853,7 +8051,7 @@ function cmdSuggest(store, args, cwd, out, err) {
7853
8051
  orgId: config.orgId,
7854
8052
  contextId: view.ctx.id,
7855
8053
  authorUserId: config.userId,
7856
- target: known ? { sectionId: known } : { layer: "fleet", storage: { type: "harness", path: rel, symbol: symbol ?? "" } },
8054
+ target: known ? { sectionId: known } : { storage: { type: "harness", path: rel, symbol: symbol ?? "" } },
7857
8055
  op: { op: "append", text: rationale },
7858
8056
  rationale,
7859
8057
  evidenceEpisodeIds: sessionId ? [episodeIdFor(view.ctx.id, sessionId)] : [],
@@ -8347,8 +8545,8 @@ async function handleMcp(msg, opts) {
8347
8545
  if (!taskAbstract.trim()) return reply(text("task_abstract is required: describe the task abstractly.", true));
8348
8546
  const identifiers = Array.isArray(args.identifiers) ? args.identifiers.filter((x) => typeof x === "string") : [];
8349
8547
  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);
8548
+ const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd, across: args.across === true });
8549
+ const body = renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes);
8352
8550
  return reply(text(res.candidates.length ? `${body}
8353
8551
 
8354
8552
  Load the ones that would change what you do with bunshin_load (0-3).` : body));
@@ -8405,6 +8603,7 @@ var init_mcp = __esm({
8405
8603
  init_v22();
8406
8604
  init_source();
8407
8605
  init_store();
8606
+ init_store2();
8408
8607
  init_library();
8409
8608
  DEFAULT_PROTOCOL = "2025-06-18";
8410
8609
  MCP_TOOLS = [
@@ -8418,7 +8617,8 @@ var init_mcp = __esm({
8418
8617
  task_abstract: { type: "string", description: "The task in abstract, structural terms (what kind of problem this is), not a restatement of its topic." },
8419
8618
  hypothetical_lesson: { type: "string", description: "The lesson you would want to find, written as if it existed." },
8420
8619
  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)." }
8620
+ limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." },
8621
+ 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
8622
  },
8423
8623
  required: ["task_abstract", "hypothetical_lesson", "identifiers"],
8424
8624
  additionalProperties: false
@@ -9627,7 +9827,7 @@ function effortOf(job) {
9627
9827
  const tokens = Math.round(chars / 4);
9628
9828
  const minutes = Math.max(2, Math.round(2 + tokens / 15e3));
9629
9829
  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`;
9830
+ 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
9831
  return `${chars.toLocaleString("en")} chars (~${tokens.toLocaleString("en")} tokens); about ${minutes} min. ${what}.`;
9632
9832
  }
9633
9833
  function jobIndex(text2) {
@@ -10115,7 +10315,7 @@ var init_runner = __esm({
10115
10315
  init_trust();
10116
10316
  init_version();
10117
10317
  DEFAULT_API = "https://api.bunshin.cc";
10118
- KINDS2 = ["reflect", "merge", "create"];
10318
+ KINDS2 = ["reflect", "merge", "create", "generalize"];
10119
10319
  str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
10120
10320
  LAUNCHD_LABEL = "cc.bunshin.runner";
10121
10321
  CRON_MARK = "# bunshin-runner";
@@ -11146,7 +11346,7 @@ import fs32 from "node:fs";
11146
11346
  import path27 from "node:path";
11147
11347
  function parseArgs(argv) {
11148
11348
  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"]);
11349
+ 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
11350
  for (let i = 0; i < argv.length; i++) {
11151
11351
  const a = argv[i];
11152
11352
  if (a === "--") {
@@ -11285,6 +11485,8 @@ Key saved privately outside the project.
11285
11485
  if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
11286
11486
  `);
11287
11487
  if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
11488
+ `);
11489
+ if (result.borrowCommand?.installed) out(`/borrow installed in ${result.borrowCommand.file} (bring in a lesson learned in another project or group)
11288
11490
  `);
11289
11491
  else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
11290
11492
  `);
@@ -11860,10 +12062,12 @@ Usage: bunshin <command> [options]
11860
12062
  status [--json] store, context, library, outbox, hooks and sync health
11861
12063
  sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
11862
12064
  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)
12065
+ recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--across] [--json]
12066
+ pull candidates from this project's scopes (offline: local view);
12067
+ --across borrows from anywhere in the workspace (/borrow)
11865
12068
  load <id> [--anchor <heading>] [--json]
11866
- print one document (or one part of it) and mark it loaded
12069
+ print one document (or one part of it) and mark it loaded; a
12070
+ lesson from outside this project's scopes is recorded as borrowed
11867
12071
  learn [focus...] capture what this session learned now (fork reflection),
11868
12072
  apply it locally as a trial and submit it
11869
12073
  review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
@@ -11883,9 +12087,8 @@ Usage: bunshin <command> [options]
11883
12087
  the deployed skills; push proposes local edits / a new skill now
11884
12088
  library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
11885
12089
  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
12090
+ library workspace (retired: each workspace keeps its own skills and links them into
12091
+ the projects its sessions run in; see library status)
11889
12092
  ui [--session <id>] [--json] [--watch] what Bunshin did this session, as the person sees it
11890
12093
  ui status | install | uninstall the Claude Code mod (band above the prompt, /bunshin, toasts)
11891
12094
  ui enable|disable --harness <h> [--mode auto|mod|widget|lines|off] how a harness shows it
@@ -11896,9 +12099,9 @@ Usage: bunshin <command> [options]
11896
12099
  repo import|attach|clone|verify|list|url|login
11897
12100
  host a git repository in Bunshin (full history; git push/pull
11898
12101
  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]
12102
+ runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create,generalize]
11900
12103
  answer the company's queued model work (cloud reflection,
11901
- reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
12104
+ reconciliation, generalization) with this machine's model. BUNSHIN_RUNNER_KEY
11902
12105
  runner next | submit <job> <file|-> | fail <job> --error <why>
11903
12106
  agent mode, for a Claude Code routine or Codex cloud task
11904
12107
  runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall