@bunshin-cc/bunshin 0.1.3 → 0.1.4
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/README.md +4 -0
- package/dist/bin.js +150 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ In Bunshin, open **Agent keys** and create a key for this computer. Paste it int
|
|
|
16
16
|
|
|
17
17
|
Run `codex` interactively in the same project. Review and accept its project trust prompt, then run `/hooks` and review and trust the four Bunshin hooks. Project trust and hook trust are separate steps. If `/hooks` does not list Bunshin, check that you opened the connected project and trusted its configuration. This guide tests interactive sessions; `codex exec` is not a substitute for this setup check.
|
|
18
18
|
|
|
19
|
+
For a linked Git worktree, setup puts Codex hooks in the primary checkout, where Codex reads them. The printed path identifies that shared hook file. Each connected worktree keeps its own `.bunshin` store and private connection. Worktrees created from a bare repository are not supported yet; use a full checkout.
|
|
20
|
+
|
|
19
21
|
For Claude Code, use `bunshin setup --harness claude-code`, then start a new session in the project.
|
|
20
22
|
|
|
21
23
|
For Pi, use Node.js 22.19 or later and run `bunshin setup --harness pi`. It installs a project extension in `.pi/extensions/bunshin.js`. Start Pi in the project (or run `/reload` in an existing session), review its project resources, and check `/bunshin status`. The extension ships in this npm package; you do not need a second package or a repository checkout.
|
|
@@ -43,4 +45,6 @@ npm uninstall -g @bunshin-cc/bunshin
|
|
|
43
45
|
|
|
44
46
|
Use `--harness claude-code` to remove Claude Code hooks or `--harness pi` to remove the Pi extension. Revoke this computer’s key in Bunshin when disconnecting it. Removing hooks keeps local lessons.
|
|
45
47
|
|
|
48
|
+
Codex hooks are shared across a repository's linked worktrees. Removing them from one removes the repository's Bunshin hooks for all its worktrees. Other tools' hooks are preserved.
|
|
49
|
+
|
|
46
50
|
Full setup guide: [bunshin.cc/docs](https://bunshin.cc/docs/).
|
package/dist/bin.js
CHANGED
|
@@ -391,6 +391,10 @@ var init_store = __esm({
|
|
|
391
391
|
saveConflict(conflict) {
|
|
392
392
|
writeJsonAtomic(path2.join(this.conflictsDir, `${conflict.id}.json`), conflict);
|
|
393
393
|
}
|
|
394
|
+
removeCachedConflict(id) {
|
|
395
|
+
if (!/^c_[a-f0-9]{8}$/.test(id)) throw new Error("Invalid cached conflict ID");
|
|
396
|
+
fs2.rmSync(path2.join(this.conflictsDir, `${id}.json`), { force: true });
|
|
397
|
+
}
|
|
394
398
|
readMetrics(fallback) {
|
|
395
399
|
return readJson(this.metricsFile, fallback);
|
|
396
400
|
}
|
|
@@ -491,6 +495,13 @@ async function runSync(store) {
|
|
|
491
495
|
store.saveConflict(c);
|
|
492
496
|
result.pulledConflicts++;
|
|
493
497
|
}
|
|
498
|
+
const pending = store.listConflicts().filter((c) => c.status === "open").map((c) => c.id);
|
|
499
|
+
for (let start = 0; start < pending.length; start += 60) {
|
|
500
|
+
const ids = pending.slice(start, start + 60);
|
|
501
|
+
const refreshed = await api(config, "POST", "/conflicts/lookup", { ids });
|
|
502
|
+
for (const c of refreshed.conflicts) if (ids.includes(c.id)) store.saveConflict(c);
|
|
503
|
+
for (const id of refreshed.unavailableIds) if (ids.includes(id)) store.removeCachedConflict(id);
|
|
504
|
+
}
|
|
494
505
|
} catch (e) {
|
|
495
506
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
496
507
|
}
|
|
@@ -503,8 +514,7 @@ async function resolveConflict(store, id, answer, note) {
|
|
|
503
514
|
const local = store.listConflicts().find((c) => c.id === id);
|
|
504
515
|
if (!local) throw new Error(`no pending conflict ${id}`);
|
|
505
516
|
const resolution = { chosen: answer, by: config.userId, at: nowIso(), note };
|
|
506
|
-
|
|
507
|
-
const resolved = { ...local, status: "resolved", resolution };
|
|
517
|
+
const resolved = canSync(config) ? (await api(config, "POST", `/conflicts/${encodeURIComponent(id)}/resolve`, resolution)).conflict : { ...local, status: "resolved", resolution };
|
|
508
518
|
store.saveConflict(resolved);
|
|
509
519
|
store.log("conflict-resolve", { id, answer });
|
|
510
520
|
return resolved;
|
|
@@ -517,7 +527,7 @@ function pendingConflictQuestion(store, asked) {
|
|
|
517
527
|
const options = c.options?.length ? `
|
|
518
528
|
Options: ${c.options.map((o, i) => `(${i + 1}) ${o}`).join(" ")}` : "";
|
|
519
529
|
const text = `## Knowledge conflict to resolve (Bunshin)
|
|
520
|
-
The organisation's knowledge base has an unresolved ${c.kind} that only a human can settle. When it fits naturally (not before doing the user's request), ask the user this
|
|
530
|
+
The organisation's knowledge base has an unresolved ${c.kind} that only a human can settle. When it fits naturally (not before doing the user's request), ask the user this question. They can compare the documents and save an explicit keep/retire decision in the Bunshin workspace's Conflicts page. Do not treat a free-text answer as permission to reactivate every document.
|
|
521
531
|
|
|
522
532
|
> ${c.question}${options}
|
|
523
533
|
|
|
@@ -716,6 +726,41 @@ var init_llm = __esm({
|
|
|
716
726
|
}
|
|
717
727
|
});
|
|
718
728
|
|
|
729
|
+
// src/project-hooks.ts
|
|
730
|
+
import fs4 from "node:fs";
|
|
731
|
+
import path5 from "node:path";
|
|
732
|
+
import { execFileSync } from "node:child_process";
|
|
733
|
+
function codexProjectHooks(cwd) {
|
|
734
|
+
const legacyFile = path5.join(cwd, ".codex", "hooks.json");
|
|
735
|
+
const git = (...args) => execFileSync("git", ["-C", cwd, "rev-parse", ...args], {
|
|
736
|
+
encoding: "utf8",
|
|
737
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
738
|
+
timeout: 2e3
|
|
739
|
+
}).trim();
|
|
740
|
+
let root, common, directory;
|
|
741
|
+
try {
|
|
742
|
+
root = git("--show-toplevel");
|
|
743
|
+
common = git("--path-format=absolute", "--git-common-dir");
|
|
744
|
+
directory = git("--absolute-git-dir");
|
|
745
|
+
} catch {
|
|
746
|
+
return { file: legacyFile, shared: false, legacyFile };
|
|
747
|
+
}
|
|
748
|
+
const linked = fs4.realpathSync(common) !== fs4.realpathSync(directory);
|
|
749
|
+
if (linked) {
|
|
750
|
+
if (path5.basename(common) !== ".git" || !fs4.statSync(common).isDirectory()) {
|
|
751
|
+
throw new Error("Codex hooks in a worktree of a bare repository are not supported yet. Use a full checkout for Bunshin setup.");
|
|
752
|
+
}
|
|
753
|
+
root = path5.dirname(common);
|
|
754
|
+
}
|
|
755
|
+
const file = fs4.realpathSync(root) === fs4.realpathSync(cwd) ? legacyFile : path5.join(root, ".codex", "hooks.json");
|
|
756
|
+
return { file, shared: linked, legacyFile };
|
|
757
|
+
}
|
|
758
|
+
var init_project_hooks = __esm({
|
|
759
|
+
"src/project-hooks.ts"() {
|
|
760
|
+
"use strict";
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
719
764
|
// src/init.ts
|
|
720
765
|
var init_exports = {};
|
|
721
766
|
__export(init_exports, {
|
|
@@ -731,8 +776,8 @@ __export(init_exports, {
|
|
|
731
776
|
removeBunshinHooks: () => removeBunshinHooks,
|
|
732
777
|
runInit: () => runInit
|
|
733
778
|
});
|
|
734
|
-
import
|
|
735
|
-
import
|
|
779
|
+
import fs5 from "node:fs";
|
|
780
|
+
import path6 from "node:path";
|
|
736
781
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
737
782
|
function isBunshinEntry(entry) {
|
|
738
783
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -785,16 +830,16 @@ function countBunshinHooks(settings) {
|
|
|
785
830
|
);
|
|
786
831
|
}
|
|
787
832
|
function claudeSettingsPath() {
|
|
788
|
-
return
|
|
833
|
+
return path6.join(homeDir(), ".claude", "settings.json");
|
|
789
834
|
}
|
|
790
835
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
791
|
-
const file = scope === "global" ?
|
|
836
|
+
const file = scope === "global" ? path6.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path6.join(cwd, ".pi/extensions/bunshin.js");
|
|
792
837
|
const marker = "// Managed by Bunshin setup.";
|
|
793
838
|
try {
|
|
794
|
-
const previous = fileExists(file) ?
|
|
839
|
+
const previous = fileExists(file) ? fs5.readFileSync(file, "utf8") : "";
|
|
795
840
|
if (previous && !previous.startsWith(marker)) throw new Error("An existing Bunshin extension file is not managed by setup; preserve or move it before continuing.");
|
|
796
841
|
if (remove) {
|
|
797
|
-
if (previous)
|
|
842
|
+
if (previous) fs5.unlinkSync(file);
|
|
798
843
|
return { file, installed: 0, changed: !!previous };
|
|
799
844
|
}
|
|
800
845
|
const extension = fileURLToPath(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -803,8 +848,8 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
803
848
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
804
849
|
`;
|
|
805
850
|
if (text !== previous) {
|
|
806
|
-
ensureDir(
|
|
807
|
-
|
|
851
|
+
ensureDir(path6.dirname(file));
|
|
852
|
+
fs5.writeFileSync(file, text);
|
|
808
853
|
}
|
|
809
854
|
return { file, installed: 1, changed: text !== previous };
|
|
810
855
|
} catch (error) {
|
|
@@ -813,14 +858,14 @@ export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
|
813
858
|
}
|
|
814
859
|
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
|
|
815
860
|
try {
|
|
816
|
-
const before = fileExists(file) ? JSON.parse(
|
|
861
|
+
const before = fileExists(file) ? JSON.parse(fs5.readFileSync(file, "utf8")) : {};
|
|
817
862
|
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
|
|
818
863
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
819
864
|
if (changed) {
|
|
820
|
-
ensureDir(
|
|
865
|
+
ensureDir(path6.dirname(file));
|
|
821
866
|
if (fileExists(file)) {
|
|
822
867
|
try {
|
|
823
|
-
|
|
868
|
+
fs5.copyFileSync(file, `${file}.bunshin-backup`);
|
|
824
869
|
} catch {
|
|
825
870
|
}
|
|
826
871
|
}
|
|
@@ -835,16 +880,16 @@ function runInit(opts = {}) {
|
|
|
835
880
|
const cwd = opts.cwd ?? process.cwd();
|
|
836
881
|
const warnings = [];
|
|
837
882
|
const scope = opts.scope ?? "repo";
|
|
838
|
-
const root = scope === "global" ?
|
|
839
|
-
const existed = fileExists(
|
|
883
|
+
const root = scope === "global" ? path6.join(homeDir(), ".bunshin") : path6.join(cwd, ".bunshin");
|
|
884
|
+
const existed = fileExists(path6.join(root, "config.json"));
|
|
840
885
|
const store = new Store(root);
|
|
841
886
|
store.ensureLayout();
|
|
842
|
-
|
|
887
|
+
fs5.chmodSync(root, 448);
|
|
843
888
|
if (scope === "repo") {
|
|
844
|
-
const ignore =
|
|
845
|
-
const current = fileExists(ignore) ?
|
|
889
|
+
const ignore = path6.join(cwd, ".gitignore");
|
|
890
|
+
const current = fileExists(ignore) ? fs5.readFileSync(ignore, "utf8") : "";
|
|
846
891
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
847
|
-
|
|
892
|
+
fs5.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
848
893
|
`);
|
|
849
894
|
}
|
|
850
895
|
}
|
|
@@ -871,8 +916,14 @@ function runInit(opts = {}) {
|
|
|
871
916
|
if (hooks.error) warnings.push(hooks.error);
|
|
872
917
|
}
|
|
873
918
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
874
|
-
const
|
|
919
|
+
const projectHooks = config.harness === "codex" && scope === "repo" && !opts.settingsFile ? codexProjectHooks(cwd) : void 0;
|
|
920
|
+
const file = opts.settingsFile ?? (config.harness === "codex" ? projectHooks?.file ?? path6.join(process.env.CODEX_HOME ?? path6.join(homeDir(), ".codex"), "hooks.json") : scope === "repo" ? path6.join(cwd, ".claude", "settings.local.json") : claudeSettingsPath());
|
|
875
921
|
hooks = installClaudeHooks({ file, bin: opts.bin, remove: opts.removeHooks, harness: config.harness });
|
|
922
|
+
if (projectHooks?.shared) hooks.sharedWithWorktrees = true;
|
|
923
|
+
if (!hooks.error && projectHooks && projectHooks.file !== projectHooks.legacyFile && fileExists(projectHooks.legacyFile) && fs5.realpathSync(projectHooks.file) !== fs5.realpathSync(projectHooks.legacyFile)) {
|
|
924
|
+
const old = installClaudeHooks({ file: projectHooks.legacyFile, remove: true, harness: "codex" });
|
|
925
|
+
if (old.error) hooks.error = `Could not remove old Bunshin hooks from ${old.file}: ${old.error}`;
|
|
926
|
+
}
|
|
876
927
|
if (hooks.error) warnings.push(`could not update ${hooks.file}: ${hooks.error}`);
|
|
877
928
|
}
|
|
878
929
|
if (!resolveBackend(config)) {
|
|
@@ -902,6 +953,7 @@ var init_init = __esm({
|
|
|
902
953
|
init_store();
|
|
903
954
|
init_util();
|
|
904
955
|
init_llm();
|
|
956
|
+
init_project_hooks();
|
|
905
957
|
HOOK_EVENTS = [
|
|
906
958
|
{ event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
|
|
907
959
|
{ event: "Stop", arg: "stop", timeout: 15 },
|
|
@@ -923,16 +975,16 @@ __export(setup_exports, {
|
|
|
923
975
|
setupProject: () => setupProject,
|
|
924
976
|
shellQuote: () => shellQuote
|
|
925
977
|
});
|
|
926
|
-
import
|
|
927
|
-
import
|
|
978
|
+
import fs6 from "node:fs";
|
|
979
|
+
import path7 from "node:path";
|
|
928
980
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
929
981
|
function executable(name) {
|
|
930
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
982
|
+
const directories = [...(process.env.PATH ?? "").split(path7.delimiter), path7.join(homeDir(), ".local/bin")];
|
|
931
983
|
for (const directory of directories) {
|
|
932
|
-
const file =
|
|
984
|
+
const file = path7.resolve(directory, name);
|
|
933
985
|
try {
|
|
934
|
-
|
|
935
|
-
if (
|
|
986
|
+
fs6.accessSync(file, fs6.constants.X_OK);
|
|
987
|
+
if (fs6.statSync(file).isFile()) return file;
|
|
936
988
|
} catch {
|
|
937
989
|
}
|
|
938
990
|
}
|
|
@@ -940,8 +992,8 @@ function executable(name) {
|
|
|
940
992
|
}
|
|
941
993
|
function hookBinary(harness) {
|
|
942
994
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
943
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
944
|
-
return `PATH=${shellQuote(directories.join(
|
|
995
|
+
const directories = [.../* @__PURE__ */ new Set([path7.dirname(process.execPath), path7.join(homeDir(), ".local/bin"), ...agent ? [path7.dirname(agent)] : []])];
|
|
996
|
+
return `PATH=${shellQuote(directories.join(path7.delimiter))}:"$PATH" ${shellQuote(process.execPath)} ${shellQuote(fs6.realpathSync(process.argv[1]))}`;
|
|
945
997
|
}
|
|
946
998
|
function apiAddress(raw) {
|
|
947
999
|
const url = new URL(raw);
|
|
@@ -1010,23 +1062,23 @@ async function setupProject(opts) {
|
|
|
1010
1062
|
const apiKey = opts.apiKey.trim();
|
|
1011
1063
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
1012
1064
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
1013
|
-
const root =
|
|
1065
|
+
const root = path7.join(opts.cwd, ".bunshin");
|
|
1014
1066
|
const store = new Store(root);
|
|
1015
1067
|
const previous = store.config();
|
|
1016
1068
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
1017
1069
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
1018
1070
|
}
|
|
1019
|
-
const credentialDirectory =
|
|
1020
|
-
|
|
1021
|
-
|
|
1071
|
+
const credentialDirectory = path7.join(homeDir(), ".config/bunshin/credentials");
|
|
1072
|
+
fs6.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
|
|
1073
|
+
fs6.chmodSync(credentialDirectory, 448);
|
|
1022
1074
|
const name = createHash2("sha256").update(`${root}
|
|
1023
1075
|
${apiUrl}
|
|
1024
1076
|
${profile.orgId}
|
|
1025
1077
|
${profile.userId}`).digest("hex");
|
|
1026
|
-
const credentialFile =
|
|
1078
|
+
const credentialFile = path7.join(credentialDirectory, `${name}.json`);
|
|
1027
1079
|
const temporary = `${credentialFile}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
1028
|
-
|
|
1029
|
-
|
|
1080
|
+
fs6.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
1081
|
+
fs6.renameSync(temporary, credentialFile);
|
|
1030
1082
|
const result = runInit({
|
|
1031
1083
|
cwd: opts.cwd,
|
|
1032
1084
|
harness: opts.harness,
|
|
@@ -2028,11 +2080,11 @@ var init_redact2 = __esm({
|
|
|
2028
2080
|
});
|
|
2029
2081
|
|
|
2030
2082
|
// src/transcript.ts
|
|
2031
|
-
import
|
|
2083
|
+
import fs7 from "node:fs";
|
|
2032
2084
|
function parseTranscript(file) {
|
|
2033
2085
|
let raw = "";
|
|
2034
2086
|
try {
|
|
2035
|
-
raw =
|
|
2087
|
+
raw = fs7.readFileSync(file, "utf8");
|
|
2036
2088
|
} catch {
|
|
2037
2089
|
return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
|
|
2038
2090
|
}
|
|
@@ -2068,10 +2120,13 @@ function parseTranscript(file) {
|
|
|
2068
2120
|
turns.push({ role: "tool", tool: item.name || "tool", text: summarizeInput(input) });
|
|
2069
2121
|
toolCalls++;
|
|
2070
2122
|
} else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2123
|
+
const outputs = Array.isArray(item.output) ? item.output.filter((part) => part && typeof part === "object" && typeof part.text === "string").map((part) => part.text) : [item.output];
|
|
2124
|
+
for (const output of outputs) {
|
|
2125
|
+
const result = codexToolResult(output);
|
|
2126
|
+
if (!result.text) continue;
|
|
2127
|
+
turns.push({ role: "tool", tool: "result", ...result });
|
|
2128
|
+
if (result.isError) toolFailures++;
|
|
2129
|
+
}
|
|
2075
2130
|
}
|
|
2076
2131
|
continue;
|
|
2077
2132
|
}
|
|
@@ -2123,6 +2178,31 @@ function blockText(content) {
|
|
|
2123
2178
|
}
|
|
2124
2179
|
return content == null ? "" : JSON.stringify(content);
|
|
2125
2180
|
}
|
|
2181
|
+
function codexToolResult(output) {
|
|
2182
|
+
const raw = blockText(output);
|
|
2183
|
+
let envelope;
|
|
2184
|
+
try {
|
|
2185
|
+
envelope = JSON.parse(raw);
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
|
|
2189
|
+
const result = envelope;
|
|
2190
|
+
if (typeof result.output === "string" && (Number.isInteger(result.exit_code) || result.exit_code === null || Number.isInteger(result.session_id))) {
|
|
2191
|
+
const pending = !Number.isInteger(result.exit_code);
|
|
2192
|
+
return {
|
|
2193
|
+
text: `${pending ? "Command still running" : `Process exited with code ${result.exit_code}`}
|
|
2194
|
+
${result.output}`,
|
|
2195
|
+
isError: !pending && result.exit_code !== 0,
|
|
2196
|
+
pending
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
text: raw,
|
|
2202
|
+
isError: /(?:Process exited with code|exit_code["']?\s*:)\s*[1-9]\d*\b/i.test(raw),
|
|
2203
|
+
pending: /(?:Process running with session ID|Script running with cell ID)/i.test(raw)
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2126
2206
|
function summarizeInput(input) {
|
|
2127
2207
|
if (input == null) return "";
|
|
2128
2208
|
if (typeof input !== "object") return String(input);
|
|
@@ -2147,7 +2227,7 @@ function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {})
|
|
|
2147
2227
|
${bounded(text, maxChars)}`);
|
|
2148
2228
|
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
2149
2229
|
${bounded(text, maxChars)}`);
|
|
2150
|
-
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : ""}]
|
|
2230
|
+
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
2151
2231
|
${bounded(text, toolResultChars)}`);
|
|
2152
2232
|
else parts.push(`[TOOL ${t.tool}] ${truncate(text, 400)}`);
|
|
2153
2233
|
}
|
|
@@ -2354,11 +2434,11 @@ __export(demo_exports, {
|
|
|
2354
2434
|
runDemo: () => runDemo
|
|
2355
2435
|
});
|
|
2356
2436
|
import assert from "node:assert/strict";
|
|
2357
|
-
import
|
|
2437
|
+
import fs8 from "node:fs";
|
|
2358
2438
|
import os2 from "node:os";
|
|
2359
|
-
import
|
|
2439
|
+
import path8 from "node:path";
|
|
2360
2440
|
async function runDemo(write) {
|
|
2361
|
-
const root =
|
|
2441
|
+
const root = fs8.mkdtempSync(path8.join(os2.tmpdir(), "bunshin-demo-"));
|
|
2362
2442
|
try {
|
|
2363
2443
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
2364
2444
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -2375,7 +2455,7 @@ Task: ${APR_TASK}
|
|
|
2375
2455
|
write(result.text);
|
|
2376
2456
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
2377
2457
|
} finally {
|
|
2378
|
-
|
|
2458
|
+
fs8.rmSync(root, { recursive: true, force: true });
|
|
2379
2459
|
}
|
|
2380
2460
|
}
|
|
2381
2461
|
var init_demo = __esm({
|
|
@@ -2394,27 +2474,27 @@ __export(reflect_exports, {
|
|
|
2394
2474
|
buildLesson: () => buildLesson,
|
|
2395
2475
|
runReflection: () => runReflection
|
|
2396
2476
|
});
|
|
2397
|
-
import
|
|
2398
|
-
import
|
|
2477
|
+
import fs9 from "node:fs";
|
|
2478
|
+
import path9 from "node:path";
|
|
2399
2479
|
import { createHash as createHash3 } from "node:crypto";
|
|
2400
2480
|
async function runReflection(store, opts) {
|
|
2401
2481
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
2402
|
-
const lock =
|
|
2482
|
+
const lock = path9.join(store.root, `reflect-${createHash3("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
2403
2483
|
const deadline = Date.now() + 66e4;
|
|
2404
2484
|
for (; ; ) {
|
|
2405
2485
|
try {
|
|
2406
|
-
|
|
2486
|
+
fs9.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
2407
2487
|
break;
|
|
2408
2488
|
} catch (error) {
|
|
2409
2489
|
if (error.code !== "EEXIST") throw error;
|
|
2410
2490
|
try {
|
|
2411
|
-
const owner = Number(
|
|
2491
|
+
const owner = Number(fs9.readFileSync(lock, "utf8"));
|
|
2412
2492
|
if (Number.isInteger(owner) && owner > 0) {
|
|
2413
2493
|
try {
|
|
2414
2494
|
process.kill(owner, 0);
|
|
2415
2495
|
} catch (error2) {
|
|
2416
2496
|
if (error2.code === "ESRCH") {
|
|
2417
|
-
|
|
2497
|
+
fs9.unlinkSync(lock);
|
|
2418
2498
|
continue;
|
|
2419
2499
|
}
|
|
2420
2500
|
}
|
|
@@ -2429,7 +2509,7 @@ async function runReflection(store, opts) {
|
|
|
2429
2509
|
return await reflectSession(store, opts);
|
|
2430
2510
|
} finally {
|
|
2431
2511
|
try {
|
|
2432
|
-
|
|
2512
|
+
fs9.unlinkSync(lock);
|
|
2433
2513
|
} catch {
|
|
2434
2514
|
}
|
|
2435
2515
|
}
|
|
@@ -2516,7 +2596,7 @@ function workflowRejection(raw, facts, excerpt) {
|
|
|
2516
2596
|
}
|
|
2517
2597
|
const normalize = (text) => redact(text).replace(/\s+/g, " ").trim();
|
|
2518
2598
|
const visible = normalize(excerpt);
|
|
2519
|
-
const results = facts.turns.filter((turn) => turn.role === "tool" && turn.tool === "result" && !turn.isError).map((turn) => normalize(turn.text));
|
|
2599
|
+
const results = facts.turns.filter((turn) => turn.role === "tool" && turn.tool === "result" && !turn.isError && !turn.pending).map((turn) => normalize(turn.text));
|
|
2520
2600
|
const evidence = raw.verificationEvidence;
|
|
2521
2601
|
if (!Array.isArray(evidence) || !evidence.length) return "workflow requires verification quotes from successful tool results";
|
|
2522
2602
|
for (const [index, quote] of evidence.entries()) {
|
|
@@ -2764,8 +2844,8 @@ var init_meta_reflect = __esm({
|
|
|
2764
2844
|
});
|
|
2765
2845
|
|
|
2766
2846
|
// src/cli.ts
|
|
2767
|
-
import
|
|
2768
|
-
import
|
|
2847
|
+
import fs10 from "node:fs";
|
|
2848
|
+
import path10 from "node:path";
|
|
2769
2849
|
|
|
2770
2850
|
// src/detach.ts
|
|
2771
2851
|
import { spawn } from "node:child_process";
|
|
@@ -2986,6 +3066,7 @@ async function hookCommand(rawEvent, opts = {}) {
|
|
|
2986
3066
|
init_llm();
|
|
2987
3067
|
init_store();
|
|
2988
3068
|
init_util();
|
|
3069
|
+
init_project_hooks();
|
|
2989
3070
|
function parseArgs(argv) {
|
|
2990
3071
|
const out = { _: [], flags: {} };
|
|
2991
3072
|
const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose"]);
|
|
@@ -3033,7 +3114,7 @@ function version() {
|
|
|
3033
3114
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
3034
3115
|
try {
|
|
3035
3116
|
const file = new URL(rel, import.meta.url);
|
|
3036
|
-
const pkg = JSON.parse(
|
|
3117
|
+
const pkg = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3037
3118
|
if (pkg.name === "@bunshin-cc/bunshin" && pkg.version) return pkg.version;
|
|
3038
3119
|
} catch {
|
|
3039
3120
|
}
|
|
@@ -3099,7 +3180,7 @@ async function main(argv, opts = {}) {
|
|
|
3099
3180
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
3100
3181
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
3101
3182
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
3102
|
-
const existing = new Store(
|
|
3183
|
+
const existing = new Store(path10.join(cwd, ".bunshin")).config();
|
|
3103
3184
|
const apiUrl = str2(args.flags["api-url"]);
|
|
3104
3185
|
const key = bool(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
3105
3186
|
let value = "";
|
|
@@ -3120,6 +3201,7 @@ Key saved privately outside the project.
|
|
|
3120
3201
|
if (result.hooks?.error) throw new Error(`Connected, but hooks could not be configured: ${result.hooks.error}. Fix the file and run setup again.`);
|
|
3121
3202
|
if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
|
|
3122
3203
|
`);
|
|
3204
|
+
if (result.hooks?.sharedWithWorktrees) out("Codex shares these hooks with the repository\u2019s linked worktrees. Each connected worktree keeps its own Bunshin store.\n");
|
|
3123
3205
|
if (harness === "codex" && result.hooks) out("Run codex interactively in this project. Review and accept project trust, then run /hooks and review and trust the four Bunshin hooks.\n");
|
|
3124
3206
|
else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
|
|
3125
3207
|
else if (result.hooks) out("Open a new Claude Code session in this project to load the hooks.\n");
|
|
@@ -3182,7 +3264,7 @@ ${HELP}`);
|
|
|
3182
3264
|
function openStore(args, cwd) {
|
|
3183
3265
|
const root = str2(args.flags.root);
|
|
3184
3266
|
if (root) {
|
|
3185
|
-
const store = new Store(
|
|
3267
|
+
const store = new Store(path10.resolve(root));
|
|
3186
3268
|
store.ensureLayout();
|
|
3187
3269
|
return store;
|
|
3188
3270
|
}
|
|
@@ -3221,6 +3303,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
3221
3303
|
if (result.hooks) {
|
|
3222
3304
|
out(` hooks: ${result.hooks.installed} Bunshin entries in ${result.hooks.file}${result.hooks.changed ? "" : " (already up to date)"}
|
|
3223
3305
|
`);
|
|
3306
|
+
if (result.hooks.sharedWithWorktrees) out(" Codex repository hooks are shared by its linked worktrees; each connected worktree uses its own Bunshin store.\n");
|
|
3224
3307
|
if (result.hooks.error) err(` hooks: ${result.hooks.error}
|
|
3225
3308
|
`);
|
|
3226
3309
|
}
|
|
@@ -3238,10 +3321,10 @@ function cmdStatus(store, args, out) {
|
|
|
3238
3321
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
3239
3322
|
const metrics = store.readMetrics(void 0);
|
|
3240
3323
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
3241
|
-
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([
|
|
3324
|
+
const hookFiles = config.harness === "codex" ? [.../* @__PURE__ */ new Set([path10.join(process.env.CODEX_HOME ?? path10.join(homeDir(), ".codex"), "hooks.json"), codexProjectHooks(path10.dirname(store.root)).file])] : config.harness === "claude-code" ? [path10.join(homeDir(), ".claude", "settings.json"), path10.join(path10.dirname(store.root), ".claude", "settings.local.json")] : config.harness === "pi" ? [path10.join(homeDir(), ".pi/agent/extensions/bunshin.js"), path10.join(path10.dirname(store.root), ".pi/extensions/bunshin.js")] : [];
|
|
3242
3325
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
3243
3326
|
try {
|
|
3244
|
-
return
|
|
3327
|
+
return fs10.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
3245
3328
|
} catch {
|
|
3246
3329
|
return false;
|
|
3247
3330
|
}
|
|
@@ -3258,7 +3341,7 @@ function cmdStatus(store, args, out) {
|
|
|
3258
3341
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
3259
3342
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
3260
3343
|
hooks: { [config.harness]: hooks },
|
|
3261
|
-
hookFiles: hookFiles.filter((file) =>
|
|
3344
|
+
hookFiles: hookFiles.filter((file) => fs10.existsSync(file)),
|
|
3262
3345
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null
|
|
3263
3346
|
};
|
|
3264
3347
|
if (bool(args.flags.json)) {
|
|
@@ -3290,7 +3373,7 @@ function cmdStatus(store, args, out) {
|
|
|
3290
3373
|
}
|
|
3291
3374
|
function hookCount(file) {
|
|
3292
3375
|
try {
|
|
3293
|
-
const raw = JSON.parse(
|
|
3376
|
+
const raw = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3294
3377
|
let n = 0;
|
|
3295
3378
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
3296
3379
|
for (const g of groups ?? []) {
|
|
@@ -3410,7 +3493,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
3410
3493
|
let content = str2(args.flags.content) ?? "";
|
|
3411
3494
|
if (!content || content === "-" || args._.includes("-")) {
|
|
3412
3495
|
try {
|
|
3413
|
-
content =
|
|
3496
|
+
content = fs10.readFileSync(0, "utf8");
|
|
3414
3497
|
} catch {
|
|
3415
3498
|
}
|
|
3416
3499
|
}
|
|
@@ -3476,7 +3559,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
3476
3559
|
}
|
|
3477
3560
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect2(), reflect_exports));
|
|
3478
3561
|
const result = await runReflection2(store, {
|
|
3479
|
-
sessionId: str2(args.flags.session) ??
|
|
3562
|
+
sessionId: str2(args.flags.session) ?? path10.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
3480
3563
|
transcriptPath: transcript,
|
|
3481
3564
|
cwd,
|
|
3482
3565
|
force: bool(args.flags.force)
|
|
@@ -3579,7 +3662,7 @@ async function cmdSessionEnd(store, args, cwd, out) {
|
|
|
3579
3662
|
const transcript = str2(args.flags.transcript);
|
|
3580
3663
|
const quiet = bool(args.flags.quiet);
|
|
3581
3664
|
const summary = { session: sessionId };
|
|
3582
|
-
if (transcript &&
|
|
3665
|
+
if (transcript && fs10.existsSync(transcript)) {
|
|
3583
3666
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect2(), reflect_exports));
|
|
3584
3667
|
summary.reflect = await runReflection2(store, { sessionId, transcriptPath: transcript, cwd, force: true });
|
|
3585
3668
|
}
|