klypix-mcp 1.68.0 → 1.70.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/bin/klypix-install.mjs +78 -5
- package/bin/klypix-mcp.mjs +2 -1
- package/bin/klypix-orphans.mjs +168 -0
- package/bin/klypix-worker.mjs +11 -1
- package/package.json +4 -3
- package/src/agent-presence.mjs +478 -12
- package/src/brain-doctor.mjs +93 -10
- package/src/codex-brain-hook.mjs +48 -11
- package/src/global-brain-hook.mjs +158 -9
- package/src/install-version.mjs +32 -1
- package/src/klypix-format.mjs +155 -1
- package/src/mcp-presence.mjs +383 -21
- package/src/mcp-supervisor.mjs +4 -0
- package/src/presence-relay.mjs +13 -0
- package/src/repo-state.mjs +283 -0
package/bin/klypix-install.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// npx klypix-mcp install # install / update the brain on this machine
|
|
11
11
|
// npx klypix-mcp install --force # overwrite even a newer / dev-deployed brain
|
|
12
12
|
// npx klypix-mcp install --codex-hooks # optional prompt/file awareness; Codex asks for trust
|
|
13
|
+
// npx klypix-mcp install --allow-untagged # acknowledge deploying an UNTAGGED source checkout
|
|
14
|
+
// # (dev deploy; also KLYPIX_MCP_ALLOW_UNTAGGED=1)
|
|
13
15
|
//
|
|
14
16
|
// Never-throws-silently: it's an explicit CLI, so it reports what it did and exits
|
|
15
17
|
// non-zero on a real failure. Never wires a broken settings.json (refuse + restore).
|
|
@@ -17,7 +19,7 @@ import fs from 'fs';
|
|
|
17
19
|
import os from 'os';
|
|
18
20
|
import path from 'path';
|
|
19
21
|
import crypto from 'crypto';
|
|
20
|
-
import { spawn } from 'child_process';
|
|
22
|
+
import { execFileSync, spawn } from 'child_process';
|
|
21
23
|
import { fileURLToPath } from 'url';
|
|
22
24
|
import {
|
|
23
25
|
connectCodexMcpServer,
|
|
@@ -29,8 +31,9 @@ import {
|
|
|
29
31
|
codexPresenceHookStatus,
|
|
30
32
|
mergeCodexPresenceHooks,
|
|
31
33
|
} from '../src/codex-hooks.mjs';
|
|
32
|
-
import { brainInstallDecision } from '../src/install-version.mjs';
|
|
34
|
+
import { brainInstallDecision, deploySourceDecision } from '../src/install-version.mjs';
|
|
33
35
|
import { acquireInstallLockSync, releaseInstallLockSync } from '../src/install-lock.mjs';
|
|
36
|
+
import { collectRepoState } from '../src/repo-state.mjs';
|
|
34
37
|
|
|
35
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
39
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
@@ -45,6 +48,11 @@ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.join(PKG_R
|
|
|
45
48
|
const FORCE = process.argv.includes('--force');
|
|
46
49
|
const CODEX_HOOKS = process.argv.includes('--codex-hooks');
|
|
47
50
|
const RUNTIME_ONLY = process.argv.includes('--runtime-only');
|
|
51
|
+
// Released-tag deploy-guard acknowledgement. Deliberately a SEPARATE axis from
|
|
52
|
+
// --force: --force is destination authority (overwrite what is installed),
|
|
53
|
+
// this is source authority (knowingly deploy an untagged working tree).
|
|
54
|
+
const ALLOW_UNTAGGED = process.argv.includes('--allow-untagged')
|
|
55
|
+
|| process.env.KLYPIX_MCP_ALLOW_UNTAGGED === '1';
|
|
48
56
|
|
|
49
57
|
const HOME = os.homedir();
|
|
50
58
|
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
@@ -224,6 +232,52 @@ try {
|
|
|
224
232
|
if (!RUNTIME_ONLY) reportCodex(wireCodex());
|
|
225
233
|
process.exit(0);
|
|
226
234
|
}
|
|
235
|
+
|
|
236
|
+
// ── Released-tag deploy guard (2026-08-14 bundle-currency incident) ──────
|
|
237
|
+
// A source checkout whose HEAD does not carry the release tag for its own
|
|
238
|
+
// package version is UNRELEASED code, yet this installer used to lay it
|
|
239
|
+
// down machine-globally with receipts claiming a clean npm delivery — the
|
|
240
|
+
// exact twin of the desktop near-miss that shipped this wave. Only
|
|
241
|
+
// PKG_ROOT's own git state is interrogated (never process.cwd(): the
|
|
242
|
+
// auto-update child runs the installer with the PROJECT as cwd), and only
|
|
243
|
+
// when PKG_ROOT itself contains a .git entry — an npm/npx tarball install
|
|
244
|
+
// has none and IS the released artifact, so it stays exempt; the registry
|
|
245
|
+
// channel is already tag-bound by publish.yml. The tag must point at HEAD
|
|
246
|
+
// (`git tag --points-at HEAD` semantics inside collectRepoState): the
|
|
247
|
+
// release tag names the exact evidence commit, so any non-HEAD comparison
|
|
248
|
+
// would certify code the tag never covered.
|
|
249
|
+
const checkout = exists(path.join(PKG_ROOT, '.git')) ? collectRepoState(PKG_ROOT) : null;
|
|
250
|
+
const sourceDecision = deploySourceDecision({ checkout, allowUntagged: ALLOW_UNTAGGED });
|
|
251
|
+
const checkoutLabel = `v${VERSION}, branch ${checkout?.branch || '(detached)'}, head ${checkout?.headShort || '?'}`;
|
|
252
|
+
if (sourceDecision.action === 'refuse') {
|
|
253
|
+
releaseInstallLockSync(installLock);
|
|
254
|
+
console.error(`✗ refusing to deploy an UNRELEASED source checkout machine-globally: ${checkoutLabel} — no release tag v${VERSION} at HEAD; no files were changed.`);
|
|
255
|
+
console.error(' Released installs come from the registry: npx -y klypix-mcp@latest install');
|
|
256
|
+
console.error(' To deliberately deploy this working tree (a dev deploy), acknowledge it: re-run with --allow-untagged or KLYPIX_MCP_ALLOW_UNTAGGED=1.');
|
|
257
|
+
console.error(' An acknowledged dev deploy is stamped dev-owned, so brain_doctor shows it and auto-update will not silently replace it.');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const untaggedSource = sourceDecision.source === 'untagged-working-tree';
|
|
261
|
+
// Deploy-time snapshot for ANY git-checkout deploy (tagged or acknowledged-
|
|
262
|
+
// untagged): the release tag certifies HEAD's bytes, not the working
|
|
263
|
+
// tree's, so uncommitted changes make even a TAGGED deploy differ from
|
|
264
|
+
// what was released — a clean `dirty:false` stamp would lie about exactly
|
|
265
|
+
// that (the receipt-honesty defect this wave exists to kill). An
|
|
266
|
+
// unreadable status degrades to false rather than inventing a DIRTY nag
|
|
267
|
+
// in every session brief. Tarball installs (checkout null) skip the spawn.
|
|
268
|
+
const sourceDirty = Boolean(checkout) && (() => {
|
|
269
|
+
try {
|
|
270
|
+
return execFileSync('git', ['status', '--porcelain'], {
|
|
271
|
+
cwd: PKG_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500,
|
|
272
|
+
}).trim().length > 0;
|
|
273
|
+
} catch { return false; }
|
|
274
|
+
})();
|
|
275
|
+
if (untaggedSource) {
|
|
276
|
+
console.log(`• untagged source deploy acknowledged: ${checkoutLabel}${sourceDirty ? ' (working tree DIRTY)' : ''} — receipts record a dev-owned install; a later released install needs --force.`);
|
|
277
|
+
} else if (sourceDirty) {
|
|
278
|
+
console.log(`⚠ release-tagged checkout has uncommitted changes (${checkoutLabel}) — deployed bytes differ from the release; stamping dirty:true so brain_doctor surfaces it.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
227
281
|
fs.mkdirSync(BRAIN_DIR, { recursive: true });
|
|
228
282
|
// 1) runtime dependency CLOSURE (jszip+fractional-indexing for the hook/engine,
|
|
229
283
|
// @modelcontextprotocol/sdk+zod for the local MCP server). Resolve each via
|
|
@@ -281,7 +335,7 @@ try {
|
|
|
281
335
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
282
336
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
283
337
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
284
|
-
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', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'remote-client.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
338
|
+
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', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.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', 'remote-client.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
285
339
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
286
340
|
}
|
|
287
341
|
for (const [src, dst] of [
|
|
@@ -344,19 +398,38 @@ try {
|
|
|
344
398
|
// a crash between receipts remains recoverable because the next installer
|
|
345
399
|
// compares both and keeps the highest valid Brain Core version.
|
|
346
400
|
const installedAt = new Date().toISOString();
|
|
401
|
+
// Receipt honesty (2026-08-14): an acknowledged untagged source deploy is
|
|
402
|
+
// a DEV delivery — stamping it 'npm'/dirty:false made unreleased code
|
|
403
|
+
// byte-for-byte indistinguishable from a released install. 'dev' is
|
|
404
|
+
// existing receipt vocabulary (brainInstallDecision's dev-owned preserve,
|
|
405
|
+
// auto-update's dev skip, doctor's channel/dev/dirty rendering all consume
|
|
406
|
+
// it); sourceSha/branch are additive fields for post-hoc audit. dev:true
|
|
407
|
+
// goes in BOTH receipts because the runtime receipt commits first — a
|
|
408
|
+
// crash between the two writes must not leave a dev deploy unmarked.
|
|
347
409
|
const runtime = {
|
|
348
410
|
protocol: 1,
|
|
349
411
|
version: VERSION,
|
|
350
412
|
worker: 'klypix-mcp-worker.mjs',
|
|
351
|
-
channel: 'npm',
|
|
413
|
+
channel: untaggedSource ? 'dev' : 'npm',
|
|
414
|
+
...(untaggedSource ? { dev: true, sourceSha: checkout?.headShort || null, branch: checkout?.branch || null } : {}),
|
|
352
415
|
installedAt,
|
|
353
416
|
files: Object.fromEntries(staged.map(st => [st.dst, crypto.createHash('sha256').update(st.content).digest('hex')])),
|
|
354
417
|
};
|
|
355
418
|
const runtimePath = path.join(BRAIN_DIR, '.mcp-runtime.json');
|
|
356
419
|
fs.writeFileSync(runtimePath + '.klypix-new', JSON.stringify(runtime, null, 2) + '\n', 'utf8');
|
|
357
420
|
fs.renameSync(runtimePath + '.klypix-new', runtimePath);
|
|
421
|
+
// A tagged-but-DIRTY checkout keeps via:'npm' (the tag still names the
|
|
422
|
+
// payload identity, and dev:true would stop auto-update from healing the
|
|
423
|
+
// machine back to clean released bytes) but stamps dirty:true + the audit
|
|
424
|
+
// fields — doctor's DIRTY line and the hook's dirty nag both read the
|
|
425
|
+
// stamp. The clean released path stays byte-identical to pre-guard.
|
|
426
|
+
const versionStamp = untaggedSource
|
|
427
|
+
? { brainVersion: VERSION, via: 'dev', dev: true, dirty: sourceDirty, sourceSha: checkout?.headShort || null, branch: checkout?.branch || null, installedAt }
|
|
428
|
+
: sourceDirty
|
|
429
|
+
? { brainVersion: VERSION, via: 'npm', dirty: true, sourceSha: checkout?.headShort || null, branch: checkout?.branch || null, installedAt }
|
|
430
|
+
: { brainVersion: VERSION, via: 'npm', dirty: false, installedAt };
|
|
358
431
|
const versionPath = path.join(BRAIN_DIR, '.brain-version.json');
|
|
359
|
-
fs.writeFileSync(versionPath + '.klypix-new', JSON.stringify(
|
|
432
|
+
fs.writeFileSync(versionPath + '.klypix-new', JSON.stringify(versionStamp, null, 2), 'utf8');
|
|
360
433
|
fs.renameSync(versionPath + '.klypix-new', versionPath);
|
|
361
434
|
|
|
362
435
|
// 7) migrate THIS project's .mcp.json off npx onto the now-installed local bundle
|
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', 'diff', 'pr-brief', 'uninstall', 'bench']);
|
|
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']);
|
|
23
23
|
|
|
24
24
|
const USAGE = [
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
@@ -38,6 +38,7 @@ const USAGE = [
|
|
|
38
38
|
' git-hook [install|remove|status] wire the agent-neutral commit-capture hook (any agent/branch/worktree → brain cards)',
|
|
39
39
|
' brain-history [list|restore <id>] restore points for this brain — undo an accidental delete, edit, or overwrite',
|
|
40
40
|
' brain-deleted [list|restore|purge] recycle bin for this brain — cards you deleted, kept recoverable',
|
|
41
|
+
' orphans [brain] [--apply|--areas] orphan gardener backfill: report unconnected cards, link the confident subset (dry-run default)',
|
|
41
42
|
' diff [ref] [--brain <path>] readable brain diff vs a git ref (default HEAD) — markdown to stdout',
|
|
42
43
|
' pr-brief [baseRef] [--brain <path>] brain decisions touching the files changed since baseRef — PR-comment markdown',
|
|
43
44
|
'',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// klypix-orphans — `npx klypix-mcp orphans`. The orphan gardener's backfill pass:
|
|
3
|
+
// report how many live decision/milestone cards have NO connections, and link the
|
|
4
|
+
// CONFIDENT subset in one pass using the same selector capture uses
|
|
5
|
+
// (proposeOrphanAnchorLinks): at most ONE lexical-anchor edge per card — exact
|
|
6
|
+
// [[wikilink]], exact file-slug tag, or an unambiguous ≥0.6 title overlap —
|
|
7
|
+
// never a fan-out, never a guess among multiple candidates.
|
|
8
|
+
//
|
|
9
|
+
// npx klypix-mcp orphans [brain] # DRY RUN (default) — report, write nothing
|
|
10
|
+
// npx klypix-mcp orphans --apply # draw the confident edges (restore point first)
|
|
11
|
+
// npx klypix-mcp orphans --areas # ALSO include each orphan's area-container edge
|
|
12
|
+
// npx klypix-mcp orphans --json # machine-readable report
|
|
13
|
+
//
|
|
14
|
+
// Contract: additive + lossless — no card is archived, rewritten, or moved; every
|
|
15
|
+
// arrow remains individually removable in KLYPIX. `--apply` takes a FORCED restore
|
|
16
|
+
// point via the brain-history machinery before writing, so the whole pass is one
|
|
17
|
+
// `brain-history restore` away from undone. Dry-run is the default because a
|
|
18
|
+
// backfill touches history, not just this session's capture.
|
|
19
|
+
//
|
|
20
|
+
// `--areas` is opt-in for backfill (unlike capture, where the area edge is part
|
|
21
|
+
// of the no-new-orphans contract): mass-adding hundreds of containment edges to
|
|
22
|
+
// an old brain is a layout decision the human should make deliberately.
|
|
23
|
+
//
|
|
24
|
+
// Synchronous top-level by design: the dispatcher does `await import(this);
|
|
25
|
+
// process.exit(0)`, so all work must complete during module evaluation. ARGV is
|
|
26
|
+
// the standalone shape — the dispatcher splices its verb out first (runVerb).
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { parseKlypix, brainInsights, proposeOrphanAnchorLinks, addBrainConnections, atomicWrite } from '../src/klypix-format.mjs';
|
|
30
|
+
import { snapshotBrain } from '../src/brain-history.mjs';
|
|
31
|
+
import { withAdvisoryWriteLock, brainCaptureLockPath } from '../src/brain-write-lock.mjs';
|
|
32
|
+
|
|
33
|
+
const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const raw = process.argv.slice(2);
|
|
37
|
+
const argv = (raw[0] === 'orphans' && !isDir(path.resolve(raw[0]))) ? raw.slice(1) : raw;
|
|
38
|
+
const apply = argv.includes('--apply');
|
|
39
|
+
const withAreas = argv.includes('--areas');
|
|
40
|
+
const json = argv.includes('--json');
|
|
41
|
+
const pathArg = argv.find(a => !a.startsWith('-'));
|
|
42
|
+
const file = path.resolve(process.cwd(), pathArg || 'brain.klypix');
|
|
43
|
+
if (!fs.existsSync(file)) {
|
|
44
|
+
console.error(`No brain at ${file} — run from the project folder (or pass a path).`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { struct } = await parseKlypix(fs.readFileSync(file));
|
|
49
|
+
const before = brainInsights(struct).orphans;
|
|
50
|
+
const byId = new Map(struct.cards.map(c => [c.id, c]));
|
|
51
|
+
const flat = (id) => {
|
|
52
|
+
const c = byId.get(id);
|
|
53
|
+
if (!c) return id;
|
|
54
|
+
const s = c.type === 'container' ? `[area] ${c.title || ''}` : String(c.text || '').replace(/\s+/g, ' ').trim();
|
|
55
|
+
return s.slice(0, 70);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const proposals = proposeOrphanAnchorLinks(struct);
|
|
59
|
+
const anchored = proposals.filter(p => p.anchor);
|
|
60
|
+
// The confident subset: anchor edges always; area edges only with --areas.
|
|
61
|
+
// Reversed twins (A anchors B, B anchors A) collapse to ONE edge here so the
|
|
62
|
+
// dry-run list, the drawn count, and the apply receipt all agree exactly.
|
|
63
|
+
const seenPair = new Set();
|
|
64
|
+
const edges = [];
|
|
65
|
+
for (const p of proposals) {
|
|
66
|
+
if (p.anchor) {
|
|
67
|
+
const key = [p.cardId, p.anchor.toId].sort().join('|');
|
|
68
|
+
if (!seenPair.has(key)) {
|
|
69
|
+
seenPair.add(key);
|
|
70
|
+
edges.push({ fromId: p.cardId, toId: p.anchor.toId, relationship: 'relates_to', label: 'auto', why: p.anchor.why });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (withAreas && p.areaId) edges.push({ fromId: p.cardId, toId: p.areaId, relationship: 'relates_to', label: 'in area', color: 'rgba(120,120,135,0.45)', width: 1, style: 'dashed', why: 'area container' });
|
|
74
|
+
}
|
|
75
|
+
// An anchor edge between two orphans repairs BOTH endpoints — project on the
|
|
76
|
+
// orphan set, not just the source card.
|
|
77
|
+
const orphanIdSet = new Set(before.map(o => o.id));
|
|
78
|
+
const repairable = new Set();
|
|
79
|
+
for (const e of edges) {
|
|
80
|
+
if (orphanIdSet.has(e.fromId)) repairable.add(e.fromId);
|
|
81
|
+
if (orphanIdSet.has(e.toId)) repairable.add(e.toId);
|
|
82
|
+
}
|
|
83
|
+
const projected = Math.max(0, before.length - repairable.size);
|
|
84
|
+
|
|
85
|
+
if (json && !apply) {
|
|
86
|
+
console.log(JSON.stringify({
|
|
87
|
+
brain: file, dryRun: true, orphans: before.length, confident: anchored.length,
|
|
88
|
+
projectedOrphans: projected,
|
|
89
|
+
edges: edges.map(e => ({ fromId: e.fromId, toId: e.toId, label: e.label, why: e.why })),
|
|
90
|
+
}, null, 2));
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!apply) {
|
|
95
|
+
console.log(`# orphan gardener — ${path.basename(file)} (DRY RUN, nothing written)`);
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(`Orphans now: ${before.length} live decision/milestone card(s) with no connections.`);
|
|
98
|
+
console.log(`Confident subset: ${anchored.length} card(s) have exactly one unambiguous lexical anchor${withAreas ? `; --areas adds ${proposals.filter(p => p.areaId).length} area-container edge(s)` : ''}.`);
|
|
99
|
+
console.log(`Projected after --apply: ${projected} orphan(s) remain (the rest need brain_connect review or a human).`);
|
|
100
|
+
if (edges.length) {
|
|
101
|
+
console.log('');
|
|
102
|
+
for (const e of edges) console.log(` - ${flat(e.fromId)} → ${flat(e.toId)} (${e.why})`);
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('Re-run with --apply to draw these. Additive only: no card is archived or rewritten;');
|
|
105
|
+
console.log('a forced restore point is taken first (undo: npx klypix-mcp brain-history).');
|
|
106
|
+
} else {
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(before.length
|
|
109
|
+
? 'No edge clears the confidence bar — review the rest with brain_connect (dry-run receipts).'
|
|
110
|
+
: 'Nothing to do — this brain has no orphaned decision/milestone cards.');
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!edges.length) {
|
|
116
|
+
console.log(before.length
|
|
117
|
+
? `No edge clears the confidence bar — ${before.length} orphan(s) remain for brain_connect review. Nothing written.`
|
|
118
|
+
: 'Nothing to do — this brain has no orphaned decision/milestone cards. Nothing written.');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// APPLY — one locked snapshot + read-modify-write. The restore point is taken
|
|
123
|
+
// INSIDE the advisory lock (forced: an additive pass never trips the shrink
|
|
124
|
+
// rule, and the throttle must not skip the one snapshot that makes a whole
|
|
125
|
+
// backfill reversible): taken outside it, a concurrent capture landing between
|
|
126
|
+
// snapshot and write would be silently rolled back by a later restore.
|
|
127
|
+
let snap = null;
|
|
128
|
+
const result = await withAdvisoryWriteLock(brainCaptureLockPath(file), async (locked) => {
|
|
129
|
+
if (!locked) return { ok: false, error: 'brain is busy (another writer holds the capture lock) — retry in a moment; nothing written' };
|
|
130
|
+
snap = snapshotBrain(file, { reason: 'orphan-backfill', force: true });
|
|
131
|
+
const { buffer, added } = await addBrainConnections(fs.readFileSync(file), edges);
|
|
132
|
+
if (!added) return { ok: true, added: 0, after: before.length };
|
|
133
|
+
await atomicWrite(file, buffer, { snapshot: false }); // the forced pre-apply snapshot above is the restore point
|
|
134
|
+
let after = projected;
|
|
135
|
+
try { after = brainInsights((await parseKlypix(buffer)).struct).orphans.length; } catch { /* projected stays the honest fallback */ }
|
|
136
|
+
return { ok: true, added, after };
|
|
137
|
+
});
|
|
138
|
+
if (!result.ok) { console.error(`✗ ${result.error}`); process.exit(1); }
|
|
139
|
+
|
|
140
|
+
// A drawn count below the planned count means the brain changed between the
|
|
141
|
+
// dry-run read and the locked write (a pair already present, an endpoint
|
|
142
|
+
// gone) — slicing the planned list by count would then name the WRONG edges,
|
|
143
|
+
// so the per-edge list is only printed when it is exact.
|
|
144
|
+
const exact = result.added === edges.length;
|
|
145
|
+
// An 'unchanged' skip means the newest restore point already holds these
|
|
146
|
+
// exact bytes — the pass is just as reversible as a fresh snapshot.
|
|
147
|
+
const covered = snap && (snap.saved || snap.skipped === 'unchanged');
|
|
148
|
+
if (json) {
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
brain: file, dryRun: false, added: result.added,
|
|
151
|
+
orphansBefore: before.length, orphansAfter: result.after,
|
|
152
|
+
restorePoint: covered ? (snap.id || true) : null,
|
|
153
|
+
edges: exact ? edges.map(e => ({ fromId: e.fromId, toId: e.toId, label: e.label, why: e.why })) : null,
|
|
154
|
+
...(exact ? {} : { edgesNote: `${edges.length - result.added} planned edge(s) were skipped (already present or endpoint missing) — re-run the dry run for the current pair list` }),
|
|
155
|
+
}, null, 2));
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
console.log(`✓ Drew ${result.added} additive connection(s). Orphan receipt: ${before.length} → ${result.after} (${Math.max(0, before.length - result.after)} repaired).`);
|
|
159
|
+
console.log(covered
|
|
160
|
+
? 'Restore point taken first — undo the whole pass: npx klypix-mcp brain-history'
|
|
161
|
+
: 'Restore point: not saved (best-effort) — arrows remain individually removable in KLYPIX.');
|
|
162
|
+
if (exact) for (const e of edges) console.log(` - ${flat(e.fromId)} → ${flat(e.toId)} (${e.why})`);
|
|
163
|
+
else console.log(` (${edges.length - result.added} planned edge(s) were already present or lost an endpoint — re-run the dry run for the current pair list)`);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error(`✗ orphans failed (brain unchanged): ${e?.message || e}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -131,6 +131,11 @@ await runVerb('brain-history', './klypix-brain-history.mjs');
|
|
|
131
131
|
// the card's bytes to graveyard/ instead of destroying them; this lists, restores
|
|
132
132
|
// and (permanently) purges them.
|
|
133
133
|
await runVerb('brain-deleted', './klypix-brain-deleted.mjs');
|
|
134
|
+
// `npx klypix-mcp orphans` — the orphan gardener's backfill: report how many live
|
|
135
|
+
// cards sit outside the graph, link the CONFIDENT subset (one unambiguous lexical
|
|
136
|
+
// anchor each — never a fan-out). Dry-run by default; --apply takes a forced
|
|
137
|
+
// restore point first, so the whole pass is one brain-history restore from undone.
|
|
138
|
+
await runVerb('orphans', './klypix-orphans.mjs');
|
|
134
139
|
await runVerb('diff', './klypix-diff.mjs');
|
|
135
140
|
await runVerb('pr-brief', './klypix-pr-brief.mjs');
|
|
136
141
|
|
|
@@ -868,8 +873,12 @@ server.registerTool('brain_sync', {
|
|
|
868
873
|
// marker before rejecting them. A strict transport schema rejected first,
|
|
869
874
|
// allowing a later result-less completion to bypass that state entirely.
|
|
870
875
|
results: z.unknown().optional().describe('On phase complete, 1-8 result manifests for stable claim keys. The in-handler versioned validator rejects malformed, empty, unknown-field, or incomparable evidence and retains task scope.'),
|
|
876
|
+
releaseIntent: z.object({
|
|
877
|
+
version: z.string().max(64).describe('The version this session intends to release (e.g. "1.70.0").'),
|
|
878
|
+
ref: z.string().max(200).describe('The git ref (branch or tag) the release will be cut from.'),
|
|
879
|
+
}).optional().describe('Declare EXCLUSIVE intent to prepare a release of this project. The first declarer takes a ~2h lease (refreshed by checkpoints, freed by phase "complete", by expiry, or when the holder session ends); a second declarer gets a structured hard conflict naming the holder, version, and ref. While any lease is active every peer\'s sync gains a "release in preparation" footer line.'),
|
|
871
880
|
},
|
|
872
|
-
}, async ({ project, intent, files, phase, include_context, results }, extra) => {
|
|
881
|
+
}, async ({ project, intent, files, phase, include_context, results, releaseIntent }, extra) => {
|
|
873
882
|
const totalStartedAt = Date.now();
|
|
874
883
|
const report = mcpPresence.sync({
|
|
875
884
|
project,
|
|
@@ -877,6 +886,7 @@ server.registerTool('brain_sync', {
|
|
|
877
886
|
files,
|
|
878
887
|
phase,
|
|
879
888
|
results,
|
|
889
|
+
releaseIntent,
|
|
880
890
|
deliverMessages: include_context !== false,
|
|
881
891
|
actionId: extra?.klypixRequestIdentity?.actionId || '',
|
|
882
892
|
preflight: extra?.klypixBrainSyncPreflight,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.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",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"klypix-append": "bin/klypix-append.mjs",
|
|
45
45
|
"klypix-install": "bin/klypix-install.mjs",
|
|
46
46
|
"klypix-uninstall": "bin/klypix-uninstall.mjs",
|
|
47
|
-
"klypix-project-map": "bin/klypix-project-map.mjs"
|
|
47
|
+
"klypix-project-map": "bin/klypix-project-map.mjs",
|
|
48
|
+
"klypix-orphans": "bin/klypix-orphans.mjs"
|
|
48
49
|
},
|
|
49
50
|
"main": "index.mjs",
|
|
50
51
|
"exports": {
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
84
85
|
"test:bench": "node test/bench.mjs",
|
|
85
86
|
"pretest": "node test/publish-workflow.mjs",
|
|
86
|
-
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/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-security.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/capture-write-failure.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",
|
|
87
|
+
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/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/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/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.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-security.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/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
87
88
|
"test:memory": "node test/memory-runtime.mjs",
|
|
88
89
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
89
90
|
"runtime": "node bin/klypix-runtime.mjs"
|