@bunshin-cc/bunshin 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +143 -57
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1127,6 +1127,23 @@ function scopePath(tree, source) {
1127
1127
  function inBoundary(section, reader) {
1128
1128
  return !isScopeLayer(section.layer) || reader.layers.includes(section.layer);
1129
1129
  }
1130
+ function parentScope(tree, layer) {
1131
+ const k = layerKind(layer);
1132
+ if (k === "source") {
1133
+ const g = tree.sources[layerValue(layer)]?.group ?? null;
1134
+ return g && tree.groups.some((x) => x.id === g) ? groupLayer(g) : "org";
1135
+ }
1136
+ if (k === "group") {
1137
+ const g = tree.groups.find((x) => x.id === layerValue(layer));
1138
+ return g?.parent && tree.groups.some((x) => x.id === g.parent) ? groupLayer(g.parent) : "org";
1139
+ }
1140
+ return null;
1141
+ }
1142
+ function ancestorScopes(tree, layer) {
1143
+ const out = [];
1144
+ for (let cur = parentScope(tree, layer); cur && !out.includes(cur); cur = parentScope(tree, cur)) out.push(cur);
1145
+ return out;
1146
+ }
1130
1147
  function sourcesIn(tree, layer) {
1131
1148
  if (layer === "org") return Object.keys(tree.sources);
1132
1149
  if (layerKind(layer) === "source") return [layerValue(layer)];
@@ -1172,13 +1189,11 @@ function layerValue(layer) {
1172
1189
  const i = layer.indexOf(":");
1173
1190
  return i < 0 ? "" : layer.slice(i + 1);
1174
1191
  }
1192
+ function privateLayer(ctx) {
1193
+ return ctx.kind === "robot" ? `robot:${ctx.id}` : `user:${ctx.userId}`;
1194
+ }
1175
1195
  function layersFor(ctx) {
1176
- if (ctx.kind === "robot" && ctx.robot) {
1177
- return ["fleet", `arm:${ctx.robot.armModel}`, `mount:${ctx.robot.mount}`, `robot:${ctx.id}`];
1178
- }
1179
- const team = ctx.person?.team ?? "";
1180
- const dept = department(team);
1181
- return ["org", ...dept ? [`dept:${dept}`] : [], ...team ? [`team:${team}`] : [], `user:${ctx.userId}`];
1196
+ return ["org", privateLayer(ctx)];
1182
1197
  }
1183
1198
  function isLocalLayer(layer) {
1184
1199
  const kind = layerKind(layer);
@@ -1191,6 +1206,7 @@ function withScope(layers, scope) {
1191
1206
  return i < 0 ? [...layers, ...path28] : [...layers.slice(0, i), ...path28, ...layers.slice(i)];
1192
1207
  }
1193
1208
  function readerFor(ctx, ownerClearance, scope) {
1209
+ if (ctx.kind === "robot" && !scope?.source) scope = { ...scope, source: `robot:${ctx.id}` };
1194
1210
  const source = scope?.source ? { source: scope.source } : {};
1195
1211
  if (ctx.kind === "robot") {
1196
1212
  const rank = (c) => c === "private" ? 0 : CLEARANCE_RANK[c];
@@ -1213,9 +1229,6 @@ function explainSectionAccess(section, reader) {
1213
1229
  const kind = layerKind(section.layer);
1214
1230
  if (kind === "user" && section.layer !== `user:${reader.userId}`) return { allowed: false, reason: "other-user" };
1215
1231
  if (kind === "robot" && section.layer !== `robot:${reader.contextId}`) return { allowed: false, reason: "other-robot" };
1216
- if ((kind === "arm" || kind === "mount") && reader.role === "robot" && !reader.layers.includes(section.layer)) {
1217
- return { allowed: false, reason: "other-hardware" };
1218
- }
1219
1232
  const access = explainAccess(
1220
1233
  {
1221
1234
  clearance: section.clearance,
@@ -1236,42 +1249,25 @@ function scopeAffinity(section, reader) {
1236
1249
  function baseAffinity(section, reader) {
1237
1250
  const kind = layerKind(section.layer);
1238
1251
  if (kind === "group" || kind === "source") {
1239
- const i = reader.layers.indexOf(section.layer);
1240
- if (i < 0) return 0.05;
1252
+ if (!reader.layers.includes(section.layer)) return 0.05;
1241
1253
  const path28 = reader.layers.filter((l) => {
1242
1254
  const k = layerKind(l);
1243
1255
  return k === "group" || k === "source";
1244
1256
  });
1245
1257
  return 0.6 + 0.4 * ((path28.indexOf(section.layer) + 1) / path28.length);
1246
1258
  }
1247
- if (reader.layers.includes(section.layer)) {
1248
- const order = reader.role === "robot" ? FLEET_LAYER_ORDER : COMPANY_LAYER_ORDER;
1249
- return 0.5 + 0.5 * (order.indexOf(kind) / Math.max(1, order.length - 1));
1250
- }
1251
- if (kind === "team") {
1252
- const team = layerValue(section.layer);
1253
- if (department(team) === department(reader.team)) return 0.3;
1254
- return 0.05;
1255
- }
1256
- if (kind === "dept") return 0.1;
1257
- return 0.2;
1259
+ if (isLocalLayer(section.layer)) return reader.layers.includes(section.layer) ? 1 : 0;
1260
+ return 0.5;
1258
1261
  }
1259
1262
  function reachesReader(section, reader) {
1260
- if (reader.layers.includes(section.layer) || section.kind === "guardrail") return true;
1261
- const kind = layerKind(section.layer);
1262
- if (kind === "org" || kind === "dept" || FLEET_LAYER_ORDER.includes(kind)) return canRead(section, reader);
1263
- if (kind !== "team") return false;
1264
- return [layerValue(section.layer), ...section.allowedTeams ?? []].some((t) => !!t && orgUnitUnder(reader.team, t));
1263
+ return reader.layers.includes(section.layer) && canRead(section, reader);
1265
1264
  }
1266
- var COMPANY_LAYER_ORDER, FLEET_LAYER_ORDER;
1267
1265
  var init_layers = __esm({
1268
1266
  "../core/src/v2/layers.ts"() {
1269
1267
  "use strict";
1270
1268
  init_permissions();
1271
1269
  init_text();
1272
1270
  init_scopes();
1273
- COMPANY_LAYER_ORDER = ["org", "dept", "team", "user"];
1274
- FLEET_LAYER_ORDER = ["fleet", "arm", "mount", "robot"];
1275
1271
  }
1276
1272
  });
1277
1273
 
@@ -1455,6 +1451,9 @@ var init_document = __esm({
1455
1451
  });
1456
1452
 
1457
1453
  // ../core/src/v2/sections/markdown.ts
1454
+ function repoLayer(repo) {
1455
+ return `source:${repo.includes("/") ? "repo" : "folder"}:${repo}`;
1456
+ }
1458
1457
  function repoSectionId(repo, path28, anchor = "") {
1459
1458
  return anchor ? `repo:${repo}/${path28}#${anchor}` : `repo:${repo}/${path28}`;
1460
1459
  }
@@ -1487,7 +1486,7 @@ function parseRepoDoc(doc, opts) {
1487
1486
  id: meta.id ? repoSectionId(doc.repo, doc.path, meta.id) : repoSectionId(doc.repo, doc.path),
1488
1487
  orgId: opts.orgId,
1489
1488
  kind,
1490
- layer: `team:${owner}`,
1489
+ layer: opts.layer ?? repoLayer(doc.repo),
1491
1490
  title,
1492
1491
  body: doc.text,
1493
1492
  when,
@@ -2081,8 +2080,9 @@ var init_engine = __esm({
2081
2080
 
2082
2081
  // ../core/src/v2/guidelines.ts
2083
2082
  function reflectionGuidelines(sections, reader, opts = {}) {
2083
+ const covering = opts.layer ? /* @__PURE__ */ new Set(["org", opts.layer, ...opts.tree ? ancestorScopes(opts.tree, opts.layer) : []]) : void 0;
2084
2084
  const pool = reader ? servable(sections, reader) : sections;
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 }));
2085
+ return pool.filter((s) => s.delivery === "reflect" && s.status === "active" && (!reader || inBoundary(s, reader))).filter((s) => !covering || covering.has(s.layer)).sort((a, b) => generality(a.layer) - generality(b.layer) || a.id.localeCompare(b.id)).map((s) => ({ id: s.id, title: s.title, layer: s.layer, body: s.body }));
2086
2086
  }
2087
2087
  function guidelineRules(body) {
2088
2088
  const at = body.search(/^## /m);
@@ -2120,7 +2120,7 @@ var init_guidelines = __esm({
2120
2120
  init_engine();
2121
2121
  init_scopes();
2122
2122
  GUIDELINES_ANCHOR = "rules";
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;
2123
+ generality = (l) => ({ org: 0, group: 1, source: 2, robot: 3, user: 3 })[layerKind(l)] ?? 4;
2124
2124
  }
2125
2125
  });
2126
2126
 
@@ -2770,8 +2770,17 @@ A lesson is read by agents on other tasks, months from now. Write the rule, not
2770
2770
 
2771
2771
  Every entry is a whole document (a skill, a spec, a policy). Prefer editing an existing document
2772
2772
  over creating one, and aim the edit: "anchor" names the heading (from its headings list) the text
2773
- belongs under, so it lands in the right part of the document. Library documents relevant to this
2774
- session:
2773
+ belongs under, so it lands in the right part of the document.
2774
+
2775
+ Steering and approach lessons (a, e) are the exception: each is its own short lesson. Use op
2776
+ "create" with kind "rule": a title naming the behaviour, a "when" naming the situations it applies
2777
+ in (as the user would phrase a request there, even one that never mentions it), and a body of at
2778
+ most about 120 words: the rule, why, and when it was learned. Do not append such a lesson inside a
2779
+ long skill or procedure: a rule buried in a long document goes unfollowed even when the document is
2780
+ loaded, and the user ends up repeating it. If a short rule in the list below already says it, append
2781
+ to that rule instead. Traps, procedures and facts (b, c, d) still go into the document they belong to.
2782
+
2783
+ Library documents relevant to this session:
2775
2784
  ${index}
2776
2785
  ${input.skills?.length ? `
2777
2786
  Skills in the library (target any of them by id, listed here or not):
@@ -2791,9 +2800,9 @@ A fact, command or habit of this project \u2192 this project. True of every proj
2791
2800
  that group. True anywhere this workspace works \u2192 org. Lessons that come up again in other scopes
2792
2801
  are widened by the nightly generalization pass, so a narrow scope is never a loss. Name it as
2793
2802
  "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,
2795
- not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
2796
- A lesson for anyone doing the task belongs in the task's document instead.
2803
+ ` : ""}${input.profileId ? `- How THIS person works in every task alike (tone, length, how they like to be asked) \u2192
2804
+ op "append" to their profile ${input.profileId}, anchor "how-i-work". A preference that applies in
2805
+ one kind of situation (how they want PRs, reports, deploys) is a short rule lesson in their layer.
2797
2806
  ` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
2798
2807
  - Cross-repo facts, another team's rules, non-code processes \u2192 a Bunshin section at the right layer.
2799
2808
  - If it only holds in some contexts, use op "append" with a "condition" (e.g. "objects over 50 g").
@@ -3129,6 +3138,17 @@ var init_generalize = __esm({
3129
3138
  }
3130
3139
  });
3131
3140
 
3141
+ // ../core/src/v2/adopt.ts
3142
+ var init_adopt = __esm({
3143
+ "../core/src/v2/adopt.ts"() {
3144
+ "use strict";
3145
+ init_text();
3146
+ init_layers();
3147
+ init_scopes();
3148
+ init_sources();
3149
+ }
3150
+ });
3151
+
3132
3152
  // ../core/src/v2/index.ts
3133
3153
  var init_v2 = __esm({
3134
3154
  "../core/src/v2/index.ts"() {
@@ -3153,6 +3173,7 @@ var init_v2 = __esm({
3153
3173
  init_sources();
3154
3174
  init_scopes();
3155
3175
  init_generalize();
3176
+ init_adopt();
3156
3177
  }
3157
3178
  });
3158
3179
 
@@ -3329,7 +3350,8 @@ function indexWorkingTree(v2, config, cwd, opts = {}) {
3329
3350
  const now = opts.now ?? Date.now();
3330
3351
  const cached = v2.readRepoIndex(root);
3331
3352
  const ownerTeam = config.ownerTeam || config.orgUnit || "unknown";
3332
- const sameRepo = cached && cached.ownerTeam === ownerTeam;
3353
+ const source = sourceFor(root, v2.store.root);
3354
+ const sameRepo = cached && cached.ownerTeam === ownerTeam && cached.format === INDEX_FORMAT;
3333
3355
  const repo = sameRepo && cached.repo && !opts.force ? cached.repo : repoName(root);
3334
3356
  const fresh = sameRepo && cached.repo === repo && cached.paths && cached.listedAt && now - Date.parse(cached.listedAt) < LIST_TTL_MS && !opts.force;
3335
3357
  const paths = fresh ? cached.paths : listDocs(root);
@@ -3352,11 +3374,12 @@ function indexWorkingTree(v2, config, cwd, opts = {}) {
3352
3374
  changed = true;
3353
3375
  try {
3354
3376
  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 }) };
3377
+ files[rel] = { mtimeMs: stat.mtimeMs, size: stat.size, sections: parseRepoDoc({ repo, path: rel, text: text2 }, { orgId: config.orgId, ownerTeam, layer: source ? sourceLayer(source) : "org" }) };
3356
3378
  } catch {
3357
3379
  }
3358
3380
  }
3359
3381
  const index = {
3382
+ format: INDEX_FORMAT,
3360
3383
  repo,
3361
3384
  ownerTeam,
3362
3385
  listedAt: fresh ? cached.listedAt : new Date(now).toISOString(),
@@ -3397,11 +3420,12 @@ function relativeToRoot(root, file, cwd = root) {
3397
3420
  }
3398
3421
  return null;
3399
3422
  }
3400
- var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
3423
+ var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill, INDEX_FORMAT;
3401
3424
  var init_repo = __esm({
3402
3425
  "src/v2/repo.ts"() {
3403
3426
  "use strict";
3404
3427
  init_v22();
3428
+ init_source();
3405
3429
  DOC_NAMES = /* @__PURE__ */ new Set(["spec.md", "testing.md", "skill.md"]);
3406
3430
  SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".bunshin"]);
3407
3431
  LIST_TTL_MS = 6e4;
@@ -3412,6 +3436,7 @@ var init_repo = __esm({
3412
3436
  return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
3413
3437
  };
3414
3438
  deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs6.existsSync(path5.join(root, path5.posix.dirname(rel), ".bunshin.json"));
3439
+ INDEX_FORMAT = 2;
3415
3440
  }
3416
3441
  });
3417
3442
 
@@ -4381,6 +4406,11 @@ function childDir(root, name) {
4381
4406
  const dir = path8.join(root, name);
4382
4407
  return path8.dirname(path8.resolve(dir)) === path8.resolve(root) && insideRoot(root, dir) ? dir : null;
4383
4408
  }
4409
+ function layerRank(layer, tree) {
4410
+ const kind = layerKind(layer);
4411
+ if (kind === "group") return 1 + (tree ? groupChain(tree, layerValue(layer)).length : 0);
4412
+ return kind === "source" ? 1e3 : kind === "user" || kind === "robot" ? 2e3 : 0;
4413
+ }
4384
4414
  function planDeploy(bundle, p, harnesses) {
4385
4415
  const active = bundle.sections.filter((s) => s.status === "active" || s.status === "stale");
4386
4416
  const byName = /* @__PURE__ */ new Map();
@@ -4391,7 +4421,7 @@ function planDeploy(bundle, p, harnesses) {
4391
4421
  continue;
4392
4422
  }
4393
4423
  const prev = byName.get(s.skill.name);
4394
- if (!prev || (LAYER_RANK[layerKind(s.layer)] ?? 0) > (LAYER_RANK[layerKind(prev.layer)] ?? 0)) byName.set(s.skill.name, s);
4424
+ if (!prev || layerRank(s.layer, bundle.scopes) > layerRank(prev.layer, bundle.scopes)) byName.set(s.skill.name, s);
4395
4425
  }
4396
4426
  const skills = [...byName.values()].sort((a, b) => a.skill.name.localeCompare(b.skill.name));
4397
4427
  const instructions = active.filter((s) => s.delivery === "always" && !s.skill && !s.profileOf).sort((a, b) => (a.packPath ?? a.title).localeCompare(b.packPath ?? b.title));
@@ -5013,7 +5043,7 @@ ${EXCLUDE_END}
5013
5043
  } catch {
5014
5044
  }
5015
5045
  }
5016
- var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, LAYER_RANK, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
5046
+ var settingsFile, orgKey, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, safeJoin, SAFE_NAME, neutralizeMarkers, docMarker, BORN_SLACK_MS, LINK_DIRS, EXCLUDE_BEGIN, EXCLUDE_END;
5017
5047
  var init_deploy = __esm({
5018
5048
  "src/v2/deploy.ts"() {
5019
5049
  "use strict";
@@ -5038,7 +5068,6 @@ var init_deploy = __esm({
5038
5068
  safeJoin = containedPath;
5039
5069
  SAFE_NAME = /^(?!\.\.?$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
5040
5070
  neutralizeMarkers = (text2) => text2.replace(/<!--(?=\s*bunshin:)/gi, "<!-\u200B-");
5041
- LAYER_RANK = { org: 0, dept: 1, group: 2, team: 2, source: 3, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
5042
5071
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
5043
5072
  BORN_SLACK_MS = 2e3;
5044
5073
  LINK_DIRS = {
@@ -6129,7 +6158,8 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
6129
6158
  }
6130
6159
  });
6131
6160
  const commit = repoCommit(root);
6132
- const res = await v2api(config, "POST", "/sections/index", { repo: index.repo, ...commit ? { commit } : {}, ownerTeam: index.ownerTeam, docs }, { deadline, timeoutMs: 3e4 });
6161
+ const source = sourceFor(root, store.root);
6162
+ const res = await v2api(config, "POST", "/sections/index", { repo: index.repo, ...commit ? { commit } : {}, ...source ? { source } : {}, ownerTeam: index.ownerTeam, docs }, { deadline, timeoutMs: 3e4 });
6133
6163
  return typeof res.indexed === "number" ? res.indexed : docs.length;
6134
6164
  }
6135
6165
  var isLocalId, carriesTrace;
@@ -6857,13 +6887,42 @@ function skillHint(sec, others) {
6857
6887
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6858
6888
  return `Bunshin: the \`${sec.skill.name}\` skill may apply: ${sec.skill.description.replace(/\s+/g, " ").trim()} If it does, invoke it (Skill tool, or \`/${sec.skill.name}\`) before doing this by hand.${also}`;
6859
6889
  }
6860
- async function remoteAuto(ctx, prompt, exclude, source) {
6890
+ function lastReply(transcriptPath, maxBytes = 256e3) {
6891
+ if (!transcriptPath) return void 0;
6892
+ try {
6893
+ const fd = fs18.openSync(transcriptPath, "r");
6894
+ try {
6895
+ const size = fs18.fstatSync(fd).size;
6896
+ const len = Math.min(size, maxBytes);
6897
+ const buf = Buffer.alloc(len);
6898
+ fs18.readSync(fd, buf, 0, len, size - len);
6899
+ const lines = buf.toString("utf8").split("\n");
6900
+ for (let i = lines.length - 1; i >= 0; i--) {
6901
+ let m;
6902
+ try {
6903
+ m = JSON.parse(lines[i]);
6904
+ } catch {
6905
+ continue;
6906
+ }
6907
+ if (m?.type !== "assistant" || m.isSidechain) continue;
6908
+ const c = m.message?.content;
6909
+ const text2 = Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("\n") : typeof c === "string" ? c : "";
6910
+ if (text2.trim()) return redact2(text2.trim()).slice(0, 1500);
6911
+ }
6912
+ } finally {
6913
+ fs18.closeSync(fd);
6914
+ }
6915
+ } catch {
6916
+ }
6917
+ return void 0;
6918
+ }
6919
+ async function remoteAuto(ctx, prompt, exclude, source, context) {
6861
6920
  const config = ctx.store.config();
6862
6921
  if (!canCall(config) || !config.recall.useMothership) return null;
6863
6922
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6864
6923
  if (budget < 300) return null;
6865
6924
  try {
6866
- const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {} }, { timeoutMs: budget });
6925
+ const res = await v2api(config, "POST", "/recall/auto", { prompt: redact2(prompt).slice(0, 8e3), exclude, ...source ? { source } : {}, ...context ? { context } : {} }, { timeoutMs: budget });
6867
6926
  return res.available ? res : null;
6868
6927
  } catch {
6869
6928
  return null;
@@ -6887,6 +6946,7 @@ async function onPrompt(ctx) {
6887
6946
  const now = (/* @__PURE__ */ new Date()).toISOString();
6888
6947
  const session = touch(v2, ctx, (s) => {
6889
6948
  if (!s.task && prompt.trim()) s.task = redact2(prompt.trim()).slice(0, 2e3);
6949
+ if (s.lastPrompt && prompt.trim()) s.recentPrompts = [...s.recentPrompts ?? [], s.lastPrompt.slice(0, 600)].slice(-3);
6890
6950
  s.lastPrompt = redact2(prompt).slice(0, 4e3);
6891
6951
  s.lastActivityAt = now;
6892
6952
  });
@@ -6918,19 +6978,45 @@ async function onPrompt(ctx) {
6918
6978
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6919
6979
  }
6920
6980
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6921
- const remote = await remoteAuto(ctx, prompt, injected, session.source);
6981
+ const recent = session.recentPrompts ?? [];
6982
+ const context = {
6983
+ ...recent.length && session.task ? { first: session.task.slice(0, 1e3) } : {},
6984
+ ...recent.length ? { recent } : {}
6985
+ };
6986
+ const reply = recent.length ? lastReply(ctx.input.transcript_path ?? session.transcriptPath) : void 0;
6987
+ if (reply) context.reply = reply;
6988
+ const remote = await remoteAuto(ctx, prompt, injected, session.source, Object.keys(context).length ? context : void 0);
6989
+ const local = (id) => view.sections.find((s) => s.id === id);
6922
6990
  if (remote) {
6923
- const local = (id) => view.sections.find((s) => s.id === id);
6991
+ const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
6924
6992
  if (remote.section) {
6925
6993
  res.section = local(remote.section.id) ?? remote.section;
6926
6994
  res.anchor = void 0;
6927
- res.reason = `dense ${remote.reason ?? ""}`.trim();
6995
+ res.reason = why;
6928
6996
  } else {
6929
6997
  res.section = void 0;
6930
6998
  res.hint = (remote.hint ?? []).map((h) => ({ id: h.id, title: local(h.id)?.title ?? h.title }));
6931
- res.reason = `dense ${remote.reason ?? ""}`.trim();
6999
+ res.reason = why;
6932
7000
  }
6933
7001
  }
7002
+ const rules = (remote?.rules ?? []).filter((r) => !injected.includes(r.id) && r.id !== res.section?.id).map((r) => local(r.id) ?? r);
7003
+ let lessons = "";
7004
+ if (rules.length) {
7005
+ v2.updateSession(session.sessionId, ctx.cwd, (s) => {
7006
+ for (const r of rules) {
7007
+ addUnique(s.served, r.id);
7008
+ addUnique(s.loaded, r.id);
7009
+ markInContext(s, r.id);
7010
+ }
7011
+ });
7012
+ v2.log("v2-auto-rules", { session: session.sessionId, sections: rules.map((r) => r.id), reason: res.reason });
7013
+ updateFeed(v2, session.sessionId, (f) => {
7014
+ for (const r of [...rules].reverse()) addServed(f, { id: r.id, title: r.title, kind: r.kind, how: "auto", why: "a lesson learned before applies here" });
7015
+ addEvent(f, "served", `lessons: ${rules.map((r) => r.title).join(", ")}`);
7016
+ });
7017
+ lessons = rules.map((r) => renderSection(r, "a lesson learned before applies to this work", { deployed: false })).join("\n\n");
7018
+ }
7019
+ const withLessons = (text2) => withLive(additional("UserPromptSubmit", [text2, lessons].filter(Boolean).join("\n\n")));
6934
7020
  if (res.section) {
6935
7021
  const sec = res.section;
6936
7022
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
@@ -6944,10 +7030,10 @@ async function onPrompt(ctx) {
6944
7030
  addServed(f, { id: sec.id, title: sec.title, kind: sec.kind, ...sec.skill ? { skill: sec.skill.name } : {}, how: "auto", why });
6945
7031
  addEvent(f, "served", `served ${label({ ...sec.skill ? { skill: sec.skill.name } : {}, title: sec.title, kind: sec.kind })} \u2014 ${why}`);
6946
7032
  });
6947
- return withLive(additional("UserPromptSubmit", renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) })));
7033
+ return withLessons(renderSection(sec, "your prompt matches it", { ...res.anchor ? { anchor: res.anchor } : {}, deployed: !!sec.skill && deployEnabled(config, view.ctx) }));
6948
7034
  }
6949
- const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
6950
- if (!fresh.length) return withLive({});
7035
+ const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id) && !rules.some((r) => r.id === h.id)).slice(0, 3);
7036
+ if (!fresh.length) return withLessons("");
6951
7037
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
6952
7038
  s.hinted = s.hinted ?? [];
6953
7039
  addUnique(s.hinted, ...fresh.map((h) => h.id));
@@ -6961,8 +7047,8 @@ async function onPrompt(ctx) {
6961
7047
  addEvent(f, "hint", `possibly relevant: ${fresh.map((h) => h.title).join(", ")}`);
6962
7048
  });
6963
7049
  const topSkill = view.sections.find((s) => s.id === fresh[0].id && s.skill);
6964
- if (topSkill && deployEnabled(config, view.ctx)) return withLive(additional("UserPromptSubmit", skillHint(topSkill, fresh.slice(1).map((h) => h.title))));
6965
- return withLive(additional("UserPromptSubmit", hintLine(fresh.map((h) => h.title), config.harness)));
7050
+ if (topSkill && deployEnabled(config, view.ctx)) return withLessons(skillHint(topSkill, fresh.slice(1).map((h) => h.title)));
7051
+ return withLessons(hintLine(fresh.map((h) => h.title), config.harness));
6966
7052
  }
6967
7053
  function toolPaths(input) {
6968
7054
  if (!input || typeof input !== "object") return [];
@@ -8030,7 +8116,7 @@ function cmdSuggest(store, args, cwd, out, err) {
8030
8116
  orgId: config.orgId,
8031
8117
  contextId: view.ctx.id,
8032
8118
  authorUserId: config.userId,
8033
- target: known ? { sectionId: known } : { layer: "fleet", storage: { type: "harness", path: rel, symbol: symbol ?? "" } },
8119
+ target: known ? { sectionId: known } : { storage: { type: "harness", path: rel, symbol: symbol ?? "" } },
8034
8120
  op: { op: "append", text: rationale },
8035
8121
  rationale,
8036
8122
  evidenceEpisodeIds: sessionId ? [episodeIdFor(view.ctx.id, sessionId)] : [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {