@viloforge/vfkb 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.5.0" : ownPackageVersion();
16095
+ var ENGINE_COMMIT = true ? "aeaa5fa" : "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");
16100
+ }
16101
+ function isUnknownCommit(commit) {
16102
+ return !commit || commit === "dev";
16086
16103
  }
16087
- function currentManifest() {
16088
- return { schema_version: SCHEMA_VERSION, engine_version: ENGINE_VERSION, engine_commit: ENGINE_COMMIT };
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,37 @@ 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
+ ];
16855
+ const HEADER = [
16856
+ "# vfkb \u2014 entries.jsonl + manifest.json (the ADR-0030 engine stamp) are committed;",
16857
+ "# the vfkb paths below are derived/operational and stay out of git"
16858
+ ];
16859
+ const STALE_HEADER = /^#\s*vfkb\s*—\s*derived\/operational \(only \.vfkb\/entries\.jsonl is committed\)\s*$/;
16860
+ const existed = existsSync9(path);
16815
16861
  const cur = existed ? readFileSync10(path, "utf8") : "";
16816
- const missing = lines.filter((l) => !cur.split(/\r?\n/).includes(l));
16817
- if (missing.length === 0) {
16862
+ const curLines = cur.split(/\r?\n/);
16863
+ const missing = lines.filter((l) => !curLines.includes(l));
16864
+ const staleAt = curLines.findIndex((l) => STALE_HEADER.test(l));
16865
+ if (missing.length === 0 && staleAt === -1) {
16818
16866
  changes.push({ path: ".gitignore", action: "skipped" });
16867
+ } else if (staleAt !== -1) {
16868
+ const out = [...curLines];
16869
+ out.splice(staleAt, 1, ...HEADER);
16870
+ let last = staleAt + HEADER.length - 1;
16871
+ for (let i = last + 1; i < out.length && /^\.vfkb\//.test(out[i].trim()); i++) last = i;
16872
+ out.splice(last + 1, 0, ...missing);
16873
+ writeFileSync5(path, out.join("\n"));
16874
+ changes.push({ path: ".gitignore", action: "updated" });
16819
16875
  } else {
16820
16876
  const prefix = cur && !cur.endsWith("\n") ? "\n" : "";
16821
- const block = `${prefix}${cur ? "\n" : ""}# vfkb \u2014 derived/operational (only .vfkb/entries.jsonl is committed)
16877
+ const block = `${prefix}${cur ? "\n" : ""}${HEADER.join("\n")}
16822
16878
  ${missing.join("\n")}
16823
16879
  `;
16824
16880
  writeFileSync5(path, cur + block);
@@ -16826,9 +16882,9 @@ ${missing.join("\n")}
16826
16882
  }
16827
16883
  }
16828
16884
  {
16829
- const path = join12(root, ".gitattributes");
16885
+ const path = join13(root, ".gitattributes");
16830
16886
  const line = ".vfkb/entries.jsonl merge=union";
16831
- const existed = existsSync8(path);
16887
+ const existed = existsSync9(path);
16832
16888
  const cur = existed ? readFileSync10(path, "utf8") : "";
16833
16889
  if (cur.split(/\r?\n/).includes(line)) {
16834
16890
  changes.push({ path: ".gitattributes", action: "skipped" });
@@ -16842,8 +16898,8 @@ ${line}
16842
16898
  }
16843
16899
  }
16844
16900
  {
16845
- const path = join12(root, "AGENTS.md");
16846
- const existed = existsSync8(path);
16901
+ const path = join13(root, "AGENTS.md");
16902
+ const existed = existsSync9(path);
16847
16903
  const cur = existed ? readFileSync10(path, "utf8") : "";
16848
16904
  if (cur.includes(AGENTS_MARKER)) {
16849
16905
  changes.push({ path: "AGENTS.md", action: "skipped" });
@@ -16869,10 +16925,10 @@ function approvalNotice(project) {
16869
16925
 
16870
16926
  // src/doctor.ts
16871
16927
  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";
16928
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, realpathSync } from "node:fs";
16929
+ import { join as join14, dirname as dirname6, relative as relative3, resolve as resolve4, isAbsolute as isAbsolute2 } from "node:path";
16874
16930
  function readJson2(path) {
16875
- if (!existsSync9(path)) return void 0;
16931
+ if (!existsSync10(path)) return void 0;
16876
16932
  try {
16877
16933
  return JSON.parse(readFileSync11(path, "utf8"));
16878
16934
  } catch {
@@ -16907,16 +16963,16 @@ function detectPluginWiring(settings, root, pluginsFile) {
16907
16963
  }
16908
16964
  function claudeConfigDir(env) {
16909
16965
  if (env.CLAUDE_CONFIG_DIR) return env.CLAUDE_CONFIG_DIR;
16910
- return env.HOME ? join13(env.HOME, ".claude") : void 0;
16966
+ return env.HOME ? join14(env.HOME, ".claude") : void 0;
16911
16967
  }
16912
16968
  function localHeadSha(cloneDir) {
16913
- const head = readFileMaybe(join13(cloneDir, ".git", "HEAD"));
16969
+ const head = readFileMaybe(join14(cloneDir, ".git", "HEAD"));
16914
16970
  if (!head) return void 0;
16915
16971
  const ref = head.trim().match(/^ref:\s*(\S+)$/)?.[1];
16916
16972
  if (!ref) return /^[0-9a-f]{40}$/.test(head.trim()) ? head.trim() : void 0;
16917
- const loose = readFileMaybe(join13(cloneDir, ".git", ...ref.split("/")));
16973
+ const loose = readFileMaybe(join14(cloneDir, ".git", ...ref.split("/")));
16918
16974
  if (loose) return loose.trim();
16919
- const packed = readFileMaybe(join13(cloneDir, ".git", "packed-refs"));
16975
+ const packed = readFileMaybe(join14(cloneDir, ".git", "packed-refs"));
16920
16976
  for (const line of packed?.split("\n") ?? []) {
16921
16977
  const [sha, name] = line.trim().split(/\s+/);
16922
16978
  if (name === ref) return sha;
@@ -16939,14 +16995,14 @@ var realGit2 = (args, cwd) => execFileSync6("git", args, {
16939
16995
  });
16940
16996
  function checkCurrency(plugin, opts, configDir) {
16941
16997
  const marketplace = plugin.key.split("@")[1];
16942
- const kmFile = opts.knownMarketplacesFile ?? (configDir ? join13(configDir, "plugins", "known_marketplaces.json") : void 0);
16998
+ const kmFile = opts.knownMarketplacesFile ?? (configDir ? join14(configDir, "plugins", "known_marketplaces.json") : void 0);
16943
16999
  const entry = kmFile ? readJson2(kmFile)?.[marketplace] : void 0;
16944
17000
  const skip = (detail) => ({ status: "skip", detail });
16945
17001
  if (!entry) return skip(`no marketplace "${marketplace}" in ${kmFile ?? "the plugin registry"} \u2014 cannot check currency`);
16946
17002
  if (entry.source?.source !== "github") {
16947
17003
  return skip(`marketplace "${marketplace}" is a ${entry.source?.source ?? "unknown"}-source \u2014 no clone to compare`);
16948
17004
  }
16949
- if (!entry.installLocation || !existsSync9(join13(entry.installLocation, ".git"))) {
17005
+ if (!entry.installLocation || !existsSync10(join14(entry.installLocation, ".git"))) {
16950
17006
  return skip(`marketplace clone not found at ${entry.installLocation ?? "(unset installLocation)"}`);
16951
17007
  }
16952
17008
  const clone2 = entry.installLocation;
@@ -16977,7 +17033,7 @@ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@viloforge%2Fvfkb/latest";
16977
17033
  var NPM_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
16978
17034
  var NPM_CACHE_FILE = "npm-currency-cache.json";
16979
17035
  function npmCacheFilePath(opts) {
16980
- return opts.cacheFile ?? join13(opts.brainDir, ".signals", NPM_CACHE_FILE);
17036
+ return opts.cacheFile ?? join14(opts.brainDir, ".signals", NPM_CACHE_FILE);
16981
17037
  }
16982
17038
  function readNpmCache(path) {
16983
17039
  const raw = readJson2(path);
@@ -16987,7 +17043,7 @@ function readNpmCache(path) {
16987
17043
  }
16988
17044
  function writeNpmCache(path, entry) {
16989
17045
  try {
16990
- mkdirSync8(dirname5(path), { recursive: true });
17046
+ mkdirSync8(dirname6(path), { recursive: true });
16991
17047
  writeFileSync6(path, JSON.stringify(entry));
16992
17048
  } catch {
16993
17049
  }
@@ -17104,9 +17160,9 @@ function runDoctor(opts) {
17104
17160
  const checks = [];
17105
17161
  const add = (name, status, detail) => checks.push({ name, status, detail });
17106
17162
  add("engine", "ok", `version ${ENGINE_VERSION} \xB7 commit ${runningCommit} \xB7 schema v${SCHEMA_VERSION}`);
17107
- const settings = readJson2(join13(root, ".claude", "settings.json"));
17163
+ const settings = readJson2(join14(root, ".claude", "settings.json"));
17108
17164
  const configDir = claudeConfigDir(env);
17109
- const pluginsFile = opts.pluginsFile ?? (configDir ? join13(configDir, "plugins", "installed_plugins.json") : void 0);
17165
+ const pluginsFile = opts.pluginsFile ?? (configDir ? join14(configDir, "plugins", "installed_plugins.json") : void 0);
17110
17166
  const plugin = detectPluginWiring(settings, root, pluginsFile);
17111
17167
  const mf = readManifest(brainDir2);
17112
17168
  if (!mf) {
@@ -17154,9 +17210,9 @@ function runDoctor(opts) {
17154
17210
  } else {
17155
17211
  add("journal", "ok", `${js.walLines} line(s) in the uncommitted window, nothing to restore`);
17156
17212
  }
17157
- if (existsSync9(join13(brainDir2, ".journal")) && isUnder(repoToplevel(root), join13(brainDir2, ".journal"))) {
17213
+ if (existsSync10(join14(brainDir2, ".journal")) && isUnder(repoToplevel(root), join14(brainDir2, ".journal"))) {
17158
17214
  try {
17159
- execFileSync6("git", ["-C", root, "check-ignore", "-q", join13(brainDir2, ".journal", "wal.jsonl")], {
17215
+ execFileSync6("git", ["-C", root, "check-ignore", "-q", join14(brainDir2, ".journal", "wal.jsonl")], {
17160
17216
  stdio: "ignore"
17161
17217
  });
17162
17218
  } catch {
@@ -17179,7 +17235,7 @@ function runDoctor(opts) {
17179
17235
  } else {
17180
17236
  add("$VFKB_BUNDLE_DIR", "warn", "unset \u2014 set it once per machine to the vfkb bundles dir (so the wiring resolves the engine)");
17181
17237
  }
17182
- } else if (!existsSync9(join13(home, "vfkb.mjs")) || !existsSync9(join13(home, "vfkb-mcp.mjs"))) {
17238
+ } else if (!existsSync10(join14(home, "vfkb.mjs")) || !existsSync10(join14(home, "vfkb-mcp.mjs"))) {
17183
17239
  add("$VFKB_BUNDLE_DIR", "warn", `set to ${home} but vfkb.mjs / vfkb-mcp.mjs not found there (run \`npm run build:bundles\`)`);
17184
17240
  } else {
17185
17241
  add("$VFKB_BUNDLE_DIR", "ok", home);
@@ -17190,7 +17246,7 @@ function runDoctor(opts) {
17190
17246
  if (env.VFKB_HOME && !env.VFKB_BUNDLE_DIR) {
17191
17247
  add("env (deprecated)", "warn", "VFKB_HOME is a deprecated alias \u2014 rename it to VFKB_BUNDLE_DIR");
17192
17248
  }
17193
- const mcp = readJson2(join13(root, ".mcp.json"));
17249
+ const mcp = readJson2(join14(root, ".mcp.json"));
17194
17250
  const mcpProject = mcp?.mcpServers?.vfkb?.env?.VFKB_PROJECT;
17195
17251
  const mcpPresent = Boolean(mcp?.mcpServers?.vfkb);
17196
17252
  if (plugin && mcpPresent) {
@@ -17220,7 +17276,7 @@ function runDoctor(opts) {
17220
17276
  if (!plugin && have.length > 0 && hooksBlob.includes("bootstrap.mjs") && !hooksBlob.includes("CLAUDE_PROJECT_DIR")) {
17221
17277
  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
17278
  }
17223
- if (existsSync9(join13(root, ".vfkb", "bin", "bootstrap.mjs"))) {
17279
+ if (existsSync10(join14(root, ".vfkb", "bin", "bootstrap.mjs"))) {
17224
17280
  add("bootstrap", "ok", ".vfkb/bin/bootstrap.mjs present");
17225
17281
  } else if (!plugin && (mcpPresent || have.length > 0)) {
17226
17282
  add("bootstrap", "warn", "wiring present but .vfkb/bin/bootstrap.mjs is missing \u2014 run `vfkb init`");
@@ -17260,9 +17316,9 @@ function renderDoctor(report) {
17260
17316
  }
17261
17317
 
17262
17318
  // src/import.ts
17263
- import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
17319
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
17264
17320
  import { homedir as homedir2 } from "node:os";
17265
- import { basename as basename4, extname, join as join14 } from "node:path";
17321
+ import { basename as basename4, extname, join as join15 } from "node:path";
17266
17322
  var MYKB_FILES = {
17267
17323
  "decisions.jsonl": "decision",
17268
17324
  "facts.jsonl": "fact",
@@ -17287,15 +17343,15 @@ function mykbText(type, e) {
17287
17343
  return parts.filter(Boolean).join("\n\n");
17288
17344
  }
17289
17345
  function resolveMykbArea(nameOrDir) {
17290
- if (existsSync10(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
17291
- return join14(homedir2(), ".mykb", "areas", nameOrDir);
17346
+ if (existsSync11(nameOrDir) && statSync3(nameOrDir).isDirectory()) return nameOrDir;
17347
+ return join15(homedir2(), ".mykb", "areas", nameOrDir);
17292
17348
  }
17293
17349
  function fromMykb(areaDir) {
17294
- if (!existsSync10(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
17350
+ if (!existsSync11(areaDir)) throw new Error(`mykb area not found: ${areaDir}`);
17295
17351
  const out = [];
17296
17352
  for (const [file2, type] of Object.entries(MYKB_FILES)) {
17297
- const path = join14(areaDir, file2);
17298
- if (!existsSync10(path)) continue;
17353
+ const path = join15(areaDir, file2);
17354
+ if (!existsSync11(path)) continue;
17299
17355
  for (const line of readFileSync12(path, "utf8").split(/\r?\n/)) {
17300
17356
  const trimmed = line.trim();
17301
17357
  if (!trimmed) continue;
@@ -17322,17 +17378,17 @@ function mdTitle(path) {
17322
17378
  return basename4(path, extname(path));
17323
17379
  }
17324
17380
  function fromAdr(dir = "docs/adr") {
17325
- if (!existsSync10(dir)) throw new Error(`ADR dir not found: ${dir}`);
17381
+ if (!existsSync11(dir)) throw new Error(`ADR dir not found: ${dir}`);
17326
17382
  const out = [];
17327
17383
  for (const file2 of readdirSync3(dir).sort()) {
17328
17384
  if (extname(file2) !== ".md" || /readme/i.test(file2)) continue;
17329
- const rel = join14(dir, file2);
17385
+ const rel = join15(dir, file2);
17330
17386
  out.push(stamp("link", `${mdTitle(rel)} \u2192 ${rel}`, ["adr"], false));
17331
17387
  }
17332
17388
  return out;
17333
17389
  }
17334
17390
  function fromMarkdown(file2) {
17335
- if (!existsSync10(file2)) throw new Error(`markdown file not found: ${file2}`);
17391
+ if (!existsSync11(file2)) throw new Error(`markdown file not found: ${file2}`);
17336
17392
  return [stamp("link", `${mdTitle(file2)} \u2192 ${file2}`, ["doc"], false)];
17337
17393
  }
17338
17394
 
@@ -17412,25 +17468,35 @@ var DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"];
17412
17468
 
17413
17469
  // src/cli.ts
17414
17470
  import { readFileSync as readFileSync13 } from "node:fs";
17415
- import { fileURLToPath } from "node:url";
17416
- import { dirname as dirname6, join as join15 } from "node:path";
17471
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
17472
+ import { dirname as dirname7, join as join16 } from "node:path";
17417
17473
  function packageVersion() {
17418
17474
  try {
17419
- const here = dirname6(fileURLToPath(import.meta.url));
17420
- const pkg = JSON.parse(readFileSync13(join15(here, "..", "package.json"), "utf8"));
17475
+ const here = dirname7(fileURLToPath2(import.meta.url));
17476
+ const pkg = JSON.parse(readFileSync13(join16(here, "..", "package.json"), "utf8"));
17421
17477
  return pkg.version || ENGINE_VERSION;
17422
17478
  } catch {
17423
17479
  return ENGINE_VERSION;
17424
17480
  }
17425
17481
  }
17482
+ var STDIN_WATCHDOG_MS = 2e3;
17426
17483
  function readStdin() {
17427
17484
  return new Promise((resolve5) => {
17428
17485
  let data = "";
17429
17486
  if (process.stdin.isTTY) return resolve5("");
17487
+ let settled = false;
17488
+ const finish = () => {
17489
+ if (settled) return;
17490
+ settled = true;
17491
+ process.stdin.pause();
17492
+ process.stdin.unref?.();
17493
+ resolve5(data);
17494
+ };
17430
17495
  process.stdin.setEncoding("utf8");
17431
17496
  process.stdin.on("data", (c) => data += c);
17432
- process.stdin.on("end", () => resolve5(data));
17433
- setTimeout(() => resolve5(data), 2e3).unref?.();
17497
+ process.stdin.on("end", finish);
17498
+ process.stdin.on("error", finish);
17499
+ setTimeout(finish, STDIN_WATCHDOG_MS).unref?.();
17434
17500
  });
17435
17501
  }
17436
17502
  function flag(args, name) {
@@ -17526,7 +17592,8 @@ async function dispatch() {
17526
17592
  let failed = 0;
17527
17593
  for (const r of results) {
17528
17594
  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)" : ""}
17595
+ 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)' : "";
17596
+ process.stdout.write(`written ${r.target} ${r.id} ${r.posture}${repair}
17530
17597
  `);
17531
17598
  } else {
17532
17599
  failed++;