klypix-mcp 1.61.0 → 1.63.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 +1 -0
- package/bin/klypix-brain-history.mjs +75 -0
- package/bin/klypix-git-hook.mjs +33 -0
- package/bin/klypix-install.mjs +2 -2
- package/bin/klypix-mcp.mjs +3 -1
- package/bin/klypix-worker.mjs +9 -0
- package/package.json +2 -2
- package/src/agent-presence.mjs +86 -5
- package/src/brain-doctor.mjs +49 -1
- package/src/brain-git-hook.mjs +9 -1
- package/src/brain-history.mjs +229 -0
- package/src/codex-brain-hook.mjs +22 -3
- package/src/codex-hooks.mjs +13 -2
- package/src/git-capture-install.mjs +215 -0
- package/src/global-brain-hook.mjs +154 -14
- package/src/klypix-format.mjs +66 -1
- package/src/mcp-presence.mjs +8 -1
package/README.md
CHANGED
|
@@ -431,6 +431,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
431
431
|
| `npx klypix-mcp runtime` | Passive per-connection process/RAM attribution (`--json`, optional `--watch seconds`); never kills or deduplicates |
|
|
432
432
|
| `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
|
|
433
433
|
| `npx klypix-mcp git-driver` | Register the lossless `.klypix` merge driver for a repo (`status` to check) |
|
|
434
|
+
| `npx klypix-mcp git-hook` | Wire the agent-neutral commit-capture hook: rationale-bearing `feat`/`fix`/`perf` commits from any agent, branch, or worktree card into the brain at commit time (`install`/`remove`/`status`; sessions auto-install it where the hook slots are free) |
|
|
434
435
|
| `npx klypix-mcp diff [ref]` | Card-level brain diff against a git ref, as markdown |
|
|
435
436
|
| `npx klypix-mcp pr-brief [ref]` | Brain cards referencing the files changed since a ref, as markdown |
|
|
436
437
|
| `npx klypix-mcp garden-code` | Print the human approval code `brain_garden` requires |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `klypix-mcp brain-history [list|restore <id>|prune] [--brain <path>]`
|
|
3
|
+
// The human surface for brain restore points. Protection nobody can see is
|
|
4
|
+
// protection nobody trusts, so `list` is the default and it says plainly what
|
|
5
|
+
// each point would give back.
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { listBrainHistory, pruneBrainHistory, restoreBrainSnapshot, historyDirFor } from '../src/brain-history.mjs';
|
|
9
|
+
|
|
10
|
+
const argv = process.argv.slice(2).filter((a) => a !== 'brain-history');
|
|
11
|
+
const action = ['list', 'restore', 'prune'].includes(argv[0]) ? argv.shift() : 'list';
|
|
12
|
+
const brainIdx = argv.indexOf('--brain');
|
|
13
|
+
const brainPath = path.resolve(brainIdx >= 0 && argv[brainIdx + 1] ? argv.splice(brainIdx, 2)[1] : 'brain.klypix');
|
|
14
|
+
const positional = argv.filter((a) => !a.startsWith('-'));
|
|
15
|
+
|
|
16
|
+
const ago = (ts) => {
|
|
17
|
+
const m = Math.max(0, Math.round((Date.now() - ts) / 60000));
|
|
18
|
+
if (m < 60) return `${m}m ago`;
|
|
19
|
+
const h = Math.round(m / 60);
|
|
20
|
+
return h < 48 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
21
|
+
};
|
|
22
|
+
const kb = (b) => `${(b / 1024).toFixed(0)} KB`;
|
|
23
|
+
|
|
24
|
+
// Card counts make a restore point meaningful ("this one still has the 14 cards
|
|
25
|
+
// you deleted"). Parsing ≤20 small zips is fine for a human-invoked command;
|
|
26
|
+
// a parse failure degrades to size only rather than failing the listing.
|
|
27
|
+
async function cardCount(file) {
|
|
28
|
+
try {
|
|
29
|
+
const { parseKlypix } = await import('../src/klypix-format.mjs');
|
|
30
|
+
const { struct } = await parseKlypix(fs.readFileSync(file));
|
|
31
|
+
return struct?.cards?.length ?? null;
|
|
32
|
+
} catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (action === 'list') {
|
|
36
|
+
const entries = listBrainHistory(brainPath);
|
|
37
|
+
if (!entries.length) {
|
|
38
|
+
console.log(`No restore points for ${brainPath}.`);
|
|
39
|
+
console.log(`They are written automatically before each brain write, to ${historyDirFor(brainPath)}.`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
const liveExists = fs.existsSync(brainPath);
|
|
43
|
+
const liveCards = liveExists ? await cardCount(brainPath) : null;
|
|
44
|
+
console.log(`Restore points for ${brainPath}${liveExists ? '' : ' (the brain itself is MISSING — restore will recreate it)'}`);
|
|
45
|
+
if (liveCards != null) console.log(`current: ${liveCards} cards, ${kb(fs.statSync(brainPath).size)}\n`);
|
|
46
|
+
for (const e of entries) {
|
|
47
|
+
const cards = await cardCount(e.file);
|
|
48
|
+
const delta = cards != null && liveCards != null ? cards - liveCards : null;
|
|
49
|
+
const deltaText = delta == null ? '' : delta > 0 ? ` (+${delta} cards vs now)` : delta < 0 ? ` (${delta} cards vs now)` : ' (same card count)';
|
|
50
|
+
console.log(` ${e.id} ${ago(e.ts).padEnd(9)} ${String(cards ?? '?').padStart(5)} cards ${kb(e.bytes).padStart(8)}${e.reason ? ` [${e.reason}]` : ''}${deltaText}`);
|
|
51
|
+
}
|
|
52
|
+
console.log(`\nRestore: npx klypix-mcp brain-history restore <id> --brain "${brainPath}"`);
|
|
53
|
+
console.log('Restoring snapshots the current file first, so it is itself undoable.');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action === 'prune') {
|
|
58
|
+
const removed = pruneBrainHistory(brainPath);
|
|
59
|
+
console.log(`Pruned ${removed} restore point(s) beyond the retention window (newest 20 + one per day for 14 days).`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// restore
|
|
64
|
+
const id = positional[0];
|
|
65
|
+
if (!id) {
|
|
66
|
+
console.error('Usage: npx klypix-mcp brain-history restore <id> [--brain <path>]');
|
|
67
|
+
console.error('Run `npx klypix-mcp brain-history list` to see the ids.');
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
const { parseKlypix } = await import('../src/klypix-format.mjs');
|
|
71
|
+
const res = await restoreBrainSnapshot(brainPath, id, { parse: parseKlypix });
|
|
72
|
+
if (!res.ok) { console.error(`Restore failed: ${res.error}`); process.exit(1); }
|
|
73
|
+
console.log(`Restored ${brainPath} from ${res.restoredFrom} (${kb(res.bytes)}).`);
|
|
74
|
+
if (res.safetyId) console.log(`The state you just replaced was saved as ${res.safetyId} — undo with: npx klypix-mcp brain-history restore ${res.safetyId}`);
|
|
75
|
+
console.log('If the app has this brain OPEN, close and reopen the tab: its in-memory copy is now older than disk and a save would merge it back.');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `klypix-mcp git-hook [install|remove|status] [repo]` — wire the agent-neutral
|
|
3
|
+
// commit-capture hook (brain-git-hook.mjs) into a repo, so rationale-bearing
|
|
4
|
+
// feat/fix/perf commits from ANY agent/branch/worktree card into ./brain.klypix
|
|
5
|
+
// at commit time. Standalone use: node bin/klypix-git-hook.mjs <args>
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { gitCaptureHookStatus, installGitCaptureHook, removeGitCaptureHook } from '../src/git-capture-install.mjs';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2).filter((a) => a !== 'git-hook');
|
|
10
|
+
const action = ['install', 'remove', 'status'].includes(args[0]) ? args[0] : 'status';
|
|
11
|
+
const repo = path.resolve(process.cwd(), args.find((a, i) => i > 0 || !['install', 'remove', 'status'].includes(a)) || '.');
|
|
12
|
+
|
|
13
|
+
const describe = (s) => `state: ${s.state}${s.hooksDir ? `\nhooks dir: ${s.hooksDir}` : ''}${s.hooks && Object.keys(s.hooks).length
|
|
14
|
+
? '\n' + Object.entries(s.hooks).map(([n, st]) => ` ${n}: ${st}`).join('\n') : ''}`;
|
|
15
|
+
|
|
16
|
+
if (action === 'status') {
|
|
17
|
+
const s = gitCaptureHookStatus(repo);
|
|
18
|
+
console.log(describe(s));
|
|
19
|
+
if (s.state === 'custom-hookspath') console.log('core.hooksPath is set — add the managed block to that hooks dir yourself, or unset it and re-run.');
|
|
20
|
+
if (s.state === 'foreign') console.log('A non-sh hook occupies the slot — not touched. Chain it manually or convert it to sh.');
|
|
21
|
+
process.exit(s.state === 'installed' ? 0 : 1);
|
|
22
|
+
} else if (action === 'install') {
|
|
23
|
+
const r = installGitCaptureHook(repo);
|
|
24
|
+
if (!r.ok) { console.error(`Not installed: ${r.reason}.`); process.exit(1); }
|
|
25
|
+
console.log(r.changed.length ? `Installed/updated: ${r.changed.join(', ')} in ${r.hooksDir}` : 'Already current — nothing to change.');
|
|
26
|
+
if (r.skipped.length) console.log(`Skipped (non-sh foreign hook, not touched): ${r.skipped.join(', ')}`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
} else {
|
|
29
|
+
const r = removeGitCaptureHook(repo);
|
|
30
|
+
if (!r.ok) { console.error(`Nothing removed: ${r.reason}.`); process.exit(1); }
|
|
31
|
+
console.log(r.changed.length ? `Removed the managed block from: ${r.changed.join(', ')}` : 'No managed block found — nothing to remove.');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -226,7 +226,7 @@ const flatten = (code) => code
|
|
|
226
226
|
.replace(/\.\.\/src\/klypix-(core|format)\.mjs/g, './klypix-$1.mjs')
|
|
227
227
|
// brain-doctor + agent-rules (the server's lazy `import('../src/brain-doctor.mjs')`
|
|
228
228
|
// for the brain_doctor tool) → flat sibling refs in the runtime layout.
|
|
229
|
-
.replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor|mcp-auto-update|semantic-memory|runtime-inspector|project-graph)\.mjs/g, './$1.mjs')
|
|
229
|
+
.replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor|mcp-auto-update|semantic-memory|runtime-inspector|project-graph|git-capture-install)\.mjs/g, './$1.mjs')
|
|
230
230
|
.replace(/klypix-worker\.mjs/g, 'klypix-mcp-worker.mjs')
|
|
231
231
|
.replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
|
|
232
232
|
|
|
@@ -293,7 +293,7 @@ try {
|
|
|
293
293
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
294
294
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
295
295
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
296
|
-
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'finding-routing.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
296
|
+
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'finding-routing.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
297
297
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
298
298
|
}
|
|
299
299
|
for (const [src, dst] of [
|
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', 'diff', 'pr-brief', 'uninstall', 'bench']);
|
|
22
|
+
const DIRECT = new Set(['install', 'link', 'doctor', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'git-hook', 'brain-history', 'diff', 'pr-brief', 'uninstall', 'bench']);
|
|
23
23
|
|
|
24
24
|
const USAGE = [
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
@@ -35,6 +35,8 @@ const USAGE = [
|
|
|
35
35
|
' garden-code [brain] print the human approval code for brain_garden',
|
|
36
36
|
' uninstall [--check|--yes|unlink] remove this install from the machine (--check inventories first; never deletes a .klypix)',
|
|
37
37
|
' git-driver [install|status] [repo] register the lossless .klypix merge driver for a repo (zero-command teams)',
|
|
38
|
+
' git-hook [install|remove|status] wire the agent-neutral commit-capture hook (any agent/branch/worktree → brain cards)',
|
|
39
|
+
' brain-history [list|restore <id>] restore points for this brain — undo an accidental delete, edit, or overwrite',
|
|
38
40
|
' diff [ref] [--brain <path>] readable brain diff vs a git ref (default HEAD) — markdown to stdout',
|
|
39
41
|
' pr-brief [baseRef] [--brain <path>] brain decisions touching the files changed since baseRef — PR-comment markdown',
|
|
40
42
|
'',
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -116,6 +116,15 @@ await runVerb('conformance', './klypix-conformance.mjs');
|
|
|
116
116
|
// git ref, and print the brain cards touching a PR's changed files. One module,
|
|
117
117
|
// three verbs (it reads argv[2] itself).
|
|
118
118
|
await runVerb('git-driver', './klypix-git-driver.mjs');
|
|
119
|
+
// `npx klypix-mcp git-hook` — wire the agent-neutral commit-capture hook
|
|
120
|
+
// (brain-git-hook.mjs) into a repo's post-commit/post-merge, so feat/fix/perf
|
|
121
|
+
// commits with rationale bodies card into the brain from ANY agent, branch, or
|
|
122
|
+
// worktree at commit time (the Stop hook alone is blind to other worktrees).
|
|
123
|
+
await runVerb('git-hook', './klypix-git-hook.mjs');
|
|
124
|
+
// `npx klypix-mcp brain-history` — list/restore the automatic restore points
|
|
125
|
+
// written before every brain write. The recovery path for an accidental card
|
|
126
|
+
// deletion, a destructive edit, a stale overwrite, or a deleted brain file.
|
|
127
|
+
await runVerb('brain-history', './klypix-brain-history.mjs');
|
|
119
128
|
await runVerb('diff', './klypix-diff.mjs');
|
|
120
129
|
await runVerb('pr-brief', './klypix-pr-brief.mjs');
|
|
121
130
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.63.0",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"test:project-graph": "node test/project-graph.mjs",
|
|
80
80
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
81
81
|
"test:bench": "node test/bench.mjs",
|
|
82
|
-
"test": "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/agent-presence.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.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/brain-connect-orphans.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/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
82
|
+
"test": "node test/publish-verdict.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.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/brain-connect-orphans.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/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
83
83
|
"test:memory": "node test/memory-runtime.mjs",
|
|
84
84
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
85
85
|
"runtime": "node bin/klypix-runtime.mjs"
|
package/src/agent-presence.mjs
CHANGED
|
@@ -119,13 +119,88 @@ export function neutralizeMarkers(text) {
|
|
|
119
119
|
return String(text || '').replace(/🧠(\s*)(BRAIN|MSG)/gi, '🧠·$2');
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// ── Machine-turn intent guard ───────────────────────────────────────────────
|
|
123
|
+
// UserPromptSubmit does not only carry human-typed text: harnesses inject task
|
|
124
|
+
// notifications, system reminders and slash-command wrappers as "user" prompts.
|
|
125
|
+
// One of those stored verbatim became a session's declared intent
|
|
126
|
+
// ("<task-notification> <task-id>…" — 2026-08-07 AgentLit field incident), so
|
|
127
|
+
// every peer surface showed tool-plumbing instead of what the session was doing.
|
|
128
|
+
// deriveIntentFromPrompt() extracts the human intent from a raw prompt, or
|
|
129
|
+
// returns null when the whole turn is machine-generated — callers must then KEEP
|
|
130
|
+
// the previous intent (and may still stamp activity), never store the junk and
|
|
131
|
+
// never clear a good intent because a background task happened to complete.
|
|
132
|
+
const MACHINE_TAGS = 'task-notification|system-reminder|system-warning|local-command-caveat'
|
|
133
|
+
+ '|command-name|command-message|command-args|command-contents|local-command-stdout|local-command-stderr'
|
|
134
|
+
+ '|ide_selection|ide_opened_file|ide_diagnostics|persisted-output|tool-use-error'
|
|
135
|
+
+ '|session-start-hook|user-prompt-submit-hook|post-tool-use-hook|hook-[a-z0-9-]+';
|
|
136
|
+
const MACHINE_BLOCK_RE = new RegExp(
|
|
137
|
+
'^(?:<(' + MACHINE_TAGS + ')\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)|\\[SYSTEM NOTIFICATION[^\\]]*\\])\\s*', 'i');
|
|
138
|
+
// Tag-shaped start AFTER stripping known blocks: an unrecognized harness tag is
|
|
139
|
+
// far more likely than a human opening a prompt with raw XML, and the cost of
|
|
140
|
+
// failing closed is only "previous intent kept" — never data loss.
|
|
141
|
+
const TAG_SHAPED_RE = /^<[a-z][\w.:-]*(?:\s|\/?>)/i;
|
|
142
|
+
|
|
143
|
+
export function deriveIntentFromPrompt(raw) {
|
|
144
|
+
let text = String(raw || '').replace(/\s+/g, ' ').trim();
|
|
145
|
+
if (!text) return null;
|
|
146
|
+
// A "[SYSTEM NOTIFICATION …]"-led turn is machine end to end — the prose
|
|
147
|
+
// after the bracket header is harness narration, never the user's intent.
|
|
148
|
+
if (/^\[SYSTEM NOTIFICATION/i.test(text)) return null;
|
|
149
|
+
for (let hops = 0; hops < 12; hops++) {
|
|
150
|
+
const m = MACHINE_BLOCK_RE.exec(text);
|
|
151
|
+
if (!m) break;
|
|
152
|
+
text = text.slice(m[0].length).trim();
|
|
153
|
+
}
|
|
154
|
+
if (!text || TAG_SHAPED_RE.test(text)) return null;
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// True when a non-empty prompt/intent value is machine-generated end to end.
|
|
159
|
+
// Empty strings are NOT machine turns — brain_sync phase "complete" clears
|
|
160
|
+
// intent with '' deliberately, and that semantic must keep working.
|
|
161
|
+
export function looksMachineTurn(text) {
|
|
162
|
+
const t = String(text || '').trim();
|
|
163
|
+
return Boolean(t) && deriveIntentFromPrompt(t) === null;
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
// tmp+rename so lock-free readers (readLane, messageFooter, peers' status
|
|
123
167
|
// lines) can never parse a torn lane as an authoritative "0 peers / no
|
|
124
168
|
// messages", and a crash mid-write can never destroy undelivered messages.
|
|
169
|
+
// On Windows, renaming over a destination a reader/AV momentarily holds open
|
|
170
|
+
// throws EPERM — one immediate retry wins that race, and on final failure the
|
|
171
|
+
// tmp is REMOVED before rethrowing (the field found dozens of orphaned
|
|
172
|
+
// `.tmp-<pid>-<rand>` files littering the sessions dir, 2026-08-07).
|
|
125
173
|
function writeLaneFileAtomic(laneFile, payload) {
|
|
126
174
|
const tmp = `${laneFile}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
127
175
|
fs.writeFileSync(tmp, payload);
|
|
128
|
-
|
|
176
|
+
try {
|
|
177
|
+
fs.renameSync(tmp, laneFile);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
try { fs.renameSync(tmp, laneFile); }
|
|
180
|
+
catch { try { fs.unlinkSync(tmp); } catch { /* best-effort */ } throw err; }
|
|
181
|
+
}
|
|
182
|
+
sweepStaleTmpFiles(path.dirname(laneFile));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Opportunistic janitor for tmp orphans left by crashes or the pre-fix rename
|
|
186
|
+
// path. Throttled to once per process per 10 minutes; only files matching our
|
|
187
|
+
// own tmp naming and older than 15 minutes are touched, so an in-flight write
|
|
188
|
+
// (millisecond lifetime) can never be swept.
|
|
189
|
+
let lastTmpSweep = 0;
|
|
190
|
+
export function sweepStaleTmpFiles(dir, { now = Date.now(), maxAgeMs = 15 * 60 * 1000, force = false } = {}) {
|
|
191
|
+
if (!force && now - lastTmpSweep < 10 * 60 * 1000) return 0;
|
|
192
|
+
lastTmpSweep = now;
|
|
193
|
+
let swept = 0;
|
|
194
|
+
try {
|
|
195
|
+
for (const name of fs.readdirSync(dir)) {
|
|
196
|
+
if (!/\.tmp-\d+(?:-[a-z0-9]+)?$|\.\d+\.tmp$/.test(name)) continue;
|
|
197
|
+
const full = path.join(dir, name);
|
|
198
|
+
try {
|
|
199
|
+
if (now - fs.statSync(full).mtimeMs > maxAgeMs) { fs.unlinkSync(full); swept++; }
|
|
200
|
+
} catch { /* raced or locked — next sweep */ }
|
|
201
|
+
}
|
|
202
|
+
} catch { /* dir unreadable — best-effort */ }
|
|
203
|
+
return swept;
|
|
129
204
|
}
|
|
130
205
|
|
|
131
206
|
function freshChannelSeen(channelSeen, now) {
|
|
@@ -242,10 +317,14 @@ export function upsertSession({
|
|
|
242
317
|
// that never touch intent, so without intentAt a 100-minute-old intent renders
|
|
243
318
|
// under "active just now" (2026-07-29 audit). Stamped only when the intent
|
|
244
319
|
// VALUE actually changes; additive — old rows/readers are unaffected.
|
|
245
|
-
|
|
246
|
-
|
|
320
|
+
// Defense-in-depth: a machine-generated value (harness notification, raw
|
|
321
|
+
// XML tag) is treated as "no update" — the derivation layer in the hooks
|
|
322
|
+
// is the primary guard, this catches any writer that skipped it.
|
|
323
|
+
const effectiveIntent = intent !== undefined && looksMachineTurn(intent) ? undefined : intent;
|
|
324
|
+
const nextIntent = effectiveIntent !== undefined
|
|
325
|
+
? String(effectiveIntent || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
247
326
|
: (previous.intent || '');
|
|
248
|
-
const intentChanged =
|
|
327
|
+
const intentChanged = effectiveIntent !== undefined && nextIntent !== (previous.intent || '');
|
|
249
328
|
// A connection heartbeat proves transport liveness, not that a task is in
|
|
250
329
|
// progress. Stamp only events that carry real user/tool work so doctor can
|
|
251
330
|
// distinguish an idle connected host from an active sync-silent session.
|
|
@@ -330,7 +409,9 @@ export function upsertRemoteSessions({ brainPath, rows, machineId = MACHINE_ID,
|
|
|
330
409
|
client: String(row.client || previous.client || 'unknown'),
|
|
331
410
|
surface: row.surface ?? previous.surface ?? null,
|
|
332
411
|
branch: row.branch ?? previous.branch ?? null,
|
|
333
|
-
|
|
412
|
+
// Same machine-turn guard as the local writer: a peer machine running a
|
|
413
|
+
// pre-guard build can relay junk intent — never mirror it verbatim.
|
|
414
|
+
intent: looksMachineTurn(row.intent) ? String(previous.intent || '') : String(row.intent || '').slice(0, 160),
|
|
334
415
|
files: normalizeFiles(row.files),
|
|
335
416
|
machine: String(row.machine),
|
|
336
417
|
host: row.host ?? previous.host ?? null,
|
package/src/brain-doctor.mjs
CHANGED
|
@@ -39,6 +39,10 @@ import { renderReceiptSummary, summarizeReceipts } from './finding-routing.mjs';
|
|
|
39
39
|
// inspect() synchronous for its existing callers.
|
|
40
40
|
let fmtLib = null;
|
|
41
41
|
try { fmtLib = await import('./klypix-format.mjs'); } catch { fmtLib = null; }
|
|
42
|
+
let gitCaptureLib = null;
|
|
43
|
+
try { gitCaptureLib = await import('./git-capture-install.mjs'); } catch { gitCaptureLib = null; }
|
|
44
|
+
let historyLib = null;
|
|
45
|
+
try { historyLib = await import('./brain-history.mjs'); } catch { historyLib = null; }
|
|
42
46
|
|
|
43
47
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
44
48
|
|
|
@@ -372,6 +376,24 @@ export function inspect(opts = {}) {
|
|
|
372
376
|
mcpInstructions: true,
|
|
373
377
|
globalInstructions: codexGlobalInstructionsInstalled(home),
|
|
374
378
|
};
|
|
379
|
+
// Commit-capture git hook (2026-08-07): informational, never a drift layer —
|
|
380
|
+
// SessionStart auto-installs it where safe; doctor reports the cases it can't
|
|
381
|
+
// (foreign hook in the slot, core.hooksPath set, brain nested below the repo
|
|
382
|
+
// top). Guarded dynamic import: on a half-updated bundle that lacks the
|
|
383
|
+
// module, doctor must keep working, not crash at load (review-caught).
|
|
384
|
+
let gitCapture = { state: 'n/a', hooks: {} };
|
|
385
|
+
if (hasBrain && gitCaptureLib && typeof gitCaptureLib.gitCaptureHookStatus === 'function') {
|
|
386
|
+
try { gitCapture = gitCaptureLib.gitCaptureHookStatus(projectDir, { home }); } catch { gitCapture = { state: 'error', hooks: {} }; }
|
|
387
|
+
}
|
|
388
|
+
// Restore points (2026-08-07). Informational: their absence on a brand-new
|
|
389
|
+
// brain is normal, and a missing history must never read as machine drift.
|
|
390
|
+
let history = { available: false, count: 0, newestAt: null };
|
|
391
|
+
if (hasBrain && historyLib && typeof historyLib.listBrainHistory === 'function') {
|
|
392
|
+
try {
|
|
393
|
+
const points = historyLib.listBrainHistory(brainPath, { home });
|
|
394
|
+
history = { available: true, count: points.length, newestAt: points[0]?.ts || null };
|
|
395
|
+
} catch { history = { available: true, count: 0, newestAt: null }; }
|
|
396
|
+
}
|
|
375
397
|
const tools = inspectTools(brainDir, PKG_ROOT);
|
|
376
398
|
const env = opts.env || process.env;
|
|
377
399
|
// MCP passes the adopted live id explicitly. The CLI can inherit a host id,
|
|
@@ -418,6 +440,9 @@ export function inspect(opts = {}) {
|
|
|
418
440
|
: (!codexHooks.installed
|
|
419
441
|
? 'optional'
|
|
420
442
|
: (codexHooks.executionStatus === 'observed' ? 'ok' : 'warning')),
|
|
443
|
+
// Informational only — a repo the auto-installer can't wire (foreign hook,
|
|
444
|
+
// custom hooksPath) must never flip the machine verdict to DRIFTED.
|
|
445
|
+
gitCapture: gitCapture.state === 'installed' ? 'ok' : (gitCapture.state === 'n/a' ? 'n/a' : 'optional'),
|
|
421
446
|
harness: hasBrain ? (harness.ok ? 'ok' : 'drift') : 'n/a',
|
|
422
447
|
// DECAY-GUARD drifts when the protection is provably missing: the loaded
|
|
423
448
|
// engine lacks classifyDecay, its renderer left the synthetic stale claim
|
|
@@ -456,9 +481,14 @@ export function inspect(opts = {}) {
|
|
|
456
481
|
if (layers.decayGuard === 'drift') actions.push('npx klypix-mcp install # decay-aware status guard missing/stale — stale build/deploy claims can render as CURRENT state');
|
|
457
482
|
for (const s of supervisors.impaired || []) actions.push(`/mcp reconnect # supervisor pid ${s.pid} is ${s.status} with no live worker — its tool calls hang until reconnected`);
|
|
458
483
|
if (peers.twinGroups && peers.twinGroups.length) actions.push(`/mcp reconnect # ${peers.twinGroups.length} session(s) split across twin lane rows (channel merge not occurring) — reconnect adopts the merged id`);
|
|
484
|
+
if (hasBrain && (gitCapture.state === 'foreign' || Object.values(gitCapture.hooks || {}).some(s => s === 'foreign' || s === 'foreign-sh'))) {
|
|
485
|
+
actions.push('npx klypix-mcp git-hook install # a pre-existing git hook occupies post-commit/post-merge — this chains the commit-capture block after it (auto-install never edits a foreign hook)');
|
|
486
|
+
} else if (hasBrain && gitCapture.state === 'custom-hookspath') {
|
|
487
|
+
actions.push('git config core.hooksPath is set # commit-capture hook not auto-installable — add the managed block to that hooks dir or unset the config, then `npx klypix-mcp git-hook install`');
|
|
488
|
+
}
|
|
459
489
|
}
|
|
460
490
|
|
|
461
|
-
return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
|
|
491
|
+
return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, gitCapture, history, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
|
|
462
492
|
}
|
|
463
493
|
|
|
464
494
|
// One-line drift summary (empty when clean) — for a footer / status line.
|
|
@@ -580,6 +610,24 @@ export function render(r, opts = {}) {
|
|
|
580
610
|
} else {
|
|
581
611
|
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}`);
|
|
582
612
|
}
|
|
613
|
+
// Commit-capture git hook — per-repo, informational (auto-installed at
|
|
614
|
+
// session start where safe; foreign hooks are never edited automatically).
|
|
615
|
+
if (r.gitCapture && r.gitCapture.state !== 'n/a') {
|
|
616
|
+
const gc = r.gitCapture;
|
|
617
|
+
const gmark = gc.state === 'installed' ? ok : warn;
|
|
618
|
+
const detail = gc.state === 'installed' ? 'post-commit/post-merge wired — any agent/branch/worktree cards its rationale-bearing commits'
|
|
619
|
+
: gc.state === 'custom-hookspath' ? `${c.yel}core.hooksPath set — not auto-installable (see actions)${c.rst}`
|
|
620
|
+
: gc.state === 'foreign' ? `${c.yel}foreign hook in the slot — run \`npx klypix-mcp git-hook install\` to chain it deliberately${c.rst}`
|
|
621
|
+
: gc.state === 'no-git' ? `${c.dim}not a git repo${c.rst}`
|
|
622
|
+
: `${c.dim}${gc.state} — installs automatically at the next session start${c.rst}`;
|
|
623
|
+
L.push(`${gmark} ${c.bold}GIT-CAP${c.rst} ${detail}`);
|
|
624
|
+
}
|
|
625
|
+
// Restore points — say the count and the age, because "you can undo an
|
|
626
|
+
// accidental delete" is only believable if the machine can show the receipts.
|
|
627
|
+
if (r.history?.available) {
|
|
628
|
+
const age = r.history.newestAt ? `${Math.max(0, Math.round((Date.now() - r.history.newestAt) / 60000))}m ago` : 'none yet';
|
|
629
|
+
L.push(`${ok} ${c.bold}HISTORY${c.rst} ${r.history.count} restore point(s) · newest ${age} ${c.dim}(npx klypix-mcp brain-history list)${c.rst}`);
|
|
630
|
+
}
|
|
583
631
|
|
|
584
632
|
// TOOLS
|
|
585
633
|
L.push(`${ok} ${c.bold}TOOLS${c.rst} ${r.tools.count} MCP verb(s)${r.tools.hash ? ` ${c.dim}[#${r.tools.hash}, ${r.tools.source}]${c.rst}` : ''}${r.tools.count ? `: ${c.dim}${r.tools.names.join(', ')}${c.rst}` : ''}`);
|
package/src/brain-git-hook.mjs
CHANGED
|
@@ -60,7 +60,15 @@ function parseLog(raw) {
|
|
|
60
60
|
return { hash: (p[0] || '').trim(), subject: (p[1] || '').trim(), body: (p[2] || '').trim() };
|
|
61
61
|
}).filter(c => c.hash && c.subject);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// The baseline is repo-writable and gets interpolated into git commands — accept
|
|
64
|
+
// ONLY a bare sha, or a malicious checkout gains shell execution at commit time
|
|
65
|
+
// (review-caught 2026-08-07). An invalid file re-baselines like a missing one.
|
|
66
|
+
const readPrev = () => {
|
|
67
|
+
try {
|
|
68
|
+
const s = fs.readFileSync(STATE, 'utf8').trim();
|
|
69
|
+
return /^[0-9a-f]{4,64}$/i.test(s) ? s : null;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
};
|
|
64
72
|
const writePrev = (s) => { try { fs.mkdirSync(path.dirname(STATE), { recursive: true }); fs.writeFileSync(STATE, String(s || '')); } catch { /* best-effort */ } };
|
|
65
73
|
|
|
66
74
|
async function main() {
|