@bunshin-cc/bunshin 0.1.2 → 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 +231 -94
- 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
|
}
|
|
@@ -405,6 +409,7 @@ var init_store = __esm({
|
|
|
405
409
|
var sync_exports = {};
|
|
406
410
|
__export(sync_exports, {
|
|
407
411
|
canSync: () => canSync,
|
|
412
|
+
mothershipLookup: () => mothershipLookup,
|
|
408
413
|
mothershipRecall: () => mothershipRecall,
|
|
409
414
|
pendingConflictQuestion: () => pendingConflictQuestion,
|
|
410
415
|
resolveConflict: () => resolveConflict,
|
|
@@ -440,6 +445,14 @@ async function mothershipRecall(config, req, timeoutMs = 8e3) {
|
|
|
440
445
|
return null;
|
|
441
446
|
}
|
|
442
447
|
}
|
|
448
|
+
async function mothershipLookup(config, ids, timeoutMs = 8e3) {
|
|
449
|
+
if (!canSync(config) || !ids.length) return null;
|
|
450
|
+
try {
|
|
451
|
+
return await api(config, "POST", "/lessons/lookup", { ids }, timeoutMs);
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
443
456
|
async function runSync(store) {
|
|
444
457
|
const config = store.config();
|
|
445
458
|
const result = { pushedLessons: 0, pushedTraces: 0, pulledConflicts: 0, errors: [] };
|
|
@@ -482,6 +495,13 @@ async function runSync(store) {
|
|
|
482
495
|
store.saveConflict(c);
|
|
483
496
|
result.pulledConflicts++;
|
|
484
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
|
+
}
|
|
485
505
|
} catch (e) {
|
|
486
506
|
result.errors.push(String(e instanceof Error ? e.message : e));
|
|
487
507
|
}
|
|
@@ -494,8 +514,7 @@ async function resolveConflict(store, id, answer, note) {
|
|
|
494
514
|
const local = store.listConflicts().find((c) => c.id === id);
|
|
495
515
|
if (!local) throw new Error(`no pending conflict ${id}`);
|
|
496
516
|
const resolution = { chosen: answer, by: config.userId, at: nowIso(), note };
|
|
497
|
-
|
|
498
|
-
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 };
|
|
499
518
|
store.saveConflict(resolved);
|
|
500
519
|
store.log("conflict-resolve", { id, answer });
|
|
501
520
|
return resolved;
|
|
@@ -508,7 +527,7 @@ function pendingConflictQuestion(store, asked) {
|
|
|
508
527
|
const options = c.options?.length ? `
|
|
509
528
|
Options: ${c.options.map((o, i) => `(${i + 1}) ${o}`).join(" ")}` : "";
|
|
510
529
|
const text = `## Knowledge conflict to resolve (Bunshin)
|
|
511
|
-
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.
|
|
512
531
|
|
|
513
532
|
> ${c.question}${options}
|
|
514
533
|
|
|
@@ -650,6 +669,7 @@ async function askCodexCli(opts) {
|
|
|
650
669
|
const outFile = path4.join(os.tmpdir(), `bunshin-codex-${process.pid}-${Date.now()}.txt`);
|
|
651
670
|
const args = ["exec", "--skip-git-repo-check", "--ephemeral", "-s", "read-only", "-o", outFile];
|
|
652
671
|
if (opts.model) args.push("-m", opts.model);
|
|
672
|
+
if (opts.effort) args.push("-c", `model_reasoning_effort="${opts.effort}"`);
|
|
653
673
|
args.push("-");
|
|
654
674
|
const prompt = `${opts.system}${jsonOnlyInstruction(opts.schema)}
|
|
655
675
|
|
|
@@ -706,6 +726,41 @@ var init_llm = __esm({
|
|
|
706
726
|
}
|
|
707
727
|
});
|
|
708
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
|
+
|
|
709
764
|
// src/init.ts
|
|
710
765
|
var init_exports = {};
|
|
711
766
|
__export(init_exports, {
|
|
@@ -721,8 +776,8 @@ __export(init_exports, {
|
|
|
721
776
|
removeBunshinHooks: () => removeBunshinHooks,
|
|
722
777
|
runInit: () => runInit
|
|
723
778
|
});
|
|
724
|
-
import
|
|
725
|
-
import
|
|
779
|
+
import fs5 from "node:fs";
|
|
780
|
+
import path6 from "node:path";
|
|
726
781
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
727
782
|
function isBunshinEntry(entry) {
|
|
728
783
|
return entry?._bunshin === true || typeof entry?.command === "string" && /\bbunshin\b.*\bhook\b/.test(entry.command);
|
|
@@ -775,16 +830,16 @@ function countBunshinHooks(settings) {
|
|
|
775
830
|
);
|
|
776
831
|
}
|
|
777
832
|
function claudeSettingsPath() {
|
|
778
|
-
return
|
|
833
|
+
return path6.join(homeDir(), ".claude", "settings.json");
|
|
779
834
|
}
|
|
780
835
|
function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
781
|
-
const file = scope === "global" ?
|
|
836
|
+
const file = scope === "global" ? path6.join(homeDir(), ".pi/agent/extensions/bunshin.js") : path6.join(cwd, ".pi/extensions/bunshin.js");
|
|
782
837
|
const marker = "// Managed by Bunshin setup.";
|
|
783
838
|
try {
|
|
784
|
-
const previous = fileExists(file) ?
|
|
839
|
+
const previous = fileExists(file) ? fs5.readFileSync(file, "utf8") : "";
|
|
785
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.");
|
|
786
841
|
if (remove) {
|
|
787
|
-
if (previous)
|
|
842
|
+
if (previous) fs5.unlinkSync(file);
|
|
788
843
|
return { file, installed: 0, changed: !!previous };
|
|
789
844
|
}
|
|
790
845
|
const extension = fileURLToPath(new URL("./pi-extension.js", import.meta.url));
|
|
@@ -793,8 +848,8 @@ function installPiExtension(cwd, remove = false, scope = "repo") {
|
|
|
793
848
|
export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
794
849
|
`;
|
|
795
850
|
if (text !== previous) {
|
|
796
|
-
ensureDir(
|
|
797
|
-
|
|
851
|
+
ensureDir(path6.dirname(file));
|
|
852
|
+
fs5.writeFileSync(file, text);
|
|
798
853
|
}
|
|
799
854
|
return { file, installed: 1, changed: text !== previous };
|
|
800
855
|
} catch (error) {
|
|
@@ -803,14 +858,14 @@ export { default } from ${JSON.stringify(pathToFileURL(extension).href)};
|
|
|
803
858
|
}
|
|
804
859
|
function installClaudeHooks({ file = claudeSettingsPath(), bin = "bunshin", remove = false, harness = "claude-code" } = {}) {
|
|
805
860
|
try {
|
|
806
|
-
const before = fileExists(file) ? JSON.parse(
|
|
861
|
+
const before = fileExists(file) ? JSON.parse(fs5.readFileSync(file, "utf8")) : {};
|
|
807
862
|
const after = remove ? removeBunshinHooks(before) : mergeClaudeSettings(before, { bin, harness });
|
|
808
863
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
809
864
|
if (changed) {
|
|
810
|
-
ensureDir(
|
|
865
|
+
ensureDir(path6.dirname(file));
|
|
811
866
|
if (fileExists(file)) {
|
|
812
867
|
try {
|
|
813
|
-
|
|
868
|
+
fs5.copyFileSync(file, `${file}.bunshin-backup`);
|
|
814
869
|
} catch {
|
|
815
870
|
}
|
|
816
871
|
}
|
|
@@ -825,16 +880,16 @@ function runInit(opts = {}) {
|
|
|
825
880
|
const cwd = opts.cwd ?? process.cwd();
|
|
826
881
|
const warnings = [];
|
|
827
882
|
const scope = opts.scope ?? "repo";
|
|
828
|
-
const root = scope === "global" ?
|
|
829
|
-
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"));
|
|
830
885
|
const store = new Store(root);
|
|
831
886
|
store.ensureLayout();
|
|
832
|
-
|
|
887
|
+
fs5.chmodSync(root, 448);
|
|
833
888
|
if (scope === "repo") {
|
|
834
|
-
const ignore =
|
|
835
|
-
const current = fileExists(ignore) ?
|
|
889
|
+
const ignore = path6.join(cwd, ".gitignore");
|
|
890
|
+
const current = fileExists(ignore) ? fs5.readFileSync(ignore, "utf8") : "";
|
|
836
891
|
if (!current.split(/\r?\n/).some((line) => /^\/?\.bunshin\/?$/.test(line.trim()))) {
|
|
837
|
-
|
|
892
|
+
fs5.appendFileSync(ignore, `${current && !current.endsWith("\n") ? "\n" : ""}.bunshin/
|
|
838
893
|
`);
|
|
839
894
|
}
|
|
840
895
|
}
|
|
@@ -861,8 +916,14 @@ function runInit(opts = {}) {
|
|
|
861
916
|
if (hooks.error) warnings.push(hooks.error);
|
|
862
917
|
}
|
|
863
918
|
if ((opts.removeHooks || !opts.noHooks) && (config.harness === "codex" || config.harness === "claude-code")) {
|
|
864
|
-
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());
|
|
865
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
|
+
}
|
|
866
927
|
if (hooks.error) warnings.push(`could not update ${hooks.file}: ${hooks.error}`);
|
|
867
928
|
}
|
|
868
929
|
if (!resolveBackend(config)) {
|
|
@@ -892,6 +953,7 @@ var init_init = __esm({
|
|
|
892
953
|
init_store();
|
|
893
954
|
init_util();
|
|
894
955
|
init_llm();
|
|
956
|
+
init_project_hooks();
|
|
895
957
|
HOOK_EVENTS = [
|
|
896
958
|
{ event: "UserPromptSubmit", arg: "user-prompt-submit", timeout: 10 },
|
|
897
959
|
{ event: "Stop", arg: "stop", timeout: 15 },
|
|
@@ -913,16 +975,16 @@ __export(setup_exports, {
|
|
|
913
975
|
setupProject: () => setupProject,
|
|
914
976
|
shellQuote: () => shellQuote
|
|
915
977
|
});
|
|
916
|
-
import
|
|
917
|
-
import
|
|
978
|
+
import fs6 from "node:fs";
|
|
979
|
+
import path7 from "node:path";
|
|
918
980
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
919
981
|
function executable(name) {
|
|
920
|
-
const directories = [...(process.env.PATH ?? "").split(
|
|
982
|
+
const directories = [...(process.env.PATH ?? "").split(path7.delimiter), path7.join(homeDir(), ".local/bin")];
|
|
921
983
|
for (const directory of directories) {
|
|
922
|
-
const file =
|
|
984
|
+
const file = path7.resolve(directory, name);
|
|
923
985
|
try {
|
|
924
|
-
|
|
925
|
-
if (
|
|
986
|
+
fs6.accessSync(file, fs6.constants.X_OK);
|
|
987
|
+
if (fs6.statSync(file).isFile()) return file;
|
|
926
988
|
} catch {
|
|
927
989
|
}
|
|
928
990
|
}
|
|
@@ -930,8 +992,8 @@ function executable(name) {
|
|
|
930
992
|
}
|
|
931
993
|
function hookBinary(harness) {
|
|
932
994
|
const agent = executable(harness === "claude-code" ? "claude" : harness);
|
|
933
|
-
const directories = [.../* @__PURE__ */ new Set([
|
|
934
|
-
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]))}`;
|
|
935
997
|
}
|
|
936
998
|
function apiAddress(raw) {
|
|
937
999
|
const url = new URL(raw);
|
|
@@ -1000,23 +1062,23 @@ async function setupProject(opts) {
|
|
|
1000
1062
|
const apiKey = opts.apiKey.trim();
|
|
1001
1063
|
if (!apiKey || /\s/.test(apiKey)) throw new Error("Paste the complete agent key from your Bunshin workspace.");
|
|
1002
1064
|
const profile = await connectionProfile(apiUrl, apiKey);
|
|
1003
|
-
const root =
|
|
1065
|
+
const root = path7.join(opts.cwd, ".bunshin");
|
|
1004
1066
|
const store = new Store(root);
|
|
1005
1067
|
const previous = store.config();
|
|
1006
1068
|
if ((previous.orgId !== profile.orgId || previous.userId !== profile.userId) && (store.listLessons().length || store.allTraces().length || store.listRecallLessons().length)) {
|
|
1007
1069
|
throw new Error("This project has lessons or activity from another profile. Use a fresh project to connect this workspace.");
|
|
1008
1070
|
}
|
|
1009
|
-
const credentialDirectory =
|
|
1010
|
-
|
|
1011
|
-
|
|
1071
|
+
const credentialDirectory = path7.join(homeDir(), ".config/bunshin/credentials");
|
|
1072
|
+
fs6.mkdirSync(credentialDirectory, { recursive: true, mode: 448 });
|
|
1073
|
+
fs6.chmodSync(credentialDirectory, 448);
|
|
1012
1074
|
const name = createHash2("sha256").update(`${root}
|
|
1013
1075
|
${apiUrl}
|
|
1014
1076
|
${profile.orgId}
|
|
1015
1077
|
${profile.userId}`).digest("hex");
|
|
1016
|
-
const credentialFile =
|
|
1078
|
+
const credentialFile = path7.join(credentialDirectory, `${name}.json`);
|
|
1017
1079
|
const temporary = `${credentialFile}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
1018
|
-
|
|
1019
|
-
|
|
1080
|
+
fs6.writeFileSync(temporary, JSON.stringify({ apiUrl, apiKey }) + "\n", { mode: 384, flag: "wx" });
|
|
1081
|
+
fs6.renameSync(temporary, credentialFile);
|
|
1020
1082
|
const result = runInit({
|
|
1021
1083
|
cwd: opts.cwd,
|
|
1022
1084
|
harness: opts.harness,
|
|
@@ -1977,10 +2039,11 @@ function redact(text) {
|
|
|
1977
2039
|
if (name === "email") {
|
|
1978
2040
|
return /^(?:no[._-]?reply|do[._-]?not[._-]?reply)@|@example\./i.test(match) ? match : "[REDACTED:email]";
|
|
1979
2041
|
}
|
|
1980
|
-
if (name === "bearer" || name === "assigned-secret") {
|
|
2042
|
+
if (name === "bearer" || name === "assigned-secret" || name === "json-secret") {
|
|
1981
2043
|
const key = String(groups[0] ?? "");
|
|
1982
2044
|
const value = String(groups[1] ?? "");
|
|
1983
2045
|
if (PLACEHOLDER_RE.test(value)) return match;
|
|
2046
|
+
if (name === "json-secret") return `${JSON.stringify(key)}:"[REDACTED:json-secret]"`;
|
|
1984
2047
|
return `${key}=[REDACTED:${name}]`;
|
|
1985
2048
|
}
|
|
1986
2049
|
return `[REDACTED:${name}]`;
|
|
@@ -1993,6 +2056,7 @@ var init_redact2 = __esm({
|
|
|
1993
2056
|
"src/redact.ts"() {
|
|
1994
2057
|
"use strict";
|
|
1995
2058
|
PATTERNS = [
|
|
2059
|
+
["bunshin-key", /\bbsk_[A-Za-z0-9]{8}_[A-Za-z0-9]{32}\b/g],
|
|
1996
2060
|
["aws-key", /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA)[0-9A-Z]{16}\b/g],
|
|
1997
2061
|
["github-pat", /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g],
|
|
1998
2062
|
["anthropic-key", /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g],
|
|
@@ -2007,6 +2071,7 @@ var init_redact2 = __esm({
|
|
|
2007
2071
|
"assigned-secret",
|
|
2008
2072
|
/\b([A-Za-z0-9_.-]*(?:secret|passwd|password|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|session[_-]?token)[A-Za-z0-9_.-]*)\s*[:=]\s*['"]?([^\s'"#,;]{6,})['"]?/gi
|
|
2009
2073
|
],
|
|
2074
|
+
["json-secret", /"([A-Za-z0-9_.-]*(?:secret|passwd|password|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|session[_-]?token)[A-Za-z0-9_.-]*)"\s*:\s*"([^"\r\n]{6,})"/gi],
|
|
2010
2075
|
["card", /\b(?:\d[ -]?){13,19}\b/g],
|
|
2011
2076
|
["email", /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g]
|
|
2012
2077
|
];
|
|
@@ -2015,11 +2080,11 @@ var init_redact2 = __esm({
|
|
|
2015
2080
|
});
|
|
2016
2081
|
|
|
2017
2082
|
// src/transcript.ts
|
|
2018
|
-
import
|
|
2083
|
+
import fs7 from "node:fs";
|
|
2019
2084
|
function parseTranscript(file) {
|
|
2020
2085
|
let raw = "";
|
|
2021
2086
|
try {
|
|
2022
|
-
raw =
|
|
2087
|
+
raw = fs7.readFileSync(file, "utf8");
|
|
2023
2088
|
} catch {
|
|
2024
2089
|
return { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 };
|
|
2025
2090
|
}
|
|
@@ -2055,10 +2120,13 @@ function parseTranscript(file) {
|
|
|
2055
2120
|
turns.push({ role: "tool", tool: item.name || "tool", text: summarizeInput(input) });
|
|
2056
2121
|
toolCalls++;
|
|
2057
2122
|
} else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
+
}
|
|
2062
2130
|
}
|
|
2063
2131
|
continue;
|
|
2064
2132
|
}
|
|
@@ -2110,6 +2178,31 @@ function blockText(content) {
|
|
|
2110
2178
|
}
|
|
2111
2179
|
return content == null ? "" : JSON.stringify(content);
|
|
2112
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
|
+
}
|
|
2113
2206
|
function summarizeInput(input) {
|
|
2114
2207
|
if (input == null) return "";
|
|
2115
2208
|
if (typeof input !== "object") return String(input);
|
|
@@ -2119,18 +2212,26 @@ function summarizeInput(input) {
|
|
|
2119
2212
|
}
|
|
2120
2213
|
return JSON.stringify(obj);
|
|
2121
2214
|
}
|
|
2122
|
-
function
|
|
2215
|
+
function bounded(text, maxChars) {
|
|
2216
|
+
if (text.length <= maxChars) return text;
|
|
2217
|
+
const marker = "\n[... middle omitted ...]\n";
|
|
2218
|
+
if (maxChars <= marker.length) return text.slice(0, Math.max(0, maxChars));
|
|
2219
|
+
const head = Math.floor((maxChars - marker.length) / 2);
|
|
2220
|
+
return text.slice(0, head) + marker + text.slice(-(maxChars - marker.length - head));
|
|
2221
|
+
}
|
|
2222
|
+
function digest(facts, { from = 0, maxChars = 6e4, toolResultChars = 6e3 } = {}) {
|
|
2123
2223
|
const parts = [];
|
|
2124
2224
|
for (const t of facts.turns.slice(from)) {
|
|
2225
|
+
const text = redact(t.text);
|
|
2125
2226
|
if (t.role === "user") parts.push(`[USER]
|
|
2126
|
-
${
|
|
2227
|
+
${bounded(text, maxChars)}`);
|
|
2127
2228
|
else if (t.role === "assistant") parts.push(`[ASSISTANT]
|
|
2128
|
-
${
|
|
2129
|
-
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : ""}]
|
|
2130
|
-
${
|
|
2131
|
-
else parts.push(`[TOOL ${t.tool}] ${truncate(
|
|
2229
|
+
${bounded(text, maxChars)}`);
|
|
2230
|
+
else if (t.tool === "result") parts.push(`[TOOL RESULT${t.isError ? " \u2014 ERROR" : t.pending ? " \u2014 RUNNING" : ""}]
|
|
2231
|
+
${bounded(text, toolResultChars)}`);
|
|
2232
|
+
else parts.push(`[TOOL ${t.tool}] ${truncate(text, 400)}`);
|
|
2132
2233
|
}
|
|
2133
|
-
return
|
|
2234
|
+
return bounded(parts.join("\n\n"), maxChars);
|
|
2134
2235
|
}
|
|
2135
2236
|
function recentContext(file, { turns = 2, maxChars = 6e3 } = {}) {
|
|
2136
2237
|
const facts = parseTranscript(file);
|
|
@@ -2176,27 +2277,53 @@ async function runRecall(store, opts) {
|
|
|
2176
2277
|
const rules = new Map(store.listRecallLessons().map((r) => [r.id, r]));
|
|
2177
2278
|
let degraded;
|
|
2178
2279
|
if (connected && !opts.offline) {
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2280
|
+
const cached = prepareRecall(local.map((lesson) => ({ lesson })), { task, ...profile }, profile).candidates.slice(0, 60).map((candidate) => candidate.lesson);
|
|
2281
|
+
const cachedIds = new Set(cached.map((lesson) => lesson.id));
|
|
2282
|
+
const [remote, lookup] = await Promise.all([
|
|
2283
|
+
mothershipRecall(config, {
|
|
2284
|
+
task,
|
|
2285
|
+
userId: config.userId,
|
|
2286
|
+
role: config.role,
|
|
2287
|
+
orgUnit: config.orgUnit,
|
|
2288
|
+
limit: 8,
|
|
2289
|
+
recentContext: history,
|
|
2290
|
+
dryRun: opts.dryRun
|
|
2291
|
+
}, remaining(8e3)),
|
|
2292
|
+
cached.length ? mothershipLookup(config, [...cachedIds], remaining(8e3)) : null
|
|
2293
|
+
]);
|
|
2294
|
+
const sameIdentity = (p) => p?.orgId === config.orgId && p.userId === config.userId;
|
|
2295
|
+
const remoteOK = remote && (!remote.profile || sameIdentity(remote.profile));
|
|
2296
|
+
const lookupOK = lookup && sameIdentity(lookup.profile);
|
|
2297
|
+
if (remoteOK || lookupOK) {
|
|
2298
|
+
profile = (remoteOK ? remote.profile : void 0) ?? (lookupOK ? lookup.profile : void 0) ?? profile;
|
|
2299
|
+
byId.clear();
|
|
2300
|
+
const refreshed = /* @__PURE__ */ new Map();
|
|
2301
|
+
if (lookupOK) {
|
|
2302
|
+
const unavailable = new Set(lookup.unavailableIds);
|
|
2303
|
+
for (const lesson of cached) {
|
|
2304
|
+
if (unavailable.has(lesson.id) && lesson.version === 1 && (!config.lastSyncAt || lesson.provenance.createdAt > config.lastSyncAt)) byId.set(lesson.id, lesson);
|
|
2305
|
+
}
|
|
2306
|
+
for (const lesson of lookup.lessons) {
|
|
2307
|
+
if (cachedIds.has(lesson.id) && lesson.provenance.orgId === profile.orgId) refreshed.set(lesson.id, lesson);
|
|
2308
|
+
}
|
|
2309
|
+
} else if (cached.length) {
|
|
2310
|
+
degraded = "local cache refresh unavailable; using current hosted knowledge";
|
|
2311
|
+
}
|
|
2312
|
+
for (const lesson of remoteOK ? remote.lessons : []) {
|
|
2191
2313
|
if (lesson.provenance.orgId !== profile.orgId) continue;
|
|
2314
|
+
const checked = refreshed.get(lesson.id);
|
|
2315
|
+
if (!checked || lesson.version > checked.version) refreshed.set(lesson.id, lesson);
|
|
2316
|
+
}
|
|
2317
|
+
for (const lesson of refreshed.values()) {
|
|
2192
2318
|
byId.set(lesson.id, lesson);
|
|
2193
2319
|
if (!opts.dryRun) store.saveLesson(lesson, { reindex: false });
|
|
2194
2320
|
}
|
|
2195
|
-
for (const rule of remote.recallLessons) rules.set(rule.id, rule);
|
|
2321
|
+
for (const rule of remoteOK ? remote.recallLessons : []) rules.set(rule.id, rule);
|
|
2196
2322
|
if (!opts.dryRun) {
|
|
2197
|
-
for (const conflict of remote.pendingConflicts) store.saveConflict(conflict);
|
|
2198
|
-
if (
|
|
2323
|
+
for (const conflict of remoteOK ? remote.pendingConflicts : []) store.saveConflict(conflict);
|
|
2324
|
+
if (refreshed.size) store.rebuildIndex();
|
|
2199
2325
|
}
|
|
2326
|
+
if (!remoteOK) degraded = "hosted search unavailable; using refreshed locally authored knowledge";
|
|
2200
2327
|
} else {
|
|
2201
2328
|
degraded = "mothership unavailable or identity mismatch; using locally authored knowledge";
|
|
2202
2329
|
}
|
|
@@ -2307,11 +2434,11 @@ __export(demo_exports, {
|
|
|
2307
2434
|
runDemo: () => runDemo
|
|
2308
2435
|
});
|
|
2309
2436
|
import assert from "node:assert/strict";
|
|
2310
|
-
import
|
|
2437
|
+
import fs8 from "node:fs";
|
|
2311
2438
|
import os2 from "node:os";
|
|
2312
|
-
import
|
|
2439
|
+
import path8 from "node:path";
|
|
2313
2440
|
async function runDemo(write) {
|
|
2314
|
-
const root =
|
|
2441
|
+
const root = fs8.mkdtempSync(path8.join(os2.tmpdir(), "bunshin-demo-"));
|
|
2315
2442
|
try {
|
|
2316
2443
|
const store = Store.init(root, { ...APR_ENGINEER, apiUrl: "" });
|
|
2317
2444
|
const { lessons, constraint, workflow } = aprScenario();
|
|
@@ -2328,7 +2455,7 @@ Task: ${APR_TASK}
|
|
|
2328
2455
|
write(result.text);
|
|
2329
2456
|
write("\nPASS: Collections constraint and workflow recalled; C-suite, other-company, and superseded lessons excluded.\n");
|
|
2330
2457
|
} finally {
|
|
2331
|
-
|
|
2458
|
+
fs8.rmSync(root, { recursive: true, force: true });
|
|
2332
2459
|
}
|
|
2333
2460
|
}
|
|
2334
2461
|
var init_demo = __esm({
|
|
@@ -2347,27 +2474,27 @@ __export(reflect_exports, {
|
|
|
2347
2474
|
buildLesson: () => buildLesson,
|
|
2348
2475
|
runReflection: () => runReflection
|
|
2349
2476
|
});
|
|
2350
|
-
import
|
|
2351
|
-
import
|
|
2477
|
+
import fs9 from "node:fs";
|
|
2478
|
+
import path9 from "node:path";
|
|
2352
2479
|
import { createHash as createHash3 } from "node:crypto";
|
|
2353
2480
|
async function runReflection(store, opts) {
|
|
2354
2481
|
const empty = { lessonIds: [], used: [], missed: [], attribution: [] };
|
|
2355
|
-
const lock =
|
|
2482
|
+
const lock = path9.join(store.root, `reflect-${createHash3("sha256").update(opts.sessionId).digest("hex")}.lock`);
|
|
2356
2483
|
const deadline = Date.now() + 66e4;
|
|
2357
2484
|
for (; ; ) {
|
|
2358
2485
|
try {
|
|
2359
|
-
|
|
2486
|
+
fs9.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
2360
2487
|
break;
|
|
2361
2488
|
} catch (error) {
|
|
2362
2489
|
if (error.code !== "EEXIST") throw error;
|
|
2363
2490
|
try {
|
|
2364
|
-
const owner = Number(
|
|
2491
|
+
const owner = Number(fs9.readFileSync(lock, "utf8"));
|
|
2365
2492
|
if (Number.isInteger(owner) && owner > 0) {
|
|
2366
2493
|
try {
|
|
2367
2494
|
process.kill(owner, 0);
|
|
2368
2495
|
} catch (error2) {
|
|
2369
2496
|
if (error2.code === "ESRCH") {
|
|
2370
|
-
|
|
2497
|
+
fs9.unlinkSync(lock);
|
|
2371
2498
|
continue;
|
|
2372
2499
|
}
|
|
2373
2500
|
}
|
|
@@ -2382,7 +2509,7 @@ async function runReflection(store, opts) {
|
|
|
2382
2509
|
return await reflectSession(store, opts);
|
|
2383
2510
|
} finally {
|
|
2384
2511
|
try {
|
|
2385
|
-
|
|
2512
|
+
fs9.unlinkSync(lock);
|
|
2386
2513
|
} catch {
|
|
2387
2514
|
}
|
|
2388
2515
|
}
|
|
@@ -2440,7 +2567,12 @@ async function reflectSession(store, opts) {
|
|
|
2440
2567
|
for (const raw of out.lessons) {
|
|
2441
2568
|
const rejection = workflowRejection(raw, facts, excerpt);
|
|
2442
2569
|
if (rejection) {
|
|
2443
|
-
store.log("reflect-workflow-rejected", {
|
|
2570
|
+
store.log("reflect-workflow-rejected", {
|
|
2571
|
+
session: opts.sessionId,
|
|
2572
|
+
title: redact(raw.title),
|
|
2573
|
+
reason: rejection,
|
|
2574
|
+
evidence: Array.isArray(raw.verificationEvidence) ? raw.verificationEvidence.filter((quote) => typeof quote === "string").map((quote) => redact(quote).slice(0, 1e3)) : []
|
|
2575
|
+
});
|
|
2444
2576
|
continue;
|
|
2445
2577
|
}
|
|
2446
2578
|
const lesson = store.saveLesson(buildLesson(raw, store, opts), { reindex: false });
|
|
@@ -2464,13 +2596,15 @@ function workflowRejection(raw, facts, excerpt) {
|
|
|
2464
2596
|
}
|
|
2465
2597
|
const normalize = (text) => redact(text).replace(/\s+/g, " ").trim();
|
|
2466
2598
|
const visible = normalize(excerpt);
|
|
2467
|
-
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));
|
|
2468
2600
|
const evidence = raw.verificationEvidence;
|
|
2469
|
-
if (!Array.isArray(evidence) || !evidence.length
|
|
2470
|
-
|
|
2601
|
+
if (!Array.isArray(evidence) || !evidence.length) return "workflow requires verification quotes from successful tool results";
|
|
2602
|
+
for (const [index, quote] of evidence.entries()) {
|
|
2603
|
+
if (typeof quote !== "string") return `workflow verification quote ${index + 1} is not text`;
|
|
2471
2604
|
const text = normalize(quote);
|
|
2472
|
-
|
|
2473
|
-
|
|
2605
|
+
if (!text || !results.some((result) => result.includes(text))) return `workflow verification quote ${index + 1} does not match a successful tool result`;
|
|
2606
|
+
if (!visible.includes(text)) return `workflow verification quote ${index + 1} is outside the visible excerpt`;
|
|
2607
|
+
}
|
|
2474
2608
|
}
|
|
2475
2609
|
function countFacts(turns) {
|
|
2476
2610
|
return {
|
|
@@ -2710,8 +2844,8 @@ var init_meta_reflect = __esm({
|
|
|
2710
2844
|
});
|
|
2711
2845
|
|
|
2712
2846
|
// src/cli.ts
|
|
2713
|
-
import
|
|
2714
|
-
import
|
|
2847
|
+
import fs10 from "node:fs";
|
|
2848
|
+
import path10 from "node:path";
|
|
2715
2849
|
|
|
2716
2850
|
// src/detach.ts
|
|
2717
2851
|
import { spawn } from "node:child_process";
|
|
@@ -2932,6 +3066,7 @@ async function hookCommand(rawEvent, opts = {}) {
|
|
|
2932
3066
|
init_llm();
|
|
2933
3067
|
init_store();
|
|
2934
3068
|
init_util();
|
|
3069
|
+
init_project_hooks();
|
|
2935
3070
|
function parseArgs(argv) {
|
|
2936
3071
|
const out = { _: [], flags: {} };
|
|
2937
3072
|
const valueless = /* @__PURE__ */ new Set(["json", "quiet", "force", "global", "help", "version", "dry-run", "offline", "apply", "no-hooks", "remove-hooks", "key-stdin", "debug", "verbose"]);
|
|
@@ -2979,7 +3114,7 @@ function version() {
|
|
|
2979
3114
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
2980
3115
|
try {
|
|
2981
3116
|
const file = new URL(rel, import.meta.url);
|
|
2982
|
-
const pkg = JSON.parse(
|
|
3117
|
+
const pkg = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
2983
3118
|
if (pkg.name === "@bunshin-cc/bunshin" && pkg.version) return pkg.version;
|
|
2984
3119
|
} catch {
|
|
2985
3120
|
}
|
|
@@ -3045,7 +3180,7 @@ async function main(argv, opts = {}) {
|
|
|
3045
3180
|
if (harness !== "codex" && harness !== "claude-code" && harness !== "pi") throw new Error("Choose your agent: bunshin setup --harness codex, claude-code, or pi.");
|
|
3046
3181
|
const agent = harness === "claude-code" ? "claude" : harness;
|
|
3047
3182
|
if (!executable2(agent)) throw new Error(`${agent} was not found. Install and sign in to your agent, then run setup again.`);
|
|
3048
|
-
const existing = new Store(
|
|
3183
|
+
const existing = new Store(path10.join(cwd, ".bunshin")).config();
|
|
3049
3184
|
const apiUrl = str2(args.flags["api-url"]);
|
|
3050
3185
|
const key = bool(args.flags["key-stdin"]) ? await new Promise((resolve, reject) => {
|
|
3051
3186
|
let value = "";
|
|
@@ -3066,6 +3201,7 @@ Key saved privately outside the project.
|
|
|
3066
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.`);
|
|
3067
3202
|
if (result.hooks) out(`${result.hooks.installed} ${harness === "pi" ? "extension" : "hooks"} configured in ${result.hooks.file}
|
|
3068
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");
|
|
3069
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");
|
|
3070
3206
|
else if (harness === "pi" && result.hooks) out("Open Pi in this project and approve its project extension. Check it with /bunshin status.\n");
|
|
3071
3207
|
else if (result.hooks) out("Open a new Claude Code session in this project to load the hooks.\n");
|
|
@@ -3128,7 +3264,7 @@ ${HELP}`);
|
|
|
3128
3264
|
function openStore(args, cwd) {
|
|
3129
3265
|
const root = str2(args.flags.root);
|
|
3130
3266
|
if (root) {
|
|
3131
|
-
const store = new Store(
|
|
3267
|
+
const store = new Store(path10.resolve(root));
|
|
3132
3268
|
store.ensureLayout();
|
|
3133
3269
|
return store;
|
|
3134
3270
|
}
|
|
@@ -3167,6 +3303,7 @@ async function cmdInit(args, cwd, out, err) {
|
|
|
3167
3303
|
if (result.hooks) {
|
|
3168
3304
|
out(` hooks: ${result.hooks.installed} Bunshin entries in ${result.hooks.file}${result.hooks.changed ? "" : " (already up to date)"}
|
|
3169
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");
|
|
3170
3307
|
if (result.hooks.error) err(` hooks: ${result.hooks.error}
|
|
3171
3308
|
`);
|
|
3172
3309
|
}
|
|
@@ -3184,10 +3321,10 @@ function cmdStatus(store, args, out) {
|
|
|
3184
3321
|
const conflicts = store.listConflicts().filter((c) => c.status === "open");
|
|
3185
3322
|
const metrics = store.readMetrics(void 0);
|
|
3186
3323
|
const last50 = metrics?.windows?.find((w) => w.name === "last50");
|
|
3187
|
-
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")] : [];
|
|
3188
3325
|
const hooks = config.harness === "pi" ? hookFiles.filter((file) => {
|
|
3189
3326
|
try {
|
|
3190
|
-
return
|
|
3327
|
+
return fs10.readFileSync(file, "utf8").startsWith("// Managed by Bunshin setup.");
|
|
3191
3328
|
} catch {
|
|
3192
3329
|
return false;
|
|
3193
3330
|
}
|
|
@@ -3204,7 +3341,7 @@ function cmdStatus(store, args, out) {
|
|
|
3204
3341
|
mothership: config.apiUrl ? { apiUrl: config.apiUrl, key: config.apiKey ? "set" : "missing", lastSyncAt: config.lastSyncAt ?? null } : null,
|
|
3205
3342
|
llmBackend: resolveBackend(config) ?? "none (set ANTHROPIC_API_KEY or install the claude/codex CLI)",
|
|
3206
3343
|
hooks: { [config.harness]: hooks },
|
|
3207
|
-
hookFiles: hookFiles.filter((file) =>
|
|
3344
|
+
hookFiles: hookFiles.filter((file) => fs10.existsSync(file)),
|
|
3208
3345
|
recallQuality: last50 ? { precision: last50.precision, recall: last50.recall, f1: last50.f1, traces: last50.traces } : null
|
|
3209
3346
|
};
|
|
3210
3347
|
if (bool(args.flags.json)) {
|
|
@@ -3236,7 +3373,7 @@ function cmdStatus(store, args, out) {
|
|
|
3236
3373
|
}
|
|
3237
3374
|
function hookCount(file) {
|
|
3238
3375
|
try {
|
|
3239
|
-
const raw = JSON.parse(
|
|
3376
|
+
const raw = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3240
3377
|
let n = 0;
|
|
3241
3378
|
for (const groups of Object.values(raw.hooks ?? {})) {
|
|
3242
3379
|
for (const g of groups ?? []) {
|
|
@@ -3356,7 +3493,7 @@ function cmdLessonAdd(store, args, out, err) {
|
|
|
3356
3493
|
let content = str2(args.flags.content) ?? "";
|
|
3357
3494
|
if (!content || content === "-" || args._.includes("-")) {
|
|
3358
3495
|
try {
|
|
3359
|
-
content =
|
|
3496
|
+
content = fs10.readFileSync(0, "utf8");
|
|
3360
3497
|
} catch {
|
|
3361
3498
|
}
|
|
3362
3499
|
}
|
|
@@ -3422,7 +3559,7 @@ async function cmdReflect(store, args, cwd, out, err) {
|
|
|
3422
3559
|
}
|
|
3423
3560
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect2(), reflect_exports));
|
|
3424
3561
|
const result = await runReflection2(store, {
|
|
3425
|
-
sessionId: str2(args.flags.session) ??
|
|
3562
|
+
sessionId: str2(args.flags.session) ?? path10.basename(transcript).replace(/\.jsonl?$/, ""),
|
|
3426
3563
|
transcriptPath: transcript,
|
|
3427
3564
|
cwd,
|
|
3428
3565
|
force: bool(args.flags.force)
|
|
@@ -3525,7 +3662,7 @@ async function cmdSessionEnd(store, args, cwd, out) {
|
|
|
3525
3662
|
const transcript = str2(args.flags.transcript);
|
|
3526
3663
|
const quiet = bool(args.flags.quiet);
|
|
3527
3664
|
const summary = { session: sessionId };
|
|
3528
|
-
if (transcript &&
|
|
3665
|
+
if (transcript && fs10.existsSync(transcript)) {
|
|
3529
3666
|
const { runReflection: runReflection2 } = await Promise.resolve().then(() => (init_reflect2(), reflect_exports));
|
|
3530
3667
|
summary.reflect = await runReflection2(store, { sessionId, transcriptPath: transcript, cwd, force: true });
|
|
3531
3668
|
}
|