klypix-mcp 1.95.0 → 1.96.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
@@ -1143,6 +1143,15 @@ independently validated.
1143
1143
  0.43, top-1 33%. The stratum that matters most is the honest one: **paraphrase questions that share
1144
1144
  no words with their card reach recall@5 44%**; status, temporal and multi-hop questions sit at
1145
1145
  81–93% because they still share vocabulary. Lexical-only scores 0% on the same set.
1146
+ - **The caller's words (`brain_ask`, 1.96).** A question can end with one more line,
1147
+ `Also: <words a card answering it would likely use>`; the calling agent is itself a model, so it
1148
+ can guess the card's wording at no extra cost. The words join the meaning query only. Measured
1149
+ 2026-10-11 on the same brain grown to 3,829 cards (102 of the frozen questions still answerable),
1150
+ with words written by Gemini 2.5 Flash as a stand-in for the calling agent: recall@5 54% → 59%
1151
+ (8 found, 3 lost), recall@8 61% → 68%, recall@20 68% → 76%, MRR 0.375 → 0.424. With no local
1152
+ model (a fresh install, the Claude plugin) the words count as extra keywords at half weight:
1153
+ recall@5 19% → 24% (5 found, none lost), recall@10 25% → 31%. Without the line, ranking is
1154
+ unchanged byte for byte.
1146
1155
  - **Second brain.** The same ranker on a different project's brain (1,601 cards, 19 hand-written
1147
1156
  paraphrases, first cross-brain run): recall@5 **42%** (CI 23–64), top-1 11%, MRR 0.25. Paraphrase
1148
1157
  recall transfers between brains; the ranker is not tuned to the brain it was built on. It is
@@ -508,7 +508,7 @@ server.registerTool('search_all_brains', {
508
508
 
509
509
  server.registerTool('brain_ask', {
510
510
  title: 'Ask the project brain a question (whole-brain, correction-aware answer)',
511
- description: 'Answer a natural-language question from the WHOLE project brain — "what did we decide about X?", "where did the auth work land?", "why did we drop Y?". Ranks every card lexically, blended with on-device semantic similarity ONLY when the optional local model is installed (a fresh install is lexical; it degrades cleanly). INCLUDES superseded/archived history (flagged, so you can see how a decision changed), and attaches each stale card\'s live CORRECTION so the answer reflects the current truth, not an outdated card. Returns a synthesis-ready context (full cards + provenance + lifecycle) for you to turn into a direct, cited answer — it does not itself write prose. Prefer this over search_canvases when the user asks a QUESTION (not a keyword lookup). Optional as_of (YYYY-MM-DD) answers "what was true then". Defaults to the project brain ("brain").',
511
+ description: 'Answer a natural-language question from the WHOLE project brain — "what did we decide about X?", "where did the auth work land?", "why did we drop Y?". Ranks every card lexically, blended with on-device semantic similarity ONLY when the optional local model is installed (a fresh install is lexical; it degrades cleanly). INCLUDES superseded/archived history (flagged, so you can see how a decision changed), and attaches each stale card\'s live CORRECTION so the answer reflects the current truth, not an outdated card. Returns a synthesis-ready context (full cards + provenance + lifecycle) for you to turn into a direct, cited answer — it does not itself write prose. Prefer this over search_canvases when the user asks a QUESTION (not a keyword lookup). Optional as_of (YYYY-MM-DD) answers "what was true then". Cards are often worded differently from the question, so end the question with one more line, `Also: <8–14 words a card answering it would likely use: synonyms, feature, file or tool names>`; they widen the search while the question still leads (measured: right card in the top 5 for 60 of 102 test questions instead of 55; 24 instead of 19 when no local model is installed). Defaults to the project brain ("brain").',
512
512
  inputSchema: {
513
513
  question: z.string().describe('The natural-language question to answer from the brain.'),
514
514
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.95.0",
3
+ "version": "1.96.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",
@@ -86,7 +86,7 @@
86
86
  "bench": "node bin/klypix-mcp.mjs bench",
87
87
  "test:bench": "node test/bench.mjs",
88
88
  "pretest": "node test/publish-workflow.mjs",
89
- "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/session-mailbox.mjs && node test/session-reopen.mjs && node test/lane-write-retry.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/arrange-receipts.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/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.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/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
89
+ "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/session-mailbox.mjs && node test/session-reopen.mjs && node test/lane-write-retry.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/brain-ask-words.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/arrange-receipts.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/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.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/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
90
90
  "test:memory": "node test/memory-runtime.mjs",
91
91
  "test:memory:soak": "node --expose-gc test/memory-soak.mjs",
92
92
  "runtime": "node bin/klypix-runtime.mjs",
@@ -30,7 +30,7 @@ import {
30
30
  findUnrecordedMigrations, captureIntoBrain, tidyBrain, noteToCaptureInput,
31
31
  selectGardenCandidates, applyGarden, detectContradictions,
32
32
  findDuplicatePartialNotes, collapseDuplicatePartialNotes,
33
- rankForQuestion, questionContextToMarkdown, findLegacyShipCards,
33
+ rankForQuestion, splitAskWords, questionContextToMarkdown, findLegacyShipCards,
34
34
  challengeBrain, challengeContextToMarkdown, buildRenderSpec, structToBrief,
35
35
  brainLensData, lensToMarkdown, deathDateOfCard,
36
36
  statusContextToMarkdown, findFulfillmentCandidates, findStaleOpenCards, releaseFulfilledOpens, openStatusSummary, perAreaTableToMarkdown,
@@ -766,7 +766,9 @@ export async function opBrainTaskContext({
766
766
  }
767
767
 
768
768
  export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log = () => {} }) {
769
- const q = String(question || '').trim();
769
+ // A last line `Also: <words>` is the caller's guess at the card's wording; it
770
+ // joins the meaning query only (splitAskWords in klypix-format.mjs).
771
+ const { question: q, words } = splitAskWords(question);
770
772
  if (!q) return err('brain_ask needs a question.');
771
773
  const t = brainTarget(vault, canvas);
772
774
  if (t.ambiguous) return ambiguousBrainErr(t.ambiguous);
@@ -783,9 +785,9 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
783
785
  try {
784
786
  const pipe = await getEmbedderForUse(log, 20_000);
785
787
  if (pipe) {
786
- const [qv] = await embedTexts(pipe, [q], { kind: 'query' });
788
+ const [qv] = await embedTexts(pipe, [words ? `${q}\n${words}` : q], { kind: 'query' });
787
789
  const vecs = await vectorsForBrain(pipe, t.file, struct.cards);
788
- if (qv && vecs && vecs.size) { semantic = new Map(); for (const [id, v] of vecs) semantic.set(id, dot(qv, v)); cardVecs = vecs; mode = 'semantic+lexical (on-device)'; }
790
+ if (qv && vecs && vecs.size) { semantic = new Map(); for (const [id, v] of vecs) semantic.set(id, dot(qv, v)); cardVecs = vecs; mode = `semantic+lexical (on-device)${words ? ' + your Also words' : ''}`; }
789
791
  }
790
792
  } catch { semantic = null; cardVecs = null; }
791
793
  // Parity with search_all_brains (8445e9c): never say "warming — retry" to a
@@ -799,6 +801,7 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
799
801
  if (!semantic) {
800
802
  fallbackNotice = semanticFallbackNotice(false);
801
803
  if (fallbackNotice) mode = fallbackNotice.startsWith('LEXICAL ONLY') ? 'lexical-only (no semantic runtime)' : 'lexical (semantic warming)';
804
+ if (words) mode += ' + your Also words';
802
805
  }
803
806
  const kk = Math.max(1, Math.min(20, k || 10));
804
807
  const timeTravel = asOfTs != null;
@@ -811,7 +814,7 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
811
814
  // vectors that rank the question, reused; null degrades pairing to its
812
815
  // lexical anchor/coverage fallback (works on every host, embeddings or not).
813
816
  const pairSim = cardVecs ? (a, b) => { const va = cardVecs.get(a), vb = cardVecs.get(b); return va && vb ? dot(va, vb) : null; } : null;
814
- const result = rankForQuestion(struct, q, { semantic, k: wantRerank ? Math.max(kk, 50) : kk, as_of: timeTravel ? as_of : null, pairSim });
817
+ const result = rankForQuestion(struct, q, { semantic, k: wantRerank ? Math.max(kk, 50) : kk, as_of: timeTravel ? as_of : null, pairSim, words });
815
818
  if (wantRerank && result.hits.length > 1) {
816
819
  try {
817
820
  const reranked = await withRerankerForUse(log, 8_000, rr => rerankHits(rr, q, result.hits));
@@ -4042,14 +4042,51 @@ export const deathDateOfCard = (card) => {
4042
4042
  const m = /(?:↩︎ superseded|↩ superseded|✅|⤵ consolidated) (\d{4}-\d{2}-\d{2})/.exec(String(c.text || ''));
4043
4043
  return m ? Date.parse(m[1]) : null;
4044
4044
  };
4045
- export function rankForQuestion(struct, question, { semantic = null, k = 10, as_of = null, now = Date.now(), recentDays = 30, pairSim = null } = {}) {
4045
+ // THE CALLER'S WORDS (1.96.0). A question is worded the asker's way; the card
4046
+ // that answers it was worded the day it was written ("magnification controls"
4047
+ // vs a card that says "zoom"). The calling agent is itself a model, so brain_ask
4048
+ // invites it to add one last line `Also: <words a card answering this would
4049
+ // likely use>`. Those words join the MEANING query only (embedded together with
4050
+ // the question, one vector); the keyword anchor test, status shape and the
4051
+ // rendered question all stay the question's own. Measured 2026-10-11 on the
4052
+ // real 3,829-card brain, frozen v2 set (102 answerable), production embedder +
4053
+ // warm enrichment-aware vectors, model-written words: recall@5 55 → 60
4054
+ // (+8 −3), @8 62 → 69, @20 69 → 78, MRR .375 → .424. Fusing a separate words
4055
+ // vector by rank instead did less (+5 −2 at equal weight). Carried inside
4056
+ // `question` because no existing tool's input schema may change (see
4057
+ // test/tool-schema-freeze.mjs) — a new property would force every host to
4058
+ // reconnect; a line in the text works the moment the worker hot-swaps.
4059
+ export const ASK_WORDS_MAX = 240;
4060
+ // Keyword-only mode (no meaning model), same brain and set, words as extra
4061
+ // tokens, through splitAskWords: recall@5 19 → 24 of 102 at half weight with
4062
+ // none lost (@10 26 → 32); full weight lost a question the question alone
4063
+ // found. Measured 2026-10-11.
4064
+ export const ASK_WORDS_LEXICAL_WEIGHT = 0.5;
4065
+ export function splitAskWords(question) {
4066
+ const whole = String(question || '').trim();
4067
+ const lines = whole.split(/\r?\n/);
4068
+ const last = lines.length > 1 ? /^\s*also\s*:\s*(.*)$/i.exec(lines[lines.length - 1]) : null;
4069
+ const rest = last ? lines.slice(0, -1).join('\n').trim() : '';
4070
+ // No words line, or nothing above it: the text is the question, untouched.
4071
+ if (!last || !rest) return { question: whole, words: '' };
4072
+ const words = last[1].replace(/\s+/g, ' ').trim().slice(0, ASK_WORDS_MAX).trim();
4073
+ // An empty `Also:` line is dropped; words equal to the question add nothing.
4074
+ return { question: rest, words: words.toLowerCase() === rest.toLowerCase() ? '' : words };
4075
+ }
4076
+
4077
+ export function rankForQuestion(struct, question, { semantic = null, k = 10, as_of = null, now = Date.now(), recentDays = 30, pairSim = null, words = '' } = {}) {
4046
4078
  // Status-shaped questions score by their CONTENT tokens only — "remaining"
4047
4079
  // must never lexically select the stale cards that say "remaining:".
4048
4080
  const { content: tokens, statusShaped, strong: statusStrong, areaFamilies = [] } = splitQueryTokens(question);
4081
+ // The caller's Also words (splitAskWords). With the meaning model they ride
4082
+ // the query vector (opBrainAsk); WITHOUT it — a fresh install, the Claude
4083
+ // plugin — they are extra keyword tokens at ASK_WORDS_LEXICAL_WEIGHT, after
4084
+ // the question's own, so the question still leads.
4085
+ const extra = words && !semantic ? splitQueryTokens(words).content.filter(t => !tokens.includes(t)) : [];
4049
4086
  // Area scope (1.85.0): this function HAS the struct, so it resolves the
4050
4087
  // prompt's static families to exact area titles for the status renderer.
4051
4088
  const areas = statusStrong ? areaHintsFromPrompt(struct, question) : null;
4052
- if (!struct || !Array.isArray(struct.cards) || (!tokens.length && !semantic)) return { hits: [], total: 0, tokens, statusShaped, statusStrong, areaFamilies, areas };
4089
+ if (!struct || !Array.isArray(struct.cards) || (!tokens.length && !extra.length && !semantic)) return { hits: [], total: 0, tokens, statusShaped, statusStrong, areaFamilies, areas };
4053
4090
  const isArchived = (c) => /^archive$/i.test(c.area || '');
4054
4091
  const asOfTs = as_of ? Date.parse(as_of) : null;
4055
4092
  const timeTravel = asOfTs != null && Number.isFinite(asOfTs);
@@ -4087,6 +4124,10 @@ export function rankForQuestion(struct, question, { semantic = null, k = 10, as_
4087
4124
  else if (wordMatches(tagWords, tagStems, tok)) lex += 3;
4088
4125
  else if (wordMatches(bodyW, bodyStems, tok)) lex += lenNorm;
4089
4126
  }
4127
+ for (const tok of extra) {
4128
+ if (wordMatches(titleW, titleStems, tok) || wordMatches(tagWords, tagStems, tok)) lex += 3 * ASK_WORDS_LEXICAL_WEIGHT;
4129
+ else if (wordMatches(bodyW, bodyStems, tok)) lex += lenNorm * ASK_WORDS_LEXICAL_WEIGHT;
4130
+ }
4090
4131
  const sem = semantic ? (semantic.get(c.id) ?? null) : null;
4091
4132
  if (!semantic && lex <= 0) continue;
4092
4133
  if (semantic && lex <= 0 && sem == null) continue;