klypix-mcp 1.100.0 → 1.101.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,34 @@
1
- # Every project gets a brain.
1
+ # klypix-mcp — shared project memory for Claude Code, Codex and Cursor
2
+
3
+ **Every project gets a brain.**
2
4
 
3
5
  Shared project memory for **Claude Code, Codex, Cursor** and other MCP coding tools. One
4
6
  `brain.klypix` file, committed with your code, carries the project's decisions, corrections and open
5
7
  questions across sessions and between tools. Corrections supersede stale decisions; sessions declare
6
8
  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
+ over MCP by a process on your machine. In the standalone setup, brain data stays in your files;
10
+ your agent's provider can still receive what the agent reads. Integration depth differs by host — see
9
11
  [Supported hosts](#supported-hosts-and-their-integration-level).
10
12
 
13
+ The **Apache-2.0 engine works without the KLYPIX desktop app or a KLYPIX account**. The optional
14
+ [Windows workspace](https://klypix.com/developers) lets people inspect the same project brain
15
+ beside their files. App-based sharing has its own cloud path; see [Security and permissions](#security-and-permissions).
16
+
17
+ [Install](#quick-start) · [Try your first decision](#your-first-decision-and-correction) ·
18
+ [Supported hosts](#supported-hosts-and-their-integration-level) ·
19
+ [Privacy](#security-and-permissions) · [Limits](#current-limitations) · [Uninstall](#uninstall)
20
+
21
+ For a team walkthrough, read the [shared project memory setup guide](https://klypix.com/guides/shared-project-memory).
22
+ Choosing an approach? Our [memory tools comparison](https://klypix.com/guides/shared-project-memory-tools)
23
+ links each tool's official documentation and discloses that Klypix wrote it.
24
+
11
25
  ![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
26
 
13
27
  <sub>Real output, not a mockup: both panes run a real MCP client against this server
14
28
  ([docs/demo/](docs/demo/) — the GIF is rendered by CI from a scripted tape against this server, not
15
29
  hand-recorded, and re-rendered when the server's responses change).</sub>
16
30
 
17
- Run this inside your project:
31
+ With **Node.js 20 or newer**, run this inside your project:
18
32
 
19
33
  ```bash
20
34
  npx klypix-mcp install
@@ -59,15 +73,14 @@ carry forward into supported coding-agent sessions.
59
73
  ## The problem
60
74
 
61
75
  You are running more than one coding agent on one codebase — a Claude Code session here, Codex in
62
- another terminal, Cursor open on the side. Each one has excellent memory of *itself* and none of
63
- the others:
76
+ another terminal, Cursor open on the side. Context saved in one tool may be missing from the next:
64
77
 
65
- - Every new session starts from zero, and you explain the same architecture again.
66
- - Codex does not know what Claude learned an hour ago.
78
+ - A new session misses a prior decision, and you explain the same architecture again.
79
+ - A decision captured only in a Claude conversation is unavailable to your Codex session.
67
80
  - One agent implements an approach the team already rejected, because the reason it was rejected
68
81
  lived in a chat that ended.
69
82
  - Two sessions start changing the same files and nobody finds out until review.
70
- - Git stores the code history. It does not store a reliable history of project *intent*.
83
+ - Git stores the code history; the reasons behind a change still need to be recorded.
71
84
 
72
85
  Your agents may run independently. Their project understanding should not.
73
86
 
@@ -75,10 +88,14 @@ Your agents may run independently. Their project understanding should not.
75
88
 
76
89
  ## 60 seconds: two agents, one project
77
90
 
91
+ Illustrative calls and responses: replace `/absolute/path/to/project` with the same project root
92
+ in both sessions (for example, `C:/work/api` on Windows).
93
+
78
94
  Session A — Claude Code, in your repo:
79
95
 
80
96
  ```jsonc
81
- brain_sync { intent: "rewrite the auth token refresh", files: ["src/auth/token.ts"] }
97
+ brain_sync { project: "/absolute/path/to/project", phase: "start",
98
+ intent: "rewrite the auth token refresh", files: ["src/auth/token.ts"] }
82
99
  // → task-relevant memory capsule (bounded, ~2.8KB)
83
100
  // → peers: none
84
101
  ```
@@ -86,7 +103,8 @@ brain_sync { intent: "rewrite the auth token refresh", files: ["src/auth/token.t
86
103
  Session B — Codex, same repo, half a minute later:
87
104
 
88
105
  ```jsonc
89
- brain_sync { intent: "add rate limiting to the auth routes",
106
+ brain_sync { project: "/absolute/path/to/project", phase: "start",
107
+ intent: "add rate limiting to the auth routes",
90
108
  files: ["src/auth/token.ts", "src/auth/routes.ts"] }
91
109
  // → task capsule
92
110
  // → peers: 1 active session (claude-code) — "rewrite the auth token refresh"
@@ -100,7 +118,7 @@ expected to touch.
100
118
  Then the brain pushes back before the decision, not after:
101
119
 
102
120
  ```jsonc
103
- brain_challenge { "move token storage to localStorage" }
121
+ brain_challenge { claim: "move token storage to localStorage" }
104
122
  // → "reversed on June 12 — here's the correction card, captured by a different agent."
105
123
  ```
106
124
 
@@ -213,6 +231,59 @@ npx klypix-mcp link --check # audits without writing; exits non-zero on drift
213
231
  [KLYPIX app](https://klypix.com) does it in one click (*Save canvas as project brain*), or
214
232
  `create_canvas` makes one from any agent.
215
233
 
234
+ ### Your first decision and correction
235
+
236
+ After installation, reopen your coding tool or reconnect its MCP server, and allow the KLYPIX
237
+ tools if your host asks. Run `npx klypix-mcp doctor` in the project to inspect the installation;
238
+ a clean config report does not prove that your host has loaded the tools.
239
+
240
+ Try the following in a **disposable trial project**, or replace the example decision with a real
241
+ one before saving it. These are MCP tool calls for your agent, not shell commands. Replace
242
+ `/absolute/path/to/project` with your project root throughout. The `files: []` scope means this
243
+ trial does not plan source-code edits.
244
+
245
+ 1. **Record a decision and its reason.** Ask your agent to call:
246
+
247
+ ```jsonc
248
+ brain_sync { project: "/absolute/path/to/project", phase: "start",
249
+ intent: "Record the trial storage decision", files: [] }
250
+ brain_note { area: "Storage",
251
+ text: "Use SQLite for the trial project database because this prototype runs on one machine." }
252
+ brain_sync { project: "/absolute/path/to/project", phase: "complete" }
253
+ ```
254
+
255
+ 2. **Open a fresh session in the same project**, optionally in a different supported coding tool.
256
+ Ask it to retrieve the decision rather than infer the answer from your code:
257
+
258
+ ```jsonc
259
+ brain_sync { project: "/absolute/path/to/project", phase: "start",
260
+ intent: "Review the trial storage decision", files: [] }
261
+ brain_ask { question: "Which database does the trial project use, and why?" }
262
+ ```
263
+
264
+ Check that the returned card contains SQLite and the one-machine rationale. If the tool is
265
+ missing, reconnect the host and check the [host integration table](#supported-hosts-and-their-integration-level).
266
+ If the card is missing, confirm both sessions used the same project and that `brain_note`
267
+ reported a successful capture. A fresh brain only knows what has been recorded.
268
+
269
+ 3. **Change the trial decision explicitly**, then ask again:
270
+
271
+ ```jsonc
272
+ brain_note { area: "Storage",
273
+ text: "CORRECTION: Use PostgreSQL instead of SQLite for the trial project database because the trial now needs concurrent writes from several machines." }
274
+ brain_ask { question: "Which database does the trial project use now, and why?" }
275
+ brain_sync { project: "/absolute/path/to/project", phase: "complete" }
276
+ ```
277
+
278
+ Inspect the returned source cards: PostgreSQL should be the current decision, with SQLite
279
+ retained as superseded history. Confirm that the correction names the earlier decision; a
280
+ correction cue alone is not a guarantee of a match. This small trial checks your capture and
281
+ recall path. It does not establish retrieval quality for a larger brain or prove host automation.
282
+
283
+ For a team on different machines, follow the [Git sharing steps](https://klypix.com/guides/shared-project-memory#team)
284
+ and install in each clone. Standalone live presence and file-overlap warnings stay local to one
285
+ machine and OS user. Keep approved architecture records and stable conventions alongside the brain.
286
+
216
287
  ---
217
288
 
218
289
  ## How the brain works
@@ -597,16 +668,29 @@ and the person opens Codex (or a fresh Claude Code) and says *continue*. Until 1
597
668
  handoff was whatever the person pasted. Now there are two layers, and the person does not have
598
669
  to ask for either.
599
670
 
600
- **Automatic.** At every turn end the hooks record the session's last state on the lane that already
601
- remembers sessions for two weeks: the last human prompt, the tail of the last reply (where "done /
602
- next" usually is), the files it touched, and its *own* commits (the shas its `git commit` printed —
603
- never a branch range, because twenty sessions can share one checkout). When that session goes quiet
604
- for ten minutes or closes with its task uncompleted, every new session on the project sees
605
- **⏸️ Unfinished work left by other sessions** at its first `brain_sync` and at session start — in
606
- Claude Code, Codex, and any MCP host — with a status (*closed*, *idle 83m*), the files, the commits,
607
- the last words, and the resume command should the person want the old transcript. The agent takes it
608
- over with `brain_handoff {action:"take", session:"<id>"}` so peers see it is taken, continues from
609
- the record, and marks it `done`. Machine-local, like the rest of the lane.
671
+ **Automatic — whichever agent stopped.** Each session's last state is recorded on the lane that
672
+ already remembers sessions for two weeks, by whatever that host offers:
673
+
674
+ | Host | Recorded automatically |
675
+ |---|---|
676
+ | Claude Code (hooks) | last prompt, the tail of the last reply, files edited, its *own* commits |
677
+ | Codex (hooks) | last prompt, the tail of the last reply, files edited, its *own* commits |
678
+ | Any MCP host — Cursor, VS Code, Windsurf, Gemini CLI, … | the task and files it declared with `brain_sync` |
679
+
680
+ *Own* commits are the shas that session's `git commit` printed, never a branch range, because
681
+ twenty sessions can share one checkout. A session is listed when it stopped in the last 24 hours
682
+ without completing its task — quiet for ten minutes, or closed — and left a record. A session that
683
+ recorded nothing is never listed, so finished chats do not pile up.
684
+
685
+ The next session on the project, in any agent, sees **⏸️ Work other sessions may have left
686
+ unfinished** at its first `brain_sync` when its task is to continue something ("continue what Codex
687
+ was doing on the invoice" puts that session first) or touches a file one of them touched. It sees the
688
+ same list at session start. Each record carries its status, files, commits, last words and resume
689
+ command. It says, in the text and in the JSON (`state`, `working:false`, `holdsFiles:false`), that
690
+ the session is not running, holds no claim on any file, and that a "do not touch" note it left no
691
+ longer applies: the list is history, not a lock. The agent takes a record over with
692
+ `brain_handoff {action:"take", session:"<id>"}` so peers see it is taken, then marks it `done`. This
693
+ layer is machine-local, like the rest of the lane.
610
694
 
611
695
  **Deliberate.** When a session knows it is stopping — the person asks for a handoff, the context or
612
696
  usage limit is near, the day ends, it is blocked, it is switching tool — it calls
@@ -1174,8 +1258,11 @@ Read this section before you build on any of it.
1174
1258
  that list together with the shared one.
1175
1259
  - **`npx klypix-mcp link` does not manage `CLAUDE.md`.** It manages `AGENTS.md` and seven other
1176
1260
  rules files. Only the desktop app writes `CLAUDE.md`.
1177
- - **A fresh `npx klypix-mcp install` gets lexical retrieval.** The optional on-device model is
1178
- deliberately not installed.
1261
+ - **Semantic retrieval depends on the install path.** An interactive terminal install enables
1262
+ the on-device search runtime by default. CI and noninteractive installs need `--semantic`;
1263
+ `--no-semantic` (or `KLYPIX_SEMANTIC=0`) opts out. The Claude plugin stays keyword-only. Without
1264
+ the model, retrieval uses keywords; a model download or a brain still being prepared is not a
1265
+ guarantee that a particular decision will be found.
1179
1266
  - **The capture lock is fail-open** past ~3.6 seconds of contention (see *Git and concurrency*).
1180
1267
  - **`test/` is not in the published tarball.** Run the suite from a clone. The publish workflow
1181
1268
  *does* gate on it — a `gate` job runs `npm ci`, asserts the test chain is intact, runs `npm test`,
@@ -1208,7 +1295,8 @@ independently validated.
1208
1295
  2026-10-11 on the same brain grown to 3,829 cards (102 of the frozen questions still answerable),
1209
1296
  with words written by Gemini 2.5 Flash as a stand-in for the calling agent: recall@5 54% → 59%
1210
1297
  (8 found, 3 lost), recall@8 61% → 68%, recall@20 68% → 76%, MRR 0.375 → 0.424. With no local
1211
- model (a fresh install, the Claude plugin) the words count as extra keywords at half weight:
1298
+ model (for example, an install with `--no-semantic` or the Claude plugin) the words count as extra
1299
+ keywords at half weight:
1212
1300
  recall@5 19% → 24% (5 found, none lost), recall@10 25% → 31%. Without the line, ranking is
1213
1301
  unchanged byte for byte.
1214
1302
  - **Second brain.** The same ranker on a different project's brain (1,601 cards, 19 hand-written
@@ -1240,14 +1328,17 @@ independently validated.
1240
1328
  ## Uninstall
1241
1329
 
1242
1330
  ```bash
1243
- npx klypix-mcp uninstall --check # full inventory — writes nothing
1244
- npx klypix-mcp uninstall # asks, then removes the machine-global install
1245
- npx klypix-mcp uninstall unlink # run inside a project: removes the files `link` wrote there
1331
+ npx klypix-mcp uninstall --check # preview machine-global removal — writes nothing
1332
+ npx klypix-mcp uninstall --yes # remove the machine-global install
1333
+ npx klypix-mcp uninstall unlink --check # preview this project's managed files
1334
+ npx klypix-mcp uninstall unlink --yes # remove this project's managed files
1246
1335
  ```
1247
1336
 
1248
1337
  It strips only KLYPIX's own entries — every other hook and setting in
1249
1338
  `~/.claude/settings.json` stays — backs up each file it edits, and **never deletes a `.klypix`**.
1250
- `--yes` skips the prompt for scripted removal.
1339
+ Removal requires `--yes`. Without it, the command prints the plan and changes nothing;
1340
+ there is no interactive confirmation prompt. Run the project-local form in each project you
1341
+ want to unlink. A preview exits non-zero while managed files remain.
1251
1342
 
1252
1343
  Your `brain.klypix` is yours — it is a plain ZIP and stays readable with or without this package.
1253
1344
 
@@ -1266,10 +1357,9 @@ are not in the published tarball, though the publish workflow does run them as a
1266
1357
 
1267
1358
  ## Why this exists
1268
1359
 
1269
- A model provider can fix continuity inside its own sessions, and several are. None of them will
1270
- ever carry a competitor's context. Cross-tool, cross-agent and cross-provider understanding is the
1271
- seam that stays open — so it should live in a file you own, in your repo, that any agent can read
1272
- and write.
1360
+ Coding tools have their own memory and instruction systems. A team using several tools still
1361
+ needs a shared place for decisions, reasons and corrections. Keeping that record in a file you
1362
+ own lets supported agents read and update it through the same interface.
1273
1363
 
1274
1364
  **Your project, your file, any supported agent, offline.**
1275
1365
 
@@ -1418,13 +1418,28 @@ server.registerTool('brain_sync', {
1418
1418
  const wantsContinue = HANDOFF_CONTINUE_RE.test(String(intent || ''));
1419
1419
  const normPath = (p) => String(p || '').replace(/\\/g, '/').toLowerCase();
1420
1420
  const mine = new Set((Array.isArray(files) ? files : []).map(normPath));
1421
- const lane = (h?.structured?.unfinished || []).filter((r) => wantsContinue
1422
- || (Array.isArray(r.files) && r.files.some((p) => mine.has(normPath(p)))));
1421
+ const overlaps = (r) => Array.isArray(r.files) && r.files.some((p) => mine.has(normPath(p)));
1422
+ // "continue what codex was doing on the invoice": rank by what the
1423
+ // request names — the agent, words of the task, last words, files,
1424
+ // commit subjects — then by recency, so the one meant leads the list.
1425
+ const words = new Set(String(intent || '').toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) || []);
1426
+ for (const w of ['continue', 'resume', 'what', 'was', 'were', 'doing', 'the', 'and', 'left', 'off', 'where', 'session', 'other', 'work', 'pick', 'carry', 'keep', 'going', 'finish', 'take', 'over', 'handoff', 'hand']) words.delete(w);
1427
+ const relevance = (r) => {
1428
+ const hay = [r.client === 'claude-code' ? 'claude' : r.client, r.intent, r.lastPrompt, r.lastWords, ...(r.files || []), ...(r.commits || []).map((c) => c.subject)].join(' ').toLowerCase();
1429
+ let n = 0;
1430
+ for (const w of words) if (hay.includes(w)) n++;
1431
+ return n + (overlaps(r) ? 3 : 0);
1432
+ };
1433
+ const lane = (h?.structured?.unfinished || [])
1434
+ .filter((r) => wantsContinue || overlaps(r))
1435
+ .map((r) => ({ r, score: relevance(r) }))
1436
+ .sort((a, b) => b.score - a.score || Number(b.r.lastSeen || 0) - Number(a.r.lastSeen || 0))
1437
+ .map(({ r }) => r);
1423
1438
  if (!h?.isError && (cards.length || lane.length)) {
1424
1439
  const now = Date.now();
1425
1440
  handoffText = [
1426
1441
  formatHandoffCards(cards, { now, max: 5 }),
1427
- formatUnfinishedWork(lane, { now, max: 4 }),
1442
+ formatUnfinishedWork(lane, { now, max: 4, ranked: wantsContinue && words.size > 0 }),
1428
1443
  ].filter(Boolean).join('\n\n');
1429
1444
  handoffData = {
1430
1445
  cards: cards.length,
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.100.0",
3
+ "version": "1.101.1",
4
4
  "mcpName": "io.github.dahshanlabs/klypix-mcp",
5
- "description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
5
+ "description": "Shared project memory for Claude Code, Codex and Cursor: decisions and corrections through MCP.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
8
8
  "keywords": [
9
9
  "mcp",
10
10
  "model-context-protocol",
11
+ "mcp-server",
12
+ "claude-code",
13
+ "codex",
14
+ "cursor",
15
+ "shared-memory",
11
16
  "multi-agent",
12
17
  "agent-coordination",
13
18
  "project-memory",
@@ -24,7 +29,7 @@
24
29
  "klypix",
25
30
  "ai"
26
31
  ],
27
- "homepage": "https://github.com/dahshanlabs/klypix-mcp#readme",
32
+ "homepage": "https://klypix.com/developers",
28
33
  "repository": {
29
34
  "type": "git",
30
35
  "url": "https://github.com/dahshanlabs/klypix-mcp"
@@ -535,12 +535,28 @@ export function normalizeWork(raw) {
535
535
  out.takenBy = { id: str(raw.takenBy.id, 160), client: str(raw.takenBy.client, 40) || 'unknown', at: ms(raw.takenBy.at) };
536
536
  }
537
537
  if (ms(raw.doneAt)) { out.doneAt = ms(raw.doneAt); if (raw.doneBy) out.doneBy = str(raw.doneBy, 160); }
538
+ // 1.99.1 (universal): a task DECLARED through brain_sync — any MCP host,
539
+ // hooks or not — stamps when it was declared, so a Cursor / Gemini / VS Code
540
+ // session that stopped mid-task is still found by the next session.
541
+ if (ms(raw.declaredAt)) out.declaredAt = ms(raw.declaredAt);
538
542
  return Object.keys(out).length ? out : null;
539
543
  }
540
544
  export function mergeWork(previous, patch) {
541
545
  if (patch === undefined) return normalizeWork(previous);
542
546
  if (patch === null) return null;
543
- return normalizeWork({ ...(normalizeWork(previous) || {}), ...(patch && typeof patch === 'object' ? patch : {}) });
547
+ const prev = normalizeWork(previous) || {};
548
+ const next = patch && typeof patch === 'object' ? { ...patch } : {};
549
+ // `addCommits` APPENDS (Codex records one commit per tool result; the
550
+ // Claude Stop hook replaces the full list with `commits`), newest last,
551
+ // de-duplicated by sha, capped like `commits`.
552
+ if (Array.isArray(next.addCommits)) {
553
+ const seen = new Set();
554
+ next.commits = [...(prev.commits || []), ...next.addCommits]
555
+ .filter((c) => c && c.sha && !seen.has(String(c.sha).slice(0, 7)) && seen.add(String(c.sha).slice(0, 7)))
556
+ .slice(-WORK_COMMITS_MAX);
557
+ delete next.addCommits;
558
+ }
559
+ return normalizeWork({ ...prev, ...next });
544
560
  }
545
561
  const pruneDirectory = (rows, now) => (Array.isArray(rows) ? rows : [])
546
562
  .filter((row) => row && recipientKey(row.id) && now - Number(row.lastSeen || 0) < DIRECTORY_FRESH_MS)
@@ -1186,7 +1202,14 @@ export function upsertSession({
1186
1202
  ...(statusDigestHash !== undefined ? { statusDigestHash: String(statusDigestHash || '').slice(0, 16) } : {}),
1187
1203
  // ADDITIVE unfinished-work state: merged only when this touch carries a
1188
1204
  // patch; the `...previous` spread keeps it verbatim otherwise.
1189
- ...(work !== undefined ? (mergeWork(previous.work, work) ? { work: mergeWork(previous.work, work) } : {}) : {}),
1205
+ ...(() => {
1206
+ // A brain_sync-DECLARED task (any MCP host) is itself a work record:
1207
+ // stamp declaredAt beside whatever patch this touch carries.
1208
+ const declared = intentSource === 'declared' && intentChanged && nextIntent ? { declaredAt: now } : null;
1209
+ if (work === undefined && !declared) return {};
1210
+ const merged = mergeWork(previous.work, { ...(work && typeof work === 'object' ? work : {}), ...(declared || {}) });
1211
+ return merged ? { work: merged } : {};
1212
+ })(),
1190
1213
  // ADDITIVE host status: stamped only when this touch carries one, kept
1191
1214
  // verbatim otherwise, so a heartbeat never turns "working" into unknown.
1192
1215
  ...(nextHostStatus
@@ -2914,7 +2937,10 @@ export const UNFINISHED_QUIET_MS = 10 * 60 * 1000;
2914
2937
  // and every record says, in text and JSON, that it is not running and holds
2915
2938
  // no claim on any file.
2916
2939
  export const UNFINISHED_WINDOW_MS = 24 * 60 * 60 * 1000;
2917
- export const hasWorkEvidence = (work) => Boolean(work && (work.lastWords || (Array.isArray(work.commits) && work.commits.length) || work.handoffCardId));
2940
+ // Evidence of real work, from ANY host: a reply tail (Claude and Codex Stop
2941
+ // hooks), own commits (Claude transcript, Codex tool results), a handoff card
2942
+ // (brain_handoff), or a task declared through brain_sync (every MCP host).
2943
+ export const hasWorkEvidence = (work) => Boolean(work && (work.lastWords || (Array.isArray(work.commits) && work.commits.length) || work.handoffCardId || work.declaredAt));
2918
2944
  export function clientWord(client) {
2919
2945
  const key = String(client || '').toLowerCase();
2920
2946
  if (key === 'claude-code' || key === 'claude') return 'Claude Code';
@@ -3026,11 +3052,11 @@ export function stampSessionWork({ brainPath, sessionId, patch, home, now = Date
3026
3052
  // one record per unfinished session, newest first, then how to take it over.
3027
3053
  // Quoted text is another session's record and is labelled as such — never as
3028
3054
  // the human's instructions; markers inside it were neutralized at store time.
3029
- export function formatUnfinishedWork(records, { now = Date.now(), max = 5, cardTitles = null } = {}) {
3055
+ export function formatUnfinishedWork(records, { now = Date.now(), max = 5, cardTitles = null, ranked = false } = {}) {
3030
3056
  const list = Array.isArray(records) ? records : [];
3031
3057
  if (!list.length) return '';
3032
3058
  const flat = (value) => String(value || '').replace(/\s+/g, ' ').trim();
3033
- const lines = [`⏸️ Work other sessions may have left unfinished (${list.length}, stopped in the last 24h, newest first). None of them is running: they hold NO claim on any file, and a "do not touch" or WIP note one of them left no longer applies. Use this only if the human asks you to continue their work:`];
3059
+ const lines = [`⏸️ Work other sessions may have left unfinished (${list.length}, stopped in the last 24h, ${ranked ? 'most relevant first' : 'newest first'}). None of them is running: they hold NO claim on any file, and a "do not touch" or WIP note one of them left no longer applies. Use this only if the human asks you to continue their work:`];
3034
3060
  for (const r of list.slice(0, max)) {
3035
3061
  const who = `${clientWord(r.client)} ${String(r.id).slice(0, 8)}`;
3036
3062
  const bits = [r.state === 'ended' ? 'ended' : `stopped (${r.status})`, `last active ${agoLabel(r.ageMs)}`];
@@ -371,6 +371,8 @@ async function statusDigestContext({ brainPath, sessionId, prompt, sessions, cwd
371
371
  // ⏸️ Unfinished work other sessions left — the engine's block (agent-presence
372
372
  // listUnfinishedWork + formatUnfinishedWork), read-only, never throws.
373
373
  const CONTINUE_PROMPT_RE = /\b(continue|resume|pick (it |this |that )?up|carry on|keep going|where (were|did) we|what('s| is| was) (next|left|remaining|pending)|unfinished|left off|hand[- ]?off|handed (over|off)|take over|finish (what|the work|it|this))\b/i;
374
+ const HANDOFF_ASK_PROMPT_RE = /\b(hand[- ]?off|hand (it|this|the work) (over|off)|before (we|i|you) (hit|reach|approach|run out of) (the )?(limit|context|window|quota)|(context|usage|token|rate) (limit|window|quota)|out of context|switch(ing)? to (claude|another (agent|session|tool)))\b/i;
375
+ const CODEX_HANDOFF_HINT = '🤝 KLYPIX: to hand this session\'s work to the next one (any agent — Claude Code or Codex), call brain_handoff with the task, what is done, what is next, the files and how to verify; the next session on this project sees it at its first brain_sync and takes it with {action:"take"}. Your last reply and commits are recorded automatically at each turn end regardless — but a deliberate handoff says what you know. Never make the human paste a summary into the next session.';
374
376
  function unfinishedWorkBlock(brainPath, sessionId) {
375
377
  try {
376
378
  return formatUnfinishedWork(listUnfinishedWork({ brainPath, selfId: sessionId }));
@@ -390,8 +392,31 @@ function codexWorkPatch({ event, humanTurn, prompt, input }) {
390
392
  const text = typeof words === 'string' ? words.trim() : '';
391
393
  if (text) { patch.lastWords = text.length > 700 ? '…' + text.slice(-699) : text; patch.lastWordsAt = Date.now(); }
392
394
  }
395
+ // 1.99.1 (universal): this session's OWN commits, from the output git
396
+ // printed for a commit it ran ("[branch abc1234] subject") — never a branch
397
+ // range, which would credit a peer's commits in a shared checkout.
398
+ if (event === 'PostToolUse') {
399
+ const commits = commitsFromToolResult(input);
400
+ if (commits.length) patch.addCommits = commits;
401
+ }
393
402
  return Object.keys(patch).length ? patch : null;
394
403
  }
404
+ function toolOutputText(input) {
405
+ const r = input.tool_response ?? input.toolResponse ?? input.tool_output ?? input.toolOutput;
406
+ if (typeof r === 'string') return r;
407
+ if (!r || typeof r !== 'object') return '';
408
+ const parts = [r.output, r.stdout, r.aggregated_output, r.formatted_output, r.text, r.content];
409
+ return parts.map((p) => (typeof p === 'string' ? p : Array.isArray(p) ? p.map((x) => (typeof x === 'string' ? x : x?.text || '')).join('\n') : '')).filter(Boolean).join('\n');
410
+ }
411
+ export function commitsFromToolResult(input) {
412
+ const ti = input.tool_input || input.toolInput || {};
413
+ const cmd = typeof ti === 'string' ? ti : [ti.command, ti.cmd, Array.isArray(ti.command) ? ti.command.join(' ') : ''].filter((v) => typeof v === 'string').join(' ');
414
+ if (!/\bgit\b[^\n]*\bcommit\b/.test(cmd)) return [];
415
+ const out = toolOutputText(input);
416
+ const commits = [];
417
+ for (const m of out.matchAll(/^\[[^\]\n]{1,80}?\s([0-9a-f]{7,40})\]\s+(.+)$/gm)) commits.push({ sha: m[1].slice(0, 12), subject: m[2].trim().slice(0, 120) });
418
+ return commits.slice(0, 6);
419
+ }
395
420
 
396
421
  async function compactTaskContext(projectDir, prompt, files) {
397
422
  if (!prompt) return '';
@@ -533,6 +558,8 @@ async function main() {
533
558
  stampReceivedMessages(messages, Date.now(), undefined, sessionId),
534
559
  // Only on a continue-shaped human prompt — SessionStart printed it once.
535
560
  ...(prompt && CONTINUE_PROMPT_RE.test(prompt) ? [unfinishedWorkBlock(brainPath, sessionId)] : []),
561
+ // The human asked for a handoff or named the limit (Codex has one too).
562
+ ...(prompt && HANDOFF_ASK_PROMPT_RE.test(prompt) ? [CODEX_HANDOFF_HINT] : []),
536
563
  ], event);
537
564
  return;
538
565
  }
@@ -1086,6 +1086,7 @@ function workNorm(raw) {
1086
1086
  if (raw.handoffCardId) out.handoffCardId = str(raw.handoffCardId, 64);
1087
1087
  if (raw.takenBy && typeof raw.takenBy === 'object' && raw.takenBy.id) out.takenBy = { id: str(raw.takenBy.id, 160), client: str(raw.takenBy.client, 40) || 'unknown', at: ms(raw.takenBy.at) };
1088
1088
  if (ms(raw.doneAt)) { out.doneAt = ms(raw.doneAt); if (raw.doneBy) out.doneBy = str(raw.doneBy, 160); }
1089
+ if (ms(raw.declaredAt)) out.declaredAt = ms(raw.declaredAt); // 1.99.1 twin: a brain_sync-declared task (any MCP host)
1089
1090
  return Object.keys(out).length ? out : null;
1090
1091
  }
1091
1092
  const workMerge = (prev, patch) => (patch === undefined ? workNorm(prev) : patch === null ? null : workNorm({ ...(workNorm(prev) || {}), ...(patch && typeof patch === 'object' ? patch : {}) }));
@@ -1470,7 +1471,7 @@ const UNFINISHED_QUIET_MS = 10 * 60 * 1000;
1470
1471
  // 1.99.1 twin of agent-presence UNFINISHED_WINDOW_MS / hasWorkEvidence: only a
1471
1472
  // session that stopped in the last 24h AND left a 1.99+ work record is listed.
1472
1473
  const UNFINISHED_WINDOW_MS = 24 * 60 * 60 * 1000;
1473
- const hasWorkEvidence = (w) => Boolean(w && (w.lastWords || (Array.isArray(w.commits) && w.commits.length) || w.handoffCardId));
1474
+ const hasWorkEvidence = (w) => Boolean(w && (w.lastWords || (Array.isArray(w.commits) && w.commits.length) || w.handoffCardId || w.declaredAt));
1474
1475
  const CONTINUE_RE = /\b(continue|resume|pick (it |this |that )?up|carry on|keep going|where (were|did) we|what('s| is| was) (next|left|remaining|pending)|unfinished|left off|hand[- ]?off|handed (over|off)|take over|finish (what|the work|it|this))\b/i;
1475
1476
  const HANDOFF_ASK_RE = /\b(hand[- ]?off|hand (it|this|the work) (over|off)|before (we|i|you) (hit|reach|approach|run out of) (the )?(limit|context|window|quota)|(context|usage|token) (limit|window|quota)|out of context|switch(ing)? to (codex|another (agent|session|tool)))\b/i;
1476
1477
  function unfinishedFooter(sid, { max = 4 } = {}) {