@viloforge/vfkb 0.4.1 → 0.6.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.
@@ -7,6 +7,9 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/engine.ts
9
9
  import { randomBytes as randomBytes2 } from "node:crypto";
10
+ import { existsSync as existsSync4 } from "node:fs";
11
+ import { dirname as dirname3, join as join6, resolve as resolvePath } from "node:path";
12
+ import { fileURLToPath } from "node:url";
10
13
 
11
14
  // src/storage.ts
12
15
  import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
@@ -15496,6 +15499,16 @@ function latestCrossRepo(all = readAll(), today = nowIso().slice(0, 10), superse
15496
15499
  }
15497
15500
  return latest;
15498
15501
  }
15502
+ var CLI_FACES = ["vfkb.mjs", "cli.js", "cli.mjs"];
15503
+ function resolveCliFace(engineDir) {
15504
+ return CLI_FACES.map((f) => join6(engineDir, f)).find((p) => existsSync4(p));
15505
+ }
15506
+ function renderCaptureFallback(engineDir = dirname3(fileURLToPath(import.meta.url))) {
15507
+ const cli = resolveCliFace(engineDir);
15508
+ const target = cli ? `node "${cli}"` : `node <vfkb CLI not found beside the engine at ${engineDir} \u2014 locate vfkb.mjs or cli.js>`;
15509
+ return `capture fallback: if the kb_* tools error or disappear, the CLI still writes (same engine, separate process; journaled per ADR-0064) \u2014
15510
+ VFKB_DATA_DIR="${resolvePath(brainDir())}" ${target} add <type> "\u2026" [--tags a,b] [--why "\u2026"]`;
15511
+ }
15499
15512
  function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET_CHARS) {
15500
15513
  const all = readAll();
15501
15514
  const today = nowIso().slice(0, 10);
@@ -15534,6 +15547,7 @@ function renderContextBundle(project = defaultProject(), budget = SESSION_BUDGET
15534
15547
  `;
15535
15548
  }
15536
15549
  body += renderContextMap() + "\n\n";
15550
+ body += renderCaptureFallback() + "\n\n";
15537
15551
  const kept = [];
15538
15552
  let keptLen = 0;
15539
15553
  let dropped = 0;
@@ -15741,7 +15755,7 @@ ${bundle}`;
15741
15755
 
15742
15756
  // src/export.ts
15743
15757
  import {
15744
- existsSync as existsSync4,
15758
+ existsSync as existsSync5,
15745
15759
  mkdirSync as mkdirSync5,
15746
15760
  readdirSync as readdirSync2,
15747
15761
  readFileSync as readFileSync5,
@@ -15749,7 +15763,7 @@ import {
15749
15763
  statSync as statSync2,
15750
15764
  writeFileSync as writeFileSync3
15751
15765
  } from "node:fs";
15752
- import { dirname as dirname3, join as join6 } from "node:path";
15766
+ import { dirname as dirname4, join as join7 } from "node:path";
15753
15767
  var GENERATED_MARKER = "generated by vfkb export";
15754
15768
  var MARKER_LINE_RE = /^<!-- generated by vfkb export /m;
15755
15769
  var EXPORT_BUDGET_CHARS = 4e4;
@@ -15798,7 +15812,7 @@ function listFiles(dir) {
15798
15812
  const out = [];
15799
15813
  const walk = (d) => {
15800
15814
  for (const name of readdirSync2(d)) {
15801
- const p = join6(d, name);
15815
+ const p = join7(d, name);
15802
15816
  if (statSync2(p).isDirectory()) walk(p);
15803
15817
  else out.push(p);
15804
15818
  }
@@ -15815,7 +15829,7 @@ function isGenerated(path) {
15815
15829
  }
15816
15830
  }
15817
15831
  function prepareTree(dir) {
15818
- if (!existsSync4(dir)) {
15832
+ if (!existsSync5(dir)) {
15819
15833
  mkdirSync5(dir, { recursive: true });
15820
15834
  return;
15821
15835
  }
@@ -15831,7 +15845,7 @@ function prepareTree(dir) {
15831
15845
  const dirs = [];
15832
15846
  const collect = (d) => {
15833
15847
  for (const name of readdirSync2(d)) {
15834
- const p = join6(d, name);
15848
+ const p = join7(d, name);
15835
15849
  if (statSync2(p).isDirectory()) {
15836
15850
  collect(p);
15837
15851
  dirs.push(p);
@@ -15847,8 +15861,8 @@ function exportAgentsMd(opts = {}) {
15847
15861
  const { superseded, asOf, eligible } = projectionInputs();
15848
15862
  const project = opts.project ?? defaultProject();
15849
15863
  const budget = opts.budget ?? EXPORT_BUDGET_CHARS;
15850
- const out = opts.out ?? join6(process.cwd(), "AGENTS.md");
15851
- if (existsSync4(out) && !MARKER_LINE_RE.test(readFileSync5(out, "utf8"))) {
15864
+ const out = opts.out ?? join7(process.cwd(), "AGENTS.md");
15865
+ if (existsSync5(out) && !MARKER_LINE_RE.test(readFileSync5(out, "utf8"))) {
15852
15866
  throw new Error(
15853
15867
  `vfkb export: refusing to overwrite ${out} \u2014 it is not a previous export (no generated marker)`
15854
15868
  );
@@ -15907,7 +15921,7 @@ ${spine.trim()}
15907
15921
  }
15908
15922
  if (dropped > 0) body += `<!-- ${dropped} entries omitted for the ${budget}-char budget -->
15909
15923
  `;
15910
- mkdirSync5(dirname3(out), { recursive: true });
15924
+ mkdirSync5(dirname4(out), { recursive: true });
15911
15925
  writeFileSync3(out, head + body);
15912
15926
  return { path: out };
15913
15927
  }
@@ -16005,13 +16019,13 @@ function deriveLog(eligibleIds, records, live) {
16005
16019
  function exportOkf(opts = {}) {
16006
16020
  const { superseded, asOf, eligible } = projectionInputs();
16007
16021
  const project = opts.project ?? defaultProject();
16008
- const dir = opts.out ?? join6(process.cwd(), ".okf");
16022
+ const dir = opts.out ?? join7(process.cwd(), ".okf");
16009
16023
  prepareTree(dir);
16010
16024
  const marker = `<!-- ${GENERATED_MARKER} okf; regenerate with \`vfkb export okf\`, do not hand-edit (as-of ${asOf}) -->`;
16011
16025
  const files = [];
16012
16026
  const write = (rel, content) => {
16013
- const p = join6(dir, rel);
16014
- mkdirSync5(dirname3(p), { recursive: true });
16027
+ const p = join7(dir, rel);
16028
+ mkdirSync5(dirname4(p), { recursive: true });
16015
16029
  writeFileSync3(p, content);
16016
16030
  files.push(rel);
16017
16031
  };
@@ -16068,41 +16082,57 @@ function runExport(target, opts = {}) {
16068
16082
 
16069
16083
  // src/broadcast.ts
16070
16084
  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";
16085
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
16086
+ import { basename as basename2, join as join9, resolve as resolve2 } from "node:path";
16073
16087
 
16074
16088
  // 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";
16089
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
16090
+ import { dirname as dirname5, join as join8 } from "node:path";
16077
16091
 
16078
16092
  // src/version.ts
16079
16093
  var SCHEMA_VERSION = 1;
16080
- var ENGINE_VERSION = true ? "0.4.1" : ownPackageVersion();
16081
- var ENGINE_COMMIT = true ? "66ee821" : "dev";
16094
+ var ENGINE_VERSION = true ? "0.6.0" : ownPackageVersion();
16095
+ var ENGINE_COMMIT = true ? "9921586" : "dev";
16082
16096
 
16083
16097
  // src/manifest.ts
16084
16098
  function manifestPath(brainDir2) {
16085
- return join7(brainDir2, "manifest.json");
16099
+ return join8(brainDir2, "manifest.json");
16086
16100
  }
16087
- function currentManifest() {
16088
- return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
16101
+ function isUnknownCommit(commit) {
16102
+ return !commit || commit === "dev";
16103
+ }
16104
+ function currentManifest(opts = {}) {
16105
+ return {
16106
+ schema_version: SCHEMA_VERSION,
16107
+ engine_version: opts.engineVersion ?? ENGINE_VERSION,
16108
+ engine_commit: opts.engineCommit ?? ENGINE_COMMIT
16109
+ };
16089
16110
  }
16090
16111
  function readManifest(brainDir2) {
16091
16112
  const p = manifestPath(brainDir2);
16092
- if (!existsSync5(p)) return void 0;
16113
+ if (!existsSync6(p)) return void 0;
16093
16114
  try {
16094
16115
  return JSON.parse(readFileSync6(p, "utf8"));
16095
16116
  } catch {
16096
16117
  return void 0;
16097
16118
  }
16098
16119
  }
16099
- function writeManifest(brainDir2) {
16120
+ function manifestNeedsStamp(brainDir2, engineCommit = ENGINE_COMMIT) {
16121
+ const cur = readManifest(brainDir2);
16122
+ if (!cur) return true;
16123
+ return isUnknownCommit(cur.engine_commit) && !isUnknownCommit(engineCommit);
16124
+ }
16125
+ function writeManifest(brainDir2, opts = {}) {
16100
16126
  const p = manifestPath(brainDir2);
16101
- const existed = existsSync5(p);
16127
+ const existed = existsSync6(p);
16102
16128
  const cur = readManifest(brainDir2);
16103
- const next = currentManifest();
16129
+ const next = currentManifest(opts);
16130
+ if (cur && isUnknownCommit(next.engine_commit) && !isUnknownCommit(cur.engine_commit)) {
16131
+ next.engine_commit = cur.engine_commit;
16132
+ next.engine_version = cur.engine_version;
16133
+ }
16104
16134
  if (cur && JSON.stringify(cur) === JSON.stringify(next)) return "skipped";
16105
- mkdirSync6(dirname4(p), { recursive: true });
16135
+ mkdirSync6(dirname5(p), { recursive: true });
16106
16136
  writeFileSync4(p, JSON.stringify(next, null, 2) + "\n");
16107
16137
  return existed ? "updated" : "created";
16108
16138
  }
@@ -16111,7 +16141,7 @@ function writeManifest(brainDir2) {
16111
16141
  var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["handoff", "next"]);
16112
16142
  function targetBrainDir(target) {
16113
16143
  const abs = resolve2(target);
16114
- return basename2(abs) === ".vfkb" ? abs : join8(abs, ".vfkb");
16144
+ return basename2(abs) === ".vfkb" ? abs : join9(abs, ".vfkb");
16115
16145
  }
16116
16146
  function gitPosture(repoDir) {
16117
16147
  const git2 = (...a) => execFileSync2("git", ["-C", repoDir, ...a], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -16152,20 +16182,24 @@ function broadcast(text, targets, opts = {}) {
16152
16182
  continue;
16153
16183
  }
16154
16184
  const repoDir = resolve2(brain, "..");
16155
- const manifestPath2 = join8(brain, "manifest.json");
16185
+ const manifestPath2 = join9(brain, "manifest.json");
16156
16186
  let healed = false;
16157
- if (!existsSync6(manifestPath2)) {
16158
- if (!existsSync6(join8(brain, "entries.jsonl"))) {
16187
+ let upgraded = false;
16188
+ const needsStamp = manifestNeedsStamp(brain, opts.engineCommit ?? ENGINE_COMMIT);
16189
+ if (needsStamp) {
16190
+ if (!existsSync7(join9(brain, "entries.jsonl"))) {
16159
16191
  results.push({ target, ok: false, reason: `no brain (no entries.jsonl in ${brain}) \u2014 never bootstrap a wire-less brain (ADR-0063 \xA73)` });
16160
16192
  continue;
16161
16193
  }
16194
+ const absent = !existsSync7(manifestPath2);
16162
16195
  try {
16163
- writeManifest(brain);
16196
+ writeManifest(brain, { engineCommit: opts.engineCommit, engineVersion: opts.engineVersion });
16164
16197
  } catch (err) {
16165
- results.push({ target, ok: false, reason: `manifest heal failed: ${err.message}` });
16198
+ results.push({ target, ok: false, reason: `manifest ${absent ? "heal" : "provenance upgrade"} failed: ${err.message}` });
16166
16199
  continue;
16167
16200
  }
16168
- healed = true;
16201
+ if (absent) healed = true;
16202
+ else upgraded = true;
16169
16203
  }
16170
16204
  let schema;
16171
16205
  try {
@@ -16183,9 +16217,9 @@ function broadcast(text, targets, opts = {}) {
16183
16217
  process.env.VFKB_DATA_DIR = brain;
16184
16218
  const e = addEntry("fact", stamped, { role: "executor", tags: [.../* @__PURE__ */ new Set(["cross-repo", ...extraTags])] });
16185
16219
  written.add(brain);
16186
- results.push({ target, ok: true, id: e.id, posture: gitPosture(repoDir), ...healed ? { healed } : {} });
16220
+ results.push({ target, ok: true, id: e.id, posture: gitPosture(repoDir), ...healed ? { healed } : {}, ...upgraded ? { upgraded } : {} });
16187
16221
  } catch (err) {
16188
- results.push({ target, ok: false, reason: err.message, ...healed ? { healed } : {} });
16222
+ results.push({ target, ok: false, reason: err.message, ...healed ? { healed } : {}, ...upgraded ? { upgraded } : {} });
16189
16223
  } finally {
16190
16224
  if (prev === void 0) delete process.env.VFKB_DATA_DIR;
16191
16225
  else process.env.VFKB_DATA_DIR = prev;
@@ -16419,7 +16453,7 @@ var GATING_REASON = "vfkb: edit the brain via the engine/CLI/MCP, not by writing
16419
16453
  // src/stop-reminder.ts
16420
16454
  import { execFileSync as execFileSync3 } from "node:child_process";
16421
16455
  import { readFileSync as readFileSync8 } from "node:fs";
16422
- import { join as join9, relative as relative2 } from "node:path";
16456
+ import { join as join10, relative as relative2 } from "node:path";
16423
16457
  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.';
16424
16458
  var HANDOFF_MIN_ENTRIES = 3;
16425
16459
  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.';
@@ -16448,7 +16482,7 @@ function hasUncommittedWork(cwd = process.cwd(), brain = brainDir()) {
16448
16482
  });
16449
16483
  }
16450
16484
  function newBrainEntriesSinceHead(brain = brainDir(), cwd = process.cwd()) {
16451
- const file2 = join9(brain, "entries.jsonl");
16485
+ const file2 = join10(brain, "entries.jsonl");
16452
16486
  let current2;
16453
16487
  try {
16454
16488
  current2 = readFileSync8(file2, "utf8").split("\n").filter(Boolean);
@@ -16484,13 +16518,13 @@ function gatherStopContext(cwd = process.cwd(), brain = brainDir()) {
16484
16518
 
16485
16519
  // src/git.ts
16486
16520
  import { execFileSync as execFileSync4 } from "node:child_process";
16487
- import { existsSync as existsSync7 } from "node:fs";
16488
- import { join as join10 } from "node:path";
16521
+ import { existsSync as existsSync8 } from "node:fs";
16522
+ import { join as join11 } from "node:path";
16489
16523
  function git(args, cwd) {
16490
16524
  return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16491
16525
  }
16492
16526
  function ensureRepo(brain) {
16493
- if (!existsSync7(join10(brain, ".git"))) {
16527
+ if (!existsSync8(join11(brain, ".git"))) {
16494
16528
  git(["init", "-q"], brain);
16495
16529
  }
16496
16530
  }
@@ -16518,10 +16552,10 @@ function save(message = "vfkb: update", role = "engine", brain = brainDir()) {
16518
16552
  // src/session-end.ts
16519
16553
  import { execFileSync as execFileSync5 } from "node:child_process";
16520
16554
  import { readFileSync as readFileSync9 } from "node:fs";
16521
- import { join as join11, isAbsolute } from "node:path";
16555
+ import { join as join12, isAbsolute } from "node:path";
16522
16556
  var realGit = (args, cwd) => execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
16523
16557
  function brainEntriesRelPath(dataDir2) {
16524
- return join11(dataDir2, "entries.jsonl").replace(/\\/g, "/");
16558
+ return join12(dataDir2, "entries.jsonl").replace(/\\/g, "/");
16525
16559
  }
16526
16560
  function tryGit(git2, args, cwd) {
16527
16561
  try {
@@ -16622,8 +16656,8 @@ function runSessionEnd(opts = {}) {
16622
16656
  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).`
16623
16657
  };
16624
16658
  }
16625
- const absBrain = isAbsolute(dataDir2) ? dataDir2 : join11(cwd, dataDir2);
16626
- const fresh = newEntriesSinceHead(git2, cwd, entries, join11(absBrain, "entries.jsonl"));
16659
+ const absBrain = isAbsolute(dataDir2) ? dataDir2 : join12(cwd, dataDir2);
16660
+ const fresh = newEntriesSinceHead(git2, cwd, entries, join12(absBrain, "entries.jsonl"));
16627
16661
  let autoHandoff = false;
16628
16662
  if (fresh.length > 0 && !fresh.some(isHandoff2)) {
16629
16663
  try {
@@ -16643,8 +16677,8 @@ function runSessionEnd(opts = {}) {
16643
16677
  }
16644
16678
 
16645
16679
  // 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";
16680
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
16681
+ import { basename as basename3, join as join13 } from "node:path";
16648
16682
  var AGENTS_MARKER = "<!-- vfkb:how-we-track-work -->";
16649
16683
  var BOOTSTRAP_REL = ".vfkb/bin/bootstrap.mjs";
16650
16684
  function mcpConfig(project) {
@@ -16720,8 +16754,9 @@ This repo uses **vfkb** as its knowledge substrate (project \`${project}\`). Kno
16720
16754
  - **Record knowledge** with the \`mcp__vfkb__kb_add\` tool (or \`node .vfkb/bin/bootstrap.mjs cli add \u2026\`):
16721
16755
  \`decision\`, \`fact\`, \`gotcha\`, \`pattern\`, \`link\` \u2014 put a decision's rationale in its text.
16722
16756
  **Capture load-bearing decisions immediately \u2014 don't defer.**
16723
- - Only \`.vfkb/entries.jsonl\`, \`.vfkb/manifest.json\`, and \`.vfkb/bin/\` are committed;
16724
- \`.vfkb/index-meta.json\`, \`.sessions/\`, \`.signals/\` are derived/gitignored.
16757
+ - Committed: \`.vfkb/entries.jsonl\`, \`.vfkb/manifest.json\` (the ADR-0030 engine stamp \u2014
16758
+ **never gitignore \`manifest.json\`**), and \`.vfkb/bin/\`. Derived/gitignored, all five:
16759
+ \`.vfkb/index-meta.json\`, \`.vfkb/.sessions/\`, \`.vfkb/.signals/\`, \`.vfkb/.journal/\`, \`.vfkb/.lock\`.
16725
16760
 
16726
16761
  Two env vars: **\`VFKB_DATA_DIR\`** = this repo's brain (\`.vfkb\`, set by the wiring) \xB7 **\`VFKB_BUNDLE_DIR\`**
16727
16762
  = the shared vfkb engine bundles \u2014 set it once per machine, e.g. \`export VFKB_BUNDLE_DIR=/path/to/vfkb/dist/bundles\`.
@@ -16729,7 +16764,7 @@ If it is unset, a session-start banner tells you; run \`vfkb doctor\` to check.
16729
16764
  `;
16730
16765
  }
16731
16766
  function readJson(path) {
16732
- if (!existsSync8(path)) return void 0;
16767
+ if (!existsSync9(path)) return void 0;
16733
16768
  try {
16734
16769
  return JSON.parse(readFileSync10(path, "utf8"));
16735
16770
  } catch {
@@ -16745,9 +16780,9 @@ function eventHasVfkb(arr2) {
16745
16780
  function initProject(root, opts = {}) {
16746
16781
  const project = opts.project || basename3(root) || "project";
16747
16782
  const changes = [];
16748
- const brainDir2 = join12(root, ".vfkb");
16749
- const entries = join12(brainDir2, "entries.jsonl");
16750
- if (!existsSync8(entries)) {
16783
+ const brainDir2 = join13(root, ".vfkb");
16784
+ const entries = join13(brainDir2, "entries.jsonl");
16785
+ if (!existsSync9(entries)) {
16751
16786
  mkdirSync7(brainDir2, { recursive: true });
16752
16787
  writeFileSync5(entries, "");
16753
16788
  changes.push({ path: ".vfkb/entries.jsonl", action: "created" });
@@ -16756,9 +16791,9 @@ function initProject(root, opts = {}) {
16756
16791
  }
16757
16792
  changes.push({ path: ".vfkb/manifest.json", action: writeManifest(brainDir2) });
16758
16793
  {
16759
- const binDir = join12(brainDir2, "bin");
16760
- const path = join12(binDir, "bootstrap.mjs");
16761
- const existed = existsSync8(path);
16794
+ const binDir = join13(brainDir2, "bin");
16795
+ const path = join13(binDir, "bootstrap.mjs");
16796
+ const existed = existsSync9(path);
16762
16797
  const same = existed && readFileSync10(path, "utf8") === BOOTSTRAP_SRC;
16763
16798
  if (same) {
16764
16799
  changes.push({ path: BOOTSTRAP_REL, action: "skipped" });
@@ -16769,8 +16804,8 @@ function initProject(root, opts = {}) {
16769
16804
  }
16770
16805
  }
16771
16806
  {
16772
- const path = join12(root, ".mcp.json");
16773
- const existed = existsSync8(path);
16807
+ const path = join13(root, ".mcp.json");
16808
+ const existed = existsSync9(path);
16774
16809
  const cfg = readJson(path) ?? {};
16775
16810
  cfg.mcpServers = cfg.mcpServers ?? {};
16776
16811
  const desired = mcpConfig(project);
@@ -16784,9 +16819,9 @@ function initProject(root, opts = {}) {
16784
16819
  }
16785
16820
  }
16786
16821
  {
16787
- const dir = join12(root, ".claude");
16788
- const path = join12(dir, "settings.json");
16789
- const existed = existsSync8(path);
16822
+ const dir = join13(root, ".claude");
16823
+ const path = join13(dir, "settings.json");
16824
+ const existed = existsSync9(path);
16790
16825
  const cfg = readJson(path) ?? {};
16791
16826
  cfg.hooks = cfg.hooks ?? {};
16792
16827
  const want = settingsHooks(project);
@@ -16809,16 +16844,38 @@ function initProject(root, opts = {}) {
16809
16844
  }
16810
16845
  }
16811
16846
  {
16812
- const path = join12(root, ".gitignore");
16813
- const lines = [".vfkb/index-meta.json", ".vfkb/.sessions/", ".vfkb/.signals/", ".vfkb/.journal/"];
16814
- const existed = existsSync8(path);
16847
+ const path = join13(root, ".gitignore");
16848
+ const lines = [
16849
+ ".vfkb/index-meta.json",
16850
+ ".vfkb/.sessions/",
16851
+ ".vfkb/.signals/",
16852
+ ".vfkb/.journal/",
16853
+ ".vfkb/.lock",
16854
+ ".vfkb/.write-probe-*"
16855
+ ];
16856
+ const HEADER = [
16857
+ "# vfkb \u2014 entries.jsonl + manifest.json (the ADR-0030 engine stamp) are committed;",
16858
+ "# the vfkb paths below are derived/operational and stay out of git"
16859
+ ];
16860
+ const STALE_HEADER = /^#\s*vfkb\s*—\s*derived\/operational \(only \.vfkb\/entries\.jsonl is committed\)\s*$/;
16861
+ const existed = existsSync9(path);
16815
16862
  const cur = existed ? readFileSync10(path, "utf8") : "";
16816
- const missing = lines.filter((l) => !cur.split(/\r?\n/).includes(l));
16817
- if (missing.length === 0) {
16863
+ const curLines = cur.split(/\r?\n/);
16864
+ const missing = lines.filter((l) => !curLines.includes(l));
16865
+ const staleAt = curLines.findIndex((l) => STALE_HEADER.test(l));
16866
+ if (missing.length === 0 && staleAt === -1) {
16818
16867
  changes.push({ path: ".gitignore", action: "skipped" });
16868
+ } else if (staleAt !== -1) {
16869
+ const out = [...curLines];
16870
+ out.splice(staleAt, 1, ...HEADER);
16871
+ let last = staleAt + HEADER.length - 1;
16872
+ for (let i = last + 1; i < out.length && /^\.vfkb\//.test(out[i].trim()); i++) last = i;
16873
+ out.splice(last + 1, 0, ...missing);
16874
+ writeFileSync5(path, out.join("\n"));
16875
+ changes.push({ path: ".gitignore", action: "updated" });
16819
16876
  } else {
16820
16877
  const prefix = cur && !cur.endsWith("\n") ? "\n" : "";
16821
- const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 derived/operational (only .vfkb/entries.jsonl is committed)
16878
+ const block = `${prefix}${cur ? "\n" : ""}${HEADER.join("\n")}
16822
16879
  ${missing.join("\n")}
16823
16880
  `;
16824
16881
  writeFileSync5(path, cur + block);
@@ -16826,9 +16883,9 @@ ${missing.join("\n")}
16826
16883
  }
16827
16884
  }
16828
16885
  {
16829
- const path = join12(root, ".gitattributes");
16886
+ const path = join13(root, ".gitattributes");
16830
16887
  const line = ".vfkb/entries.jsonl merge=union";
16831
- const existed = existsSync8(path);
16888
+ const existed = existsSync9(path);
16832
16889
  const cur = existed ? readFileSync10(path, "utf8") : "";
16833
16890
  if (cur.split(/\r?\n/).includes(line)) {
16834
16891
  changes.push({ path: ".gitattributes", action: "skipped" });
@@ -16842,8 +16899,8 @@ ${line}
16842
16899
  }
16843
16900
  }
16844
16901
  {
16845
- const path = join12(root, "AGENTS.md");
16846
- const existed = existsSync8(path);
16902
+ const path = join13(root, "AGENTS.md");
16903
+ const existed = existsSync9(path);
16847
16904
  const cur = existed ? readFileSync10(path, "utf8") : "";
16848
16905
  if (cur.includes(AGENTS_MARKER)) {
16849
16906
  changes.push({ path: "AGENTS.md", action: "skipped" });
@@ -16869,10 +16926,11 @@ function approvalNotice(project) {
16869
16926
 
16870
16927
  // src/doctor.ts
16871
16928
  import { execFileSync as execFileSync6 } from "node:child_process";
16872
- import { existsSync as existsSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, realpathSync } from "node:fs";
16873
- import { join as join13, dirname as dirname5, relative as relative3, resolve as resolve4, isAbsolute as isAbsolute2 } from "node:path";
16929
+ import { randomBytes as randomBytes3 } from "node:crypto";
16930
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, realpathSync, unlinkSync as unlinkSync2 } from "node:fs";
16931
+ import { join as join14, dirname as dirname6, relative as relative3, resolve as resolve4, isAbsolute as isAbsolute2 } from "node:path";
16874
16932
  function readJson2(path) {
16875
- if (!existsSync9(path)) return void 0;
16933
+ if (!existsSync10(path)) return void 0;
16876
16934
  try {
16877
16935
  return JSON.parse(readFileSync11(path, "utf8"));
16878
16936
  } catch {
@@ -16907,16 +16965,16 @@ function detectPluginWiring(settings, root, pluginsFile) {
16907
16965
  }
16908
16966
  function claudeConfigDir(env) {
16909
16967
  if (env.CLAUDE_CONFIG_DIR) return env.CLAUDE_CONFIG_DIR;
16910
- return env.HOME ? join13(env.HOME, ".claude") : void 0;
16968
+ return env.HOME ? join14(env.HOME, ".claude") : void 0;
16911
16969
  }
16912
16970
  function localHeadSha(cloneDir) {
16913
- const head = readFileMaybe(join13(cloneDir, ".git", "HEAD"));
16971
+ const head = readFileMaybe(join14(cloneDir, ".git", "HEAD"));
16914
16972
  if (!head) return void 0;
16915
16973
  const ref = head.trim().match(/^ref:\s*(\S+)$/)?.[1];
16916
16974
  if (!ref) return /^[0-9a-f]{40}$/.test(head.trim()) ? head.trim() : void 0;
16917
- const loose = readFileMaybe(join13(cloneDir, ".git", ...ref.split("/")));
16975
+ const loose = readFileMaybe(join14(cloneDir, ".git", ...ref.split("/")));
16918
16976
  if (loose) return loose.trim();
16919
- const packed = readFileMaybe(join13(cloneDir, ".git", "packed-refs"));
16977
+ const packed = readFileMaybe(join14(cloneDir, ".git", "packed-refs"));
16920
16978
  for (const line of packed?.split("\n") ?? []) {
16921
16979
  const [sha, name] = line.trim().split(/\s+/);
16922
16980
  if (name === ref) return sha;
@@ -16939,14 +16997,14 @@ var realGit2 = (args, cwd) => execFileSync6("git", args, {
16939
16997
  });
16940
16998
  function checkCurrency(plugin, opts, configDir) {
16941
16999
  const marketplace = plugin.key.split("@")[1];
16942
- const kmFile = opts.knownMarketplacesFile ?? (configDir ? join13(configDir, "plugins", "known_marketplaces.json") : void 0);
17000
+ const kmFile = opts.knownMarketplacesFile ?? (configDir ? join14(configDir, "plugins", "known_marketplaces.json") : void 0);
16943
17001
  const entry = kmFile ? readJson2(kmFile)?.[marketplace] : void 0;
16944
17002
  const skip = (detail) => ({ status: "skip", detail });
16945
17003
  if (!entry) return skip(`no marketplace "${marketplace}" in ${kmFile ?? "the plugin registry"} \u2014 cannot check currency`);
16946
17004
  if (entry.source?.source !== "github") {
16947
17005
  return skip(`marketplace "${marketplace}" is a ${entry.source?.source ?? "unknown"}-source \u2014 no clone to compare`);
16948
17006
  }
16949
- if (!entry.installLocation || !existsSync9(join13(entry.installLocation, ".git"))) {
17007
+ if (!entry.installLocation || !existsSync10(join14(entry.installLocation, ".git"))) {
16950
17008
  return skip(`marketplace clone not found at ${entry.installLocation ?? "(unset installLocation)"}`);
16951
17009
  }
16952
17010
  const clone2 = entry.installLocation;
@@ -16977,7 +17035,7 @@ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@viloforge%2Fvfkb/latest";
16977
17035
  var NPM_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
16978
17036
  var NPM_CACHE_FILE = "npm-currency-cache.json";
16979
17037
  function npmCacheFilePath(opts) {
16980
- return opts.cacheFile ?? join13(opts.brainDir, ".signals", NPM_CACHE_FILE);
17038
+ return opts.cacheFile ?? join14(opts.brainDir, ".signals", NPM_CACHE_FILE);
16981
17039
  }
16982
17040
  function readNpmCache(path) {
16983
17041
  const raw = readJson2(path);
@@ -16987,7 +17045,7 @@ function readNpmCache(path) {
16987
17045
  }
16988
17046
  function writeNpmCache(path, entry) {
16989
17047
  try {
16990
- mkdirSync8(dirname5(path), { recursive: true });
17048
+ mkdirSync8(dirname6(path), { recursive: true });
16991
17049
  writeFileSync6(path, JSON.stringify(entry));
16992
17050
  } catch {
16993
17051
  }
@@ -17104,9 +17162,9 @@ function runDoctor(opts) {
17104
17162
  const checks = [];
17105
17163
  const add = (name, status, detail) => checks.push({ name, status, detail });
17106
17164
  add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${runningCommit} \xB7 schema v${SCHEMA_VERSION}`);
17107
- const settings = readJson2(join13(root, ".claude", "settings.json"));
17165
+ const settings = readJson2(join14(root, ".claude", "settings.json"));
17108
17166
  const configDir = claudeConfigDir(env);
17109
- const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
17167
+ const pluginsFile = opts.pluginsFile ?? (configDir ? join14(configDir, "plugins", "installed_plugins.json") : void 0);
17110
17168
  const plugin = detectPluginWiring(settings, root, pluginsFile);
17111
17169
  const mf = readManifest(brainDir2);
17112
17170
  if (!mf) {
@@ -17137,6 +17195,29 @@ function runDoctor(opts) {
17137
17195
  add("engine drift", "warn", `brain last stamped by engine ${mf.engine_commit}, running ${runningCommit} \u2014 possible dual-clone drift`);
17138
17196
  }
17139
17197
  }
17198
+ {
17199
+ const probe = join14(brainDir2, `.write-probe-${process.pid}-${randomBytes3(4).toString("hex")}`);
17200
+ const payload = `vfkb write-probe ${process.pid} ${Date.now()}`;
17201
+ let failure;
17202
+ try {
17203
+ mkdirSync8(brainDir2, { recursive: true });
17204
+ writeFileSync6(probe, payload);
17205
+ if (readFileSync11(probe, "utf8") !== payload) failure = "wrote the probe file but read back different bytes";
17206
+ } catch (e) {
17207
+ failure = e.message;
17208
+ } finally {
17209
+ try {
17210
+ if (existsSync10(probe)) unlinkSync2(probe);
17211
+ } catch {
17212
+ }
17213
+ }
17214
+ const SCOPE = " \u2014 scope: this is the CLI/engine/filesystem path only; it does NOT check the MCP server, so it cannot tell you whether kb_* capture is reaching the brain (ADR-0065 \xA70: a hung MCP server loses a write with no error at all)";
17215
+ if (failure) {
17216
+ add("write-health (filesystem)", "fail", `cannot write to ${brainDir2}: ${failure}${SCOPE}`);
17217
+ } else {
17218
+ add("write-health (filesystem)", "ok", `round-trip verified in ${brainDir2}${SCOPE}`);
17219
+ }
17220
+ }
17140
17221
  {
17141
17222
  const js = journalStatus(brainDir2);
17142
17223
  if (js.suppressedInEntries > 0) {
@@ -17154,9 +17235,9 @@ function runDoctor(opts) {
17154
17235
  } else {
17155
17236
  add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
17156
17237
  }
17157
- if (existsSync9(join13(brainDir2, ".journal")) && isUnder(repoToplevel(root), join13(brainDir2, ".journal"))) {
17238
+ if (existsSync10(join14(brainDir2, ".journal")) && isUnder(repoToplevel(root), join14(brainDir2, ".journal"))) {
17158
17239
  try {
17159
- execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
17240
+ execFileSync6("git", ["-C", root, "check-ignore", "-q", join14(brainDir2, ".journal", "wal.jsonl")], {
17160
17241
  stdio: "ignore"
17161
17242
  });
17162
17243
  } catch {
@@ -17179,7 +17260,7 @@ function runDoctor(opts) {
17179
17260
  } else {
17180
17261
  add("$VFKB_BUNDLE_DIR", "warn", "unset \u2014 set it once per machine to the vfkb bundles dir (so the wiring resolves the engine)");
17181
17262
  }
17182
- } else if (!existsSync9(join13(home, "vfkb.mjs")) || !existsSync9(join13(home, "vfkb-mcp.mjs"))) {
17263
+ } else if (!existsSync10(join14(home, "vfkb.mjs")) || !existsSync10(join14(home, "vfkb-mcp.mjs"))) {
17183
17264
  add("$VFKB_BUNDLE_DIR", "warn", `set to ${home} but vfkb.mjs / vfkb-mcp.mjs not found there (run \`npm run build:bundles\`)`);
17184
17265
  } else {
17185
17266
  add("$VFKB_BUNDLE_DIR", "ok", home);
@@ -17190,7 +17271,7 @@ function runDoctor(opts) {
17190
17271
  if (env.VFKB_HOME && !env.VFKB_BUNDLE_DIR) {
17191
17272
  add("env (deprecated)", "warn", "VFKB_HOME is a deprecated alias \u2014 rename it to VFKB_BUNDLE_DIR");
17192
17273
  }
17193
- const mcp = readJson2(join13(root, ".mcp.json"));
17274
+ const mcp = readJson2(join14(root, ".mcp.json"));
17194
17275
  const mcpProject = mcp?.mcpServers?.vfkb?.env?.VFKB_PROJECT;
17195
17276
  const mcpPresent = Boolean(mcp?.mcpServers?.vfkb);
17196
17277
  if (plugin && mcpPresent) {
@@ -17220,7 +17301,7 @@ function runDoctor(opts) {
17220
17301
  if (!plugin && have.length > 0 && hooksBlob.includes("bootstrap.mjs") && !hooksBlob.includes("CLAUDE_PROJECT_DIR")) {
17221
17302
  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)");
17222
17303
  }
17223
- if (existsSync9(join13(root, ".vfkb", "bin", "bootstrap.mjs"))) {
17304
+ if (existsSync10(join14(root, ".vfkb", "bin", "bootstrap.mjs"))) {
17224
17305
  add("bootstrap", "ok", ".vfkb/bin/bootstrap.mjs present");
17225
17306
  } else if (!plugin && (mcpPresent || have.length > 0)) {
17226
17307
  add("bootstrap", "warn", "wiring present but .vfkb/bin/bootstrap.mjs is missing \u2014 run `vfkb init`");
@@ -17260,9 +17341,9 @@ function renderDoctor(report) {
17260
17341
  }
17261
17342
 
17262
17343
  // src/import.ts
17263
- import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
17344
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
17264
17345
  import { homedir as homedir2 } from "node:os";
17265
- import { basename as basename4, extname, join as join14 } from "node:path";
17346
+ import { basename as basename4, extname, join as join15 } from "node:path";
17266
17347
  var MYKB_FILES = {
17267
17348
  "decisions.jsonl": "decision",
17268
17349
  "facts.jsonl": "fact",
@@ -17287,15 +17368,15 @@ function mykbText(type, e) {
17287
17368
  return parts.filter(Boolean).join("\n\n");
17288
17369
  }
17289
17370
  function resolveMykbArea(nameOrDir) {
17290
- if (existsSync10(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
17291
- return join14(homedir2(), ".mykb", "areas", nameOrDir);
17371
+ if (existsSync11(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
17372
+ return join15(homedir2(), ".mykb", "areas", nameOrDir);
17292
17373
  }
17293
17374
  function fromMykb(areaDir) {
17294
- if (!existsSync10(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
17375
+ if (!existsSync11(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
17295
17376
  const out = [];
17296
17377
  for (const [file2, type] of Object.entries(MYKB_FILES)) {
17297
- const path = join14(areaDir, file2);
17298
- if (!existsSync10(path)) continue;
17378
+ const path = join15(areaDir, file2);
17379
+ if (!existsSync11(path)) continue;
17299
17380
  for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
17300
17381
  const trimmed = line.trim();
17301
17382
  if (!trimmed) continue;
@@ -17322,17 +17403,17 @@ function mdTitle(path) {
17322
17403
  return basename4(path, extname(path));
17323
17404
  }
17324
17405
  function fromAdr(dir = "docs/adr") {
17325
- if (!existsSync10(dir)) throw new Error(`ADR dir not found: ${dir}`);
17406
+ if (!existsSync11(dir)) throw new Error(`ADR dir not found: ${dir}`);
17326
17407
  const out = [];
17327
17408
  for (const file2 of readdirSync3(dir).sort()) {
17328
17409
  if (extname(file2) !== ".md" || /readme/i.test(file2)) continue;
17329
- const rel = join14(dir, file2);
17410
+ const rel = join15(dir, file2);
17330
17411
  out.push(stamp("link", `${mdTitle(rel)} \u2192 ${rel}`, ["adr"], false));
17331
17412
  }
17332
17413
  return out;
17333
17414
  }
17334
17415
  function fromMarkdown(file2) {
17335
- if (!existsSync10(file2)) throw new Error(`markdown file not found: ${file2}`);
17416
+ if (!existsSync11(file2)) throw new Error(`markdown file not found: ${file2}`);
17336
17417
  return [stamp("link", `${mdTitle(file2)} \u2192 ${file2}`, ["doc"], false)];
17337
17418
  }
17338
17419
 
@@ -17412,25 +17493,35 @@ var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
17412
17493
 
17413
17494
  // src/cli.ts
17414
17495
  import { readFileSync as readFileSync13 } from "node:fs";
17415
- import { fileURLToPath } from "node:url";
17416
- import { dirname as dirname6, join as join15 } from "node:path";
17496
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
17497
+ import { dirname as dirname7, join as join16 } from "node:path";
17417
17498
  function packageVersion() {
17418
17499
  try {
17419
- const here = dirname6(fileURLToPath(import.meta.url));
17420
- const pkg = JSON.parse(readFileSync13(join15(here, "..", "package.json"), "utf8"));
17500
+ const here = dirname7(fileURLToPath2(import.meta.url));
17501
+ const pkg = JSON.parse(readFileSync13(join16(here, "..", "package.json"), "utf8"));
17421
17502
  return pkg.version || ENGINE_VERSION;
17422
17503
  } catch {
17423
17504
  return ENGINE_VERSION;
17424
17505
  }
17425
17506
  }
17507
+ var STDIN_WATCHDOG_MS = 2e3;
17426
17508
  function readStdin() {
17427
17509
  return new Promise((resolve5) => {
17428
17510
  let data = "";
17429
17511
  if (process.stdin.isTTY) return resolve5("");
17512
+ let settled = false;
17513
+ const finish = () => {
17514
+ if (settled) return;
17515
+ settled = true;
17516
+ process.stdin.pause();
17517
+ process.stdin.unref?.();
17518
+ resolve5(data);
17519
+ };
17430
17520
  process.stdin.setEncoding("utf8");
17431
17521
  process.stdin.on("data", (c) => data += c);
17432
- process.stdin.on("end", () => resolve5(data));
17433
- setTimeout(() => resolve5(data), 2e3).unref?.();
17522
+ process.stdin.on("end", finish);
17523
+ process.stdin.on("error", finish);
17524
+ setTimeout(finish, STDIN_WATCHDOG_MS).unref?.();
17434
17525
  });
17435
17526
  }
17436
17527
  function flag(args, name) {
@@ -17526,7 +17617,8 @@ async function dispatch() {
17526
17617
  let failed = 0;
17527
17618
  for (const r of results) {
17528
17619
  if (r.ok) {
17529
- process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${r.healed ? " (manifest healed \u2014 brain was wired but manifest-less, vfkb#193)" : ""}
17620
+ const repair = r.healed ? " (manifest healed \u2014 brain was wired but manifest-less, vfkb#193)" : r.upgraded ? ' (manifest provenance upgraded \u2014 engine identity was unknown ("dev"), now the running sha, vfkb#212)' : "";
17621
+ process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${repair}
17530
17622
  `);
17531
17623
  } else {
17532
17624
  failed++;