@bunshin-cc/bunshin 0.4.5 → 0.4.6

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 +100 -26
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -431,9 +431,11 @@ var init_store = __esm({
431
431
  function emptyStats() {
432
432
  return { served: 0, loaded: 0, helped: 0, hurt: 0, byContext: {} };
433
433
  }
434
+ var LEARNING_SIGNALS;
434
435
  var init_types = __esm({
435
436
  "../core/src/v2/types.ts"() {
436
437
  "use strict";
438
+ LEARNING_SIGNALS = ["steering", "environment", "knowledge", "capability", "approach"];
437
439
  }
438
440
  });
439
441
 
@@ -1521,7 +1523,8 @@ function renderSection(s, trigger, opts = {}) {
1521
1523
  const label2 = s.kind === "guardrail" ? "GUARDRAIL" : s.kind.toUpperCase();
1522
1524
  const src = sourceLabel(s);
1523
1525
  const why = trigger ? `_Why you are seeing this: ${trigger}. Id: ${s.id}_` : `_Id: ${s.id}_`;
1524
- const head = [`### ${label2} (${src}): ${s.title}`, why];
1526
+ const trial = s.localTrial?.length ? [`_Includes ${s.localTrial.length === 1 ? "an edit" : `${s.localTrial.length} edits`} learned on this machine and not yet reviewed: treat ${s.localTrial.length === 1 ? "it" : "them"} as provisional._`] : [];
1527
+ const head = [`### ${label2} (${src}): ${s.title}`, why, ...trial];
1525
1528
  const max = opts.maxChars ?? 6e3;
1526
1529
  if (s.skill && opts.deployed) {
1527
1530
  const part2 = opts.anchor ? (s.outline ?? []).find((h) => h.anchor === opts.anchor) : void 0;
@@ -1726,9 +1729,20 @@ Lessons already taken from it. Never propose these again, nor a rewording of the
1726
1729
  ${lessons}
1727
1730
  `;
1728
1731
  }
1732
+ function stampLesson(op, signal, at) {
1733
+ const day = at.slice(0, 10);
1734
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return op;
1735
+ const recheck = signal === "environment" ? `; recheck after ${new Date(Date.parse(day) + ENVIRONMENT_RECHECK_DAYS * 864e5).toISOString().slice(0, 10)}` : "";
1736
+ const stamp = `_(learned ${day}${signal ? ` from ${SIGNAL_WORDS[signal]}` : ""}${recheck})_`;
1737
+ if (op.op === "append") return /_\(learned \d{4}-/.test(op.text) ? op : { ...op, text: `${op.text.trimEnd()} ${stamp}` };
1738
+ if (op.op === "create") return /_\(learned \d{4}-/.test(op.section.body) ? op : { ...op, section: { ...op.section, body: `${op.section.body.trimEnd()}
1739
+
1740
+ ${stamp}` } };
1741
+ return op;
1742
+ }
1729
1743
  function summarizeOp(op) {
1730
1744
  const clip2 = (s) => {
1731
- const one = s.replace(/\s+/g, " ").trim();
1745
+ const one = s.replace(/\s*_\(learned \d{4}-\d{2}-\d{2}[^)]*\)_/g, "").replace(/\s+/g, " ").trim();
1732
1746
  return one.length > 100 ? `${one.slice(0, 97)}\u2026` : one;
1733
1747
  };
1734
1748
  switch (op.op) {
@@ -1783,8 +1797,14 @@ Capture only what would change how a future agent acts, and is recorded nowhere
1783
1797
  b. the environment corrected the agent (a command, test, API, motion or grasp failed, then
1784
1798
  another approach worked): record the trap AND what worked;
1785
1799
  c. knowledge owned by another team surfaced (a process, policy or manual step);
1786
- d. a capability worked for the first time, or a task was done unusually efficiently.
1800
+ d. a capability worked for the first time, or a task was done unusually efficiently;
1801
+ e. looking back over the whole session, the approach itself was the problem (it took many
1802
+ rounds of fixes, it was over-built, it verified too late) and a different way of working
1803
+ would have avoided it. This has no direct ground truth: say what you would do differently
1804
+ next time, as generally as it truly holds, and where it does not hold.
1787
1805
  "Nothing new" is the expected outcome for most sessions. Never invent a lesson.
1806
+ Label each suggestion with its "signal": a \u2192 "steering", b \u2192 "environment", c \u2192 "knowledge",
1807
+ d \u2192 "capability", e \u2192 "approach".
1788
1808
 
1789
1809
  ## Where it goes
1790
1810
 
@@ -1797,7 +1817,10 @@ ${index}
1797
1817
  Already captured this session (do not repeat): ${input.captured.length ? input.captured.join(", ") : "none"}
1798
1818
 
1799
1819
  Layers you may target: ${input.layers.join(", ")}.
1800
- - Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
1820
+ ${input.profileId ? `- How THIS person works, prefers or wants things done (a steering or approach lesson about them,
1821
+ not about the task) \u2192 op "append" to their profile ${input.profileId}, anchor "how-i-work".
1822
+ A lesson for anyone doing the task belongs in the task's document instead.
1823
+ ` : ""}- Knowledge about one repo that its team owns \u2192 that repo's section (repo:... ids).
1801
1824
  - Cross-repo facts, another team's rules, non-code processes \u2192 a Bunshin section at the right layer.
1802
1825
  - If it only holds in some contexts, use op "append" with a "condition" (e.g. "objects over 50 g").
1803
1826
  ${robot ? `- Values that belong to THIS robot or cell (calibration, pixel columns, colour thresholds under this
@@ -1824,7 +1847,8 @@ At most ${max} suggestions. Schema:
1824
1847
  "paths"?: string[], "entities"?: string[], "skills"?: string[]}}
1825
1848
  | {"op": "param", "key": string, "value": string|number|boolean}
1826
1849
  | {"op": "situation", "text": string},
1827
- "rationale": string, "urgency": "normal"|"urgent"}]}`;
1850
+ "rationale": string, "urgency": "normal"|"urgent",
1851
+ "signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
1828
1852
  }
1829
1853
  function extractJson(text2) {
1830
1854
  const fenced = text2.match(/```(?:json)?\s*([\s\S]*?)```/);
@@ -1900,7 +1924,8 @@ function parseReflection(raw, opts = { layers: [] }) {
1900
1924
  target: { ...sectionId ? { sectionId } : {}, ...layer ? { layer } : {} },
1901
1925
  op,
1902
1926
  rationale: clean(it.rationale) || "captured from session",
1903
- urgency: it.urgency === "urgent" ? "urgent" : "normal"
1927
+ urgency: it.urgency === "urgent" ? "urgent" : "normal",
1928
+ ...LEARNING_SIGNALS.includes(it.signal) ? { signal: it.signal } : {}
1904
1929
  });
1905
1930
  if (out.length >= (opts.max ?? 4)) break;
1906
1931
  }
@@ -1913,8 +1938,9 @@ function toSuggestion(p, base) {
1913
1938
  contextId: base.contextId,
1914
1939
  authorUserId: base.authorUserId,
1915
1940
  target: p.target,
1916
- op: p.op,
1941
+ op: stampLesson(p.op, p.signal, base.createdAt),
1917
1942
  rationale: p.rationale,
1943
+ ...p.signal ? { signal: p.signal } : {},
1918
1944
  evidenceEpisodeIds: base.episodeId ? [base.episodeId] : [],
1919
1945
  trial: { uses: 0, successes: 0, failures: 0 },
1920
1946
  origin: base.origin,
@@ -1923,12 +1949,21 @@ function toSuggestion(p, base) {
1923
1949
  createdAt: base.createdAt
1924
1950
  };
1925
1951
  }
1926
- var REFLECT_MARKER, REFLECT_SCHEMA, KINDS, anchorOf, clean, strList;
1952
+ var REFLECT_MARKER, ENVIRONMENT_RECHECK_DAYS, SIGNAL_WORDS, REFLECT_SCHEMA, KINDS, anchorOf, clean, strList;
1927
1953
  var init_reflect = __esm({
1928
1954
  "../core/src/v2/reflect.ts"() {
1929
1955
  "use strict";
1930
1956
  init_redact();
1957
+ init_types();
1931
1958
  REFLECT_MARKER = "BUNSHIN:reflect";
1959
+ ENVIRONMENT_RECHECK_DAYS = 90;
1960
+ SIGNAL_WORDS = {
1961
+ steering: "a person's correction",
1962
+ environment: "the environment",
1963
+ knowledge: "another team's knowledge",
1964
+ capability: "a first success",
1965
+ approach: "reflection on the approach"
1966
+ };
1932
1967
  REFLECT_SCHEMA = {
1933
1968
  type: "object",
1934
1969
  additionalProperties: false,
@@ -1944,7 +1979,8 @@ var init_reflect = __esm({
1944
1979
  target: { type: "object", properties: { sectionId: { type: "string" }, layer: { type: "string" } } },
1945
1980
  op: { type: "object" },
1946
1981
  rationale: { type: "string" },
1947
- urgency: { type: "string", enum: ["normal", "urgent"] }
1982
+ urgency: { type: "string", enum: ["normal", "urgent"] },
1983
+ signal: { type: "string", enum: ["steering", "environment", "knowledge", "capability", "approach"] }
1948
1984
  }
1949
1985
  }
1950
1986
  }
@@ -2003,6 +2039,14 @@ var init_research = __esm({
2003
2039
  }
2004
2040
  });
2005
2041
 
2042
+ // ../core/src/v2/profile.ts
2043
+ var init_profile = __esm({
2044
+ "../core/src/v2/profile.ts"() {
2045
+ "use strict";
2046
+ init_types();
2047
+ }
2048
+ });
2049
+
2006
2050
  // ../core/src/v2/index.ts
2007
2051
  var init_v2 = __esm({
2008
2052
  "../core/src/v2/index.ts"() {
@@ -2019,6 +2063,7 @@ var init_v2 = __esm({
2019
2063
  init_reconcile();
2020
2064
  init_release();
2021
2065
  init_research();
2066
+ init_profile();
2022
2067
  }
2023
2068
  });
2024
2069
 
@@ -2682,7 +2727,7 @@ function planDeploy(bundle, p, harnesses) {
2682
2727
  if (!prev || (LAYER_RANK[layerKind(s.layer)] ?? 0) > (LAYER_RANK[layerKind(prev.layer)] ?? 0)) byName.set(s.skill.name, s);
2683
2728
  }
2684
2729
  const skills = [...byName.values()].sort((a, b) => a.skill.name.localeCompare(b.skill.name));
2685
- const instructions = active.filter((s) => s.delivery === "always" && !s.skill).sort((a, b) => (a.packPath ?? a.title).localeCompare(b.packPath ?? b.title));
2730
+ const instructions = active.filter((s) => s.delivery === "always" && !s.skill && !s.profileOf).sort((a, b) => (a.packPath ?? a.title).localeCompare(b.packPath ?? b.title));
2686
2731
  const plan = { docs: /* @__PURE__ */ new Map(), instructions, skills };
2687
2732
  const roots = [...harnesses.has("claude-code") ? [p.claudeSkills] : [], ...harnesses.has("codex") ? [p.codexSkills] : []];
2688
2733
  for (const s of skills) {
@@ -3197,13 +3242,25 @@ function baselineOf(bundle) {
3197
3242
  }
3198
3243
  function renderInstructions(bundle) {
3199
3244
  const docs = instructionDocs(bundle);
3200
- if (!docs.length) return "";
3201
- return [
3202
- `## Standing instructions (your organization, from Bunshin ${lib(bundle?.libraryVersion)})`,
3203
- "Follow these as you would CLAUDE.md. Bunshin re-delivers them after /clear and compaction, and tells you when they change.",
3204
- "",
3205
- ...docs.map((d) => d.body.trim())
3206
- ].join("\n");
3245
+ const profiles = docs.filter((d) => d.profileOf);
3246
+ const standing = docs.filter((d) => !d.profileOf);
3247
+ const parts = [];
3248
+ if (profiles.length) {
3249
+ parts.push(
3250
+ ["## Who you work for (Bunshin profile)", "Bunshin keeps this current from their sessions and corrections, and tells you when it changes.", "", ...profiles.map((d) => d.body.trim())].join("\n")
3251
+ );
3252
+ }
3253
+ if (standing.length) {
3254
+ parts.push(
3255
+ [
3256
+ `## Standing instructions (your organization, from Bunshin ${lib(bundle?.libraryVersion)})`,
3257
+ "Follow these as you would CLAUDE.md. Bunshin re-delivers them after /clear and compaction, and tells you when they change.",
3258
+ "",
3259
+ ...standing.map((d) => d.body.trim())
3260
+ ].join("\n")
3261
+ );
3262
+ }
3263
+ return parts.join("\n\n");
3207
3264
  }
3208
3265
  function lineDiff(before, after, max = MAX_DIFF_LINES) {
3209
3266
  const a = before.split("\n");
@@ -3237,11 +3294,11 @@ function changesSince(base, bundle, skillPath) {
3237
3294
  const old = base.instructions[d.id];
3238
3295
  if (old === d.body) continue;
3239
3296
  if (old === void 0) {
3240
- lines.push(`New standing instructions ("${d.title}"):`, d.body.trim(), "");
3297
+ lines.push(d.profileOf ? `The profile of the person you work for ("${d.title}"):` : `New standing instructions ("${d.title}"):`, d.body.trim(), "");
3241
3298
  continue;
3242
3299
  }
3243
3300
  const diff = lineDiff(old, d.body);
3244
- if (diff.length) lines.push(`Standing instructions changed ("${d.title}"); follow the new text:`, ...diff, "");
3301
+ if (diff.length) lines.push(d.profileOf ? `The profile of the person you work for changed ("${d.title}"):` : `Standing instructions changed ("${d.title}"); follow the new text:`, ...diff, "");
3245
3302
  }
3246
3303
  for (const id of Object.keys(base.instructions)) if (!now.some((d) => d.id === id)) lines.push(`Standing instructions withdrawn (${id}): stop following them.`, "");
3247
3304
  const skillLines = [];
@@ -3707,13 +3764,13 @@ function applyOverlays(sections, overlays, ctx) {
3707
3764
  if (!ACTIVE_OVERLAY.has(o.status) || o.kind !== "local-section") continue;
3708
3765
  if (o.op.op === "create") {
3709
3766
  const id = o.sectionId || localSectionId(o.suggestionId);
3710
- sections.set(id, draftToSection(id, o.op.section, ctx, ownLayer(ctx), o.appliedAt));
3767
+ sections.set(id, { ...draftToSection(id, o.op.section, ctx, ownLayer(ctx), o.appliedAt), localTrial: [o.suggestionId] });
3711
3768
  continue;
3712
3769
  }
3713
3770
  const base = o.sectionId ? sections.get(o.sectionId) : void 0;
3714
3771
  if (!base) continue;
3715
3772
  const res = applyOpToBody(base.body, o.op);
3716
- if (res.ok && res.text !== base.body) sections.set(base.id, { ...base, body: res.text, updatedAt: o.appliedAt });
3773
+ if (res.ok && res.text !== base.body) sections.set(base.id, { ...base, body: res.text, updatedAt: o.appliedAt, localTrial: [...base.localTrial ?? [], o.suggestionId] });
3717
3774
  }
3718
3775
  }
3719
3776
  function localView(v2, config, cwd, opts = {}) {
@@ -4482,14 +4539,18 @@ async function runReflectionV2(store, opts) {
4482
4539
  const episodeId = episodeIdFor(view.ctx.id, opts.sessionId);
4483
4540
  const prior = session.suggestionIds.map((id) => v2.readSuggestion(id)).filter((s) => !!s);
4484
4541
  const captured = [.../* @__PURE__ */ new Set([...session.captured, ...prior.map((s) => s.target.sectionId ?? localSectionId(s.id))])];
4485
- const index = reflectionIndex(view, [...session.loaded, ...session.pushed, ...session.served], session.task, opts.focus, humanTurnsOf(session.transcriptPath));
4542
+ const profile = view.sections.find((s) => s.profileOf && s.profileOf === config.userId && s.status === "active");
4543
+ const index = [
4544
+ ...profile ? [{ id: profile.id, kind: profile.kind, layer: profile.layer, title: profile.title, when: "how the person you work for works", ...profile.outline?.length ? { outline: profile.outline } : {} }] : [],
4545
+ ...reflectionIndex(view, [...session.loaded, ...session.pushed, ...session.served], session.task, opts.focus, humanTurnsOf(session.transcriptPath)).filter((s) => s.id !== profile?.id)
4546
+ ];
4486
4547
  const harness = session.harness ?? config.harness;
4487
4548
  const facts = readFacts(session.transcriptPath);
4488
4549
  const turns = facts?.turns.length ?? 0;
4489
4550
  const from = reflectCursor(session, turns);
4490
4551
  const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
4491
4552
  const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
4492
- const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {} };
4553
+ const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {}, ...profile ? { profileId: profile.id } : {} };
4493
4554
  let raw = null;
4494
4555
  if (harness === "claude-code") {
4495
4556
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd });
@@ -4784,6 +4845,13 @@ var init_uicmd = __esm({
4784
4845
  // src/v2/hooks.ts
4785
4846
  import fs14 from "node:fs";
4786
4847
  import path11 from "node:path";
4848
+ function inContext(s) {
4849
+ return s.inContext ?? [...s.loaded, ...s.pushed];
4850
+ }
4851
+ function markInContext(s, ...ids) {
4852
+ s.inContext = s.inContext ?? [...s.loaded, ...s.pushed];
4853
+ addUnique(s.inContext, ...ids);
4854
+ }
4787
4855
  function touch(v2, ctx, patch = () => {
4788
4856
  }) {
4789
4857
  const id = ctx.input.session_id || "no-session";
@@ -4852,6 +4920,7 @@ function liveSummary(text2) {
4852
4920
  for (const m of text2.matchAll(/- `([^`]+)` updated \((v\d+ → v\d+)\)/g)) items.push(`${m[1]} ${m[2]}`);
4853
4921
  for (const m of text2.matchAll(/- `([^`]+)` was retired/g)) items.push(`${m[1]} retired`);
4854
4922
  if (/Standing instructions (changed|withdrawn)|New standing instructions/.test(text2)) items.push("standing instructions changed");
4923
+ if (/The profile of the person you work for/.test(text2)) items.push("your profile");
4855
4924
  return `library updated${range ? ` (${range[2]})` : ""}: ${items.join(", ") || "changes delivered to the agent"}`;
4856
4925
  }
4857
4926
  async function dispatch(event, ctx) {
@@ -4996,7 +5065,7 @@ async function onPrompt(ctx) {
4996
5065
  if (!prompt.trim()) return withLive({});
4997
5066
  const config = ctx.store.config();
4998
5067
  const view = localView(v2, config, ctx.cwd);
4999
- const injected = [...session.loaded, ...session.pushed];
5068
+ const injected = inContext(session);
5000
5069
  const res = auto(view.sections, view.reader, prompt, { exclude: injected });
5001
5070
  const remote = await remoteAuto(ctx, prompt, injected);
5002
5071
  if (remote) {
@@ -5016,6 +5085,7 @@ async function onPrompt(ctx) {
5016
5085
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
5017
5086
  addUnique(s.served, sec.id);
5018
5087
  addUnique(s.loaded, sec.id);
5088
+ markInContext(s, sec.id);
5019
5089
  });
5020
5090
  v2.log("v2-auto", { session: session.sessionId, section: sec.id, reason: res.reason });
5021
5091
  updateFeed(v2, session.sessionId, (f) => {
@@ -5069,13 +5139,14 @@ function onPreToolUse(ctx) {
5069
5139
  const paths = raw.map((p) => relativeToRoot(view.root, p, ctx.cwd)).filter((p) => !!p);
5070
5140
  const text2 = [session.task, session.lastPrompt].filter(Boolean).join("\n");
5071
5141
  if (!paths.length && !text2) return withLive({});
5072
- const seen = /* @__PURE__ */ new Set([...session.pushed, ...session.loaded]);
5142
+ const seen = new Set(inContext(session));
5073
5143
  const hits = push(view.sections, view.reader, { paths, text: text2 }).filter((h) => !seen.has(h.section.id)).slice(0, MAX_PUSH);
5074
5144
  if (!hits.length) return withLive({});
5075
5145
  const ids = hits.map((h) => h.section.id);
5076
5146
  v2.updateSession(session.sessionId, ctx.cwd, (s) => {
5077
5147
  addUnique(s.pushed, ...ids);
5078
5148
  addUnique(s.loaded, ...ids);
5149
+ markInContext(s, ...ids);
5079
5150
  addUnique(s.served, ...ids);
5080
5151
  });
5081
5152
  v2.log("v2-push", { session: session.sessionId, tool: ctx.input.tool_name, hits: hits.map((h) => `${h.section.id} ${h.trigger}`) });
@@ -5278,7 +5349,10 @@ function onSessionEnd(ctx) {
5278
5349
  function onPreCompact(ctx) {
5279
5350
  const v2 = new V2Store(ctx.store);
5280
5351
  const config = ctx.store.config();
5281
- const session = touch(v2, ctx);
5352
+ const session = touch(v2, ctx, (s) => {
5353
+ s.inContext = [];
5354
+ s.hinted = [];
5355
+ });
5282
5356
  if (!config.reflect.enabled) return {};
5283
5357
  const scan = scanTranscript(ctx.input.transcript_path ?? session.transcriptPath);
5284
5358
  const from = reflectCursor(session, scan.facts.turns.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {