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