@bunshin-cc/bunshin 0.3.1 → 0.3.3

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 +64 -9
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -3352,7 +3352,7 @@ function repoCommit(root) {
3352
3352
  }
3353
3353
  function listDocs(root) {
3354
3354
  const listed = git(root, ["ls-files", "-co", "--exclude-standard", "-z"], 3e3);
3355
- if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs7.existsSync(path6.join(root, rel))).sort();
3355
+ if (listed !== null) return [...new Set(listed.split("\0").filter(Boolean).filter(isDoc))].filter((rel) => fs7.existsSync(path6.join(root, rel)) && !deployedSkill(root, rel)).sort();
3356
3356
  const out = [];
3357
3357
  let visited = 0;
3358
3358
  const walk = (dir, rel, depth) => {
@@ -3365,10 +3365,10 @@ function listDocs(root) {
3365
3365
  }
3366
3366
  for (const e of entries) {
3367
3367
  if (e.isDirectory()) {
3368
- if (!SKIP_DIRS.has(e.name)) walk(path6.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
3368
+ if (!SKIP_DIRS.has(e.name) && !fs7.existsSync(path6.join(dir, e.name, ".git"))) walk(path6.join(dir, e.name), rel ? `${rel}/${e.name}` : e.name, depth + 1);
3369
3369
  } else if (e.isFile()) {
3370
3370
  const r = rel ? `${rel}/${e.name}` : e.name;
3371
- if (isDoc(r)) out.push(r);
3371
+ if (isDoc(r) && !deployedSkill(root, r)) out.push(r);
3372
3372
  }
3373
3373
  }
3374
3374
  };
@@ -3448,7 +3448,7 @@ function relativeToRoot(root, file, cwd = root) {
3448
3448
  }
3449
3449
  return null;
3450
3450
  }
3451
- var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc;
3451
+ var DOC_NAMES, SKIP_DIRS, LIST_TTL_MS, rootCache, isDoc, deployedSkill;
3452
3452
  var init_repo = __esm({
3453
3453
  "src/v2/repo.ts"() {
3454
3454
  "use strict";
@@ -3462,6 +3462,7 @@ var init_repo = __esm({
3462
3462
  if (parts.some((p) => SKIP_DIRS.has(p))) return false;
3463
3463
  return DOC_NAMES.has(parts[parts.length - 1].toLowerCase());
3464
3464
  };
3465
+ deployedSkill = (root, rel) => /(^|\/)SKILL\.md$/i.test(rel) && fs7.existsSync(path6.join(root, path6.posix.dirname(rel), ".bunshin.json"));
3465
3466
  }
3466
3467
  });
3467
3468
 
@@ -4175,11 +4176,13 @@ var init_exports = {};
4175
4176
  __export(init_exports, {
4176
4177
  CODEX_HOOK_EVENTS: () => CODEX_HOOK_EVENTS,
4177
4178
  HOOK_EVENTS: () => HOOK_EVENTS,
4179
+ LEARN_COMMAND: () => LEARN_COMMAND,
4178
4180
  PRE_TOOL_MATCHER: () => PRE_TOOL_MATCHER,
4179
4181
  bunshinHookEntry: () => bunshinHookEntry,
4180
4182
  claudeSettingsPath: () => claudeSettingsPath,
4181
4183
  countBunshinHooks: () => countBunshinHooks,
4182
4184
  installClaudeHooks: () => installClaudeHooks,
4185
+ installLearnCommand: () => installLearnCommand,
4183
4186
  installMcpConfig: () => installMcpConfig,
4184
4187
  installPiExtension: () => installPiExtension,
4185
4188
  isBunshinEntry: () => isBunshinEntry,
@@ -4290,6 +4293,24 @@ function mcpLauncher(bin) {
4290
4293
  }
4291
4294
  return { command: process.execPath, args: [script, "mcp"] };
4292
4295
  }
4296
+ function installLearnCommand({ file, remove = false }) {
4297
+ try {
4298
+ const current = fileExists(file) ? fs13.readFileSync(file, "utf8") : null;
4299
+ const ours = current === null || current === LEARN_COMMAND;
4300
+ if (remove) {
4301
+ if (current !== null && ours) fs13.unlinkSync(file);
4302
+ return { file, installed: 0, changed: current !== null && ours };
4303
+ }
4304
+ if (!ours) return { file, installed: 0, changed: false, error: "a different learn.md is already there; left as it is" };
4305
+ if (current === null) {
4306
+ fs13.mkdirSync(path10.dirname(file), { recursive: true });
4307
+ fs13.writeFileSync(file, LEARN_COMMAND);
4308
+ }
4309
+ return { file, installed: 1, changed: current === null };
4310
+ } catch (err) {
4311
+ return { file, installed: 0, changed: false, error: err instanceof Error ? err.message : String(err) };
4312
+ }
4313
+ }
4293
4314
  function installMcpConfig({ file, launcher, remove = false }) {
4294
4315
  try {
4295
4316
  const before = fileExists(file) ? JSON.parse(fs13.readFileSync(file, "utf8")) : {};
@@ -4362,15 +4383,18 @@ function runInit(opts = {}) {
4362
4383
  if (hooks.error) warnings.push(`could not update ${hooks.file}: ${hooks.error}`);
4363
4384
  }
4364
4385
  let mcp;
4386
+ let learnCommand;
4365
4387
  if ((opts.removeHooks || !opts.noHooks) && config.harness === "claude-code" && scope === "repo") {
4366
4388
  mcp = installMcpConfig({ file: path10.join(cwd, ".mcp.json"), launcher: opts.mcp ?? mcpLauncher(opts.bin), remove: opts.removeHooks });
4367
4389
  if (mcp.error) warnings.push(`could not update ${mcp.file}: ${mcp.error}`);
4390
+ learnCommand = installLearnCommand({ file: path10.join(cwd, ".claude", "commands", "learn.md"), remove: opts.removeHooks });
4391
+ if (learnCommand.error) warnings.push(`/learn not installed at ${learnCommand.file}: ${learnCommand.error}`);
4368
4392
  }
4369
4393
  if (!resolveBackend(config)) {
4370
4394
  warnings.push("No model backend found. Install and sign in to Codex or Claude Code before using automatic learning.");
4371
4395
  }
4372
4396
  store.log("init", { root, scope, role: config.role, orgUnit: config.orgUnit, hooks: hooks?.installed ?? 0 });
4373
- return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, warnings };
4397
+ return { root, config, created: !existed, hooks, ...mcp ? { mcp } : {}, ...learnCommand ? { learnCommand } : {}, warnings };
4374
4398
  }
4375
4399
  function otherHarnessInstructions() {
4376
4400
  return [
@@ -4386,7 +4410,7 @@ function otherHarnessInstructions() {
4386
4410
  " Claude Code plugin users need no settings.json wiring \u2014 the plugin ships hooks/hooks.json."
4387
4411
  ].join("\n");
4388
4412
  }
4389
- var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS;
4413
+ var PRE_TOOL_MATCHER, HOOK_EVENTS, CODEX_HOOK_EVENTS, LEARN_COMMAND;
4390
4414
  var init_init = __esm({
4391
4415
  "src/init.ts"() {
4392
4416
  "use strict";
@@ -4410,6 +4434,7 @@ var init_init = __esm({
4410
4434
  { event: "PreCompact", arg: "pre-compact", timeout: 10 },
4411
4435
  { event: "SessionEnd", arg: "session-end", timeout: 3 }
4412
4436
  ];
4437
+ 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';
4413
4438
  }
4414
4439
  });
4415
4440
 
@@ -4491,6 +4516,7 @@ __export(commands_exports, {
4491
4516
  cmdRecall: () => cmdRecall,
4492
4517
  cmdReflectFork: () => cmdReflectFork,
4493
4518
  cmdReview: () => cmdReview,
4519
+ cmdReviewDecide: () => cmdReviewDecide,
4494
4520
  cmdSuggest: () => cmdSuggest,
4495
4521
  cmdSync: () => cmdSync,
4496
4522
  robotAttrsFromFlags: () => robotAttrsFromFlags,
@@ -4532,7 +4558,7 @@ function cmdReview(store, args, cwd, out, err) {
4532
4558
  `);
4533
4559
  }
4534
4560
  out(`
4535
- bunshin review apply <id> | submit <id> | skip <id> | apply-all
4561
+ bunshin review apply <id> | submit <id> | skip <id> | apply-all (owners: approve <id> | reject <id> [--note])
4536
4562
  `);
4537
4563
  return 0;
4538
4564
  }
@@ -4574,6 +4600,29 @@ bunshin review apply <id> | submit <id> | skip <id> | apply-all
4574
4600
  `);
4575
4601
  return 1;
4576
4602
  }
4603
+ async function cmdReviewDecide(store, args, out, err) {
4604
+ const sub = args._[1];
4605
+ const id = args._[2];
4606
+ if (!id) {
4607
+ err(`bunshin review ${sub}: a suggestion id is required (see the console's Review, or bunshin review list --all)
4608
+ `);
4609
+ return 1;
4610
+ }
4611
+ const config = store.config();
4612
+ if (!canCall(config)) {
4613
+ err(`bunshin review ${sub}: this project is not connected to Bunshin
4614
+ `);
4615
+ return 1;
4616
+ }
4617
+ const note = str(args.flags.note);
4618
+ const res = await v2api(config, "POST", `/suggestions/${encodeURIComponent(id)}/review`, {
4619
+ approve: sub === "approve",
4620
+ ...note ? { note } : {}
4621
+ });
4622
+ out(`${res.suggestion.id}: ${res.suggestion.status}${res.suggestion.decision?.reason ? ` (${res.suggestion.decision.reason})` : ""}
4623
+ `);
4624
+ return 0;
4625
+ }
4577
4626
  async function cmdRecall(store, args, cwd, out, err) {
4578
4627
  const abstract = args._.slice(1).join(" ").trim() || str(args.flags.abstract) || "";
4579
4628
  if (!abstract) {
@@ -8320,8 +8369,9 @@ Usage: bunshin <command> [options]
8320
8369
  print one document (or one part of it) and mark it loaded
8321
8370
  learn [focus...] capture what this session learned now (fork reflection),
8322
8371
  apply it locally as a trial and submit it
8323
- review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all]
8324
- suggestions awaiting review (apply = local trial + submit)
8372
+ review [list [--all]|apply <id>|submit <id>|skip <id>|apply-all|approve <id>|reject <id> [--note <text>]]
8373
+ suggestions awaiting review (apply = local trial + submit;
8374
+ approve / reject = owner decision: merge now, or keep it out)
8325
8375
  index [--upload] [--json] parse SPEC.md / TESTING.md / SKILL.md into sections
8326
8376
  sync [--index] [--json] upload episodes + suggestions (incl. in-place skill edits), pull
8327
8377
  the library, install skills / instructions / files, settle trials
@@ -8440,6 +8490,10 @@ Key saved privately outside the project.
8440
8490
  `);
8441
8491
  if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
8442
8492
  if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
8493
+ `);
8494
+ if (result.learnCommand?.installed) out(`/learn installed in ${result.learnCommand.file}
8495
+ `);
8496
+ else if (result.learnCommand?.error) out(`/learn not installed: ${result.learnCommand.error} (${result.learnCommand.file})
8443
8497
  `);
8444
8498
  if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
8445
8499
  if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the five Bunshin hooks.\n");
@@ -8487,6 +8541,7 @@ Key saved privately outside the project.
8487
8541
  case "suggest":
8488
8542
  return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdSuggest(store, args, cwd, out, err);
8489
8543
  case "review":
8544
+ if (args._[1] === "approve" || args._[1] === "reject") return await (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdReviewDecide(store, args, out, err);
8490
8545
  return (await Promise.resolve().then(() => (init_commands(), commands_exports))).cmdReview(store, args, cwd, out, err);
8491
8546
  case "questions":
8492
8547
  case "question":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {