klypix-mcp 1.86.2 → 1.87.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.
package/README.md CHANGED
@@ -290,7 +290,7 @@ For a reproducible map artifact on every pull request and main-branch push, inst
290
290
  read-only workflow into a Git checkout:
291
291
 
292
292
  ```bash
293
- npx klypix-project-map setup-github /path/to/project
293
+ npx -y -p klypix-mcp klypix-project-map setup-github /path/to/project
294
294
  ```
295
295
 
296
296
  The command refuses to overwrite an existing workflow unless `--force` is explicit. The installed
@@ -314,7 +314,7 @@ behaviour is unverified.
314
314
  | **VS Code (Copilot / Continue)** | MCP config + instructions file | `link` | Model must call `brain_sync` | Model must call `brain_note` | For the MCP connection |
315
315
  | **Gemini CLI / Antigravity** | MCP config + always-on rules file | `link` | Model must call `brain_sync` | Model must call `brain_note` | For the MCP connection |
316
316
  | **Windsurf** | Rules file only | `link` | Reaches the tools through Windsurf's own global MCP config | Model must call `brain_note` | Via its own MCP config |
317
- | **Aider** | Rules file only (no MCP) | `link` | CLI path: `npx klypix-read` | CLI path: `npx klypix-append` | — |
317
+ | **Aider** | Rules file only (no MCP) | `link` | CLI path: `npx -y -p klypix-mcp klypix-read` | CLI path: `npx -y -p klypix-mcp klypix-append` | — |
318
318
  | **Claude Desktop** | One-time manual config edit | you | Model must call `brain_sync` | Model must call `brain_note` | For the MCP connection |
319
319
 
320
320
  `install` and `link` are different things and are not interchangeable: `install` sets up the
@@ -395,7 +395,7 @@ try {
395
395
  // canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
396
396
  // file must never get a JS-comment banner) beside the flat server, which
397
397
  // resolves it via its ./canvas-view-app.html candidate path.
398
- for (const f of ['global-brain-hook.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
398
+ for (const f of ['global-brain-hook.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
399
399
  const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
400
400
  }
401
401
  for (const [src, dst] of [
@@ -94,7 +94,9 @@ const runVerb = async (verb, moduleId) => {
94
94
  // into ~/.claude/project-brain and wire the Claude Code hooks. This is the single
95
95
  // agent-neutral installer, so a brain release reaches every machine via one npm
96
96
  // publish + this command (the global brain serves every project). Runs before any
97
- // server setup; delegates to the dedicated bin so `npx klypix-install` also works.
97
+ // server setup; delegates to the dedicated bin so `npx -p klypix-mcp klypix-install`
98
+ // also works (the bin name on its own is not an npm package we own — see
99
+ // test/npx-owned-names.mjs).
98
100
  await runVerb('install', './klypix-install.mjs');
99
101
 
100
102
  // `npx klypix-mcp link` — make THIS project's brain automatic for EVERY agent tool,
@@ -619,7 +621,7 @@ server.registerTool('brain_lens', {
619
621
 
620
622
  server.registerTool('brain_connect', {
621
623
  title: 'Connect related-but-unlinked brain cards (densify the graph)',
622
- description: 'Repairs orphaned decision/milestone cards first (scope:"orphans", the default) by proposing genuinely related unlinked pairs — semantic similarity at a conservative 0.55 threshold when the on-device model is installed, else shared tags + [[mentions]]. The dry run includes a before→projected orphan receipt; apply:true draws only additive, removable arrows and reports the measured after count. It NEVER archives or rewrites cards. Use scope:"all" for deliberate whole-graph densification. To DISMISS a brain_reconcile false-positive contradiction, pass pairs:[{fromId,toId}] with relationship:"not_contradiction".',
624
+ description: 'Repairs orphaned decision/milestone cards first (scope:"orphans", the default) by proposing genuinely related unlinked pairs — semantic similarity at a conservative 0.55 threshold when the on-device model is installed, else shared tags + [[mentions]]. The dry run includes a before→projected orphan receipt; apply:true draws only additive, removable arrows and reports the measured after count. It NEVER archives or rewrites cards. Use scope:"all" for deliberate whole-graph densification. To DISMISS a brain_reconcile false-positive contradiction, pass pairs:[{fromId,toId}] with relationship:"not_contradiction". Every applied not_fulfilled / not_contradiction dismissal is also recorded, with its surface and client, to the machine-local provenance sidecar (nothing extra is written into the brain file).',
623
625
  inputSchema: {
624
626
  canvas: z.string().optional().describe('Canvas filename/path. Defaults to the project brain ("brain").'),
625
627
  apply: z.boolean().optional().describe('false (default) = suggest only; true = draw the connections.'),
@@ -629,11 +631,11 @@ server.registerTool('brain_connect', {
629
631
  pairs: z.array(z.object({ fromId: z.string(), toId: z.string() })).optional().describe('Explicit card-id pairs to connect (bypasses auto-proposal). Use to dismiss a reconcile false-positive: pass the two card ids with relationship:"not_contradiction".'),
630
632
  relationship: z.string().optional().describe('Relationship for explicit `pairs` (e.g. "not_contradiction" to permanently dismiss a contradiction candidate, or "relates_to", "depends_on", "supports").'),
631
633
  },
632
- }, async ({ canvas, apply, max, threshold, scope, pairs, relationship }) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, max, threshold, scope, pairs, relationship, log })));
634
+ }, async ({ canvas, apply, max, threshold, scope, pairs, relationship }, extra) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, max, threshold, scope, pairs, relationship, via: extra.klypixClientName, log })));
633
635
 
634
636
  server.registerTool('brain_reconcile', {
635
637
  title: 'Reconcile the brain — contradictions, unrecorded migrations, and what a release already closed',
636
- description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth, UNLESS the cue predates its counterpart: then the pair is marked "presumed superseded" and the newer card is presumed current — verify before retiring) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces (and its cue stops overlaying recall/ask for that pair). (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. (4) RELEASE: which open cards look fulfilled by the commits a release ref already carries (subject+body coverage, the card\'s own #commit- receipt, or a hint edge whose milestone is in the ref). READ-ONLY by default and on every other mode. THE ONE EXCEPTION: on mode "claims" and mode "release" you may pass confirm/dismiss to actually close what you verified — confirm names exact card ids, so nothing is matched by prose; covering only part of a multi-item clause writes "✔ partial" and KEEPS the card open unless you pass whole:true; a call whose every entry is refused leaves the brain byte-identical. Never reads the database or the network. Run it periodically, when recall surfaces something you believe is stale, or right before cutting a release.',
638
+ description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth, UNLESS the cue predates its counterpart: then the pair is marked "presumed superseded" and the newer card is presumed current — verify before retiring) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces (and its cue stops overlaying recall/ask for that pair). (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. (4) RELEASE: which open cards look fulfilled by the commits a release ref already carries (subject+body coverage, the card\'s own #commit- receipt, or a hint edge whose milestone is in the ref). READ-ONLY by default and on every other mode. THE ONE EXCEPTION: on mode "claims" and mode "release" you may pass confirm/dismiss to actually close what you verified — confirm names exact card ids, so nothing is matched by prose; covering only part of a multi-item clause writes "✔ partial" and KEEPS the card open unless you pass whole:true; a call whose every entry is refused leaves the brain byte-identical. Never reads the database or the network. Every applied confirm/dismiss is also recorded to the machine-local provenance sidecar (actor, surface, exact card ids — nothing extra is written into the brain file), and a claims confirm settles the pair\'s dashed "likely closed by" hint into a solid confirmed edge. Run it periodically, when recall surfaces something you believe is stale, or right before cutting a release.',
637
639
  inputSchema: {
638
640
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
639
641
  root: z.string().optional().describe("Project root holding the migrations dir / git repo (default: the brain file's folder)."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.86.2",
3
+ "version": "1.87.0",
4
4
  "mcpName": "io.github.dahshanlabs/klypix-mcp",
5
5
  "description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
6
6
  "type": "module",
@@ -84,7 +84,7 @@
84
84
  "bench": "node bin/klypix-mcp.mjs bench",
85
85
  "test:bench": "node test/bench.mjs",
86
86
  "pretest": "node test/publish-workflow.mjs",
87
- "test": "node test/publish-verdict.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/enrichment.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs",
87
+ "test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs &&node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs",
88
88
  "test:memory": "node test/memory-runtime.mjs",
89
89
  "test:memory:soak": "node --expose-gc test/memory-soak.mjs",
90
90
  "runtime": "node bin/klypix-runtime.mjs",
@@ -444,12 +444,12 @@ survives across sessions, agents, and context resets.
444
444
  - do NOT automatically read the full \`.claude/brain-brief.md\`; open it only when \`brain_sync\`
445
445
  says its compact context is insufficient or the task asks for broad project history/status.
446
446
  - with the \`klypix-canvas\` MCP server: to **answer a question** from the brain ("what did we decide about X?", "where did Y land?"), call \`brain_ask\` — it ranks the whole brain, includes superseded history, and surfaces the current truth for any corrected card. Use \`search_canvases\` for a raw keyword lookup, \`read_canvas\` (canvas: \`"brain"\`) for the whole thing, or \`brain_insights\` for the load-bearing cards.
447
- - or via CLI: \`npx klypix-read brain.klypix\`
447
+ - or via CLI: \`npx -y -p klypix-mcp klypix-read brain.klypix\`
448
448
 
449
449
  **When you make a real decision, finding, or milestone — capture it HERE** so it persists for the next session/agent:
450
450
  - with MCP: call \`brain_note\` with a one-line decision, or
451
451
  - emit a marker line in your output: \`🧠 BRAIN [Area]: <one-line decision>\`, or
452
- - via CLI: \`echo "🧠 BRAIN [Area]: <decision>" | npx klypix-append brain.klypix\`
452
+ - via CLI: \`echo "🧠 BRAIN [Area]: <decision>" | npx -y -p klypix-mcp klypix-append brain.klypix\`
453
453
 
454
454
  Capture **sparingly** — real decisions / milestones / open questions / reusable gotchas, not routine
455
455
  steps — and capture it **at the moment you decide** (a one-line marker inline), not batched or left in
@@ -57,6 +57,11 @@ try { repoStateLib = await import('./repo-state.mjs'); } catch { repoStateLib =
57
57
  // (the exact drift that once produced three different live-session counts).
58
58
  let presenceLib = null;
59
59
  try { presenceLib = await import('./agent-presence.mjs'); } catch { presenceLib = null; }
60
+ // provenance powers the informational JUDGMENTS line (confirm/dismiss verdict
61
+ // counts + rejected-prompt pool). Same failure-tolerant idiom; absence of the
62
+ // module OR of any records is a fact, never drift.
63
+ let provenanceLib = null;
64
+ try { provenanceLib = await import('./provenance.mjs'); } catch { provenanceLib = null; }
60
65
 
61
66
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
62
67
 
@@ -497,6 +502,13 @@ export function inspect(opts = {}) {
497
502
  history = { available: true, count: points.length, newestAt: points[0]?.ts || null };
498
503
  } catch { history = { available: true, count: 0, newestAt: null }; }
499
504
  }
505
+ // Judgment provenance (2026-09-29). Informational only: a brain with zero
506
+ // recorded verdicts is NEW, not drifted (same doctrine as history above).
507
+ let provenance = null;
508
+ if (hasBrain && provenanceLib && typeof provenanceLib.provenanceCounts === 'function') {
509
+ try { provenance = { available: true, ...provenanceLib.provenanceCounts(brainPath, { home }) }; }
510
+ catch { provenance = null; }
511
+ }
500
512
  const tools = inspectTools(brainDir, PKG_ROOT);
501
513
  // ── CHECKOUT (release-state visibility, 2026-08-14 incident) ──────────────
502
514
  // Advisory, never a verdict layer: when the PROJECT itself is a versioned
@@ -669,7 +681,7 @@ export function inspect(opts = {}) {
669
681
 
670
682
  // `checkout` is additive (schema-stable): downstream renderers keep parsing
671
683
  // every existing field; it never feeds layers/verdict/actions by design.
672
- return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, gitCapture, history, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, checkout, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
684
+ return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, gitCapture, history, provenance, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, checkout, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
673
685
  }
674
686
 
675
687
  // One-line drift summary (empty when clean) — for a footer / status line.
@@ -840,6 +852,17 @@ export function render(r, opts = {}) {
840
852
  const age = r.history.newestAt ? `${Math.max(0, Math.round((Date.now() - r.history.newestAt) / 60000))}m ago` : 'none yet';
841
853
  L.push(`${ok} ${c.bold}HISTORY${c.rst} ${r.history.count} restore point(s) · newest ${age} ${c.dim}(npx klypix-mcp brain-history list)${c.rst}`);
842
854
  }
855
+ // Judgment provenance — say what confirm/dismiss verdicts this machine has
856
+ // recorded, by verdict and surface, because "the trail exists" is only
857
+ // believable with the counts on the table. Zero records = a new sidecar,
858
+ // never a warning.
859
+ if (r.provenance?.available) {
860
+ const j = r.provenance.judgments || { total: 0, byVerdict: {}, bySource: {} };
861
+ const rej = r.provenance.rejected || { total: 0 };
862
+ const since = j.firstTs ? ` · since ${new Date(j.firstTs).toISOString().slice(0, 10)}` : '';
863
+ const srcBits = Object.entries(j.bySource || {}).map(([source, n]) => `${source} ${n}`).join(' · ');
864
+ L.push(`${ok} ${c.bold}JUDGMENTS${c.rst} ${j.total} recorded (${j.byVerdict?.yes || 0} yes / ${j.byVerdict?.no || 0} no)${since} · rejected-prompt pool ${rej.total}${srcBits ? ` ${c.dim}(${srcBits})${c.rst}` : ''}`);
865
+ }
843
866
 
844
867
  // TOOLS
845
868
  L.push(`${ok} ${c.bold}TOOLS${c.rst} ${r.tools.count} MCP verb(s)${r.tools.hash ? ` ${c.dim}[#${r.tools.hash}, ${r.tools.source}]${c.rst}` : ''}${r.tools.count ? `: ${c.dim}${r.tools.names.join(', ')}${c.rst}` : ''}`);
@@ -118,6 +118,21 @@ async function embedQueries(pipe, texts) {
118
118
  // Vectors are unit-normalized, so dot == cosine.
119
119
  export const dot = (a, b) => { let s = 0; const n = Math.min(a.length, b.length); for (let i = 0; i < n; i++) s += a[i] * b[i]; return s; };
120
120
 
121
+ // Copy of vectorEntryMatchesText in semantic-memory.mjs, where the rule is
122
+ // explained (this one-shot module cannot import the long-lived runtime). It
123
+ // takes precomputed hashes and references nothing outside itself. Keep the two
124
+ // identical: test/semantic-hash-parity.mjs compares them.
125
+ export function vectorEntryMatchesText(entry, fullHash, truncatedHash) {
126
+ try {
127
+ if (!entry || !entry.v || typeof fullHash !== 'string' || !fullHash) return false;
128
+ if (typeof entry.t === 'string' && entry.t === fullHash) return true;
129
+ if (typeof entry.h !== 'string' || !entry.h) return false;
130
+ if (entry.h === fullHash) return true;
131
+ const truncated = typeof truncatedHash === 'function' ? truncatedHash() : null;
132
+ return typeof truncated === 'string' && truncated !== '' && entry.h === truncated;
133
+ } catch { return false; }
134
+ }
135
+
121
136
  // READ-ONLY card-vector cache — NEVER embeds or writes (embedding all cards in the
122
137
  // per-prompt process is the multi-second stall we forbid). Reuses the SAME warm
123
138
  // cache the MCP host fills. Current workers canonicalize absolute Windows paths
@@ -141,10 +156,23 @@ function readCachedVecs(brainPath, cards) {
141
156
  } catch { /* try next variant */ }
142
157
  }
143
158
  const map = new Map();
144
- // A vector is accepted only for the text it was embedded from (the cache
145
- // stores sha1(text) as `h`) — an edited card must fall back to lexical, never
146
- // pair on a stale embedding (parity with semantic-memory.cachedVectorsForBrain).
147
- if (cache && cache.cards) for (const c of cards) { const e = cache.cards[c.id]; if (e && e.v && (!e.h || e.h === sha1(String(c.text)))) map.set(c.id, e.v); }
159
+ // A vector is accepted only when its entry proves it was embedded from the
160
+ // card's current text: the full-text fingerprint `t`, or for an entry written
161
+ // before `t` existed its `h` against the full or the truncated text (`h` is
162
+ // the hash of the EMBED INPUT: the first 1,500 characters plus enrichment).
163
+ // An edited card must fall back to lexical, never pair on a stale embedding.
164
+ // Same card filter and same acceptance rule as
165
+ // semantic-memory.cachedVectorsForBrain, on ONE cache file: this reader
166
+ // stops at the first variant carrying the current modelKey, the server
167
+ // reader merges every alias file, so the two can differ until a writer run
168
+ // folds the aliases into the canonical file. The entry lookup comes first
169
+ // so a card with no cached vector costs no hash.
170
+ if (cache && cache.cards) for (const c of cards) {
171
+ if (!c || c.type === 'container' || typeof c.text !== 'string' || !c.text.trim()) continue;
172
+ const e = cache.cards[c.id];
173
+ if (!e || !e.v) continue;
174
+ if (vectorEntryMatchesText(e, sha1(c.text), () => (c.text.length > 1500 ? sha1(c.text.slice(0, 1500)) : null))) map.set(c.id, e.v);
175
+ }
148
176
  return map;
149
177
  }
150
178
 
@@ -120,18 +120,34 @@ export function enrichmentQuestionQuality(question) {
120
120
  * pruned — enrichment is a rolling quality window, not an archive, and unlike
121
121
  * the claims lane nothing downstream depends on any single entry existing.
122
122
  * Returns { recorded, rejected } — `rejected` counts texts the quality gate
123
- * refused (see enrichmentQuestionQuality); they are never written.
123
+ * refused (see enrichmentQuestionQuality); they never reach the enrichment
124
+ * sidecar or the embedder. Since 2026-09-29 each refusal is forwarded, with its
125
+ * reason, to the privacy-screened rejected-log in provenance.mjs (best-effort).
124
126
  */
125
127
  export function recordEnrichment(brainPath, items, { home = os.homedir(), now = Date.now() } = {}) {
126
128
  const list = [];
129
+ const discarded = [];
127
130
  let rejected = 0;
128
131
  for (const item of (Array.isArray(items) ? items : [])) {
129
132
  const key = enrichmentKeyFor(item?.body);
130
133
  if (key.length < 24) continue;
131
134
  const quality = enrichmentQuestionQuality(item?.question);
132
- if (!quality.ok) { rejected++; continue; }
135
+ if (!quality.ok) { rejected++; discarded.push({ text: quality.text, reason: quality.reason }); continue; }
133
136
  list.push({ key, q: quality.text });
134
137
  }
138
+ // A discard is still SIGNAL — the acknowledgement class is exactly the
139
+ // negative pool a judge study needs — so the text goes, WITH its refusal
140
+ // reason, to the bounded rejected-log sidecar (provenance.mjs applies the
141
+ // privacy screen: machine/console/pasted-doc classes store hash+length only).
142
+ // Lazy fire-and-forget import, never-throw: a stale deployment without
143
+ // provenance.mjs just skips, costing training data, never a capture.
144
+ if (discarded.length) {
145
+ try {
146
+ import('./provenance.mjs')
147
+ .then((prov) => { try { prov.recordRejectedEnrichment(brainPath, discarded, { home, now }); } catch { /* additive */ } })
148
+ .catch(() => { /* stale deployment — additive signal only */ });
149
+ } catch { /* dynamic import unavailable — additive signal only */ }
150
+ }
135
151
  if (!list.length) return { recorded: 0, rejected };
136
152
  const file = enrichmentFileFor(brainPath, home);
137
153
  fs.mkdirSync(path.dirname(file), { recursive: true });