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.
@@ -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({ brainVersion: VERSION, via: 'npm', dirty: false, installedAt }, null, 2), 'utf8');
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
@@ -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
+ }
@@ -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.68.0",
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"