@bunshin-cc/bunshin 0.8.1 → 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 +81 -16
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -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").
@@ -6878,13 +6887,42 @@ function skillHint(sec, others) {
6878
6887
  const also = others.length ? ` Also possibly relevant: ${others.map((t) => `"${t}"`).join(", ")}.` : "";
6879
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}`;
6880
6889
  }
6881
- 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) {
6882
6920
  const config = ctx.store.config();
6883
6921
  if (!canCall(config) || !config.recall.useMothership) return null;
6884
6922
  const budget = Math.min(2500, ctx.deadline - Date.now() - 600);
6885
6923
  if (budget < 300) return null;
6886
6924
  try {
6887
- 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 });
6888
6926
  return res.available ? res : null;
6889
6927
  } catch {
6890
6928
  return null;
@@ -6908,6 +6946,7 @@ async function onPrompt(ctx) {
6908
6946
  const now = (/* @__PURE__ */ new Date()).toISOString();
6909
6947
  const session = touch(v2, ctx, (s) => {
6910
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);
6911
6950
  s.lastPrompt = redact2(prompt).slice(0, 4e3);
6912
6951
  s.lastActivityAt = now;
6913
6952
  });
@@ -6939,19 +6978,45 @@ async function onPrompt(ctx) {
6939
6978
  return withLive(deployed || !fresh2 ? {} : additional("UserPromptSubmit", renderSection(named, why, { deployed: false })));
6940
6979
  }
6941
6980
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
6942
- 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);
6943
6990
  if (remote) {
6944
- const local = (id) => view.sections.find((s) => s.id === id);
6991
+ const why = remote.router ? remote.reason ?? `router ${remote.router}` : `dense ${remote.reason ?? ""}`.trim();
6945
6992
  if (remote.section) {
6946
6993
  res.section = local(remote.section.id) ?? remote.section;
6947
6994
  res.anchor = void 0;
6948
- res.reason = `dense ${remote.reason ?? ""}`.trim();
6995
+ res.reason = why;
6949
6996
  } else {
6950
6997
  res.section = void 0;
6951
6998
  res.hint = (remote.hint ?? []).map((h) => ({ id: h.id, title: local(h.id)?.title ?? h.title }));
6952
- res.reason = `dense ${remote.reason ?? ""}`.trim();
6999
+ res.reason = why;
6953
7000
  }
6954
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")));
6955
7020
  if (res.section) {
6956
7021
  const sec = res.section;
6957
7022
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
@@ -6965,10 +7030,10 @@ async function onPrompt(ctx) {
6965
7030
  addServed(f, { id: sec.id, title: sec.title, kind: sec.kind, ...sec.skill ? { skill: sec.skill.name } : {}, how: "auto", why });
6966
7031
  addEvent(f, "served", `served ${label({ ...sec.skill ? { skill: sec.skill.name } : {}, title: sec.title, kind: sec.kind })} \u2014 ${why}`);
6967
7032
  });
6968
- 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) }));
6969
7034
  }
6970
- const fresh = res.hint.filter((h) => !session.hinted?.includes(h.id)).slice(0, 3);
6971
- 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("");
6972
7037
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
6973
7038
  s.hinted = s.hinted ?? [];
6974
7039
  addUnique(s.hinted, ...fresh.map((h) => h.id));
@@ -6982,8 +7047,8 @@ async function onPrompt(ctx) {
6982
7047
  addEvent(f, "hint", `possibly relevant: ${fresh.map((h) => h.title).join(", ")}`);
6983
7048
  });
6984
7049
  const topSkill = view.sections.find((s) => s.id === fresh[0].id && s.skill);
6985
- if (topSkill && deployEnabled(config, view.ctx)) return withLive(additional("UserPromptSubmit", skillHint(topSkill, fresh.slice(1).map((h) => h.title))));
6986
- 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));
6987
7052
  }
6988
7053
  function toolPaths(input) {
6989
7054
  if (!input || typeof input !== "object") return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.8.1",
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": {