klypix-mcp 1.84.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/README.md +42 -27
- package/bin/klypix-install.mjs +5 -1
- package/bin/klypix-worker.mjs +94 -9
- package/package.json +2 -2
- package/src/agent-presence.mjs +6 -0
- package/src/codex-brain-hook.mjs +60 -1
- package/src/global-brain-hook.mjs +131 -9
- package/src/klypix-core.mjs +318 -6
- package/src/klypix-format.mjs +1183 -101
- package/src/repo-state.mjs +75 -0
package/README.md
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
# Every project gets a brain.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+

|
|
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
|
[](#supported-hosts-and-their-integration-level)
|
|
14
28
|
[](#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
|
-

|
|
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
|
|
38
|
-
|
|
38
|
+
router, a worktree manager, or a replacement for Git. It holds what the project currently believes.
|
|
39
|
+
|
|
40
|
+
[](https://github.com/dahshanlabs/klypix-mcp/actions/workflows/ci.yml)
|
|
41
|
+
[](https://www.npmjs.com/package/klypix-mcp)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](package.json)
|
|
44
|
+
[](https://modelcontextprotocol.io)
|
|
45
|
+
[](https://glama.ai/mcp/servers/dahshanlabs/klypix-mcp)
|
|
46
|
+
[](BENCHMARKS.md)
|
|
39
47
|
|
|
40
48
|
## See the shared project brain in action
|
|
41
49
|
|
|
@@ -587,10 +595,10 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
587
595
|
| `brain_ask` | Whole-brain question answering — correction-aware, `as_of` time travel |
|
|
588
596
|
| `brain_challenge` | The brain argues back: contradictions with receipts, tried-and-reversed chains, standing rules, other-agent provenance flags |
|
|
589
597
|
| `brain_note` | Capture with the full lifecycle — supersede / re-adopt / ✓ resolve / ~ update / 🛠 skill / `closes:` |
|
|
590
|
-
| `brain_reconcile` | Proposes stale-vs-correction pairs
|
|
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 |
|
|
591
599
|
| `brain_insights` | Hubs, orphaned decisions, stale questions, area sizes |
|
|
592
600
|
| `brain_lens` | Machine-readable freshness, provenance, activity, timeline, orrery and unresolved views |
|
|
593
|
-
| `brain_garden` | Maintenance pass — proposes first
|
|
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) |
|
|
594
602
|
| `brain_doctor` | Self-diagnosis: version, core/enhanced host adapters, active sessions, tool count, projection drift |
|
|
595
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) |
|
|
596
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 |
|
|
@@ -613,8 +621,15 @@ Exactly 22, machine-verifiable with `npx klypix-mcp doctor`.
|
|
|
613
621
|
> screenshot and no host-level test. Hosts without the extension get clean text, which is the path
|
|
614
622
|
> that is actually verified.
|
|
615
623
|
|
|
616
|
-
`brain_doctor`, `brain_lens
|
|
617
|
-
|
|
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.
|
|
618
633
|
`npx klypix-mcp doctor` gives one verdict and exits non-zero on drift, so it doubles as a CI gate.
|
|
619
634
|
|
|
620
635
|
## One file you can hold
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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,
|
|
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',
|
|
@@ -908,6 +928,71 @@ server.registerTool('brain_sync', {
|
|
|
908
928
|
}
|
|
909
929
|
} catch { /* observation is best-effort — never fail a sync */ }
|
|
910
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
|
+
}
|
|
911
996
|
// ── Uncaptured-work check, host-neutral half ────────────────────────────────
|
|
912
997
|
// The Stop hook can REFUSE a stop; every other host has no lifecycle hook at
|
|
913
998
|
// all, so brain_sync is the only place the same question can be asked. Stamp
|
|
@@ -1031,7 +1116,7 @@ server.registerTool('brain_sync', {
|
|
|
1031
1116
|
return {
|
|
1032
1117
|
content: [{
|
|
1033
1118
|
type: 'text',
|
|
1034
|
-
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'),
|
|
1035
1120
|
}],
|
|
1036
1121
|
structuredContent,
|
|
1037
1122
|
...(report.isError ? { isError: true } : {}),
|
|
@@ -1040,7 +1125,7 @@ server.registerTool('brain_sync', {
|
|
|
1040
1125
|
|
|
1041
1126
|
server.registerTool('brain_doctor', {
|
|
1042
1127
|
title: 'Brain doctor — is this brain current, wired, and in sync?',
|
|
1043
|
-
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`.',
|
|
1044
1129
|
inputSchema: {
|
|
1045
1130
|
project: z.string().optional().describe('Project dir to audit harness + peers for. Defaults to the server\'s working directory.'),
|
|
1046
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.
|
|
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,7 +84,7 @@
|
|
|
84
84
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
85
85
|
"test:bench": "node test/bench.mjs",
|
|
86
86
|
"pretest": "node test/publish-workflow.mjs",
|
|
87
|
-
"test": "node test/publish-verdict.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/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-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",
|
|
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
90
|
"runtime": "node bin/klypix-runtime.mjs",
|
package/src/agent-presence.mjs
CHANGED
|
@@ -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);
|
package/src/codex-brain-hook.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Codex lifecycle adapter for the agent-neutral KLYPIX presence lane.
|
|
3
3
|
// It never reads Codex's unstable transcript format and never modifies the brain.
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
+
import crypto from 'crypto';
|
|
5
6
|
import fs from 'fs';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import {
|
|
@@ -310,6 +311,61 @@ function queueConflictAlerts({ brainPath, sessionId, intent, conflicts, turnId,
|
|
|
310
311
|
return queued;
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
// ── Status digest, Codex lane (1.85.0) ───────────────────────────────────────
|
|
315
|
+
// Parity with the Claude hook's T8 injection: a STRONG status-shaped prompt
|
|
316
|
+
// ("do i need to update the desk? ios? or web?", "ما تبقى") gets the engine's
|
|
317
|
+
// COMPUTED current-state digest INSTEAD of card retrieval — same detector
|
|
318
|
+
// (splitQueryTokens), same area scoping (areaHintsFromPrompt, resolved only
|
|
319
|
+
// once the struct is loaded), same renderer and header, and the same
|
|
320
|
+
// per-session sha1 dedup (a repeat gets a one-line pointer). Every engine
|
|
321
|
+
// export is typeof-guarded so a stale bundle degrades to the task-context
|
|
322
|
+
// path below, never a throw. Returns null when the prompt is not a status
|
|
323
|
+
// question — the caller then runs today's retrieval unchanged.
|
|
324
|
+
async function statusDigestContext({ brainPath, sessionId, prompt, sessions, cwd }) {
|
|
325
|
+
const lib = brainFormat;
|
|
326
|
+
if (!prompt || typeof lib.splitQueryTokens !== 'function' || typeof lib.areaStatusDigest !== 'function'
|
|
327
|
+
|| typeof lib.parseKlypix !== 'function') return null;
|
|
328
|
+
let sp = null;
|
|
329
|
+
try { sp = lib.splitQueryTokens(String(prompt)); } catch { return null; }
|
|
330
|
+
if (!sp || !sp.strong) return null;
|
|
331
|
+
let struct = null;
|
|
332
|
+
try { ({ struct } = await lib.parseKlypix(fs.readFileSync(brainPath))); } catch { return null; }
|
|
333
|
+
if (!struct) return null;
|
|
334
|
+
let areas = null;
|
|
335
|
+
if (Array.isArray(sp.areaFamilies) && sp.areaFamilies.length && typeof lib.areaHintsFromPrompt === 'function') {
|
|
336
|
+
try { areas = lib.areaHintsFromPrompt(struct, String(prompt)); } catch { areas = null; }
|
|
337
|
+
}
|
|
338
|
+
// ONE count (1.85.0): the same per-brain summary cache the Claude lane
|
|
339
|
+
// reads (keyed on brain mtime + size, written by whichever lane misses
|
|
340
|
+
// first), so both hosts print identical header numbers for one brain and a
|
|
341
|
+
// status prompt pays ~0 for the detector. typeof-guarded: an older engine
|
|
342
|
+
// computes nothing here and the renderers derive the summary themselves.
|
|
343
|
+
let summary = null;
|
|
344
|
+
try {
|
|
345
|
+
if (typeof lib.cachedOpenStatusSummary === 'function') summary = lib.cachedOpenStatusSummary(struct, { brainPath }).summary;
|
|
346
|
+
else if (typeof lib.openStatusSummary === 'function') summary = lib.openStatusSummary(struct);
|
|
347
|
+
} catch { summary = null; }
|
|
348
|
+
let digest = [];
|
|
349
|
+
try { digest = lib.areaStatusDigest(struct, { maxAreas: 12, areas, summary }); } catch { return null; }
|
|
350
|
+
if (!Array.isArray(digest) || !digest.length) return null;
|
|
351
|
+
let body = null;
|
|
352
|
+
if (typeof lib.statusContextToMarkdown === 'function') {
|
|
353
|
+
try {
|
|
354
|
+
const md = lib.statusContextToMarkdown(struct, { budgetChars: 5200, areas, summary });
|
|
355
|
+
// Drop its own H1; the hook's stronger header replaces it (Claude-lane parity).
|
|
356
|
+
if (md && md.trim()) body = md.split('\n').slice(1).join('\n').trimEnd();
|
|
357
|
+
} catch { body = null; }
|
|
358
|
+
}
|
|
359
|
+
const content = body || digest.join('\n');
|
|
360
|
+
const h = crypto.createHash('sha1').update(content).digest('hex').slice(0, 12);
|
|
361
|
+
const me = (Array.isArray(sessions) ? sessions : []).find((session) => session.id === sessionId);
|
|
362
|
+
if (me && me.statusDigestHash === h) {
|
|
363
|
+
return '## 📊 Current state — unchanged since the digest shown earlier this session (`brain_ask` gives the full computed status view).';
|
|
364
|
+
}
|
|
365
|
+
try { upsertSession({ brainPath, id: sessionId, client: 'codex', cwd, statusDigestHash: h }); } catch { /* best-effort */ }
|
|
366
|
+
return ['## 📊 Computed current state (status-shaped question detected — answer from THIS + `brain_ask`, never from memory of past sessions)', content].join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
313
369
|
async function compactTaskContext(projectDir, prompt, files) {
|
|
314
370
|
if (!prompt) return '';
|
|
315
371
|
try {
|
|
@@ -430,7 +486,10 @@ async function main() {
|
|
|
430
486
|
}
|
|
431
487
|
if (event === 'UserPromptSubmit') {
|
|
432
488
|
const me = sessions.find((session) => session.id === sessionId);
|
|
433
|
-
|
|
489
|
+
// A STRONG status question is answered from computed state and REPLACES
|
|
490
|
+
// card retrieval (never additive) — the Claude hook's `freshHits = []`.
|
|
491
|
+
const statusMd = await statusDigestContext({ brainPath, sessionId, prompt, sessions, cwd });
|
|
492
|
+
const context = statusMd ?? await compactTaskContext(projectDir, prompt,
|
|
434
493
|
[...new Set([...(me?.files || []), ...(me?.observedFiles || [])])]);
|
|
435
494
|
emitSystemMessage([
|
|
436
495
|
context,
|