klypix-mcp 1.104.0 → 1.105.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 +16 -1
- package/bin/klypix-install.mjs +8 -3
- package/bin/klypix-mcp.mjs +3 -1
- package/bin/klypix-rescue.mjs +190 -0
- package/bin/klypix-worker.mjs +5 -0
- package/package.json +3 -3
- package/src/agent-rules.mjs +3 -1
- package/src/codex-brain-hook.mjs +98 -14
- package/src/global-brain-hook.mjs +64 -2
- package/src/mcp-presence.mjs +1 -1
- package/src/whose-classify.mjs +2795 -0
- package/src/whose-ledger.mjs +863 -0
- package/src/whose-rescue.mjs +1220 -0
- package/src/whose.mjs +302 -0
package/README.md
CHANGED
|
@@ -10,10 +10,17 @@ over MCP by a process on your machine. In the standalone setup, brain data stays
|
|
|
10
10
|
your agent's provider can still receive what the agent reads. Integration depth differs by host — see
|
|
11
11
|
[Supported hosts](#supported-hosts-and-their-integration-level).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Klypix is a **portable project workspace with a shared brain**. This repository provides its
|
|
14
|
+
local MCP server for shared project memory in coding tools. The **Apache-2.0 engine works without the KLYPIX
|
|
15
|
+
desktop app or a KLYPIX account**. The optional
|
|
14
16
|
[Windows workspace](https://klypix.com/developers) lets people inspect the same project brain
|
|
15
17
|
beside their files. App-based sharing has its own cloud path; see [Security and permissions](#security-and-permissions).
|
|
16
18
|
|
|
19
|
+
For the **Projects & Ideas** path — saved project materials in ChatGPT or Claude chat — follow
|
|
20
|
+
[Save project materials and use them with your assistant](https://klypix.com/guides/connect-klypix-to-chatgpt-and-claude).
|
|
21
|
+
That uses Klypix's separate hosted connection for the spaces you authorize. Installing this local
|
|
22
|
+
MCP server does not set up that connection.
|
|
23
|
+
|
|
17
24
|
[Install](#quick-start) · [Try your first decision](#your-first-decision-and-correction) ·
|
|
18
25
|
[Supported hosts](#supported-hosts-and-their-integration-level) ·
|
|
19
26
|
[Privacy](#security-and-permissions) · [Limits](#current-limitations) · [Uninstall](#uninstall)
|
|
@@ -38,6 +45,11 @@ It creates `brain.klypix` if the project has none, wires the editors it finds on
|
|
|
38
45
|
registers the `.klypix` merge driver if this is a git repo, and exits only after a real MCP
|
|
39
46
|
handshake has counted the tools that answered.
|
|
40
47
|
|
|
48
|
+
Next, reconnect your coding host and [try your first decision and correction](#your-first-decision-and-correction).
|
|
49
|
+
A successful trial means a fresh session finds your recorded decision and its reason, then finds
|
|
50
|
+
the corrected decision as current. Check the returned source cards; a successful install alone
|
|
51
|
+
does not prove that your host has loaded the tools or that a particular decision was retrieved.
|
|
52
|
+
|
|
41
53
|
[](#supported-hosts-and-their-integration-level)
|
|
42
54
|
[](#supported-hosts-and-their-integration-level)
|
|
43
55
|
[](#supported-hosts-and-their-integration-level)
|
|
@@ -158,6 +170,9 @@ so running it from `src/` is fine), gives the project a brain if it doesn't have
|
|
|
158
170
|
agent tools you actually have installed, registers the card-level `.klypix` merge driver if it's a
|
|
159
171
|
git repo, and then **proves the result** before it exits:
|
|
160
172
|
|
|
173
|
+
Illustrative output; detected editors, file counts, tool counts and timings vary with the machine
|
|
174
|
+
and installed version:
|
|
175
|
+
|
|
161
176
|
```text
|
|
162
177
|
project E:\work\api (git repository root)
|
|
163
178
|
brain created brain.klypix — a starter brain, ready for its first decision
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -495,7 +495,7 @@ try {
|
|
|
495
495
|
// a newer klypix-format cannot. merge-brains and the driver ship here so
|
|
496
496
|
// brain-history's restore merge, the KLYPIX core and the git driver all
|
|
497
497
|
// find one engine in this directory.
|
|
498
|
-
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'live-projection.mjs', 'canvas-view-app.html']) {
|
|
498
|
+
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'live-projection.mjs', 'whose.mjs', 'whose-classify.mjs', 'whose-ledger.mjs', 'whose-rescue.mjs', 'canvas-view-app.html']) {
|
|
499
499
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
500
500
|
}
|
|
501
501
|
for (const [src, dst] of [
|
|
@@ -568,11 +568,16 @@ try {
|
|
|
568
568
|
['SessionStart', { matcher: 'startup|resume|clear', hooks: [{ type: 'command', command: brainCmd('') }] }],
|
|
569
569
|
['UserPromptSubmit', { hooks: [{ type: 'command', command: brainCmd('--prompt'), timeout: 10 }] }],
|
|
570
570
|
['Stop', { hooks: [{ type: 'command', command: brainCmd('--capture') }] }],
|
|
571
|
-
['PostToolUse', { matcher: 'Bash|PowerShell|Edit|Write', hooks: [{ type: 'command', command: brainCmd('--live'), timeout: 10 }] }],
|
|
571
|
+
['PostToolUse', { matcher: 'Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit', hooks: [{ type: 'command', command: brainCmd('--live'), timeout: 10 }] }],
|
|
572
572
|
// Guard cards (2026-08-24): pre-action lane. Fast path — reads only the
|
|
573
573
|
// compiled guard sidecar; 'warn' injects context, 'block' denies with
|
|
574
574
|
// the card's message. Absent guards = stat + one tiny read, then no-op.
|
|
575
575
|
['PreToolUse', { matcher: 'Bash|PowerShell|Edit|Write', hooks: [{ type: 'command', command: brainCmd('--guard'), timeout: 10 }] }],
|
|
576
|
+
// Session end (1.105.0): a closed or cut-off Claude Code session leaves
|
|
577
|
+
// the lane at once. Before this, only the 10-minute TTL removed it, so the
|
|
578
|
+
// next session read a dead peer as "live" and refused to continue its
|
|
579
|
+
// work (round-4 continuity bench, 2026-10-20). Codex already had this.
|
|
580
|
+
['SessionEnd', { hooks: [{ type: 'command', command: brainCmd('--end'), timeout: 10 }] }],
|
|
576
581
|
];
|
|
577
582
|
const stripOurs = (arr) => (Array.isArray(arr) ? arr : [])
|
|
578
583
|
.map(g => (g && Array.isArray(g.hooks)) ? { ...g, hooks: g.hooks.filter(h => !(typeof h?.command === 'string' && h.command.includes(HOOK_MARK))) } : g)
|
|
@@ -679,7 +684,7 @@ try {
|
|
|
679
684
|
console.log(`✓ installed klypix brain v${VERSION} → ${BRAIN_DIR} (${n} scripts, ${deps} dep packages)`);
|
|
680
685
|
reportSemantic(semanticStep);
|
|
681
686
|
if (RUNTIME_ONLY) console.log('✓ runtime-only update: host settings and project files were preserved');
|
|
682
|
-
else if (!notWired.length) console.log('✓ wired
|
|
687
|
+
else if (!notWired.length) console.log('✓ wired 6 hooks: SessionEnd (--end) · SessionStart · UserPromptSubmit (--prompt) · Stop (--capture) · PostToolUse (--live) · PreToolUse (--guard) → settings.json');
|
|
683
688
|
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}.`);
|
|
684
689
|
console.log(`✓ MCP supervisor runs from the local bundle (node ${fwd(path.join(BRAIN_DIR, 'klypix-mcp-server.mjs'))}) — compatible core updates activate without restarting the host.`);
|
|
685
690
|
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.`);
|
package/bin/klypix-mcp.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const PKG_VERSION = (() => {
|
|
|
19
19
|
}
|
|
20
20
|
})();
|
|
21
21
|
|
|
22
|
-
const DIRECT = new Set(['install', 'link', 'doctor', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'git-hook', 'brain-history', 'brain-deleted', 'orphans', 'diff', 'pr-brief', 'uninstall', 'bench', 'sessions']);
|
|
22
|
+
const DIRECT = new Set(['install', 'link', 'doctor', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'git-hook', 'brain-history', 'brain-deleted', 'orphans', 'diff', 'pr-brief', 'uninstall', 'bench', 'sessions', 'rescue']);
|
|
23
23
|
|
|
24
24
|
const USAGE = [
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
@@ -41,6 +41,8 @@ const USAGE = [
|
|
|
41
41
|
' git-hook [install|remove|status] wire the agent-neutral commit-capture hook (any agent/branch/worktree → brain cards)',
|
|
42
42
|
' brain-history [list|restore <id>] restore points for this brain — undo an accidental delete, edit, or overwrite',
|
|
43
43
|
' brain-deleted [list|restore|purge] recycle bin for this brain — cards you deleted, kept recoverable',
|
|
44
|
+
' rescue [list|show <id>|restore <id> [paths…]|prune]',
|
|
45
|
+
' copies of uncommitted files KLYPIX saved before a git command could discard them',
|
|
44
46
|
' orphans [brain] [--apply|--areas] orphan gardener backfill: report unconnected cards, link the confident subset (dry-run default)',
|
|
45
47
|
' diff [ref] [--brain <path>] readable brain diff vs a git ref (default HEAD) — markdown to stdout',
|
|
46
48
|
' pr-brief [baseRef] [--brain <path>] brain decisions touching the files changed since baseRef — PR-comment markdown',
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `klypix-mcp rescue [list|show <n|id>|restore <n|id> [paths…]|prune]`
|
|
3
|
+
// The human surface for the rescue copies KLYPIX takes before a git command
|
|
4
|
+
// could discard uncommitted files (src/whose-rescue.mjs). A copy nobody can
|
|
5
|
+
// find is a copy nobody trusts, so `list` is the default, `show` says exactly
|
|
6
|
+
// what a copy holds and what it left out, and `restore` says what it changed
|
|
7
|
+
// and how to undo it — a restore first saves the files it is about to replace.
|
|
8
|
+
//
|
|
9
|
+
// A restore writes the working tree only. It never stages, commits, switches a
|
|
10
|
+
// branch or touches the git index.
|
|
11
|
+
//
|
|
12
|
+
// ARGV: process.argv.slice(2) — the standalone shape. The dispatcher
|
|
13
|
+
// (bin/klypix-worker.mjs runVerb) removes its own `rescue` token first.
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import {
|
|
16
|
+
listRescuesResult, showRescue, restoreRescue, pruneRescues,
|
|
17
|
+
rescueRestoreCommand, RESCUE_KEEP, RESCUE_MAX_AGE_MS,
|
|
18
|
+
} from '../src/whose-rescue.mjs';
|
|
19
|
+
|
|
20
|
+
const USAGE = [
|
|
21
|
+
'klypix-mcp rescue — copies of uncommitted files KLYPIX saved before a git command could discard them.',
|
|
22
|
+
'',
|
|
23
|
+
'Usage:',
|
|
24
|
+
' npx klypix-mcp rescue [list] [--all] newest first (--all: every worktree of this repository)',
|
|
25
|
+
' npx klypix-mcp rescue show <n|id> what one copy holds, and what it left out',
|
|
26
|
+
' npx klypix-mcp rescue restore <n|id> [paths…] write its files back (all, or the paths/folders named)',
|
|
27
|
+
' npx klypix-mcp rescue prune [--days 14] [--keep 50] [--dry-run]',
|
|
28
|
+
'',
|
|
29
|
+
'Flags: --repo <dir> (default: the current folder), --json, --other-worktree (restore a copy taken in another worktree here).',
|
|
30
|
+
'<n> is the number `list` shows; an id may be shortened while it stays unique.',
|
|
31
|
+
'A restore first saves the files it replaces as a new copy, so it can be undone the same way.',
|
|
32
|
+
'It writes the working tree only: nothing is staged, committed or switched.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
const raw = process.argv.slice(2);
|
|
36
|
+
if (raw.includes('--help') || raw.includes('-h')) { console.log(USAGE); process.exit(0); }
|
|
37
|
+
|
|
38
|
+
const VALUE_FLAGS = new Set(['--repo', '--days', '--keep']);
|
|
39
|
+
const BOOL_FLAGS = new Set(['--json', '--all', '--dry-run', '--other-worktree']);
|
|
40
|
+
const flags = {};
|
|
41
|
+
const positional = [];
|
|
42
|
+
for (let i = 0; i < raw.length; i++) {
|
|
43
|
+
const a = raw[i];
|
|
44
|
+
if (a === '--') { positional.push(...raw.slice(i + 1)); break; }
|
|
45
|
+
if (VALUE_FLAGS.has(a)) {
|
|
46
|
+
if (i + 1 >= raw.length) { console.error(`klypix-mcp rescue: ${a} needs a value.\n\n${USAGE}`); process.exit(2); }
|
|
47
|
+
flags[a] = raw[++i];
|
|
48
|
+
} else if (BOOL_FLAGS.has(a)) flags[a] = true;
|
|
49
|
+
else if (a.startsWith('-') && a.length > 1) { console.error(`klypix-mcp rescue: unknown flag ${a}\n\n${USAGE}`); process.exit(2); }
|
|
50
|
+
else positional.push(a);
|
|
51
|
+
}
|
|
52
|
+
const ACTIONS = ['list', 'show', 'restore', 'prune'];
|
|
53
|
+
const action = ACTIONS.includes(positional[0]) ? positional.shift() : 'list';
|
|
54
|
+
if (action === 'list' && positional.length) {
|
|
55
|
+
console.error(`klypix-mcp rescue: unknown command "${positional[0]}".\n\n${USAGE}`);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const repoRoot = path.resolve(flags['--repo'] || process.cwd());
|
|
59
|
+
const json = Boolean(flags['--json']);
|
|
60
|
+
const out = (value) => console.log(JSON.stringify(value, null, 2));
|
|
61
|
+
|
|
62
|
+
const ago = (ts) => {
|
|
63
|
+
const m = Math.max(0, Math.round((Date.now() - ts) / 60000));
|
|
64
|
+
if (m < 1) return 'just now';
|
|
65
|
+
if (m < 60) return `${m}m ago`;
|
|
66
|
+
const h = Math.round(m / 60);
|
|
67
|
+
return h < 48 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
68
|
+
};
|
|
69
|
+
const utc = (ts) => new Date(ts).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
70
|
+
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
71
|
+
const SHOW = 12;
|
|
72
|
+
const listLines = (items, fmt) => {
|
|
73
|
+
for (const x of items.slice(0, SHOW)) console.log(` ${fmt(x)}`);
|
|
74
|
+
if (items.length > SHOW) console.log(` …and ${items.length - SHOW} more`);
|
|
75
|
+
};
|
|
76
|
+
const summary = (e) => {
|
|
77
|
+
const absent = e.deleted.length ? ` (${e.deleted.length} absent)` : '';
|
|
78
|
+
const left = e.skipped.length ? `, ${e.skipped.length} not included` : '';
|
|
79
|
+
return `${plural(e.paths.length, 'file')}${absent}${left}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (action === 'list') {
|
|
83
|
+
const res = listRescuesResult({ repoRoot, allWorktrees: Boolean(flags['--all']) });
|
|
84
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
85
|
+
if (json) { out(res.rescues); process.exit(0); }
|
|
86
|
+
if (!res.rescues.length) {
|
|
87
|
+
console.log(`No rescue copies in ${res.top}${flags['--all'] ? '' : ' (this worktree)'}.`);
|
|
88
|
+
if (res.elsewhere) console.log(`${plural(res.elsewhere, 'copy', 'copies')} taken in other worktrees of this repository: add --all.`);
|
|
89
|
+
console.log('KLYPIX takes one before a git command could discard uncommitted files.');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
console.log(`Rescue copies in ${res.top}, newest first:`);
|
|
93
|
+
// Numbers count this worktree's copies only — the same numbering `show <n>`
|
|
94
|
+
// and `restore <n>` use — so a copy from another worktree gets none.
|
|
95
|
+
let n = 0;
|
|
96
|
+
for (const e of res.rescues) {
|
|
97
|
+
const num = e.here ? String(++n) : '-';
|
|
98
|
+
const where = e.here ? '' : ` [${e.worktree}]`;
|
|
99
|
+
console.log(` ${num.padStart(3)} ${e.id} ${ago(e.at).padEnd(9)} ${e.reason} · ${summary(e)}${where}`);
|
|
100
|
+
}
|
|
101
|
+
if (!flags['--all'] && res.elsewhere) console.log(` (+${plural(res.elsewhere, 'copy', 'copies')} from other worktrees: add --all)`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Show one: npx klypix-mcp rescue show <n|id>');
|
|
104
|
+
console.log('Restore one: npx klypix-mcp rescue restore <n|id> [paths…]');
|
|
105
|
+
console.log('A restore saves the current files first, so it can be undone the same way.');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (action === 'show') {
|
|
110
|
+
if (!positional[0]) { console.error(`Usage: npx klypix-mcp rescue show <n|id>\n\n${USAGE}`); process.exit(2); }
|
|
111
|
+
const res = showRescue({ repoRoot, id: positional[0] });
|
|
112
|
+
if (json) { out(res); process.exit(res.ok ? 0 : 1); }
|
|
113
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
114
|
+
console.log(`Rescue ${res.id} — ${res.reason}`);
|
|
115
|
+
console.log(` taken ${ago(res.at)} (${utc(res.at)})${res.sid ? ` by session ${res.sid}` : ''}`);
|
|
116
|
+
console.log(` on ${res.branch || 'a detached HEAD'}${res.head ? ` @ ${res.head.slice(0, 10)}` : ' (no commit yet)'}${res.worktree ? `, in ${res.worktree}` : ''}${res.here ? '' : ' — ANOTHER worktree'}`);
|
|
117
|
+
console.log(` git ${res.ref} (commit ${res.commit.slice(0, 10)})`);
|
|
118
|
+
console.log(`\n${plural(res.files.length, 'file')}; ${res.stat.added} line(s) added and ${res.stat.removed} removed against that HEAD:`);
|
|
119
|
+
const width = Math.min(48, Math.max(...res.files.map((f) => f.path.length), 4));
|
|
120
|
+
listLines(res.files, (f) => {
|
|
121
|
+
const lines = f.binary ? 'binary' : f.change === 'deleted' || f.change === 'absent' ? '' : `+${f.added} -${f.removed}`;
|
|
122
|
+
return `${f.change.padEnd(12)} ${f.path.padEnd(width)} ${lines}`.trimEnd();
|
|
123
|
+
});
|
|
124
|
+
if (res.skipped.length) {
|
|
125
|
+
console.log('\nNot included:');
|
|
126
|
+
listLines(res.skipped, (s) => `${s.path} (${s.why})`);
|
|
127
|
+
}
|
|
128
|
+
console.log(`\nRestore: ${rescueRestoreCommand(res.id)} [paths…]`);
|
|
129
|
+
console.log(`Or with git: ${res.head ? `git diff ${res.commit.slice(0, 10)}^ ${res.commit.slice(0, 10)}` : `git show ${res.commit.slice(0, 10)}`}`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (action === 'restore') {
|
|
134
|
+
if (!positional[0]) {
|
|
135
|
+
console.error('Usage: npx klypix-mcp rescue restore <n|id> [paths…]');
|
|
136
|
+
console.error('Run `npx klypix-mcp rescue list` to see the copies.');
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
const [id, ...paths] = positional;
|
|
140
|
+
const res = restoreRescue({
|
|
141
|
+
repoRoot, id, paths: paths.length ? paths : null, sid: 'cli', cwd: process.cwd(),
|
|
142
|
+
allowOtherWorktree: Boolean(flags['--other-worktree']),
|
|
143
|
+
});
|
|
144
|
+
if (json) { out(res); process.exit(res.ok && !res.skipped?.length ? 0 : 1); }
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
console.error(`Restore failed: ${res.error}`);
|
|
147
|
+
if (res.skipped?.length) listLines(res.skipped, (s) => `${s.path} — ${s.why}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const written = res.restored.filter((p) => !res.deleted.includes(p));
|
|
151
|
+
if (res.restored.length) {
|
|
152
|
+
console.log(`Restored ${plural(res.restored.length, 'path')} from rescue ${res.from}:`);
|
|
153
|
+
listLines(written, (p) => p);
|
|
154
|
+
if (res.deleted.length) listLines(res.deleted, (p) => `removed ${p} (it did not exist when the copy was taken)`);
|
|
155
|
+
} else console.log(`Nothing needed writing from rescue ${res.from}.`);
|
|
156
|
+
if (res.unchanged.length) {
|
|
157
|
+
console.log('Already as in the copy:');
|
|
158
|
+
listLines(res.unchanged, (p) => p);
|
|
159
|
+
}
|
|
160
|
+
if (res.skipped.length) {
|
|
161
|
+
console.log('Left as it is:');
|
|
162
|
+
listLines(res.skipped, (s) => `${s.path} — ${s.why}`);
|
|
163
|
+
}
|
|
164
|
+
if (res.backupId) {
|
|
165
|
+
console.log(`\nWhat was there before is saved as ${res.backupId}. Undo this restore: ${rescueRestoreCommand(res.backupId)}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('Only the working tree changed: nothing was staged or committed. `git status` shows the result.');
|
|
168
|
+
process.exit(res.skipped.length ? 1 : 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// prune
|
|
172
|
+
const days = flags['--days'] != null ? Number(flags['--days']) : RESCUE_MAX_AGE_MS / 86400000;
|
|
173
|
+
const keep = flags['--keep'] != null ? Number(flags['--keep']) : RESCUE_KEEP;
|
|
174
|
+
if (!Number.isFinite(days) || days < 0 || !Number.isInteger(keep) || keep < 0) {
|
|
175
|
+
console.error(`klypix-mcp rescue prune: --days takes a number of days and --keep a whole number.\n\n${USAGE}`);
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
const res = pruneRescues({ repoRoot, keep, maxAgeMs: days * 86400000, dryRun: Boolean(flags['--dry-run']) });
|
|
179
|
+
if (json) { out(res); process.exit(res.ok ? 0 : 1); }
|
|
180
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
181
|
+
const rule = `older than ${days} day(s), keeping the newest ${keep} of each worktree`;
|
|
182
|
+
if (flags['--dry-run']) {
|
|
183
|
+
const would = res.wouldDelete || [];
|
|
184
|
+
console.log(would.length ? `Would prune ${plural(would.length, 'copy', 'copies')} ${rule}:` : `Nothing to prune (${rule}).`);
|
|
185
|
+
listLines(would, (x) => x);
|
|
186
|
+
console.log('DRY RUN — nothing was deleted.');
|
|
187
|
+
} else {
|
|
188
|
+
console.log(res.deleted.length ? `Pruned ${plural(res.deleted.length, 'copy', 'copies')} ${rule}; ${res.kept} remain.` : `Nothing to prune (${rule}); ${res.kept} kept.`);
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -190,6 +190,11 @@ await runVerb('pr-brief', './klypix-pr-brief.mjs');
|
|
|
190
190
|
// CLI's own --help) said no uninstall existed at all.
|
|
191
191
|
await runVerb('uninstall', './klypix-uninstall.mjs');
|
|
192
192
|
|
|
193
|
+
// `npx klypix-mcp rescue` — list, show and restore the rescue copies KLYPIX
|
|
194
|
+
// takes before a git command could discard uncommitted files (whose-rescue.mjs).
|
|
195
|
+
// A restore writes the working tree only, after saving what it replaces.
|
|
196
|
+
await runVerb('rescue', './klypix-rescue.mjs');
|
|
197
|
+
|
|
193
198
|
// `npx klypix-mcp garden-code` — the HUMAN half of the garden approval gate.
|
|
194
199
|
// brain_garden's apply requires an 8-char code derived from the exact dormant
|
|
195
200
|
// candidate set + day; the agent is deliberately never shown it. The human runs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.105.0",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
5
|
"description": "Shared project memory for Claude Code, Codex and Cursor: decisions and corrections through MCP.",
|
|
6
6
|
"type": "module",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
92
92
|
"test:bench": "node test/bench.mjs",
|
|
93
93
|
"pretest": "node test/publish-workflow.mjs",
|
|
94
|
-
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.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/session-mailbox.mjs && node test/session-reopen.mjs && node test/session-handoff.mjs && node test/lock-notes.mjs && node test/live-projection.mjs && node test/lane-write-retry.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/brain-ask-words.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/arrange-receipts.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/semantic-preparation.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/marker-after-colon.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/overlap-tiers.mjs && node test/coordination-bench.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/results-not-manifests.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.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/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/current-first.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
|
|
94
|
+
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.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/session-mailbox.mjs && node test/session-reopen.mjs && node test/session-handoff.mjs && node test/lock-notes.mjs && node test/whose-classify.mjs && node test/whose-ledger.mjs && node test/whose-rescue.mjs && node test/whose-hook.mjs && node test/live-projection.mjs && node test/lane-write-retry.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/brain-ask-words.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/arrange-receipts.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/semantic-preparation.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/marker-after-colon.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/overlap-tiers.mjs && node test/coordination-bench.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/results-not-manifests.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.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/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/current-first.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
|
|
95
95
|
"test:memory": "node test/memory-runtime.mjs",
|
|
96
96
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
97
97
|
"runtime": "node bin/klypix-runtime.mjs",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
104
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
104
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
105
105
|
"fractional-indexing": "^3.2.0",
|
|
106
106
|
"jpeg-js": "^0.4.4",
|
|
107
107
|
"jszip": "^3.10.1",
|
package/src/agent-rules.mjs
CHANGED
|
@@ -515,7 +515,9 @@ commits and the checkouts it worked in (with their uncommitted files) automatica
|
|
|
515
515
|
\`brain_sync\` may list **unfinished work** another session left without a card: if the human asks
|
|
516
516
|
you to continue it, take it with \`brain_handoff\` {action:"take", session:"<id>"} first, and look
|
|
517
517
|
first in the checkout the record names — it may be a side folder, not the project. That list is history, not a lock — a stopped session holds no claim on its
|
|
518
|
-
files.
|
|
518
|
+
files. A LIVE session's uncommitted edits are its work in progress: never sweep, discard or stash them
|
|
519
|
+
(\`git add -A\`, \`commit -a\`, \`reset --hard\`, \`stash\`); commit only your own files. A stopped session's leftovers
|
|
520
|
+
are yours to finish and commit when you are continuing its work; never discard them without the human. Never ask the human to paste a summary into the next session.
|
|
519
521
|
|
|
520
522
|
**A note that asks agents to keep off a file**, whatever its wording (here, in AGENTS.md / CLAUDE.md or any
|
|
521
523
|
notes file) can outlive the work it protected. Before you obey one, or before you edit the file
|
package/src/codex-brain-hook.mjs
CHANGED
|
@@ -91,29 +91,105 @@ function toolInput(input) {
|
|
|
91
91
|
return candidate && typeof candidate === 'object' ? candidate : {};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// A relative path resolves against `baseDir`, the session's working directory
|
|
95
|
+
// (see touchedFiles). A path outside the project returns null, including one
|
|
96
|
+
// on another drive, where path.relative returns an absolute path.
|
|
97
|
+
function projectRelative(file, projectDir, baseDir = projectDir) {
|
|
95
98
|
if (!file) return null;
|
|
96
|
-
const absolute = path.isAbsolute(file) ? path.normalize(file) : path.resolve(
|
|
97
|
-
const
|
|
99
|
+
const absolute = path.isAbsolute(file) ? path.normalize(file) : path.resolve(baseDir, file);
|
|
100
|
+
const fromProject = path.relative(projectDir, absolute);
|
|
101
|
+
if (path.isAbsolute(fromProject)) return null;
|
|
102
|
+
const relative = fromProject.replace(/\\/g, '/');
|
|
98
103
|
if (!relative || relative === '.' || relative === '..' || relative.startsWith('../')) return null;
|
|
99
104
|
return relative;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
// File paths an apply_patch body names, read the way Codex's own parser reads
|
|
108
|
+
// them (codex-rs/apply-patch/src/streaming_parser.rs, process_line):
|
|
109
|
+
// - Outside an Update hunk (right after *** Begin Patch, among an Add File's
|
|
110
|
+
// '+' lines, after a Delete File) a header is matched on the line with
|
|
111
|
+
// leading and trailing whitespace trimmed, so an indented header there is
|
|
112
|
+
// still a header and Codex edits that file.
|
|
113
|
+
// - Inside an Update hunk only trailing whitespace is trimmed, so a header
|
|
114
|
+
// must start at column 0 and an indented one is a context line. Hunk lines
|
|
115
|
+
// that start with '+', '-' or ' ' are never headers.
|
|
116
|
+
// - The marker includes its one space ("*** Update File: ") and the path is
|
|
117
|
+
// the rest of the line with nothing more trimmed, as Codex's PathBuf::from
|
|
118
|
+
// takes it: "*** Update File:a.md" is no header, and a double space names
|
|
119
|
+
// " a.md". An empty path is no header (trimming removes the space).
|
|
120
|
+
// - "*** Move to: " is read only on the line straight after an Update File
|
|
121
|
+
// header. A move touches the old path and the new one.
|
|
122
|
+
const APPLY_PATCH_HEADERS = [
|
|
123
|
+
['*** Add File: ', 'add'],
|
|
124
|
+
['*** Delete File: ', 'delete'],
|
|
125
|
+
['*** Update File: ', 'update'],
|
|
126
|
+
];
|
|
127
|
+
const APPLY_PATCH_MOVE = '*** Move to: ';
|
|
128
|
+
|
|
129
|
+
function applyPatchHeader(line) {
|
|
130
|
+
for (const [marker, mode] of APPLY_PATCH_HEADERS) {
|
|
131
|
+
if (line.startsWith(marker) && line.length > marker.length) return { mode, path: line.slice(marker.length) };
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function applyPatchPaths(patch) {
|
|
137
|
+
const paths = [];
|
|
138
|
+
let mode = 'started';
|
|
139
|
+
let moveAllowed = false;
|
|
140
|
+
for (const line of patch.trim().split(/\r?\n/)) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (trimmed === '*** Begin Patch') { mode = 'started'; moveAllowed = false; continue; }
|
|
143
|
+
if (trimmed === '*** End Patch') { mode = 'ended'; moveAllowed = false; continue; }
|
|
144
|
+
if (mode === 'ended') continue;
|
|
145
|
+
const candidate = mode === 'update' ? line.trimEnd() : trimmed;
|
|
146
|
+
const header = applyPatchHeader(candidate);
|
|
147
|
+
if (header) {
|
|
148
|
+
paths.push(header.path);
|
|
149
|
+
mode = header.mode;
|
|
150
|
+
moveAllowed = header.mode === 'update';
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (mode === 'update' && moveAllowed && candidate.startsWith(APPLY_PATCH_MOVE)
|
|
154
|
+
&& candidate.length > APPLY_PATCH_MOVE.length) {
|
|
155
|
+
paths.push(candidate.slice(APPLY_PATCH_MOVE.length));
|
|
156
|
+
}
|
|
157
|
+
moveAllowed = false;
|
|
158
|
+
}
|
|
159
|
+
return paths;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The apply_patch body. The official Codex hooks docs
|
|
163
|
+
// (https://learn.chatgpt.com/docs/hooks) say: "Bash and apply_patch use
|
|
164
|
+
// tool_input.command". Codex builds both PreToolUse and PostToolUse input as
|
|
165
|
+
// { tool_name: "apply_patch", tool_input: { command: <the raw patch> } }.
|
|
166
|
+
// Before 2026-10-16 only `patch`, `input` and `content` were read, so no real
|
|
167
|
+
// Codex edit was attributed. `command` is read first; those three stay as
|
|
168
|
+
// fallbacks, and a bare-string tool_input is taken as the patch itself.
|
|
169
|
+
function applyPatchBody(input) {
|
|
170
|
+
const args = toolInput(input);
|
|
171
|
+
const raw = input.tool_input ?? input.toolInput ?? input.tool?.input;
|
|
172
|
+
const command = Array.isArray(args.command) ? args.command : [args.command];
|
|
173
|
+
return [...command, args.patch, args.input, args.content, raw]
|
|
174
|
+
.find((value) => typeof value === 'string' && value.includes('*** ')) || '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Project-relative paths this tool call names. `cwd` is the session's working
|
|
178
|
+
// directory: Codex applies a patch's relative paths from there, and
|
|
179
|
+
// findProjectBrain walks UP from it, so the brain's folder can be an ancestor.
|
|
180
|
+
export function touchedFiles(input, projectDir,
|
|
181
|
+
cwd = typeof input?.cwd === 'string' && input.cwd ? path.resolve(input.cwd) : projectDir) {
|
|
103
182
|
const name = toolName(input);
|
|
104
183
|
const args = toolInput(input);
|
|
105
184
|
const candidates = [];
|
|
106
185
|
for (const key of ['path', 'file_path', 'filePath', 'target_file', 'targetFile']) {
|
|
107
186
|
if (typeof args[key] === 'string') candidates.push(args[key]);
|
|
108
187
|
}
|
|
109
|
-
const patch =
|
|
110
|
-
.find((value) => typeof value === 'string' && value.includes('*** '));
|
|
188
|
+
const patch = applyPatchBody(input);
|
|
111
189
|
if (patch && /apply_patch|Edit|Write/i.test(name)) {
|
|
112
|
-
|
|
113
|
-
candidates.push(match[1].trim());
|
|
114
|
-
}
|
|
190
|
+
candidates.push(...applyPatchPaths(patch));
|
|
115
191
|
}
|
|
116
|
-
return [...new Set(candidates.map((file) => projectRelative(file, projectDir)).filter(Boolean))];
|
|
192
|
+
return [...new Set(candidates.map((file) => projectRelative(file, projectDir, cwd)).filter(Boolean))];
|
|
117
193
|
}
|
|
118
194
|
|
|
119
195
|
function ownBrainMessageText(input) {
|
|
@@ -262,7 +338,7 @@ function formatConflictWarning(conflicts, event, sessions = []) {
|
|
|
262
338
|
// klypix-format predates guards degrades to silence, never a throw. This lane
|
|
263
339
|
// only ever WARNS: block-severity guards are named as such so the model treats
|
|
264
340
|
// them as a hard stop, but no mechanism here denies anything.
|
|
265
|
-
async function guardWarnings(projectDir, brainPath, input) {
|
|
341
|
+
async function guardWarnings(projectDir, brainPath, input, cwd) {
|
|
266
342
|
try {
|
|
267
343
|
if (typeof brainFormat.evaluateGuards !== 'function'
|
|
268
344
|
|| typeof brainFormat.ensureGuardSidecar !== 'function') return '';
|
|
@@ -273,7 +349,7 @@ async function guardWarnings(projectDir, brainPath, input) {
|
|
|
273
349
|
const ensured = await brainFormat.ensureGuardSidecar(brainPath);
|
|
274
350
|
const guards = Array.isArray(ensured.guards) ? ensured.guards : [];
|
|
275
351
|
if (!guards.length) return '';
|
|
276
|
-
const files = touchedFiles(input, projectDir);
|
|
352
|
+
const files = touchedFiles(input, projectDir, cwd);
|
|
277
353
|
const cmd = toolInput(input).command;
|
|
278
354
|
const needWt = guards.some((g) => g?.when?.multiWorktree);
|
|
279
355
|
const fired = brainFormat.evaluateGuards(guards, {
|
|
@@ -508,8 +584,16 @@ async function main() {
|
|
|
508
584
|
// while marking them observed, and skips any path the session's DECLARED
|
|
509
585
|
// scope (brain_sync) already covers.
|
|
510
586
|
const observed = event === 'PreToolUse' || event === 'PostToolUse'
|
|
511
|
-
? touchedFiles(input, projectDir)
|
|
587
|
+
? touchedFiles(input, projectDir, cwd)
|
|
512
588
|
: undefined;
|
|
589
|
+
// Whose is this? (1.104.0): remember the bytes Codex's edits left, so a Claude
|
|
590
|
+
// session's sweeping git command can name this session as the owner.
|
|
591
|
+
if (event === 'PostToolUse' && Array.isArray(observed) && observed.length) {
|
|
592
|
+
try {
|
|
593
|
+
const whose = await import(new URL('./whose.mjs', import.meta.url).href);
|
|
594
|
+
await whose.recordEdits({ brainPath, sid: sessionId, client: 'codex', cwd: projectDir, files: observed });
|
|
595
|
+
} catch { /* attribution is best-effort */ }
|
|
596
|
+
}
|
|
513
597
|
const files = humanTurn ? [] : undefined;
|
|
514
598
|
const workPatch = codexWorkPatch({ event, humanTurn, prompt, input }) || {};
|
|
515
599
|
// Where it works (1.103.0): the directory a tool call ran in (Codex's exec
|
|
@@ -653,7 +737,7 @@ async function main() {
|
|
|
653
737
|
// call — Codex hook output is `continue: true` by design — so a matching
|
|
654
738
|
// guard renders as a warning; a block-severity guard says it should be
|
|
655
739
|
// treated as a hard stop, without claiming a mechanism this lane lacks.
|
|
656
|
-
...(event === 'PreToolUse' ? [await guardWarnings(projectDir, brainPath, input)] : []),
|
|
740
|
+
...(event === 'PreToolUse' ? [await guardWarnings(projectDir, brainPath, input, cwd)] : []),
|
|
657
741
|
stampReceivedMessages(messages, Date.now(), undefined, sessionId),
|
|
658
742
|
], event);
|
|
659
743
|
return;
|
|
@@ -6262,8 +6262,62 @@ async function registerBrain() {
|
|
|
6262
6262
|
} catch { /* registry is best-effort */ }
|
|
6263
6263
|
}
|
|
6264
6264
|
|
|
6265
|
+
// "Whose is this?" (1.104.0): a risky git command that would sweep, discard or
|
|
6266
|
+
// stash ANOTHER session's uncommitted work gets deny / ask / context, and edits
|
|
6267
|
+
// are recorded so ownership is known. Runs before the brain-at-cwd check because
|
|
6268
|
+
// it finds the brain by walking up (an agent may have cd'd into a subfolder).
|
|
6269
|
+
// Cheap gates first: only edit tools (--live) or a command that mentions git
|
|
6270
|
+
// (--guard) pay the dynamic import. Never throws.
|
|
6271
|
+
const WHOSE_EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'edit_file', 'write_file']);
|
|
6272
|
+
async function whoseStep(isGuard) {
|
|
6273
|
+
try {
|
|
6274
|
+
const input = readHookInput();
|
|
6275
|
+
const tool = String(input.tool_name || '');
|
|
6276
|
+
if (!isGuard) {
|
|
6277
|
+
if (!WHOSE_EDIT_TOOLS.has(tool)) return null;
|
|
6278
|
+
const w = await import(new URL('./whose.mjs', import.meta.url).href);
|
|
6279
|
+
await w.recordFromClaudeHook(input);
|
|
6280
|
+
return null;
|
|
6281
|
+
}
|
|
6282
|
+
if (tool !== 'Bash' && tool !== 'PowerShell') return null;
|
|
6283
|
+
const cmd = input.tool_input && input.tool_input.command;
|
|
6284
|
+
if (typeof cmd !== 'string' || !/\bgit|\bgh\b/i.test(cmd)) return null;
|
|
6285
|
+
const w = await import(new URL('./whose.mjs', import.meta.url).href);
|
|
6286
|
+
let r = await w.decide(input);
|
|
6287
|
+
if (r && w.whoseMode() === 'shadow') r = w.shadowOf(r);
|
|
6288
|
+
return r;
|
|
6289
|
+
} catch { return null; }
|
|
6290
|
+
}
|
|
6291
|
+
function emitWhose(r) {
|
|
6292
|
+
if (!r || !r.text) return;
|
|
6293
|
+
const out = r.decision === 'deny' || r.decision === 'ask'
|
|
6294
|
+
? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: r.decision, permissionDecisionReason: r.text } }
|
|
6295
|
+
: { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: r.text } };
|
|
6296
|
+
try { fs.writeSync(1, JSON.stringify(out)); } catch { /* stdout gone */ }
|
|
6297
|
+
}
|
|
6298
|
+
|
|
6299
|
+
// --end (SessionEnd, 1.105.0): the session is over — remove its live rows and
|
|
6300
|
+
// tombstone it through the engine's own endSession (the same call Codex's
|
|
6301
|
+
// SessionEnd makes), so peers stop reading a dead session as working. Walks up
|
|
6302
|
+
// for the brain like Whose does. Never throws.
|
|
6303
|
+
async function endStep() {
|
|
6304
|
+
try {
|
|
6305
|
+
const input = readHookInput();
|
|
6306
|
+
const sid = String(input.session_id || '');
|
|
6307
|
+
if (!sid) return;
|
|
6308
|
+
const ap = await import(new URL('./agent-presence.mjs', import.meta.url).href);
|
|
6309
|
+
const brainPath = ap.findProjectBrain(input.cwd || process.cwd());
|
|
6310
|
+
if (brainPath) ap.endSession({ brainPath, id: sid });
|
|
6311
|
+
} catch { /* never fail a session's exit */ }
|
|
6312
|
+
}
|
|
6313
|
+
|
|
6265
6314
|
async function main() {
|
|
6266
|
-
if (
|
|
6315
|
+
if (process.argv.includes('--end')) { await endStep(); return; }
|
|
6316
|
+
const isLive = process.argv.includes('--live');
|
|
6317
|
+
const isGuard = process.argv.includes('--guard');
|
|
6318
|
+
const whose = (isLive || isGuard) ? await whoseStep(isGuard) : null;
|
|
6319
|
+
if (whose && (whose.decision === 'deny' || whose.decision === 'ask')) { emitWhose(whose); return; }
|
|
6320
|
+
if (!fs.existsSync(BRAIN)) { if (whose) emitWhose(whose); return; } // not a brain project → instant no-op
|
|
6267
6321
|
// --live (PostToolUse) is the cheapest mode: pure payload string-work + at most
|
|
6268
6322
|
// one tiny locked append. It must NOT pay the lazy klypix-format import, so it
|
|
6269
6323
|
// runs BEFORE everything else and returns. (Fires on Bash|PowerShell|Edit|Write.)
|
|
@@ -6271,7 +6325,15 @@ async function main() {
|
|
|
6271
6325
|
// --guard (PreToolUse) is the second fast path: stat + sidecar read on the
|
|
6272
6326
|
// common case; it lazy-imports the lib itself only when a compiled guard's
|
|
6273
6327
|
// tool pattern could match this call. Runs before the unconditional import.
|
|
6274
|
-
if (process.argv.includes('--guard')) {
|
|
6328
|
+
if (process.argv.includes('--guard')) {
|
|
6329
|
+
// A brain guard card's own decision wins; Whose context rides only when it said nothing.
|
|
6330
|
+
let wrote = false;
|
|
6331
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
6332
|
+
process.stdout.write = (...a) => { wrote = true; return orig(...a); };
|
|
6333
|
+
try { await guardCheck(); } finally { process.stdout.write = orig; }
|
|
6334
|
+
if (!wrote && whose) emitWhose(whose);
|
|
6335
|
+
return;
|
|
6336
|
+
}
|
|
6275
6337
|
const lib = await import('./klypix-format.mjs'); // lazy: only when a brain exists
|
|
6276
6338
|
// Per-prompt retrieval runs on EVERY prompt — skip the registry write and
|
|
6277
6339
|
// go straight to the (mtime-cached) ranked lookup to keep it cheap.
|
package/src/mcp-presence.mjs
CHANGED
|
@@ -69,7 +69,7 @@ export const KLYPIX_MCP_INSTRUCTIONS = [
|
|
|
69
69
|
'Use its active-task, message, and file-overlap report to coordinate concurrent work.',
|
|
70
70
|
'A coordination note is not consumed merely because it was offered: after a later independent KLYPIX action acknowledges the offer, call brain_message_receipt with its exact message id and offer token only when you actually incorporated it into your work. If you do not, a further independent action auto-consumes the note without a receipt — which records activity, not uptake — so the sender is told it was auto-consumed rather than acted on. Sending an explicit receipt is what makes your uptake visible to them.',
|
|
71
71
|
'Never make the human carry a message between agents: if you would otherwise ask the human to relay or paste something to another agent session on this project — live, idle, or recently closed — send it with brain_message addressed to that session id instead, and tell the human it was sent or queued; a closed session receives a directed note the moment it next starts. If the human wants a queued note handled sooner, offer brain_reopen: KLYPIX asks the human with a Reopen / Not now prompt and reopens that session only on yes. Hand the human the text only if brain_message refuses.',
|
|
72
|
-
'When you stop before the work is done — the human asks for a handoff, your context or usage limit is near, the day ends, you are blocked or switching tool — call brain_handoff {action:"write"} with the task, what is done, what is NEXT in order, the files and how to verify: it is a card in the brain the human can see, and the next session on this project (any agent) sees it at its first brain_sync and takes it with {action:"take"}; never ask the human to paste a summary into the next session. KLYPIX also records every session\'s last reply, files and commits automatically, so brain_sync may list "unfinished work" another session left: if the human asks you to continue it, take it with brain_handoff {action:"take", session} and finish with {action:"done"}; that list is history, not a lock — a stopped session holds no claim on its files. When a session stopped without writing a card, KLYPIX drafts one from that record at the next brain_sync (its card says "Drafted: by KLYPIX"): treat it as a lead, not a verified state — confirm with git before relying on it. A note that asks agents to keep off a file (whatever its wording: do not touch, hands off, WIP, someone else has it) can outlive its work: before obeying one, call brain_sync with that file in files — KLYPIX reports local sessions, branches and registered worktrees. Decide which kind of note it is: a temporary reservation for someone\'s in-flight change ends when that work ends; a permanent rule (generated file, policy, legal or human restriction) always holds. If a temporary reservation\'s work has ended and the task needs the file, change it there and tell the human (never delete the note yourself). Incomplete checks say UNKNOWN.',
|
|
72
|
+
'When you stop before the work is done — the human asks for a handoff, your context or usage limit is near, the day ends, you are blocked or switching tool — call brain_handoff {action:"write"} with the task, what is done, what is NEXT in order, the files and how to verify: it is a card in the brain the human can see, and the next session on this project (any agent) sees it at its first brain_sync and takes it with {action:"take"}; never ask the human to paste a summary into the next session. KLYPIX also records every session\'s last reply, files and commits automatically, so brain_sync may list "unfinished work" another session left: if the human asks you to continue it, take it with brain_handoff {action:"take", session} and finish with {action:"done"}; that list is history, not a lock — a stopped session holds no claim on its files. A LIVE session\'s uncommitted edits are its work in progress: never sweep, discard or stash them; commit only your own files. A stopped session\'s leftovers are yours to finish and commit when you are continuing its work; never discard them without the human. When a session stopped without writing a card, KLYPIX drafts one from that record at the next brain_sync (its card says "Drafted: by KLYPIX"): treat it as a lead, not a verified state — confirm with git before relying on it. A note that asks agents to keep off a file (whatever its wording: do not touch, hands off, WIP, someone else has it) can outlive its work: before obeying one, call brain_sync with that file in files — KLYPIX reports local sessions, branches and registered worktrees. Decide which kind of note it is: a temporary reservation for someone\'s in-flight change ends when that work ends; a permanent rule (generated file, policy, legal or human restriction) always holds. If a temporary reservation\'s work has ended and the task needs the file, change it there and tell the human (never delete the note yourself). Incomplete checks say UNKNOWN.',
|
|
73
73
|
'Call brain_sync again when your file scope materially changes, and with phase "complete" before your final response.',
|
|
74
74
|
'When a task publishes a quantified or otherwise machine-checkable claim, include its validated result manifest in the completion sync; conflicting or incomparable peer results retain the task scope until reconciled.',
|
|
75
75
|
'A session preparing a RELEASE of the project should declare it by adding releaseIntent {version, ref} to brain_sync: the first declarer takes an exclusive lease every peer sees, and a second declarer gets a hard conflict naming the holder.',
|