@bunshin-cc/bunshin 0.4.7 → 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.
- package/dist/bin.js +403 -89
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -189,7 +189,7 @@ var init_store = __esm({
|
|
|
189
189
|
"use strict";
|
|
190
190
|
init_frontmatter();
|
|
191
191
|
init_util();
|
|
192
|
-
RETIRED_DEFAULT_MODELS = /* @__PURE__ */ new Set(["claude-fable-5-1"]);
|
|
192
|
+
RETIRED_DEFAULT_MODELS = /* @__PURE__ */ new Set(["claude-fable-5-1", "claude-opus-5-5"]);
|
|
193
193
|
DEFAULT_CONFIG = {
|
|
194
194
|
orgId: "local",
|
|
195
195
|
userId: process.env.USER || "me",
|
|
@@ -200,7 +200,8 @@ var init_store = __esm({
|
|
|
200
200
|
apiUrl: "",
|
|
201
201
|
scope: "repo",
|
|
202
202
|
harness: "claude-code",
|
|
203
|
-
|
|
203
|
+
/** `[1m]` asks Claude Code for the 1M-token context window; API calls strip it (apiModelId). */
|
|
204
|
+
model: "claude-opus-5-5[1m]",
|
|
204
205
|
gateModel: "claude-haiku-4-5-20251001",
|
|
205
206
|
backend: "auto",
|
|
206
207
|
recall: { enabled: true, maxLessons: 4, hookBudgetMs: 3500, useMothership: true },
|
|
@@ -1854,6 +1855,18 @@ At most ${max} suggestions. Schema:
|
|
|
1854
1855
|
"signal": "steering"|"environment"|"knowledge"|"capability"|"approach"}]}`;
|
|
1855
1856
|
}
|
|
1856
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
|
+
}
|
|
1857
1870
|
const fenced = text2.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1858
1871
|
const candidate = fenced ? fenced[1] : text2;
|
|
1859
1872
|
const start = candidate.indexOf("{");
|
|
@@ -2028,6 +2041,20 @@ var init_reconcile = __esm({
|
|
|
2028
2041
|
}
|
|
2029
2042
|
});
|
|
2030
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
|
+
|
|
2031
2058
|
// ../core/src/v2/release.ts
|
|
2032
2059
|
var init_release = __esm({
|
|
2033
2060
|
"../core/src/v2/release.ts"() {
|
|
@@ -2064,6 +2091,7 @@ var init_v2 = __esm({
|
|
|
2064
2091
|
init_engine();
|
|
2065
2092
|
init_reflect();
|
|
2066
2093
|
init_reconcile();
|
|
2094
|
+
init_merge();
|
|
2067
2095
|
init_release();
|
|
2068
2096
|
init_research();
|
|
2069
2097
|
init_profile();
|
|
@@ -3377,27 +3405,27 @@ async function askSdk(opts) {
|
|
|
3377
3405
|
const client = new Anthropic({ maxRetries: 1 });
|
|
3378
3406
|
const isHaiku = /haiku/.test(opts.model);
|
|
3379
3407
|
const base = {
|
|
3380
|
-
model: opts.model,
|
|
3408
|
+
model: apiModelId(opts.model),
|
|
3381
3409
|
max_tokens: opts.maxTokens ?? 16e3,
|
|
3382
3410
|
system: opts.system,
|
|
3383
3411
|
messages: [{ role: "user", content: opts.user }]
|
|
3384
3412
|
};
|
|
3385
|
-
const format = { type: "json_schema", schema: opts.schema };
|
|
3413
|
+
const format = opts.enforceSchema === false ? void 0 : { type: "json_schema", schema: opts.schema };
|
|
3386
3414
|
const reqOpts = { timeout: opts.timeoutMs ?? 3e5 };
|
|
3387
3415
|
let response;
|
|
3388
3416
|
try {
|
|
3389
|
-
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(
|
|
3390
3418
|
{
|
|
3391
3419
|
...base,
|
|
3392
3420
|
betas: ["server-side-fallback-2026-07-01"],
|
|
3393
3421
|
fallbacks: "default",
|
|
3394
|
-
output_config: { format, effort: opts.effort ?? "medium" }
|
|
3422
|
+
output_config: { ...format ? { format } : {}, effort: opts.effort ?? "medium" }
|
|
3395
3423
|
},
|
|
3396
3424
|
reqOpts
|
|
3397
3425
|
);
|
|
3398
3426
|
} catch (err) {
|
|
3399
3427
|
if (!(err instanceof Anthropic.BadRequestError)) throw err;
|
|
3400
|
-
response = await client.messages.create({ ...base, output_config: { format } }, reqOpts);
|
|
3428
|
+
response = await client.messages.create({ ...base, ...format ? { output_config: { format } } : {} }, reqOpts);
|
|
3401
3429
|
}
|
|
3402
3430
|
if (response.stop_reason === "refusal") throw new Error("model refused the request");
|
|
3403
3431
|
return response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
@@ -3519,10 +3547,11 @@ function parseJsonLoose(text2) {
|
|
|
3519
3547
|
}
|
|
3520
3548
|
return void 0;
|
|
3521
3549
|
}
|
|
3522
|
-
var CLAUDE_SESSION_ENV;
|
|
3550
|
+
var apiModelId, CLAUDE_SESSION_ENV;
|
|
3523
3551
|
var init_llm = __esm({
|
|
3524
3552
|
"src/llm.ts"() {
|
|
3525
3553
|
"use strict";
|
|
3554
|
+
apiModelId = (model) => model.replace(/\[[^\]]*\]$/, "");
|
|
3526
3555
|
CLAUDE_SESSION_ENV = [
|
|
3527
3556
|
"CLAUDE_CODE_SESSION_ID",
|
|
3528
3557
|
"CLAUDE_CODE_REMOTE_SESSION_ID",
|
|
@@ -4282,7 +4311,7 @@ async function syncV2(store, opts = {}) {
|
|
|
4282
4311
|
report.errors.push("sync already running in another process; work stays queued");
|
|
4283
4312
|
return report;
|
|
4284
4313
|
}
|
|
4285
|
-
const
|
|
4314
|
+
const call2 = (method, route, body) => v2api(config, method, route, body, { deadline, timeoutMs: 15e3 });
|
|
4286
4315
|
const ctx = contextFor(v2, config);
|
|
4287
4316
|
const deploying = (opts.deploy ?? true) && deployEnabled(config, ctx);
|
|
4288
4317
|
try {
|
|
@@ -4296,7 +4325,7 @@ async function syncV2(store, opts = {}) {
|
|
|
4296
4325
|
for (const e of v2.listEpisodes().filter((x) => !x.uploaded)) {
|
|
4297
4326
|
if (Date.now() >= deadline) break;
|
|
4298
4327
|
try {
|
|
4299
|
-
const res = await
|
|
4328
|
+
const res = await call2("POST", "/episodes", { episode: episodePayload(e) });
|
|
4300
4329
|
if (res && (typeof res.reflectedTurns === "number" || Array.isArray(res.lessons)) && v2.readSession(e.sessionId)) {
|
|
4301
4330
|
v2.updateSession(e.sessionId, "", (s) => {
|
|
4302
4331
|
if (typeof res.reflectedTurns === "number") s.serverReflectedTurns = Math.max(s.serverReflectedTurns ?? 0, res.reflectedTurns);
|
|
@@ -4311,8 +4340,8 @@ async function syncV2(store, opts = {}) {
|
|
|
4311
4340
|
const served = e.sectionsServed.filter((id) => !isLocalId(id) && !doneServed.has(id));
|
|
4312
4341
|
const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id) && !doneLoaded.has(id));
|
|
4313
4342
|
try {
|
|
4314
|
-
if (served.length) await
|
|
4315
|
-
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 });
|
|
4316
4345
|
next.reportedServed = [...doneServed, ...served];
|
|
4317
4346
|
next.reportedLoaded = [...doneLoaded, ...loaded];
|
|
4318
4347
|
delete next.servedReported;
|
|
@@ -4328,7 +4357,7 @@ async function syncV2(store, opts = {}) {
|
|
|
4328
4357
|
const pending = v2.listSuggestions().filter((s) => s.status === "submitted" && !s.uploaded && s.op.op !== "param");
|
|
4329
4358
|
if (pending.length && Date.now() < deadline) {
|
|
4330
4359
|
try {
|
|
4331
|
-
const res = await
|
|
4360
|
+
const res = await call2("POST", "/suggestions", { suggestions: pending.map(suggestionPayload) });
|
|
4332
4361
|
const ok = new Set(Array.isArray(res.ids) ? res.ids : pending.map((s) => s.id));
|
|
4333
4362
|
for (const s of pending) {
|
|
4334
4363
|
if (!ok.has(s.id)) continue;
|
|
@@ -4343,7 +4372,7 @@ async function syncV2(store, opts = {}) {
|
|
|
4343
4372
|
for (const s of v2.listSuggestions().filter((x) => x.uploaded && x.trialDirty)) {
|
|
4344
4373
|
if (Date.now() >= deadline) break;
|
|
4345
4374
|
try {
|
|
4346
|
-
await
|
|
4375
|
+
await call2("PATCH", `/suggestions/${encodeURIComponent(s.id)}/trial`, s.trial);
|
|
4347
4376
|
v2.writeSuggestion({ ...v2.readSuggestion(s.id) ?? s, trialDirty: false });
|
|
4348
4377
|
report.trialUpdates++;
|
|
4349
4378
|
} catch (err) {
|
|
@@ -4356,7 +4385,7 @@ async function syncV2(store, opts = {}) {
|
|
|
4356
4385
|
const q = new URLSearchParams();
|
|
4357
4386
|
if (previous?.generatedAt) q.set("since", previous.generatedAt);
|
|
4358
4387
|
if (previous?.libraryVersion && previous.sections.length) q.set("have", String(previous.libraryVersion));
|
|
4359
|
-
const bundle = await
|
|
4388
|
+
const bundle = await call2("GET", `/bundle${q.size ? `?${q}` : ""}`);
|
|
4360
4389
|
if (bundle && Array.isArray(bundle.sections)) {
|
|
4361
4390
|
const next = bundle.unchanged && previous ? { ...bundle, sections: previous.sections, unchanged: void 0 } : bundle;
|
|
4362
4391
|
delete next.unchanged;
|
|
@@ -5807,7 +5836,10 @@ function mark(v2, sessionId, cwd, field, ids) {
|
|
|
5807
5836
|
try {
|
|
5808
5837
|
v2.updateSession(sessionId, cwd, (s) => {
|
|
5809
5838
|
addUnique(s.served, ...ids);
|
|
5810
|
-
if (field === "loaded")
|
|
5839
|
+
if (field === "loaded") {
|
|
5840
|
+
addUnique(s.loaded, ...ids);
|
|
5841
|
+
if (s.inContext) addUnique(s.inContext, ...ids);
|
|
5842
|
+
}
|
|
5811
5843
|
s.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5812
5844
|
});
|
|
5813
5845
|
} catch {
|
|
@@ -8145,9 +8177,276 @@ Common flags: --api-url URL (default: this project's, else https://api.bunshin.c
|
|
|
8145
8177
|
}
|
|
8146
8178
|
});
|
|
8147
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
|
+
|
|
8148
8447
|
// src/v2/importer.ts
|
|
8149
8448
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
8150
|
-
import
|
|
8449
|
+
import fs24 from "node:fs";
|
|
8151
8450
|
import path20 from "node:path";
|
|
8152
8451
|
function rewriteRepoPaths(text2, repoName2, pack) {
|
|
8153
8452
|
const re = new RegExp(`(?<=^|[\\s\`'"(=:])(?:~|\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?)?(?:/[\\w.@+-]+)*/${escapeRe(repoName2)}(?=[/\\s\`'")\\]]|[.,;:](?:\\s|$)|$)`, "gm");
|
|
@@ -8285,7 +8584,7 @@ function isGitRepo(root) {
|
|
|
8285
8584
|
function workingTreeFiles(root) {
|
|
8286
8585
|
const out = /* @__PURE__ */ new Map();
|
|
8287
8586
|
const list2 = isGitRepo(root) ? git3(root, ["ls-files", "-z"]).split("\0").filter(Boolean) : (function walk(dir, base = "") {
|
|
8288
|
-
return
|
|
8587
|
+
return fs24.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
8289
8588
|
if (e.name === ".git" || e.name === "node_modules") return [];
|
|
8290
8589
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
8291
8590
|
return e.isDirectory() ? walk(path20.join(dir, e.name), rel) : e.isFile() ? [rel] : [];
|
|
@@ -8295,12 +8594,12 @@ function workingTreeFiles(root) {
|
|
|
8295
8594
|
const abs = path20.join(root, rel);
|
|
8296
8595
|
let buf;
|
|
8297
8596
|
try {
|
|
8298
|
-
buf =
|
|
8597
|
+
buf = fs24.readFileSync(abs);
|
|
8299
8598
|
} catch {
|
|
8300
8599
|
continue;
|
|
8301
8600
|
}
|
|
8302
8601
|
if (!isText(buf)) continue;
|
|
8303
|
-
const exec = (
|
|
8602
|
+
const exec = (fs24.statSync(abs).mode & 73) !== 0;
|
|
8304
8603
|
out.set(rel, { content: buf.toString("utf8"), ...exec ? { mode: "x" } : {} });
|
|
8305
8604
|
}
|
|
8306
8605
|
return out;
|
|
@@ -8447,7 +8746,7 @@ __export(skills_exports, {
|
|
|
8447
8746
|
cmdSkill: () => cmdSkill,
|
|
8448
8747
|
parsePin: () => parsePin
|
|
8449
8748
|
});
|
|
8450
|
-
import
|
|
8749
|
+
import fs25 from "node:fs";
|
|
8451
8750
|
import path21 from "node:path";
|
|
8452
8751
|
function requireOnline(store, what) {
|
|
8453
8752
|
if (!canCall(store.config())) throw new Error(`${what} needs the hosted library: run \`bunshin setup\` first`);
|
|
@@ -8458,10 +8757,10 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
8458
8757
|
return 1;
|
|
8459
8758
|
}
|
|
8460
8759
|
const root = path21.resolve(cwd, args._[2]);
|
|
8461
|
-
if (!
|
|
8462
|
-
const pack =
|
|
8760
|
+
if (!fs25.existsSync(root)) throw new Error(`${root} does not exist`);
|
|
8761
|
+
const pack = str6(args.flags.pack) ?? path21.basename(root);
|
|
8463
8762
|
const history = args.flags.history !== false && !bool4(args.flags["no-history"]);
|
|
8464
|
-
const read = readSkillSystem(root, { pack, history, ...
|
|
8763
|
+
const read = readSkillSystem(root, { pack, history, ...str6(args.flags.since) ? { since: str6(args.flags.since) } : {} });
|
|
8465
8764
|
const kinds = /* @__PURE__ */ new Map();
|
|
8466
8765
|
for (const d of read.docs) {
|
|
8467
8766
|
const label2 = d.delivery === "always" ? "instructions" : d.delivery === "stop" ? "learning check" : d.kind;
|
|
@@ -8477,7 +8776,7 @@ async function cmdImport2(store, args, cwd, out, err) {
|
|
|
8477
8776
|
}
|
|
8478
8777
|
requireOnline(store, "bunshin import");
|
|
8479
8778
|
const config = store.config();
|
|
8480
|
-
const body = { pack, ...
|
|
8779
|
+
const body = { pack, ...str6(args.flags.layer) ? { layer: str6(args.flags.layer) } : {}, ...str6(args.flags["owner-team"]) ? { ownerTeam: str6(args.flags["owner-team"]) } : {} };
|
|
8481
8780
|
let written = 0;
|
|
8482
8781
|
let last = null;
|
|
8483
8782
|
const batches = batchVersions(read.versions);
|
|
@@ -8605,7 +8904,7 @@ async function cmdSkill(store, args, cwd, out, err) {
|
|
|
8605
8904
|
}
|
|
8606
8905
|
case "log": {
|
|
8607
8906
|
requireOnline(store, "bunshin skill log");
|
|
8608
|
-
const since =
|
|
8907
|
+
const since = str6(args.flags.since);
|
|
8609
8908
|
const cutoff = since ? sinceCutoff(since) : 0;
|
|
8610
8909
|
const targets = args._[2] ? [byName(args._[2])].filter((x) => !!x) : skills;
|
|
8611
8910
|
if (!targets.length) throw new Error(`no skill ${args._[2]}`);
|
|
@@ -8632,9 +8931,9 @@ async function cmdSkill(store, args, cwd, out, err) {
|
|
|
8632
8931
|
${report.skills} skills, ${report.references} references, ${report.errors.length} errors, ${report.warnings.length} warnings
|
|
8633
8932
|
`);
|
|
8634
8933
|
}
|
|
8635
|
-
const graph =
|
|
8934
|
+
const graph = str6(args.flags.graph) ?? (bool4(args.flags.graph) ? "SKILLS-GRAPH.md" : void 0);
|
|
8636
8935
|
if (graph) {
|
|
8637
|
-
|
|
8936
|
+
fs25.writeFileSync(path21.resolve(cwd, graph), renderSkillGraph(report));
|
|
8638
8937
|
out(`graph written to ${graph}
|
|
8639
8938
|
`);
|
|
8640
8939
|
}
|
|
@@ -8643,7 +8942,7 @@ ${report.skills} skills, ${report.references} references, ${report.errors.length
|
|
|
8643
8942
|
case "push": {
|
|
8644
8943
|
const ctx = contextFor(v2, config);
|
|
8645
8944
|
const name = args._[2];
|
|
8646
|
-
const layer =
|
|
8945
|
+
const layer = str6(args.flags.layer);
|
|
8647
8946
|
const captured = captureEdits(v2, config, ctx, { libraryNames: skills.map((s) => s.skill.name), ...layer ? { newSkillLayer: layer } : {}, ...name && !byName(name) ? { newSkill: name } : {} });
|
|
8648
8947
|
for (const c of captured) out(`proposed: ${c.name}
|
|
8649
8948
|
`);
|
|
@@ -8694,7 +8993,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
8694
8993
|
}
|
|
8695
8994
|
case "log": {
|
|
8696
8995
|
requireOnline(store, "bunshin library log");
|
|
8697
|
-
const res = await v2api(config, "GET", `/library?limit=${Number(
|
|
8996
|
+
const res = await v2api(config, "GET", `/library?limit=${Number(str6(args.flags.limit) ?? 30) || 30}`);
|
|
8698
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)
|
|
8699
8998
|
`);
|
|
8700
8999
|
return 0;
|
|
@@ -8731,27 +9030,27 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
8731
9030
|
const res = await v2api(config, "GET", "/library?limit=1");
|
|
8732
9031
|
const line = `bunshin: lib@${res.version}
|
|
8733
9032
|
`;
|
|
8734
|
-
const file =
|
|
9033
|
+
const file = str6(args.flags.write) ?? (bool4(args.flags.write) ? ".claude/skills-version" : void 0);
|
|
8735
9034
|
if (file) {
|
|
8736
9035
|
const abs = path21.resolve(cwd, file);
|
|
8737
|
-
|
|
8738
|
-
|
|
9036
|
+
fs25.mkdirSync(path21.dirname(abs), { recursive: true });
|
|
9037
|
+
fs25.writeFileSync(abs, line);
|
|
8739
9038
|
out(`${file}: ${line}`);
|
|
8740
9039
|
} else out(line);
|
|
8741
9040
|
return 0;
|
|
8742
9041
|
}
|
|
8743
9042
|
case "checkout": {
|
|
8744
9043
|
requireOnline(store, "bunshin library checkout");
|
|
8745
|
-
const to =
|
|
9044
|
+
const to = str6(args.flags.to);
|
|
8746
9045
|
if (!to) throw new Error("--to <dir> is required");
|
|
8747
|
-
const pinFile =
|
|
8748
|
-
const pin =
|
|
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] ?? "");
|
|
8749
9048
|
const q = pin.git ? `git=${pin.git}` : pin.version ? `version=${pin.version}` : "";
|
|
8750
9049
|
if (!q) throw new Error("name a version: lib@N, --git <sha>, or --pin <file>");
|
|
8751
9050
|
const snap = await v2api(config, "GET", `/library/snapshot?${q}`, void 0, { timeoutMs: 12e4 });
|
|
8752
9051
|
const dir = path21.resolve(cwd, to);
|
|
8753
|
-
if (
|
|
8754
|
-
const 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);
|
|
8755
9054
|
const docs = snap.sections.filter((s) => !pack || s.pack === pack);
|
|
8756
9055
|
const res = materialize(docs, dir);
|
|
8757
9056
|
out(`lib@${snap.version}${pin.git ? ` (commit ${pin.git})` : ""}: ${res.files} files in ${dir}
|
|
@@ -8764,7 +9063,7 @@ async function cmdLibrary(store, args, cwd, out, err) {
|
|
|
8764
9063
|
return 1;
|
|
8765
9064
|
}
|
|
8766
9065
|
}
|
|
8767
|
-
var
|
|
9066
|
+
var str6, bool4, skillsOf;
|
|
8768
9067
|
var init_skills = __esm({
|
|
8769
9068
|
"src/v2/skills.ts"() {
|
|
8770
9069
|
"use strict";
|
|
@@ -8776,7 +9075,7 @@ var init_skills = __esm({
|
|
|
8776
9075
|
init_store2();
|
|
8777
9076
|
init_sync();
|
|
8778
9077
|
init_view();
|
|
8779
|
-
|
|
9078
|
+
str6 = (v) => typeof v === "string" ? v : void 0;
|
|
8780
9079
|
bool4 = (v) => v === true || v === "true";
|
|
8781
9080
|
skillsOf = (sections) => sections.filter((s) => s.skill && (s.status === "active" || s.status === "stale"));
|
|
8782
9081
|
}
|
|
@@ -8789,7 +9088,7 @@ __export(reflect_exports, {
|
|
|
8789
9088
|
buildLesson: () => buildLesson,
|
|
8790
9089
|
runReflection: () => runReflection
|
|
8791
9090
|
});
|
|
8792
|
-
import
|
|
9091
|
+
import fs26 from "node:fs";
|
|
8793
9092
|
import path22 from "node:path";
|
|
8794
9093
|
import { createHash as createHash7 } from "node:crypto";
|
|
8795
9094
|
async function runReflection(store, opts) {
|
|
@@ -8798,18 +9097,18 @@ async function runReflection(store, opts) {
|
|
|
8798
9097
|
const deadline = Date.now() + 66e4;
|
|
8799
9098
|
for (; ; ) {
|
|
8800
9099
|
try {
|
|
8801
|
-
|
|
9100
|
+
fs26.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
8802
9101
|
break;
|
|
8803
9102
|
} catch (error) {
|
|
8804
9103
|
if (error.code !== "EEXIST") throw error;
|
|
8805
9104
|
try {
|
|
8806
|
-
const owner = Number(
|
|
9105
|
+
const owner = Number(fs26.readFileSync(lock, "utf8"));
|
|
8807
9106
|
if (Number.isInteger(owner) && owner > 0) {
|
|
8808
9107
|
try {
|
|
8809
9108
|
process.kill(owner, 0);
|
|
8810
9109
|
} catch (error2) {
|
|
8811
9110
|
if (error2.code === "ESRCH") {
|
|
8812
|
-
|
|
9111
|
+
fs26.unlinkSync(lock);
|
|
8813
9112
|
continue;
|
|
8814
9113
|
}
|
|
8815
9114
|
}
|
|
@@ -8824,7 +9123,7 @@ async function runReflection(store, opts) {
|
|
|
8824
9123
|
return await reflectSession(store, opts);
|
|
8825
9124
|
} finally {
|
|
8826
9125
|
try {
|
|
8827
|
-
|
|
9126
|
+
fs26.unlinkSync(lock);
|
|
8828
9127
|
} catch {
|
|
8829
9128
|
}
|
|
8830
9129
|
}
|
|
@@ -9166,11 +9465,11 @@ __export(cli_exports, {
|
|
|
9166
9465
|
parseArgs: () => parseArgs,
|
|
9167
9466
|
version: () => version
|
|
9168
9467
|
});
|
|
9169
|
-
import
|
|
9468
|
+
import fs27 from "node:fs";
|
|
9170
9469
|
import path23 from "node:path";
|
|
9171
9470
|
function parseArgs(argv) {
|
|
9172
9471
|
const out = { _: [], flags: {} };
|
|
9173
|
-
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"]);
|
|
9174
9473
|
for (let i = 0; i < argv.length; i++) {
|
|
9175
9474
|
const a = argv[i];
|
|
9176
9475
|
if (a === "--") {
|
|
@@ -9213,7 +9512,7 @@ function version() {
|
|
|
9213
9512
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
9214
9513
|
try {
|
|
9215
9514
|
const file = new URL(rel, import.meta.url);
|
|
9216
|
-
const pkg = JSON.parse(
|
|
9515
|
+
const pkg = JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
9217
9516
|
if (pkg.name === "@bunshin-cc/bunshin" && pkg.version) return pkg.version;
|
|
9218
9517
|
} catch {
|
|
9219
9518
|
}
|
|
@@ -9236,13 +9535,13 @@ async function main(argv, opts = {}) {
|
|
|
9236
9535
|
return 0;
|
|
9237
9536
|
}
|
|
9238
9537
|
if (cmd === "hook") {
|
|
9239
|
-
const root =
|
|
9538
|
+
const root = str7(args.flags.root);
|
|
9240
9539
|
return hookCommand(args._[1], { cwd, store: root ? new Store(root) : void 0, write: out });
|
|
9241
9540
|
}
|
|
9242
9541
|
if (cmd === "setup" && bool5(args.flags.robot)) {
|
|
9243
9542
|
try {
|
|
9244
9543
|
const { setupRobot: setupRobot2, robotAttrsFromFlags: robotAttrsFromFlags2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
9245
|
-
const robotId =
|
|
9544
|
+
const robotId = str7(args.flags["robot-id"]);
|
|
9246
9545
|
if (!robotId) throw new Error("--robot-id <id> is required with --robot");
|
|
9247
9546
|
const apiKey = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
9248
9547
|
let value = "";
|
|
@@ -9253,9 +9552,9 @@ async function main(argv, opts = {}) {
|
|
|
9253
9552
|
process.stdin.on("end", () => resolve(value.trim()));
|
|
9254
9553
|
process.stdin.on("error", reject);
|
|
9255
9554
|
}) : process.env.BUNSHIN_API_KEY || void 0;
|
|
9256
|
-
const harnessFlag =
|
|
9555
|
+
const harnessFlag = str7(args.flags.harness);
|
|
9257
9556
|
if (harnessFlag && !["claude-code", "codex", "pi", "other"].includes(harnessFlag)) throw new Error("--harness must be claude-code, codex, pi or other");
|
|
9258
|
-
const res = await setupRobot2({ cwd, robotId, robot: robotAttrsFromFlags2(args.flags), apiUrl:
|
|
9557
|
+
const res = await setupRobot2({ cwd, robotId, robot: robotAttrsFromFlags2(args.flags), apiUrl: str7(args.flags["api-url"]), apiKey, ...harnessFlag ? { harness: harnessFlag } : {} });
|
|
9259
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})
|
|
9260
9559
|
`);
|
|
9261
9560
|
out(` store: ${res.store.root}/v2/context.json
|
|
@@ -9274,12 +9573,12 @@ async function main(argv, opts = {}) {
|
|
|
9274
9573
|
if (cmd === "setup") {
|
|
9275
9574
|
try {
|
|
9276
9575
|
const { setupProject: setupProject2, executable: executable2, readKey: readKey2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
9277
|
-
const harness =
|
|
9576
|
+
const harness = str7(args.flags.harness);
|
|
9278
9577
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
9279
9578
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
9280
9579
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
9281
9580
|
const existing = new Store(path23.join(cwd, ".bunshin")).config();
|
|
9282
|
-
const apiUrl =
|
|
9581
|
+
const apiUrl = str7(args.flags["api-url"]);
|
|
9283
9582
|
const key = bool5(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
9284
9583
|
let value = "";
|
|
9285
9584
|
process.stdin.setEncoding("utf8");
|
|
@@ -9320,7 +9619,7 @@ Key saved privately outside the project.
|
|
|
9320
9619
|
if (cmd === "init") return cmdInit(args, cwd, out, err);
|
|
9321
9620
|
if (cmd === "mcp") {
|
|
9322
9621
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
9323
|
-
const root =
|
|
9622
|
+
const root = str7(args.flags.root);
|
|
9324
9623
|
await runMcpServer2({ cwd, ...root ? { store: new Store(path23.resolve(root)) } : {}, version: version() });
|
|
9325
9624
|
return 0;
|
|
9326
9625
|
}
|
|
@@ -9330,6 +9629,15 @@ Key saved privately outside the project.
|
|
|
9330
9629
|
return 0;
|
|
9331
9630
|
}
|
|
9332
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
|
+
}
|
|
9333
9641
|
const store = openStore(args, cwd);
|
|
9334
9642
|
if (!store) {
|
|
9335
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");
|
|
@@ -9393,7 +9701,7 @@ ${HELP2}`);
|
|
|
9393
9701
|
}
|
|
9394
9702
|
}
|
|
9395
9703
|
function openStore(args, cwd) {
|
|
9396
|
-
const root =
|
|
9704
|
+
const root = str7(args.flags.root);
|
|
9397
9705
|
if (root) {
|
|
9398
9706
|
const store = new Store(path23.resolve(root));
|
|
9399
9707
|
store.ensureLayout();
|
|
@@ -9403,21 +9711,21 @@ function openStore(args, cwd) {
|
|
|
9403
9711
|
}
|
|
9404
9712
|
async function cmdInit(args, cwd, out, err) {
|
|
9405
9713
|
const { runInit: runInit2, otherHarnessInstructions: otherHarnessInstructions2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
9406
|
-
const harness =
|
|
9714
|
+
const harness = str7(args.flags.harness);
|
|
9407
9715
|
const result = runInit2({
|
|
9408
9716
|
cwd,
|
|
9409
|
-
role:
|
|
9410
|
-
orgUnit:
|
|
9411
|
-
orgId:
|
|
9412
|
-
userId:
|
|
9413
|
-
apiUrl:
|
|
9414
|
-
apiKey:
|
|
9415
|
-
scope: bool5(args.flags.global) ? "global" :
|
|
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),
|
|
9416
9724
|
harness,
|
|
9417
9725
|
noHooks: args.flags.hooks === false || bool5(args.flags["no-hooks"]),
|
|
9418
9726
|
removeHooks: bool5(args.flags["remove-hooks"]),
|
|
9419
|
-
settingsFile:
|
|
9420
|
-
bin:
|
|
9727
|
+
settingsFile: str7(args.flags["settings-file"]),
|
|
9728
|
+
bin: str7(args.flags.bin)
|
|
9421
9729
|
});
|
|
9422
9730
|
if (bool5(args.flags.json)) {
|
|
9423
9731
|
out(JSON.stringify({ ...result, config: { ...result.config, apiKey: void 0 } }, null, 2) + "\n");
|
|
@@ -9457,7 +9765,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
9457
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")] : [];
|
|
9458
9766
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
9459
9767
|
try {
|
|
9460
|
-
return
|
|
9768
|
+
return fs27.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
9461
9769
|
} catch {
|
|
9462
9770
|
return false;
|
|
9463
9771
|
}
|
|
@@ -9474,7 +9782,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
9474
9782
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
9475
9783
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
9476
9784
|
hooks: { [config.harness]: hooks },
|
|
9477
|
-
hookFiles: hookFiles.filter((file) =>
|
|
9785
|
+
hookFiles: hookFiles.filter((file) => fs27.existsSync(file)),
|
|
9478
9786
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null,
|
|
9479
9787
|
v2
|
|
9480
9788
|
};
|
|
@@ -9515,7 +9823,7 @@ function cmdStatus(store, args, out, v2) {
|
|
|
9515
9823
|
}
|
|
9516
9824
|
function hookCount(file) {
|
|
9517
9825
|
try {
|
|
9518
|
-
const raw = JSON.parse(
|
|
9826
|
+
const raw = JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
9519
9827
|
let n = 0;
|
|
9520
9828
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
9521
9829
|
for (const g of groups ?? []) {
|
|
@@ -9528,22 +9836,22 @@ function hookCount(file) {
|
|
|
9528
9836
|
}
|
|
9529
9837
|
}
|
|
9530
9838
|
async function cmdRecall2(store, args, cwd, out, err) {
|
|
9531
|
-
const task = args._.slice(1).join(" ").trim() ||
|
|
9839
|
+
const task = args._.slice(1).join(" ").trim() || str7(args.flags.task) || "";
|
|
9532
9840
|
if (!task) {
|
|
9533
9841
|
err('bunshin recall: a task is required, e.g. bunshin recall "close an account with a negative balance"\n');
|
|
9534
9842
|
return 1;
|
|
9535
9843
|
}
|
|
9536
9844
|
const { runRecall: runRecall2 } = await Promise.resolve().then(() => (init_recall3(), recall_exports));
|
|
9537
|
-
const sessionId =
|
|
9845
|
+
const sessionId = str7(args.flags.session);
|
|
9538
9846
|
const dryRun = bool5(args.flags["dry-run"]) || !sessionId && !bool5(args.flags.apply);
|
|
9539
9847
|
const result = await runRecall2(store, {
|
|
9540
9848
|
sessionId: sessionId ?? `cli-${Date.now()}`,
|
|
9541
9849
|
prompt: task,
|
|
9542
9850
|
cwd,
|
|
9543
|
-
transcriptPath:
|
|
9851
|
+
transcriptPath: str7(args.flags.transcript),
|
|
9544
9852
|
dryRun,
|
|
9545
9853
|
offline: bool5(args.flags.offline),
|
|
9546
|
-
budgetMs:
|
|
9854
|
+
budgetMs: str7(args.flags["budget-ms"]) ? Math.max(1, Number(args.flags["budget-ms"]) || 1) : void 0
|
|
9547
9855
|
});
|
|
9548
9856
|
if (bool5(args.flags.json)) {
|
|
9549
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");
|
|
@@ -9594,8 +9902,8 @@ function cmdLessons(store, args, out, err) {
|
|
|
9594
9902
|
return 0;
|
|
9595
9903
|
}
|
|
9596
9904
|
if (sub === "add") return cmdLessonAdd(store, args, out, err);
|
|
9597
|
-
const kind =
|
|
9598
|
-
const q =
|
|
9905
|
+
const kind = str7(args.flags.kind);
|
|
9906
|
+
const q = str7(args.flags.q) ?? (sub === "list" ? args._.slice(2).join(" ") : args._.slice(1).join(" "));
|
|
9599
9907
|
const qTerms = q ? terms(q) : [];
|
|
9600
9908
|
let lessons = store.listLessons().filter((l) => l.status !== "retired" && l.status !== "superseded");
|
|
9601
9909
|
if (kind) lessons = lessons.filter((l) => l.kind === kind);
|
|
@@ -9627,23 +9935,23 @@ ${lessons.length} lesson(s). \`bunshin lessons show <id>\` for the full body.
|
|
|
9627
9935
|
return 0;
|
|
9628
9936
|
}
|
|
9629
9937
|
function cmdLessonAdd(store, args, out, err) {
|
|
9630
|
-
const title =
|
|
9938
|
+
const title = str7(args.flags.title);
|
|
9631
9939
|
if (!title) {
|
|
9632
9940
|
err('bunshin lessons add: --title is required (e.g. --title "Collections closes negative balances by hand")\n');
|
|
9633
9941
|
return 1;
|
|
9634
9942
|
}
|
|
9635
|
-
let content =
|
|
9943
|
+
let content = str7(args.flags.content) ?? "";
|
|
9636
9944
|
if (!content || content === "-" || args._.includes("-")) {
|
|
9637
9945
|
try {
|
|
9638
|
-
content =
|
|
9946
|
+
content = fs27.readFileSync(0, "utf8");
|
|
9639
9947
|
} catch {
|
|
9640
9948
|
}
|
|
9641
9949
|
}
|
|
9642
9950
|
const config = store.config();
|
|
9643
9951
|
const now = nowIso();
|
|
9644
|
-
const kind =
|
|
9645
|
-
const gist =
|
|
9646
|
-
const tags = (
|
|
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);
|
|
9647
9955
|
const base = {
|
|
9648
9956
|
id: newId("l"),
|
|
9649
9957
|
title,
|
|
@@ -9651,7 +9959,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
9651
9959
|
tags,
|
|
9652
9960
|
content: content.trim(),
|
|
9653
9961
|
status: "active",
|
|
9654
|
-
clearance:
|
|
9962
|
+
clearance: str7(args.flags.clearance) ?? "org",
|
|
9655
9963
|
provenance: {
|
|
9656
9964
|
orgId: config.orgId,
|
|
9657
9965
|
userId: config.userId,
|
|
@@ -9664,7 +9972,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
9664
9972
|
version: 1,
|
|
9665
9973
|
updatedAt: now
|
|
9666
9974
|
};
|
|
9667
|
-
const lesson = kind === "workflow" ? { ...base, kind: "workflow", preconditions: [], steps: [], outcome:
|
|
9975
|
+
const lesson = kind === "workflow" ? { ...base, kind: "workflow", preconditions: [], steps: [], outcome: str7(args.flags.outcome) ?? "Task completed as described." } : { ...base, kind: "fact" };
|
|
9668
9976
|
store.saveLesson(lesson);
|
|
9669
9977
|
if (bool5(args.flags.json)) out(JSON.stringify(lesson, null, 2) + "\n");
|
|
9670
9978
|
else out(`Added ${lesson.id} [${lesson.kind}] ${lesson.title}
|
|
@@ -9694,14 +10002,14 @@ function renderLessonDetail(l) {
|
|
|
9694
10002
|
return lines.join("\n") + "\n";
|
|
9695
10003
|
}
|
|
9696
10004
|
async function cmdReflect(store, args, cwd, out, err) {
|
|
9697
|
-
const transcript = args._[1] ??
|
|
10005
|
+
const transcript = args._[1] ?? str7(args.flags.transcript);
|
|
9698
10006
|
if (!transcript) {
|
|
9699
10007
|
err("bunshin reflect: a transcript path is required\n");
|
|
9700
10008
|
return 1;
|
|
9701
10009
|
}
|
|
9702
10010
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect4(), reflect_exports));
|
|
9703
10011
|
const result = await runReflection2(store, {
|
|
9704
|
-
sessionId:
|
|
10012
|
+
sessionId: str7(args.flags.session) ?? path23.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
9705
10013
|
transcriptPath: transcript,
|
|
9706
10014
|
cwd,
|
|
9707
10015
|
force: bool5(args.flags.force)
|
|
@@ -9769,13 +10077,13 @@ async function cmdConflict(store, args, out, err) {
|
|
|
9769
10077
|
const sub = args._[1] ?? "list";
|
|
9770
10078
|
if (sub === "resolve") {
|
|
9771
10079
|
const id = args._[2];
|
|
9772
|
-
const answer = args._.slice(3).join(" ").trim() ||
|
|
10080
|
+
const answer = args._.slice(3).join(" ").trim() || str7(args.flags.answer) || "";
|
|
9773
10081
|
if (!id || !answer) {
|
|
9774
10082
|
err('bunshin conflict resolve: usage: bunshin conflict resolve <id> "<answer>"\n');
|
|
9775
10083
|
return 1;
|
|
9776
10084
|
}
|
|
9777
10085
|
const { resolveConflict: resolveConflict2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
|
|
9778
|
-
const resolved = await resolveConflict2(store, id, answer,
|
|
10086
|
+
const resolved = await resolveConflict2(store, id, answer, str7(args.flags.note));
|
|
9779
10087
|
if (bool5(args.flags.json)) out(JSON.stringify(resolved, null, 2) + "\n");
|
|
9780
10088
|
else out(`Resolved ${resolved.id}: ${answer}
|
|
9781
10089
|
`);
|
|
@@ -9807,7 +10115,7 @@ async function cmdConflict(store, args, out, err) {
|
|
|
9807
10115
|
function store0HasKey(store) {
|
|
9808
10116
|
return !!store.config().apiKey;
|
|
9809
10117
|
}
|
|
9810
|
-
var
|
|
10118
|
+
var str7, bool5, HELP2;
|
|
9811
10119
|
var init_cli = __esm({
|
|
9812
10120
|
"src/cli.ts"() {
|
|
9813
10121
|
"use strict";
|
|
@@ -9816,7 +10124,7 @@ var init_cli = __esm({
|
|
|
9816
10124
|
init_store();
|
|
9817
10125
|
init_util();
|
|
9818
10126
|
init_project_hooks();
|
|
9819
|
-
|
|
10127
|
+
str7 = (v) => typeof v === "string" ? v : void 0;
|
|
9820
10128
|
bool5 = (v) => v === true || v === "true";
|
|
9821
10129
|
HELP2 = `bunshin \u2014 shadow-clone memory for coding agents and robots
|
|
9822
10130
|
|
|
@@ -9868,6 +10176,12 @@ Usage: bunshin <command> [options]
|
|
|
9868
10176
|
repo import|attach|clone|verify|list|url|login
|
|
9869
10177
|
host a git repository in Bunshin (full history; git push/pull
|
|
9870
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]
|
|
9871
10185
|
reflect --fork --session <id> [--focus "<text>"] (what the session-end hook detaches)
|
|
9872
10186
|
|
|
9873
10187
|
lessons [list] [--kind fact|workflow] [--q <query>] [--json] (legacy lesson store)
|