@bunshin-cc/bunshin 0.4.8 → 0.5.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 +397 -85
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1855,6 +1855,18 @@ At most ${max} suggestions. Schema:
1855
1855
  "signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
1856
1856
  }
1857
1857
  function extractJson(text2) {
1858
+ try {
1859
+ const whole = JSON.parse(text2.trim());
1860
+ if (whole && typeof whole === "object") return whole;
1861
+ } catch {
1862
+ }
1863
+ const outer = /^\s*```(?:json)?\s*\n([\s\S]*)\n```\s*$/.exec(text2);
1864
+ if (outer) {
1865
+ try {
1866
+ return JSON.parse(outer[1]);
1867
+ } catch {
1868
+ }
1869
+ }
1858
1870
  const fenced = text2.match(/```(?:json)?\s*([\s\S]*?)```/);
1859
1871
  const candidate = fenced ? fenced[1] : text2;
1860
1872
  const start = candidate.indexOf("{");
@@ -2029,6 +2041,20 @@ var init_reconcile = __esm({
2029
2041
  }
2030
2042
  });
2031
2043
 
2044
+ // ../core/src/v2/merge.ts
2045
+ var init_merge = __esm({
2046
+ "../core/src/v2/merge.ts"() {
2047
+ "use strict";
2048
+ init_text();
2049
+ init_layers();
2050
+ init_reconcile();
2051
+ init_markdown();
2052
+ init_reflect();
2053
+ init_redact();
2054
+ init_text_index();
2055
+ }
2056
+ });
2057
+
2032
2058
  // ../core/src/v2/release.ts
2033
2059
  var init_release = __esm({
2034
2060
  "../core/src/v2/release.ts"() {
@@ -2065,6 +2091,7 @@ var init_v2 = __esm({
2065
2091
  init_engine();
2066
2092
  init_reflect();
2067
2093
  init_reconcile();
2094
+ init_merge();
2068
2095
  init_release();
2069
2096
  init_research();
2070
2097
  init_profile();
@@ -3383,22 +3410,22 @@ async function askSdk(opts) {
3383
3410
  system: opts.system,
3384
3411
  messages: [{ role: "user", content: opts.user }]
3385
3412
  };
3386
- const format = { type: "json_schema", schema: opts.schema };
3413
+ const format = opts.enforceSchema === false ? void 0 : { type: "json_schema", schema: opts.schema };
3387
3414
  const reqOpts = { timeout: opts.timeoutMs ?? 3e5 };
3388
3415
  let response;
3389
3416
  try {
3390
- response = isHaiku ? await client.messages.create({ ...base, output_config: { format } }, reqOpts) : await client.beta.messages.create(
3417
+ response = isHaiku ? await client.messages.create({ ...base, ...format ? { output_config: { format } } : {} }, reqOpts) : await client.beta.messages.create(
3391
3418
  {
3392
3419
  ...base,
3393
3420
  betas: ["server-side-fallback-2026-07-01"],
3394
3421
  fallbacks: "default",
3395
- output_config: { format, effort: opts.effort ?? "medium" }
3422
+ output_config: { ...format ? { format } : {}, effort: opts.effort ?? "medium" }
3396
3423
  },
3397
3424
  reqOpts
3398
3425
  );
3399
3426
  } catch (err) {
3400
3427
  if (!(err instanceof Anthropic.BadRequestError)) throw err;
3401
- response = await client.messages.create({ ...base, output_config: { format } }, reqOpts);
3428
+ response = await client.messages.create({ ...base, ...format ? { output_config: { format } } : {} }, reqOpts);
3402
3429
  }
3403
3430
  if (response.stop_reason === "refusal") throw new Error("model refused the request");
3404
3431
  return response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -4284,7 +4311,7 @@ async function syncV2(store, opts = {}) {
4284
4311
  report.errors.push("sync already running in another process; work stays queued");
4285
4312
  return report;
4286
4313
  }
4287
- const call = (method, route, body) => v2api(config, method, route, body, { deadline, timeoutMs: 15e3 });
4314
+ const call2 = (method, route, body) => v2api(config, method, route, body, { deadline, timeoutMs: 15e3 });
4288
4315
  const ctx = contextFor(v2, config);
4289
4316
  const deploying = (opts.deploy ?? true) && deployEnabled(config, ctx);
4290
4317
  try {
@@ -4298,7 +4325,7 @@ async function syncV2(store, opts = {}) {
4298
4325
  for (const e of v2.listEpisodes().filter((x) => !x.uploaded)) {
4299
4326
  if (Date.now() >= deadline) break;
4300
4327
  try {
4301
- const res = await call("POST", "/episodes", { episode: episodePayload(e) });
4328
+ const res = await call2("POST", "/episodes", { episode: episodePayload(e) });
4302
4329
  if (res && (typeof res.reflectedTurns === "number" || Array.isArray(res.lessons)) && v2.readSession(e.sessionId)) {
4303
4330
  v2.updateSession(e.sessionId, "", (s) => {
4304
4331
  if (typeof res.reflectedTurns === "number") s.serverReflectedTurns = Math.max(s.serverReflectedTurns ?? 0, res.reflectedTurns);
@@ -4313,8 +4340,8 @@ async function syncV2(store, opts = {}) {
4313
4340
  const served = e.sectionsServed.filter((id) => !isLocalId(id) && !doneServed.has(id));
4314
4341
  const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id) && !doneLoaded.has(id));
4315
4342
  try {
4316
- if (served.length) await call("POST", "/recall/served", { sectionIds: served });
4317
- if (loaded.length) await call("POST", "/recall/served", { sectionIds: loaded, loaded: true });
4343
+ if (served.length) await call2("POST", "/recall/served", { sectionIds: served });
4344
+ if (loaded.length) await call2("POST", "/recall/served", { sectionIds: loaded, loaded: true });
4318
4345
  next.reportedServed = [...doneServed, ...served];
4319
4346
  next.reportedLoaded = [...doneLoaded, ...loaded];
4320
4347
  delete next.servedReported;
@@ -4330,7 +4357,7 @@ async function syncV2(store, opts = {}) {
4330
4357
  const pending = v2.listSuggestions().filter((s) => s.status === "submitted" && !s.uploaded && s.op.op !== "param");
4331
4358
  if (pending.length && Date.now() < deadline) {
4332
4359
  try {
4333
- const res = await call("POST", "/suggestions", { suggestions: pending.map(suggestionPayload) });
4360
+ const res = await call2("POST", "/suggestions", { suggestions: pending.map(suggestionPayload) });
4334
4361
  const ok = new Set(Array.isArray(res.ids) ? res.ids : pending.map((s) => s.id));
4335
4362
  for (const s of pending) {
4336
4363
  if (!ok.has(s.id)) continue;
@@ -4345,7 +4372,7 @@ async function syncV2(store, opts = {}) {
4345
4372
  for (const s of v2.listSuggestions().filter((x) => x.uploaded && x.trialDirty)) {
4346
4373
  if (Date.now() >= deadline) break;
4347
4374
  try {
4348
- await call("PATCH", `/suggestions/${encodeURIComponent(s.id)}/trial`, s.trial);
4375
+ await call2("PATCH", `/suggestions/${encodeURIComponent(s.id)}/trial`, s.trial);
4349
4376
  v2.writeSuggestion({ ...v2.readSuggestion(s.id) ?? s, trialDirty: false });
4350
4377
  report.trialUpdates++;
4351
4378
  } catch (err) {
@@ -4358,7 +4385,7 @@ async function syncV2(store, opts = {}) {
4358
4385
  const q = new URLSearchParams();
4359
4386
  if (previous?.generatedAt) q.set("since", previous.generatedAt);
4360
4387
  if (previous?.libraryVersion && previous.sections.length) q.set("have", String(previous.libraryVersion));
4361
- const bundle = await call("GET", `/bundle${q.size ? `?${q}` : ""}`);
4388
+ const bundle = await call2("GET", `/bundle${q.size ? `?${q}` : ""}`);
4362
4389
  if (bundle && Array.isArray(bundle.sections)) {
4363
4390
  const next = bundle.unchanged && previous ? { ...bundle, sections: previous.sections, unchanged: void 0 } : bundle;
4364
4391
  delete next.unchanged;
@@ -5809,7 +5836,10 @@ function mark(v2, sessionId, cwd, field, ids) {
5809
5836
  try {
5810
5837
  v2.updateSession(sessionId, cwd, (s) => {
5811
5838
  addUnique(s.served, ...ids);
5812
- if (field === "loaded") addUnique(s.loaded, ...ids);
5839
+ if (field === "loaded") {
5840
+ addUnique(s.loaded, ...ids);
5841
+ if (s.inContext) addUnique(s.inContext, ...ids);
5842
+ }
5813
5843
  s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
5814
5844
  });
5815
5845
  } catch {
@@ -8147,9 +8177,276 @@ Common flags: --api-url URL (default: this project's, else https://api.bunshin.c
8147
8177
  }
8148
8178
  });
8149
8179
 
8180
+ // src/v2/runner.ts
8181
+ var runner_exports = {};
8182
+ __export(runner_exports, {
8183
+ ROUTINE_PROMPT: () => ROUTINE_PROMPT,
8184
+ cmdRunner: () => cmdRunner,
8185
+ connection: () => connection,
8186
+ renderJob: () => renderJob,
8187
+ runOnce: () => runOnce,
8188
+ setupText: () => setupText
8189
+ });
8190
+ import fs23 from "node:fs";
8191
+ import os5 from "node:os";
8192
+ function connection(args, cwd) {
8193
+ const store = Store.discover(cwd);
8194
+ const config = store?.config();
8195
+ const key = process.env.BUNSHIN_RUNNER_KEY || process.env.BUNSHIN_API_KEY || config?.apiKey || "";
8196
+ const apiUrl = (str5(args.flags["api-url"]) ?? process.env.BUNSHIN_API_URL ?? config?.apiUrl ?? DEFAULT_API).replace(/\/$/, "");
8197
+ if (!key) throw new Error("no key: set BUNSHIN_RUNNER_KEY (create a runner key in the console: Runners \u2192 New runner key) or run inside a connected project");
8198
+ const name = str5(args.flags.name) ?? process.env.BUNSHIN_RUNNER_NAME ?? `${os5.hostname().split(".")[0] || "runner"}`;
8199
+ return { apiUrl, key, name: name.replace(/[^\w.:@/-]/g, "-").slice(0, 80) };
8200
+ }
8201
+ async function call(c, method, route, body, timeoutMs = 6e4) {
8202
+ const ctrl = new AbortController();
8203
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
8204
+ try {
8205
+ const res = await fetch(`${c.apiUrl}/v1/runner${route}`, {
8206
+ method,
8207
+ headers: { authorization: `Bearer ${c.key}`, "content-type": "application/json" },
8208
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
8209
+ signal: ctrl.signal
8210
+ });
8211
+ const text2 = await res.text();
8212
+ const parsed = text2 ? JSON.parse(text2) : null;
8213
+ if (res.status >= 400 && res.status !== 409) throw new Error(`${method} /v1/runner${route}: ${res.status} ${text2.slice(0, 200)}`);
8214
+ return { status: res.status, body: parsed };
8215
+ } finally {
8216
+ clearTimeout(timer);
8217
+ }
8218
+ }
8219
+ function kindsOf(args) {
8220
+ const k = str5(args.flags.kinds);
8221
+ if (!k) return void 0;
8222
+ const list2 = k.split(",").map((x) => x.trim()).filter(Boolean);
8223
+ for (const x of list2) if (!KINDS2.includes(x)) throw new Error(`--kinds: ${x} is not one of ${KINDS2.join(", ")}`);
8224
+ return list2;
8225
+ }
8226
+ async function lease(c, args, max = 1) {
8227
+ const kinds = kindsOf(args);
8228
+ return (await call(c, "POST", "/lease", { max, runner: c.name, ...kinds ? { kinds } : {} }, 12e4)).body.jobs;
8229
+ }
8230
+ function modelConfig(args) {
8231
+ const backend = str5(args.flags.backend) ?? "auto";
8232
+ if (!["auto", "sdk", "claude-cli", "codex-cli", "pi-cli"].includes(backend)) throw new Error("--backend must be auto, sdk, claude-cli, codex-cli or pi-cli");
8233
+ const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL;
8234
+ const harness = backend === "codex-cli" ? "codex" : backend === "pi-cli" ? "pi" : "claude-code";
8235
+ return { ...DEFAULT_CONFIG, backend, harness, ...model ? { model } : {}, ...model && backend === "codex-cli" ? { codexModel: model } : {}, ...model && backend === "pi-cli" ? { piModel: model } : {} };
8236
+ }
8237
+ async function runOnce(c, args, out, err) {
8238
+ const config = modelConfig(args);
8239
+ const max = Number(str5(args.flags.max) ?? 200);
8240
+ let done = 0;
8241
+ let failed = 0;
8242
+ for (let i = 0; i < max; i++) {
8243
+ const [job] = await lease(c, args);
8244
+ if (!job) break;
8245
+ const started = Date.now();
8246
+ const res = await askJson(config, { system: job.system, user: job.prompt, schema: job.schema, enforceSchema: false, timeoutMs: 15 * 6e4, maxTokens: 16e3 });
8247
+ const model = config.backend === "codex-cli" ? config.codexModel ?? "codex default" : config.backend === "pi-cli" ? config.piModel ?? "pi default" : config.model;
8248
+ if (!res.ok) {
8249
+ const r2 = await call(c, "POST", `/jobs/${job.id}/fail`, { error: res.error.slice(0, 1900), model, runner: c.name });
8250
+ err(`${job.id} ${job.kind}: ${res.error.slice(0, 160)} (${r2.body.status})
8251
+ `);
8252
+ failed++;
8253
+ if (r2.body.status === "retry" && /no LLM backend|not found|ENOENT/.test(res.error)) break;
8254
+ continue;
8255
+ }
8256
+ const r = await call(c, "POST", `/jobs/${job.id}/answer`, { answer: res.value, model, runner: c.name }, 12e4);
8257
+ out(`${job.id} ${job.kind}: ${r.body.status}${r.body.error ? ` (${r.body.error})` : ""} in ${Math.round((Date.now() - started) / 1e3)}s via ${res.backend}
8258
+ `);
8259
+ if (r.body.status === "done" || r.body.status === "stale") done++;
8260
+ else failed++;
8261
+ }
8262
+ return { done, failed };
8263
+ }
8264
+ function renderJob(job) {
8265
+ return `JOB ${job.id} (${job.kind}), attempt ${job.attempts}, answer before ${job.leaseUntil}
8266
+
8267
+ ===== SYSTEM =====
8268
+ ${job.system}
8269
+
8270
+ ===== TASK =====
8271
+ ${job.prompt}
8272
+
8273
+ ===== HOW TO ANSWER =====
8274
+ You are the model for this job. Read the task above and decide it yourself; do not run other
8275
+ tools to answer it. Write ONLY the JSON object it asks for to a file, then run:
8276
+
8277
+ bunshin runner submit ${job.id} <file>
8278
+
8279
+ If you cannot answer it, run: bunshin runner fail ${job.id} --error "<why>"
8280
+ Then run \`bunshin runner next\` again until it says there are no jobs.
8281
+ `;
8282
+ }
8283
+ function setupText(target) {
8284
+ switch (target) {
8285
+ case "claude-routine":
8286
+ return `Claude Code routine (claude.ai/code \u2192 Routines, or /schedule in Claude Code)
8287
+
8288
+ 1. Console \u2192 Runners \u2192 New runner key. Copy it.
8289
+ 2. Create an environment for the routine with:
8290
+ BUNSHIN_RUNNER_KEY=<the runner key>
8291
+ BUNSHIN_API_URL=https://api.bunshin.cc
8292
+ and network access to api.bunshin.cc and registry.npmjs.org. No repository is needed.
8293
+ 3. Create a routine on that environment, daily (e.g. 03:17), with this prompt:
8294
+
8295
+ ${ROUTINE_PROMPT.replace(/^/gm, " ")}
8296
+
8297
+ The routine's own model (Opus 5.5) answers the jobs; nothing else needs a key.
8298
+ `;
8299
+ case "codex-cloud":
8300
+ return `Codex cloud task (chatgpt.com/codex)
8301
+
8302
+ 1. Console \u2192 Runners \u2192 New runner key. Copy it.
8303
+ 2. In the Codex environment, add the secret BUNSHIN_RUNNER_KEY and the variable
8304
+ BUNSHIN_API_URL=https://api.bunshin.cc; allow internet access to api.bunshin.cc and
8305
+ registry.npmjs.org.
8306
+ 3. Run a task (schedule it daily if your plan allows) with this prompt:
8307
+
8308
+ ${ROUTINE_PROMPT.replace(/^/gm, " ")}
8309
+ `;
8310
+ case "github-action":
8311
+ return `GitHub Actions: a daily workflow on your Claude subscription (no API key)
8312
+
8313
+ 1. Console \u2192 Runners \u2192 New runner key \u2192 repository secret BUNSHIN_RUNNER_KEY.
8314
+ 2. \`claude setup-token\` on your machine \u2192 repository secret CLAUDE_CODE_OAUTH_TOKEN.
8315
+ (Codex instead: secret OPENAI_API_KEY, install @openai/codex, --backend codex-cli.)
8316
+ 3. .github/workflows/bunshin-runner.yml:
8317
+
8318
+ name: Bunshin runner
8319
+ on:
8320
+ schedule: [{ cron: "17 3 * * *" }]
8321
+ workflow_dispatch: {}
8322
+ jobs:
8323
+ run:
8324
+ runs-on: ubuntu-latest
8325
+ timeout-minutes: 60
8326
+ steps:
8327
+ - uses: actions/setup-node@v4
8328
+ with: { node-version: 22 }
8329
+ - run: npm i -g @anthropic-ai/claude-code @bunshin-cc/bunshin
8330
+ - run: bunshin runner --once --backend claude-cli --model "claude-opus-5-5[1m]" --name github-actions
8331
+ env:
8332
+ BUNSHIN_RUNNER_KEY: \${{ secrets.BUNSHIN_RUNNER_KEY }}
8333
+ BUNSHIN_API_URL: https://api.bunshin.cc
8334
+ CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
8335
+ `;
8336
+ case "docker":
8337
+ return `A container that keeps the queue drained (Cloudflare Containers, Fly, any box)
8338
+
8339
+ FROM node:22-slim
8340
+ RUN npm i -g @bunshin-cc/bunshin @anthropic-ai/claude-code @openai/codex
8341
+ ENV BUNSHIN_API_URL=https://api.bunshin.cc
8342
+ # Provide BUNSHIN_RUNNER_KEY and the model's credentials (CLAUDE_CODE_OAUTH_TOKEN, or
8343
+ # ANTHROPIC_API_KEY, or OPENAI_API_KEY for codex) as secrets at run time.
8344
+ CMD ["bunshin", "runner", "--watch", "--backend", "claude-cli", "--model", "claude-opus-5-5[1m]", "--name", "container"]
8345
+ `;
8346
+ case "cron":
8347
+ return `cron on any machine with claude, codex or pi signed in
8348
+
8349
+ 17 3 * * * BUNSHIN_RUNNER_KEY=<key> bunshin runner --once --backend claude-cli --model "claude-opus-5-5[1m]" >> ~/.bunshin/runner.log 2>&1
8350
+ `;
8351
+ default:
8352
+ return `bunshin runner setup <target>: claude-routine | codex-cloud | github-action | docker | cron
8353
+
8354
+ Or give the server a key and it runs model work itself (wrangler secret put \u2026):
8355
+ ANTHROPIC_API_KEY (+ BUNSHIN_MODEL, default claude-opus-5-5)
8356
+ OPENAI_API_KEY + BUNSHIN_OPENAI_MODEL (BUNSHIN_LLM_PROVIDER=openai)
8357
+ OPENROUTER_API_KEY + BUNSHIN_OPENROUTER_MODEL (BUNSHIN_LLM_PROVIDER=openrouter)
8358
+ `;
8359
+ }
8360
+ }
8361
+ async function cmdRunner(args, cwd, out, err) {
8362
+ const sub = args._[1] ?? "";
8363
+ if (sub === "setup") {
8364
+ out(setupText(args._[2] ?? ""));
8365
+ return 0;
8366
+ }
8367
+ const c = connection(args, cwd);
8368
+ if (sub === "status") {
8369
+ const s = (await call(c, "GET", "/status")).body;
8370
+ if (args.flags.json) {
8371
+ out(`${JSON.stringify(s, null, 2)}
8372
+ `);
8373
+ return 0;
8374
+ }
8375
+ out(`server: ${s.executor.provider ? `${s.executor.provider} (${s.executor.model})` : `none (${s.executor.reason})`}
8376
+ `);
8377
+ for (const [kind, by] of Object.entries(s.counts)) out(`${kind.padEnd(8)} ${Object.entries(by).map(([k, n]) => `${k} ${n}`).join(" \xB7 ")}
8378
+ `);
8379
+ for (const r of s.runners) out(`runner ${r.name}: ${r.done} done, last ${r.lastSeen}${r.model ? `, ${r.model}` : ""}
8380
+ `);
8381
+ return 0;
8382
+ }
8383
+ if (sub === "next") {
8384
+ const [job] = await lease(c, args);
8385
+ if (!job) {
8386
+ out("No jobs.\n");
8387
+ return 0;
8388
+ }
8389
+ out(args.flags.json ? `${JSON.stringify(job, null, 2)}
8390
+ ` : renderJob(job));
8391
+ return 0;
8392
+ }
8393
+ if (sub === "submit") {
8394
+ const id = args._[2];
8395
+ const file = args._[3] ?? "-";
8396
+ if (!id) throw new Error("usage: bunshin runner submit <job id> <file|->");
8397
+ const text2 = file === "-" ? fs23.readFileSync(0, "utf8") : fs23.readFileSync(file, "utf8");
8398
+ const model = str5(args.flags.model) ?? process.env.BUNSHIN_MODEL ?? "agent";
8399
+ const r = await call(c, "POST", `/jobs/${id}/answer`, { answer: text2, model, runner: c.name }, 12e4);
8400
+ out(`${id}: ${r.body.status}${r.body.error ? ` (${r.body.error})` : ""}
8401
+ `);
8402
+ return r.body.status === "done" || r.body.status === "stale" ? 0 : 1;
8403
+ }
8404
+ if (sub === "fail") {
8405
+ const id = args._[2];
8406
+ if (!id) throw new Error("usage: bunshin runner fail <job id> --error <why>");
8407
+ const r = await call(c, "POST", `/jobs/${id}/fail`, { error: str5(args.flags.error) ?? "the runner could not answer", runner: c.name });
8408
+ out(`${id}: ${r.body.status}
8409
+ `);
8410
+ return 0;
8411
+ }
8412
+ if (sub && sub !== "run") throw new Error(`unknown runner command "${sub}" (run, next, submit, fail, status, setup)`);
8413
+ const watch = args.flags.watch === true;
8414
+ const interval = Number(str5(args.flags.interval) ?? 300) * 1e3;
8415
+ for (; ; ) {
8416
+ const r = await runOnce(c, args, out, err);
8417
+ if (!watch) {
8418
+ out(`runner: ${r.done} answered, ${r.failed} failed
8419
+ `);
8420
+ return r.failed && !r.done ? 1 : 0;
8421
+ }
8422
+ await new Promise((resolve) => setTimeout(resolve, interval));
8423
+ }
8424
+ }
8425
+ var DEFAULT_API, KINDS2, str5, NPX, ROUTINE_PROMPT;
8426
+ var init_runner = __esm({
8427
+ "src/v2/runner.ts"() {
8428
+ "use strict";
8429
+ init_llm();
8430
+ init_store();
8431
+ DEFAULT_API = "https://api.bunshin.cc";
8432
+ KINDS2 = ["reflect", "merge", "create"];
8433
+ str5 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
8434
+ NPX = "npx -y @bunshin-cc/bunshin@latest";
8435
+ ROUTINE_PROMPT = `You are this company's Bunshin runner. Bunshin queues model work (reflecting on work sessions,
8436
+ reconciling the shared documents); you answer it, then stop.
8437
+
8438
+ 1. Run: ${NPX} runner next
8439
+ 2. If it prints "No jobs", stop: you are done.
8440
+ 3. Otherwise it prints one job. Do exactly what its TASK asks: you are the model, so think it
8441
+ through yourself; do not edit any repository. Write only the JSON answer to /tmp/bunshin-answer.json.
8442
+ 4. Run: ${NPX} runner submit <job id> /tmp/bunshin-answer.json
8443
+ 5. Go back to step 1. Stop after 40 jobs.`;
8444
+ }
8445
+ });
8446
+
8150
8447
  // src/v2/importer.ts
8151
8448
  import { execFileSync as execFileSync3 } from "node:child_process";
8152
- import fs23 from "node:fs";
8449
+ import fs24 from "node:fs";
8153
8450
  import path20 from "node:path";
8154
8451
  function rewriteRepoPaths(text2, repoName2, pack) {
8155
8452
  const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
@@ -8287,7 +8584,7 @@ function isGitRepo(root) {
8287
8584
  function workingTreeFiles(root) {
8288
8585
  const out = /* @__PURE__ */ new Map();
8289
8586
  const list2 = isGitRepo(root) ? git3(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
8290
- return fs23.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
8587
+ return fs24.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
8291
8588
  if (e.name === ".git" || e.name === "node_modules") return [];
8292
8589
  const rel = base ? `${base}/${e.name}` : e.name;
8293
8590
  return e.isDirectory() ? walk(path20.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
@@ -8297,12 +8594,12 @@ function workingTreeFiles(root) {
8297
8594
  const abs = path20.join(root, rel);
8298
8595
  let buf;
8299
8596
  try {
8300
- buf = fs23.readFileSync(abs);
8597
+ buf = fs24.readFileSync(abs);
8301
8598
  } catch {
8302
8599
  continue;
8303
8600
  }
8304
8601
  if (!isText(buf)) continue;
8305
- const exec = (fs23.statSync(abs).mode & 73) !== 0;
8602
+ const exec = (fs24.statSync(abs).mode & 73) !== 0;
8306
8603
  out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
8307
8604
  }
8308
8605
  return out;
@@ -8449,7 +8746,7 @@ __export(skills_exports, {
8449
8746
  cmdSkill: () => cmdSkill,
8450
8747
  parsePin: () => parsePin
8451
8748
  });
8452
- import fs24 from "node:fs";
8749
+ import fs25 from "node:fs";
8453
8750
  import path21 from "node:path";
8454
8751
  function requireOnline(store, what) {
8455
8752
  if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
@@ -8460,10 +8757,10 @@ async function cmdImport2(store, args, cwd, out, err) {
8460
8757
  return 1;
8461
8758
  }
8462
8759
  const root = path21.resolve(cwd, args._[2]);
8463
- if (!fs24.existsSync(root)) throw new Error(`${root} does not exist`);
8464
- const pack = str5(args.flags.pack) ?? path21.basename(root);
8760
+ if (!fs25.existsSync(root)) throw new Error(`${root} does not exist`);
8761
+ const pack = str6(args.flags.pack) ?? path21.basename(root);
8465
8762
  const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
8466
- const read = readSkillSystem(root, { pack, history, ...str5(args.flags.since) ? { since: str5(args.flags.since) } : {} });
8763
+ const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
8467
8764
  const kinds = /* @__PURE__ */ new Map();
8468
8765
  for (const d of read.docs) {
8469
8766
  const label2 = d.delivery === "always" ? "instructions" : d.delivery === "stop" ? "learning check" : d.kind;
@@ -8479,7 +8776,7 @@ async function cmdImport2(store, args, cwd, out, err) {
8479
8776
  }
8480
8777
  requireOnline(store, "bunshin import");
8481
8778
  const config = store.config();
8482
- const body = { pack, ...str5(args.flags.layer) ? { layer: str5(args.flags.layer) } : {}, ...str5(args.flags["owner-team"]) ? { ownerTeam: str5(args.flags["owner-team"]) } : {} };
8779
+ const body = { pack, ...str6(args.flags.layer) ? { layer: str6(args.flags.layer) } : {}, ...str6(args.flags["owner-team"]) ? { ownerTeam: str6(args.flags["owner-team"]) } : {} };
8483
8780
  let written = 0;
8484
8781
  let last = null;
8485
8782
  const batches = batchVersions(read.versions);
@@ -8607,7 +8904,7 @@ async function cmdSkill(store, args, cwd, out, err) {
8607
8904
  }
8608
8905
  case "log": {
8609
8906
  requireOnline(store, "bunshin skill log");
8610
- const since = str5(args.flags.since);
8907
+ const since = str6(args.flags.since);
8611
8908
  const cutoff = since ? sinceCutoff(since) : 0;
8612
8909
  const targets = args._[2] ? [byName(args._[2])].filter((x) => !!x) : skills;
8613
8910
  if (!targets.length) throw new Error(`no skill ${args._[2]}`);
@@ -8634,9 +8931,9 @@ async function cmdSkill(store, args, cwd, out, err) {
8634
8931
  ${report.skills} skills, ${report.references} references, ${report.errors.length} errors, ${report.warnings.length} warnings
8635
8932
  `);
8636
8933
  }
8637
- const graph = str5(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
8934
+ const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
8638
8935
  if (graph) {
8639
- fs24.writeFileSync(path21.resolve(cwd, graph), renderSkillGraph(report));
8936
+ fs25.writeFileSync(path21.resolve(cwd, graph), renderSkillGraph(report));
8640
8937
  out(`graph written to ${graph}
8641
8938
  `);
8642
8939
  }
@@ -8645,7 +8942,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
8645
8942
  case "push": {
8646
8943
  const ctx = contextFor(v2, config);
8647
8944
  const name = args._[2];
8648
- const layer = str5(args.flags.layer);
8945
+ const layer = str6(args.flags.layer);
8649
8946
  const captured = captureEdits(v2, config, ctx, { libraryNames: skills.map((s) => s.skill.name), ...layer ? { newSkillLayer: layer } : {}, ...name && !byName(name) ? { newSkill: name } : {} });
8650
8947
  for (const c of captured) out(`proposed: ${c.name}
8651
8948
  `);
@@ -8696,7 +8993,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
8696
8993
  }
8697
8994
  case "log": {
8698
8995
  requireOnline(store, "bunshin library log");
8699
- const res = await v2api(config, "GET", `/library?limit=${Number(str5(args.flags.limit) ?? 30) || 30}`);
8996
+ const res = await v2api(config, "GET", `/library?limit=${Number(str6(args.flags.limit) ?? 30) || 30}`);
8700
8997
  for (const v of res.versions) out(`lib@${String(v.version).padEnd(5)} ${v.createdAt.slice(0, 16).replace("T", " ")} ${v.source.padEnd(8)} ${v.gitCommit ? `${v.gitCommit.slice(0, 10)} ` : ""}${v.summary} (${v.sectionIds.length} documents)
8701
8998
  `);
8702
8999
  return 0;
@@ -8733,27 +9030,27 @@ async function cmdLibrary(store, args, cwd, out, err) {
8733
9030
  const res = await v2api(config, "GET", "/library?limit=1");
8734
9031
  const line = `bunshin: lib@${res.version}
8735
9032
  `;
8736
- const file = str5(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
9033
+ const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
8737
9034
  if (file) {
8738
9035
  const abs = path21.resolve(cwd, file);
8739
- fs24.mkdirSync(path21.dirname(abs), { recursive: true });
8740
- fs24.writeFileSync(abs, line);
9036
+ fs25.mkdirSync(path21.dirname(abs), { recursive: true });
9037
+ fs25.writeFileSync(abs, line);
8741
9038
  out(`${file}: ${line}`);
8742
9039
  } else out(line);
8743
9040
  return 0;
8744
9041
  }
8745
9042
  case "checkout": {
8746
9043
  requireOnline(store, "bunshin library checkout");
8747
- const to = str5(args.flags.to);
9044
+ const to = str6(args.flags.to);
8748
9045
  if (!to) throw new Error("--to <dir> is required");
8749
- const pinFile = str5(args.flags.pin);
8750
- const pin = str5(args.flags.git) ? { git: str5(args.flags.git) } : pinFile ? parsePin(fs24.readFileSync(path21.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
9046
+ const pinFile = str6(args.flags.pin);
9047
+ const pin = str6(args.flags.git) ? { git: str6(args.flags.git) } : pinFile ? parsePin(fs25.readFileSync(path21.resolve(cwd, pinFile), "utf8")) : parsePin(args._[2] ?? "");
8751
9048
  const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
8752
9049
  if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
8753
9050
  const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
8754
9051
  const dir = path21.resolve(cwd, to);
8755
- if (fs24.existsSync(dir) && fs24.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
8756
- const pack = str5(args.flags.pack);
9052
+ if (fs25.existsSync(dir) && fs25.readdirSync(dir).length && !bool4(args.flags.force)) throw new Error(`${dir} is not empty (use --force)`);
9053
+ const pack = str6(args.flags.pack);
8757
9054
  const docs = snap.sections.filter((s) => !pack || s.pack === pack);
8758
9055
  const res = materialize(docs, dir);
8759
9056
  out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir}
@@ -8766,7 +9063,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
8766
9063
  return 1;
8767
9064
  }
8768
9065
  }
8769
- var str5, bool4, skillsOf;
9066
+ var str6, bool4, skillsOf;
8770
9067
  var init_skills = __esm({
8771
9068
  "src/v2/skills.ts"() {
8772
9069
  "use strict";
@@ -8778,7 +9075,7 @@ var init_skills = __esm({
8778
9075
  init_store2();
8779
9076
  init_sync();
8780
9077
  init_view();
8781
- str5 = (v) => typeof v === "string" ? v : void 0;
9078
+ str6 = (v) => typeof v === "string" ? v : void 0;
8782
9079
  bool4 = (v) => v === true || v === "true";
8783
9080
  skillsOf = (sections) => sections.filter((s) => s.skill && (s.status === "active" || s.status === "stale"));
8784
9081
  }
@@ -8791,7 +9088,7 @@ __export(reflect_exports, {
8791
9088
  buildLesson: () => buildLesson,
8792
9089
  runReflection: () => runReflection
8793
9090
  });
8794
- import fs25 from "node:fs";
9091
+ import fs26 from "node:fs";
8795
9092
  import path22 from "node:path";
8796
9093
  import { createHash as createHash7 } from "node:crypto";
8797
9094
  async function runReflection(store, opts) {
@@ -8800,18 +9097,18 @@ async function runReflection(store, opts) {
8800
9097
  const deadline = Date.now() + 66e4;
8801
9098
  for (; ; ) {
8802
9099
  try {
8803
- fs25.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
9100
+ fs26.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
8804
9101
  break;
8805
9102
  } catch (error) {
8806
9103
  if (error.code !== "EEXIST") throw error;
8807
9104
  try {
8808
- const owner = Number(fs25.readFileSync(lock, "utf8"));
9105
+ const owner = Number(fs26.readFileSync(lock, "utf8"));
8809
9106
  if (Number.isInteger(owner) && owner > 0) {
8810
9107
  try {
8811
9108
  process.kill(owner, 0);
8812
9109
  } catch (error2) {
8813
9110
  if (error2.code === "ESRCH") {
8814
- fs25.unlinkSync(lock);
9111
+ fs26.unlinkSync(lock);
8815
9112
  continue;
8816
9113
  }
8817
9114
  }
@@ -8826,7 +9123,7 @@ async function runReflection(store, opts) {
8826
9123
  return await reflectSession(store, opts);
8827
9124
  } finally {
8828
9125
  try {
8829
- fs25.unlinkSync(lock);
9126
+ fs26.unlinkSync(lock);
8830
9127
  } catch {
8831
9128
  }
8832
9129
  }
@@ -9168,11 +9465,11 @@ __export(cli_exports, {
9168
9465
  parseArgs: () => parseArgs,
9169
9466
  version: () => version
9170
9467
  });
9171
- import fs26 from "node:fs";
9468
+ import fs27 from "node:fs";
9172
9469
  import path23 from "node:path";
9173
9470
  function parseArgs(argv) {
9174
9471
  const out = { _: [], flags: {} };
9175
- 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"]);
9472
+ 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"]);
9176
9473
  for (let i = 0; i < argv.length; i++) {
9177
9474
  const a = argv[i];
9178
9475
  if (a === "--") {
@@ -9215,7 +9512,7 @@ function version() {
9215
9512
  for (const rel of ["../package.json", "../../package.json"]) {
9216
9513
  try {
9217
9514
  const file = new URL(rel, import.meta.url);
9218
- const pkg = JSON.parse(fs26.readFileSync(file, "utf8"));
9515
+ const pkg = JSON.parse(fs27.readFileSync(file, "utf8"));
9219
9516
  if (pkg.name === "@bunshin-cc/bunshin" && pkg.version) return pkg.version;
9220
9517
  } catch {
9221
9518
  }
@@ -9238,13 +9535,13 @@ async function main(argv, opts = {}) {
9238
9535
  return 0;
9239
9536
  }
9240
9537
  if (cmd === "hook") {
9241
- const root = str6(args.flags.root);
9538
+ const root = str7(args.flags.root);
9242
9539
  return hookCommand(args._[1], { cwd, store: root ? new Store(root) : void 0, write: out });
9243
9540
  }
9244
9541
  if (cmd === "setup" && bool5(args.flags.robot)) {
9245
9542
  try {
9246
9543
  const { setupRobot: setupRobot2, robotAttrsFromFlags: robotAttrsFromFlags2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
9247
- const robotId = str6(args.flags["robot-id"]);
9544
+ const robotId = str7(args.flags["robot-id"]);
9248
9545
  if (!robotId) throw new Error("--robot-id <id> is required with --robot");
9249
9546
  const apiKey = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
9250
9547
  let value = "";
@@ -9255,9 +9552,9 @@ async function main(argv, opts = {}) {
9255
9552
  process.stdin.on("end", () => resolve(value.trim()));
9256
9553
  process.stdin.on("error", reject);
9257
9554
  }) : process.env.BUNSHIN_API_KEY || void 0;
9258
- const harnessFlag = str6(args.flags.harness);
9555
+ const harnessFlag = str7(args.flags.harness);
9259
9556
  if (harnessFlag && !["claude-code", "codex", "pi", "other"].includes(harnessFlag)) throw new Error("--harness must be claude-code, codex, pi or other");
9260
- const res = await setupRobot2({ cwd, robotId, robot: robotAttrsFromFlags2(args.flags), apiUrl: str6(args.flags["api-url"]), apiKey, ...harnessFlag ? { harness: harnessFlag } : {} });
9557
+ const res = await setupRobot2({ cwd, robotId, robot: robotAttrsFromFlags2(args.flags), apiUrl: str7(args.flags["api-url"]), apiKey, ...harnessFlag ? { harness: harnessFlag } : {} });
9261
9558
  out(`Robot context ${res.context.id} (${res.context.robot?.armModel} / ${res.context.robot?.mount}, ${res.context.robot?.domain} @ ${res.context.robot?.site}/${res.context.robot?.cell})
9262
9559
  `);
9263
9560
  out(` store: ${res.store.root}/v2/context.json
@@ -9276,12 +9573,12 @@ async function main(argv, opts = {}) {
9276
9573
  if (cmd === "setup") {
9277
9574
  try {
9278
9575
  const { setupProject: setupProject2, executable: executable2, readKey: readKey2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
9279
- const harness = str6(args.flags.harness);
9576
+ const harness = str7(args.flags.harness);
9280
9577
  if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
9281
9578
  const agent = harness === "claude-code" ? "claude" : harness;
9282
9579
  if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
9283
9580
  const existing = new Store(path23.join(cwd, ".bunshin")).config();
9284
- const apiUrl = str6(args.flags["api-url"]);
9581
+ const apiUrl = str7(args.flags["api-url"]);
9285
9582
  const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
9286
9583
  let value = "";
9287
9584
  process.stdin.setEncoding("utf8");
@@ -9322,7 +9619,7 @@ Key saved privately outside the project.
9322
9619
  if (cmd === "init") return cmdInit(args, cwd, out, err);
9323
9620
  if (cmd === "mcp") {
9324
9621
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
9325
- const root = str6(args.flags.root);
9622
+ const root = str7(args.flags.root);
9326
9623
  await runMcpServer2({ cwd, ...root ? { store: new Store(path23.resolve(root)) } : {}, version: version() });
9327
9624
  return 0;
9328
9625
  }
@@ -9332,6 +9629,15 @@ Key saved privately outside the project.
9332
9629
  return 0;
9333
9630
  }
9334
9631
  if (cmd === "repo") return (await Promise.resolve().then(() => (init_gitrepo(), gitrepo_exports))).cmdRepo(args, cwd, out, err);
9632
+ if (cmd === "runner") {
9633
+ try {
9634
+ return await (await Promise.resolve().then(() => (init_runner(), runner_exports))).cmdRunner(args, cwd, out, err);
9635
+ } catch (e) {
9636
+ err(`bunshin runner: ${e instanceof Error ? e.message : String(e)}
9637
+ `);
9638
+ return 1;
9639
+ }
9640
+ }
9335
9641
  const store = openStore(args, cwd);
9336
9642
  if (!store) {
9337
9643
  err("bunshin: this project is not connected. Run `bunshin setup --harness codex` (or use --harness claude-code / --harness pi). For offline use, run `bunshin init`.\n");
@@ -9395,7 +9701,7 @@ ${HELP2}`);
9395
9701
  }
9396
9702
  }
9397
9703
  function openStore(args, cwd) {
9398
- const root = str6(args.flags.root);
9704
+ const root = str7(args.flags.root);
9399
9705
  if (root) {
9400
9706
  const store = new Store(path23.resolve(root));
9401
9707
  store.ensureLayout();
@@ -9405,21 +9711,21 @@ function openStore(args, cwd) {
9405
9711
  }
9406
9712
  async function cmdInit(args, cwd, out, err) {
9407
9713
  const { runInit: runInit2, otherHarnessInstructions: otherHarnessInstructions2 } = await Promise.resolve().then(() => (init_init(), init_exports));
9408
- const harness = str6(args.flags.harness);
9714
+ const harness = str7(args.flags.harness);
9409
9715
  const result = runInit2({
9410
9716
  cwd,
9411
- role: str6(args.flags.role),
9412
- orgUnit: str6(args.flags["org-unit"]),
9413
- orgId: str6(args.flags["org-id"]),
9414
- userId: str6(args.flags["user-id"]),
9415
- apiUrl: str6(args.flags["api-url"]),
9416
- apiKey: str6(args.flags["api-key"]),
9417
- scope: bool5(args.flags.global) ? "global" : str6(args.flags.scope),
9717
+ role: str7(args.flags.role),
9718
+ orgUnit: str7(args.flags["org-unit"]),
9719
+ orgId: str7(args.flags["org-id"]),
9720
+ userId: str7(args.flags["user-id"]),
9721
+ apiUrl: str7(args.flags["api-url"]),
9722
+ apiKey: str7(args.flags["api-key"]),
9723
+ scope: bool5(args.flags.global) ? "global" : str7(args.flags.scope),
9418
9724
  harness,
9419
9725
  noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
9420
9726
  removeHooks: bool5(args.flags["remove-hooks"]),
9421
- settingsFile: str6(args.flags["settings-file"]),
9422
- bin: str6(args.flags.bin)
9727
+ settingsFile: str7(args.flags["settings-file"]),
9728
+ bin: str7(args.flags.bin)
9423
9729
  });
9424
9730
  if (bool5(args.flags.json)) {
9425
9731
  out(JSON.stringify({ ...result, config: { ...result.config, apiKey: void 0 } }, null, 2) + "\n");
@@ -9459,7 +9765,7 @@ function cmdStatus(store, args, out, v2) {
9459
9765
  const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path23.join(process.env.CODEX_HOME ?? path23.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path23.dirname(store.root)).file])] : config.harness === "claude-code" ? [path23.join(homeDir(), ".claude", "settings.json"), path23.join(path23.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path23.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path23.join(path23.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
9460
9766
  const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
9461
9767
  try {
9462
- return fs26.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
9768
+ return fs27.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
9463
9769
  } catch {
9464
9770
  return false;
9465
9771
  }
@@ -9476,7 +9782,7 @@ function cmdStatus(store, args, out, v2) {
9476
9782
  mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
9477
9783
  llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
9478
9784
  hooks: { [config.harness]: hooks },
9479
- hookFiles: hookFiles.filter((file) => fs26.existsSync(file)),
9785
+ hookFiles: hookFiles.filter((file) => fs27.existsSync(file)),
9480
9786
  recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
9481
9787
  v2
9482
9788
  };
@@ -9517,7 +9823,7 @@ function cmdStatus(store, args, out, v2) {
9517
9823
  }
9518
9824
  function hookCount(file) {
9519
9825
  try {
9520
- const raw = JSON.parse(fs26.readFileSync(file, "utf8"));
9826
+ const raw = JSON.parse(fs27.readFileSync(file, "utf8"));
9521
9827
  let n = 0;
9522
9828
  for (const groups of Object.values(raw.hooks ?? {})) {
9523
9829
  for (const g of groups ?? []) {
@@ -9530,22 +9836,22 @@ function hookCount(file) {
9530
9836
  }
9531
9837
  }
9532
9838
  async function cmdRecall2(store, args, cwd, out, err) {
9533
- const task = args._.slice(1).join(" ").trim() || str6(args.flags.task) || "";
9839
+ const task = args._.slice(1).join(" ").trim() || str7(args.flags.task) || "";
9534
9840
  if (!task) {
9535
9841
  err('bunshin recall: a task is required, e.g. bunshin recall "close an account with a negative balance"\n');
9536
9842
  return 1;
9537
9843
  }
9538
9844
  const { runRecall: runRecall2 } = await Promise.resolve().then(() => (init_recall3(), recall_exports));
9539
- const sessionId = str6(args.flags.session);
9845
+ const sessionId = str7(args.flags.session);
9540
9846
  const dryRun = bool5(args.flags["dry-run"]) || !sessionId && !bool5(args.flags.apply);
9541
9847
  const result = await runRecall2(store, {
9542
9848
  sessionId: sessionId ?? `cli-${Date.now()}`,
9543
9849
  prompt: task,
9544
9850
  cwd,
9545
- transcriptPath: str6(args.flags.transcript),
9851
+ transcriptPath: str7(args.flags.transcript),
9546
9852
  dryRun,
9547
9853
  offline: bool5(args.flags.offline),
9548
- budgetMs: str6(args.flags["budget-ms"]) ? Math.max(1, Number(args.flags["budget-ms"]) || 1) : void 0
9854
+ budgetMs: str7(args.flags["budget-ms"]) ? Math.max(1, Number(args.flags["budget-ms"]) || 1) : void 0
9549
9855
  });
9550
9856
  if (bool5(args.flags.json)) {
9551
9857
  out(JSON.stringify({ served: result.served.map((l) => ({ id: l.id, kind: l.kind, title: l.title, gist: l.gist })), trace: result.trace, reasoning: result.reasoning, degraded: result.degraded, text: result.text }, null, 2) + "\n");
@@ -9596,8 +9902,8 @@ function cmdLessons(store, args, out, err) {
9596
9902
  return 0;
9597
9903
  }
9598
9904
  if (sub === "add") return cmdLessonAdd(store, args, out, err);
9599
- const kind = str6(args.flags.kind);
9600
- const q = str6(args.flags.q) ?? (sub === "list" ? args._.slice(2).join(" ") : args._.slice(1).join(" "));
9905
+ const kind = str7(args.flags.kind);
9906
+ const q = str7(args.flags.q) ?? (sub === "list" ? args._.slice(2).join(" ") : args._.slice(1).join(" "));
9601
9907
  const qTerms = q ? terms(q) : [];
9602
9908
  let lessons = store.listLessons().filter((l) => l.status !== "retired" && l.status !== "superseded");
9603
9909
  if (kind) lessons = lessons.filter((l) => l.kind === kind);
@@ -9629,23 +9935,23 @@ ${lessons.length} lesson(s). \`bunshin lessons show <id>\` for the full body.
9629
9935
  return 0;
9630
9936
  }
9631
9937
  function cmdLessonAdd(store, args, out, err) {
9632
- const title = str6(args.flags.title);
9938
+ const title = str7(args.flags.title);
9633
9939
  if (!title) {
9634
9940
  err('bunshin lessons add: --title is required (e.g. --title "Collections closes negative balances by hand")\n');
9635
9941
  return 1;
9636
9942
  }
9637
- let content = str6(args.flags.content) ?? "";
9943
+ let content = str7(args.flags.content) ?? "";
9638
9944
  if (!content || content === "-" || args._.includes("-")) {
9639
9945
  try {
9640
- content = fs26.readFileSync(0, "utf8");
9946
+ content = fs27.readFileSync(0, "utf8");
9641
9947
  } catch {
9642
9948
  }
9643
9949
  }
9644
9950
  const config = store.config();
9645
9951
  const now = nowIso();
9646
- const kind = str6(args.flags.kind) ?? "fact";
9647
- const gist = str6(args.flags.gist) ?? content.split("\n").find((l) => l.trim())?.slice(0, 200) ?? title;
9648
- const tags = (str6(args.flags.tags) ?? "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
9952
+ const kind = str7(args.flags.kind) ?? "fact";
9953
+ const gist = str7(args.flags.gist) ?? content.split("\n").find((l) => l.trim())?.slice(0, 200) ?? title;
9954
+ const tags = (str7(args.flags.tags) ?? "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
9649
9955
  const base = {
9650
9956
  id: newId("l"),
9651
9957
  title,
@@ -9653,7 +9959,7 @@ function cmdLessonAdd(store, args, out, err) {
9653
9959
  tags,
9654
9960
  content: content.trim(),
9655
9961
  status: "active",
9656
- clearance: str6(args.flags.clearance) ?? "org",
9962
+ clearance: str7(args.flags.clearance) ?? "org",
9657
9963
  provenance: {
9658
9964
  orgId: config.orgId,
9659
9965
  userId: config.userId,
@@ -9666,7 +9972,7 @@ function cmdLessonAdd(store, args, out, err) {
9666
9972
  version: 1,
9667
9973
  updatedAt: now
9668
9974
  };
9669
- const lesson = kind === "workflow" ? { ...base, kind: "workflow", preconditions: [], steps: [], outcome: str6(args.flags.outcome) ?? "Task completed as described." } : { ...base, kind: "fact" };
9975
+ const lesson = kind === "workflow" ? { ...base, kind: "workflow", preconditions: [], steps: [], outcome: str7(args.flags.outcome) ?? "Task completed as described." } : { ...base, kind: "fact" };
9670
9976
  store.saveLesson(lesson);
9671
9977
  if (bool5(args.flags.json)) out(JSON.stringify(lesson, null, 2) + "\n");
9672
9978
  else out(`Added ${lesson.id} [${lesson.kind}] ${lesson.title}
@@ -9696,14 +10002,14 @@ function renderLessonDetail(l) {
9696
10002
  return lines.join("\n") + "\n";
9697
10003
  }
9698
10004
  async function cmdReflect(store, args, cwd, out, err) {
9699
- const transcript = args._[1] ?? str6(args.flags.transcript);
10005
+ const transcript = args._[1] ?? str7(args.flags.transcript);
9700
10006
  if (!transcript) {
9701
10007
  err("bunshin reflect: a transcript path is required\n");
9702
10008
  return 1;
9703
10009
  }
9704
10010
  const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
9705
10011
  const result = await runReflection2(store, {
9706
- sessionId: str6(args.flags.session) ?? path23.basename(transcript).replace(/\.jsonl?$/, ""),
10012
+ sessionId: str7(args.flags.session) ?? path23.basename(transcript).replace(/\.jsonl?$/, ""),
9707
10013
  transcriptPath: transcript,
9708
10014
  cwd,
9709
10015
  force: bool5(args.flags.force)
@@ -9771,13 +10077,13 @@ async function cmdConflict(store, args, out, err) {
9771
10077
  const sub = args._[1] ?? "list";
9772
10078
  if (sub === "resolve") {
9773
10079
  const id = args._[2];
9774
- const answer = args._.slice(3).join(" ").trim() || str6(args.flags.answer) || "";
10080
+ const answer = args._.slice(3).join(" ").trim() || str7(args.flags.answer) || "";
9775
10081
  if (!id || !answer) {
9776
10082
  err('bunshin conflict resolve: usage: bunshin conflict resolve <id> "<answer>"\n');
9777
10083
  return 1;
9778
10084
  }
9779
10085
  const { resolveConflict: resolveConflict2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
9780
- const resolved = await resolveConflict2(store, id, answer, str6(args.flags.note));
10086
+ const resolved = await resolveConflict2(store, id, answer, str7(args.flags.note));
9781
10087
  if (bool5(args.flags.json)) out(JSON.stringify(resolved, null, 2) + "\n");
9782
10088
  else out(`Resolved ${resolved.id}: ${answer}
9783
10089
  `);
@@ -9809,7 +10115,7 @@ async function cmdConflict(store, args, out, err) {
9809
10115
  function store0HasKey(store) {
9810
10116
  return !!store.config().apiKey;
9811
10117
  }
9812
- var str6, bool5, HELP2;
10118
+ var str7, bool5, HELP2;
9813
10119
  var init_cli = __esm({
9814
10120
  "src/cli.ts"() {
9815
10121
  "use strict";
@@ -9818,7 +10124,7 @@ var init_cli = __esm({
9818
10124
  init_store();
9819
10125
  init_util();
9820
10126
  init_project_hooks();
9821
- str6 = (v) => typeof v === "string" ? v : void 0;
10127
+ str7 = (v) => typeof v === "string" ? v : void 0;
9822
10128
  bool5 = (v) => v === true || v === "true";
9823
10129
  HELP2 = `bunshin \u2014 shadow-clone memory for coding agents and robots
9824
10130
 
@@ -9870,6 +10176,12 @@ Usage: bunshin <command> [options]
9870
10176
  repo import|attach|clone|verify|list|url|login
9871
10177
  host a git repository in Bunshin (full history; git push/pull
9872
10178
  keep working through git-remote-bunshin). bunshin repo help
10179
+ runner [--once|--watch] [--backend auto|sdk|claude-cli|codex-cli|pi-cli] [--model <m>] [--kinds reflect,merge,create]
10180
+ answer the company's queued model work (cloud reflection,
10181
+ reconciliation) with this machine's model. BUNSHIN_RUNNER_KEY
10182
+ runner next | submit <job> <file|-> | fail <job> --error <why>
10183
+ agent mode, for a Claude Code routine or Codex cloud task
10184
+ runner status | setup [claude-routine|codex-cloud|github-action|docker|cron]
9873
10185
  reflect --fork --session <id> [--focus "<text>"] (what the session-end hook detaches)
9874
10186
 
9875
10187
  lessons [list] [--kind fact|workflow] [--q <query>] [--json] (legacy lesson store)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.4.8",
3
+ "version": "0.5.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": {