@bunshin-cc/bunshin 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +811 -629
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1074,6 +1074,95 @@ var init_permissions = __esm({
|
|
|
1074
1074
|
}
|
|
1075
1075
|
});
|
|
1076
1076
|
|
|
1077
|
+
// ../core/src/v2/sources.ts
|
|
1078
|
+
function sourceId(kind, name) {
|
|
1079
|
+
return `${kind}:${name.trim()}`;
|
|
1080
|
+
}
|
|
1081
|
+
function parseSource(id) {
|
|
1082
|
+
if (!id) return null;
|
|
1083
|
+
const i = id.indexOf(":");
|
|
1084
|
+
if (i < 0) return null;
|
|
1085
|
+
const kind = id.slice(0, i);
|
|
1086
|
+
const name = id.slice(i + 1);
|
|
1087
|
+
if (!SOURCE_KINDS.includes(kind) || !NAME.test(name) || name.trim() !== name) return null;
|
|
1088
|
+
return { kind, name };
|
|
1089
|
+
}
|
|
1090
|
+
function sourceName(id) {
|
|
1091
|
+
return parseSource(id)?.name ?? id;
|
|
1092
|
+
}
|
|
1093
|
+
var SOURCE_KINDS, NAME;
|
|
1094
|
+
var init_sources = __esm({
|
|
1095
|
+
"../core/src/v2/sources.ts"() {
|
|
1096
|
+
"use strict";
|
|
1097
|
+
SOURCE_KINDS = ["repo", "folder", "robot"];
|
|
1098
|
+
NAME = /^[^\0-\x1f\x7f]{1,280}$/;
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// ../core/src/v2/scopes.ts
|
|
1103
|
+
function emptyTree() {
|
|
1104
|
+
return { version: 0, groups: [], sources: {} };
|
|
1105
|
+
}
|
|
1106
|
+
function isScopeLayer(layer) {
|
|
1107
|
+
const k = layerKind(layer);
|
|
1108
|
+
return k === "group" || k === "source";
|
|
1109
|
+
}
|
|
1110
|
+
function groupChain(tree, groupId) {
|
|
1111
|
+
const byId = new Map(tree.groups.map((g) => [g.id, g]));
|
|
1112
|
+
const out = [];
|
|
1113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1114
|
+
let cur = groupId ? byId.get(groupId) : void 0;
|
|
1115
|
+
while (cur && !seen.has(cur.id)) {
|
|
1116
|
+
seen.add(cur.id);
|
|
1117
|
+
out.unshift(cur.id);
|
|
1118
|
+
cur = cur.parent ? byId.get(cur.parent) : void 0;
|
|
1119
|
+
}
|
|
1120
|
+
return out;
|
|
1121
|
+
}
|
|
1122
|
+
function scopePath(tree, source) {
|
|
1123
|
+
if (!source || !parseSource(source)) return [];
|
|
1124
|
+
const placed = tree?.sources[source]?.group ?? null;
|
|
1125
|
+
return [...(tree ? groupChain(tree, placed) : []).map(groupLayer), sourceLayer(source)];
|
|
1126
|
+
}
|
|
1127
|
+
function inBoundary(section, reader) {
|
|
1128
|
+
return !isScopeLayer(section.layer) || reader.layers.includes(section.layer);
|
|
1129
|
+
}
|
|
1130
|
+
function sourcesIn(tree, layer) {
|
|
1131
|
+
if (layer === "org") return Object.keys(tree.sources);
|
|
1132
|
+
if (layerKind(layer) === "source") return [layerValue(layer)];
|
|
1133
|
+
if (layerKind(layer) !== "group") return [];
|
|
1134
|
+
const gid = layerValue(layer);
|
|
1135
|
+
return Object.keys(tree.sources).filter((sid) => groupChain(tree, tree.sources[sid].group).includes(gid));
|
|
1136
|
+
}
|
|
1137
|
+
function scopeLabel(layer, tree) {
|
|
1138
|
+
const k = layerKind(layer);
|
|
1139
|
+
if (layer === "org") return "everywhere";
|
|
1140
|
+
if (k === "source") {
|
|
1141
|
+
const sid = layerValue(layer);
|
|
1142
|
+
return `${parseSource(sid)?.kind === "robot" ? "robot" : "project"} ${sourceName(sid)}`;
|
|
1143
|
+
}
|
|
1144
|
+
if (k === "group") return `group ${tree?.groups.find((g) => g.id === layerValue(layer))?.name ?? layerValue(layer)}`;
|
|
1145
|
+
if (k === "user") return "personal";
|
|
1146
|
+
return layer;
|
|
1147
|
+
}
|
|
1148
|
+
function scopeChoices(tree, source) {
|
|
1149
|
+
const t = tree ?? emptyTree();
|
|
1150
|
+
return scopePath(t, source).map((layer) => {
|
|
1151
|
+
const n = sourcesIn(t, layer).length;
|
|
1152
|
+
return { layer, label: layerKind(layer) === "source" ? `this ${scopeLabel(layer, t)} only` : `${scopeLabel(layer, t)} (${n} project${n === 1 ? "" : "s"})` };
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
var groupLayer, sourceLayer;
|
|
1156
|
+
var init_scopes = __esm({
|
|
1157
|
+
"../core/src/v2/scopes.ts"() {
|
|
1158
|
+
"use strict";
|
|
1159
|
+
init_layers();
|
|
1160
|
+
init_sources();
|
|
1161
|
+
groupLayer = (id) => `group:${id}`;
|
|
1162
|
+
sourceLayer = (id) => `source:${id}`;
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1077
1166
|
// ../core/src/v2/layers.ts
|
|
1078
1167
|
function layerKind(layer) {
|
|
1079
1168
|
const kind = layer.split(":")[0];
|
|
@@ -1091,11 +1180,22 @@ function layersFor(ctx) {
|
|
|
1091
1180
|
const dept = department(team);
|
|
1092
1181
|
return ["org", ...dept ? [`dept:${dept}`] : [], ...team ? [`team:${team}`] : [], `user:${ctx.userId}`];
|
|
1093
1182
|
}
|
|
1094
|
-
function
|
|
1183
|
+
function isLocalLayer(layer) {
|
|
1184
|
+
const kind = layerKind(layer);
|
|
1185
|
+
return kind === "user" || kind === "robot";
|
|
1186
|
+
}
|
|
1187
|
+
function withScope(layers, scope) {
|
|
1188
|
+
const path28 = scopePath(scope?.tree, scope?.source);
|
|
1189
|
+
if (!path28.length) return layers;
|
|
1190
|
+
const i = layers.findIndex((l) => isLocalLayer(l));
|
|
1191
|
+
return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
|
|
1192
|
+
}
|
|
1193
|
+
function readerFor(ctx, ownerClearance, scope) {
|
|
1194
|
+
const source = scope?.source ? { source: scope.source } : {};
|
|
1095
1195
|
if (ctx.kind === "robot") {
|
|
1096
1196
|
const rank = (c) => c === "private" ? 0 : CLEARANCE_RANK[c];
|
|
1097
1197
|
const clearance = ownerClearance && rank(ownerClearance) < CLEARANCE_RANK.org ? ownerClearance : "org";
|
|
1098
|
-
return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: layersFor(ctx), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {} };
|
|
1198
|
+
return { userId: ctx.userId, orgId: ctx.orgId, role: "robot", team: `fleet/${ctx.robot?.site ?? "site"}`, clearance, contextId: ctx.id, layers: withScope(layersFor(ctx), scope), ...ctx.robot?.domain ? { domain: ctx.robot.domain } : {}, ...source };
|
|
1099
1199
|
}
|
|
1100
1200
|
return {
|
|
1101
1201
|
userId: ctx.userId,
|
|
@@ -1104,7 +1204,8 @@ function readerFor(ctx, ownerClearance) {
|
|
|
1104
1204
|
team: ctx.person?.team ?? "",
|
|
1105
1205
|
clearance: ctx.person?.clearance ?? "team",
|
|
1106
1206
|
contextId: ctx.id,
|
|
1107
|
-
layers: layersFor(ctx)
|
|
1207
|
+
layers: withScope(layersFor(ctx), scope),
|
|
1208
|
+
...source
|
|
1108
1209
|
};
|
|
1109
1210
|
}
|
|
1110
1211
|
function explainSectionAccess(section, reader) {
|
|
@@ -1134,6 +1235,15 @@ function scopeAffinity(section, reader) {
|
|
|
1134
1235
|
}
|
|
1135
1236
|
function baseAffinity(section, reader) {
|
|
1136
1237
|
const kind = layerKind(section.layer);
|
|
1238
|
+
if (kind === "group" || kind === "source") {
|
|
1239
|
+
const i = reader.layers.indexOf(section.layer);
|
|
1240
|
+
if (i < 0) return 0.05;
|
|
1241
|
+
const path28 = reader.layers.filter((l) => {
|
|
1242
|
+
const k = layerKind(l);
|
|
1243
|
+
return k === "group" || k === "source";
|
|
1244
|
+
});
|
|
1245
|
+
return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
|
|
1246
|
+
}
|
|
1137
1247
|
if (reader.layers.includes(section.layer)) {
|
|
1138
1248
|
const order = reader.role === "robot" ? FLEET_LAYER_ORDER : COMPANY_LAYER_ORDER;
|
|
1139
1249
|
return 0.5 + 0.5 * (order.indexOf(kind) / Math.max(1, order.length - 1));
|
|
@@ -1159,6 +1269,7 @@ var init_layers = __esm({
|
|
|
1159
1269
|
"use strict";
|
|
1160
1270
|
init_permissions();
|
|
1161
1271
|
init_text();
|
|
1272
|
+
init_scopes();
|
|
1162
1273
|
COMPANY_LAYER_ORDER = ["org", "dept", "team", "user"];
|
|
1163
1274
|
FLEET_LAYER_ORDER = ["fleet", "arm", "mount", "robot"];
|
|
1164
1275
|
}
|
|
@@ -1748,8 +1859,8 @@ function servable(sections, reader) {
|
|
|
1748
1859
|
}
|
|
1749
1860
|
return [...out.values()];
|
|
1750
1861
|
}
|
|
1751
|
-
function recallable(sections, reader) {
|
|
1752
|
-
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect");
|
|
1862
|
+
function recallable(sections, reader, opts = {}) {
|
|
1863
|
+
return servable(sections, reader).filter((s) => s.delivery !== "always" && s.delivery !== "stop" && s.delivery !== "reflect" && (opts.across || inBoundary(s, reader)));
|
|
1753
1864
|
}
|
|
1754
1865
|
function passagesOfSection(s) {
|
|
1755
1866
|
const hit = passageCache.get(s);
|
|
@@ -1797,6 +1908,7 @@ function passageDense(embedder, pool, q) {
|
|
|
1797
1908
|
function sourceLabel(s) {
|
|
1798
1909
|
if (s.storage.type === "repo") return `repo:${s.storage.repo}/${s.storage.path}`;
|
|
1799
1910
|
if (s.storage.type === "harness") return `harness:${s.storage.path}`;
|
|
1911
|
+
if (isScopeLayer(s.layer)) return s.layer;
|
|
1800
1912
|
return s.owner.team || s.layer;
|
|
1801
1913
|
}
|
|
1802
1914
|
function push(sections, reader, query) {
|
|
@@ -1837,7 +1949,7 @@ function maxSim(embedder, q, lists) {
|
|
|
1837
1949
|
function pull(sections, reader, query, opts = {}) {
|
|
1838
1950
|
const embedders = opts.embedders ?? DEFAULT_EMBEDDERS;
|
|
1839
1951
|
const exclude = new Set(opts.exclude ?? []);
|
|
1840
|
-
const pool = recallable(sections, reader).filter((s) => !exclude.has(s.id));
|
|
1952
|
+
const pool = recallable(sections, reader, opts).filter((s) => !exclude.has(s.id));
|
|
1841
1953
|
if (!pool.length) return [];
|
|
1842
1954
|
const limit = Math.max(1, Math.min(20, query.limit ?? 10));
|
|
1843
1955
|
const signals = [];
|
|
@@ -1878,6 +1990,7 @@ function pull(sections, reader, query, opts = {}) {
|
|
|
1878
1990
|
kind: section.kind,
|
|
1879
1991
|
layer: section.layer,
|
|
1880
1992
|
source: sourceLabel(section),
|
|
1993
|
+
...isScopeLayer(section.layer) && !reader.layers.includes(section.layer) ? { outside: true } : {},
|
|
1881
1994
|
score: Math.round(score2 * 1e3) / 1e3,
|
|
1882
1995
|
reasons,
|
|
1883
1996
|
...part.get(section.id) ? { part: part.get(section.id) } : {},
|
|
@@ -1946,9 +2059,9 @@ function renderSection(s, trigger, opts = {}) {
|
|
|
1946
2059
|
`_Load the whole document, or one part, with bunshin_load {"section_id": "${s.id}"${part ? `, "anchor": "${opts.anchor}"` : ""}} (or \`bunshin load ${s.id}${part ? ` --anchor ${opts.anchor}` : ""}\`)._`
|
|
1947
2060
|
].join("\n");
|
|
1948
2061
|
}
|
|
1949
|
-
function renderCandidates(c) {
|
|
2062
|
+
function renderCandidates(c, tree) {
|
|
1950
2063
|
if (!c.length) return "No sections matched. Proceed with general knowledge; nothing here is required.";
|
|
1951
|
-
return c.map((x, i) => `${i + 1}. [${x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""})
|
|
2064
|
+
return c.map((x, i) => `${i + 1}. [${isScopeLayer(x.source) ? scopeLabel(x.source, tree) : x.source}] ${x.title} (id: ${x.id}${x.part ? `, best part: \xA7${x.part}` : ""}${x.size ? `, ${x.size} words` : ""}${x.outside ? "; borrowed from outside this project's scope" : ""})
|
|
1952
2065
|
when: ${x.when}`).join("\n");
|
|
1953
2066
|
}
|
|
1954
2067
|
var PASSAGE_CHARS, passageCache, words;
|
|
@@ -1957,6 +2070,7 @@ var init_engine = __esm({
|
|
|
1957
2070
|
"use strict";
|
|
1958
2071
|
init_text();
|
|
1959
2072
|
init_layers();
|
|
2073
|
+
init_scopes();
|
|
1960
2074
|
init_document();
|
|
1961
2075
|
init_text_index();
|
|
1962
2076
|
PASSAGE_CHARS = 4e3;
|
|
@@ -1968,7 +2082,7 @@ var init_engine = __esm({
|
|
|
1968
2082
|
// ../core/src/v2/guidelines.ts
|
|
1969
2083
|
function reflectionGuidelines(sections, reader, opts = {}) {
|
|
1970
2084
|
const pool = reader ? servable(sections, reader) : sections;
|
|
1971
|
-
return pool.filter((s) => s.delivery === "reflect" && s.status === "active").filter((s) => !opts.layer || s.layer === "org" || opts.layer === s.layer || opts.layer.startsWith(`${s.layer}/`)).sort((a, b) => generality(a.layer) - generality(b.layer) || a.id.localeCompare(b.id)).map((s) => ({ id: s.id, title: s.title, layer: s.layer, body: s.body }));
|
|
2085
|
+
return pool.filter((s) => s.delivery === "reflect" && s.status === "active" && (!reader || inBoundary(s, reader))).filter((s) => !opts.layer || s.layer === "org" || opts.layer === s.layer || opts.layer.startsWith(`${s.layer}/`)).sort((a, b) => generality(a.layer) - generality(b.layer) || a.id.localeCompare(b.id)).map((s) => ({ id: s.id, title: s.title, layer: s.layer, body: s.body }));
|
|
1972
2086
|
}
|
|
1973
2087
|
function guidelineRules(body) {
|
|
1974
2088
|
const at = body.search(/^## /m);
|
|
@@ -2004,8 +2118,9 @@ var init_guidelines = __esm({
|
|
|
2004
2118
|
init_types();
|
|
2005
2119
|
init_layers();
|
|
2006
2120
|
init_engine();
|
|
2121
|
+
init_scopes();
|
|
2007
2122
|
GUIDELINES_ANCHOR = "rules";
|
|
2008
|
-
generality = (l) => ({ org: 0, dept: 1, fleet: 1, team: 2, arm: 3, mount: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
|
|
2123
|
+
generality = (l) => ({ org: 0, dept: 1, fleet: 1, group: 2, team: 2, arm: 3, mount: 3, source: 3, robot: 4, user: 4 })[layerKind(l)] ?? 5;
|
|
2009
2124
|
}
|
|
2010
2125
|
});
|
|
2011
2126
|
|
|
@@ -2191,7 +2306,7 @@ function redactDeep(value, opts = {}) {
|
|
|
2191
2306
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v, opts)]));
|
|
2192
2307
|
return value;
|
|
2193
2308
|
}
|
|
2194
|
-
var REDACTED, MARKER_RE, isMarked, words2, KEY_FALSE, SCHEMA_KEY, PASS_WORD, PASS_STAT, nameCache, KEYWORD_VALUES, PLACEHOLDER_RE, CODE_RE, STRONG, HASH_WORD, COUNT_NAME, RESOURCE_TAIL, UUID_RE, PATH_RE, credentialish, CARD_DIGITS, ASSET_EXT, NOREPLY_RE,
|
|
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
|
-
|
|
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]?)(${
|
|
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})?(["']?)(${
|
|
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.
|
|
2786
|
+
${input.scopes?.length ? `
|
|
2787
|
+
Where a lesson applies (put each at the narrowest scope it holds for; when unsure, the narrowest):
|
|
2788
|
+
${[...input.scopes].reverse().map((x) => `- ${x.layer}: ${x.label}`).join("\n")}
|
|
2789
|
+
- org: everywhere in this workspace
|
|
2790
|
+
A fact, command or habit of this project \u2192 this project. True of every project in a group alike \u2192
|
|
2791
|
+
that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
|
|
2792
|
+
are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
|
|
2793
|
+
"layer" in the target of a new document (an edit keeps the document's own scope).
|
|
2794
|
+
` : ""}${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
|
|
2672
2795
|
not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
|
|
2673
2796
|
A lesson for anyone doing the task belongs in the task's document instead.
|
|
2674
2797
|
` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
|
|
@@ -2952,6 +3075,8 @@ var init_reconcile = __esm({
|
|
|
2952
3075
|
init_document();
|
|
2953
3076
|
init_markdown();
|
|
2954
3077
|
init_text_index();
|
|
3078
|
+
init_scopes();
|
|
3079
|
+
init_sources();
|
|
2955
3080
|
init_types();
|
|
2956
3081
|
}
|
|
2957
3082
|
});
|
|
@@ -2993,13 +3118,14 @@ var init_profile = __esm({
|
|
|
2993
3118
|
}
|
|
2994
3119
|
});
|
|
2995
3120
|
|
|
2996
|
-
// ../core/src/v2/
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
}
|
|
3000
|
-
var init_sources = __esm({
|
|
3001
|
-
"../core/src/v2/sources.ts"() {
|
|
3121
|
+
// ../core/src/v2/generalize.ts
|
|
3122
|
+
var init_generalize = __esm({
|
|
3123
|
+
"../core/src/v2/generalize.ts"() {
|
|
3002
3124
|
"use strict";
|
|
3125
|
+
init_layers();
|
|
3126
|
+
init_guidelines();
|
|
3127
|
+
init_reflect();
|
|
3128
|
+
init_scopes();
|
|
3003
3129
|
}
|
|
3004
3130
|
});
|
|
3005
3131
|
|
|
@@ -3025,6 +3151,8 @@ var init_v2 = __esm({
|
|
|
3025
3151
|
init_profile();
|
|
3026
3152
|
init_guidelines();
|
|
3027
3153
|
init_sources();
|
|
3154
|
+
init_scopes();
|
|
3155
|
+
init_generalize();
|
|
3028
3156
|
}
|
|
3029
3157
|
});
|
|
3030
3158
|
|
|
@@ -3123,77 +3251,355 @@ var init_api = __esm({
|
|
|
3123
3251
|
}
|
|
3124
3252
|
});
|
|
3125
3253
|
|
|
3126
|
-
//
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3254
|
+
// src/v2/repo.ts
|
|
3255
|
+
import fs6 from "node:fs";
|
|
3256
|
+
import path5 from "node:path";
|
|
3257
|
+
import { execFileSync } from "node:child_process";
|
|
3258
|
+
function git(cwd, args, timeout = 1500) {
|
|
3259
|
+
try {
|
|
3260
|
+
return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
3261
|
+
} catch {
|
|
3262
|
+
return null;
|
|
3130
3263
|
}
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
);
|
|
3264
|
+
}
|
|
3265
|
+
function repoRoot(cwd, storeRoot) {
|
|
3266
|
+
const key = `${cwd}
|
|
3267
|
+
${storeRoot ?? ""}`;
|
|
3268
|
+
const hit = rootCache.get(key);
|
|
3269
|
+
if (hit) return hit;
|
|
3270
|
+
const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
3271
|
+
let root = top || "";
|
|
3272
|
+
if (!root && storeRoot && path5.basename(storeRoot) === ".bunshin") {
|
|
3273
|
+
const parent = path5.dirname(storeRoot);
|
|
3274
|
+
if (path5.resolve(cwd).startsWith(path5.resolve(parent))) root = parent;
|
|
3143
3275
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3276
|
+
root = root || path5.resolve(cwd);
|
|
3277
|
+
rootCache.set(key, root);
|
|
3278
|
+
return root;
|
|
3279
|
+
}
|
|
3280
|
+
function parseRemote(url) {
|
|
3281
|
+
const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
3282
|
+
if (!clean2) return null;
|
|
3283
|
+
const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
|
|
3284
|
+
const pathPart = scp ? scp[1] : (() => {
|
|
3285
|
+
try {
|
|
3286
|
+
return new URL(clean2).pathname;
|
|
3287
|
+
} catch {
|
|
3288
|
+
return clean2;
|
|
3289
|
+
}
|
|
3290
|
+
})();
|
|
3291
|
+
const segs = pathPart.split("/").filter(Boolean);
|
|
3292
|
+
if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
|
|
3293
|
+
return segs[0] ?? null;
|
|
3294
|
+
}
|
|
3295
|
+
function repoName(root) {
|
|
3296
|
+
const url = git(root, ["remote", "get-url", "origin"]);
|
|
3297
|
+
return url && parseRemote(url) || path5.basename(root);
|
|
3298
|
+
}
|
|
3299
|
+
function repoCommit(root) {
|
|
3300
|
+
return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
|
|
3301
|
+
}
|
|
3302
|
+
function listDocs(root) {
|
|
3303
|
+
const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
|
|
3304
|
+
if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs6.existsSync(path5.join(root, rel)) && !deployedSkill(root, rel)).sort();
|
|
3305
|
+
const out = [];
|
|
3306
|
+
let visited = 0;
|
|
3307
|
+
const walk = (dir, rel, depth) => {
|
|
3308
|
+
if (depth > 8 || visited++ > 2e4) return;
|
|
3309
|
+
let entries;
|
|
3310
|
+
try {
|
|
3311
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
3312
|
+
} catch {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
for (const e of entries) {
|
|
3316
|
+
if (e.isDirectory()) {
|
|
3317
|
+
if (!SKIP_DIRS.has(e.name) && !fs6.existsSync(path5.join(dir, e.name, ".git"))) walk(path5.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
|
|
3318
|
+
} else if (e.isFile()) {
|
|
3319
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
3320
|
+
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
walk(root, "", 0);
|
|
3325
|
+
return out.sort();
|
|
3326
|
+
}
|
|
3327
|
+
function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
3328
|
+
const root = repoRoot(cwd, v2.store.root);
|
|
3329
|
+
const now = opts.now ?? Date.now();
|
|
3330
|
+
const cached = v2.readRepoIndex(root);
|
|
3331
|
+
const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
|
|
3332
|
+
const sameRepo = cached && cached.ownerTeam === ownerTeam;
|
|
3333
|
+
const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
|
|
3334
|
+
const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
|
|
3335
|
+
const paths = fresh ? cached.paths : listDocs(root);
|
|
3336
|
+
let changed = !fresh || !sameRepo || cached.repo !== repo;
|
|
3337
|
+
const files = {};
|
|
3338
|
+
for (const rel of paths) {
|
|
3339
|
+
const abs = path5.join(root, rel);
|
|
3340
|
+
let stat;
|
|
3341
|
+
try {
|
|
3342
|
+
stat = fs6.statSync(abs);
|
|
3343
|
+
} catch {
|
|
3344
|
+
changed = true;
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
|
|
3348
|
+
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
|
|
3349
|
+
files[rel] = prev;
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
changed = true;
|
|
3353
|
+
try {
|
|
3354
|
+
const text2 = fs6.readFileSync(abs, "utf8");
|
|
3355
|
+
files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
|
|
3356
|
+
} catch {
|
|
3357
|
+
}
|
|
3150
3358
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3359
|
+
const index = {
|
|
3360
|
+
repo,
|
|
3361
|
+
ownerTeam,
|
|
3362
|
+
listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
|
|
3363
|
+
paths,
|
|
3364
|
+
files
|
|
3365
|
+
};
|
|
3366
|
+
if (changed) {
|
|
3367
|
+
try {
|
|
3368
|
+
v2.writeRepoIndex(root, index);
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3157
3371
|
}
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3372
|
+
return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
|
|
3373
|
+
}
|
|
3374
|
+
function relativeToRoot(root, file, cwd = root) {
|
|
3375
|
+
if (!file) return null;
|
|
3376
|
+
const abs = path5.isAbsolute(file) ? file : path5.resolve(cwd, file);
|
|
3377
|
+
let real = abs;
|
|
3378
|
+
let realRoot = root;
|
|
3379
|
+
try {
|
|
3380
|
+
realRoot = fs6.realpathSync(root);
|
|
3381
|
+
} catch {
|
|
3164
3382
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3383
|
+
try {
|
|
3384
|
+
real = fs6.realpathSync(abs);
|
|
3385
|
+
} catch {
|
|
3386
|
+
try {
|
|
3387
|
+
real = path5.join(fs6.realpathSync(path5.dirname(abs)), path5.basename(abs));
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
for (const [r, f] of [
|
|
3392
|
+
[realRoot, real],
|
|
3393
|
+
[root, abs]
|
|
3394
|
+
]) {
|
|
3395
|
+
const rel = path5.relative(r, f);
|
|
3396
|
+
if (rel && !rel.startsWith("..") && !path5.isAbsolute(rel)) return rel.split(path5.sep).join("/");
|
|
3397
|
+
}
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
|
|
3401
|
+
var init_repo = __esm({
|
|
3402
|
+
"src/v2/repo.ts"() {
|
|
3170
3403
|
"use strict";
|
|
3404
|
+
init_v22();
|
|
3405
|
+
DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
|
|
3406
|
+
SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
|
|
3407
|
+
LIST_TTL_MS = 6e4;
|
|
3408
|
+
rootCache = /* @__PURE__ */ new Map();
|
|
3409
|
+
isDoc = (rel) => {
|
|
3410
|
+
const parts = rel.split("/");
|
|
3411
|
+
if (parts.some((p) => SKIP_DIRS.has(p))) return false;
|
|
3412
|
+
return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
|
|
3413
|
+
};
|
|
3414
|
+
deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs6.existsSync(path5.join(root, path5.posix.dirname(rel), ".bunshin.json"));
|
|
3171
3415
|
}
|
|
3172
3416
|
});
|
|
3173
3417
|
|
|
3174
|
-
//
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3418
|
+
// src/v2/source.ts
|
|
3419
|
+
var source_exports = {};
|
|
3420
|
+
__export(source_exports, {
|
|
3421
|
+
cwdInPool: () => cwdInPool,
|
|
3422
|
+
inPool: () => inPool,
|
|
3423
|
+
listFolders: () => listFolders,
|
|
3424
|
+
projectRootFor: () => projectRootFor,
|
|
3425
|
+
select: () => select,
|
|
3426
|
+
sourceFor: () => sourceFor,
|
|
3427
|
+
storeHome: () => storeHome,
|
|
3428
|
+
topFolder: () => topFolder
|
|
3429
|
+
});
|
|
3430
|
+
import fs7 from "node:fs";
|
|
3431
|
+
import path6 from "node:path";
|
|
3432
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3433
|
+
function storeHome(storeRoot) {
|
|
3434
|
+
return path6.dirname(path6.resolve(storeRoot));
|
|
3181
3435
|
}
|
|
3182
|
-
function
|
|
3183
|
-
const
|
|
3184
|
-
return
|
|
3185
|
-
|
|
3186
|
-
${lesson.tags.join(" ")}
|
|
3187
|
-
${lesson.content}
|
|
3188
|
-
${extra}`;
|
|
3436
|
+
function topFolder(cwd, storeRoot) {
|
|
3437
|
+
const rel = path6.relative(storeHome(storeRoot), path6.resolve(cwd));
|
|
3438
|
+
if (!rel || rel.startsWith("..") || path6.isAbsolute(rel)) return "";
|
|
3439
|
+
return rel.split(path6.sep)[0] ?? "";
|
|
3189
3440
|
}
|
|
3190
|
-
function
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3441
|
+
function inPool(selection, folder) {
|
|
3442
|
+
if (!folder || !selection) return true;
|
|
3443
|
+
if (selection.exclude?.includes(folder)) return false;
|
|
3444
|
+
return selection.only ? selection.only.includes(folder) : true;
|
|
3445
|
+
}
|
|
3446
|
+
function cwdInPool(config, cwd, storeRoot) {
|
|
3447
|
+
return inPool(config.sources, topFolder(cwd, storeRoot));
|
|
3448
|
+
}
|
|
3449
|
+
function git2(cwd, args) {
|
|
3450
|
+
try {
|
|
3451
|
+
return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
|
|
3452
|
+
} catch {
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function sourceFor(cwd, storeRoot, ctx) {
|
|
3457
|
+
if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
|
|
3458
|
+
const key = `${path6.resolve(cwd)}
|
|
3459
|
+
${storeRoot}`;
|
|
3460
|
+
if (sourceCache.has(key)) return sourceCache.get(key);
|
|
3461
|
+
const found = findSource(cwd, storeRoot);
|
|
3462
|
+
sourceCache.set(key, found);
|
|
3463
|
+
return found;
|
|
3464
|
+
}
|
|
3465
|
+
function findSource(cwd, storeRoot) {
|
|
3466
|
+
const home = storeHome(storeRoot);
|
|
3467
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
3468
|
+
if (top) {
|
|
3469
|
+
const remote = git2(top, ["remote", "get-url", "origin"]);
|
|
3470
|
+
const named = remote ? parseRemote(remote) : null;
|
|
3471
|
+
if (named) return sourceId("repo", named);
|
|
3472
|
+
const rel = path6.relative(home, top);
|
|
3473
|
+
return sourceId("folder", rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel.split(path6.sep).join("/") : path6.basename(top));
|
|
3474
|
+
}
|
|
3475
|
+
const folder = topFolder(cwd, storeRoot);
|
|
3476
|
+
return folder ? sourceId("folder", folder) : void 0;
|
|
3477
|
+
}
|
|
3478
|
+
function projectRootFor(cwd, storeRoot) {
|
|
3479
|
+
const home = storeHome(storeRoot);
|
|
3480
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
3481
|
+
if (top) {
|
|
3482
|
+
const rel = path6.relative(home, top);
|
|
3483
|
+
if (!rel.startsWith("..") && !path6.isAbsolute(rel)) return top;
|
|
3484
|
+
}
|
|
3485
|
+
const folder = topFolder(cwd, storeRoot);
|
|
3486
|
+
return folder ? path6.join(home, folder) : home;
|
|
3487
|
+
}
|
|
3488
|
+
function listFolders(storeRoot, selection, opts = {}) {
|
|
3489
|
+
const home = storeHome(storeRoot);
|
|
3490
|
+
let names = [];
|
|
3491
|
+
try {
|
|
3492
|
+
names = fs7.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
return names.map((folder) => ({
|
|
3496
|
+
folder,
|
|
3497
|
+
included: inPool(selection, folder),
|
|
3498
|
+
...opts.sources ? { source: sourceFor(path6.join(home, folder), storeRoot) } : {}
|
|
3499
|
+
}));
|
|
3500
|
+
}
|
|
3501
|
+
function select(current, action, folders) {
|
|
3502
|
+
const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
|
|
3503
|
+
const only = current?.only;
|
|
3504
|
+
const exclude = current?.exclude ?? [];
|
|
3505
|
+
let next;
|
|
3506
|
+
switch (action) {
|
|
3507
|
+
case "all":
|
|
3508
|
+
return void 0;
|
|
3509
|
+
case "only":
|
|
3510
|
+
next = { only: uniq(folders) };
|
|
3511
|
+
break;
|
|
3512
|
+
case "include":
|
|
3513
|
+
next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
|
|
3514
|
+
break;
|
|
3515
|
+
case "exclude":
|
|
3516
|
+
next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
|
|
3517
|
+
break;
|
|
3518
|
+
}
|
|
3519
|
+
if (!next.exclude?.length) delete next.exclude;
|
|
3520
|
+
return next.only || next.exclude ? next : void 0;
|
|
3521
|
+
}
|
|
3522
|
+
var sourceCache;
|
|
3523
|
+
var init_source = __esm({
|
|
3524
|
+
"src/v2/source.ts"() {
|
|
3525
|
+
"use strict";
|
|
3526
|
+
init_v22();
|
|
3527
|
+
init_repo();
|
|
3528
|
+
sourceCache = /* @__PURE__ */ new Map();
|
|
3529
|
+
}
|
|
3530
|
+
});
|
|
3531
|
+
|
|
3532
|
+
// ../core/src/types.ts
|
|
3533
|
+
var init_types2 = __esm({
|
|
3534
|
+
"../core/src/types.ts"() {
|
|
3535
|
+
"use strict";
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
// ../core/src/gate.ts
|
|
3540
|
+
var CURRENT_CHAT_PREFIX, ANAPHORIC_QUESTION;
|
|
3541
|
+
var init_gate = __esm({
|
|
3542
|
+
"../core/src/gate.ts"() {
|
|
3543
|
+
"use strict";
|
|
3544
|
+
CURRENT_CHAT_PREFIX = "(?:(?:so\\s+)?(?:in|within)\\s+(?:this|the)\\s+(?:chat|conversation|session)\\s*,?\\s*)?";
|
|
3545
|
+
ANAPHORIC_QUESTION = new RegExp(
|
|
3546
|
+
"^" + CURRENT_CHAT_PREFIX + "(?:(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\s+.*\\b(?:this|that|it|these|those|they)\\b.*|(?:and\\s+)?(?:does|do|did|is|are|was|were|can|could|would|will|has|have|had)\\b.*\\b(?:also|too|as\\s+well)\\b.*|(?:what|why|how|where|when)\\s+(?:does|do|did|is|are|was|were)\\s+(?:this|that|it|these|those|they)\\b.*|(?:can|could|would)\\s+you\\s+(?:explain|clarify|show)\\s+(?:this|that|it)\\b.*)$",
|
|
3547
|
+
"i"
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
});
|
|
3551
|
+
|
|
3552
|
+
// ../core/src/metrics.ts
|
|
3553
|
+
var init_metrics = __esm({
|
|
3554
|
+
"../core/src/metrics.ts"() {
|
|
3555
|
+
"use strict";
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
// ../core/src/lesson/id.ts
|
|
3560
|
+
var init_id = __esm({
|
|
3561
|
+
"../core/src/lesson/id.ts"() {
|
|
3562
|
+
"use strict";
|
|
3563
|
+
}
|
|
3564
|
+
});
|
|
3565
|
+
|
|
3566
|
+
// ../core/src/lesson/markdown.ts
|
|
3567
|
+
var init_markdown2 = __esm({
|
|
3568
|
+
"../core/src/lesson/markdown.ts"() {
|
|
3569
|
+
"use strict";
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
// ../core/src/lesson/index.ts
|
|
3574
|
+
var init_lesson = __esm({
|
|
3575
|
+
"../core/src/lesson/index.ts"() {
|
|
3576
|
+
"use strict";
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
|
|
3580
|
+
// ../core/src/serve/score.ts
|
|
3581
|
+
function classifyTaskMode(task) {
|
|
3582
|
+
const t = (task || "").trim();
|
|
3583
|
+
if (INVESTIGATE_RE.test(t) && !BUILD_RE.test(t)) return "investigate";
|
|
3584
|
+
if (BUILD_RE.test(t)) return "build";
|
|
3585
|
+
if (CONVERSE_RE.test(t) || t.endsWith("?")) return "converse";
|
|
3586
|
+
return "build";
|
|
3587
|
+
}
|
|
3588
|
+
function lessonText(lesson) {
|
|
3589
|
+
const extra = lesson.kind === "workflow" ? [...lesson.preconditions, ...lesson.steps.map((s) => `${s.action} ${s.detail ?? ""} ${s.verify ?? ""}`), lesson.outcome].join(" ") : "";
|
|
3590
|
+
return `${lesson.title}
|
|
3591
|
+
${lesson.gist}
|
|
3592
|
+
${lesson.tags.join(" ")}
|
|
3593
|
+
${lesson.content}
|
|
3594
|
+
${extra}`;
|
|
3595
|
+
}
|
|
3596
|
+
function score(lesson, request, profile, textRelevance, opts = {}) {
|
|
3597
|
+
const w = { ...DEFAULT_WEIGHTS, ...opts.weights ?? {} };
|
|
3598
|
+
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3599
|
+
const reasons = [];
|
|
3600
|
+
const mode = classifyTaskMode(request.task);
|
|
3601
|
+
if (lesson.status === "retired" || lesson.status === "superseded") {
|
|
3602
|
+
return { score: -1, reasons: [`status:${lesson.status} excluded`], excluded: true, mode };
|
|
3197
3603
|
}
|
|
3198
3604
|
let s = 0;
|
|
3199
3605
|
const rel = clamp(textRelevance);
|
|
@@ -3643,8 +4049,8 @@ var init_src = __esm({
|
|
|
3643
4049
|
});
|
|
3644
4050
|
|
|
3645
4051
|
// src/v2/store.ts
|
|
3646
|
-
import
|
|
3647
|
-
import
|
|
4052
|
+
import fs8 from "node:fs";
|
|
4053
|
+
import path7 from "node:path";
|
|
3648
4054
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
|
|
3649
4055
|
function hexId(prefix) {
|
|
3650
4056
|
return `${prefix}_${randomBytes3(6).toString("hex")}`;
|
|
@@ -3678,12 +4084,12 @@ var init_store2 = __esm({
|
|
|
3678
4084
|
V2Store = class {
|
|
3679
4085
|
constructor(store) {
|
|
3680
4086
|
this.store = store;
|
|
3681
|
-
this.dir =
|
|
4087
|
+
this.dir = path7.join(store.root, "v2");
|
|
3682
4088
|
}
|
|
3683
4089
|
store;
|
|
3684
4090
|
dir;
|
|
3685
4091
|
p(...parts) {
|
|
3686
|
-
return
|
|
4092
|
+
return path7.join(this.dir, ...parts);
|
|
3687
4093
|
}
|
|
3688
4094
|
// ── context / bundle / params ──
|
|
3689
4095
|
readContext() {
|
|
@@ -3764,9 +4170,9 @@ var init_store2 = __esm({
|
|
|
3764
4170
|
const dir = this.p("sessions");
|
|
3765
4171
|
if (!fileExists(dir)) return [];
|
|
3766
4172
|
const out = [];
|
|
3767
|
-
for (const f of
|
|
4173
|
+
for (const f of fs8.readdirSync(dir)) {
|
|
3768
4174
|
if (!f.endsWith(".json") || f === "current.json") continue;
|
|
3769
|
-
const raw = readJson(
|
|
4175
|
+
const raw = readJson(path7.join(dir, f), null);
|
|
3770
4176
|
if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
|
|
3771
4177
|
}
|
|
3772
4178
|
return out;
|
|
@@ -3794,7 +4200,7 @@ var init_store2 = __esm({
|
|
|
3794
4200
|
let at = raw.at;
|
|
3795
4201
|
if (!at) {
|
|
3796
4202
|
try {
|
|
3797
|
-
at =
|
|
4203
|
+
at = fs8.statSync(this.p("outcome", `${fileSafe(sessionId)}.json`)).mtime.toISOString();
|
|
3798
4204
|
} catch {
|
|
3799
4205
|
at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3800
4206
|
}
|
|
@@ -3821,13 +4227,13 @@ var init_store2 = __esm({
|
|
|
3821
4227
|
/** Exclusive lock file; returns a release function, or null when another process holds it. */
|
|
3822
4228
|
tryLock(name, staleMs = 10 * 6e4) {
|
|
3823
4229
|
const file = this.p("locks", `${fileSafe(name)}.lock`);
|
|
3824
|
-
ensureDir(
|
|
4230
|
+
ensureDir(path7.dirname(file));
|
|
3825
4231
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3826
4232
|
try {
|
|
3827
|
-
|
|
4233
|
+
fs8.writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
3828
4234
|
return () => {
|
|
3829
4235
|
try {
|
|
3830
|
-
|
|
4236
|
+
fs8.unlinkSync(file);
|
|
3831
4237
|
} catch {
|
|
3832
4238
|
}
|
|
3833
4239
|
};
|
|
@@ -3835,8 +4241,8 @@ var init_store2 = __esm({
|
|
|
3835
4241
|
if (err.code !== "EEXIST") return null;
|
|
3836
4242
|
let stale = false;
|
|
3837
4243
|
try {
|
|
3838
|
-
const owner = Number(
|
|
3839
|
-
const age = Date.now() -
|
|
4244
|
+
const owner = Number(fs8.readFileSync(file, "utf8"));
|
|
4245
|
+
const age = Date.now() - fs8.statSync(file).mtimeMs;
|
|
3840
4246
|
if (age > staleMs) stale = true;
|
|
3841
4247
|
else if (Number.isInteger(owner) && owner > 0) {
|
|
3842
4248
|
try {
|
|
@@ -3850,7 +4256,7 @@ var init_store2 = __esm({
|
|
|
3850
4256
|
}
|
|
3851
4257
|
if (!stale) return null;
|
|
3852
4258
|
try {
|
|
3853
|
-
|
|
4259
|
+
fs8.unlinkSync(file);
|
|
3854
4260
|
} catch {
|
|
3855
4261
|
}
|
|
3856
4262
|
}
|
|
@@ -3867,15 +4273,15 @@ var init_store2 = __esm({
|
|
|
3867
4273
|
const dir = this.p(name);
|
|
3868
4274
|
if (!fileExists(dir)) return [];
|
|
3869
4275
|
const out = [];
|
|
3870
|
-
for (const f of
|
|
4276
|
+
for (const f of fs8.readdirSync(dir)) {
|
|
3871
4277
|
if (!f.endsWith(".json")) continue;
|
|
3872
|
-
const v = readJson(
|
|
4278
|
+
const v = readJson(path7.join(dir, f), null);
|
|
3873
4279
|
if (v) out.push(v);
|
|
3874
4280
|
}
|
|
3875
4281
|
return out;
|
|
3876
4282
|
}
|
|
3877
4283
|
};
|
|
3878
|
-
rootKey = (root) => createHash2("sha256").update(
|
|
4284
|
+
rootKey = (root) => createHash2("sha256").update(path7.resolve(root)).digest("hex").slice(0, 16);
|
|
3879
4285
|
addUnique = (list2, ...ids) => {
|
|
3880
4286
|
for (const id of ids) if (id && !list2.includes(id)) list2.push(id);
|
|
3881
4287
|
};
|
|
@@ -3883,15 +4289,15 @@ var init_store2 = __esm({
|
|
|
3883
4289
|
});
|
|
3884
4290
|
|
|
3885
4291
|
// src/v2/deploy.ts
|
|
3886
|
-
import { execFileSync } from "node:child_process";
|
|
4292
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3887
4293
|
import { createHash as createHash3 } from "node:crypto";
|
|
3888
|
-
import
|
|
4294
|
+
import fs9 from "node:fs";
|
|
3889
4295
|
import os2 from "node:os";
|
|
3890
|
-
import
|
|
4296
|
+
import path8 from "node:path";
|
|
3891
4297
|
function deployWorkspace(env = process.env) {
|
|
3892
|
-
if (env.BUNSHIN_DEPLOY_WORKSPACE) return
|
|
4298
|
+
if (env.BUNSHIN_DEPLOY_WORKSPACE) return path8.resolve(env.BUNSHIN_DEPLOY_WORKSPACE);
|
|
3893
4299
|
try {
|
|
3894
|
-
const ws = JSON.parse(
|
|
4300
|
+
const ws = JSON.parse(fs9.readFileSync(settingsFile(env.BUNSHIN_DEPLOY_HOME || os2.homedir()), "utf8")).workspace;
|
|
3895
4301
|
return typeof ws === "string" && ws ? ws : null;
|
|
3896
4302
|
} catch {
|
|
3897
4303
|
return null;
|
|
@@ -3900,24 +4306,24 @@ function deployWorkspace(env = process.env) {
|
|
|
3900
4306
|
function deployPaths(env = process.env, orgId) {
|
|
3901
4307
|
const home = env.BUNSHIN_DEPLOY_HOME || os2.homedir();
|
|
3902
4308
|
const ws = deployWorkspace(env);
|
|
3903
|
-
const state =
|
|
3904
|
-
const shared = { claude:
|
|
3905
|
-
const org = orgId ?
|
|
4309
|
+
const state = path8.join(home, ".bunshin", "deploy");
|
|
4310
|
+
const shared = { claude: path8.join(ws ?? home, ".claude", "skills"), codex: path8.join(ws ?? home, ".codex", "skills") };
|
|
4311
|
+
const org = orgId ? path8.join(state, orgKey(orgId)) : null;
|
|
3906
4312
|
return {
|
|
3907
4313
|
home,
|
|
3908
|
-
claudeSkills: org ?
|
|
3909
|
-
codexSkills: org ?
|
|
3910
|
-
...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex,
|
|
3911
|
-
claudeMd: ws ?
|
|
3912
|
-
agentsMd: ws ?
|
|
3913
|
-
library: org ?
|
|
4314
|
+
claudeSkills: org ? path8.join(org, "claude-skills") : shared.claude,
|
|
4315
|
+
codexSkills: org ? path8.join(org, "codex-skills") : shared.codex,
|
|
4316
|
+
...org ? { legacy: [.../* @__PURE__ */ new Set([shared.claude, shared.codex, path8.join(home, ".claude", "skills"), path8.join(home, ".codex", "skills"), path8.join(home, ".bunshin", "library")])] } : {},
|
|
4317
|
+
claudeMd: ws ? path8.join(ws, "CLAUDE.md") : path8.join(home, ".claude", "CLAUDE.md"),
|
|
4318
|
+
agentsMd: ws ? path8.join(ws, "AGENTS.md") : path8.join(home, ".codex", "AGENTS.md"),
|
|
4319
|
+
library: org ? path8.join(org, "library") : path8.join(home, ".bunshin", "library"),
|
|
3914
4320
|
// Pack executables are linked into one folder on PATH (shared by name; their files are per organization).
|
|
3915
|
-
bin:
|
|
4321
|
+
bin: path8.join(home, ".bunshin", "bin"),
|
|
3916
4322
|
state
|
|
3917
4323
|
};
|
|
3918
4324
|
}
|
|
3919
4325
|
function statePath(p, orgId) {
|
|
3920
|
-
return
|
|
4326
|
+
return path8.join(p.state, `${orgKey(orgId)}.json`);
|
|
3921
4327
|
}
|
|
3922
4328
|
function readState(p, orgId) {
|
|
3923
4329
|
const s = readJson(statePath(p, orgId), null);
|
|
@@ -3925,7 +4331,7 @@ function readState(p, orgId) {
|
|
|
3925
4331
|
}
|
|
3926
4332
|
function readText(file) {
|
|
3927
4333
|
try {
|
|
3928
|
-
return
|
|
4334
|
+
return fs9.readFileSync(file, "utf8");
|
|
3929
4335
|
} catch {
|
|
3930
4336
|
return null;
|
|
3931
4337
|
}
|
|
@@ -3933,13 +4339,13 @@ function readText(file) {
|
|
|
3933
4339
|
function writeFile(file, content, mode) {
|
|
3934
4340
|
writeTextAtomic(file, content);
|
|
3935
4341
|
try {
|
|
3936
|
-
|
|
4342
|
+
fs9.chmodSync(file, mode === "x" ? 493 : 420);
|
|
3937
4343
|
} catch {
|
|
3938
4344
|
}
|
|
3939
4345
|
}
|
|
3940
4346
|
function removeFile(file) {
|
|
3941
4347
|
try {
|
|
3942
|
-
|
|
4348
|
+
fs9.unlinkSync(file);
|
|
3943
4349
|
} catch {
|
|
3944
4350
|
}
|
|
3945
4351
|
}
|
|
@@ -3947,33 +4353,33 @@ function pruneDirs(dir, stop) {
|
|
|
3947
4353
|
let d = dir;
|
|
3948
4354
|
while (d.startsWith(stop) && d !== stop) {
|
|
3949
4355
|
try {
|
|
3950
|
-
if (
|
|
3951
|
-
|
|
4356
|
+
if (fs9.readdirSync(d).length) return;
|
|
4357
|
+
fs9.rmdirSync(d);
|
|
3952
4358
|
} catch {
|
|
3953
4359
|
return;
|
|
3954
4360
|
}
|
|
3955
|
-
d =
|
|
4361
|
+
d = path8.dirname(d);
|
|
3956
4362
|
}
|
|
3957
4363
|
}
|
|
3958
4364
|
function listFiles(dir, base = dir) {
|
|
3959
4365
|
let entries;
|
|
3960
4366
|
try {
|
|
3961
|
-
entries =
|
|
4367
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3962
4368
|
} catch {
|
|
3963
4369
|
return [];
|
|
3964
4370
|
}
|
|
3965
4371
|
const out = [];
|
|
3966
4372
|
for (const e of entries) {
|
|
3967
|
-
const abs =
|
|
4373
|
+
const abs = path8.join(dir, e.name);
|
|
3968
4374
|
if (e.isDirectory()) out.push(...listFiles(abs, base));
|
|
3969
|
-
else if (e.isFile()) out.push(
|
|
4375
|
+
else if (e.isFile()) out.push(path8.relative(base, abs).split(path8.sep).join("/"));
|
|
3970
4376
|
}
|
|
3971
4377
|
return out.sort();
|
|
3972
4378
|
}
|
|
3973
4379
|
function childDir(root, name) {
|
|
3974
4380
|
if (!name || !SAFE_NAME.test(name)) return null;
|
|
3975
|
-
const dir =
|
|
3976
|
-
return
|
|
4381
|
+
const dir = path8.join(root, name);
|
|
4382
|
+
return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
|
|
3977
4383
|
}
|
|
3978
4384
|
function planDeploy(bundle, p, harnesses) {
|
|
3979
4385
|
const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
|
|
@@ -4080,7 +4486,7 @@ function removeBlock(file) {
|
|
|
4080
4486
|
if (start < 0 || end < start) return false;
|
|
4081
4487
|
const rest = (text2.slice(0, start).replace(/\s*$/, "") + "\n" + text2.slice(end + BLOCK_END.length).replace(/^\s*/, "")).trim();
|
|
4082
4488
|
if (rest) writeTextAtomic(file, rest + "\n");
|
|
4083
|
-
else
|
|
4489
|
+
else fs9.unlinkSync(file);
|
|
4084
4490
|
return true;
|
|
4085
4491
|
}
|
|
4086
4492
|
function writeBlock(file, block2) {
|
|
@@ -4101,7 +4507,7 @@ function skillIndex(skills, p) {
|
|
|
4101
4507
|
"",
|
|
4102
4508
|
"When a task matches a skill below, read its SKILL.md and follow it before doing the work by hand.",
|
|
4103
4509
|
"",
|
|
4104
|
-
...skills.map((s) => `- **${s.skill.name}** (\`${
|
|
4510
|
+
...skills.map((s) => `- **${s.skill.name}** (\`${path8.join(p.codexSkills, s.skill.name, "SKILL.md")}\`): ${neutralizeMarkers(s.skill.description).replace(/\s+/g, " ").trim()}`)
|
|
4105
4511
|
];
|
|
4106
4512
|
return lines.join("\n");
|
|
4107
4513
|
}
|
|
@@ -4150,7 +4556,7 @@ function captureDoc(c, doc, out) {
|
|
|
4150
4556
|
for (const dir of doc.dirs ?? []) {
|
|
4151
4557
|
for (const rel of listFiles(dir)) {
|
|
4152
4558
|
if (rel === "SKILL.md" || rel === MANIFEST || seen.has(`file:${rel}`)) continue;
|
|
4153
|
-
const abs =
|
|
4559
|
+
const abs = path8.join(dir, rel);
|
|
4154
4560
|
const content = readText(abs);
|
|
4155
4561
|
if (content === null || Buffer.byteLength(content) > MAX_FILE) continue;
|
|
4156
4562
|
byRel.set(`file:${rel}`, { target: { abs, rel, role: "file", sha: "", ...isExecutable(abs) ? { mode: "x" } : {} }, content });
|
|
@@ -4189,18 +4595,18 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4189
4595
|
for (const root of roots) {
|
|
4190
4596
|
let names = [];
|
|
4191
4597
|
try {
|
|
4192
|
-
names =
|
|
4598
|
+
names = fs9.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4193
4599
|
} catch {
|
|
4194
4600
|
continue;
|
|
4195
4601
|
}
|
|
4196
4602
|
for (const name of names) {
|
|
4197
|
-
const dir =
|
|
4603
|
+
const dir = path8.join(root, name);
|
|
4198
4604
|
if (owned.has(dir) || incoming.has(name) || only && name !== only) continue;
|
|
4199
|
-
const skillMd = readText(
|
|
4605
|
+
const skillMd = readText(path8.join(dir, "SKILL.md"));
|
|
4200
4606
|
if (skillMd === null) continue;
|
|
4201
4607
|
let born = 0;
|
|
4202
4608
|
try {
|
|
4203
|
-
const st =
|
|
4609
|
+
const st = fs9.statSync(path8.join(dir, "SKILL.md"));
|
|
4204
4610
|
born = Math.min(st.birthtimeMs || st.mtimeMs, st.mtimeMs);
|
|
4205
4611
|
} catch {
|
|
4206
4612
|
continue;
|
|
@@ -4209,8 +4615,8 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
4209
4615
|
const files = {};
|
|
4210
4616
|
for (const rel of listFiles(dir).filter((r) => !r.split("/").some((seg) => seg.startsWith(".")))) {
|
|
4211
4617
|
if (rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
4212
|
-
const content = readText(
|
|
4213
|
-
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(
|
|
4618
|
+
const content = readText(path8.join(dir, rel));
|
|
4619
|
+
if (content !== null && Buffer.byteLength(content) <= MAX_FILE) files[rel] = { content, ...isExecutable(path8.join(dir, rel)) ? { mode: "x" } : {} };
|
|
4214
4620
|
}
|
|
4215
4621
|
const hash = sha(JSON.stringify([skillMd, files]));
|
|
4216
4622
|
if (state.created[name]?.hash === hash) continue;
|
|
@@ -4258,14 +4664,14 @@ function captureEdits(v2, config, ctx, opts = {}) {
|
|
|
4258
4664
|
}
|
|
4259
4665
|
function lockDeploy(p) {
|
|
4260
4666
|
ensureDir(p.state);
|
|
4261
|
-
const file =
|
|
4667
|
+
const file = path8.join(p.state, ".lock");
|
|
4262
4668
|
for (let i = 0; i < 2; i++) {
|
|
4263
4669
|
try {
|
|
4264
|
-
|
|
4670
|
+
fs9.writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
4265
4671
|
return () => removeFile(file);
|
|
4266
4672
|
} catch {
|
|
4267
4673
|
try {
|
|
4268
|
-
const age = Date.now() -
|
|
4674
|
+
const age = Date.now() - fs9.statSync(file).mtimeMs;
|
|
4269
4675
|
const pid = Number(readText(file));
|
|
4270
4676
|
let alive = age < 12e4;
|
|
4271
4677
|
if (alive && pid > 0) {
|
|
@@ -4332,7 +4738,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4332
4738
|
const foreign = /* @__PURE__ */ new Set();
|
|
4333
4739
|
if (d.kind === "skill" && !prev) {
|
|
4334
4740
|
for (const dir of d.dirs ?? []) {
|
|
4335
|
-
const cur = readText(
|
|
4741
|
+
const cur = readText(path8.join(dir, "SKILL.md"));
|
|
4336
4742
|
if (cur === null || cur === d.section.body || state.created[d.name]) continue;
|
|
4337
4743
|
foreign.add(dir);
|
|
4338
4744
|
report.warnings.push(`${dir} is not managed by Bunshin; the library's ${d.name} skill was not installed there`);
|
|
@@ -4344,7 +4750,7 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4344
4750
|
targets.push({ ...t, sha: sha(neutralizeMarkers(d.section.body).replace(/\n*$/, "\n")) });
|
|
4345
4751
|
continue;
|
|
4346
4752
|
}
|
|
4347
|
-
if ([...foreign].some((dir) => t.abs.startsWith(dir +
|
|
4753
|
+
if ([...foreign].some((dir) => t.abs.startsWith(dir + path8.sep))) continue;
|
|
4348
4754
|
if (!(d.dirs ?? (d.root ? [d.root] : [])).some((r) => insideRoot(r, t.abs))) {
|
|
4349
4755
|
report.warnings.push(`${t.abs} not written: it leads outside its folder`);
|
|
4350
4756
|
continue;
|
|
@@ -4353,10 +4759,10 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4353
4759
|
const existing = readText(t.abs);
|
|
4354
4760
|
try {
|
|
4355
4761
|
if (existing !== content) {
|
|
4356
|
-
ensureDir(
|
|
4762
|
+
ensureDir(path8.dirname(t.abs));
|
|
4357
4763
|
writeFile(t.abs, content, t.mode);
|
|
4358
4764
|
report.written++;
|
|
4359
|
-
} else if (t.mode === "x" && !isExecutable(t.abs))
|
|
4765
|
+
} else if (t.mode === "x" && !isExecutable(t.abs)) fs9.chmodSync(t.abs, 493);
|
|
4360
4766
|
} catch (err) {
|
|
4361
4767
|
report.warnings.push(`${t.abs} not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
4362
4768
|
continue;
|
|
@@ -4369,19 +4775,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4369
4775
|
const cur = readText(old.abs);
|
|
4370
4776
|
if (cur !== null && sha(cur) === old.sha) {
|
|
4371
4777
|
removeFile(old.abs);
|
|
4372
|
-
pruneDirs(
|
|
4778
|
+
pruneDirs(path8.dirname(old.abs), d.kind === "skill" ? path8.dirname(path8.dirname(old.abs)) : p.library);
|
|
4373
4779
|
report.removed++;
|
|
4374
4780
|
}
|
|
4375
4781
|
}
|
|
4376
4782
|
if (d.kind === "skill") {
|
|
4377
4783
|
for (const dir of prev?.dirs ?? []) {
|
|
4378
4784
|
if ((d.dirs ?? []).includes(dir) || !removable(dir)) continue;
|
|
4379
|
-
removeFile(
|
|
4380
|
-
pruneDirs(dir,
|
|
4785
|
+
removeFile(path8.join(dir, MANIFEST));
|
|
4786
|
+
pruneDirs(dir, path8.dirname(dir));
|
|
4381
4787
|
}
|
|
4382
4788
|
for (const dir of d.dirs ?? []) {
|
|
4383
4789
|
const manifest = safeJoin(dir, MANIFEST);
|
|
4384
|
-
if (foreign.has(dir) || !manifest || !
|
|
4790
|
+
if (foreign.has(dir) || !manifest || !fs9.existsSync(path8.join(dir, "SKILL.md"))) continue;
|
|
4385
4791
|
writeJsonAtomic(manifest, { sectionId: id, name: d.name, version: d.section.version, libraryVersion: bundle.libraryVersion ?? null, managedBy: "bunshin sync" });
|
|
4386
4792
|
}
|
|
4387
4793
|
report.skills++;
|
|
@@ -4406,12 +4812,12 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4406
4812
|
}
|
|
4407
4813
|
for (const dir of doc.dirs ?? []) {
|
|
4408
4814
|
if (!removable(dir)) continue;
|
|
4409
|
-
removeFile(
|
|
4410
|
-
pruneDirs(dir,
|
|
4411
|
-
if (
|
|
4815
|
+
removeFile(path8.join(dir, MANIFEST));
|
|
4816
|
+
pruneDirs(dir, path8.dirname(dir));
|
|
4817
|
+
if (fs9.existsSync(dir)) {
|
|
4412
4818
|
const left = listFiles(dir);
|
|
4413
4819
|
if (left.length) report.warnings.push(`${dir} kept: it has files Bunshin did not write`);
|
|
4414
|
-
else pruneDirs(dir,
|
|
4820
|
+
else pruneDirs(dir, path8.dirname(dir));
|
|
4415
4821
|
}
|
|
4416
4822
|
}
|
|
4417
4823
|
}
|
|
@@ -4427,17 +4833,17 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4427
4833
|
if (d.kind === "skill") continue;
|
|
4428
4834
|
for (const t of d.targets) {
|
|
4429
4835
|
if (t.role === "block" || t.mode !== "x" || !/(^|\/)bin\/[^/]+$/.test(t.rel) || !owned(t.abs)) continue;
|
|
4430
|
-
const link =
|
|
4836
|
+
const link = path8.join(p.bin, path8.basename(t.abs));
|
|
4431
4837
|
ensureDir(p.bin);
|
|
4432
4838
|
try {
|
|
4433
|
-
const cur =
|
|
4839
|
+
const cur = fs9.readlinkSync(link);
|
|
4434
4840
|
if (cur !== t.abs) {
|
|
4435
|
-
|
|
4436
|
-
|
|
4841
|
+
fs9.unlinkSync(link);
|
|
4842
|
+
fs9.symlinkSync(t.abs, link);
|
|
4437
4843
|
}
|
|
4438
4844
|
} catch {
|
|
4439
4845
|
try {
|
|
4440
|
-
if (!
|
|
4846
|
+
if (!fs9.existsSync(link)) fs9.symlinkSync(t.abs, link);
|
|
4441
4847
|
} catch {
|
|
4442
4848
|
}
|
|
4443
4849
|
}
|
|
@@ -4448,19 +4854,19 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4448
4854
|
if (harnesses.has("claude-code")) {
|
|
4449
4855
|
for (const pack of packs) {
|
|
4450
4856
|
const dir = childDir(p.library, pack);
|
|
4451
|
-
if (!dir || !insideRoot(dir,
|
|
4452
|
-
const link =
|
|
4857
|
+
if (!dir || !insideRoot(dir, path8.join(dir, ".claude", "x"))) continue;
|
|
4858
|
+
const link = path8.join(dir, ".claude", "skills");
|
|
4453
4859
|
try {
|
|
4454
|
-
|
|
4455
|
-
if (
|
|
4456
|
-
|
|
4457
|
-
|
|
4860
|
+
fs9.lstatSync(link);
|
|
4861
|
+
if (fs9.readlinkSync(link) !== p.claudeSkills) {
|
|
4862
|
+
fs9.unlinkSync(link);
|
|
4863
|
+
fs9.symlinkSync(p.claudeSkills, link);
|
|
4458
4864
|
}
|
|
4459
4865
|
} catch {
|
|
4460
|
-
ensureDir(
|
|
4866
|
+
ensureDir(path8.dirname(link));
|
|
4461
4867
|
ensureDir(p.claudeSkills);
|
|
4462
4868
|
try {
|
|
4463
|
-
|
|
4869
|
+
fs9.symlinkSync(p.claudeSkills, link);
|
|
4464
4870
|
} catch {
|
|
4465
4871
|
}
|
|
4466
4872
|
}
|
|
@@ -4469,17 +4875,26 @@ function deployLocked(v2, config, ctx, bundle, opts, p, harnesses, now) {
|
|
|
4469
4875
|
for (const old of state.links) {
|
|
4470
4876
|
if (links.has(old)) continue;
|
|
4471
4877
|
try {
|
|
4472
|
-
const target =
|
|
4473
|
-
if (!
|
|
4878
|
+
const target = fs9.readlinkSync(old);
|
|
4879
|
+
if (!fs9.existsSync(target)) fs9.unlinkSync(old);
|
|
4474
4880
|
} catch {
|
|
4475
4881
|
}
|
|
4476
4882
|
}
|
|
4477
4883
|
let linked = state.linked ?? [];
|
|
4478
4884
|
if (p.legacy) {
|
|
4479
|
-
const
|
|
4480
|
-
const
|
|
4885
|
+
const deployed = Object.values(next).filter((d) => d.kind === "skill" && (d.dirs ?? []).length);
|
|
4886
|
+
const layerOf = new Map(plan.skills.map((x) => [x.skill.name, x.layer]));
|
|
4887
|
+
const roots = [.../* @__PURE__ */ new Set([...linked, ...opts.linkRoot ? [path8.resolve(opts.linkRoot)] : []])];
|
|
4481
4888
|
linked = [];
|
|
4482
4889
|
for (const root of roots) {
|
|
4890
|
+
let source;
|
|
4891
|
+
try {
|
|
4892
|
+
source = sourceFor(root, v2.store.root, ctx);
|
|
4893
|
+
} catch {
|
|
4894
|
+
source = void 0;
|
|
4895
|
+
}
|
|
4896
|
+
const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle.scopes ? { tree: bundle.scopes } : {} });
|
|
4897
|
+
const names = deployed.filter((d) => inBoundary({ layer: layerOf.get(d.name) ?? "org" }, reader)).map((d) => d.name);
|
|
4483
4898
|
const res = linkSkills(p, root, opts.linkHarnesses ?? [...harnesses], names);
|
|
4484
4899
|
report.warnings.push(...res.warnings);
|
|
4485
4900
|
if (res.kept) linked.push(root);
|
|
@@ -4500,13 +4915,13 @@ function materialize(sections, dir) {
|
|
|
4500
4915
|
const put = (rel, content, mode) => {
|
|
4501
4916
|
const abs = safeJoin(dir, rel);
|
|
4502
4917
|
if (!abs) return;
|
|
4503
|
-
ensureDir(
|
|
4918
|
+
ensureDir(path8.dirname(abs));
|
|
4504
4919
|
writeFile(abs, content, mode);
|
|
4505
4920
|
files++;
|
|
4506
4921
|
};
|
|
4507
4922
|
for (const s of sections.filter((x) => x.status === "active" || x.status === "stale")) {
|
|
4508
4923
|
if (s.skill) {
|
|
4509
|
-
const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ?
|
|
4924
|
+
const base = s.packPath && /(^|\/)SKILL\.md$/.test(s.packPath) ? path8.posix.dirname(s.packPath) : `.claude/skills/${s.skill.name}`;
|
|
4510
4925
|
put(`${base}/SKILL.md`, s.body);
|
|
4511
4926
|
for (const [rel, f] of Object.entries(s.skill.files ?? {})) put(`${base}/${rel}`, f.content, f.mode);
|
|
4512
4927
|
continue;
|
|
@@ -4518,51 +4933,51 @@ function materialize(sections, dir) {
|
|
|
4518
4933
|
}
|
|
4519
4934
|
function linkSkills(p, root, harnesses, names) {
|
|
4520
4935
|
const out = { linked: 0, removed: 0, kept: false, warnings: [] };
|
|
4521
|
-
const abs =
|
|
4522
|
-
if (abs ===
|
|
4523
|
-
const managed = (target) => target.startsWith(
|
|
4936
|
+
const abs = path8.resolve(root);
|
|
4937
|
+
if (abs === path8.resolve(p.home) || abs === path8.resolve(os2.homedir()) || !fs9.existsSync(abs)) return out;
|
|
4938
|
+
const managed = (target) => target.startsWith(path8.resolve(p.state) + path8.sep);
|
|
4524
4939
|
const excluded = [];
|
|
4525
4940
|
for (const h of harnesses) {
|
|
4526
4941
|
const spec = LINK_DIRS[h];
|
|
4527
4942
|
if (!spec) continue;
|
|
4528
|
-
const dir =
|
|
4529
|
-
const canonical =
|
|
4530
|
-
const want = new Set(names.filter((n) => SAFE_NAME.test(n) &&
|
|
4943
|
+
const dir = path8.join(abs, spec.rel);
|
|
4944
|
+
const canonical = path8.resolve(p[spec.key]);
|
|
4945
|
+
const want = new Set(names.filter((n) => SAFE_NAME.test(n) && fs9.existsSync(path8.join(canonical, n, "SKILL.md"))));
|
|
4531
4946
|
let entries = [];
|
|
4532
4947
|
try {
|
|
4533
|
-
entries =
|
|
4948
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
4534
4949
|
} catch {
|
|
4535
4950
|
}
|
|
4536
4951
|
for (const e of entries) {
|
|
4537
4952
|
if (!e.isSymbolicLink()) continue;
|
|
4538
|
-
const link =
|
|
4953
|
+
const link = path8.join(dir, e.name);
|
|
4539
4954
|
let target = "";
|
|
4540
4955
|
try {
|
|
4541
|
-
target =
|
|
4956
|
+
target = path8.resolve(dir, fs9.readlinkSync(link));
|
|
4542
4957
|
} catch {
|
|
4543
4958
|
continue;
|
|
4544
4959
|
}
|
|
4545
4960
|
if (!managed(target)) continue;
|
|
4546
|
-
if (want.has(e.name) && target ===
|
|
4961
|
+
if (want.has(e.name) && target === path8.join(canonical, e.name)) {
|
|
4547
4962
|
want.delete(e.name);
|
|
4548
|
-
excluded.push(
|
|
4963
|
+
excluded.push(path8.join(spec.rel, e.name));
|
|
4549
4964
|
continue;
|
|
4550
4965
|
}
|
|
4551
4966
|
removeFile(link);
|
|
4552
4967
|
out.removed++;
|
|
4553
4968
|
}
|
|
4554
4969
|
for (const name of want) {
|
|
4555
|
-
const link =
|
|
4970
|
+
const link = path8.join(dir, name);
|
|
4556
4971
|
try {
|
|
4557
|
-
|
|
4972
|
+
fs9.lstatSync(link);
|
|
4558
4973
|
out.warnings.push(`${link} is the project's own; the library's ${name} skill was not linked there`);
|
|
4559
4974
|
continue;
|
|
4560
4975
|
} catch {
|
|
4561
4976
|
}
|
|
4562
4977
|
try {
|
|
4563
4978
|
ensureDir(dir);
|
|
4564
|
-
|
|
4565
|
-
excluded.push(
|
|
4979
|
+
fs9.symlinkSync(path8.join(canonical, name), link, "dir");
|
|
4980
|
+
excluded.push(path8.join(spec.rel, name));
|
|
4566
4981
|
out.linked++;
|
|
4567
4982
|
} catch (err) {
|
|
4568
4983
|
out.warnings.push(`${link} not linked: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4575,10 +4990,10 @@ function linkSkills(p, root, harnesses, names) {
|
|
|
4575
4990
|
return out;
|
|
4576
4991
|
}
|
|
4577
4992
|
function gitExclude(root, rels) {
|
|
4578
|
-
if (!
|
|
4993
|
+
if (!fs9.existsSync(path8.join(root, ".git"))) return;
|
|
4579
4994
|
let file;
|
|
4580
4995
|
try {
|
|
4581
|
-
file =
|
|
4996
|
+
file = path8.resolve(root, execFileSync3("git", ["-C", root, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).trim());
|
|
4582
4997
|
} catch {
|
|
4583
4998
|
return;
|
|
4584
4999
|
}
|
|
@@ -4587,14 +5002,14 @@ function gitExclude(root, rels) {
|
|
|
4587
5002
|
const end = start >= 0 ? cur.indexOf(EXCLUDE_END, start) : -1;
|
|
4588
5003
|
const rest = start >= 0 && end >= 0 ? cur.slice(0, start) + cur.slice(end + EXCLUDE_END.length).replace(/^\n/, "") : cur;
|
|
4589
5004
|
const block2 = rels.length ? `${EXCLUDE_BEGIN}
|
|
4590
|
-
${[...new Set(rels)].sort().map((r) => `/${r.split(
|
|
5005
|
+
${[...new Set(rels)].sort().map((r) => `/${r.split(path8.sep).join("/")}`).join("\n")}
|
|
4591
5006
|
${EXCLUDE_END}
|
|
4592
5007
|
` : "";
|
|
4593
5008
|
const next = block2 ? `${rest}${rest && !rest.endsWith("\n") ? "\n" : ""}${block2}` : rest;
|
|
4594
5009
|
if (next === cur) return;
|
|
4595
5010
|
try {
|
|
4596
|
-
ensureDir(
|
|
4597
|
-
|
|
5011
|
+
ensureDir(path8.dirname(file));
|
|
5012
|
+
fs9.writeFileSync(file, next);
|
|
4598
5013
|
} catch {
|
|
4599
5014
|
}
|
|
4600
5015
|
}
|
|
@@ -4603,9 +5018,10 @@ var init_deploy = __esm({
|
|
|
4603
5018
|
"src/v2/deploy.ts"() {
|
|
4604
5019
|
"use strict";
|
|
4605
5020
|
init_v22();
|
|
5021
|
+
init_source();
|
|
4606
5022
|
init_util();
|
|
4607
5023
|
init_store2();
|
|
4608
|
-
settingsFile = (home) =>
|
|
5024
|
+
settingsFile = (home) => path8.join(home, ".bunshin", "deploy", "settings.json");
|
|
4609
5025
|
orgKey = (orgId) => orgId.replace(/[^A-Za-z0-9._-]/g, "_") || "org";
|
|
4610
5026
|
sha = (text2) => createHash3("sha256").update(text2).digest("hex");
|
|
4611
5027
|
BLOCK_BEGIN = "<!-- bunshin:begin -->";
|
|
@@ -4614,7 +5030,7 @@ var init_deploy = __esm({
|
|
|
4614
5030
|
MAX_FILE = 2 * 1024 * 1024;
|
|
4615
5031
|
isExecutable = (file) => {
|
|
4616
5032
|
try {
|
|
4617
|
-
return (
|
|
5033
|
+
return (fs9.statSync(file).mode & 73) !== 0;
|
|
4618
5034
|
} catch {
|
|
4619
5035
|
return false;
|
|
4620
5036
|
}
|
|
@@ -4622,12 +5038,12 @@ var init_deploy = __esm({
|
|
|
4622
5038
|
safeJoin = containedPath;
|
|
4623
5039
|
SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4624
5040
|
neutralizeMarkers = (text2) => text2.replace(/<!--(?=\s*bunshin:)/gi, "<!-\u200B-");
|
|
4625
|
-
LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
5041
|
+
LAYER_RANK = { org: 0, dept: 1, group: 2, team: 2, source: 3, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
4626
5042
|
docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
|
|
4627
5043
|
BORN_SLACK_MS = 2e3;
|
|
4628
5044
|
LINK_DIRS = {
|
|
4629
|
-
"claude-code": { rel:
|
|
4630
|
-
codex: { rel:
|
|
5045
|
+
"claude-code": { rel: path8.join(".claude", "skills"), key: "claudeSkills" },
|
|
5046
|
+
codex: { rel: path8.join(".codex", "skills"), key: "codexSkills" }
|
|
4631
5047
|
};
|
|
4632
5048
|
EXCLUDE_BEGIN = "# >>> bunshin: library skills linked here (managed by bunshin sync)";
|
|
4633
5049
|
EXCLUDE_END = "# <<< bunshin";
|
|
@@ -4739,11 +5155,11 @@ var init_live = __esm({
|
|
|
4739
5155
|
});
|
|
4740
5156
|
|
|
4741
5157
|
// src/transcript.ts
|
|
4742
|
-
import
|
|
5158
|
+
import fs10 from "node:fs";
|
|
4743
5159
|
function parseTranscript(file) {
|
|
4744
5160
|
let raw = "";
|
|
4745
5161
|
try {
|
|
4746
|
-
raw =
|
|
5162
|
+
raw = fs10.readFileSync(file, "utf8");
|
|
4747
5163
|
} catch {
|
|
4748
5164
|
return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
|
|
4749
5165
|
}
|
|
@@ -4836,290 +5252,126 @@ function blockText(content) {
|
|
|
4836
5252
|
if (Array.isArray(content)) {
|
|
4837
5253
|
return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
|
|
4838
5254
|
}
|
|
4839
|
-
return content == null ? "" : JSON.stringify(content);
|
|
4840
|
-
}
|
|
4841
|
-
function codexToolResults(output, depth = 0) {
|
|
4842
|
-
const raw = blockText(output);
|
|
4843
|
-
let envelope = output;
|
|
4844
|
-
if (typeof output === "string") {
|
|
4845
|
-
try {
|
|
4846
|
-
envelope = JSON.parse(output);
|
|
4847
|
-
} catch {
|
|
4848
|
-
}
|
|
4849
|
-
}
|
|
4850
|
-
if (depth > 6) return [{ text: raw, pending: true }];
|
|
4851
|
-
if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
|
|
4852
|
-
if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
|
|
4853
|
-
const result = envelope;
|
|
4854
|
-
if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
|
|
4855
|
-
const pending = !Number.isInteger(result.exit_code);
|
|
4856
|
-
return [{
|
|
4857
|
-
text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
|
|
4858
|
-
${result.output}`,
|
|
4859
|
-
isError: !pending && result.exit_code !== 0,
|
|
4860
|
-
pending
|
|
4861
|
-
}];
|
|
4862
|
-
}
|
|
4863
|
-
if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
|
|
4864
|
-
if (result.status === "rejected") return [{ text: raw, isError: true }];
|
|
4865
|
-
}
|
|
4866
|
-
return [{
|
|
4867
|
-
text: raw,
|
|
4868
|
-
isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
|
|
4869
|
-
pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
|
|
4870
|
-
}];
|
|
4871
|
-
}
|
|
4872
|
-
function summarizeInput(input) {
|
|
4873
|
-
if (input == null) return "";
|
|
4874
|
-
if (typeof input !== "object") return String(input);
|
|
4875
|
-
const obj = input;
|
|
4876
|
-
for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
|
|
4877
|
-
if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
|
|
4878
|
-
}
|
|
4879
|
-
return JSON.stringify(obj);
|
|
4880
|
-
}
|
|
4881
|
-
function bounded(text2, maxChars) {
|
|
4882
|
-
if (text2.length <= maxChars) return text2;
|
|
4883
|
-
const marker = "\n[... middle omitted ...]\n";
|
|
4884
|
-
if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
|
|
4885
|
-
const head = Math.floor((maxChars - marker.length) / 2);
|
|
4886
|
-
return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
|
|
4887
|
-
}
|
|
4888
|
-
function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
|
|
4889
|
-
const parts = [];
|
|
4890
|
-
for (const t of facts.turns.slice(from)) {
|
|
4891
|
-
const text2 = redact2(t.text);
|
|
4892
|
-
if (t.role === "user") parts.push(`[USER]
|
|
4893
|
-
${bounded(text2, maxChars)}`);
|
|
4894
|
-
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
4895
|
-
${bounded(text2, maxChars)}`);
|
|
4896
|
-
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
4897
|
-
${bounded(text2, toolResultChars)}`);
|
|
4898
|
-
else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
|
|
4899
|
-
}
|
|
4900
|
-
return bounded(parts.join("\n\n"), maxChars);
|
|
4901
|
-
}
|
|
4902
|
-
function fullTrace(facts, { from = 0, to } = {}) {
|
|
4903
|
-
return facts.turns.slice(from, to).map((t) => {
|
|
4904
|
-
if (t.role === "user") return `[USER]
|
|
4905
|
-
${redact2(t.text)}`;
|
|
4906
|
-
if (t.role === "assistant") return `[ASSISTANT]
|
|
4907
|
-
${redact2(t.text)}`;
|
|
4908
|
-
if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
4909
|
-
${redact2(t.text)}`;
|
|
4910
|
-
const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
|
|
4911
|
-
return `[TOOL ${t.tool}]
|
|
4912
|
-
${redact2(input)}`;
|
|
4913
|
-
});
|
|
4914
|
-
}
|
|
4915
|
-
function traceChunks(turns, maxBytes = 1e6) {
|
|
4916
|
-
const out = [];
|
|
4917
|
-
let cur = "";
|
|
4918
|
-
let size = 0;
|
|
4919
|
-
turns.forEach((turn, i) => {
|
|
4920
|
-
let piece = (i ? "\n\n" : "") + turn;
|
|
4921
|
-
let bytes = jsonBytes(piece);
|
|
4922
|
-
if (size + bytes <= maxBytes) {
|
|
4923
|
-
cur += piece;
|
|
4924
|
-
size += bytes;
|
|
4925
|
-
return;
|
|
4926
|
-
}
|
|
4927
|
-
if (cur) out.push(cur);
|
|
4928
|
-
while (bytes > maxBytes) {
|
|
4929
|
-
let n = Math.min(piece.length, maxBytes);
|
|
4930
|
-
for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
|
|
4931
|
-
const code2 = piece.charCodeAt(n - 1);
|
|
4932
|
-
if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
|
|
4933
|
-
const head = piece.slice(0, n);
|
|
4934
|
-
out.push(head);
|
|
4935
|
-
piece = piece.slice(n);
|
|
4936
|
-
bytes -= jsonBytes(head);
|
|
4937
|
-
}
|
|
4938
|
-
cur = piece;
|
|
4939
|
-
size = bytes;
|
|
4940
|
-
});
|
|
4941
|
-
if (cur) out.push(cur);
|
|
4942
|
-
return out;
|
|
4943
|
-
}
|
|
4944
|
-
function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
|
|
4945
|
-
const facts = parseTranscript(file);
|
|
4946
|
-
const visible = facts.turns.filter((t) => t.role !== "tool");
|
|
4947
|
-
const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
4948
|
-
const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
|
|
4949
|
-
return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
|
|
4950
|
-
}
|
|
4951
|
-
var BANNER, jsonBytes;
|
|
4952
|
-
var init_transcript = __esm({
|
|
4953
|
-
"src/transcript.ts"() {
|
|
4954
|
-
"use strict";
|
|
4955
|
-
init_redact2();
|
|
4956
|
-
init_util();
|
|
4957
|
-
BANNER = "## Recalled lessons (Bunshin)";
|
|
4958
|
-
jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
|
|
4959
|
-
}
|
|
4960
|
-
});
|
|
4961
|
-
|
|
4962
|
-
// src/v2/repo.ts
|
|
4963
|
-
import fs9 from "node:fs";
|
|
4964
|
-
import path7 from "node:path";
|
|
4965
|
-
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4966
|
-
function git(cwd, args, timeout = 1500) {
|
|
4967
|
-
try {
|
|
4968
|
-
return execFileSync2("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
4969
|
-
} catch {
|
|
4970
|
-
return null;
|
|
4971
|
-
}
|
|
4972
|
-
}
|
|
4973
|
-
function repoRoot(cwd, storeRoot) {
|
|
4974
|
-
const key = `${cwd}
|
|
4975
|
-
${storeRoot ?? ""}`;
|
|
4976
|
-
const hit = rootCache.get(key);
|
|
4977
|
-
if (hit) return hit;
|
|
4978
|
-
const top = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
4979
|
-
let root = top || "";
|
|
4980
|
-
if (!root && storeRoot && path7.basename(storeRoot) === ".bunshin") {
|
|
4981
|
-
const parent = path7.dirname(storeRoot);
|
|
4982
|
-
if (path7.resolve(cwd).startsWith(path7.resolve(parent))) root = parent;
|
|
4983
|
-
}
|
|
4984
|
-
root = root || path7.resolve(cwd);
|
|
4985
|
-
rootCache.set(key, root);
|
|
4986
|
-
return root;
|
|
4987
|
-
}
|
|
4988
|
-
function parseRemote(url) {
|
|
4989
|
-
const clean2 = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
4990
|
-
if (!clean2) return null;
|
|
4991
|
-
const scp = clean2.match(/^[^@/\s]+@[^:/\s]+:(.+)$/);
|
|
4992
|
-
const pathPart = scp ? scp[1] : (() => {
|
|
4993
|
-
try {
|
|
4994
|
-
return new URL(clean2).pathname;
|
|
4995
|
-
} catch {
|
|
4996
|
-
return clean2;
|
|
4997
|
-
}
|
|
4998
|
-
})();
|
|
4999
|
-
const segs = pathPart.split("/").filter(Boolean);
|
|
5000
|
-
if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`;
|
|
5001
|
-
return segs[0] ?? null;
|
|
5002
|
-
}
|
|
5003
|
-
function repoName(root) {
|
|
5004
|
-
const url = git(root, ["remote", "get-url", "origin"]);
|
|
5005
|
-
return url && parseRemote(url) || path7.basename(root);
|
|
5006
|
-
}
|
|
5007
|
-
function repoCommit(root) {
|
|
5008
|
-
return git(root, ["rev-parse", "HEAD"])?.trim() || void 0;
|
|
5009
|
-
}
|
|
5010
|
-
function listDocs(root) {
|
|
5011
|
-
const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
|
|
5012
|
-
if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs9.existsSync(path7.join(root, rel)) && !deployedSkill(root, rel)).sort();
|
|
5013
|
-
const out = [];
|
|
5014
|
-
let visited = 0;
|
|
5015
|
-
const walk = (dir, rel, depth) => {
|
|
5016
|
-
if (depth > 8 || visited++ > 2e4) return;
|
|
5017
|
-
let entries;
|
|
5018
|
-
try {
|
|
5019
|
-
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
5020
|
-
} catch {
|
|
5021
|
-
return;
|
|
5022
|
-
}
|
|
5023
|
-
for (const e of entries) {
|
|
5024
|
-
if (e.isDirectory()) {
|
|
5025
|
-
if (!SKIP_DIRS.has(e.name) && !fs9.existsSync(path7.join(dir, e.name, ".git"))) walk(path7.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
|
|
5026
|
-
} else if (e.isFile()) {
|
|
5027
|
-
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
5028
|
-
if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
|
|
5029
|
-
}
|
|
5030
|
-
}
|
|
5031
|
-
};
|
|
5032
|
-
walk(root, "", 0);
|
|
5033
|
-
return out.sort();
|
|
5034
|
-
}
|
|
5035
|
-
function indexWorkingTree(v2, config, cwd, opts = {}) {
|
|
5036
|
-
const root = repoRoot(cwd, v2.store.root);
|
|
5037
|
-
const now = opts.now ?? Date.now();
|
|
5038
|
-
const cached = v2.readRepoIndex(root);
|
|
5039
|
-
const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
|
|
5040
|
-
const sameRepo = cached && cached.ownerTeam === ownerTeam;
|
|
5041
|
-
const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
|
|
5042
|
-
const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
|
|
5043
|
-
const paths = fresh ? cached.paths : listDocs(root);
|
|
5044
|
-
let changed = !fresh || !sameRepo || cached.repo !== repo;
|
|
5045
|
-
const files = {};
|
|
5046
|
-
for (const rel of paths) {
|
|
5047
|
-
const abs = path7.join(root, rel);
|
|
5048
|
-
let stat;
|
|
5049
|
-
try {
|
|
5050
|
-
stat = fs9.statSync(abs);
|
|
5051
|
-
} catch {
|
|
5052
|
-
changed = true;
|
|
5053
|
-
continue;
|
|
5054
|
-
}
|
|
5055
|
-
const prev = sameRepo && cached.repo === repo ? cached.files[rel] : void 0;
|
|
5056
|
-
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) {
|
|
5057
|
-
files[rel] = prev;
|
|
5058
|
-
continue;
|
|
5059
|
-
}
|
|
5060
|
-
changed = true;
|
|
5061
|
-
try {
|
|
5062
|
-
const text2 = fs9.readFileSync(abs, "utf8");
|
|
5063
|
-
files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam }) };
|
|
5064
|
-
} catch {
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
const index = {
|
|
5068
|
-
repo,
|
|
5069
|
-
ownerTeam,
|
|
5070
|
-
listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
|
|
5071
|
-
paths,
|
|
5072
|
-
files
|
|
5073
|
-
};
|
|
5074
|
-
if (changed) {
|
|
5075
|
-
try {
|
|
5076
|
-
v2.writeRepoIndex(root, index);
|
|
5077
|
-
} catch {
|
|
5078
|
-
}
|
|
5079
|
-
}
|
|
5080
|
-
return { root, index, sections: Object.values(files).flatMap((f) => f.sections) };
|
|
5255
|
+
return content == null ? "" : JSON.stringify(content);
|
|
5081
5256
|
}
|
|
5082
|
-
function
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
let realRoot = root;
|
|
5087
|
-
try {
|
|
5088
|
-
realRoot = fs9.realpathSync(root);
|
|
5089
|
-
} catch {
|
|
5090
|
-
}
|
|
5091
|
-
try {
|
|
5092
|
-
real = fs9.realpathSync(abs);
|
|
5093
|
-
} catch {
|
|
5257
|
+
function codexToolResults(output, depth = 0) {
|
|
5258
|
+
const raw = blockText(output);
|
|
5259
|
+
let envelope = output;
|
|
5260
|
+
if (typeof output === "string") {
|
|
5094
5261
|
try {
|
|
5095
|
-
|
|
5262
|
+
envelope = JSON.parse(output);
|
|
5096
5263
|
} catch {
|
|
5097
5264
|
}
|
|
5098
5265
|
}
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5266
|
+
if (depth > 6) return [{ text: raw, pending: true }];
|
|
5267
|
+
if (Array.isArray(envelope)) return envelope.flatMap((part) => codexToolResults(part, depth + 1));
|
|
5268
|
+
if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
|
|
5269
|
+
const result = envelope;
|
|
5270
|
+
if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
|
|
5271
|
+
const pending = !Number.isInteger(result.exit_code);
|
|
5272
|
+
return [{
|
|
5273
|
+
text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
|
|
5274
|
+
${result.output}`,
|
|
5275
|
+
isError: !pending && result.exit_code !== 0,
|
|
5276
|
+
pending
|
|
5277
|
+
}];
|
|
5278
|
+
}
|
|
5279
|
+
if (result.status === "fulfilled" && "value" in result) return codexToolResults(result.value, depth + 1);
|
|
5280
|
+
if (result.status === "rejected") return [{ text: raw, isError: true }];
|
|
5105
5281
|
}
|
|
5106
|
-
return
|
|
5282
|
+
return [{
|
|
5283
|
+
text: raw,
|
|
5284
|
+
isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
|
|
5285
|
+
pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
|
|
5286
|
+
}];
|
|
5107
5287
|
}
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
"
|
|
5288
|
+
function summarizeInput(input) {
|
|
5289
|
+
if (input == null) return "";
|
|
5290
|
+
if (typeof input !== "object") return String(input);
|
|
5291
|
+
const obj = input;
|
|
5292
|
+
for (const key of ["command", "prompt", "pattern", "file_path", "query", "description"]) {
|
|
5293
|
+
if (typeof obj[key] === "string") return `${key}: ${obj[key]}`;
|
|
5294
|
+
}
|
|
5295
|
+
return JSON.stringify(obj);
|
|
5296
|
+
}
|
|
5297
|
+
function bounded(text2, maxChars) {
|
|
5298
|
+
if (text2.length <= maxChars) return text2;
|
|
5299
|
+
const marker = "\n[... middle omitted ...]\n";
|
|
5300
|
+
if (maxChars <= marker.length) return text2.slice(0, Math.max(0, maxChars));
|
|
5301
|
+
const head = Math.floor((maxChars - marker.length) / 2);
|
|
5302
|
+
return text2.slice(0, head) + marker + text2.slice(-(maxChars - marker.length - head));
|
|
5303
|
+
}
|
|
5304
|
+
function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
|
|
5305
|
+
const parts = [];
|
|
5306
|
+
for (const t of facts.turns.slice(from)) {
|
|
5307
|
+
const text2 = redact2(t.text);
|
|
5308
|
+
if (t.role === "user") parts.push(`[USER]
|
|
5309
|
+
${bounded(text2, maxChars)}`);
|
|
5310
|
+
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
5311
|
+
${bounded(text2, maxChars)}`);
|
|
5312
|
+
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
5313
|
+
${bounded(text2, toolResultChars)}`);
|
|
5314
|
+
else parts.push(`[TOOL ${t.tool}] ${truncate(text2, 400)}`);
|
|
5315
|
+
}
|
|
5316
|
+
return bounded(parts.join("\n\n"), maxChars);
|
|
5317
|
+
}
|
|
5318
|
+
function fullTrace(facts, { from = 0, to } = {}) {
|
|
5319
|
+
return facts.turns.slice(from, to).map((t) => {
|
|
5320
|
+
if (t.role === "user") return `[USER]
|
|
5321
|
+
${redact2(t.text)}`;
|
|
5322
|
+
if (t.role === "assistant") return `[ASSISTANT]
|
|
5323
|
+
${redact2(t.text)}`;
|
|
5324
|
+
if (t.tool === "result") return `[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
5325
|
+
${redact2(t.text)}`;
|
|
5326
|
+
const input = t.input === void 0 ? t.text : typeof t.input === "string" ? t.input : JSON.stringify(t.input, null, 1);
|
|
5327
|
+
return `[TOOL ${t.tool}]
|
|
5328
|
+
${redact2(input)}`;
|
|
5329
|
+
});
|
|
5330
|
+
}
|
|
5331
|
+
function traceChunks(turns, maxBytes = 1e6) {
|
|
5332
|
+
const out = [];
|
|
5333
|
+
let cur = "";
|
|
5334
|
+
let size = 0;
|
|
5335
|
+
turns.forEach((turn, i) => {
|
|
5336
|
+
let piece = (i ? "\n\n" : "") + turn;
|
|
5337
|
+
let bytes = jsonBytes(piece);
|
|
5338
|
+
if (size + bytes <= maxBytes) {
|
|
5339
|
+
cur += piece;
|
|
5340
|
+
size += bytes;
|
|
5341
|
+
return;
|
|
5342
|
+
}
|
|
5343
|
+
if (cur) out.push(cur);
|
|
5344
|
+
while (bytes > maxBytes) {
|
|
5345
|
+
let n = Math.min(piece.length, maxBytes);
|
|
5346
|
+
for (let b = jsonBytes(piece.slice(0, n)); b > maxBytes; b = jsonBytes(piece.slice(0, n))) n = Math.max(1, Math.floor(n * maxBytes / b) - 1);
|
|
5347
|
+
const code2 = piece.charCodeAt(n - 1);
|
|
5348
|
+
if (n > 1 && code2 >= 55296 && code2 <= 56319) n--;
|
|
5349
|
+
const head = piece.slice(0, n);
|
|
5350
|
+
out.push(head);
|
|
5351
|
+
piece = piece.slice(n);
|
|
5352
|
+
bytes -= jsonBytes(head);
|
|
5353
|
+
}
|
|
5354
|
+
cur = piece;
|
|
5355
|
+
size = bytes;
|
|
5356
|
+
});
|
|
5357
|
+
if (cur) out.push(cur);
|
|
5358
|
+
return out;
|
|
5359
|
+
}
|
|
5360
|
+
function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
|
|
5361
|
+
const facts = parseTranscript(file);
|
|
5362
|
+
const visible = facts.turns.filter((t) => t.role !== "tool");
|
|
5363
|
+
const userIdx = visible.map((t, i) => t.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
5364
|
+
const start = userIdx.length > turns ? userIdx[userIdx.length - turns] : 0;
|
|
5365
|
+
return digest({ ...facts, turns: visible.slice(start) }, { maxChars });
|
|
5366
|
+
}
|
|
5367
|
+
var BANNER, jsonBytes;
|
|
5368
|
+
var init_transcript = __esm({
|
|
5369
|
+
"src/transcript.ts"() {
|
|
5111
5370
|
"use strict";
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
rootCache = /* @__PURE__ */ new Map();
|
|
5117
|
-
isDoc = (rel) => {
|
|
5118
|
-
const parts = rel.split("/");
|
|
5119
|
-
if (parts.some((p) => SKIP_DIRS.has(p))) return false;
|
|
5120
|
-
return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
|
|
5121
|
-
};
|
|
5122
|
-
deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs9.existsSync(path7.join(root, path7.posix.dirname(rel), ".bunshin.json"));
|
|
5371
|
+
init_redact2();
|
|
5372
|
+
init_util();
|
|
5373
|
+
BANNER = "## Recalled lessons (Bunshin)";
|
|
5374
|
+
jsonBytes = (s) => Buffer.byteLength(JSON.stringify(s)) - 2;
|
|
5123
5375
|
}
|
|
5124
5376
|
});
|
|
5125
5377
|
|
|
@@ -5188,8 +5440,14 @@ function applyOverlays(sections, overlays, ctx) {
|
|
|
5188
5440
|
}
|
|
5189
5441
|
function localView(v2, config, cwd, opts = {}) {
|
|
5190
5442
|
const ctx = contextFor(v2, config);
|
|
5191
|
-
const reader = readerFor(ctx);
|
|
5192
5443
|
const bundle = v2.readBundle();
|
|
5444
|
+
let source;
|
|
5445
|
+
try {
|
|
5446
|
+
source = opts.source !== void 0 ? opts.source || void 0 : sourceFor(cwd, v2.store.root, ctx);
|
|
5447
|
+
} catch {
|
|
5448
|
+
source = void 0;
|
|
5449
|
+
}
|
|
5450
|
+
const reader = readerFor(ctx, void 0, { ...source ? { source } : {}, ...bundle?.scopes ? { tree: bundle.scopes } : {} });
|
|
5193
5451
|
const byId = /* @__PURE__ */ new Map();
|
|
5194
5452
|
for (const s of bundle?.sections ?? []) byId.set(s.id, s);
|
|
5195
5453
|
let root = cwd;
|
|
@@ -5207,7 +5465,7 @@ function localView(v2, config, cwd, opts = {}) {
|
|
|
5207
5465
|
}
|
|
5208
5466
|
}
|
|
5209
5467
|
applyOverlays(byId, v2.readOverlays(), ctx);
|
|
5210
|
-
return { ctx, reader, layers:
|
|
5468
|
+
return { ctx, reader, ...source ? { source } : {}, layers: reader.layers, bundle, sections: [...byId.values()], byId, root, repo };
|
|
5211
5469
|
}
|
|
5212
5470
|
var ACTIVE_OVERLAY;
|
|
5213
5471
|
var init_view = __esm({
|
|
@@ -5215,12 +5473,13 @@ var init_view = __esm({
|
|
|
5215
5473
|
"use strict";
|
|
5216
5474
|
init_v22();
|
|
5217
5475
|
init_repo();
|
|
5476
|
+
init_source();
|
|
5218
5477
|
ACTIVE_OVERLAY = /* @__PURE__ */ new Set(["proposed", "trial", "submitted", "kept-local", "needs-review"]);
|
|
5219
5478
|
}
|
|
5220
5479
|
});
|
|
5221
5480
|
|
|
5222
5481
|
// src/v2/apply.ts
|
|
5223
|
-
import
|
|
5482
|
+
import fs11 from "node:fs";
|
|
5224
5483
|
function appendedText(op) {
|
|
5225
5484
|
return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
|
|
5226
5485
|
}
|
|
@@ -5275,12 +5534,12 @@ function applyLocally(v2, view, s) {
|
|
|
5275
5534
|
if (section.storage.type === "repo") {
|
|
5276
5535
|
const file = containedPath(view.root, section.storage.path);
|
|
5277
5536
|
const ours = !view.repo || section.storage.repo === view.repo;
|
|
5278
|
-
if (ours && file &&
|
|
5279
|
-
const before =
|
|
5537
|
+
if (ours && file && fs11.existsSync(file)) {
|
|
5538
|
+
const before = fs11.readFileSync(file, "utf8");
|
|
5280
5539
|
const res2 = applyOpToMarkdown(before, section.storage.anchor, op);
|
|
5281
5540
|
if (res2.ok) {
|
|
5282
5541
|
const changed = res2.text !== before;
|
|
5283
|
-
if (changed)
|
|
5542
|
+
if (changed) fs11.writeFileSync(file, res2.text);
|
|
5284
5543
|
const overlay2 = { ...base, kind: "working-tree", sectionId: section.id, file: section.storage.path, anchor: section.storage.anchor, changed };
|
|
5285
5544
|
upsertOverlay(v2, overlay2);
|
|
5286
5545
|
return { applied: true, target: describeTarget(s, view), overlay: overlay2 };
|
|
@@ -5301,7 +5560,7 @@ function revertWorkingTree(root, o) {
|
|
|
5301
5560
|
if (!file) return { reverted: false, reason: "path outside the repository" };
|
|
5302
5561
|
let text2;
|
|
5303
5562
|
try {
|
|
5304
|
-
text2 =
|
|
5563
|
+
text2 = fs11.readFileSync(file, "utf8");
|
|
5305
5564
|
} catch {
|
|
5306
5565
|
return { reverted: false, reason: "file missing" };
|
|
5307
5566
|
}
|
|
@@ -5321,7 +5580,7 @@ ${appendedText(o.op)}`;
|
|
|
5321
5580
|
if (once(text2, o.op.text) >= 0 && !text2.includes(o.op.find)) next = text2.replace(o.op.text, o.op.find);
|
|
5322
5581
|
}
|
|
5323
5582
|
if (next === null) return { reverted: false, reason: "file changed since the trial edit" };
|
|
5324
|
-
|
|
5583
|
+
fs11.writeFileSync(file, next);
|
|
5325
5584
|
return { reverted: true };
|
|
5326
5585
|
}
|
|
5327
5586
|
function revertParam(v2, o) {
|
|
@@ -5394,7 +5653,7 @@ var init_apply = __esm({
|
|
|
5394
5653
|
});
|
|
5395
5654
|
|
|
5396
5655
|
// src/v2/signals.ts
|
|
5397
|
-
import
|
|
5656
|
+
import fs12 from "node:fs";
|
|
5398
5657
|
function isCorrection(text2) {
|
|
5399
5658
|
const t = text2.trim().slice(0, 600);
|
|
5400
5659
|
return !!t && CORRECTION_RE.test(t);
|
|
@@ -5416,7 +5675,7 @@ function scanTranscript(file) {
|
|
|
5416
5675
|
if (!file) return empty;
|
|
5417
5676
|
let raw;
|
|
5418
5677
|
try {
|
|
5419
|
-
raw =
|
|
5678
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
5420
5679
|
} catch {
|
|
5421
5680
|
return empty;
|
|
5422
5681
|
}
|
|
@@ -5500,8 +5759,8 @@ var init_signals = __esm({
|
|
|
5500
5759
|
});
|
|
5501
5760
|
|
|
5502
5761
|
// src/v2/ui.ts
|
|
5503
|
-
import
|
|
5504
|
-
import
|
|
5762
|
+
import fs13 from "node:fs";
|
|
5763
|
+
import path9 from "node:path";
|
|
5505
5764
|
function feedPath(v2, sessionId) {
|
|
5506
5765
|
return v2.p("ui", `${safe(sessionId)}.json`);
|
|
5507
5766
|
}
|
|
@@ -5517,7 +5776,7 @@ function emptyFeed(v2, sessionId, now) {
|
|
|
5517
5776
|
}
|
|
5518
5777
|
function cliArgv() {
|
|
5519
5778
|
const entry = process.argv[1];
|
|
5520
|
-
return entry ? [process.execPath,
|
|
5779
|
+
return entry ? [process.execPath, path9.resolve(entry)] : ["bunshin"];
|
|
5521
5780
|
}
|
|
5522
5781
|
function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5523
5782
|
if (!sessionId || sessionId === "no-session") return null;
|
|
@@ -5532,7 +5791,7 @@ function updateFeed(v2, sessionId, patch, now = (/* @__PURE__ */ new Date()).toI
|
|
|
5532
5791
|
f.cli = cliArgv();
|
|
5533
5792
|
f.served = f.served.slice(0, MAX_SERVED);
|
|
5534
5793
|
f.events = f.events.slice(-MAX_EVENTS);
|
|
5535
|
-
|
|
5794
|
+
fs13.mkdirSync(path9.dirname(feedPath(v2, sessionId)), { recursive: true });
|
|
5536
5795
|
writeJsonAtomic(feedPath(v2, sessionId), f);
|
|
5537
5796
|
return f;
|
|
5538
5797
|
} catch {
|
|
@@ -5633,16 +5892,16 @@ function latestFeed(v2) {
|
|
|
5633
5892
|
const dir = v2.p("ui");
|
|
5634
5893
|
let best = null;
|
|
5635
5894
|
try {
|
|
5636
|
-
for (const f2 of
|
|
5895
|
+
for (const f2 of fs13.readdirSync(dir)) {
|
|
5637
5896
|
if (!f2.endsWith(".json") || f2.endsWith(".mod.json")) continue;
|
|
5638
|
-
const m =
|
|
5897
|
+
const m = fs13.statSync(path9.join(dir, f2)).mtimeMs;
|
|
5639
5898
|
if (!best || m > best.m) best = { f: f2, m };
|
|
5640
5899
|
}
|
|
5641
5900
|
} catch {
|
|
5642
5901
|
return null;
|
|
5643
5902
|
}
|
|
5644
5903
|
if (!best) return null;
|
|
5645
|
-
const f = readJson(
|
|
5904
|
+
const f = readJson(path9.join(dir, best.f), null);
|
|
5646
5905
|
return f && f.v === UI_FEED_VERSION ? f : null;
|
|
5647
5906
|
}
|
|
5648
5907
|
var UI_FEED_VERSION, MAX_SERVED, MAX_EVENTS, MOD_FRESH_MS, DEFAULT_MODES;
|
|
@@ -5658,110 +5917,6 @@ var init_ui = __esm({
|
|
|
5658
5917
|
}
|
|
5659
5918
|
});
|
|
5660
5919
|
|
|
5661
|
-
// src/v2/source.ts
|
|
5662
|
-
var source_exports = {};
|
|
5663
|
-
__export(source_exports, {
|
|
5664
|
-
cwdInPool: () => cwdInPool,
|
|
5665
|
-
inPool: () => inPool,
|
|
5666
|
-
listFolders: () => listFolders,
|
|
5667
|
-
projectRootFor: () => projectRootFor,
|
|
5668
|
-
select: () => select,
|
|
5669
|
-
sourceFor: () => sourceFor,
|
|
5670
|
-
storeHome: () => storeHome,
|
|
5671
|
-
topFolder: () => topFolder
|
|
5672
|
-
});
|
|
5673
|
-
import fs13 from "node:fs";
|
|
5674
|
-
import path9 from "node:path";
|
|
5675
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
5676
|
-
function storeHome(storeRoot) {
|
|
5677
|
-
return path9.dirname(path9.resolve(storeRoot));
|
|
5678
|
-
}
|
|
5679
|
-
function topFolder(cwd, storeRoot) {
|
|
5680
|
-
const rel = path9.relative(storeHome(storeRoot), path9.resolve(cwd));
|
|
5681
|
-
if (!rel || rel.startsWith("..") || path9.isAbsolute(rel)) return "";
|
|
5682
|
-
return rel.split(path9.sep)[0] ?? "";
|
|
5683
|
-
}
|
|
5684
|
-
function inPool(selection, folder) {
|
|
5685
|
-
if (!folder || !selection) return true;
|
|
5686
|
-
if (selection.exclude?.includes(folder)) return false;
|
|
5687
|
-
return selection.only ? selection.only.includes(folder) : true;
|
|
5688
|
-
}
|
|
5689
|
-
function cwdInPool(config, cwd, storeRoot) {
|
|
5690
|
-
return inPool(config.sources, topFolder(cwd, storeRoot));
|
|
5691
|
-
}
|
|
5692
|
-
function git2(cwd, args) {
|
|
5693
|
-
try {
|
|
5694
|
-
return execFileSync3("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim() || null;
|
|
5695
|
-
} catch {
|
|
5696
|
-
return null;
|
|
5697
|
-
}
|
|
5698
|
-
}
|
|
5699
|
-
function sourceFor(cwd, storeRoot, ctx) {
|
|
5700
|
-
if (ctx?.kind === "robot") return sourceId("robot", ctx.id);
|
|
5701
|
-
const home = storeHome(storeRoot);
|
|
5702
|
-
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
5703
|
-
if (top) {
|
|
5704
|
-
const remote = git2(top, ["remote", "get-url", "origin"]);
|
|
5705
|
-
const named = remote ? parseRemote(remote) : null;
|
|
5706
|
-
if (named) return sourceId("repo", named);
|
|
5707
|
-
const rel = path9.relative(home, top);
|
|
5708
|
-
return sourceId("folder", rel && !rel.startsWith("..") && !path9.isAbsolute(rel) ? rel.split(path9.sep).join("/") : path9.basename(top));
|
|
5709
|
-
}
|
|
5710
|
-
const folder = topFolder(cwd, storeRoot);
|
|
5711
|
-
return folder ? sourceId("folder", folder) : void 0;
|
|
5712
|
-
}
|
|
5713
|
-
function projectRootFor(cwd, storeRoot) {
|
|
5714
|
-
const home = storeHome(storeRoot);
|
|
5715
|
-
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
5716
|
-
if (top) {
|
|
5717
|
-
const rel = path9.relative(home, top);
|
|
5718
|
-
if (!rel.startsWith("..") && !path9.isAbsolute(rel)) return top;
|
|
5719
|
-
}
|
|
5720
|
-
const folder = topFolder(cwd, storeRoot);
|
|
5721
|
-
return folder ? path9.join(home, folder) : home;
|
|
5722
|
-
}
|
|
5723
|
-
function listFolders(storeRoot, selection, opts = {}) {
|
|
5724
|
-
const home = storeHome(storeRoot);
|
|
5725
|
-
let names = [];
|
|
5726
|
-
try {
|
|
5727
|
-
names = fs13.readdirSync(home, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
5728
|
-
} catch {
|
|
5729
|
-
}
|
|
5730
|
-
return names.map((folder) => ({
|
|
5731
|
-
folder,
|
|
5732
|
-
included: inPool(selection, folder),
|
|
5733
|
-
...opts.sources ? { source: sourceFor(path9.join(home, folder), storeRoot) } : {}
|
|
5734
|
-
}));
|
|
5735
|
-
}
|
|
5736
|
-
function select(current, action, folders) {
|
|
5737
|
-
const uniq = (xs) => [...new Set(xs)].sort((a, b) => a.localeCompare(b));
|
|
5738
|
-
const only = current?.only;
|
|
5739
|
-
const exclude = current?.exclude ?? [];
|
|
5740
|
-
let next;
|
|
5741
|
-
switch (action) {
|
|
5742
|
-
case "all":
|
|
5743
|
-
return void 0;
|
|
5744
|
-
case "only":
|
|
5745
|
-
next = { only: uniq(folders) };
|
|
5746
|
-
break;
|
|
5747
|
-
case "include":
|
|
5748
|
-
next = { ...only ? { only: uniq([...only, ...folders]) } : {}, exclude: exclude.filter((f) => !folders.includes(f)) };
|
|
5749
|
-
break;
|
|
5750
|
-
case "exclude":
|
|
5751
|
-
next = { ...only ? { only: only.filter((f) => !folders.includes(f)) } : {}, exclude: uniq([...exclude, ...folders]) };
|
|
5752
|
-
break;
|
|
5753
|
-
}
|
|
5754
|
-
if (!next.exclude?.length) delete next.exclude;
|
|
5755
|
-
return next.only || next.exclude ? next : void 0;
|
|
5756
|
-
}
|
|
5757
|
-
var init_source = __esm({
|
|
5758
|
-
"src/v2/source.ts"() {
|
|
5759
|
-
"use strict";
|
|
5760
|
-
init_v22();
|
|
5761
|
-
init_repo();
|
|
5762
|
-
}
|
|
5763
|
-
});
|
|
5764
|
-
|
|
5765
5920
|
// src/v2/sync.ts
|
|
5766
5921
|
var sync_exports = {};
|
|
5767
5922
|
__export(sync_exports, {
|
|
@@ -6113,7 +6268,7 @@ async function runReflectionV2(store, opts) {
|
|
|
6113
6268
|
if (!release) return { ...result, skipped: "a reflection of this session is already running" };
|
|
6114
6269
|
try {
|
|
6115
6270
|
const session = v2.session(opts.sessionId, opts.cwd ?? process.cwd());
|
|
6116
|
-
const view = localView(v2, config, session.cwd || opts.cwd || process.cwd());
|
|
6271
|
+
const view = localView(v2, config, session.cwd || opts.cwd || process.cwd(), session.source !== void 0 ? { source: session.source } : {});
|
|
6117
6272
|
const origin = opts.origin ?? (opts.focus !== void 0 ? "learn" : "session-end");
|
|
6118
6273
|
const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
|
|
6119
6274
|
const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
|
|
@@ -6130,7 +6285,7 @@ async function runReflectionV2(store, opts) {
|
|
|
6130
6285
|
const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
|
|
6131
6286
|
const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
|
|
6132
6287
|
const guidelines = reflectionGuidelines(view.sections, view.reader);
|
|
6133
|
-
const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
|
|
6288
|
+
const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, guidelines, scopes: scopeChoices(view.bundle?.scopes, view.source), ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
|
|
6134
6289
|
let raw = null;
|
|
6135
6290
|
if (harness === "claude-code") {
|
|
6136
6291
|
const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd, model: config.model });
|
|
@@ -6170,7 +6325,7 @@ async function runReflectionV2(store, opts) {
|
|
|
6170
6325
|
result.nothingNew = result.method !== "none" && parsed.length === 0;
|
|
6171
6326
|
const apply = opts.forceApply || config.reflect.autoApply === "all" || config.reflect.autoApply === "robot" && view.ctx.kind === "robot";
|
|
6172
6327
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6173
|
-
const source =
|
|
6328
|
+
const source = view.source;
|
|
6174
6329
|
for (const p of parsed) {
|
|
6175
6330
|
const s = toSuggestion(p, { id: hexId("g"), orgId: config.orgId, contextId: view.ctx.id, authorUserId: config.userId, origin, createdAt, episodeId });
|
|
6176
6331
|
if (source) s.source = source;
|
|
@@ -6257,7 +6412,6 @@ var init_reflect3 = __esm({
|
|
|
6257
6412
|
init_signals();
|
|
6258
6413
|
init_ui();
|
|
6259
6414
|
init_view();
|
|
6260
|
-
init_source();
|
|
6261
6415
|
FORK_TIMEOUT_MS = 12e4;
|
|
6262
6416
|
FORK_DENIED_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task,Agent,Skill,SlashCommand";
|
|
6263
6417
|
}
|
|
@@ -6677,7 +6831,7 @@ function liveUpdate(v2, ctx, session) {
|
|
|
6677
6831
|
return "";
|
|
6678
6832
|
}
|
|
6679
6833
|
const config = ctx.store.config();
|
|
6680
|
-
const deployed = deployEnabled(config,
|
|
6834
|
+
const deployed = deployEnabled(config, contextFor(v2, config));
|
|
6681
6835
|
const paths = deployed ? deployPaths(process.env, config.orgId) : null;
|
|
6682
6836
|
const root = paths ? config.harness === "codex" ? paths.codexSkills : paths.claudeSkills : null;
|
|
6683
6837
|
const skillPath = (name) => root ? `${root}/${name}/SKILL.md` : null;
|
|
@@ -6744,7 +6898,7 @@ async function onPrompt(ctx) {
|
|
|
6744
6898
|
};
|
|
6745
6899
|
if (!prompt.trim()) return withLive({});
|
|
6746
6900
|
const config = ctx.store.config();
|
|
6747
|
-
const view = localView(v2, config, ctx.cwd);
|
|
6901
|
+
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6748
6902
|
const injected = inContext(session);
|
|
6749
6903
|
const named = invokedSkill(prompt, view.sections);
|
|
6750
6904
|
if (named) {
|
|
@@ -6832,7 +6986,7 @@ function onPreToolUse(ctx) {
|
|
|
6832
6986
|
const raw = toolPaths(ctx.input.tool_input);
|
|
6833
6987
|
if (!raw.length) return withLive({});
|
|
6834
6988
|
const config = ctx.store.config();
|
|
6835
|
-
const view = localView(v2, config, ctx.cwd);
|
|
6989
|
+
const view = localView(v2, config, ctx.cwd, session.source !== void 0 ? { source: session.source } : {});
|
|
6836
6990
|
const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
|
|
6837
6991
|
const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
|
|
6838
6992
|
if (!paths.length && !text2) return withLive({});
|
|
@@ -6957,6 +7111,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
6957
7111
|
...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
|
|
6958
7112
|
cliVersion: version(),
|
|
6959
7113
|
...source ? { source } : {},
|
|
7114
|
+
...session.borrowed?.length ? { borrowed: session.borrowed } : {},
|
|
6960
7115
|
uploaded: false,
|
|
6961
7116
|
...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
|
|
6962
7117
|
...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
|
|
@@ -7265,6 +7420,7 @@ var init_project_hooks = __esm({
|
|
|
7265
7420
|
// src/init.ts
|
|
7266
7421
|
var init_exports = {};
|
|
7267
7422
|
__export(init_exports, {
|
|
7423
|
+
BORROW_COMMAND: () => BORROW_COMMAND,
|
|
7268
7424
|
CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
|
|
7269
7425
|
HOOK_EVENTS: () => HOOK_EVENTS,
|
|
7270
7426
|
LEARN_COMMAND: () => LEARN_COMMAND,
|
|
@@ -7386,18 +7542,18 @@ function mcpLauncher(bin) {
|
|
|
7386
7542
|
}
|
|
7387
7543
|
return { command: process.execPath, args: [script, "mcp"] };
|
|
7388
7544
|
}
|
|
7389
|
-
function installLearnCommand({ file, remove = false }) {
|
|
7545
|
+
function installLearnCommand({ file, remove = false, content = LEARN_COMMAND }) {
|
|
7390
7546
|
try {
|
|
7391
7547
|
const current = fileExists(file) ? fs21.readFileSync(file, "utf8") : null;
|
|
7392
|
-
const ours = current === null || current ===
|
|
7548
|
+
const ours = current === null || current === content;
|
|
7393
7549
|
if (remove) {
|
|
7394
7550
|
if (current !== null && ours) fs21.unlinkSync(file);
|
|
7395
7551
|
return { file, installed: 0, changed: current !== null && ours };
|
|
7396
7552
|
}
|
|
7397
|
-
if (!ours) return { file, installed: 0, changed: false, error:
|
|
7553
|
+
if (!ours) return { file, installed: 0, changed: false, error: `a different ${path16.basename(file)} is already there; left as it is` };
|
|
7398
7554
|
if (current === null) {
|
|
7399
7555
|
fs21.mkdirSync(path16.dirname(file), { recursive: true });
|
|
7400
|
-
fs21.writeFileSync(file,
|
|
7556
|
+
fs21.writeFileSync(file, content);
|
|
7401
7557
|
}
|
|
7402
7558
|
return { file, installed: 1, changed: current === null };
|
|
7403
7559
|
} catch (err) {
|
|
@@ -7519,11 +7675,20 @@ function runInit(opts = {}) {
|
|
|
7519
7675
|
learnCommand = installLearnCommand({ file: path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "learn.md"), remove: opts.removeHooks });
|
|
7520
7676
|
if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
|
|
7521
7677
|
}
|
|
7678
|
+
let borrowCommand;
|
|
7679
|
+
if (opts.removeHooks || !opts.noHooks) {
|
|
7680
|
+
const user = hookScope === "user";
|
|
7681
|
+
const file = config.harness === "claude-code" ? path16.join(user ? path16.join(homeDir(), ".claude") : path16.join(cwd, ".claude"), "commands", "borrow.md") : config.harness === "codex" ? path16.join(process.env.CODEX_HOME ?? path16.join(homeDir(), ".codex"), "prompts", "borrow.md") : config.harness === "pi" ? user ? path16.join(homeDir(), ".pi", "agent", "prompts", "borrow.md") : path16.join(cwd, ".pi", "prompts", "borrow.md") : null;
|
|
7682
|
+
if (file) {
|
|
7683
|
+
borrowCommand = installLearnCommand({ file, remove: opts.removeHooks, content: BORROW_COMMAND });
|
|
7684
|
+
if (borrowCommand.error) warnings.push(`/borrow not installed at ${borrowCommand.file}: ${borrowCommand.error}`);
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7522
7687
|
if (!resolveBackend(config)) {
|
|
7523
7688
|
warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
|
|
7524
7689
|
}
|
|
7525
7690
|
store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
|
|
7526
|
-
return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, warnings };
|
|
7691
|
+
return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, ...borrowCommand ? { borrowCommand } : {}, warnings };
|
|
7527
7692
|
}
|
|
7528
7693
|
function otherHarnessInstructions() {
|
|
7529
7694
|
return [
|
|
@@ -7539,7 +7704,7 @@ function otherHarnessInstructions() {
|
|
|
7539
7704
|
" Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
|
|
7540
7705
|
].join("\n");
|
|
7541
7706
|
}
|
|
7542
|
-
var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
|
|
7707
|
+
var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND, BORROW_COMMAND;
|
|
7543
7708
|
var init_init = __esm({
|
|
7544
7709
|
"src/init.ts"() {
|
|
7545
7710
|
"use strict";
|
|
@@ -7565,6 +7730,7 @@ var init_init = __esm({
|
|
|
7565
7730
|
{ event: "SessionEnd", arg: "session-end", timeout: 3 }
|
|
7566
7731
|
];
|
|
7567
7732
|
LEARN_COMMAND = '---\ndescription: Capture what this session learned into the Bunshin library now (applied locally as a trial, submitted for the nightly review)\nargument-hint: "[what to capture, e.g. \\"the ledger must be reconciled before closing\\"]"\nallowed-tools: Bash(bunshin learn:*)\n---\n\nCapture knowledge from this session into Bunshin.\n\nRun exactly this with the Bash tool (it takes up to two minutes; use a timeout of 180000 ms):\n\n```bash\nbunshin learn "$ARGUMENTS"\n```\n\n`bunshin learn` forks this conversation (`claude -p --resume <this session> --fork-session`),\nso the reflection sees everything you saw without re-sending the transcript. It applies what it\nfinds locally as a trial and queues it for the nightly review:\n\n- edits to this repo\'s SPEC.md / TESTING.md / SKILL.md land in the working tree (they show up\n in `git diff`);\n- knowledge with no repo home becomes a local overlay on the Bunshin library;\n- per-machine values stay private.\n\nRelay its output to the user as it is. Each line reads\n`captured: <summary> \u2192 <section or file#anchor>`. If it prints `nothing new to capture.`, say so\nin one line; that is the normal outcome. If it reports an error, show it and stop; do not retry\nin a loop, and do not edit the library files by hand.\n\nIf the user named nothing specific, run it without arguments. Do not continue with other work in\nthe same reply unless the user asked you to.\n';
|
|
7733
|
+
BORROW_COMMAND = '---\ndescription: Bring in a lesson or skill learned elsewhere in this workspace (another project or group) when you know it will help here\nargument-hint: "[what to look for, e.g. \\"the release checklist from the api repo\\"]"\nallowed-tools: Bash(bunshin recall:*), Bash(bunshin load:*)\n---\n\nThe person wants something learned outside this project\'s scope. Search the whole workspace for it\n(run with the Bash tool):\n\n```bash\nbunshin recall "$ARGUMENTS" --hypothetical "$ARGUMENTS" --across\n```\n\nShow the person the numbered results as printed (each says where it was learned). Load the one\nthey pick, or the one that plainly matches, with `bunshin load <id>`, then use it as prior\nknowledge for the task. Loading it records that it was borrowed into this project: the nightly\ngeneralization reads that as evidence the lesson applies beyond where it was learned.\n\nIf nothing matches, say so in one line and carry on.\n';
|
|
7568
7734
|
}
|
|
7569
7735
|
});
|
|
7570
7736
|
|
|
@@ -7586,19 +7752,19 @@ function mark(v2, sessionId, cwd, field, ids) {
|
|
|
7586
7752
|
} catch {
|
|
7587
7753
|
}
|
|
7588
7754
|
}
|
|
7589
|
-
async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3 }) {
|
|
7755
|
+
async function recallPull(store, query, { cwd, sessionId, timeoutMs = 6e3, across = false }) {
|
|
7590
7756
|
const v2 = new V2Store(store);
|
|
7591
7757
|
const config = store.config();
|
|
7592
7758
|
const sid = sessionId ?? sessionIdFromEnv(v2);
|
|
7593
7759
|
const session = sid ? v2.readSession(sid) : null;
|
|
7594
7760
|
const exclude = session ? [...session.loaded] : [];
|
|
7595
7761
|
const limit = Math.max(1, Math.min(10, query.limit ?? 5));
|
|
7596
|
-
const view = localView(v2, config, cwd);
|
|
7597
|
-
const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude });
|
|
7762
|
+
const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
|
|
7763
|
+
const local = () => pull(view.sections, view.reader, { ...query, limit }, { exclude, across });
|
|
7598
7764
|
let out;
|
|
7599
7765
|
if (canCall(config)) {
|
|
7600
7766
|
try {
|
|
7601
|
-
const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude }, { timeoutMs });
|
|
7767
|
+
const res = await v2api(config, "POST", "/recall/pull", { ...redactDeep(query), limit, exclude, ...view.source ? { source: view.source } : {}, ...across ? { across: true } : {} }, { timeoutMs });
|
|
7602
7768
|
const remote = Array.isArray(res.candidates) ? res.candidates : [];
|
|
7603
7769
|
const extra = local().filter((c) => c.id.startsWith("local:") && !remote.some((r) => r.id === c.id));
|
|
7604
7770
|
out = { candidates: [...remote, ...extra].slice(0, limit), source: "mothership" };
|
|
@@ -7613,7 +7779,8 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
|
|
|
7613
7779
|
const v2 = new V2Store(store);
|
|
7614
7780
|
const config = store.config();
|
|
7615
7781
|
const sid = sessionId ?? sessionIdFromEnv(v2);
|
|
7616
|
-
const
|
|
7782
|
+
const session = sid ? v2.readSession(sid) : null;
|
|
7783
|
+
const view = localView(v2, config, cwd, session?.source !== void 0 ? { source: session.source } : {});
|
|
7617
7784
|
let section = view.byId.get(id);
|
|
7618
7785
|
let source = "local";
|
|
7619
7786
|
if (!section && canCall(config)) {
|
|
@@ -7626,8 +7793,17 @@ async function loadSection(store, id, { cwd, sessionId, anchor }) {
|
|
|
7626
7793
|
}
|
|
7627
7794
|
if (!section) return null;
|
|
7628
7795
|
mark(v2, sid, cwd, "loaded", [section.id]);
|
|
7796
|
+
const borrowed = !inBoundary(section, view.reader);
|
|
7797
|
+
if (borrowed && sid) {
|
|
7798
|
+
try {
|
|
7799
|
+
v2.updateSession(sid, cwd, (s) => {
|
|
7800
|
+
s.borrowed = [...(s.borrowed ?? []).filter((b) => b.sectionId !== section.id), { sectionId: section.id, layer: section.layer, at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-50);
|
|
7801
|
+
});
|
|
7802
|
+
} catch {
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7629
7805
|
const part = anchor?.replace(/^[§#]/, "");
|
|
7630
|
-
return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source };
|
|
7806
|
+
return { section, text: renderSection(section, void 0, part ? { anchor: part, maxChars: 0 } : { maxChars: Number.POSITIVE_INFINITY }), source, ...borrowed ? { borrowed: true } : {} };
|
|
7631
7807
|
}
|
|
7632
7808
|
var init_library = __esm({
|
|
7633
7809
|
"src/v2/library.ts"() {
|
|
@@ -7761,18 +7937,19 @@ async function cmdReviewDecide(store, args, out, err) {
|
|
|
7761
7937
|
async function cmdRecall(store, args, cwd, out, err) {
|
|
7762
7938
|
const abstract = args._.slice(1).join(" ").trim() || str3(args.flags.abstract) || "";
|
|
7763
7939
|
if (!abstract) {
|
|
7764
|
-
err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--json]\n');
|
|
7940
|
+
err('bunshin recall: usage: bunshin recall "<the task, abstractly>" --hypothetical "<the lesson you would want>" --id a,b [--across] [--json]\n');
|
|
7765
7941
|
return 1;
|
|
7766
7942
|
}
|
|
7767
7943
|
const limit = Number(str3(args.flags.limit)) || void 0;
|
|
7768
|
-
const
|
|
7944
|
+
const across = bool2(args.flags.across);
|
|
7945
|
+
const res = await recallPull(store, { taskAbstract: abstract, hypotheticalLesson: str3(args.flags.hypothetical) ?? abstract, identifiers: list(args.flags.id ?? args.flags.ids), ...limit ? { limit } : {} }, { cwd, sessionId: str3(args.flags.session), across });
|
|
7769
7946
|
if (bool2(args.flags.json)) {
|
|
7770
7947
|
out(JSON.stringify(res, null, 2) + "\n");
|
|
7771
7948
|
return 0;
|
|
7772
7949
|
}
|
|
7773
|
-
out(renderCandidates(res.candidates) + "\n");
|
|
7950
|
+
out(renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes) + "\n");
|
|
7774
7951
|
if (res.candidates.length) out(`
|
|
7775
|
-
(${res.source}) Load one with: bunshin load <id
|
|
7952
|
+
(${res.source}) Load one with: bunshin load <id>${across && res.candidates.some((c) => c.outside) ? " (a lesson from outside this project's scope is recorded as borrowed: evidence it applies here too)" : ""}
|
|
7776
7953
|
`);
|
|
7777
7954
|
return 0;
|
|
7778
7955
|
}
|
|
@@ -7789,7 +7966,7 @@ async function cmdLoad(store, args, cwd, out, err) {
|
|
|
7789
7966
|
`);
|
|
7790
7967
|
return 1;
|
|
7791
7968
|
}
|
|
7792
|
-
out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source }, null, 2) + "\n" : res.text + "\n");
|
|
7969
|
+
out(bool2(args.flags.json) ? JSON.stringify({ section: res.section, source: res.source, ...res.borrowed ? { borrowed: true } : {} }, null, 2) + "\n" : res.text + "\n");
|
|
7793
7970
|
return 0;
|
|
7794
7971
|
}
|
|
7795
7972
|
async function cmdLearn(store, args, cwd, out, err) {
|
|
@@ -8347,8 +8524,8 @@ async function handleMcp(msg, opts) {
|
|
|
8347
8524
|
if (!taskAbstract.trim()) return reply(text("task_abstract is required: describe the task abstractly.", true));
|
|
8348
8525
|
const identifiers = Array.isArray(args.identifiers) ? args.identifiers.filter((x) => typeof x === "string") : [];
|
|
8349
8526
|
const limit = typeof args.limit === "number" ? args.limit : void 0;
|
|
8350
|
-
const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd });
|
|
8351
|
-
const body = renderCandidates(res.candidates);
|
|
8527
|
+
const res = await recallPull(store, { taskAbstract, hypotheticalLesson, identifiers, ...limit ? { limit } : {} }, { cwd, across: args.across === true });
|
|
8528
|
+
const body = renderCandidates(res.candidates, new V2Store(store).readBundle()?.scopes);
|
|
8352
8529
|
return reply(text(res.candidates.length ? `${body}
|
|
8353
8530
|
|
|
8354
8531
|
Load the ones that would change what you do with bunshin_load (0-3).` : body));
|
|
@@ -8405,6 +8582,7 @@ var init_mcp = __esm({
|
|
|
8405
8582
|
init_v22();
|
|
8406
8583
|
init_source();
|
|
8407
8584
|
init_store();
|
|
8585
|
+
init_store2();
|
|
8408
8586
|
init_library();
|
|
8409
8587
|
DEFAULT_PROTOCOL = "2025-06-18";
|
|
8410
8588
|
MCP_TOOLS = [
|
|
@@ -8418,7 +8596,8 @@ var init_mcp = __esm({
|
|
|
8418
8596
|
task_abstract: { type: "string", description: "The task in abstract, structural terms (what kind of problem this is), not a restatement of its topic." },
|
|
8419
8597
|
hypothetical_lesson: { type: "string", description: "The lesson you would want to find, written as if it existed." },
|
|
8420
8598
|
identifiers: { type: "array", items: { type: "string" }, description: "Concrete identifiers involved: function names, error codes, file paths, product or system names. May be empty." },
|
|
8421
|
-
limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." }
|
|
8599
|
+
limit: { type: "integer", minimum: 1, maximum: 10, description: "Maximum results (default 5)." },
|
|
8600
|
+
across: { type: "boolean", description: "Only when the person asks for something learned elsewhere (another project or group of this workspace): search across the project's boundary. Default false." }
|
|
8422
8601
|
},
|
|
8423
8602
|
required: ["task_abstract", "hypothetical_lesson", "identifiers"],
|
|
8424
8603
|
additionalProperties: false
|
|
@@ -9627,7 +9806,7 @@ function effortOf(job) {
|
|
|
9627
9806
|
const tokens = Math.round(chars / 4);
|
|
9628
9807
|
const minutes = Math.max(2, Math.round(2 + tokens / 15e3));
|
|
9629
9808
|
const proposals = (job.prompt.match(/^### g_\w+/gm) ?? []).length;
|
|
9630
|
-
const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
|
|
9809
|
+
const what = job.kind === "reflect" ? "read every [USER] message and each failed or denied result first; most sessions teach nothing new, and at most 4 lessons are kept" : job.kind === "merge" ? `${proposals} proposal${proposals === 1 ? "" : "s"} against one document: decide each, edit only what the applied ones need` : job.kind === "generalize" ? `${(job.prompt.match(/^<document /gm) ?? []).length} documents from sibling scopes: write once at the parent what at least two of them share, and trim it out of them; "nothing_shared" is a normal answer` : `${proposals} proposed new document${proposals === 1 ? "" : "s"}: combine, fold into existing ones, or reject`;
|
|
9631
9810
|
return `${chars.toLocaleString("en")} chars (~${tokens.toLocaleString("en")} tokens); about ${minutes} min. ${what}.`;
|
|
9632
9811
|
}
|
|
9633
9812
|
function jobIndex(text2) {
|
|
@@ -10115,7 +10294,7 @@ var init_runner = __esm({
|
|
|
10115
10294
|
init_trust();
|
|
10116
10295
|
init_version();
|
|
10117
10296
|
DEFAULT_API = "https://api.bunshin.cc";
|
|
10118
|
-
KINDS2 = ["reflect", "merge", "create"];
|
|
10297
|
+
KINDS2 = ["reflect", "merge", "create", "generalize"];
|
|
10119
10298
|
str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
10120
10299
|
LAUNCHD_LABEL = "cc.bunshin.runner";
|
|
10121
10300
|
CRON_MARK = "# bunshin-runner";
|
|
@@ -11146,7 +11325,7 @@ import fs32 from "node:fs";
|
|
|
11146
11325
|
import path27 from "node:path";
|
|
11147
11326
|
function parseArgs(argv) {
|
|
11148
11327
|
const out = { _: [], flags: {} };
|
|
11149
|
-
const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace"]);
|
|
11328
|
+
const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "sync", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose", "robot", "fork", "upload", "index", "all", "legacy", "depth", "local", "skip-github", "propose-rewrites", "clear", "watch", "once", "allow-codex-shell", "retrace", "across"]);
|
|
11150
11329
|
for (let i = 0; i < argv.length; i++) {
|
|
11151
11330
|
const a = argv[i];
|
|
11152
11331
|
if (a === "--") {
|
|
@@ -11285,6 +11464,8 @@ Key saved privately outside the project.
|
|
|
11285
11464
|
if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
|
|
11286
11465
|
`);
|
|
11287
11466
|
if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
|
|
11467
|
+
`);
|
|
11468
|
+
if (result.borrowCommand?.installed) out(`/borrow installed in ${result.borrowCommand.file} (bring in a lesson learned in another project or group)
|
|
11288
11469
|
`);
|
|
11289
11470
|
else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
|
|
11290
11471
|
`);
|
|
@@ -11860,10 +12041,12 @@ Usage: bunshin <command> [options]
|
|
|
11860
12041
|
status [--json] store, context, library, outbox, hooks and sync health
|
|
11861
12042
|
sources [all | only <folders\u2026> | include <folders\u2026> | exclude <folders\u2026>] [--json]
|
|
11862
12043
|
which folders under this store's folder take part (learning pool)
|
|
11863
|
-
recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--json]
|
|
11864
|
-
pull candidates from
|
|
12044
|
+
recall "<task, abstractly>" [--hypothetical "<lesson>"] [--id a,b] [--limit n] [--across] [--json]
|
|
12045
|
+
pull candidates from this project's scopes (offline: local view);
|
|
12046
|
+
--across borrows from anywhere in the workspace (/borrow)
|
|
11865
12047
|
load <id> [--anchor <heading>] [--json]
|
|
11866
|
-
print one document (or one part of it) and mark it loaded
|
|
12048
|
+
print one document (or one part of it) and mark it loaded; a
|
|
12049
|
+
lesson from outside this project's scopes is recorded as borrowed
|
|
11867
12050
|
learn [focus...] capture what this session learned now (fork reflection),
|
|
11868
12051
|
apply it locally as a trial and submit it
|
|
11869
12052
|
review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
|
|
@@ -11883,9 +12066,8 @@ Usage: bunshin <command> [options]
|
|
|
11883
12066
|
the deployed skills; push proposes local edits / a new skill now
|
|
11884
12067
|
library version|log|status|pin [--write [file]]|checkout <lib@N> --to <dir> [--git <sha>] [--pin <file>]
|
|
11885
12068
|
library versions (lib@N), history, and exact reproductions
|
|
11886
|
-
library workspace
|
|
11887
|
-
|
|
11888
|
-
~/Documents/dev) instead of ~/.claude and ~/.codex; set it before the first sync
|
|
12069
|
+
library workspace (retired: each workspace keeps its own skills and links them into
|
|
12070
|
+
the projects its sessions run in; see library status)
|
|
11889
12071
|
ui [--session <id>] [--json] [--watch] what Bunshin did this session, as the person sees it
|
|
11890
12072
|
ui status | install | uninstall the Claude Code mod (band above the prompt, /bunshin, toasts)
|
|
11891
12073
|
ui enable|disable --harness <h> [--mode auto|mod|widget|lines|off] how a harness shows it
|
|
@@ -11896,9 +12078,9 @@ Usage: bunshin <command> [options]
|
|
|
11896
12078
|
repo import|attach|clone|verify|list|url|login
|
|
11897
12079
|
host a git repository in Bunshin (full history; git push/pull
|
|
11898
12080
|
keep working through git-remote-bunshin). bunshin repo help
|
|
11899
|
-
runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create]
|
|
12081
|
+
runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create,generalize]
|
|
11900
12082
|
answer the company's queued model work (cloud reflection,
|
|
11901
|
-
reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
|
|
12083
|
+
reconciliation, generalization) with this machine's model. BUNSHIN_RUNNER_KEY
|
|
11902
12084
|
runner next | submit <job> <file|-> | fail <job> --error <why>
|
|
11903
12085
|
agent mode, for a Claude Code routine or Codex cloud task
|
|
11904
12086
|
runner install --key <runner key> [--every <minutes>] [--no-run] | uninstall
|