@viloforge/vfkb 0.2.3 → 0.4.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.
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  import { randomBytes as randomBytes2 } from "node:crypto";
10
10
 
11
11
  // src/storage.ts
12
- import { basename, dirname, join as join3, resolve } from "node:path";
12
+ import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { createHash } from "node:crypto";
15
15
 
@@ -207,6 +207,134 @@ function storageBackend() {
207
207
  return current;
208
208
  }
209
209
 
210
+ // src/journal.ts
211
+ import { execFileSync } from "node:child_process";
212
+ import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
213
+ import { dirname, join as join3, relative, sep } from "node:path";
214
+ var walPath = (brain) => join3(brain, ".journal", "wal.jsonl");
215
+ var suppressedPath = (brain) => join3(brain, ".journal", "suppressed");
216
+ function rewriteWal(wal, keep) {
217
+ const tmp = `${wal}.tmp`;
218
+ writeFileSync2(tmp, keep.length > 0 ? keep.map((l) => l.raw).join("\n") + "\n" : "", "utf8");
219
+ renameSync(tmp, wal);
220
+ }
221
+ var pairOf = (r) => `${String(r.id)}\0${String(r.updated ?? "")}`;
222
+ function parseLines(text) {
223
+ const out = [];
224
+ for (const raw of text.split("\n")) {
225
+ if (raw.trim().length === 0) continue;
226
+ try {
227
+ const rec = JSON.parse(raw);
228
+ if (typeof rec.id !== "string") continue;
229
+ out.push({ raw, pair: pairOf(rec), id: rec.id });
230
+ } catch {
231
+ }
232
+ }
233
+ return out;
234
+ }
235
+ function pairsOfFile(path) {
236
+ if (!existsSync2(path)) return /* @__PURE__ */ new Set();
237
+ return new Set(parseLines(readFileSync3(path, "utf8")).map((l) => l.pair));
238
+ }
239
+ function suppressedPairs(brain) {
240
+ const p = suppressedPath(brain);
241
+ if (!existsSync2(p)) return /* @__PURE__ */ new Set();
242
+ return new Set(
243
+ readFileSync3(p, "utf8").split("\n").filter((l) => l.includes(" ")).map((l) => l.replace(" ", "\0"))
244
+ );
245
+ }
246
+ function journalAppend(brain, rec) {
247
+ if (process.env.VFKB_NO_JOURNAL) return;
248
+ try {
249
+ mkdirSync3(join3(brain, ".journal"), { recursive: true });
250
+ appendFileSync2(walPath(brain), JSON.stringify(rec) + "\n", "utf8");
251
+ } catch (err) {
252
+ process.stderr.write(
253
+ `vfkb: journal mirror failed (primary append proceeds unprotected): ${err.message}
254
+ `
255
+ );
256
+ }
257
+ }
258
+ function pairsAtHead(brain) {
259
+ const repoDir = dirname(brain);
260
+ const git2 = (...a) => execFileSync("git", ["-C", repoDir, ...a], {
261
+ encoding: "utf8",
262
+ stdio: ["ignore", "pipe", "ignore"]
263
+ }).trim();
264
+ try {
265
+ if (git2("rev-parse", "--is-inside-work-tree") !== "true") return "not-git";
266
+ } catch {
267
+ return "not-git";
268
+ }
269
+ try {
270
+ const top = git2("rev-parse", "--show-toplevel");
271
+ const rel = relative(top, join3(brain, "entries.jsonl")).split(sep).join("/");
272
+ const head = execFileSync("git", ["-C", repoDir, "cat-file", "-p", `HEAD:${rel}`], {
273
+ encoding: "utf8",
274
+ stdio: ["ignore", "pipe", "ignore"]
275
+ });
276
+ return new Set(parseLines(head).map((l) => l.pair));
277
+ } catch {
278
+ return "unknown";
279
+ }
280
+ }
281
+ function recoverFromJournal(brain) {
282
+ if (process.env.VFKB_NO_JOURNAL) return { restored: 0, pruned: 0 };
283
+ const wal = walPath(brain);
284
+ if (!existsSync2(wal)) return { restored: 0, pruned: 0 };
285
+ const walLines = parseLines(readFileSync3(wal, "utf8"));
286
+ if (walLines.length === 0) return { restored: 0, pruned: 0 };
287
+ const entriesPath = join3(brain, "entries.jsonl");
288
+ const present = pairsOfFile(entriesPath);
289
+ const suppressed = suppressedPairs(brain);
290
+ const toRestore = walLines.filter((l) => !present.has(l.pair) && !suppressed.has(l.pair));
291
+ if (toRestore.length > 0) {
292
+ let prefix = "";
293
+ if (existsSync2(entriesPath)) {
294
+ const cur = readFileSync3(entriesPath, "utf8");
295
+ if (cur.length > 0 && !cur.endsWith("\n")) prefix = "\n";
296
+ }
297
+ appendFileSync2(entriesPath, prefix + toRestore.map((l) => l.raw).join("\n") + "\n", "utf8");
298
+ }
299
+ const head = pairsAtHead(brain);
300
+ let keep;
301
+ if (head === "unknown") {
302
+ keep = walLines;
303
+ } else if (head === "not-git") {
304
+ keep = walLines.filter((l) => !suppressed.has(l.pair) && !present.has(l.pair) && !toRestore.includes(l));
305
+ } else {
306
+ keep = walLines.filter((l) => !suppressed.has(l.pair) && !head.has(l.pair));
307
+ }
308
+ const pruned = walLines.length - keep.length;
309
+ if (pruned > 0) {
310
+ rewriteWal(wal, keep);
311
+ }
312
+ return { restored: toRestore.length, pruned };
313
+ }
314
+ function purgeJournal(brain, opts) {
315
+ const wal = walPath(brain);
316
+ if (!existsSync2(wal)) return { purged: 0 };
317
+ const walLines = parseLines(readFileSync3(wal, "utf8"));
318
+ const gone = walLines.filter((l) => opts.all ? true : l.id === opts.id);
319
+ if (gone.length === 0) return { purged: 0 };
320
+ const keep = walLines.filter((l) => !gone.includes(l));
321
+ mkdirSync3(join3(brain, ".journal"), { recursive: true });
322
+ appendFileSync2(suppressedPath(brain), gone.map((l) => l.pair.replace("\0", " ")).join("\n") + "\n", "utf8");
323
+ rewriteWal(wal, keep);
324
+ return { purged: gone.length };
325
+ }
326
+ function journalStatus(brain) {
327
+ const wal = walPath(brain);
328
+ const walLines = existsSync2(wal) ? parseLines(readFileSync3(wal, "utf8")) : [];
329
+ const present = pairsOfFile(join3(brain, "entries.jsonl"));
330
+ const suppressed = suppressedPairs(brain);
331
+ return {
332
+ walLines: walLines.length,
333
+ restorable: walLines.filter((l) => !present.has(l.pair) && !suppressed.has(l.pair)).length,
334
+ suppressedInEntries: [...suppressed].filter((p) => present.has(p)).length
335
+ };
336
+ }
337
+
210
338
  // node_modules/zod/v4/classic/external.js
211
339
  var external_exports = {};
212
340
  __export(external_exports, {
@@ -14772,7 +14900,7 @@ function isTombstone(r) {
14772
14900
  return r.deleted === true;
14773
14901
  }
14774
14902
  function brainDir() {
14775
- return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR || join3(homedir(), ".vfkb");
14903
+ return process.env.VFKB_DATA_DIR || process.env.VFKB_DIR || join4(homedir(), ".vfkb");
14776
14904
  }
14777
14905
  function defaultProject() {
14778
14906
  const raw = (() => {
@@ -14781,7 +14909,7 @@ function defaultProject() {
14781
14909
  if (explicit) {
14782
14910
  const abs = resolve(explicit);
14783
14911
  const name = basename(abs);
14784
- return name.startsWith(".") ? basename(dirname(abs)) : name;
14912
+ return name.startsWith(".") ? basename(dirname2(abs)) : name;
14785
14913
  }
14786
14914
  const root = process.env.CLAUDE_PROJECT_DIR;
14787
14915
  if (root) return basename(resolve(root));
@@ -14790,7 +14918,9 @@ function defaultProject() {
14790
14918
  return raw.replace(/["<>&]/g, "") || "spike";
14791
14919
  }
14792
14920
  function appendRecord(rec) {
14793
- storageBackend().append(rec);
14921
+ const be = storageBackend();
14922
+ if (be.name === "jsonl-fs") journalAppend(be.location(), rec);
14923
+ be.append(rec);
14794
14924
  writeMeta();
14795
14925
  }
14796
14926
  function withExclusive(fn) {
@@ -14960,21 +15090,21 @@ function assertNoSecrets(text) {
14960
15090
  }
14961
15091
 
14962
15092
  // src/counters.ts
14963
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
14964
- import { join as join4 } from "node:path";
15093
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
15094
+ import { join as join5 } from "node:path";
14965
15095
  function signalsFile() {
14966
- return join4(brainDir(), ".signals", "counters.jsonl");
15096
+ return join5(brainDir(), ".signals", "counters.jsonl");
14967
15097
  }
14968
15098
  function recordSignal(entryId, kind, source) {
14969
15099
  const sig = { entryId, kind, at: (/* @__PURE__ */ new Date()).toISOString(), source };
14970
- mkdirSync3(join4(brainDir(), ".signals"), { recursive: true });
14971
- appendFileSync2(signalsFile(), JSON.stringify(sig) + "\n", "utf8");
15100
+ mkdirSync4(join5(brainDir(), ".signals"), { recursive: true });
15101
+ appendFileSync3(signalsFile(), JSON.stringify(sig) + "\n", "utf8");
14972
15102
  return sig;
14973
15103
  }
14974
15104
  function readSignals() {
14975
15105
  const f = signalsFile();
14976
- if (!existsSync2(f)) return [];
14977
- return readFileSync3(f, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
15106
+ if (!existsSync3(f)) return [];
15107
+ return readFileSync4(f, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
14978
15108
  }
14979
15109
  function tally(entryId, signals = readSignals()) {
14980
15110
  let helpful = 0;
@@ -15350,6 +15480,22 @@ function latestHandoff(all = readAll(), today = nowIso().slice(0, 10), supersede
15350
15480
  }
15351
15481
  return latest;
15352
15482
  }
15483
+ var CROSS_REPO_PIN_CAP_CHARS = 2e3;
15484
+ function latestCrossRepo(all = readAll(), today = nowIso().slice(0, 10), superseded = supersededIds(all)) {
15485
+ let latest = null;
15486
+ for (const e of all) {
15487
+ if (!e.tags.includes("cross-repo")) continue;
15488
+ if (e.tags.includes("handoff") || e.tags.includes("next")) continue;
15489
+ if (!isInjectable(e, today, superseded)) continue;
15490
+ if (!latest) {
15491
+ latest = e;
15492
+ continue;
15493
+ }
15494
+ const cmp = e.updated.localeCompare(latest.updated) || e.created.localeCompare(latest.created);
15495
+ if (cmp >= 0) latest = e;
15496
+ }
15497
+ return latest;
15498
+ }
15353
15499
  function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET_CHARS) {
15354
15500
  const all = readAll();
15355
15501
  const today = nowIso().slice(0, 10);
@@ -15377,25 +15523,57 @@ function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET
15377
15523
  body += `## Last handoff
15378
15524
  - [${handoff.type} ${trustGlyph(handoff)}] ${text}
15379
15525
 
15526
+ `;
15527
+ }
15528
+ const crossRepo = latestCrossRepo(all, today, superseded);
15529
+ if (crossRepo && !constitutionalIds.has(crossRepo.id)) {
15530
+ const text = crossRepo.text.length > CROSS_REPO_PIN_CAP_CHARS ? `${crossRepo.text.slice(0, CROSS_REPO_PIN_CAP_CHARS)}\u2026 (truncated \u2014 kb_get ${crossRepo.id} for the rest)` : crossRepo.text;
15531
+ body += `## Cross-repo operations
15532
+ - [${crossRepo.type} ${trustGlyph(crossRepo)}] ${text}
15533
+
15380
15534
  `;
15381
15535
  }
15382
15536
  body += renderContextMap() + "\n\n";
15537
+ const kept = [];
15538
+ let keptLen = 0;
15383
15539
  let dropped = 0;
15540
+ const fixedLen = () => header.length + body.length + footer.length;
15384
15541
  for (const e of injectable) {
15385
15542
  if (constitutionalIds.has(e.id)) continue;
15386
15543
  if (handoff && e.id === handoff.id) continue;
15544
+ if (crossRepo && e.id === crossRepo.id) continue;
15387
15545
  const line = `- [${e.type} ${trustGlyph(e)}] ${e.text}
15388
15546
  `;
15389
- if (header.length + body.length + line.length + footer.length > budget) {
15547
+ if (fixedLen() + keptLen + line.length > budget) {
15390
15548
  dropped++;
15391
15549
  continue;
15392
15550
  }
15393
- body += line;
15551
+ kept.push(line);
15552
+ keptLen += line.length;
15394
15553
  }
15395
15554
  if (dropped > 0) {
15396
- const note = `<!-- ${dropped} lower-ranked entries omitted for the ${budget}-char budget -->
15555
+ const note = () => `(+ ${dropped} lower-ranked entries omitted for the ${budget}-char budget \u2014 kb_search / kb_list pulls them)
15397
15556
  `;
15398
- if (header.length + body.length + note.length + footer.length <= budget) body += note;
15557
+ const evicted = [];
15558
+ while (kept.length > 0 && fixedLen() + keptLen + note().length > budget) {
15559
+ const line = kept.pop();
15560
+ keptLen -= line.length;
15561
+ evicted.push(line);
15562
+ dropped++;
15563
+ }
15564
+ if (fixedLen() + keptLen + note().length <= budget) {
15565
+ body += kept.join("") + note();
15566
+ } else {
15567
+ while (evicted.length > 0) {
15568
+ const line = evicted.pop();
15569
+ kept.push(line);
15570
+ keptLen += line.length;
15571
+ dropped--;
15572
+ }
15573
+ body += kept.join("");
15574
+ }
15575
+ } else {
15576
+ body += kept.join("");
15399
15577
  }
15400
15578
  return header + body + footer;
15401
15579
  }
@@ -15563,15 +15741,15 @@ ${bundle}`;
15563
15741
 
15564
15742
  // src/export.ts
15565
15743
  import {
15566
- existsSync as existsSync3,
15567
- mkdirSync as mkdirSync4,
15744
+ existsSync as existsSync4,
15745
+ mkdirSync as mkdirSync5,
15568
15746
  readdirSync as readdirSync2,
15569
- readFileSync as readFileSync4,
15747
+ readFileSync as readFileSync5,
15570
15748
  rmSync,
15571
15749
  statSync as statSync2,
15572
- writeFileSync as writeFileSync2
15750
+ writeFileSync as writeFileSync3
15573
15751
  } from "node:fs";
15574
- import { dirname as dirname2, join as join5 } from "node:path";
15752
+ import { dirname as dirname3, join as join6 } from "node:path";
15575
15753
  var GENERATED_MARKER = "generated by vfkb export";
15576
15754
  var MARKER_LINE_RE = /^<!-- generated by vfkb export /m;
15577
15755
  var EXPORT_BUDGET_CHARS = 4e4;
@@ -15620,7 +15798,7 @@ function listFiles(dir) {
15620
15798
  const out = [];
15621
15799
  const walk = (d) => {
15622
15800
  for (const name of readdirSync2(d)) {
15623
- const p = join5(d, name);
15801
+ const p = join6(d, name);
15624
15802
  if (statSync2(p).isDirectory()) walk(p);
15625
15803
  else out.push(p);
15626
15804
  }
@@ -15631,14 +15809,14 @@ function listFiles(dir) {
15631
15809
  function isGenerated(path) {
15632
15810
  if (!path.endsWith(".md")) return false;
15633
15811
  try {
15634
- return MARKER_LINE_RE.test(readFileSync4(path, "utf8"));
15812
+ return MARKER_LINE_RE.test(readFileSync5(path, "utf8"));
15635
15813
  } catch {
15636
15814
  return false;
15637
15815
  }
15638
15816
  }
15639
15817
  function prepareTree(dir) {
15640
- if (!existsSync3(dir)) {
15641
- mkdirSync4(dir, { recursive: true });
15818
+ if (!existsSync4(dir)) {
15819
+ mkdirSync5(dir, { recursive: true });
15642
15820
  return;
15643
15821
  }
15644
15822
  const files = listFiles(dir);
@@ -15653,7 +15831,7 @@ function prepareTree(dir) {
15653
15831
  const dirs = [];
15654
15832
  const collect = (d) => {
15655
15833
  for (const name of readdirSync2(d)) {
15656
- const p = join5(d, name);
15834
+ const p = join6(d, name);
15657
15835
  if (statSync2(p).isDirectory()) {
15658
15836
  collect(p);
15659
15837
  dirs.push(p);
@@ -15669,8 +15847,8 @@ function exportAgentsMd(opts = {}) {
15669
15847
  const { superseded, asOf, eligible } = projectionInputs();
15670
15848
  const project = opts.project ?? defaultProject();
15671
15849
  const budget = opts.budget ?? EXPORT_BUDGET_CHARS;
15672
- const out = opts.out ?? join5(process.cwd(), "AGENTS.md");
15673
- if (existsSync3(out) && !MARKER_LINE_RE.test(readFileSync4(out, "utf8"))) {
15850
+ const out = opts.out ?? join6(process.cwd(), "AGENTS.md");
15851
+ if (existsSync4(out) && !MARKER_LINE_RE.test(readFileSync5(out, "utf8"))) {
15674
15852
  throw new Error(
15675
15853
  `vfkb export: refusing to overwrite ${out} \u2014 it is not a previous export (no generated marker)`
15676
15854
  );
@@ -15729,8 +15907,8 @@ ${spine.trim()}
15729
15907
  }
15730
15908
  if (dropped > 0) body += `<!-- ${dropped} entries omitted for the ${budget}-char budget -->
15731
15909
  `;
15732
- mkdirSync4(dirname2(out), { recursive: true });
15733
- writeFileSync2(out, head + body);
15910
+ mkdirSync5(dirname3(out), { recursive: true });
15911
+ writeFileSync3(out, head + body);
15734
15912
  return { path: out };
15735
15913
  }
15736
15914
  function okfDoc(e, asOf, exportedDirs) {
@@ -15827,14 +16005,14 @@ function deriveLog(eligibleIds, records, live) {
15827
16005
  function exportOkf(opts = {}) {
15828
16006
  const { superseded, asOf, eligible } = projectionInputs();
15829
16007
  const project = opts.project ?? defaultProject();
15830
- const dir = opts.out ?? join5(process.cwd(), ".okf");
16008
+ const dir = opts.out ?? join6(process.cwd(), ".okf");
15831
16009
  prepareTree(dir);
15832
16010
  const marker = `<!-- ${GENERATED_MARKER} okf; regenerate with \`vfkb export okf\`, do not hand-edit (as-of ${asOf}) -->`;
15833
16011
  const files = [];
15834
16012
  const write = (rel, content) => {
15835
- const p = join5(dir, rel);
15836
- mkdirSync4(dirname2(p), { recursive: true });
15837
- writeFileSync2(p, content);
16013
+ const p = join6(dir, rel);
16014
+ mkdirSync5(dirname3(p), { recursive: true });
16015
+ writeFileSync3(p, content);
15838
16016
  files.push(rel);
15839
16017
  };
15840
16018
  const exportedDirs = new Map(eligible.map((e) => [e.id, TYPE_DIR[e.type]]));
@@ -15888,6 +16066,134 @@ function runExport(target, opts = {}) {
15888
16066
  throw new Error("usage: vfkb export <agents-md|okf> [--out <path>]");
15889
16067
  }
15890
16068
 
16069
+ // src/broadcast.ts
16070
+ import { execFileSync as execFileSync2 } from "node:child_process";
16071
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
16072
+ import { basename as basename2, join as join8, resolve as resolve2 } from "node:path";
16073
+
16074
+ // src/manifest.ts
16075
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
16076
+ import { dirname as dirname4, join as join7 } from "node:path";
16077
+
16078
+ // src/version.ts
16079
+ var SCHEMA_VERSION = 1;
16080
+ var ENGINE_VERSION = true ? "0.4.0" : ownPackageVersion();
16081
+ var ENGINE_COMMIT = true ? "63659e5" : "dev";
16082
+
16083
+ // src/manifest.ts
16084
+ function manifestPath(brainDir2) {
16085
+ return join7(brainDir2, "manifest.json");
16086
+ }
16087
+ function currentManifest() {
16088
+ return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
16089
+ }
16090
+ function readManifest(brainDir2) {
16091
+ const p = manifestPath(brainDir2);
16092
+ if (!existsSync5(p)) return void 0;
16093
+ try {
16094
+ return JSON.parse(readFileSync6(p, "utf8"));
16095
+ } catch {
16096
+ return void 0;
16097
+ }
16098
+ }
16099
+ function writeManifest(brainDir2) {
16100
+ const p = manifestPath(brainDir2);
16101
+ const existed = existsSync5(p);
16102
+ const cur = readManifest(brainDir2);
16103
+ const next = currentManifest();
16104
+ if (cur && JSON.stringify(cur) === JSON.stringify(next)) return "skipped";
16105
+ mkdirSync6(dirname4(p), { recursive: true });
16106
+ writeFileSync4(p, JSON.stringify(next, null, 2) + "\n");
16107
+ return existed ? "updated" : "created";
16108
+ }
16109
+
16110
+ // src/broadcast.ts
16111
+ var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["handoff", "next"]);
16112
+ function targetBrainDir(target) {
16113
+ const abs = resolve2(target);
16114
+ return basename2(abs) === ".vfkb" ? abs : join8(abs, ".vfkb");
16115
+ }
16116
+ function gitPosture(repoDir) {
16117
+ const git2 = (...a) => execFileSync2("git", ["-C", repoDir, ...a], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
16118
+ try {
16119
+ const branch = git2("rev-parse", "--abbrev-ref", "HEAD");
16120
+ const parked = branch === "main" || branch === "master";
16121
+ return `uncommitted; target on ${branch}${parked ? " (parked \u2014 rides until a topic-branch brain commit)" : ""}`;
16122
+ } catch {
16123
+ try {
16124
+ if (git2("rev-parse", "--is-inside-work-tree") === "true") {
16125
+ return "uncommitted; target git repository on an unborn branch (no commits yet)";
16126
+ }
16127
+ } catch {
16128
+ }
16129
+ return "uncommitted; target not a git repository";
16130
+ }
16131
+ }
16132
+ function broadcast(text, targets, opts = {}) {
16133
+ const origin = defaultProject();
16134
+ const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16135
+ const extraTags = opts.tags ?? [];
16136
+ for (const t of extraTags) {
16137
+ if (FORBIDDEN_TAGS.has(t)) {
16138
+ throw new Error(
16139
+ `broadcast: tag '${t}' is forbidden on cross-repo records \u2014 it claims the resident's ADR-0049 continuity pin (ADR-0063 \xA71)`
16140
+ );
16141
+ }
16142
+ }
16143
+ const op = (opts.op || "OPERATION").toUpperCase();
16144
+ const MARKER = /^CROSS-REPO \S+ \(\d{4}-\d{2}-\d{2}, from [^)]+\):/;
16145
+ const stamped = MARKER.test(text) ? text : `CROSS-REPO ${op} (${date5}, from ${origin}): ${text}`;
16146
+ const results = [];
16147
+ const written = /* @__PURE__ */ new Set();
16148
+ for (const target of targets) {
16149
+ const brain = targetBrainDir(target);
16150
+ if (written.has(brain)) {
16151
+ results.push({ target, ok: false, reason: "duplicate target (already written in this broadcast)" });
16152
+ continue;
16153
+ }
16154
+ const repoDir = resolve2(brain, "..");
16155
+ const manifestPath2 = join8(brain, "manifest.json");
16156
+ let healed = false;
16157
+ if (!existsSync6(manifestPath2)) {
16158
+ if (!existsSync6(join8(brain, "entries.jsonl"))) {
16159
+ results.push({ target, ok: false, reason: `no brain (no entries.jsonl in ${brain}) \u2014 never bootstrap a wire-less brain (ADR-0063 \xA73)` });
16160
+ continue;
16161
+ }
16162
+ try {
16163
+ writeManifest(brain);
16164
+ } catch (err) {
16165
+ results.push({ target, ok: false, reason: `manifest heal failed: ${err.message}` });
16166
+ continue;
16167
+ }
16168
+ healed = true;
16169
+ }
16170
+ let schema;
16171
+ try {
16172
+ schema = JSON.parse(readFileSync7(manifestPath2, "utf8")).schema_version;
16173
+ } catch {
16174
+ results.push({ target, ok: false, reason: "unreadable manifest.json" });
16175
+ continue;
16176
+ }
16177
+ if (schema !== SCHEMA_VERSION) {
16178
+ results.push({ target, ok: false, reason: `brain schema ${JSON.stringify(schema)} unsupported by engine schema v${SCHEMA_VERSION}` });
16179
+ continue;
16180
+ }
16181
+ const prev = process.env.VFKB_DATA_DIR;
16182
+ try {
16183
+ process.env.VFKB_DATA_DIR = brain;
16184
+ const e = addEntry("fact", stamped, { role: "executor", tags: [.../* @__PURE__ */ new Set(["cross-repo", ...extraTags])] });
16185
+ written.add(brain);
16186
+ results.push({ target, ok: true, id: e.id, posture: gitPosture(repoDir), ...healed ? { healed } : {} });
16187
+ } catch (err) {
16188
+ results.push({ target, ok: false, reason: err.message, ...healed ? { healed } : {} });
16189
+ } finally {
16190
+ if (prev === void 0) delete process.env.VFKB_DATA_DIR;
16191
+ else process.env.VFKB_DATA_DIR = prev;
16192
+ }
16193
+ }
16194
+ return results;
16195
+ }
16196
+
15891
16197
  // src/curator.ts
15892
16198
  function get(id) {
15893
16199
  const e = readAll().find((x) => x.id === id);
@@ -16086,7 +16392,7 @@ function queryExplained(opts = {}) {
16086
16392
  }
16087
16393
 
16088
16394
  // src/gating.ts
16089
- import { resolve as resolve2 } from "node:path";
16395
+ import { resolve as resolve3 } from "node:path";
16090
16396
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
16091
16397
  "write",
16092
16398
  "edit",
@@ -16104,16 +16410,16 @@ function isBrainWrite(toolName, input, brain = brainDir()) {
16104
16410
  if (!toolName || !WRITE_TOOLS.has(toolName.toLowerCase())) return false;
16105
16411
  const p = extractPath(input);
16106
16412
  if (!p) return false;
16107
- const abs = resolve2(p);
16108
- const root = resolve2(brain);
16413
+ const abs = resolve3(p);
16414
+ const root = resolve3(brain);
16109
16415
  return abs === root || abs.startsWith(root + "/");
16110
16416
  }
16111
16417
  var GATING_REASON = "vfkb: edit the brain via the engine/CLI/MCP, not by writing files directly (keeps the index, freshness, and no-secrets invariants).";
16112
16418
 
16113
16419
  // src/stop-reminder.ts
16114
- import { execFileSync } from "node:child_process";
16115
- import { readFileSync as readFileSync5 } from "node:fs";
16116
- import { join as join6, relative } from "node:path";
16420
+ import { execFileSync as execFileSync3 } from "node:child_process";
16421
+ import { readFileSync as readFileSync8 } from "node:fs";
16422
+ import { join as join9, relative as relative2 } from "node:path";
16117
16423
  var STOP_REMINDER = 'vfkb decision-capture check: this turn changed code/docs but no `decision` was recorded to the brain. If a load-bearing decision was made, capture it now via `mcp__vfkb__kb_add` (type=decision, why=<rationale>, role=human) \u2014 or `vfkb add decision "\u2026" --why "\u2026" --role human` \u2014 and add an ADR under docs/adr/ for anything architectural. If NO decision was made this turn, just finish normally.';
16118
16424
  var HANDOFF_MIN_ENTRIES = 3;
16119
16425
  var HANDOFF_REMINDER = 'vfkb handoff check: this session has recorded knowledge but no `handoff`/`next` entry. If you are WRAPPING UP, record a durable handoff now \u2014 `mcp__vfkb__kb_add` (type=fact, tags=handoff,next, role=human) naming what the NEXT session should pick up (a real "next:", not just a summary). If you are still mid-session, ignore this and finish normally \u2014 the SessionEnd floor will leave a fallback if you never do.';
@@ -16129,11 +16435,11 @@ function decideStop(input, ctx) {
16129
16435
  function hasUncommittedWork(cwd = process.cwd(), brain = brainDir()) {
16130
16436
  let out;
16131
16437
  try {
16132
- out = execFileSync("git", ["status", "--porcelain"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
16438
+ out = execFileSync3("git", ["status", "--porcelain"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
16133
16439
  } catch {
16134
16440
  return false;
16135
16441
  }
16136
- const brainRel = relative(cwd, brain).replace(/\\/g, "/");
16442
+ const brainRel = relative2(cwd, brain).replace(/\\/g, "/");
16137
16443
  return out.split("\n").some((line) => {
16138
16444
  const p = line.slice(3).trim();
16139
16445
  if (!p) return false;
@@ -16142,17 +16448,17 @@ function hasUncommittedWork(cwd = process.cwd(), brain = brainDir()) {
16142
16448
  });
16143
16449
  }
16144
16450
  function newBrainEntriesSinceHead(brain = brainDir(), cwd = process.cwd()) {
16145
- const file2 = join6(brain, "entries.jsonl");
16451
+ const file2 = join9(brain, "entries.jsonl");
16146
16452
  let current2;
16147
16453
  try {
16148
- current2 = readFileSync5(file2, "utf8").split("\n").filter(Boolean);
16454
+ current2 = readFileSync8(file2, "utf8").split("\n").filter(Boolean);
16149
16455
  } catch {
16150
16456
  return [];
16151
16457
  }
16152
- const rel = relative(cwd, file2).replace(/\\/g, "/");
16458
+ const rel = relative2(cwd, file2).replace(/\\/g, "/");
16153
16459
  let headCount = 0;
16154
16460
  try {
16155
- const head = execFileSync("git", ["show", `HEAD:${rel}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
16461
+ const head = execFileSync3("git", ["show", `HEAD:${rel}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
16156
16462
  headCount = head.split("\n").filter(Boolean).length;
16157
16463
  } catch {
16158
16464
  headCount = 0;
@@ -16177,14 +16483,14 @@ function gatherStopContext(cwd = process.cwd(), brain = brainDir()) {
16177
16483
  }
16178
16484
 
16179
16485
  // src/git.ts
16180
- import { execFileSync as execFileSync2 } from "node:child_process";
16181
- import { existsSync as existsSync4 } from "node:fs";
16182
- import { join as join7 } from "node:path";
16486
+ import { execFileSync as execFileSync4 } from "node:child_process";
16487
+ import { existsSync as existsSync7 } from "node:fs";
16488
+ import { join as join10 } from "node:path";
16183
16489
  function git(args, cwd) {
16184
- return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16490
+ return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16185
16491
  }
16186
16492
  function ensureRepo(brain) {
16187
- if (!existsSync4(join7(brain, ".git"))) {
16493
+ if (!existsSync7(join10(brain, ".git"))) {
16188
16494
  git(["init", "-q"], brain);
16189
16495
  }
16190
16496
  }
@@ -16210,12 +16516,12 @@ function save(message = "vfkb: update", role = "engine", brain = brainDir()) {
16210
16516
  }
16211
16517
 
16212
16518
  // src/session-end.ts
16213
- import { execFileSync as execFileSync3 } from "node:child_process";
16214
- import { readFileSync as readFileSync6 } from "node:fs";
16215
- import { join as join8, isAbsolute } from "node:path";
16216
- var realGit = (args, cwd) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16519
+ import { execFileSync as execFileSync5 } from "node:child_process";
16520
+ import { readFileSync as readFileSync9 } from "node:fs";
16521
+ import { join as join11, isAbsolute } from "node:path";
16522
+ var realGit = (args, cwd) => execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16217
16523
  function brainEntriesRelPath(dataDir2) {
16218
- return join8(dataDir2, "entries.jsonl").replace(/\\/g, "/");
16524
+ return join11(dataDir2, "entries.jsonl").replace(/\\/g, "/");
16219
16525
  }
16220
16526
  function tryGit(git2, args, cwd) {
16221
16527
  try {
@@ -16239,7 +16545,7 @@ function countAdded(git2, cwd, path) {
16239
16545
  function newEntriesSinceHead(git2, cwd, repoRelEntries, absEntries) {
16240
16546
  let lines;
16241
16547
  try {
16242
- lines = readFileSync6(absEntries, "utf8").split("\n").filter(Boolean);
16548
+ lines = readFileSync9(absEntries, "utf8").split("\n").filter(Boolean);
16243
16549
  } catch {
16244
16550
  return [];
16245
16551
  }
@@ -16316,8 +16622,8 @@ function runSessionEnd(opts = {}) {
16316
16622
  systemMessage: `vfkb: ${added2} new brain entr${added2 === 1 ? "y" : "ies"} on \`${branch}\` left uncommitted \u2014 branch + commit to preserve continuity (vfkb never auto-commits the default branch).`
16317
16623
  };
16318
16624
  }
16319
- const absBrain = isAbsolute(dataDir2) ? dataDir2 : join8(cwd, dataDir2);
16320
- const fresh = newEntriesSinceHead(git2, cwd, entries, join8(absBrain, "entries.jsonl"));
16625
+ const absBrain = isAbsolute(dataDir2) ? dataDir2 : join11(cwd, dataDir2);
16626
+ const fresh = newEntriesSinceHead(git2, cwd, entries, join11(absBrain, "entries.jsonl"));
16321
16627
  let autoHandoff = false;
16322
16628
  if (fresh.length > 0 && !fresh.some(isHandoff2)) {
16323
16629
  try {
@@ -16337,46 +16643,8 @@ function runSessionEnd(opts = {}) {
16337
16643
  }
16338
16644
 
16339
16645
  // src/init.ts
16340
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
16341
- import { basename as basename2, join as join10 } from "node:path";
16342
-
16343
- // src/manifest.ts
16344
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
16345
- import { dirname as dirname3, join as join9 } from "node:path";
16346
-
16347
- // src/version.ts
16348
- var SCHEMA_VERSION = 1;
16349
- var ENGINE_VERSION = true ? "0.2.3" : ownPackageVersion();
16350
- var ENGINE_COMMIT = true ? "21adfd7" : "dev";
16351
-
16352
- // src/manifest.ts
16353
- function manifestPath(brainDir2) {
16354
- return join9(brainDir2, "manifest.json");
16355
- }
16356
- function currentManifest() {
16357
- return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
16358
- }
16359
- function readManifest(brainDir2) {
16360
- const p = manifestPath(brainDir2);
16361
- if (!existsSync5(p)) return void 0;
16362
- try {
16363
- return JSON.parse(readFileSync7(p, "utf8"));
16364
- } catch {
16365
- return void 0;
16366
- }
16367
- }
16368
- function writeManifest(brainDir2) {
16369
- const p = manifestPath(brainDir2);
16370
- const existed = existsSync5(p);
16371
- const cur = readManifest(brainDir2);
16372
- const next = currentManifest();
16373
- if (cur && JSON.stringify(cur) === JSON.stringify(next)) return "skipped";
16374
- mkdirSync5(dirname3(p), { recursive: true });
16375
- writeFileSync3(p, JSON.stringify(next, null, 2) + "\n");
16376
- return existed ? "updated" : "created";
16377
- }
16378
-
16379
- // src/init.ts
16646
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
16647
+ import { basename as basename3, join as join12 } from "node:path";
16380
16648
  var AGENTS_MARKER = "<!-- vfkb:how-we-track-work -->";
16381
16649
  var BOOTSTRAP_REL = ".vfkb/bin/bootstrap.mjs";
16382
16650
  function mcpConfig(project) {
@@ -16461,48 +16729,48 @@ If it is unset, a session-start banner tells you; run \`vfkb doctor\` to check.
16461
16729
  `;
16462
16730
  }
16463
16731
  function readJson(path) {
16464
- if (!existsSync6(path)) return void 0;
16732
+ if (!existsSync8(path)) return void 0;
16465
16733
  try {
16466
- return JSON.parse(readFileSync8(path, "utf8"));
16734
+ return JSON.parse(readFileSync10(path, "utf8"));
16467
16735
  } catch {
16468
16736
  return void 0;
16469
16737
  }
16470
16738
  }
16471
16739
  function writeJson(path, value) {
16472
- writeFileSync4(path, JSON.stringify(value, null, 2) + "\n");
16740
+ writeFileSync5(path, JSON.stringify(value, null, 2) + "\n");
16473
16741
  }
16474
16742
  function eventHasVfkb(arr2) {
16475
16743
  return JSON.stringify(arr2 ?? "").includes(BOOTSTRAP_REL);
16476
16744
  }
16477
16745
  function initProject(root, opts = {}) {
16478
- const project = opts.project || basename2(root) || "project";
16746
+ const project = opts.project || basename3(root) || "project";
16479
16747
  const changes = [];
16480
- const brainDir2 = join10(root, ".vfkb");
16481
- const entries = join10(brainDir2, "entries.jsonl");
16482
- if (!existsSync6(entries)) {
16483
- mkdirSync6(brainDir2, { recursive: true });
16484
- writeFileSync4(entries, "");
16748
+ const brainDir2 = join12(root, ".vfkb");
16749
+ const entries = join12(brainDir2, "entries.jsonl");
16750
+ if (!existsSync8(entries)) {
16751
+ mkdirSync7(brainDir2, { recursive: true });
16752
+ writeFileSync5(entries, "");
16485
16753
  changes.push({ path: ".vfkb/entries.jsonl", action: "created" });
16486
16754
  } else {
16487
16755
  changes.push({ path: ".vfkb/entries.jsonl", action: "skipped" });
16488
16756
  }
16489
16757
  changes.push({ path: ".vfkb/manifest.json", action: writeManifest(brainDir2) });
16490
16758
  {
16491
- const binDir = join10(brainDir2, "bin");
16492
- const path = join10(binDir, "bootstrap.mjs");
16493
- const existed = existsSync6(path);
16494
- const same = existed && readFileSync8(path, "utf8") === BOOTSTRAP_SRC;
16759
+ const binDir = join12(brainDir2, "bin");
16760
+ const path = join12(binDir, "bootstrap.mjs");
16761
+ const existed = existsSync8(path);
16762
+ const same = existed && readFileSync10(path, "utf8") === BOOTSTRAP_SRC;
16495
16763
  if (same) {
16496
16764
  changes.push({ path: BOOTSTRAP_REL, action: "skipped" });
16497
16765
  } else {
16498
- mkdirSync6(binDir, { recursive: true });
16499
- writeFileSync4(path, BOOTSTRAP_SRC);
16766
+ mkdirSync7(binDir, { recursive: true });
16767
+ writeFileSync5(path, BOOTSTRAP_SRC);
16500
16768
  changes.push({ path: BOOTSTRAP_REL, action: existed ? "updated" : "created" });
16501
16769
  }
16502
16770
  }
16503
16771
  {
16504
- const path = join10(root, ".mcp.json");
16505
- const existed = existsSync6(path);
16772
+ const path = join12(root, ".mcp.json");
16773
+ const existed = existsSync8(path);
16506
16774
  const cfg = readJson(path) ?? {};
16507
16775
  cfg.mcpServers = cfg.mcpServers ?? {};
16508
16776
  const desired = mcpConfig(project);
@@ -16516,9 +16784,9 @@ function initProject(root, opts = {}) {
16516
16784
  }
16517
16785
  }
16518
16786
  {
16519
- const dir = join10(root, ".claude");
16520
- const path = join10(dir, "settings.json");
16521
- const existed = existsSync6(path);
16787
+ const dir = join12(root, ".claude");
16788
+ const path = join12(dir, "settings.json");
16789
+ const existed = existsSync8(path);
16522
16790
  const cfg = readJson(path) ?? {};
16523
16791
  cfg.hooks = cfg.hooks ?? {};
16524
16792
  const want = settingsHooks(project);
@@ -16533,7 +16801,7 @@ function initProject(root, opts = {}) {
16533
16801
  touched = true;
16534
16802
  }
16535
16803
  if (touched) {
16536
- mkdirSync6(dir, { recursive: true });
16804
+ mkdirSync7(dir, { recursive: true });
16537
16805
  writeJson(path, cfg);
16538
16806
  changes.push({ path: ".claude/settings.json", action: existed ? "updated" : "created" });
16539
16807
  } else {
@@ -16541,10 +16809,10 @@ function initProject(root, opts = {}) {
16541
16809
  }
16542
16810
  }
16543
16811
  {
16544
- const path = join10(root, ".gitignore");
16545
- const lines = [".vfkb/index-meta.json", ".vfkb/.sessions/", ".vfkb/.signals/"];
16546
- const existed = existsSync6(path);
16547
- const cur = existed ? readFileSync8(path, "utf8") : "";
16812
+ const path = join12(root, ".gitignore");
16813
+ const lines = [".vfkb/index-meta.json", ".vfkb/.sessions/", ".vfkb/.signals/", ".vfkb/.journal/"];
16814
+ const existed = existsSync8(path);
16815
+ const cur = existed ? readFileSync10(path, "utf8") : "";
16548
16816
  const missing = lines.filter((l) => !cur.split(/\r?\n/).includes(l));
16549
16817
  if (missing.length === 0) {
16550
16818
  changes.push({ path: ".gitignore", action: "skipped" });
@@ -16553,15 +16821,15 @@ function initProject(root, opts = {}) {
16553
16821
  const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 derived/operational (only .vfkb/entries.jsonl is committed)
16554
16822
  ${missing.join("\n")}
16555
16823
  `;
16556
- writeFileSync4(path, cur + block);
16824
+ writeFileSync5(path, cur + block);
16557
16825
  changes.push({ path: ".gitignore", action: existed ? "updated" : "created" });
16558
16826
  }
16559
16827
  }
16560
16828
  {
16561
- const path = join10(root, ".gitattributes");
16829
+ const path = join12(root, ".gitattributes");
16562
16830
  const line = ".vfkb/entries.jsonl merge=union";
16563
- const existed = existsSync6(path);
16564
- const cur = existed ? readFileSync8(path, "utf8") : "";
16831
+ const existed = existsSync8(path);
16832
+ const cur = existed ? readFileSync10(path, "utf8") : "";
16565
16833
  if (cur.split(/\r?\n/).includes(line)) {
16566
16834
  changes.push({ path: ".gitattributes", action: "skipped" });
16567
16835
  } else {
@@ -16569,19 +16837,19 @@ ${missing.join("\n")}
16569
16837
  const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 the append-only brain unions across branches (ADR-0041)
16570
16838
  ${line}
16571
16839
  `;
16572
- writeFileSync4(path, cur + block);
16840
+ writeFileSync5(path, cur + block);
16573
16841
  changes.push({ path: ".gitattributes", action: existed ? "updated" : "created" });
16574
16842
  }
16575
16843
  }
16576
16844
  {
16577
- const path = join10(root, "AGENTS.md");
16578
- const existed = existsSync6(path);
16579
- const cur = existed ? readFileSync8(path, "utf8") : "";
16845
+ const path = join12(root, "AGENTS.md");
16846
+ const existed = existsSync8(path);
16847
+ const cur = existed ? readFileSync10(path, "utf8") : "";
16580
16848
  if (cur.includes(AGENTS_MARKER)) {
16581
16849
  changes.push({ path: "AGENTS.md", action: "skipped" });
16582
16850
  } else {
16583
- const sep = cur && !cur.endsWith("\n") ? "\n\n" : cur ? "\n" : "";
16584
- writeFileSync4(path, cur + sep + agentsSnippet(project));
16851
+ const sep2 = cur && !cur.endsWith("\n") ? "\n\n" : cur ? "\n" : "";
16852
+ writeFileSync5(path, cur + sep2 + agentsSnippet(project));
16585
16853
  changes.push({ path: "AGENTS.md", action: existed ? "updated" : "created" });
16586
16854
  }
16587
16855
  }
@@ -16600,13 +16868,13 @@ function approvalNotice(project) {
16600
16868
  }
16601
16869
 
16602
16870
  // src/doctor.ts
16603
- import { execFileSync as execFileSync4 } from "node:child_process";
16604
- import { existsSync as existsSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7 } from "node:fs";
16605
- import { join as join11, dirname as dirname4 } from "node:path";
16871
+ import { execFileSync as execFileSync6 } from "node:child_process";
16872
+ import { existsSync as existsSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
16873
+ import { join as join13, dirname as dirname5 } from "node:path";
16606
16874
  function readJson2(path) {
16607
- if (!existsSync7(path)) return void 0;
16875
+ if (!existsSync9(path)) return void 0;
16608
16876
  try {
16609
- return JSON.parse(readFileSync9(path, "utf8"));
16877
+ return JSON.parse(readFileSync11(path, "utf8"));
16610
16878
  } catch {
16611
16879
  return void 0;
16612
16880
  }
@@ -16639,16 +16907,16 @@ function detectPluginWiring(settings, root, pluginsFile) {
16639
16907
  }
16640
16908
  function claudeConfigDir(env) {
16641
16909
  if (env.CLAUDE_CONFIG_DIR) return env.CLAUDE_CONFIG_DIR;
16642
- return env.HOME ? join11(env.HOME, ".claude") : void 0;
16910
+ return env.HOME ? join13(env.HOME, ".claude") : void 0;
16643
16911
  }
16644
16912
  function localHeadSha(cloneDir) {
16645
- const head = readFileMaybe(join11(cloneDir, ".git", "HEAD"));
16913
+ const head = readFileMaybe(join13(cloneDir, ".git", "HEAD"));
16646
16914
  if (!head) return void 0;
16647
16915
  const ref = head.trim().match(/^ref:\s*(\S+)$/)?.[1];
16648
16916
  if (!ref) return /^[0-9a-f]{40}$/.test(head.trim()) ? head.trim() : void 0;
16649
- const loose = readFileMaybe(join11(cloneDir, ".git", ...ref.split("/")));
16917
+ const loose = readFileMaybe(join13(cloneDir, ".git", ...ref.split("/")));
16650
16918
  if (loose) return loose.trim();
16651
- const packed = readFileMaybe(join11(cloneDir, ".git", "packed-refs"));
16919
+ const packed = readFileMaybe(join13(cloneDir, ".git", "packed-refs"));
16652
16920
  for (const line of packed?.split("\n") ?? []) {
16653
16921
  const [sha, name] = line.trim().split(/\s+/);
16654
16922
  if (name === ref) return sha;
@@ -16657,12 +16925,12 @@ function localHeadSha(cloneDir) {
16657
16925
  }
16658
16926
  function readFileMaybe(path) {
16659
16927
  try {
16660
- return readFileSync9(path, "utf8");
16928
+ return readFileSync11(path, "utf8");
16661
16929
  } catch {
16662
16930
  return void 0;
16663
16931
  }
16664
16932
  }
16665
- var realGit2 = (args, cwd) => execFileSync4("git", args, {
16933
+ var realGit2 = (args, cwd) => execFileSync6("git", args, {
16666
16934
  cwd,
16667
16935
  encoding: "utf8",
16668
16936
  timeout: 5e3,
@@ -16671,14 +16939,14 @@ var realGit2 = (args, cwd) => execFileSync4("git", args, {
16671
16939
  });
16672
16940
  function checkCurrency(plugin, opts, configDir) {
16673
16941
  const marketplace = plugin.key.split("@")[1];
16674
- const kmFile = opts.knownMarketplacesFile ?? (configDir ? join11(configDir, "plugins", "known_marketplaces.json") : void 0);
16942
+ const kmFile = opts.knownMarketplacesFile ?? (configDir ? join13(configDir, "plugins", "known_marketplaces.json") : void 0);
16675
16943
  const entry = kmFile ? readJson2(kmFile)?.[marketplace] : void 0;
16676
16944
  const skip = (detail) => ({ status: "skip", detail });
16677
16945
  if (!entry) return skip(`no marketplace "${marketplace}" in ${kmFile ?? "the plugin registry"} \u2014 cannot check currency`);
16678
16946
  if (entry.source?.source !== "github") {
16679
16947
  return skip(`marketplace "${marketplace}" is a ${entry.source?.source ?? "unknown"}-source \u2014 no clone to compare`);
16680
16948
  }
16681
- if (!entry.installLocation || !existsSync7(join11(entry.installLocation, ".git"))) {
16949
+ if (!entry.installLocation || !existsSync9(join13(entry.installLocation, ".git"))) {
16682
16950
  return skip(`marketplace clone not found at ${entry.installLocation ?? "(unset installLocation)"}`);
16683
16951
  }
16684
16952
  const clone2 = entry.installLocation;
@@ -16709,7 +16977,7 @@ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@viloforge%2Fvfkb/latest";
16709
16977
  var NPM_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
16710
16978
  var NPM_CACHE_FILE = "npm-currency-cache.json";
16711
16979
  function npmCacheFilePath(opts) {
16712
- return opts.cacheFile ?? join11(opts.brainDir, ".signals", NPM_CACHE_FILE);
16980
+ return opts.cacheFile ?? join13(opts.brainDir, ".signals", NPM_CACHE_FILE);
16713
16981
  }
16714
16982
  function readNpmCache(path) {
16715
16983
  const raw = readJson2(path);
@@ -16719,8 +16987,8 @@ function readNpmCache(path) {
16719
16987
  }
16720
16988
  function writeNpmCache(path, entry) {
16721
16989
  try {
16722
- mkdirSync7(dirname4(path), { recursive: true });
16723
- writeFileSync5(path, JSON.stringify(entry));
16990
+ mkdirSync8(dirname5(path), { recursive: true });
16991
+ writeFileSync6(path, JSON.stringify(entry));
16724
16992
  } catch {
16725
16993
  }
16726
16994
  }
@@ -16802,9 +17070,9 @@ function runDoctor(opts) {
16802
17070
  const checks = [];
16803
17071
  const add = (name, status, detail) => checks.push({ name, status, detail });
16804
17072
  add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${ENGINE_COMMIT} \xB7 schema v${SCHEMA_VERSION}`);
16805
- const settings = readJson2(join11(root, ".claude", "settings.json"));
17073
+ const settings = readJson2(join13(root, ".claude", "settings.json"));
16806
17074
  const configDir = claudeConfigDir(env);
16807
- const pluginsFile = opts.pluginsFile ?? (configDir ? join11(configDir, "plugins", "installed_plugins.json") : void 0);
17075
+ const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
16808
17076
  const plugin = detectPluginWiring(settings, root, pluginsFile);
16809
17077
  const mf = readManifest(brainDir2);
16810
17078
  if (!mf) {
@@ -16825,6 +17093,41 @@ function runDoctor(opts) {
16825
17093
  add("engine drift", "warn", `brain last stamped by engine ${mf.engine_commit}, running ${ENGINE_COMMIT} \u2014 possible dual-clone drift`);
16826
17094
  }
16827
17095
  }
17096
+ {
17097
+ const js = journalStatus(brainDir2);
17098
+ if (js.suppressedInEntries > 0) {
17099
+ add(
17100
+ "journal",
17101
+ "warn",
17102
+ `${js.suppressedInEntries} suppressed (purged) pair(s) still present in entries.jsonl \u2014 a redaction is half-done: remove the line(s) from entries.jsonl too (ADR-0064 \xA74)`
17103
+ );
17104
+ } else if (js.restorable > 0) {
17105
+ add(
17106
+ "journal",
17107
+ "warn",
17108
+ `${js.restorable} journaled entr${js.restorable === 1 ? "y" : "ies"} missing from entries.jsonl \u2014 the next session-start restores them (or run \`vfkb hook session-start\` after checking why they vanished; recovery runs ONLY there)`
17109
+ );
17110
+ } else {
17111
+ add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
17112
+ }
17113
+ if (existsSync9(join13(brainDir2, ".journal"))) {
17114
+ try {
17115
+ execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
17116
+ stdio: "ignore"
17117
+ });
17118
+ } catch {
17119
+ try {
17120
+ execFileSync6("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
17121
+ add(
17122
+ "journal gitignore",
17123
+ "warn",
17124
+ `.vfkb/.journal/ exists but is NOT gitignored \u2014 add '.vfkb/.journal/' to .gitignore before the next brain commit (a committed journal defeats its purpose and the ADR-0064 \xA74 redaction)`
17125
+ );
17126
+ } catch {
17127
+ }
17128
+ }
17129
+ }
17130
+ }
16828
17131
  const home = env.VFKB_BUNDLE_DIR || env.VFKB_HOME;
16829
17132
  if (!home) {
16830
17133
  if (plugin) {
@@ -16832,7 +17135,7 @@ function runDoctor(opts) {
16832
17135
  } else {
16833
17136
  add("$VFKB_BUNDLE_DIR", "warn", "unset \u2014 set it once per machine to the vfkb bundles dir (so the wiring resolves the engine)");
16834
17137
  }
16835
- } else if (!existsSync7(join11(home, "vfkb.mjs")) || !existsSync7(join11(home, "vfkb-mcp.mjs"))) {
17138
+ } else if (!existsSync9(join13(home, "vfkb.mjs")) || !existsSync9(join13(home, "vfkb-mcp.mjs"))) {
16836
17139
  add("$VFKB_BUNDLE_DIR", "warn", `set to ${home} but vfkb.mjs / vfkb-mcp.mjs not found there (run \`npm run build:bundles\`)`);
16837
17140
  } else {
16838
17141
  add("$VFKB_BUNDLE_DIR", "ok", home);
@@ -16843,7 +17146,7 @@ function runDoctor(opts) {
16843
17146
  if (env.VFKB_HOME && !env.VFKB_BUNDLE_DIR) {
16844
17147
  add("env (deprecated)", "warn", "VFKB_HOME is a deprecated alias \u2014 rename it to VFKB_BUNDLE_DIR");
16845
17148
  }
16846
- const mcp = readJson2(join11(root, ".mcp.json"));
17149
+ const mcp = readJson2(join13(root, ".mcp.json"));
16847
17150
  const mcpProject = mcp?.mcpServers?.vfkb?.env?.VFKB_PROJECT;
16848
17151
  const mcpPresent = Boolean(mcp?.mcpServers?.vfkb);
16849
17152
  if (plugin && mcpPresent) {
@@ -16873,7 +17176,7 @@ function runDoctor(opts) {
16873
17176
  if (!plugin && have.length > 0 && hooksBlob.includes("bootstrap.mjs") && !hooksBlob.includes("CLAUDE_PROJECT_DIR")) {
16874
17177
  add("hooks anchor", "warn", "vfkb hooks use a CWD-relative bootstrap path \u2014 they break when the session cd's out of the repo root; re-run `vfkb init` to anchor them to $CLAUDE_PROJECT_DIR (issue #22)");
16875
17178
  }
16876
- if (existsSync7(join11(root, ".vfkb", "bin", "bootstrap.mjs"))) {
17179
+ if (existsSync9(join13(root, ".vfkb", "bin", "bootstrap.mjs"))) {
16877
17180
  add("bootstrap", "ok", ".vfkb/bin/bootstrap.mjs present");
16878
17181
  } else if (!plugin && (mcpPresent || have.length > 0)) {
16879
17182
  add("bootstrap", "warn", "wiring present but .vfkb/bin/bootstrap.mjs is missing \u2014 run `vfkb init`");
@@ -16913,9 +17216,9 @@ function renderDoctor(report) {
16913
17216
  }
16914
17217
 
16915
17218
  // src/import.ts
16916
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
17219
+ import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
16917
17220
  import { homedir as homedir2 } from "node:os";
16918
- import { basename as basename3, extname, join as join12 } from "node:path";
17221
+ import { basename as basename4, extname, join as join14 } from "node:path";
16919
17222
  var MYKB_FILES = {
16920
17223
  "decisions.jsonl": "decision",
16921
17224
  "facts.jsonl": "fact",
@@ -16940,16 +17243,16 @@ function mykbText(type, e) {
16940
17243
  return parts.filter(Boolean).join("\n\n");
16941
17244
  }
16942
17245
  function resolveMykbArea(nameOrDir) {
16943
- if (existsSync8(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
16944
- return join12(homedir2(), ".mykb", "areas", nameOrDir);
17246
+ if (existsSync10(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
17247
+ return join14(homedir2(), ".mykb", "areas", nameOrDir);
16945
17248
  }
16946
17249
  function fromMykb(areaDir) {
16947
- if (!existsSync8(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
17250
+ if (!existsSync10(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
16948
17251
  const out = [];
16949
17252
  for (const [file2, type] of Object.entries(MYKB_FILES)) {
16950
- const path = join12(areaDir, file2);
16951
- if (!existsSync8(path)) continue;
16952
- for (const line of readFileSync10(path, "utf8").split(/\r?\n/)) {
17253
+ const path = join14(areaDir, file2);
17254
+ if (!existsSync10(path)) continue;
17255
+ for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
16953
17256
  const trimmed = line.trim();
16954
17257
  if (!trimmed) continue;
16955
17258
  let e;
@@ -16968,24 +17271,24 @@ function fromMykb(areaDir) {
16968
17271
  }
16969
17272
  function mdTitle(path) {
16970
17273
  try {
16971
- const heading = readFileSync10(path, "utf8").split(/\r?\n/).find((l) => l.startsWith("# "));
17274
+ const heading = readFileSync12(path, "utf8").split(/\r?\n/).find((l) => l.startsWith("# "));
16972
17275
  if (heading) return heading.replace(/^#\s+/, "").trim();
16973
17276
  } catch {
16974
17277
  }
16975
- return basename3(path, extname(path));
17278
+ return basename4(path, extname(path));
16976
17279
  }
16977
17280
  function fromAdr(dir = "docs/adr") {
16978
- if (!existsSync8(dir)) throw new Error(`ADR dir not found: ${dir}`);
17281
+ if (!existsSync10(dir)) throw new Error(`ADR dir not found: ${dir}`);
16979
17282
  const out = [];
16980
17283
  for (const file2 of readdirSync3(dir).sort()) {
16981
17284
  if (extname(file2) !== ".md" || /readme/i.test(file2)) continue;
16982
- const rel = join12(dir, file2);
17285
+ const rel = join14(dir, file2);
16983
17286
  out.push(stamp("link", `${mdTitle(rel)} \u2192 ${rel}`, ["adr"], false));
16984
17287
  }
16985
17288
  return out;
16986
17289
  }
16987
17290
  function fromMarkdown(file2) {
16988
- if (!existsSync8(file2)) throw new Error(`markdown file not found: ${file2}`);
17291
+ if (!existsSync10(file2)) throw new Error(`markdown file not found: ${file2}`);
16989
17292
  return [stamp("link", `${mdTitle(file2)} \u2192 ${file2}`, ["doc"], false)];
16990
17293
  }
16991
17294
 
@@ -17064,26 +17367,26 @@ var ENTRY_TYPES = ["fact", "decision", "gotcha", "pattern", "link"];
17064
17367
  var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
17065
17368
 
17066
17369
  // src/cli.ts
17067
- import { readFileSync as readFileSync11 } from "node:fs";
17370
+ import { readFileSync as readFileSync13 } from "node:fs";
17068
17371
  import { fileURLToPath } from "node:url";
17069
- import { dirname as dirname5, join as join13 } from "node:path";
17372
+ import { dirname as dirname6, join as join15 } from "node:path";
17070
17373
  function packageVersion() {
17071
17374
  try {
17072
- const here = dirname5(fileURLToPath(import.meta.url));
17073
- const pkg = JSON.parse(readFileSync11(join13(here, "..", "package.json"), "utf8"));
17375
+ const here = dirname6(fileURLToPath(import.meta.url));
17376
+ const pkg = JSON.parse(readFileSync13(join15(here, "..", "package.json"), "utf8"));
17074
17377
  return pkg.version || ENGINE_VERSION;
17075
17378
  } catch {
17076
17379
  return ENGINE_VERSION;
17077
17380
  }
17078
17381
  }
17079
17382
  function readStdin() {
17080
- return new Promise((resolve3) => {
17383
+ return new Promise((resolve4) => {
17081
17384
  let data = "";
17082
- if (process.stdin.isTTY) return resolve3("");
17385
+ if (process.stdin.isTTY) return resolve4("");
17083
17386
  process.stdin.setEncoding("utf8");
17084
17387
  process.stdin.on("data", (c) => data += c);
17085
- process.stdin.on("end", () => resolve3(data));
17086
- setTimeout(() => resolve3(data), 2e3).unref?.();
17388
+ process.stdin.on("end", () => resolve4(data));
17389
+ setTimeout(() => resolve4(data), 2e3).unref?.();
17087
17390
  });
17088
17391
  }
17089
17392
  function flag(args, name) {
@@ -17111,7 +17414,7 @@ ${USAGE}`);
17111
17414
  throw err;
17112
17415
  }
17113
17416
  }
17114
- var USAGE = "usage: vfkb <add|list|search|query|map|context|context init|resume|resume-note|curate|distill|save|export|import|init|doctor|supersede|context-block|context-block-naive|--version|hook session-start|hook pre-tool-use|hook post-tool-use|hook stop|hook session-end>\n";
17417
+ var USAGE = "usage: vfkb <add|broadcast|journal purge|list|search|query|map|context|context init|resume|resume-note|curate|distill|save|export|import|init|doctor|supersede|context-block|context-block-naive|--version|hook session-start|hook pre-tool-use|hook post-tool-use|hook stop|hook session-end>\n";
17115
17418
  async function dispatch() {
17116
17419
  const [cmd, sub, ...rest] = process.argv.slice(2);
17117
17420
  if (cmd === "--help" || cmd === "-h" || cmd === "help") {
@@ -17160,6 +17463,43 @@ async function dispatch() {
17160
17463
  }
17161
17464
  return;
17162
17465
  }
17466
+ if (cmd === "broadcast") {
17467
+ const p = parseArgs("broadcast", argsOf(sub, rest), {
17468
+ to: "value",
17469
+ op: "value",
17470
+ tag: "value"
17471
+ });
17472
+ const text = p.positionals.join(" ").trim();
17473
+ const targets = flagList(p, "to") ?? [];
17474
+ if (!text || targets.length === 0) {
17475
+ throw new UsageError('usage: vfkb broadcast "<text>" --to <dir>[,<dir>\u2026] [--op <name>] [--tag a,b]');
17476
+ }
17477
+ try {
17478
+ const results = broadcast(text, targets, {
17479
+ op: flagValue(p, "op"),
17480
+ tags: flagList(p, "tag")
17481
+ });
17482
+ let failed = 0;
17483
+ for (const r of results) {
17484
+ if (r.ok) {
17485
+ process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${r.healed ? " (manifest healed \u2014 brain was wired but manifest-less, vfkb#193)" : ""}
17486
+ `);
17487
+ } else {
17488
+ failed++;
17489
+ process.stdout.write(`REFUSED ${r.target} ${r.reason}
17490
+ `);
17491
+ }
17492
+ }
17493
+ process.stdout.write(`
17494
+ broadcast: ${results.length - failed}/${results.length} written${failed ? ` \u2014 ${failed} refused (partial broadcast, visible by design)` : ""}
17495
+ `);
17496
+ process.exit(failed > 0 ? 1 : 0);
17497
+ } catch (err) {
17498
+ process.stderr.write(`error: ${err.message}
17499
+ `);
17500
+ process.exit(1);
17501
+ }
17502
+ }
17163
17503
  if (cmd === "export") {
17164
17504
  const p = parseArgs("export", rest, { out: "value" });
17165
17505
  if (p.positionals.length > 0) {
@@ -17465,7 +17805,18 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
17465
17805
  const project = defaultProject();
17466
17806
  const lim = flag(rest, "limit");
17467
17807
  const session = SessionState.load(effectiveSessionId(payloadId));
17468
- const additionalContext = rest.includes("--naive") ? renderNaiveDump(project, void 0, lim ? Number(lim) : void 0) : renderResume(project, session);
17808
+ let restoreNote = "";
17809
+ try {
17810
+ const rec = withExclusive(() => recoverFromJournal(brainDir()));
17811
+ if (rec.restored > 0) {
17812
+ writeMeta();
17813
+ restoreNote = `\u26A0 vfkb restored ${rec.restored} journaled entr${rec.restored === 1 ? "y" : "ies"} lost from entries.jsonl \u2014 likely a destructive git operation on uncommitted brain state (ADR-0064). Verify with kb_list and commit the brain on your next topic branch.
17814
+
17815
+ `;
17816
+ }
17817
+ } catch {
17818
+ }
17819
+ const additionalContext = restoreNote + (rest.includes("--naive") ? renderNaiveDump(project, void 0, lim ? Number(lim) : void 0) : renderResume(project, session));
17469
17820
  session.save();
17470
17821
  process.stdout.write(
17471
17822
  JSON.stringify({
@@ -17582,6 +17933,23 @@ imported ${results.length} entr${results.length === 1 ? "y" : "ies"} (role=impor
17582
17933
  process.stdout.write((r.committed ? "committed: " : "no-op: ") + r.message + "\n");
17583
17934
  return;
17584
17935
  }
17936
+ if (cmd === "journal") {
17937
+ if (sub !== "purge") {
17938
+ throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
17939
+ }
17940
+ const p = parseArgs("journal purge", rest, { id: "value", all: "boolean" });
17941
+ const id = flagValue(p, "id");
17942
+ const all = p.flags.has("all");
17943
+ if (!!id === all) {
17944
+ throw new UsageError("usage: vfkb journal purge (--id <id> | --all)");
17945
+ }
17946
+ const r = withExclusive(() => purgeJournal(brainDir(), { id, all }));
17947
+ process.stdout.write(
17948
+ r.purged > 0 ? `purged ${r.purged} journal line(s); pair(s) suppressed \u2014 recovery will never restore them (ADR-0064 \xA74). Remember: a redaction of entries.jsonl is complete only with this purge.
17949
+ ` : "no matching journal lines\n"
17950
+ );
17951
+ return;
17952
+ }
17585
17953
  process.stderr.write(USAGE);
17586
17954
  process.exit(1);
17587
17955
  }