klypix-mcp 1.83.0 → 1.85.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/A2A.md CHANGED
@@ -113,6 +113,12 @@ If a write (`make_board`/`remember`) returns `input-required`, reply with a mess
113
113
  carrying the same `taskId` plus the missing input to **continue that task** (the
114
114
  server resumes it with a stable id and accumulated history).
115
115
 
116
+ For `remember` or `learn_skill`, one-card requests may also provide `args.evidence`
117
+ and `args.verify` using the [brain capture schema](README.md#capture-and-corrections).
118
+ Such requests use the shared capture engine even without a marker. References and
119
+ verification text are preserved; malformed metadata is rejected instead of discarded.
120
+ Verification text is never executed.
121
+
116
122
  ## Notes
117
123
 
118
124
  - Tasks complete synchronously (the work is local file I/O), so `message/send`
package/FORMAT.md CHANGED
@@ -132,6 +132,18 @@ strings: `text`, `box`, `image`, `file`, `container`, `approval`, `link`,
132
132
  `createdBy` (`user` | `agent`) and the optional `createdVia` (which agent/channel
133
133
  captured it) are the provenance bits the brain surfaces as badges and lenses.
134
134
 
135
+ Brain cards may also carry `evidence` and `verify`. An evidence reference has `kind`
136
+ (`file`, `pr`, `url`, `commit`, or `run`), `ref`, optional caller-supplied file blob `oid`,
137
+ and optional caller-reported ISO `verifiedAt`. The host-neutral capture API rejects
138
+ unknown input fields, unsafe file paths, and malformed metadata before writing.
139
+ It adds `capturedAt`; readable local files up to 2 MiB also receive a SHA-256 `sha256`
140
+ and `sourceBasis: "working-tree"`. Optional `headRevision` identifies HEAD at capture,
141
+ which does not imply the captured working bytes were committed. Legacy `oid`-only
142
+ references are read with both HEAD and working-file changes considered. These fields
143
+ describe source provenance and change detection, never factual verification.
144
+ `verify` is retained text, not executable configuration. All these optional fields
145
+ survive the format codec and capture lifecycle; an explicit empty amendment clears them.
146
+
135
147
  The optional **`author`** answers the question a team actually asks: `createdBy` says
136
148
  *what* wrote a card, `author` says *whose*. It is resolved from `git config user.name`
137
149
  so brain attribution matches commit attribution with no configuration (override with
package/README.md CHANGED
@@ -1,14 +1,28 @@
1
1
  # Every project gets a brain.
2
2
 
3
- **Active state management for multi-agent coding a local-first active context engine with a shared brain.**
4
- *One project. One shared understanding.*
3
+ Shared project memory for **Claude Code, Codex, Cursor** and other MCP coding tools. One
4
+ `brain.klypix` file, committed with your code, carries the project's decisions, corrections and open
5
+ questions across sessions and between tools. Corrections supersede stale decisions; sessions declare
6
+ the files they expect to touch and get warned about same-machine overlap. Versioned in Git. Served
7
+ over MCP by a process on your machine; Klypix uploads nothing (your agent's provider still receives
8
+ what the agent reads). Integration depth differs by host — see
9
+ [Supported hosts](#supported-hosts-and-their-integration-level).
5
10
 
6
- [![CI](https://img.shields.io/github/actions/workflow/status/dahshanlabs/klypix-mcp/ci.yml?branch=master&style=flat-square&label=CI)](https://github.com/dahshanlabs/klypix-mcp/actions/workflows/ci.yml)
7
- [![npm](https://img.shields.io/npm/v/klypix-mcp?style=flat-square)](https://www.npmjs.com/package/klypix-mcp)
8
- [![License](https://img.shields.io/github/license/dahshanlabs/klypix-mcp?style=flat-square)](LICENSE)
9
- [![Node](https://img.shields.io/node/v/klypix-mcp?style=flat-square)](package.json)
10
- [![MCP](https://img.shields.io/badge/MCP-server-475569?style=flat-square)](https://modelcontextprotocol.io)
11
- [![bench](https://img.shields.io/badge/npx_klypix--mcp_bench-10_writers_%C2%B7_0_lost-475569?style=flat-square)](BENCHMARKS.md)
11
+ ![Two real MCP sessions on one project: Session B declares a file Session A already declared, and the server's exact-file-overlap warning fires; Session A then records a correction that supersedes its stale card](docs/demo/demo.gif)
12
+
13
+ <sub>Real output, not a mockup: both panes run a real MCP client against this server
14
+ ([docs/demo/](docs/demo/) — the GIF is rendered by CI from a scripted tape against this server, not
15
+ hand-recorded, and re-rendered when the server's responses change).</sub>
16
+
17
+ Run this inside your project:
18
+
19
+ ```bash
20
+ npx klypix-mcp install
21
+ ```
22
+
23
+ It creates `brain.klypix` if the project has none, wires the editors it finds on this machine,
24
+ registers the `.klypix` merge driver if this is a git repo, and exits only after a real MCP
25
+ handshake has counted the tools that answered.
12
26
 
13
27
  [![Claude Code](https://img.shields.io/badge/Claude_Code-5_lifecycle_hooks-475569?style=flat-square)](#supported-hosts-and-their-integration-level)
14
28
  [![Codex](https://img.shields.io/badge/Codex-native_MCP_%2B_presence-475569?style=flat-square)](#supported-hosts-and-their-integration-level)
@@ -18,24 +32,18 @@
18
32
  <sub>Host badges name the **integration level**, not a flat "compatible" — the levels and what is
19
33
  actually tested are in [Supported hosts](#supported-hosts-and-their-integration-level).</sub>
20
34
 
21
- **One actively managed project brain for multi-agent coding.** `klypix-mcp` keeps one versioned
22
- `brain.klypix` in your repo: the project's active state — current decisions, corrections, evidence
23
- anchors, open questions, active work, and handoffs. Corrections supersede stale decisions,
24
- `brain_challenge` tests proposed decisions against standing rules and reversed approaches, and
25
- sessions declare their scope and get warned about same-machine file overlap. Agents read it and
26
- write to it over MCP. You read it and correct it in the [KLYPIX app](https://klypix.com).
27
-
28
35
  > **One project. Many agents. One current understanding.**
29
36
 
30
- ![Two real MCP sessions on one project: Session B declares a file Session A already declared, and the server's exact-file-overlap warning fires; Session A then records a correction that supersedes its stale card](docs/demo/demo.gif)
31
-
32
- <sub>Real output, not a mockup: both panes run a real MCP client against this server
33
- ([docs/demo/](docs/demo/) — the GIF is re-rendered by CI from a scripted tape, so it can never
34
- drift from what the product actually does).</sub>
35
-
36
37
  Klypix does not launch, run, supervise, or replace your agents. It is not an agent runtime, a model
37
- router, a worktree manager, or a replacement for Git. It is the layer underneath them that holds
38
- what the project currently believes.
38
+ router, a worktree manager, or a replacement for Git. It holds what the project currently believes.
39
+
40
+ [![CI](https://img.shields.io/github/actions/workflow/status/dahshanlabs/klypix-mcp/ci.yml?branch=master&style=flat-square&label=CI)](https://github.com/dahshanlabs/klypix-mcp/actions/workflows/ci.yml)
41
+ [![npm](https://img.shields.io/npm/v/klypix-mcp?style=flat-square)](https://www.npmjs.com/package/klypix-mcp)
42
+ [![License](https://img.shields.io/github/license/dahshanlabs/klypix-mcp?style=flat-square)](LICENSE)
43
+ [![Node](https://img.shields.io/node/v/klypix-mcp?style=flat-square)](package.json)
44
+ [![MCP](https://img.shields.io/badge/MCP-server-475569?style=flat-square)](https://modelcontextprotocol.io)
45
+ [![Glama score](https://glama.ai/mcp/servers/dahshanlabs/klypix-mcp/badges/score.svg)](https://glama.ai/mcp/servers/dahshanlabs/klypix-mcp)
46
+ [![bench](https://img.shields.io/badge/npx_klypix--mcp_bench-10_writers_%C2%B7_0_lost-475569?style=flat-square)](BENCHMARKS.md)
39
47
 
40
48
  ## See the shared project brain in action
41
49
 
@@ -346,6 +354,30 @@ there are no lifecycle hooks on those hosts.
346
354
 
347
355
  ## Capture and corrections
348
356
 
357
+ `brain_note` accepts structured supporting references and inert verification text:
358
+
359
+ ```json
360
+ {
361
+ "text": "Retry failed uploads with a bounded backoff to preserve queued work.",
362
+ "area": "Storage",
363
+ "evidence": [{ "kind": "file", "ref": "src/uploads.mjs:42" }],
364
+ "verify": "node test/uploads.mjs"
365
+ }
366
+ ```
367
+
368
+ File references must stay inside the project. The capture records a fingerprint of the
369
+ working file and, when available, the repository HEAD revision. An unchanged fingerprint
370
+ means **source unchanged**, not that the remembered claim is correct or that tests passed.
371
+ Dirty working files are fingerprinted as they are; HEAD alone does not describe those bytes.
372
+ Read results distinguish changed, missing, and unverified sources. External references
373
+ (`pr`, `url`, `commit`, `run`) are retained without fetching or verifying them. `verify` is
374
+ shown as recorded text and never executed. Optional `verifiedAt` is explicitly caller-reported.
375
+
376
+ On an amendment (`marker: "~"`), omitted metadata is preserved; `evidence: []` and
377
+ `verify: ""` clear obsolete metadata. A resolve (`✓`) archives existing evidence; attach
378
+ new evidence with a milestone and `closes`, or amend before resolving. The CLI accepts the
379
+ same JSON on stdin, or `--evidence '<JSON array>'` and `--verify '<text>'`.
380
+
349
381
  On Claude Code, decisions are captured automatically at turn end from inline `🧠 BRAIN [Area]:`
350
382
  markers in the transcript, deduped, under a capture lock.
351
383
 
@@ -563,10 +595,10 @@ The MCP verbs below are what agents call. These are what **you** call:
563
595
  | `brain_ask` | Whole-brain question answering — correction-aware, `as_of` time travel |
564
596
  | `brain_challenge` | The brain argues back: contradictions with receipts, tried-and-reversed chains, standing rules, other-agent provenance flags |
565
597
  | `brain_note` | Capture with the full lifecycle — supersede / re-adopt / ✓ resolve / ~ update / 🛠 skill / `closes:` |
566
- | `brain_reconcile` | Proposes stale-vs-correction pairs and unrecorded migrations for a human to confirm |
598
+ | `brain_reconcile` | Proposes stale-vs-correction pairs, unrecorded migrations, and the open cards a release ref's commits look to have closed — then closes the exact pairs you confirm |
567
599
  | `brain_insights` | Hubs, orphaned decisions, stale questions, area sizes |
568
600
  | `brain_lens` | Machine-readable freshness, provenance, activity, timeline, orrery and unresolved views |
569
- | `brain_garden` | Maintenance pass — proposes first, and cannot apply without an approval code the human generates |
601
+ | `brain_garden` | Maintenance pass — proposes first; consolidation cannot apply without an approval code the human generates. The separate `repair:"duplicate-partials"` pass is dry-run first and needs no code (it removes only exact repeats and archives nothing) |
570
602
  | `brain_doctor` | Self-diagnosis: version, core/enhanced host adapters, active sessions, tool count, projection drift |
571
603
  | `brain_message` | Session-to-session coordination notes with a fixed send-time audience and per-recipient pending / offer / acknowledgement / consumption / failure receipts (24h TTL, never written into the brain) |
572
604
  | `brain_message_receipt` | Explicitly record model-side consumption using the exact message id and per-recipient offer token; acknowledgement alone never consumes a note |
@@ -589,8 +621,15 @@ Exactly 22, machine-verifiable with `npx klypix-mcp doctor`.
589
621
  > screenshot and no host-level test. Hosts without the extension get clean text, which is the path
590
622
  > that is actually verified.
591
623
 
592
- `brain_doctor`, `brain_lens`, `brain_insights` and `brain_reconcile` are read-only introspection.
593
- `brain_garden`, `brain_reconcile` and `brain_connect` always propose before they apply.
624
+ `brain_doctor`, `brain_lens` and `brain_insights` are read-only introspection. `brain_reconcile`
625
+ is read-only too, with one exception: on `mode:"claims"` and `mode:"release"` you may pass
626
+ `confirm` / `dismiss` to close the pairs you verified. Confirm names exact card ids — nothing is
627
+ matched by prose — and covering only part of a multi-item clause writes `✔ partial` and keeps the
628
+ card open unless you pass `whole:true`. A call whose every entry is refused leaves the brain
629
+ byte-identical. A `dismiss` is recorded as a `not_fulfilled` edge between two CARDS, so a hint
630
+ whose only evidence is a raw commit has nothing to point at — name a `cardId`, or resolve the open
631
+ card itself. `brain_garden`, `brain_reconcile` and `brain_connect` always propose before they
632
+ apply.
594
633
  `npx klypix-mcp doctor` gives one verdict and exits non-zero on drift, so it doubles as a CI gate.
595
634
 
596
635
  ## One file you can hold
@@ -327,14 +327,15 @@ async function runSkill(skill, args, text, via) {
327
327
  if (skill === 'learn_skill') marker = '+';
328
328
  const single = (!args.cards && text.trim()) ? stripVerb(text) : null;
329
329
  if (!marker && single && looksLikeSkill(single)) marker = '+'; // NL "remember this gotcha: always…" → skill
330
- if (marker) {
330
+ if (marker || args.evidence !== undefined || args.verify !== undefined) {
331
331
  const noteText = single ?? (Array.isArray(args.cards) && args.cards[0]?.text) ?? '';
332
332
  if (!String(noteText).trim()) return needInput('Nothing to capture — send text or a card to remember.');
333
333
  if (String(noteText).length > 20_000) return needInput('Captured text must be 20,000 characters or fewer.');
334
334
  const requested = args.canvas ?? 'brain';
335
335
  const target = confinedCanvas(requested);
336
336
  if (!target.ok) return refusedCanvas(requested);
337
- return await opBrainNote({ vault: VAULT, canvas: target.canvas, text: noteText, area: args.area, marker, closes: args.closes, via });
337
+ if (Array.isArray(args.cards) && args.cards.length !== 1) return needInput('Evidence capture requires exactly one card per request.');
338
+ return await opBrainNote({ vault: VAULT, canvas: target.canvas, text: noteText, area: args.area, marker, closes: args.closes, evidence: args.evidence, verify: args.verify, via });
338
339
  }
339
340
  // NL convenience: a bare "remember: X" becomes a single card on the brain.
340
341
  const cards = args.cards ?? (text.trim() ? [{ text: stripVerb(text) }] : null);
@@ -254,7 +254,11 @@ const flatten = (code) => code
254
254
  // only thing standing between that and the field; it caught exactly this.
255
255
  // (remote-client deliberately absent: the Remote feature was removed in
256
256
  // 1.73.x, and this cherry-pick must not resurrect it — recorded rule.)
257
- .replace(/\.\.\/src\/(bench|brain-doctor|agent-presence|agent-rules|capture-gap|enrichment|finding-routing|mcp-presence|mcp-supervisor|mcp-auto-update|presence-relay|semantic-memory|runtime-inspector|project-graph|git-capture-install)\.mjs/g, './$1.mjs')
257
+ // repo-state: the worker's release-cut reconcile advisory (1.85.0) imports
258
+ // commitsInRange / makeContainmentProbe from it. It was already STAGED in
259
+ // the flat bundle (mcp-presence needs it) but never flattened, because
260
+ // nothing in bin/ had imported it directly before.
261
+ .replace(/\.\.\/src\/(bench|brain-doctor|agent-presence|agent-rules|capture-gap|enrichment|finding-routing|mcp-presence|mcp-supervisor|mcp-auto-update|presence-relay|repo-state|semantic-memory|runtime-inspector|project-graph|git-capture-install)\.mjs/g, './$1.mjs')
258
262
  .replace(/klypix-worker\.mjs/g, 'klypix-mcp-worker.mjs')
259
263
  .replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
260
264
 
@@ -391,7 +395,7 @@ try {
391
395
  // canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
392
396
  // file must never get a JS-comment banner) beside the flat server, which
393
397
  // resolves it via its ./canvas-view-app.html candidate path.
394
- for (const f of ['global-brain-hook.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'brain-note.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', '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']) {
395
399
  const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
396
400
  }
397
401
  for (const [src, dst] of [
@@ -34,6 +34,7 @@ import { compareProjectGraphResults, projectGraphContextMarkdown, queryProjectGr
34
34
  import { auditProject, compactAgentsBrief, linkProject, mcpServerEntry } from '../src/agent-rules.mjs';
35
35
  import { createMcpPresence, KLYPIX_MCP_INSTRUCTIONS } from '../src/mcp-presence.mjs';
36
36
  import { consumeMessageReceipt, findProjectBrain } from '../src/agent-presence.mjs';
37
+ import { collectRepoState, commitsInRange, makeContainmentProbe } from '../src/repo-state.mjs';
37
38
  import {
38
39
  reconcileRegisteredProjects,
39
40
  registerProjectBrain,
@@ -219,6 +220,10 @@ const mcpPresence = createMcpPresence({
219
220
  formatDecayAge: typeof brainFormat.formatDecayAge === 'function' ? brainFormat.formatDecayAge : undefined,
220
221
  } : {},
221
222
  });
223
+ // Release-cut reconcile: the ref each lane last scanned, so a checkpoint that
224
+ // merely REFRESHES the same lease does not re-walk the range. In-memory only —
225
+ // a worker restart rescans once, which costs one bounded git log.
226
+ const lastReconcileRef = new Map();
222
227
  // Once brain_sync binds this connection to an exact project brain, all
223
228
  // project-brain-default tools must use that same file. Leaving canvas undefined
224
229
  // lets klypix-core's intentional cwd/env precedence substitute an ambient brain
@@ -627,18 +632,32 @@ server.registerTool('brain_connect', {
627
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 })));
628
633
 
629
634
  server.registerTool('brain_reconcile', {
630
- title: 'Reconcile the brain — contradictions between cards + unrecorded migrations',
631
- 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. Reads ONLY the filesystem never the database, never the network — and changes nothing. Run it periodically, or when recall surfaces something you believe is stale.',
635
+ 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.',
632
637
  inputSchema: {
633
638
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
634
- root: z.string().optional().describe("Project root holding the migrations dir (default: the brain file's folder)."),
635
- mode: z.enum(['all', 'contradictions', 'migrations', 'legacy', 'claims', 'plans']).optional().describe('Which pass to run (default "all"): contradictions · migrations · legacy (pre-v1.15 raw-bash ship cards to tidy) · claims (open "remaining:/next:" clauses a later milestone likely fulfilled — receipts + ✓ markers, never auto-archived) · plans (plan / proposal / "design decided" cards a LATER 🏁 appears to have built — embedding-first because the ship is usually renamed; receipts + ✓ markers, never auto-archived).'),
639
+ root: z.string().optional().describe("Project root holding the migrations dir / git repo (default: the brain file's folder)."),
640
+ mode: z.enum(['all', 'contradictions', 'migrations', 'legacy', 'claims', 'plans', 'release']).optional().describe('Which pass to run (default "all"): contradictions · migrations · legacy (pre-v1.15 raw-bash ship cards to tidy) · claims (open "remaining:/next:" clauses a later milestone likely fulfilled — receipts + ✓ markers; confirm with {id, milestoneId}) · plans (plan / proposal / "design decided" cards a LATER 🏁 appears to have built — embedding-first because the ship is usually renamed) · release (open cards the commits in `ref` look to have fulfilled; confirm with {id, sha}).'),
641
+ ref: z.string().max(200).optional().describe('mode "release": the git ref being cut. Defaults to this session\'s active release lease, else HEAD.'),
642
+ sinceRef: z.string().max(200).optional().describe('mode "release": the baseline the range starts from. Defaults to the highest release-shaped tag in the repo.'),
643
+ confirm: z.array(z.object({
644
+ id: z.string().max(64).describe('The OPEN card id to close.'),
645
+ milestoneId: z.string().max(64).optional().describe('mode "claims": the live milestone card that fulfilled it. Requires an existing "likely closed by" link or a current coverage gap on this exact pair.'),
646
+ sha: z.string().max(40).optional().describe('mode "release": a commit the listing named as covering this card (cov ≥ 0.6, confirmable). Omit to use the card\'s own #commit- receipt.'),
647
+ whole: z.boolean().optional().describe('Assert the WHOLE card is done. Without it, covering one item of a multi-item clause writes "✔ partial" and the card stays open.'),
648
+ })).max(64).optional().describe('Pairs YOU verified. Honoured on mode "claims" and mode "release" only. Each confirmed card is stamped ✅, archived, and arrowed "closed by" to its evidence.'),
649
+ dismiss: z.array(z.object({
650
+ openId: z.string().max(64).describe('The open card the hint was wrong about.'),
651
+ cardId: z.string().max(64).optional().describe('The milestone/evidence card to dismiss it against (required unless the listing already named one).'),
652
+ sha: z.string().max(40).optional().describe('Informational only — the commit that produced the wrong hint. A dismissal is recorded against a CARD, so a hint whose only evidence is a raw commit (no milestone card) cannot be dismissed: pass a cardId, or retire the open card itself.'),
653
+ })).max(64).optional().describe('Wrong hints to retire permanently as "not_fulfilled" edges between two CARDS — never re-suggested by claims, release, or the self-heal. A card-to-card pair is what makes the dismissal durable; a coverage hint built straight from a commit has no card to point at and will be re-listed at the next release cut until the open card is resolved or the pair is named with a cardId.'),
654
+ note: z.string().max(400).optional().describe('One line of why, echoed in the receipt.'),
636
655
  },
637
- }, async ({ canvas, root, mode }) => toContent(await opBrainReconcile({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), root, mode, log })));
656
+ }, async ({ canvas, root, mode, ref, sinceRef, confirm, dismiss, note }) => toContent(await opBrainReconcile({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), root, mode, ref, sinceRef, confirm, dismiss, note, log })));
638
657
 
639
658
  server.registerTool('brain_garden', {
640
659
  title: 'Garden the brain — consolidate over-grown areas (sleep-time compute)',
641
- description: 'Tidy an over-grown brain WITHOUT losing anything — SMART and non-invasive: it only consolidates DORMANT cards (old + peripheral), never load-bearing ones. Two phases: call it with no apply to get the areas that have accumulated forgotten cards (deterministic: >3 cards that are older than 14 days, beyond the area\'s newest 8, AND have ≤1 connection — so hubs and still-referenced decisions are left untouched; Focus/Instructions/Archive/Open-questions areas protected) plus their card text; YOU write one tight synthesis per area; then call again with apply:true, syntheses:[{title, synthesis}] AND the human\'s 8-char `approve` code (apply is REFUSED without it — you are never shown the code; the human generates it with `npx klypix-mcp garden-code` after reviewing your plan). Each area gets a 🌿 synthesis card, the originals are stamped "⤵ consolidated", moved to Archive, and arrowed to the synthesis — nothing is deleted, and one undo un-gardens. Run it when brain_insights or the brief shows an area has grown noisy.',
660
+ description: 'Tidy an over-grown brain WITHOUT losing anything — SMART and non-invasive: it only consolidates DORMANT cards (old + peripheral), never load-bearing ones. Two phases: call it with no apply to get the areas that have accumulated forgotten cards (deterministic: >3 cards that are older than 14 days, beyond the area\'s newest 8, AND have ≤1 connection — so hubs and still-referenced decisions are left untouched; Focus/Instructions/Archive/Open-questions areas protected) plus their card text; YOU write one tight synthesis per area; then call again with apply:true, syntheses:[{title, synthesis}] AND the human\'s 8-char `approve` code (apply is REFUSED without it — you are never shown the code; the human generates it with `npx klypix-mcp garden-code` after reviewing your plan). Each area gets a 🌿 synthesis card, the originals are stamped "⤵ consolidated", moved to Archive, and arrowed to the synthesis — nothing is deleted, and one undo un-gardens. Run it when brain_insights or the brief shows an area has grown noisy. SEPARATE PASS: `repair:"duplicate-partials"` lists (and with apply:true collapses) cards that carry the SAME `✔ partial` note more than once — the residue of a partial ✓ on a card that stays live by design. It keeps the earliest note of each distinct body and removes only exact repeats, so nothing is archived, nothing is deleted, no synthesis and no approval code are needed, and a second run finds nothing.',
642
661
  inputSchema: {
643
662
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
644
663
  apply: z.boolean().optional().describe('false (default) = list over-grown areas + cards to synthesize; true = consolidate using the supplied syntheses.'),
@@ -647,8 +666,9 @@ server.registerTool('brain_garden', {
647
666
  synthesis: z.string().describe('3-6 sentence prose synthesis preserving every still-relevant fact/decision/number.'),
648
667
  })).optional().describe('Required when apply:true — one entry per area you want consolidated.'),
649
668
  approve: z.string().optional().describe('Required when apply:true — the 8-char human-approval code. You are never shown it: the human runs `npx klypix-mcp garden-code` and pastes the code into chat after reviewing your plan. Never guess or fabricate it.'),
669
+ repair: z.enum(['duplicate-partials']).optional().describe('Run a targeted repair instead of the consolidation pass. "duplicate-partials" collapses repeated ✔ partial notes on a card to the earliest one — lossless, idempotent, no syntheses and no approval code. Dry-run by default; apply:true writes.'),
650
670
  },
651
- }, async ({ canvas, apply, syntheses, approve }) => toContent(await opBrainGarden({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, syntheses, approve })));
671
+ }, async ({ canvas, apply, syntheses, approve, repair }) => toContent(await opBrainGarden({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, syntheses, approve, repair })));
652
672
 
653
673
  server.registerTool('create_canvas', {
654
674
  title: 'Create a KLYPIX canvas',
@@ -684,6 +704,13 @@ server.registerTool('brain_note', {
684
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.'),
685
705
  area: z.string().optional().describe('Area/topic — routes the card into that titled container and becomes a #tag (e.g. "Auth", "Release").'),
686
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
+ evidence: z.array(z.object({
708
+ kind: z.enum(['file', 'pr', 'url', 'commit', 'run']),
709
+ 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.'),
710
+ oid: z.string().optional().describe('Optional full file blob hash from Git; a caller-supplied anchor, not proof of the claim.'),
711
+ verifiedAt: z.string().optional().describe('Optional caller-reported ISO verification date/time; not independently verified.'),
712
+ }).strict()).max(16).optional().describe('Supporting references. File bytes are fingerprinted as captured working-tree sources; hashes only detect source changes. On ~, [] clears evidence. Not accepted on resolve; use a milestone with closes to attach new evidence.'),
713
+ verify: z.string().max(2000).optional().describe('Verification instructions or command text to retain and display. Never executed by KLYPIX. On ~, an empty string clears it.'),
687
714
  guard: z.object({
688
715
  when: z.object({
689
716
  tool: z.string().max(200).optional().describe('Regex matched against the tool name (e.g. "Bash", "Edit|Write").'),
@@ -697,10 +724,10 @@ server.registerTool('brain_note', {
697
724
  }).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."),
698
725
  canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
699
726
  },
700
- }, async ({ text, marker, area, closes, guard, canvas }, extra) => {
727
+ }, async ({ text, marker, area, closes, evidence, verify, guard, canvas }, extra) => {
701
728
  // Both 1.77 and 1.78 ride this call: the enrichment question (the asker's
702
729
  // vocabulary for retrieval) AND the per-session capture receipt below.
703
- const result = await opBrainNote({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), text, area, marker: marker || '', closes, guard, via: extra.klypixClientName, enrichmentQuestion: mcpPresence.declaredIntent });
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 });
704
731
  // Per-session capture receipt — this is what stops the uncaptured-work nudge
705
732
  // from firing at a session that DID record its reasoning, just through MCP
706
733
  // rather than a 🧠 marker. The Stop hook and this server share one session-id
@@ -708,7 +735,7 @@ server.registerTool('brain_note', {
708
735
  // hook reads. Best-effort: a receipt failure must never fail the note.
709
736
  try {
710
737
  const { recordSessionCapture } = await import('../src/capture-gap.mjs');
711
- recordSessionCapture(extra?.klypixRequestIdentity?.sessionId || mcpPresence.id);
738
+ if (!result.isError) recordSessionCapture(extra?.klypixRequestIdentity?.sessionId || mcpPresence.id, undefined, Date.now(), { project: mcpPresence.vault });
712
739
  } catch { /* receipt is best-effort */ }
713
740
  return toContent(result);
714
741
  });
@@ -901,6 +928,71 @@ server.registerTool('brain_sync', {
901
928
  }
902
929
  } catch { /* observation is best-effort — never fail a sync */ }
903
930
  }
931
+ // ── Release-cut reconcile advisory (1.85.0) ─────────────────────────────────
932
+ // A release lease was just GRANTED, so this session is about to cut a build.
933
+ // The one question nobody ever asked at that moment: does anything still open
934
+ // in the brain look like it ALREADY SHIPPED in this ref? Advisory only — it
935
+ // never blocks, never writes, never joins the refusal object, and any git or
936
+ // brain failure degrades to `{ skipped }` rather than failing a sync.
937
+ //
938
+ // Recomputed only on a NEW lease or a CHANGED ref (the worker is long-lived;
939
+ // a restart rescans once, which is acceptable) so a checkpoint refresh every
940
+ // few minutes does not re-walk 500 commits.
941
+ let releaseReconcileText = '';
942
+ {
943
+ const lease = report.structured?.releaseLease;
944
+ const granted = lease && (lease.status === 'taken' || lease.status === 'refreshed');
945
+ const ref = granted ? String(lease.holder?.ref || '').trim() : '';
946
+ const laneKey = `${String(report.structured?.project || mcpPresence.vault || '').toLowerCase()}|${String(mcpPresence.id || '')}`;
947
+ if (granted && ref && lastReconcileRef.get(laneKey) !== ref) {
948
+ lastReconcileRef.set(laneKey, ref);
949
+ const projectDir = report.structured?.project || mcpPresence.vault;
950
+ const brainPath = report.structured?.brain;
951
+ try {
952
+ const { execFileSync } = await import('child_process');
953
+ const repoState = (() => { try { return collectRepoState(projectDir); } catch { return null; } })();
954
+ const sinceRef = repoState?.latestReleaseTag?.tag
955
+ || (() => {
956
+ try {
957
+ return brainFormat.readShipSignals(projectDir, (args) => execFileSync('git', String(args).split(/\s+/).filter(Boolean), {
958
+ cwd: projectDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000,
959
+ })).tag || '';
960
+ } catch { return ''; }
961
+ })();
962
+ // NO `${ref}~50` guess. On a young repo — no release-shaped tag and
963
+ // fewer than 50 commits, i.e. the FIRST release — `git log <ref>~50..`
964
+ // exits non-zero, commitsInRange reports 'bad-range', and the lease
965
+ // holder is told "git history for <ref> could not be read", which reads
966
+ // as a broken checkout when the truth is "this repo is young".
967
+ // commitsInRange already walks the ref's own tip window on an empty
968
+ // baseline, capped at the same 500 commits / 4 s either way.
969
+ const range = commitsInRange(projectDir, sinceRef, ref);
970
+ if (range.status !== 'ok' || !brainPath) {
971
+ lease.reconcile = { skipped: true, reason: range.status !== 'ok' ? (range.reason || 'git-unreadable') : 'no-brain' };
972
+ if (range.status !== 'ok') releaseReconcileText = brainFormat.releaseReconcileNotice({ ref, skipped: true });
973
+ } else {
974
+ const { struct } = await brainFormat.parseKlypix(fs.readFileSync(brainPath));
975
+ const { candidates, truncated } = brainFormat.releaseFulfilledOpens(struct, range.commits, {
976
+ ref, containedFn: makeContainmentProbe(projectDir, ref),
977
+ });
978
+ // Zero candidates → NO key at all (RL10 parity): an absent advisory
979
+ // and an empty one must not look the same to a reader.
980
+ if (candidates.length) {
981
+ lease.reconcile = {
982
+ kind: 'open-cards-likely-fulfilled-by-release', severity: 'advisory',
983
+ ref, sinceRef, commitsScanned: range.commits.length, scanCapped: range.capped,
984
+ candidates, truncated,
985
+ confirmWith: brainFormat.releaseReconcileConfirmTemplate(ref),
986
+ };
987
+ }
988
+ releaseReconcileText = brainFormat.releaseReconcileNotice({ ref, sinceRef, candidates });
989
+ }
990
+ } catch {
991
+ try { lease.reconcile = { skipped: true, reason: 'error' }; } catch { /* lease is frozen — advisory only */ }
992
+ releaseReconcileText = brainFormat.releaseReconcileNotice({ ref, skipped: true });
993
+ }
994
+ }
995
+ }
904
996
  // ── Uncaptured-work check, host-neutral half ────────────────────────────────
905
997
  // The Stop hook can REFUSE a stop; every other host has no lifecycle hook at
906
998
  // all, so brain_sync is the only place the same question can be asked. Stamp
@@ -939,32 +1031,34 @@ server.registerTool('brain_sync', {
939
1031
  if (head) gap.recordTaskBaseline(sid, { head, project: projectDir });
940
1032
  } else if (phase === 'complete' && projectDir && sid) {
941
1033
  const baseline = gap.readTaskBaseline(sid);
942
- if (baseline?.head) {
943
- // An ancestry check first: a rebase/reset makes the range meaningless,
944
- // and reporting a rewritten history as "commits you didn't record" is
945
- // exactly the cry-wolf that gets a nudge switched off.
946
- const reachable = gitOk(['merge-base', '--is-ancestor', baseline.head, 'HEAD']);
947
- const count = reachable ? Number(gitOut(['rev-list', '--count', '--no-merges', `${baseline.head}..HEAD`]) || 0) : 0;
1034
+ const sameProject = baseline?.project && path.resolve(baseline.project) === path.resolve(projectDir);
1035
+ if (sameProject && baseline?.head) {
1036
+ // A successful note checkpoints the observed HEAD only. Work committed
1037
+ // after that note belongs to a new outcome even in the same task/session.
1038
+ const receipt = gap.sessionCaptureReceipt(sid, projectDir);
1039
+ let from = baseline.head;
1040
+ if (receipt?.head && receipt.at >= baseline.at
1041
+ && gitOk(['merge-base', '--is-ancestor', baseline.head, receipt.head])
1042
+ && gitOk(['merge-base', '--is-ancestor', receipt.head, 'HEAD'])) from = receipt.head;
1043
+ const reachable = gitOk(['merge-base', '--is-ancestor', from, 'HEAD']);
1044
+ const range = from + '..HEAD';
1045
+ const count = reachable ? Number(gitOut(['rev-list', '--count', '--no-merges', range]) || 0) : 0;
948
1046
  if (count > 0) {
949
- const subjects = gitOut(['log', '--no-merges', '--format=%s', `${baseline.head}..HEAD`])
1047
+ const subjects = gitOut(['log', '--no-merges', '--format=%s', range])
950
1048
  .split('\n').map((s) => s.trim()).filter(Boolean).slice(0, 5);
951
- // Bodies decide whether ANY rationale was recorded — the same rule
952
- // the hook uses, so the two halves never disagree about one session.
953
- const withRationale = gitOut(['log', '--no-merges', '--format=%x1e%b', `${baseline.head}..HEAD`])
1049
+ const withRationale = gitOut(['log', '--no-merges', '--format=%x1e%b', range])
954
1050
  .split('\x1e').map((b) => b.replace(/\s+/g, ' ').trim()).filter((b) => b.length >= 12).length;
1051
+ const outcome = projectDir + ':' + gitOut(['rev-parse', 'HEAD']);
955
1052
  const decision = gap.captureGapDecision({
956
1053
  commitTotal: count,
957
1054
  commitCards: withRationale,
958
- sessionCaptured: gap.sessionHasCaptured(sid),
1055
+ alreadyNudged: gap.outcomeWasNudged(sid, outcome),
959
1056
  });
960
1057
  if (decision) {
961
- const changed = gitOut(['diff', '--name-only', `${baseline.head}..HEAD`]).split('\n').filter(Boolean).slice(0, 20);
962
- const draft = gap.draftCaptureMarker({
963
- commits: subjects.map((subject) => ({ subject })),
964
- filesTouched: changed,
965
- });
1058
+ const changed = gitOut(['diff', '--name-only', range]).split('\n').filter(Boolean).slice(0, 20);
1059
+ const draft = gap.draftCaptureMarker({ commits: subjects.map((subject) => ({ subject })), filesTouched: changed });
966
1060
  captureGapText = gap.captureGapReason({ ...decision, draft, mode: 'advise' });
967
- gap.recordCaptureGapNudge(sid);
1061
+ gap.recordCaptureGapNudge(sid, undefined, outcome);
968
1062
  }
969
1063
  }
970
1064
  }
@@ -1022,7 +1116,7 @@ server.registerTool('brain_sync', {
1022
1116
  return {
1023
1117
  content: [{
1024
1118
  type: 'text',
1025
- text: [report.text, harnessText, shipNotice, captureGapText, contextText, timingText].filter(Boolean).join('\n\n'),
1119
+ text: [report.text, harnessText, shipNotice, releaseReconcileText, captureGapText, contextText, timingText].filter(Boolean).join('\n\n'),
1026
1120
  }],
1027
1121
  structuredContent,
1028
1122
  ...(report.isError ? { isError: true } : {}),
@@ -1031,7 +1125,7 @@ server.registerTool('brain_sync', {
1031
1125
 
1032
1126
  server.registerTool('brain_doctor', {
1033
1127
  title: 'Brain doctor — is this brain current, wired, and in sync?',
1034
- description: 'Read-only self-check of the installed klypix brain, as ONE verdict: VERSION (deployed brain-core + optional npm currency), CLAUDE (existing 5-hook capture readiness), CODEX (automatic MCP presence plus optional enhanced-hook status), TOOLS (discoverable MCP verbs), SESSIONS (all active presence-adapter sessions across hosts, never recent-chat history), and HARNESS (projection drift). Use to answer "is my brain current, correctly installed, in sync, and who is actually live?" without file-spelunking. Never writes. SCOPE: only CLAUDE and CODEX get behavioural verdicts. HARNESS classifies the projected config/rules FILES on disk — a project can read fully ok while no other host has ever actually loaded them, so do not report a clean HARNESS as "Cursor/Cline/Windsurf/Copilot is working". The MCP-callable twin of `npx klypix-mcp doctor`.',
1128
+ description: 'Read-only self-check of the installed klypix brain, as ONE verdict: VERSION (deployed brain-core + optional npm currency), CLAUDE (existing 5-hook capture readiness), CODEX (automatic MCP presence plus optional enhanced-hook status), TOOLS (discoverable MCP verbs), SESSIONS (all active presence-adapter sessions across hosts, never recent-chat history), and HARNESS (projection drift). Use to answer "is my brain current, correctly installed, in sync, and who is actually live?" without file-spelunking. Never writes: the only side effects are read-only subprocess queries (git rev-parse / tag --list / log / merge-base with fixed argument arrays, and `npm view` only when check_npm is true) — it creates, edits, and deletes nothing. SCOPE: only CLAUDE and CODEX get behavioural verdicts. HARNESS classifies the projected config/rules FILES on disk — a project can read fully ok while no other host has ever actually loaded them, so do not report a clean HARNESS as "Cursor/Cline/Windsurf/Copilot is working". The MCP-callable twin of `npx klypix-mcp doctor`.',
1035
1129
  inputSchema: {
1036
1130
  project: z.string().optional().describe('Project dir to audit harness + peers for. Defaults to the server\'s working directory.'),
1037
1131
  check_npm: z.boolean().optional().describe('Also fetch npm latest to flag a stale brain (default false — this one does a network `npm view`).'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.83.0",
3
+ "version": "1.85.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,10 +84,11 @@
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/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/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/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-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",
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",
88
88
  "test:memory": "node test/memory-runtime.mjs",
89
89
  "test:memory:soak": "node --expose-gc test/memory-soak.mjs",
90
- "runtime": "node bin/klypix-runtime.mjs"
90
+ "runtime": "node bin/klypix-runtime.mjs",
91
+ "eval:retrieval": "node scripts/eval-retrieval.mjs"
91
92
  },
92
93
  "dependencies": {
93
94
  "@modelcontextprotocol/ext-apps": "^1.7.5",
@@ -766,6 +766,11 @@ export function upsertSession({
766
766
  identitySource = null,
767
767
  aliases,
768
768
  home,
769
+ // Status-digest dedup hash (1.85.0): sha1[0:12] of the last computed status
770
+ // digest this session was shown, so a repeat status prompt gets a one-line
771
+ // pointer instead of the same ~5k chars. ADDITIVE — written only when a
772
+ // writer passes it; kept verbatim by every other touch (…previous spread).
773
+ statusDigestHash,
769
774
  now = Date.now(),
770
775
  }) {
771
776
  if (!brainPath || !id) return withWriteVerdict([], false, 'no-brain-or-id');
@@ -926,6 +931,7 @@ export function upsertSession({
926
931
  ...(previous.scopeStartedAt ? { scopeStartedAt: previous.scopeStartedAt } : {}),
927
932
  ...(taskCompleted ? { completedAt: now } : (previous.completedAt ? { completedAt: previous.completedAt } : {})),
928
933
  }),
934
+ ...(statusDigestHash !== undefined ? { statusDigestHash: String(statusDigestHash || '').slice(0, 16) } : {}),
929
935
  lastSeen: now,
930
936
  };
931
937
  const kept = sessions.filter((session) => session.id !== id);