klypix-mcp 1.85.0 โ†’ 1.86.1

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/FORMAT.md CHANGED
@@ -143,6 +143,17 @@ references are read with both HEAD and working-file changes considered. These fi
143
143
  describe source provenance and change detection, never factual verification.
144
144
  `verify` is retained text, not executable configuration. All these optional fields
145
145
  survive the format codec and capture lifecycle; an explicit empty amendment clears them.
146
+ A cleared `verify` is stored as the empty string `""`, and a reader treats any string `verify`,
147
+ the empty one included, as authoritative: `""` means "no probe" and nothing is derived from the
148
+ card's prose.
149
+ A card without a `verify` field may still carry one in its text as a line-end
150
+ `verify: <command>` suffix (or a `verify: <command>` line of its own); the reference reader
151
+ derives the field with the same grammar the capture hook uses (lowercase key followed by
152
+ whitespace, trailing position, a command-shaped value with no prose after it), reading a
153
+ hard-wrapped card as its unwrapped sentences, so prose such as `npm run verify:mcp` or
154
+ "every agent verify: the tag" never becomes a probe. A probe stops before a following line that
155
+ opens like a new sentence (a plain capitalised word), because such a break may have been typed
156
+ rather than wrapped.
146
157
 
147
158
  The optional **`author`** answers the question a team actually asks: `createdBy` says
148
159
  *what* wrote a card, `author` says *whose*. It is resolved from `git config user.name`
package/README.md CHANGED
@@ -381,6 +381,28 @@ same JSON on stdin, or `--evidence '<JSON array>'` and `--verify '<text>'`.
381
381
  On Claude Code, decisions are captured automatically at turn end from inline `๐Ÿง  BRAIN [Area]:`
382
382
  markers in the transcript, deduped, under a capture lock.
383
383
 
384
+ A marker can end with optional suffixes, in any order: `closes: <card title or [[wikilink]]>`,
385
+ `ev: <file[:line]>, PR#<n>`, `verify: <command>` and `q: <the question this answers?>`. They count
386
+ only as one run at the end of the line, written lowercase as `key: value`, and each value must
387
+ have its key's shape: references for `ev:`, a command for `verify:`, a question (question word
388
+ first, `?` last) for `q:`. Anything else is kept as card text, so "a Q: and A: layout", "the ev:
389
+ field" or "every agent verify: the tag" never cuts a note short. A malformed segment after a
390
+ well-formed one also stays in the text, and the other suffixes still count; the hook tells the
391
+ agent on its next prompt. On `โœ“` and `~` markers a `closes:` is plain text, because those markers
392
+ close nothing. A `closes:` that will not act keeps the sentence as written: it names no live card,
393
+ or it names more than four. A `closes:` that does not come after the end of a sentence, a
394
+ `[[wikilink]]` or another suffix closes only a card it names by title. One whose named card is
395
+ already closed closes nothing else. A `~` update too thin to replace its card (fewer than six
396
+ content words and under half the card's) is appended to that card as a dated `(~ amended โ€ฆ)` line
397
+ instead. The card keeps its date, colour and probe, and previews show the newest amendment first.
398
+ The correction lands and nothing is lost.
399
+
400
+ The Stop hook reads the whole transcript every time, so a `~` or `โœ“` line applies once. A marker
401
+ that an older hook (1.85 or 1.86.0) already captured is not captured again after an upgrade. If
402
+ 1.86.0 cut the note short, the stub is restored to the full text in place. When a marker does not
403
+ do what it says, the next prompt tells the agent. A receipt left by a session that has ended goes
404
+ to the next session that starts in the project.
405
+
384
406
  On every other host, capture is explicit: `brain_note` runs the same capture engine as the hooks โ€”
385
407
  dedup, supersession, round-trip re-adoption receipts, `โœ“` resolve, `~` update in place, `+` skill,
386
408
  `closes:` โ€” and stamps which agent wrote the card. A `โœ“` question preference ranks only candidates
@@ -789,19 +811,37 @@ Read this section before you build on any of it.
789
811
  Every number here is measured on our own project brain. Nothing below is published, benchmarked or
790
812
  independently validated.
791
813
 
792
- - **Dogfood scale.** KLYPIX itself is built with its own brain: **2,479 cards and 2,018
814
+ - **Dogfood scale.** KLYPIX itself is built with its own brain: **2,695 cards and 2,333
793
815
  connections**, written by multiple concurrent agent sessions, receipts in the file. Current as of
794
- 2026-08-13.
816
+ 2026-09-16.
795
817
  - **Recall.** 73% of past decisions recovered with one search round, 55% brief-only, 0% cold.
796
818
  Caveat that travels with it: n=20, our own brain, self-authored questions, LLM-judged.
797
- - **Ranker.** With the production embedder (the eval harness was fixed 2026-08-10 โ€” it had been
798
- measuring a vector space the product does not use): recall@5 **30%**, recall@10 35%, recall@20
799
- 45%, MRR 0.22 of the true source card on n=20 frozen human-paraphrase questions. Lexical-only
800
- scores 0% on the same set. The previously published "15% โ†’ 40% with the reranker" is **retired**:
801
- re-measured validly, the reranker *reduced* recall@5 to 25% and now ships off by default. At n=20
802
- every one of these percentages carries a ยฑ20-point 95% confidence interval โ€” treat them as
803
- directional until the larger frozen set lands. The regressions are recorded next to the wins:
804
- contextual prefixes on short cards, and the reranker itself.
819
+ - **Ranker (`brain_ask`).** Measured 2026-09-16 on the real 2,695-card brain with the production
820
+ embedder, n=107 frozen questions (agent-authored, adversarially verified, four strata), model-free
821
+ rank of the true source card: recall@5 **63%** (95% CI 53โ€“71), recall@10 65%, recall@20 69%, MRR
822
+ 0.43, top-1 33%. The stratum that matters most is the honest one: **paraphrase questions that share
823
+ no words with their card reach recall@5 44%**; status, temporal and multi-hop questions sit at
824
+ 81โ€“93% because they still share vocabulary. Lexical-only scores 0% on the same set.
825
+ - **Second brain.** The same ranker on a different project's brain (1,601 cards, 19 hand-written
826
+ paraphrases, first cross-brain run): recall@5 **42%** (CI 23โ€“64), top-1 11%, MRR 0.25. Paraphrase
827
+ recall transfers between brains; the ranker is not tuned to the brain it was built on. It is
828
+ simply weak on paraphrase everywhere โ€” the embedder's ceiling, documented in `rankForQuestion`.
829
+ - **Retired numbers.** "recall@5 30% (n=20)", "15% โ†’ 40% with the reranker" and the "5% โ†’ 15% โ†’
830
+ 40%" curve are all **retired**: the first was the n=20 set the larger set replaced, the others were
831
+ measured with a reranker that ships off by default (validly re-measured it *reduced* recall@5) or
832
+ by a harness that had drifted from the production vector space (fixed 2026-08-10). The
833
+ regressions are recorded next to the wins: contextual prefixes on short cards, and the reranker.
834
+ - **The per-prompt hook lane** (the retrieval every Claude Code session receives on every prompt)
835
+ had never been measured before 1.86. `scripts/eval-hook-lane.mjs` now measures it through the
836
+ production rankers on a private capture-pair set built from the machine's own enrichment sidecar
837
+ (`scripts/build-hook-eval-set.mjs`). First measurement, 110 real prompts on our own brain: the
838
+ lexical lane fired on 97% of content-bearing prompts and held the right card in its top five 29%
839
+ of the time, and the lane as a whole injected cards on 93% of prompts that should inject nothing
840
+ (acknowledgements, console echoes, relayed machine turns). 1.86 raises the lexical bar so a
841
+ single title word no longer injects, and skips the uncorroborated semantic guess for prompts with
842
+ fewer than four content words: junk injection 93% โ†’ 35%, mean cards injected on junk 3.6 โ†’ 1.5,
843
+ at a one-question cost on the 35 real prompts (inside noise). The prompts stay private; the
844
+ sweep tables are in the source next to the bars they chose.
805
845
  - **What we do not publish.** No download count: this package's own 24-hour auto-updater generates
806
846
  most of it, so it is not a user count. No adoption, team or customer figures. No brief-token
807
847
  figure โ€” the last one was measured at ~600 cards and is stale at 2,479.
@@ -704,6 +704,7 @@ server.registerTool('brain_note', {
704
704
  marker: z.enum(['', '?', '!', '+', 'โœ“', '~']).optional().describe('(none)=decision ยท ?=open question ยท !=milestone ยท +=๐Ÿ› ๏ธ skill (reusable how-to/gotcha; always resurfaces, never ages out) ยท โœ“=resolve+archive the best-matching card ยท ~=update the matching card in place. Default: decision.'),
705
705
  area: z.string().optional().describe('Area/topic โ€” routes the card into that titled container and becomes a #tag (e.g. "Auth", "Release").'),
706
706
  closes: z.string().optional().describe('Title or [[wikilink]] of a strategy/question card this note fulfils โ€” resolves+archives it and draws a "closed by" arrow.'),
707
+ question: z.string().max(240).optional().describe('The question this note ANSWERS, phrased as someone would ask it ("how do we keep a draft separate from what runs?"). Recorded as retrieval enrichment beside the vector cache โ€” never on the canvas โ€” so the card is found by paraphrase, not only by its own words. The session\'s declared intent is recorded as well.'),
707
708
  evidence: z.array(z.object({
708
709
  kind: z.enum(['file', 'pr', 'url', 'commit', 'run']),
709
710
  ref: z.string().min(1).max(1000).describe('A project-relative file path (optional :line or #Lline), or an external reference. External references are stored without fetching them.'),
@@ -724,10 +725,12 @@ server.registerTool('brain_note', {
724
725
  }).optional().describe("GUARD CARDS: make this '+' skill fire BEFORE a matching tool call runs (Claude Code PreToolUse denies on severity block; other hosts warn), not just resurface in briefs. The card stays a normal ๐Ÿ› ๏ธ rule โ€” โœ“-resolving it retires the guard, ~ with {remove:true} disarms it."),
725
726
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
726
727
  },
727
- }, async ({ text, marker, area, closes, evidence, verify, guard, canvas }, extra) => {
728
+ }, async ({ text, marker, area, closes, question, evidence, verify, guard, canvas }, extra) => {
728
729
  // Both 1.77 and 1.78 ride this call: the enrichment question (the asker's
729
- // vocabulary for retrieval) AND the per-session capture receipt below.
730
- const result = await opBrainNote({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), text, area, marker: marker || '', closes, evidence, verify, guard, via: extra.klypixClientName, enrichmentQuestion: mcpPresence.declaredIntent });
730
+ // vocabulary for retrieval) AND the per-session capture receipt below. Since
731
+ // 1.86 the authored `question` (the pattern) rides beside the declared intent
732
+ // (the instance); the sidecar's quality gate decides what is worth keeping.
733
+ const result = await opBrainNote({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), text, area, marker: marker || '', closes, evidence, verify, guard, via: extra.klypixClientName, enrichmentQuestion: [question, mcpPresence.declaredIntent] });
731
734
  // Per-session capture receipt โ€” this is what stops the uncaptured-work nudge
732
735
  // from firing at a session that DID record its reasoning, just through MCP
733
736
  // rather than a ๐Ÿง  marker. The Stop hook and this server share one session-id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.85.0",
3
+ "version": "1.86.1",
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,11 +84,13 @@
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/decay-status.mjs && node test/decay-hook.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/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",
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",
91
- "eval:retrieval": "node scripts/eval-retrieval.mjs"
91
+ "eval:retrieval": "node scripts/eval-retrieval.mjs",
92
+ "eval:hook-lane": "node scripts/eval-hook-lane.mjs",
93
+ "eval:hook-lane:build": "node scripts/build-hook-eval-set.mjs"
92
94
  },
93
95
  "dependencies": {
94
96
  "@modelcontextprotocol/ext-apps": "^1.7.5",
@@ -80,7 +80,7 @@ function workingTreeStatus(root, relative, deadline) {
80
80
  } catch (error) { return error.status === 1 ? 'changed' : 'unverified'; }
81
81
  }
82
82
 
83
- export function prepareBrainEvidence({ projectRoot, evidence, verify, marker = '', text = '' } = {}) {
83
+ export function prepareBrainEvidence({ projectRoot, evidence, verify, marker = '', text = '', deriveVerify = null } = {}) {
84
84
  const bad = error => ({ ok: false, error });
85
85
  if (marker === 'โœ“' && (evidence !== undefined || verify !== undefined)) {
86
86
  return bad('A resolve marker archives existing evidence. To record new evidence, write a milestone with closes, or amend the card with marker ~.');
@@ -88,7 +88,15 @@ export function prepareBrainEvidence({ projectRoot, evidence, verify, marker = '
88
88
  if (verify !== undefined && (typeof verify !== 'string' || verify.length > 2000 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(verify))) {
89
89
  return bad('verify must be a string of at most 2000 characters; it is recorded, never executed.');
90
90
  }
91
- if (marker === '~' && typeof verify === 'string' && !verify.trim() && /(?:^|\s)verify:\s*[^\n\s]/i.test(String(text))) {
91
+ // The question is "would the READER derive a verify from this text?" โ€” so
92
+ // the callers pass the reader itself (klypix-format parseVerifySuffix; this
93
+ // module may not import it). Then "every agent verify: the tag" is not
94
+ // refused (the reader derives nothing) and an own-line `verify: <command>`
95
+ // is (the reader derives it). Without a deriver: a lowercase `verify: value`.
96
+ const inlineVerify = () => (typeof deriveVerify === 'function'
97
+ ? Boolean(deriveVerify(String(text)))
98
+ : /(?:^|\s)verify:\s+\S/.test(String(text)));
99
+ if (marker === '~' && typeof verify === 'string' && !verify.trim() && inlineVerify()) {
92
100
  return bad('To clear verification, remove the inline verify: suffix from the amended text too.');
93
101
  }
94
102
  if (evidence !== undefined && (!Array.isArray(evidence) || evidence.length > 16)) return bad('evidence must be an array of at most 16 references.');
@@ -22,7 +22,7 @@ import fs from 'fs';
22
22
  import path from 'path';
23
23
  import { prepareBrainEvidence } from './brain-evidence.mjs';
24
24
  import { looksLikeUnfilledDraft } from './capture-gap.mjs';
25
- import { captureIntoBrain, tidyBrain, atomicWrite, noteToCaptureInput, formatCaptureReceipts } from './klypix-format.mjs';
25
+ import { captureIntoBrain, tidyBrain, atomicWrite, noteToCaptureInput, formatCaptureReceipts, parseVerifySuffix } from './klypix-format.mjs';
26
26
  import { brainCaptureLockPath, withAdvisoryWriteLock } from './brain-write-lock.mjs';
27
27
 
28
28
  const MARKERS = { '': '', '?': '?', '!': '!', 'โœ“': 'โœ“', '~': '~', question: '?', milestone: '!', resolve: 'โœ“', update: '~', decision: '', done: 'โœ“' };
@@ -73,7 +73,7 @@ if (!opts.text) { console.error('brain-note: nothing to write โ€” pass a note as
73
73
  if (!fs.existsSync(file)) { console.error(`brain-note: no brain at ${file} (run from a project with ./brain.klypix, or pass a path).`); process.exit(1); }
74
74
 
75
75
  if (looksLikeUnfilledDraft(opts.text)) { console.error('brain-note refused (brain unchanged): complete or discard the draft rationale before capturing it.'); process.exit(1); }
76
- const metadata = prepareBrainEvidence({ projectRoot: path.dirname(file), evidence: opts.evidence, verify: opts.verify, marker: opts.marker, text: opts.text });
76
+ const metadata = prepareBrainEvidence({ projectRoot: path.dirname(file), evidence: opts.evidence, verify: opts.verify, marker: opts.marker, text: opts.text, deriveVerify: parseVerifySuffix });
77
77
  if (!metadata.ok) { console.error(`brain-note refused (brain unchanged): ${metadata.error}`); process.exit(1); }
78
78
  const input = noteToCaptureInput({ text: opts.text, area: opts.area, marker: opts.marker, closes: opts.closes, evidence: metadata.evidence, verify: metadata.verify, createdVia: 'cli' });
79
79
  try {
@@ -95,6 +95,10 @@ try {
95
95
  const s = res.stats || {};
96
96
  const bits = [`${s.added || 0} added`];
97
97
  for (const k of ['resolved', 'updated', 'closed', 'superseded']) if (s[k]) bits.push(`${s[k]} ${k}`);
98
+ // A thin ~ is appended to its card (1.86.1), and one the card already says
99
+ // changes nothing โ€” neither may read as a plain "1 updated" / "0 added".
100
+ if (Array.isArray(s.updateAmended) && s.updateAmended.length) bits.push(`${s.updateAmended.length} appended as a dated amendment (too thin to replace its card)`);
101
+ if (Array.isArray(s.updateUnchanged) && s.updateUnchanged.length) bits.push(`${s.updateUnchanged.length} unchanged (the card already says it)`);
98
102
  if (s.reAdopted) bits.push(`${s.reAdopted} re-adopted`);
99
103
  if (s.linked) bits.push(`${s.linked} linked`);
100
104
  console.error(`โœ“ brain-note โ†’ ${path.basename(file)} (${bits.join(' ยท ')})`);
@@ -66,18 +66,73 @@ function readFile(file) {
66
66
 
67
67
  const cleanQuestion = (q) => String(q || '').replace(/\s+/g, ' ').trim().slice(0, ENRICHMENT_MAX_QUESTION_CHARS);
68
68
 
69
+ // โ”€โ”€ Question quality gate (1.86) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
70
+ // "The nearest human prompt" is the right SOURCE of asker language but is not
71
+ // always a question. Measured on this project's own sidecar (2026-09-16, 187
72
+ // recorded texts): 36% were question-shaped, 18% were acknowledgements ("done,
73
+ // what now", "300mb is ok", "ok do them"), and the rest included npm script
74
+ // echoes ("> klypix@1.3.127 release:register โ€ฆ"), hook feedback re-injected
75
+ // as a user turn, and pasted documents. Every one of those was appended to a
76
+ // card's EMBED input as if it were the words someone would ask with โ€” pulling
77
+ // the vector toward generic chatter, the opposite of the vocabulary bridge
78
+ // enrichment exists to build. The gate rejects text that carries no askable
79
+ // vocabulary and runs on BOTH sides (record and read), so an already-recorded
80
+ // sidecar is cleaned lazily on its next read without a rewrite. A rejection
81
+ // costs nothing but recall the text never carried.
82
+ export const ENRICHMENT_MIN_CONTENT_TOKENS = 4;
83
+ const ENRICHMENT_STOPWORDS = new Set((
84
+ 'a an and are as at be been but by can could did do does for from had has have he her his how i if in is it its '
85
+ + 'just let lets me my no nor not of ok okay on or our out she so than that the their them then there these they this '
86
+ + 'those to up us was we were what when where which who whom why will with would you your yes yep nope sure please '
87
+ + 'thanks thank great good fine done cool right now also very really any all both each more most some such too again '
88
+ + 'about into over under after before while because '
89
+ // Arabic function words and acknowledgements (folded forms; enrichment text is
90
+ // recorded as typed, so the common unfolded spellings are listed too).
91
+ + 'ู‡ู„ ู…ุง ู…ุงุฐุง ู„ู…ุงุฐุง ูƒูŠู ุงูŠู† ุฃูŠู† ูˆูŠู† ู…ุชู‰ ู…ู† ููŠ ุนู„ู‰ ุงู„ู‰ ุฅู„ู‰ ุนู† ู‡ุฐุง ู‡ุฐู‡ ุฐู„ูƒ ุชู„ูƒ ู‡ูˆ ู‡ูŠ ู‡ู… ุงู†ุง ุฃู†ุง ู†ุญู† ุงู†ุช ุฃู†ุช ูˆ ุงูˆ ุฃูˆ ู„ุง ู†ุนู… ุชู…ุงู… ุทูŠุจ ุงูˆูƒูŠ ุงูŠูˆู‡ ุงูŠูˆุง'
92
+ ).split(/\s+/).filter(Boolean));
93
+ const ENRICHMENT_MACHINE_RE = /stop hook feedback|<agent-message|\[subagent hand-back\]|<task-notification|system-reminder|\[system notification|<command-(?:name|message|args)|local-command-(?:stdout|stderr)|<hook-[a-z0-9-]+|^\[image:|base directory for this skill:/i;
94
+ const ENRICHMENT_CONSOLE_RE = /^[>$]\s|(?:^|\s)npm (?:err!|warn)\b|\bexited with code \d|[โœ“โœ”]|^\s*\{"|^\s*\[\{/i;
95
+ const ENRICHMENT_PASTED_DOC_RE = /^#{1,6}\s|^```|^---\s|^(?:import|export|const|function|class)\s/;
96
+ const contentTokens = (text) => String(text || '').toLowerCase().split(/[^\p{L}\p{N}]+/u)
97
+ .filter((token) => token.length >= 2 && !ENRICHMENT_STOPWORDS.has(token));
98
+
99
+ /**
100
+ * Decide whether a text carries askable vocabulary worth embedding beside a
101
+ * card. Returns { ok, reason, text } โ€” `text` is the cleaned form that would be
102
+ * recorded. Reasons: too-short ยท machine (harness/hook output) ยท console (npm /
103
+ * shell echo) ยท pasted-doc (markdown/code block) ยท low-content (fewer than
104
+ * ENRICHMENT_MIN_CONTENT_TOKENS non-stopword tokens โ€” the acknowledgement class).
105
+ */
106
+ export function enrichmentQuestionQuality(question) {
107
+ const text = cleanQuestion(question);
108
+ if (text.length < 8) return { ok: false, reason: 'too-short', text };
109
+ if (ENRICHMENT_MACHINE_RE.test(text)) return { ok: false, reason: 'machine', text };
110
+ if (ENRICHMENT_CONSOLE_RE.test(text)) return { ok: false, reason: 'console', text };
111
+ if (ENRICHMENT_PASTED_DOC_RE.test(text)) return { ok: false, reason: 'pasted-doc', text };
112
+ if (contentTokens(text).length < ENRICHMENT_MIN_CONTENT_TOKENS) return { ok: false, reason: 'low-content', text };
113
+ return { ok: true, reason: null, text };
114
+ }
115
+
69
116
  /**
70
117
  * Record question/intent text for captured card bodies. `items` is
71
118
  * [{ body, question }]; entries merge per body key (deduped, newest kept,
72
119
  * capped). Bounded overall: past ENRICHMENT_MAX_ENTRIES the OLDEST entries are
73
120
  * pruned โ€” enrichment is a rolling quality window, not an archive, and unlike
74
121
  * the claims lane nothing downstream depends on any single entry existing.
122
+ * Returns { recorded, rejected } โ€” `rejected` counts texts the quality gate
123
+ * refused (see enrichmentQuestionQuality); they are never written.
75
124
  */
76
125
  export function recordEnrichment(brainPath, items, { home = os.homedir(), now = Date.now() } = {}) {
77
- const list = (Array.isArray(items) ? items : [])
78
- .map((item) => ({ key: enrichmentKeyFor(item?.body), q: cleanQuestion(item?.question) }))
79
- .filter((item) => item.key.length >= 24 && item.q.length >= 8);
80
- if (!list.length) return { recorded: 0 };
126
+ const list = [];
127
+ let rejected = 0;
128
+ for (const item of (Array.isArray(items) ? items : [])) {
129
+ const key = enrichmentKeyFor(item?.body);
130
+ if (key.length < 24) continue;
131
+ const quality = enrichmentQuestionQuality(item?.question);
132
+ if (!quality.ok) { rejected++; continue; }
133
+ list.push({ key, q: quality.text });
134
+ }
135
+ if (!list.length) return { recorded: 0, rejected };
81
136
  const file = enrichmentFileFor(brainPath, home);
82
137
  fs.mkdirSync(path.dirname(file), { recursive: true });
83
138
  const data = readFile(file);
@@ -104,7 +159,7 @@ export function recordEnrichment(brainPath, items, { home = os.homedir(), now =
104
159
  const tmp = `${file}.tmp-${process.pid}`;
105
160
  fs.writeFileSync(tmp, JSON.stringify(data), 'utf8');
106
161
  fs.renameSync(tmp, file);
107
- return { recorded };
162
+ return { recorded, rejected };
108
163
  }
109
164
 
110
165
  /**
@@ -126,9 +181,14 @@ export function readEnrichment(brainPath, { home = os.homedir(), now = Date.now(
126
181
  const memo = readMemo.get(file);
127
182
  if (memo && memo.stamp === stamp) return memo.entries;
128
183
  const data = stamp ? readFile(file) : { entries: {} };
184
+ // The quality gate runs here too: a sidecar written before 1.86 (or by an
185
+ // older hook) is cleaned on read, so its acknowledgements and console echoes
186
+ // stop reaching the embedder without anyone rewriting the file. Entries left
187
+ // with no acceptable text are dropped from the served array entirely.
129
188
  const entries = Object.entries(data.entries)
130
189
  .filter(([, entry]) => now - Number(entry.ts || 0) <= ENRICHMENT_TTL_MS)
131
- .map(([key, entry]) => ({ key, q: (entry.q || []).map(cleanQuestion).filter(Boolean) }));
190
+ .map(([key, entry]) => ({ key, q: (entry.q || []).map((q) => enrichmentQuestionQuality(q)).filter((r) => r.ok).map((r) => r.text) }))
191
+ .filter((entry) => entry.q.length > 0);
132
192
  readMemo.set(file, { stamp, entries });
133
193
  if (readMemo.size > 8) readMemo.delete(readMemo.keys().next().value);
134
194
  return entries;