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 +11 -0
- package/README.md +50 -10
- package/bin/klypix-worker.mjs +6 -3
- package/package.json +5 -3
- package/src/brain-evidence.mjs +10 -2
- package/src/brain-note.mjs +6 -2
- package/src/enrichment.mjs +66 -6
- package/src/global-brain-hook.mjs +595 -62
- package/src/klypix-core.mjs +15 -4
- package/src/klypix-format.mjs +839 -136
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,
|
|
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-
|
|
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.**
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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.
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|
package/src/brain-evidence.mjs
CHANGED
|
@@ -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
|
-
|
|
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.');
|
package/src/brain-note.mjs
CHANGED
|
@@ -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(' ยท ')})`);
|
package/src/enrichment.mjs
CHANGED
|
@@ -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 =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
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;
|