klypix-mcp 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,9 @@ Claude Code keeps its live hooks for auto-brief + auto-capture. Codex gets nativ
20
20
  tools, **automatic live-session presence**, and approval-free **smart task synchronization**
21
21
  from the authorized MCP connection itself. `brain_sync` is a bounded **Context Gateway**:
22
22
  one call returns task-relevant brain memory, meaningful active-task peers, structured
23
- exact-file conflicts, one-time notes, and automatic late-arrival overlap alerts. Idle MCP
23
+ exact-file conflicts, one-time notes, and automatic late-arrival overlap alerts. The
24
+ earlier session gets a best-effort live MCP logging notification within seconds, while
25
+ the same alert remains guaranteed on its next KLYPIX action if its host hides logs. Idle MCP
24
26
  connections stay counted for diagnostics but are hidden from the task-peer list. MCP
25
27
  server instructions plus a managed Codex `AGENTS.md` block teach Codex to call it at task
26
28
  start, when scope changes, and on completion—no Codex hook approval required. Run inside a
@@ -35,8 +37,19 @@ npx klypix-mcp install --codex-hooks
35
37
  ```
36
38
 
37
39
  Codex owns this native layer's security decision and asks the user to review/trust the
38
- hooks on surfaces that expose hook management. `brain_doctor` reports it separately as
39
- off, execution-unverified, or active; the approval-free smart layer remains active either way.
40
+ hooks on surfaces that expose hook management. Once trusted, the enhanced layer
41
+ automatically injects bounded task-ranked brain memory on every prompt and warns before
42
+ an exact overlapping file edit. `brain_doctor` reports it separately as off,
43
+ execution-unverified, or active; the approval-free smart layer remains active either way.
44
+
45
+ Prove the exact installed runtime with two isolated real MCP clients:
46
+
47
+ ```bash
48
+ npx klypix-mcp conformance
49
+ ```
50
+
51
+ The test touches only a temporary fixture. It verifies tool discovery, task memory,
52
+ truthful peers, blocking overlap detection, proactive logging, and guaranteed in-band delivery.
40
53
 
41
54
  Give a project a brain by dropping a `brain.klypix` in it — the
42
55
  [KLYPIX app](https://klypix.com) does it in one click (*Save canvas as project brain*),
@@ -91,7 +104,7 @@ Any MCP client gets the tools and connection-lifecycle presence without host hoo
91
104
  If the vault contains `brain.klypix`, that MCP connection also appears as an active
92
105
  session. `brain_sync` adds compact task memory, expected-file coordination, task-only peer
93
106
  reporting, structured exact-overlap warnings, automatic conflict alerts, timing receipts,
94
- and one-time peer-note delivery. Then ask your
107
+ best-effort proactive notifications, and guaranteed one-time peer-note delivery. Then ask your
95
108
  agent things like *"what did we decide about auth?"*, *"challenge this: let's switch
96
109
  to polling"*, or *"turn these notes into a board."*
97
110
 
@@ -132,7 +145,7 @@ it, enrich it with intent/files, and remove only their own channel. The TTL cove
132
145
 
133
146
  Future hosts get baseline support merely by connecting the MCP server. A deeper adapter
134
147
  can import `klypix-mcp/presence` and map lifecycle events to `upsertSession`,
135
- `removeSession`, and `receiveMessages`. The shared contract accepts `id`, `client`,
148
+ `removeSession`, `peekMessages`, and `receiveMessages`. The shared contract accepts `id`, `client`,
136
149
  `surface`, `model`, `branch`, `intent`, touched `files`, and adapter `channel`;
137
150
  host-specific transcript parsing stays outside the protocol.
138
151
 
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ // Self-contained KLYPIX MCP conformance test.
3
+ //
4
+ // Launches two real stdio clients against this exact installed server build,
5
+ // using a temporary brain + isolated presence registry. It verifies the
6
+ // Context Gateway, task memory, truthful task counts, exact overlap detection,
7
+ // proactive MCP logging, and guaranteed next-action delivery without touching
8
+ // the user's project or durable brain.
9
+ import fs from 'fs';
10
+ import os from 'os';
11
+ import path from 'path';
12
+ import { createRequire } from 'module';
13
+ import { fileURLToPath } from 'url';
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
+ import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
17
+ import { buildKlypixMap } from '../src/klypix-core.mjs';
18
+
19
+ const here = path.dirname(fileURLToPath(import.meta.url));
20
+ const jsonMode = process.argv.includes('--json');
21
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
+ const PKG_VERSION = (() => {
23
+ try { return createRequire(import.meta.url)('../package.json').version; }
24
+ catch {
25
+ try {
26
+ const stamp = JSON.parse(fs.readFileSync(path.join(here, '.brain-version.json'), 'utf8'));
27
+ return stamp.brainVersion || stamp.appVersion || 'deployed';
28
+ } catch { return 'deployed'; }
29
+ }
30
+ })();
31
+ const serverPath = [
32
+ path.join(here, 'klypix-mcp.mjs'),
33
+ path.join(here, 'klypix-mcp-server.mjs'),
34
+ ].find((candidate) => fs.existsSync(candidate));
35
+
36
+ if (!serverPath) {
37
+ console.error('KLYPIX conformance failed: the MCP server executable is missing beside this command.');
38
+ process.exit(1);
39
+ }
40
+
41
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'klypix-conformance-'));
42
+ const vault = path.join(tempRoot, 'project');
43
+ const isolatedHome = path.join(tempRoot, 'home');
44
+ fs.mkdirSync(vault, { recursive: true });
45
+ fs.mkdirSync(isolatedHome, { recursive: true });
46
+ fs.writeFileSync(path.join(vault, 'brain.klypix'), await buildKlypixMap({
47
+ title: 'KLYPIX conformance brain',
48
+ kind: 'brain',
49
+ areas: [{
50
+ title: 'Brain',
51
+ cards: [{
52
+ text: 'Codex Context Gateway automatically coordinates active tasks and exact file overlaps across MCP sessions.',
53
+ }],
54
+ }],
55
+ }));
56
+
57
+ const baseEnv = Object.fromEntries(
58
+ Object.entries(process.env).filter(([, value]) => typeof value === 'string'),
59
+ );
60
+
61
+ async function connect(name, sessionId) {
62
+ const logs = [];
63
+ const transport = new StdioClientTransport({
64
+ command: process.execPath,
65
+ args: [serverPath, '--vault', vault],
66
+ env: {
67
+ ...baseEnv,
68
+ USERPROFILE: isolatedHome,
69
+ HOME: isolatedHome,
70
+ KLYPIX_SESSION_ID: sessionId,
71
+ KLYPIX_MCP_INBOX_POLL_MS: '250',
72
+ KLYPIX_RERANK: '0',
73
+ },
74
+ stderr: 'pipe',
75
+ });
76
+ const client = new Client({ name, version: '1.0.0' });
77
+ client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
78
+ logs.push(String(notification?.params?.data || ''));
79
+ });
80
+ await client.connect(transport);
81
+ await client.setLoggingLevel('info');
82
+ return { client, transport, logs };
83
+ }
84
+
85
+ const checks = {};
86
+ const metrics = {};
87
+ let a;
88
+ let b;
89
+ try {
90
+ a = await connect('codex-klypix-conformance-a', 'klypix-conformance-a');
91
+ b = await connect('codex-klypix-conformance-b', 'klypix-conformance-b');
92
+ const tools = await a.client.listTools();
93
+ checks.brainSyncDiscoverable = tools.tools?.some((tool) => tool.name === 'brain_sync') === true;
94
+
95
+ const aStart = await a.client.callTool({
96
+ name: 'brain_sync',
97
+ arguments: {
98
+ phase: 'start',
99
+ intent: 'Validate automatic Codex Context Gateway coordination',
100
+ files: ['src/conformance-overlap.ts'],
101
+ },
102
+ });
103
+ const bStart = await b.client.callTool({
104
+ name: 'brain_sync',
105
+ arguments: {
106
+ phase: 'start',
107
+ intent: 'Validate proactive overlap delivery from a second session',
108
+ files: ['src/conformance-overlap.ts'],
109
+ },
110
+ });
111
+ const aStructured = aStart.structuredContent || {};
112
+ const bStructured = bStart.structuredContent || {};
113
+ checks.taskMemory = Array.isArray(aStructured.context?.hits) && aStructured.context.hits.length > 0;
114
+ checks.truthfulTaskCount = bStructured.counts?.activeTasks === 2
115
+ && bStructured.peers?.length === 1;
116
+ checks.exactBlockingOverlap = bStructured.conflicts?.length === 1
117
+ && bStructured.conflicts[0]?.kind === 'exact-file'
118
+ && bStructured.conflicts[0]?.severity === 'blocking';
119
+ checks.alertQueued = bStructured.alertsQueued?.length === 1;
120
+ metrics.firstClientMs = aStructured.timingMs?.total ?? null;
121
+ metrics.secondClientMs = bStructured.timingMs?.total ?? null;
122
+
123
+ const pushDeadline = Date.now() + 4_000;
124
+ while (Date.now() < pushDeadline
125
+ && !a.logs.some((line) => line.includes('Automatic KLYPIX overlap alert'))) {
126
+ await wait(100);
127
+ }
128
+ checks.proactiveLogging = a.logs.some((line) =>
129
+ line.includes('Automatic KLYPIX overlap alert'));
130
+ metrics.firstClientLogCount = a.logs.length;
131
+ if (!checks.proactiveLogging && a.logs.length) metrics.firstClientLogs = a.logs.slice(-4);
132
+
133
+ const aCheckpoint = await a.client.callTool({
134
+ name: 'brain_sync',
135
+ arguments: {
136
+ phase: 'checkpoint',
137
+ intent: 'Validate automatic Codex Context Gateway coordination',
138
+ files: ['src/conformance-overlap.ts'],
139
+ },
140
+ });
141
+ const checkpointStructured = aCheckpoint.structuredContent || {};
142
+ checks.guaranteedInBandDelivery = checkpointStructured.messages?.some((message) =>
143
+ String(message.text).includes('Automatic KLYPIX overlap alert')) === true;
144
+
145
+ await Promise.all([
146
+ a.client.callTool({ name: 'brain_sync', arguments: { phase: 'complete' } }),
147
+ b.client.callTool({ name: 'brain_sync', arguments: { phase: 'complete' } }),
148
+ ]);
149
+ } catch (error) {
150
+ checks.runtime = false;
151
+ checks.error = error?.message || String(error);
152
+ } finally {
153
+ await Promise.allSettled([a?.client?.close(), b?.client?.close()]);
154
+ fs.rmSync(tempRoot, { recursive: true, force: true });
155
+ }
156
+
157
+ const required = [
158
+ 'brainSyncDiscoverable',
159
+ 'taskMemory',
160
+ 'truthfulTaskCount',
161
+ 'exactBlockingOverlap',
162
+ 'alertQueued',
163
+ 'proactiveLogging',
164
+ 'guaranteedInBandDelivery',
165
+ ];
166
+ const ok = required.every((name) => checks[name] === true);
167
+ const result = {
168
+ ok,
169
+ packageVersion: PKG_VERSION,
170
+ checks,
171
+ metrics,
172
+ contract: {
173
+ proactive: 'best-effort MCP logging notification',
174
+ guaranteed: 'same alert on the next KLYPIX action',
175
+ },
176
+ };
177
+
178
+ if (jsonMode) {
179
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
180
+ } else {
181
+ console.log(`KLYPIX MCP conformance v${PKG_VERSION}: ${ok ? 'PASS' : 'FAIL'}`);
182
+ for (const name of required) {
183
+ console.log(` ${checks[name] ? '✓' : '✗'} ${name}`);
184
+ }
185
+ if (checks.error) console.log(` error: ${checks.error}`);
186
+ console.log(` memory/coordination: ${metrics.firstClientMs ?? '?'}ms / ${metrics.secondClientMs ?? '?'}ms`);
187
+ console.log(' proactive notifications are best-effort; next-action delivery is guaranteed.');
188
+ }
189
+ process.exit(ok ? 0 : 1);
@@ -276,7 +276,11 @@ try {
276
276
  for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
277
277
  const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
278
278
  }
279
- for (const [src, dst] of [['klypix-mcp.mjs', 'klypix-mcp-server.mjs'], ['klypix-a2a.mjs', 'klypix-a2a-server.mjs']]) {
279
+ for (const [src, dst] of [
280
+ ['klypix-mcp.mjs', 'klypix-mcp-server.mjs'],
281
+ ['klypix-a2a.mjs', 'klypix-a2a-server.mjs'],
282
+ ['klypix-conformance.mjs', 'klypix-conformance.mjs'],
283
+ ]) {
280
284
  const s = path.join(BIN, src); if (exists(s)) staged.push({ dst, content: flatten(fs.readFileSync(s, 'utf8')) });
281
285
  }
282
286
  for (const st of staged) fs.writeFileSync(path.join(BRAIN_DIR, st.dst + '.klypix-new'), st.content);
@@ -344,10 +348,10 @@ try {
344
348
  else console.error(`⚠ readiness: ${notWired.length} hook(s) did NOT take (${notWired.join(', ')}) — the brain will read but not capture/sync. Re-run \`npx klypix-mcp install --force\` or check ${SETTINGS}.`);
345
349
  console.log(`✓ MCP server runs from the local bundle (node ${fwd(path.join(BRAIN_DIR, 'klypix-mcp-server.mjs'))}) — no npx cache, works offline, always the installed version.`);
346
350
  if (migrated) console.log(`✓ migrated ${migrated.file} klypix-canvas server: ${migrated.from} → ${migrated.to} (backup: .mcp.json.klypix-bak). Reconnect (/mcp) or restart to pick it up.`);
347
- console.log(' Claude Code keeps its existing auto-brief/capture hooks; Codex gets the brain_sync Context Gateway (compact task memory + clean peers + automatic conflict alerts) with no hook trust prompt.');
348
- console.log(' Optional mechanical Codex lifecycle capture: re-run with `--codex-hooks`, then approve/review KLYPIX in a Codex surface that supports hook trust.');
351
+ console.log(' Claude Code keeps its existing auto-brief/capture hooks; Codex gets the brain_sync Context Gateway (compact task memory + clean peers + proactive/guaranteed conflict alerts) with no hook trust prompt.');
352
+ console.log(' Enhanced Codex auto-context + pre-edit overlap guard: re-run with `--codex-hooks`, then approve/review KLYPIX once in a Codex surface that supports hook trust.');
349
353
  console.log(' ⚠ Open sessions keep their OLD server until relaunched: fully quit & reopen the app (a session resume / new chat does NOT respawn the MCP server). `brain_doctor`\'s RUNNING line confirms when you\'re current.');
350
- console.log(' Verify anytime: `npx klypix-mcp doctor` (is the brain current + wired + in sync, who else is live).');
354
+ console.log(' Verify anytime: `npx klypix-mcp doctor`; prove two-client behavior with `npx klypix-mcp conformance`.');
351
355
  } catch (e) {
352
356
  if (gotLock) releaseLock();
353
357
  console.error(`✗ install failed: ${e?.message || e}`);
@@ -57,6 +57,11 @@ if (process.argv[2] === 'link') { await import('./klypix-link.mjs'); process.exi
57
57
  // projection in sync? One verdict, one reconcile block. Exits 1 on drift (CI gate).
58
58
  if (process.argv[2] === 'doctor') { await import('./klypix-doctor.mjs'); process.exit(0); }
59
59
 
60
+ // `npx klypix-mcp conformance` — launch two real, isolated MCP clients against
61
+ // this exact installed server and verify task memory, truthful peers, blocking
62
+ // overlap detection, proactive logging, and guaranteed next-action delivery.
63
+ if (process.argv[2] === 'conformance') { await import('./klypix-conformance.mjs'); process.exit(0); }
64
+
60
65
  // `npx klypix-mcp garden-code` — the HUMAN half of the garden approval gate.
61
66
  // brain_garden's apply requires an 8-char code derived from the exact dormant
62
67
  // candidate set + day; the agent is deliberately never shown it. The human runs
@@ -106,7 +111,10 @@ const vaultArgIdx = process.argv.indexOf('--vault');
106
111
  const VAULT = resolveVault(vaultArgIdx >= 0 ? process.argv[vaultArgIdx + 1] : undefined);
107
112
  const server = new McpServer(
108
113
  { name: 'klypix-canvas', version: PKG_VERSION },
109
- { instructions: KLYPIX_MCP_INSTRUCTIONS },
114
+ {
115
+ instructions: KLYPIX_MCP_INSTRUCTIONS,
116
+ capabilities: { logging: {} },
117
+ },
110
118
  );
111
119
  const mcpPresence = createMcpPresence({ server, initialVault: VAULT });
112
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Every project gets a brain — one open .klypix file your AI agents read, write, and argue from, over MCP. Works with Claude, Codex, Cursor, Cline, any model.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "bin": {
28
28
  "klypix-mcp": "bin/klypix-mcp.mjs",
29
+ "klypix-conformance": "bin/klypix-conformance.mjs",
29
30
  "klypix-link": "bin/klypix-link.mjs",
30
31
  "klypix-doctor": "bin/klypix-doctor.mjs",
31
32
  "klypix-a2a": "bin/klypix-a2a.mjs",
@@ -55,7 +56,7 @@
55
56
  "node": ">=18"
56
57
  },
57
58
  "scripts": {
58
- "test": "node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.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/canvas-view.mjs && node test/status-completeness.mjs"
59
+ "test": "node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.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/canvas-view.mjs && node test/status-completeness.mjs"
59
60
  },
60
61
  "dependencies": {
61
62
  "@modelcontextprotocol/ext-apps": "^1.7.4",
@@ -292,6 +292,39 @@ export function receiveMessages({
292
292
  }
293
293
  }
294
294
 
295
+ // Non-destructive inbox preview for MCP logging notifications. Unlike
296
+ // receiveMessages(), this NEVER appends the session id to message.seen, so a
297
+ // host that ignores notifications cannot make a coordination warning vanish.
298
+ // The next KLYPIX tool result or lifecycle hook still receives + acknowledges
299
+ // the same message through the guaranteed in-band path.
300
+ export function peekMessages({
301
+ brainPath,
302
+ sessionId,
303
+ ignoreTexts = [],
304
+ home,
305
+ now = Date.now(),
306
+ }) {
307
+ if (!brainPath || !sessionId) return [];
308
+ const data = readLane(laneFileFor(brainPath, home));
309
+ const sessions = pruneSessions(data.sessions, now);
310
+ const messages = pruneMessages(data.messages, now);
311
+ const me = sessions.find((session) => session.id === sessionId);
312
+ const ignored = new Set(ignoreTexts.map((value) =>
313
+ String(value || '').replace(/\s+/g, ' ').trim().toLowerCase()));
314
+ const shown = [];
315
+ const seenText = new Set();
316
+ for (const message of messages) {
317
+ if (message.from === sessionId
318
+ || (Array.isArray(message.seen) && message.seen.includes(sessionId))
319
+ || !messageTargetsSession(message, me, sessionId)) continue;
320
+ const key = String(message.text || '').replace(/\s+/g, ' ').trim().toLowerCase();
321
+ if (!key || ignored.has(key) || seenText.has(key)) continue;
322
+ seenText.add(key);
323
+ shown.push(message);
324
+ }
325
+ return shown.slice(0, 6);
326
+ }
327
+
295
328
  // Queue a one-time coordination message directly on the shared presence lane.
296
329
  // `dedupeKey` makes machine-generated alerts (for example, exact file overlap)
297
330
  // idempotent without weakening deliberate brain_message notes.
@@ -290,7 +290,7 @@ export function render(r, opts = {}) {
290
290
  else L.push(`${hmark} ${c.bold}CLAUDE${c.rst} existing 4-hook capture path intact: ${r.hooks.wired.join(', ')}`);
291
291
  const chmark = r.layers.codexHooks === 'warning' ? warn : ok;
292
292
  const smart = r.codexSmart?.globalInstructions
293
- ? 'approval-free Context Gateway active (task memory + clean peers + auto-alerts)'
293
+ ? 'approval-free Context Gateway active (task memory + clean peers + proactive/guaranteed alerts)'
294
294
  : 'approval-free brain_sync Context Gateway available through MCP';
295
295
  if (r.codexHooks.error) {
296
296
  L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart}; optional native-hook config unreadable: ${c.yel}${r.codexHooks.error}${c.rst}`);
@@ -298,9 +298,9 @@ export function render(r, opts = {}) {
298
298
  L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native hooks off ${c.dim}(optional)${c.rst}`);
299
299
  } else if (r.codexHooks.executionStatus !== 'observed') {
300
300
  L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · ${c.yel}native hooks configured but execution not verified${c.rst}`);
301
- L.push(` ${c.dim}Context Gateway memory/coordination already works. Native hooks add mechanical lifecycle capture on Codex surfaces that support trust review.${c.rst}`);
301
+ L.push(` ${c.dim}Context Gateway memory/coordination already works. Once trusted, native hooks auto-inject task memory and warn before exact overlapping edits.${c.rst}`);
302
302
  } else {
303
- L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native hooks active ${c.dim}(last observed ${r.codexHooks.lastExecutedAt})${c.rst}`);
303
+ L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native auto-context + pre-edit guard active ${c.dim}(last observed ${r.codexHooks.lastExecutedAt})${c.rst}`);
304
304
  }
305
305
 
306
306
  // TOOLS
@@ -8,11 +8,14 @@ import {
8
8
  findProjectBrain,
9
9
  formatPresenceMessage,
10
10
  formatReceivedMessages,
11
+ postPresenceMessage,
11
12
  receiveMessages,
12
13
  removeSession,
13
14
  upsertSession,
14
15
  } from './agent-presence.mjs';
15
16
  import { recordCodexHookExecution } from './codex-hooks.mjs';
17
+ import { opBrainTaskContext } from './klypix-core.mjs';
18
+ import { findPresenceConflicts } from './mcp-presence.mjs';
16
19
 
17
20
  function readInput() {
18
21
  try {
@@ -84,6 +87,53 @@ function emitSystemMessage(parts) {
84
87
  process.stdout.write(JSON.stringify({ continue: true, systemMessage }));
85
88
  }
86
89
 
90
+ function formatConflictWarning(conflicts, event) {
91
+ if (!conflicts.length) return '';
92
+ const moment = event === 'PreToolUse' ? 'before this edit runs' : 'after this file operation';
93
+ return [
94
+ `KLYPIX BLOCKING exact-file overlap detected ${moment}:`,
95
+ ...conflicts.map((peer) =>
96
+ `- ${String(peer.id).slice(0, 12)}${peer.intent ? ` "${String(peer.intent).slice(0, 90)}"` : ''}: ${peer.files.join(', ')}`),
97
+ 'Coordinate file ownership now. Do not continue overlapping edits until one session yields or the scopes are separated.',
98
+ ].join('\n');
99
+ }
100
+
101
+ function queueConflictAlerts({ brainPath, sessionId, intent, conflicts, turnId }) {
102
+ const queued = [];
103
+ for (const peer of conflicts) {
104
+ const filesKey = peer.files.map((file) => String(file).toLowerCase()).sort().join('|');
105
+ const result = postPresenceMessage({
106
+ brainPath,
107
+ from: sessionId,
108
+ to: peer.id,
109
+ text: `Automatic KLYPIX pre-edit overlap alert: ${String(sessionId).slice(0, 12)}`
110
+ + `${intent ? ` is working on "${String(intent).slice(0, 110)}"` : ' is editing'}`
111
+ + ` and reports the same file(s): ${peer.files.join(', ')}. Coordinate ownership now.`,
112
+ dedupeKey: `hook-overlap|${sessionId}|${peer.id}|${turnId || 'turn'}|${filesKey}`,
113
+ });
114
+ if (result.posted) queued.push(peer.id);
115
+ }
116
+ return queued;
117
+ }
118
+
119
+ async function compactTaskContext(projectDir, prompt, files) {
120
+ if (!prompt) return '';
121
+ try {
122
+ const result = await opBrainTaskContext({
123
+ vault: projectDir,
124
+ intent: String(prompt).slice(0, 2000),
125
+ files,
126
+ k: 4,
127
+ budgetChars: 1800,
128
+ });
129
+ return result?.context
130
+ ? result.blocks?.filter((block) => block.kind === 'text').map((block) => block.text).filter(Boolean).join('\n\n') || ''
131
+ : '';
132
+ } catch {
133
+ return '';
134
+ }
135
+ }
136
+
87
137
  async function main() {
88
138
  const input = readInput();
89
139
  const event = String(input.hook_event_name || input.hookEventName || '');
@@ -100,7 +150,9 @@ async function main() {
100
150
 
101
151
  const projectDir = path.dirname(brainPath);
102
152
  const prompt = input.prompt || input.user_prompt || input.userPrompt;
103
- const files = event === 'PostToolUse' ? touchedFiles(input, projectDir) : undefined;
153
+ const files = event === 'PreToolUse' || event === 'PostToolUse'
154
+ ? touchedFiles(input, projectDir)
155
+ : (event === 'UserPromptSubmit' ? [] : undefined);
104
156
  const sessions = upsertSession({
105
157
  brainPath,
106
158
  id: sessionId,
@@ -111,6 +163,7 @@ async function main() {
111
163
  branch: gitBranch(cwd),
112
164
  intent: prompt === undefined ? undefined : prompt,
113
165
  files,
166
+ replaceFiles: event === 'UserPromptSubmit',
114
167
  event,
115
168
  channel: 'lifecycle',
116
169
  cwd,
@@ -118,6 +171,19 @@ async function main() {
118
171
 
119
172
  const ignored = event === 'PostToolUse' ? ownBrainMessageText(input) : [];
120
173
  const messages = receiveMessages({ brainPath, sessionId, ignoreTexts: ignored });
174
+ const conflicts = (event === 'PreToolUse' || event === 'PostToolUse')
175
+ ? findPresenceConflicts(sessions, sessionId)
176
+ : [];
177
+ if (conflicts.length) {
178
+ const me = sessions.find((session) => session.id === sessionId);
179
+ queueConflictAlerts({
180
+ brainPath,
181
+ sessionId,
182
+ intent: me?.intent || '',
183
+ conflicts,
184
+ turnId: input.turn_id || input.turnId || '',
185
+ });
186
+ }
121
187
  if (event === 'SessionStart') {
122
188
  emitSystemMessage([
123
189
  formatPresenceMessage(sessions, sessionId, { includeSolo: true }),
@@ -126,12 +192,22 @@ async function main() {
126
192
  return;
127
193
  }
128
194
  if (event === 'UserPromptSubmit') {
195
+ const me = sessions.find((session) => session.id === sessionId);
196
+ const context = await compactTaskContext(projectDir, prompt, me?.files || []);
129
197
  emitSystemMessage([
198
+ context,
130
199
  formatPresenceMessage(sessions, sessionId),
131
200
  formatReceivedMessages(messages),
132
201
  ]);
133
202
  return;
134
203
  }
204
+ if (event === 'PreToolUse' || event === 'PostToolUse') {
205
+ emitSystemMessage([
206
+ formatConflictWarning(conflicts, event),
207
+ formatReceivedMessages(messages),
208
+ ]);
209
+ return;
210
+ }
135
211
  if (messages.length) emitSystemMessage([formatReceivedMessages(messages)]);
136
212
  }
137
213
 
@@ -10,6 +10,7 @@ import crypto from 'crypto';
10
10
  export const CODEX_PRESENCE_EVENTS = [
11
11
  'SessionStart',
12
12
  'UserPromptSubmit',
13
+ 'PreToolUse',
13
14
  'Stop',
14
15
  'PostToolUse',
15
16
  'SessionEnd',
@@ -29,10 +30,19 @@ export function resolveCodexHooksPath(home = os.homedir()) {
29
30
  }
30
31
 
31
32
  export function codexPresenceGroups(command) {
32
- const handler = (timeout = 5) => ({ type: 'command', command, timeout });
33
+ const handler = (timeout = 5, statusMessage) => ({
34
+ type: 'command',
35
+ command,
36
+ timeout,
37
+ ...(statusMessage ? { statusMessage } : {}),
38
+ });
33
39
  return {
34
- SessionStart: [{ hooks: [handler()] }],
35
- UserPromptSubmit: [{ hooks: [handler()] }],
40
+ SessionStart: [{ hooks: [handler(5, 'Loading KLYPIX project awareness')] }],
41
+ UserPromptSubmit: [{ hooks: [handler(5, 'Syncing KLYPIX task context')] }],
42
+ PreToolUse: [{
43
+ matcher: 'apply_patch|Edit|Write',
44
+ hooks: [handler(5, 'Checking KLYPIX file ownership')],
45
+ }],
36
46
  Stop: [{ hooks: [handler()] }],
37
47
  PostToolUse: [{ hooks: [handler()] }],
38
48
  SessionEnd: [{ hooks: [handler(3)] }],
@@ -12,6 +12,7 @@ import {
12
12
  findProjectBrain,
13
13
  formatPresenceMessage,
14
14
  formatReceivedMessages,
15
+ peekMessages,
15
16
  postPresenceMessage,
16
17
  receiveMessages,
17
18
  removeSession,
@@ -19,6 +20,7 @@ import {
19
20
  } from './agent-presence.mjs';
20
21
 
21
22
  export const MCP_HEARTBEAT_MS = 60_000;
23
+ export const MCP_INBOX_POLL_MS = 3_000;
22
24
 
23
25
  // Standard MCP server instructions are the approval-free awareness path. Hosts
24
26
  // that surface InitializeResult.instructions to the model learn the same
@@ -185,19 +187,25 @@ export function createMcpPresence({
185
187
  env = process.env,
186
188
  home,
187
189
  heartbeatMs = MCP_HEARTBEAT_MS,
190
+ inboxPollMs = MCP_INBOX_POLL_MS,
188
191
  now = () => Date.now(),
189
192
  setIntervalFn = setInterval,
190
193
  clearIntervalFn = clearInterval,
191
194
  } = {}) {
192
195
  const sessionId = resolveMcpSessionId({ env });
196
+ const effectiveInboxPollMs = Number(env?.KLYPIX_MCP_INBOX_POLL_MS)
197
+ || Number(inboxPollMs)
198
+ || MCP_INBOX_POLL_MS;
193
199
  let vault = path.resolve(initialVault || process.cwd());
194
200
  let brainPath = null;
195
201
  let timer = null;
202
+ let inboxTimer = null;
196
203
  let started = false;
197
204
  let lastPeers = '';
198
205
  let taskStartedAt = 0;
199
206
  const sentTexts = new Set();
200
207
  const notifiedConflicts = new Set();
208
+ const announcedMessageIds = new Set();
201
209
 
202
210
  const clientInfo = () => {
203
211
  let version = {};
@@ -209,12 +217,37 @@ export function createMcpPresence({
209
217
  };
210
218
 
211
219
  const sendNotice = (message) => {
212
- if (!message || typeof server?.sendLoggingMessage !== 'function') return;
220
+ if (!message || typeof server?.sendLoggingMessage !== 'function') return false;
213
221
  Promise.resolve(server.sendLoggingMessage({
214
222
  level: 'info',
215
223
  logger: 'klypix-presence',
216
224
  data: message,
217
225
  })).catch(() => {});
226
+ return true;
227
+ };
228
+
229
+ // Best-effort proactive path: MCP logging notifications can surface an
230
+ // overlap while the older session is still working. This deliberately PEEKS
231
+ // instead of acknowledging. Hosts are allowed to hide logging notifications,
232
+ // so the same warning remains unread and is guaranteed to arrive in-band on
233
+ // the next KLYPIX tool result / brain_sync call.
234
+ const pollInbox = () => {
235
+ if (!brainPath) return [];
236
+ const pending = peekMessages({
237
+ brainPath,
238
+ sessionId,
239
+ ignoreTexts: [...sentTexts],
240
+ home,
241
+ now: now(),
242
+ }).filter((message) => !announcedMessageIds.has(message.id));
243
+ if (!pending.length) return [];
244
+ if (sendNotice(formatReceivedMessages(pending, now()))) {
245
+ for (const message of pending) announcedMessageIds.add(message.id);
246
+ while (announcedMessageIds.size > 100) {
247
+ announcedMessageIds.delete(announcedMessageIds.values().next().value);
248
+ }
249
+ }
250
+ return pending;
218
251
  };
219
252
 
220
253
  const touch = ({
@@ -271,7 +304,9 @@ export function createMcpPresence({
271
304
 
272
305
  const stop = () => {
273
306
  if (timer) clearIntervalFn(timer);
307
+ if (inboxTimer) clearIntervalFn(inboxTimer);
274
308
  timer = null;
309
+ inboxTimer = null;
275
310
  if (brainPath) removeSession({
276
311
  brainPath,
277
312
  id: sessionId,
@@ -282,6 +317,7 @@ export function createMcpPresence({
282
317
  brainPath = null;
283
318
  started = false;
284
319
  lastPeers = '';
320
+ announcedMessageIds.clear();
285
321
  };
286
322
 
287
323
  const start = (nextVault = vault, details = {}) => {
@@ -302,7 +338,12 @@ export function createMcpPresence({
302
338
  replaceFiles: details.replaceFiles === true,
303
339
  });
304
340
  timer = setIntervalFn(() => touch(), Math.max(5_000, Number(heartbeatMs) || MCP_HEARTBEAT_MS));
341
+ inboxTimer = setIntervalFn(
342
+ () => pollInbox(),
343
+ Math.max(250, effectiveInboxPollMs),
344
+ );
305
345
  timer?.unref?.();
346
+ inboxTimer?.unref?.();
306
347
  return first;
307
348
  };
308
349
 
@@ -398,6 +439,10 @@ export function createMcpPresence({
398
439
  ts: message.ts,
399
440
  })),
400
441
  alertsQueued,
442
+ delivery: {
443
+ proactive: 'mcp-logging-best-effort',
444
+ guaranteed: 'next-klypix-action',
445
+ },
401
446
  timingMs: { coordination: durationMs },
402
447
  };
403
448
  const text = [
@@ -429,6 +474,7 @@ export function createMcpPresence({
429
474
  start,
430
475
  stop,
431
476
  touch,
477
+ pollInbox,
432
478
  sync,
433
479
  noteSent,
434
480
  decorateToolResult,