klypix-mcp 1.103.0 → 1.104.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 CHANGED
@@ -515,6 +515,33 @@ so fresh relevant findings are not crowded out by older area vocabulary.
515
515
  Briefs are **not** injected automatically on Cursor, Cline, Copilot, Gemini CLI or Antigravity —
516
516
  there are no lifecycle hooks on those hosts.
517
517
 
518
+ ### The live projection — the file the agent obeys
519
+
520
+ The coordination bench showed that an agent trusts the instruction file it was launched with
521
+ over anything a tool or hook tells it, and that each host reads a different surface. So the
522
+ brain's state is projected into those files, in two halves:
523
+
524
+ - **Committed, from the brain:** the compact brief block in `AGENTS.md` (standing rules,
525
+ corrections, open handoff cards), refreshed by the Claude Code Stop hook only when its content
526
+ changed, so diffs stay quiet.
527
+ - **Machine-local, from the lane (klypix-mcp 1.104.0):** which sessions are live on this PC and
528
+ which files they hold, the work a stopped session left, and the evidence beside every
529
+ "keep off this file" note — written at session start and at every Stop by the Claude Code and
530
+ Codex hooks into `CLAUDE.local.md` at the project root (Claude Code loads it beside
531
+ `CLAUDE.md`; KLYPIX keeps it out of git through `.git/info/exclude`, never a committed file)
532
+ and into one section per project of the Codex global instructions file
533
+ (`~/.codex/AGENTS.override.md` when you have one, else `~/.codex/AGENTS.md`). A project-level
534
+ `AGENTS.override.md` is never written: Codex loads one file per directory, so it would hide
535
+ your `AGENTS.md`.
536
+
537
+ The block is a snapshot and says so: bounded to about 2.4KB, rewritten only when its content
538
+ changed (the time stamp alone never causes a write), with the keep-off evidence kept first when
539
+ the budget bites, and "nothing live" written in words so an older snapshot never stands. It
540
+ holds the same strings the hooks print, so the file and the session context cannot disagree.
541
+ Sections for projects not touched in a day are pruned from the Codex file, at most eight are
542
+ kept, and `uninstall unlink` strips the block (deleting `CLAUDE.local.md` only when nothing else
543
+ is in it). `brain_sync` remains the live source; the block tells the agent to call it.
544
+
518
545
  ## Capture and corrections
519
546
 
520
547
  `brain_note` accepts structured supporting references and inert verification text:
@@ -1350,7 +1377,10 @@ It strips only KLYPIX's own entries — every other hook and setting in
1350
1377
  `~/.claude/settings.json` stays — backs up each file it edits, and **never deletes a `.klypix`**.
1351
1378
  Removal requires `--yes`. Without it, the command prints the plan and changes nothing;
1352
1379
  there is no interactive confirmation prompt. Run the project-local form in each project you
1353
- want to unlink. A preview exits non-zero while managed files remain.
1380
+ want to unlink. A preview exits non-zero while managed files remain. The project-local form also
1381
+ strips the live-projection block from `CLAUDE.local.md` (the file goes only when nothing else is
1382
+ in it) and this project's section from the Codex global file; the machine-global form strips
1383
+ every live section there.
1354
1384
 
1355
1385
  Your `brain.klypix` is yours — it is a plain ZIP and stays readable with or without this package.
1356
1386
 
@@ -495,7 +495,7 @@ try {
495
495
  // a newer klypix-format cannot. merge-brains and the driver ship here so
496
496
  // brain-history's restore merge, the KLYPIX core and the git driver all
497
497
  // find one engine in this directory.
498
- for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'canvas-view-app.html']) {
498
+ for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'live-projection.mjs', 'canvas-view-app.html']) {
499
499
  const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
500
500
  }
501
501
  for (const [src, dst] of [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.103.0",
3
+ "version": "1.104.0",
4
4
  "mcpName": "io.github.dahshanlabs/klypix-mcp",
5
5
  "description": "Shared project memory for Claude Code, Codex and Cursor: decisions and corrections through MCP.",
6
6
  "type": "module",
@@ -91,7 +91,7 @@
91
91
  "bench": "node bin/klypix-mcp.mjs bench",
92
92
  "test:bench": "node test/bench.mjs",
93
93
  "pretest": "node test/publish-workflow.mjs",
94
- "test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/session-mailbox.mjs && node test/session-reopen.mjs && node test/session-handoff.mjs && node test/lock-notes.mjs && node test/lane-write-retry.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/brain-ask-words.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/arrange-receipts.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-preparation.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/marker-after-colon.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/overlap-tiers.mjs && node test/coordination-bench.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/results-not-manifests.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/current-first.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
94
+ "test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/session-mailbox.mjs && node test/session-reopen.mjs && node test/session-handoff.mjs && node test/lock-notes.mjs && node test/live-projection.mjs && node test/lane-write-retry.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/brain-ask-words.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/arrange-receipts.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-preparation.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/marker-after-colon.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/overlap-tiers.mjs && node test/coordination-bench.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/results-not-manifests.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/current-first.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
95
95
  "test:memory": "node test/memory-runtime.mjs",
96
96
  "test:memory:soak": "node --expose-gc test/memory-soak.mjs",
97
97
  "runtime": "node bin/klypix-runtime.mjs",
@@ -385,7 +385,7 @@ function unfinishedWorkBlock(brainPath, sessionId) {
385
385
  // a "do not touch" / hands-off / WIP line in AGENTS.md checked against live
386
386
  // sessions, local branches and uncommitted changes. Dynamic import so a stale
387
387
  // deployment without it no-ops; never throws.
388
- async function lockNotesBlock(projectDir, brainPath, sessionId, sessions) {
388
+ async function lockNotesBlock(projectDir, brainPath, sessionId, sessions, { budgetMs } = {}) {
389
389
  try {
390
390
  const lib = await import(new URL('./session-handoff.mjs', import.meta.url).href);
391
391
  if (typeof lib.readLockNotes !== 'function') return '';
@@ -393,7 +393,7 @@ async function lockNotesBlock(projectDir, brainPath, sessionId, sessions) {
393
393
  try { stopped = listUnfinishedWork({ brainPath, selfId: sessionId }); } catch { /* git still answers */ }
394
394
  // Lines naming a file a stopped session worked on ride too, whatever their wording.
395
395
  const mentions = [...new Set(stopped.flatMap((r) => (Array.isArray(r.files) ? r.files : [])))].slice(0, 20);
396
- const probe = lib.createLockNoteProbe(projectDir);
396
+ const probe = lib.createLockNoteProbe(projectDir, budgetMs ? { budgetMs } : {});
397
397
  const notes = lib.readLockNotes(projectDir, { mentions, probe });
398
398
  if (!notes.length) return '';
399
399
  const assessed = notes.slice(0, 6).map((n) => lib.assessLockNote(projectDir, n, { live: sessions || [], stopped, selfId: sessionId ? [sessionId] : [], probe }));
@@ -403,6 +403,22 @@ async function lockNotesBlock(projectDir, brainPath, sessionId, sessions) {
403
403
  }
404
404
  }
405
405
 
406
+ // Live projection (1.104.0): the same presence / unfinished / keep-off strings
407
+ // this hook emits, written into CLAUDE.local.md and the Codex global
408
+ // instructions file for the NEXT session of any host (session start and Stop).
409
+ // Dynamic import so a stale deployment no-ops; never throws.
410
+ async function projectLiveSafe({ projectDir, sessions, sessionId, lockNotes = '', unfinished = '' }) {
411
+ try {
412
+ const lib = await import(new URL('./live-projection.mjs', import.meta.url).href);
413
+ if (typeof lib.projectLive !== 'function') return null;
414
+ const peers = (Array.isArray(sessions) ? sessions : []).filter((s) => s && s.id && s.id !== sessionId)
415
+ .map((s) => ({ id: s.id, client: s.client, intent: s.intent, files: s.files, observedFiles: s.observedFiles }));
416
+ return lib.projectLive({ projectDir, peers, lockNotesText: String(lockNotes || '').trim(), unfinishedText: String(unfinished || '').trim() });
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+
406
422
  // Unfinished-work patch for one Codex lifecycle event (session handoff). A
407
423
  // human prompt records what was asked; a Stop records the reply tail only if
408
424
  // the host put it in the payload. Null when the event carries nothing.
@@ -587,6 +603,8 @@ async function main() {
587
603
  } catch { return ''; }
588
604
  })();
589
605
  const lockNotes = await lockNotesBlock(projectDir, brainPath, sessionId, sessions);
606
+ const unfinished = unfinishedWorkBlock(brainPath, sessionId);
607
+ await projectLiveSafe({ projectDir, sessions, sessionId, lockNotes, unfinished });
590
608
  emitSystemMessage([
591
609
  formatPresenceMessage(sessions, sessionId, { includeSolo: true }),
592
610
  lockNotes,
@@ -600,10 +618,16 @@ async function main() {
600
618
  // ⏸️ Unfinished work other sessions left (session handoff, 1.99.0): the
601
619
  // same engine block brain_sync prints, so a Codex session asked to
602
620
  // "continue" what a Claude session stopped in starts from its record.
603
- unfinishedWorkBlock(brainPath, sessionId),
621
+ unfinished,
604
622
  ], event);
605
623
  return;
606
624
  }
625
+ if (event === 'Stop') {
626
+ // The lane now carries this turn's files and reply tail: refresh the live
627
+ // projection for the next session, with a short git budget (a Stop must not wait).
628
+ const lockNotes = await lockNotesBlock(projectDir, brainPath, sessionId, sessions, { budgetMs: 1000 });
629
+ await projectLiveSafe({ projectDir, sessions, sessionId, lockNotes, unfinished: unfinishedWorkBlock(brainPath, sessionId) });
630
+ }
607
631
  if (event === 'UserPromptSubmit') {
608
632
  const me = sessions.find((session) => session.id === sessionId);
609
633
  // A STRONG status question is answered from computed state and REPLACES
@@ -1571,7 +1571,7 @@ function unfinishedFooter(sid, { max = 4 } = {}) {
1571
1571
  // live sessions, local branches and uncommitted changes — at session start,
1572
1572
  // before the agent decides to obey or ignore it. Same code as brain_sync
1573
1573
  // (session-handoff.mjs). Never edits the note; '' on any failure.
1574
- async function lockNotesFooter(sid) {
1574
+ async function lockNotesFooter(sid, { budgetMs } = {}) {
1575
1575
  try {
1576
1576
  const lib = await import(new URL('./session-handoff.mjs', import.meta.url).href);
1577
1577
  if (typeof lib.readLockNotes !== 'function') return '';
@@ -1586,7 +1586,7 @@ async function lockNotesFooter(sid) {
1586
1586
  // Lines naming a file a stopped session worked on ride too, whatever
1587
1587
  // their wording (bench S5d/S5f: the agent with hooks never asked).
1588
1588
  const mentions = [...new Set(stopped.flatMap(r => Array.isArray(r.files) ? r.files : []))].slice(0, 20);
1589
- const probe = lib.createLockNoteProbe(CWD);
1589
+ const probe = lib.createLockNoteProbe(CWD, budgetMs ? { budgetMs } : {});
1590
1590
  const notes = lib.readLockNotes(CWD, { mentions, probe });
1591
1591
  if (!notes.length) return '';
1592
1592
  const assessed = notes.slice(0, 6).map(n => lib.assessLockNote(CWD, n, { live, stopped, selfId: sid ? [sid] : [], now, probe }));
@@ -1595,6 +1595,28 @@ async function lockNotesFooter(sid) {
1595
1595
  } catch { return ''; }
1596
1596
  }
1597
1597
 
1598
+ // Live projection (1.104.0, bench rounds 2–3): write the machine-local half of
1599
+ // the brain's state — who is live on this PC and what they hold, the work a
1600
+ // stopped session left, the evidence beside every keep-off note — into the
1601
+ // files the hosts load by themselves and never commit (CLAUDE.local.md and the
1602
+ // Codex global instructions file), at session start and at every Stop. The
1603
+ // inputs are the SAME strings this hook prints, so the file and the context
1604
+ // cannot disagree. Dynamic import so a stale flat deployment no-ops; never throws.
1605
+ async function writeLiveProjection(sid, { lockText = null, unfinishedText = null, budgetMs } = {}) {
1606
+ try {
1607
+ const lib = await import(new URL('./live-projection.mjs', import.meta.url).href);
1608
+ if (typeof lib.projectLive !== 'function') return null;
1609
+ const lock = lockText === null ? await lockNotesFooter(sid, budgetMs ? { budgetMs } : {}) : lockText;
1610
+ const unfinished = unfinishedText === null ? unfinishedFooter(sid) : unfinishedText;
1611
+ return lib.projectLive({
1612
+ projectDir: CWD,
1613
+ peers: livePeers(sid).map(p => ({ id: p.id, client: p.client, intent: p.intent, files: p.files, observedFiles: p.observedFiles })),
1614
+ lockNotesText: String(lock || '').trim(),
1615
+ unfinishedText: String(unfinished || '').trim(),
1616
+ });
1617
+ } catch { return null; }
1618
+ }
1619
+
1598
1620
  // The nudge a session gets when the human asks for a handoff or says the
1599
1621
  // limit is near: the deliberate layer (a 🤝 card in the brain) on top of the
1600
1622
  // automatic state above. One line; the tool's own description carries the rest.
@@ -6185,12 +6207,16 @@ async function read(lib) {
6185
6207
  // The full brief keeps its order.
6186
6208
  const messages = messageFooter(input.session_id || '', input.transcript_path, lib);
6187
6209
  const lockLine = await lockNotesFooter(input.session_id);
6210
+ const unfinishedLine = unfinishedFooter(input.session_id);
6188
6211
  const out = ultra + messages + lockLine + versionLine + presenceLine + shipObsLine + gitHookNotice + laneWarning + healLine + draftLine
6189
6212
  + receiptLine
6190
6213
  + inflightFooter(input.session_id, struct)
6191
- + unfinishedFooter(input.session_id)
6214
+ + unfinishedLine
6192
6215
  + selfCheckFooter() + doctorFooter();
6193
6216
  process.stdout.write(out);
6217
+ // The same lock / unfinished strings go into CLAUDE.local.md and the Codex
6218
+ // global file for the NEXT session of any host (live projection).
6219
+ await writeLiveProjection(input.session_id, { lockText: lockLine, unfinishedText: unfinishedLine });
6194
6220
  // Heartbeat: prove the brief actually injected (and how big) so a dead or
6195
6221
  // stale live-copy of the hook stops being a silent no-op.
6196
6222
  appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'read', ok: true, briefBytes: Buffer.byteLength(out), fullBriefBytes: Buffer.byteLength(full), cards: struct?.counts?.cards ?? null }, 500);
@@ -6262,6 +6288,9 @@ async function main() {
6262
6288
  // without paying the rebuild itself. Best-effort; guardCheck's currency
6263
6289
  // stat is the backstop.
6264
6290
  try { if (typeof lib.ensureGuardSidecar === 'function') await lib.ensureGuardSidecar(BRAIN); } catch { /* backstopped */ }
6291
+ // Refresh the live projection after the turn: the lane now carries this
6292
+ // session's files and reply tail. A short git budget — a Stop must not wait.
6293
+ await writeLiveProjection(readHookInput().session_id, { budgetMs: 1000 });
6265
6294
  // Ambient version-currency: piggyback the post-session Stop hook to refresh the
6266
6295
  // npm-latest cache (≤ once/day, best-effort, failure-silent) so the next
6267
6296
  // SessionStart footer can surface a stale install with ZERO network. Awaited so
@@ -0,0 +1,286 @@
1
+ // Live projection (1.104.0) — the machine-local half of "project the brain
2
+ // into the file the agent obeys".
3
+ //
4
+ // WHY. The coordination bench (rounds 2 and 3, 2026-10-12/13) showed that an
5
+ // agent trusts the instruction file it was launched with more than anything a
6
+ // tool or hook tells it: a two-day-old "keep off this file" line in AGENTS.md
7
+ // beat KLYPIX's lane in 14 of 15 runs, and the hosts read different surfaces
8
+ // (Claude the JSON, Codex the text, both the files). The brain-derived half —
9
+ // standing rules, corrections, handoff cards — already lives in the committed
10
+ // `<!-- klypix-brain-brief -->` block of AGENTS.md (refreshAgentsBrief). The
11
+ // LIVE half — which sessions are on this PC right now and what files they
12
+ // hold, what work a stopped session left, and the evidence beside every
13
+ // keep-off note — reached the agent only through hook output and brain_sync,
14
+ // never through a file. This module writes that half into the two files the
15
+ // hosts load by themselves and never commit:
16
+ //
17
+ // • `CLAUDE.local.md` at the project root — Claude Code loads it beside
18
+ // CLAUDE.md ("personal project-specific preferences; add to .gitignore");
19
+ // its HTML comments are stripped before injection, so the markers cost
20
+ // nothing. KLYPIX keeps it out of git through `.git/info/exclude` (local
21
+ // to this clone, never a committed file).
22
+ // • the Codex global instructions file — `~/.codex/AGENTS.override.md` when
23
+ // the person has one, else `~/.codex/AGENTS.md`, the file klypix-mcp
24
+ // already manages. Codex loads the global file first, then one file per
25
+ // directory from the project root down, so a project-level override would
26
+ // HIDE the project's AGENTS.md: never written. One section per project,
27
+ // keyed by the project's path, pruned when stale.
28
+ //
29
+ // CONTRACT. Snapshot semantics, said in the text: written at session start and
30
+ // stop, not live, not shared, brain_sync is the live source. Bounded (budget
31
+ // below), deterministic for one input, rewritten only when the non-volatile
32
+ // content changed (the time stamp alone never causes a write). Never throws:
33
+ // every writer returns an action word and swallows its own errors, because it
34
+ // runs inside hooks that must exit 0.
35
+ import crypto from 'node:crypto';
36
+ import { execFileSync } from 'node:child_process';
37
+ import fs from 'node:fs';
38
+ import os from 'node:os';
39
+ import path from 'node:path';
40
+
41
+ export const LIVE_PROJECTION_VERSION = 1;
42
+ export const LIVE_BUDGET_CHARS = 2400;
43
+ export const LIVE_STALE_MS = 24 * 60 * 60 * 1000;
44
+ export const LIVE_PROJECT_CAP = 8;
45
+ export const CLAUDE_LOCAL_FILE = 'CLAUDE.local.md';
46
+ export const LIVE_END = '<!-- klypix-live:end -->';
47
+
48
+ const flat = (v) => String(v || '').replace(/\s+/g, ' ').trim();
49
+ const minuteIso = (ms) => new Date(ms).toISOString().slice(0, 16) + 'Z';
50
+ const CLIENT_WORDS = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor', cline: 'Cline', windsurf: 'Windsurf', copilot: 'Copilot', gemini: 'Gemini CLI', aider: 'Aider', antigravity: 'Antigravity' };
51
+ const clientWord = (c) => CLIENT_WORDS[String(c || '').toLowerCase()] || (c ? String(c) : 'agent');
52
+
53
+ /** Stable key for a project path: same spelling rules as the presence lane. */
54
+ export function liveKey(projectDir) {
55
+ const norm = path.resolve(String(projectDir || '.')).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
56
+ return crypto.createHash('sha1').update(norm).digest('hex').slice(0, 16);
57
+ }
58
+
59
+ export function liveStart(key, { at = Date.now(), projectDir = '' } = {}) {
60
+ const where = projectDir ? ` project=${JSON.stringify(path.resolve(projectDir).replace(/\\/g, '/'))}` : '';
61
+ return `<!-- klypix-live:start key=${key} at=${new Date(at).toISOString()}${where} (machine-local; written by klypix-mcp at session start and stop; do not edit; never commit) -->`;
62
+ }
63
+
64
+ // One regex per key; the marker's attributes are free text up to `-->`.
65
+ const blockRe = (key) => new RegExp(`<!-- klypix-live:start key=${key}[^\\n]*-->[\\s\\S]*?<!-- klypix-live:end -->\\r?\\n?`);
66
+ const ANY_BLOCK_RE = /<!-- klypix-live:start key=([0-9a-f]{16}) at=([^ \n]+)[^\n]*-->[\s\S]*?<!-- klypix-live:end -->\r?\n?/g;
67
+
68
+ /** Everything in a block that may change without the content changing. */
69
+ function stable(text) {
70
+ return String(text || '')
71
+ .replace(/<!-- klypix-live:start key=[0-9a-f]{16} at=[^ \n]+/g, '<!-- klypix-live:start')
72
+ .replace(/· as of [0-9T:-]+Z/g, '· as of')
73
+ .replace(/\r\n/g, '\n').trim();
74
+ }
75
+
76
+ function peerLine(p) {
77
+ const files = [...new Set([...(Array.isArray(p.files) ? p.files : []), ...(Array.isArray(p.observedFiles) ? p.observedFiles : [])])].filter(Boolean);
78
+ const bits = [`${clientWord(p.client)} ${String(p.id || '').slice(0, 8)}`];
79
+ if (p.intent) bits.push(`“${flat(p.intent).slice(0, 70)}”`);
80
+ if (files.length) bits.push(`files: ${files.slice(0, 4).join(', ')}${files.length > 4 ? ` +${files.length - 4}` : ''}`);
81
+ return bits.join(' · ');
82
+ }
83
+
84
+ function clip(text, max) {
85
+ const t = String(text || '').trim();
86
+ if (t.length <= max) return t;
87
+ return t.slice(0, Math.max(0, max - 60)).replace(/\s+\S*$/, '') + '\n … (cut for size — brain_sync has the rest)';
88
+ }
89
+
90
+ /**
91
+ * The block body. Pure: same inputs, same text (the "as of" minute aside).
92
+ * `peers` are presence-lane rows OTHER than the writing session; `unfinishedText`
93
+ * and `lockNotesText` are the engine's own blocks (formatUnfinishedWork,
94
+ * formatLockNotes), passed through so this file never re-derives a verdict.
95
+ */
96
+ export function renderLiveProjection({ projectDir, now = Date.now(), version = '', peers = [], unfinishedText = '', lockNotesText = '', budgetChars = LIVE_BUDGET_CHARS } = {}) {
97
+ const name = path.basename(path.resolve(projectDir || '.')) || 'project';
98
+ const budget = Math.max(800, Math.min(8000, Number(budgetChars) || LIVE_BUDGET_CHARS));
99
+ const head = [
100
+ `## KLYPIX live — ${name} on this PC · as of ${minuteIso(now)}`,
101
+ `Snapshot written by klypix-mcp${version ? ` ${version}` : ''} at session start and stop: this PC only, not committed, not shared. The decisions and corrections are in the AGENTS.md brain block; \`brain_sync\` for this project is the live source and must be called before acting on anything below.`,
102
+ ];
103
+ const rows = (Array.isArray(peers) ? peers : []).filter((p) => p && p.id);
104
+ const peersText = rows.length
105
+ ? `👥 ${rows.length} other live session(s) on this PC right now (a snapshot — re-query brain_sync before reporting them): ${rows.slice(0, 6).map(peerLine).join(' | ')}${rows.length > 6 ? ` | …and ${rows.length - 6} more` : ''}. A file two sessions both hold is a warning, not a lock: coordinate with brain_message.`
106
+ : '👥 No other live session on this PC at the time of writing.';
107
+ const lock = String(lockNotesText || '').trim();
108
+ const unfinished = String(unfinishedText || '').trim();
109
+ // Priority when the budget bites: the keep-off evidence first (it is what the
110
+ // bench lost on), then who is live, then the unfinished work.
111
+ const fixed = head.join('\n').length + 2;
112
+ let left = budget - fixed;
113
+ const lockPart = lock ? clip(lock, Math.max(600, Math.floor(left * 0.6))) : '';
114
+ left -= lockPart.length + 2;
115
+ const peersPart = clip(peersText, Math.max(200, Math.min(700, left)));
116
+ left -= peersPart.length + 2;
117
+ const unfinishedPart = unfinished ? clip(unfinished, Math.max(240, left)) : '⏸️ No unfinished work recorded from a stopped session.';
118
+ return [...head, '', peersPart, '', lockPart || '📌 No keep-off note in this project\'s instruction files named a file with a verdict to report.', '', unfinishedPart].join('\n').replace(/\n{3,}/g, '\n\n').trim();
119
+ }
120
+
121
+ export function wrapLiveBlock(key, body, opts = {}) {
122
+ return `${liveStart(key, opts)}\n${body}\n${LIVE_END}`;
123
+ }
124
+
125
+ /** Replace the block with this key in `raw`, or append it. */
126
+ export function upsertLiveBlock(raw, key, block) {
127
+ const text = String(raw || '');
128
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
129
+ const b = block.replace(/\r?\n/g, eol);
130
+ const re = blockRe(key);
131
+ if (re.test(text)) return text.replace(re, b + eol);
132
+ const base = text.trim() ? text.replace(/\s*$/, '') + eol + eol : '';
133
+ return base + b + eol;
134
+ }
135
+
136
+ export function stripLiveBlocks(raw, key = null) {
137
+ const text = String(raw || '');
138
+ const next = key ? text.replace(blockRe(key), '') : text.replace(ANY_BLOCK_RE, '');
139
+ return next.replace(/^\s+/, '').replace(/\s+$/, (m) => (m.includes('\n') ? '\n' : ''));
140
+ }
141
+
142
+ function atomicWrite(file, content) {
143
+ fs.mkdirSync(path.dirname(file), { recursive: true });
144
+ const tmp = `${file}.klypix-tmp-${process.pid}`;
145
+ fs.writeFileSync(tmp, content, 'utf8');
146
+ fs.renameSync(tmp, file);
147
+ }
148
+
149
+ function git(cwd, args, timeout = 1500) {
150
+ return execFileSync('git', args, { cwd, encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
151
+ }
152
+
153
+ /**
154
+ * Keep CLAUDE.local.md out of this clone's git status without touching a
155
+ * committed file: `.git/info/exclude` (the worktree-aware git-path form). A
156
+ * project that already ignores it, or is not a repository, is left alone.
157
+ */
158
+ export function ensureLocalExcluded(projectDir, { rel = CLAUDE_LOCAL_FILE } = {}) {
159
+ const root = path.resolve(projectDir);
160
+ try {
161
+ execFileSync('git', ['check-ignore', '-q', rel], { cwd: root, timeout: 1500, stdio: 'ignore', windowsHide: true });
162
+ return 'already-ignored';
163
+ } catch (e) {
164
+ if (e && e.status !== 1) return 'not-a-repo';
165
+ }
166
+ try {
167
+ const excludeRel = git(root, ['rev-parse', '--git-path', 'info/exclude']);
168
+ const exclude = path.isAbsolute(excludeRel) ? excludeRel : path.join(root, excludeRel);
169
+ let raw = '';
170
+ try { raw = fs.readFileSync(exclude, 'utf8'); } catch { /* fresh */ }
171
+ if (raw.split(/\r?\n/).some((l) => l.trim() === rel || l.trim() === `/${rel}`)) return 'already-excluded';
172
+ fs.mkdirSync(path.dirname(exclude), { recursive: true });
173
+ fs.appendFileSync(exclude, `${raw && !raw.endsWith('\n') ? '\n' : ''}# KLYPIX live projection (machine-local; written by klypix-mcp)\n${rel}\n`, 'utf8');
174
+ return 'excluded';
175
+ } catch {
176
+ return 'exclude-failed';
177
+ }
178
+ }
179
+
180
+ export function writeClaudeLocal(projectDir, body, { now = Date.now(), file = null } = {}) {
181
+ const root = path.resolve(projectDir);
182
+ const target = file || path.join(root, CLAUDE_LOCAL_FILE);
183
+ try {
184
+ const key = liveKey(root);
185
+ let raw = '';
186
+ try { raw = fs.readFileSync(target, 'utf8'); } catch { /* fresh */ }
187
+ const block = wrapLiveBlock(key, body, { at: now, projectDir: root });
188
+ const next = upsertLiveBlock(raw, key, block);
189
+ if (stable(next) === stable(raw)) return { path: target, action: 'unchanged', excluded: ensureLocalExcluded(root) };
190
+ atomicWrite(target, next);
191
+ return { path: target, action: raw ? 'updated' : 'created', excluded: ensureLocalExcluded(root) };
192
+ } catch (e) {
193
+ return { path: target, action: 'failed', error: e?.message || String(e) };
194
+ }
195
+ }
196
+
197
+ /** The one global instruction file Codex actually loads. */
198
+ export function codexGlobalInstructionFile(home = os.homedir()) {
199
+ const dir = path.join(process.env.CODEX_HOME || path.join(home, '.codex'));
200
+ const override = path.join(dir, 'AGENTS.override.md');
201
+ try { if (fs.statSync(override).isFile()) return override; } catch { /* no override */ }
202
+ return path.join(dir, 'AGENTS.md');
203
+ }
204
+
205
+ /** Drop other projects' sections that are stale or beyond the cap (oldest first). */
206
+ export function pruneLiveSections(raw, { keep = null, now = Date.now(), staleMs = LIVE_STALE_MS, cap = LIVE_PROJECT_CAP } = {}) {
207
+ const text = String(raw || '');
208
+ const found = [];
209
+ for (const m of text.matchAll(ANY_BLOCK_RE)) found.push({ key: m[1], at: Date.parse(m[2]) || 0, text: m[0] });
210
+ const others = found.filter((f) => f.key !== keep).sort((a, b) => a.at - b.at);
211
+ const drop = new Set();
212
+ for (const f of others) if (now - f.at > staleMs) drop.add(f.text);
213
+ const fresh = others.filter((f) => !drop.has(f.text));
214
+ const room = Math.max(0, cap - (keep ? 1 : 0));
215
+ for (const f of fresh.slice(0, Math.max(0, fresh.length - room))) drop.add(f.text);
216
+ let next = text;
217
+ for (const t of drop) next = next.replace(t, '');
218
+ return { text: next, dropped: drop.size };
219
+ }
220
+
221
+ export function writeCodexGlobal(projectDir, body, { now = Date.now(), home = os.homedir(), file = null } = {}) {
222
+ const root = path.resolve(projectDir);
223
+ const target = file || codexGlobalInstructionFile(home);
224
+ try {
225
+ const key = liveKey(root);
226
+ let raw = '';
227
+ try { raw = fs.readFileSync(target, 'utf8'); } catch { /* fresh */ }
228
+ const pruned = pruneLiveSections(raw, { keep: key, now });
229
+ const block = wrapLiveBlock(key, body, { at: now, projectDir: root });
230
+ const next = upsertLiveBlock(pruned.text, key, block);
231
+ if (stable(next) === stable(raw)) return { path: target, action: 'unchanged', dropped: pruned.dropped };
232
+ atomicWrite(target, next);
233
+ return { path: target, action: raw ? 'updated' : 'created', dropped: pruned.dropped };
234
+ } catch (e) {
235
+ return { path: target, action: 'failed', error: e?.message || String(e) };
236
+ }
237
+ }
238
+
239
+ /** Both files, never throws. The caller supplies what the hook already computed. */
240
+ export function projectLive({ projectDir, now = Date.now(), home = os.homedir(), version = '', peers = [], unfinishedText = '', lockNotesText = '', claudeFile = null, codexFile = null } = {}) {
241
+ const body = renderLiveProjection({ projectDir, now, version, peers, unfinishedText, lockNotesText });
242
+ return {
243
+ claude: writeClaudeLocal(projectDir, body, { now, file: claudeFile }),
244
+ codex: writeCodexGlobal(projectDir, body, { now, home, file: codexFile }),
245
+ bytes: Buffer.byteLength(body),
246
+ };
247
+ }
248
+
249
+ /** Uninstall / unlink: remove this project's block from both files; delete CLAUDE.local.md only if nothing else is in it. */
250
+ export function removeLiveProjection(projectDir, { home = os.homedir(), claudeFile = null } = {}) {
251
+ const root = path.resolve(projectDir);
252
+ const key = liveKey(root);
253
+ const out = { claude: 'unchanged', codex: 'unchanged' };
254
+ const local = claudeFile || path.join(root, CLAUDE_LOCAL_FILE);
255
+ try {
256
+ const raw = fs.readFileSync(local, 'utf8');
257
+ if (blockRe(key).test(raw)) {
258
+ const rest = stripLiveBlocks(raw, key);
259
+ if (rest.trim()) { atomicWrite(local, rest); out.claude = 'block-removed'; } else { fs.unlinkSync(local); out.claude = 'file-removed'; }
260
+ }
261
+ } catch { /* absent */ }
262
+ const dir = path.join(process.env.CODEX_HOME || path.join(home, '.codex'));
263
+ for (const f of [path.join(dir, 'AGENTS.override.md'), path.join(dir, 'AGENTS.md')]) {
264
+ try {
265
+ const raw = fs.readFileSync(f, 'utf8');
266
+ if (!blockRe(key).test(raw)) continue;
267
+ atomicWrite(f, stripLiveBlocks(raw, key));
268
+ out.codex = 'section-removed';
269
+ } catch { /* absent */ }
270
+ }
271
+ return out;
272
+ }
273
+
274
+ /** Does `raw` carry this project's live block? (uninstall planning, read-only) */
275
+ export function hasLiveBlock(raw, projectDir) {
276
+ return blockRe(liveKey(projectDir)).test(String(raw || ''));
277
+ }
278
+
279
+ /** Every live section in the Codex global file (for doctor and uninstall). */
280
+ export function listLiveSections(raw) {
281
+ const text = String(raw || '');
282
+ return [...text.matchAll(ANY_BLOCK_RE)].map((m) => {
283
+ const project = /project="([^"]*)"/.exec(m[0]);
284
+ return { key: m[1], at: Date.parse(m[2]) || 0, project: project ? project[1] : '' };
285
+ });
286
+ }
package/src/uninstall.mjs CHANGED
@@ -43,6 +43,13 @@ import {
43
43
  removeCodexPresenceHooks,
44
44
  resolveCodexHooksPath,
45
45
  } from './codex-hooks.mjs';
46
+ import {
47
+ CLAUDE_LOCAL_FILE,
48
+ hasLiveBlock,
49
+ removeLiveProjection,
50
+ listLiveSections,
51
+ stripLiveBlocks,
52
+ } from './live-projection.mjs';
46
53
 
47
54
  // ── the one absolute rule ────────────────────────────────────────────────────
48
55
  // A canvas is USER DATA. install/link never created one and uninstall must never
@@ -290,6 +297,20 @@ export function planUnlink(projectDir, { sidecars = false } = {}) {
290
297
  const target = { file, kind: meta.kind, wrapKey: meta.wrapKey, frontmatter: meta.frontmatter };
291
298
  items.push({ label: r.tool, rel, file, ...target, ...planOne(target) });
292
299
  }
300
+ // Live projection block (1.104.0). CLAUDE.local.md is not a managed install
301
+ // target — the person may own the rest of the file — so only KLYPIX's own
302
+ // marked block goes, and the file only when nothing else is in it. The
303
+ // project's section in the Codex global file goes with it.
304
+ {
305
+ const file = path.join(root, CLAUDE_LOCAL_FILE);
306
+ const raw = readText(file);
307
+ items.push({
308
+ label: 'Live projection (machine-local snapshot)', rel: CLAUDE_LOCAL_FILE, file, kind: 'live-block',
309
+ ...(raw !== null && hasLiveBlock(raw, root)
310
+ ? { action: 'strip-block', why: 'remove the KLYPIX live block (and this project\'s section in the Codex global file); your own lines stay' }
311
+ : { action: 'none', why: 'not present' }),
312
+ });
313
+ }
293
314
  if (sidecars) {
294
315
  for (const rel of PROJECT_SIDECARS) {
295
316
  const file = path.join(root, rel);
@@ -332,6 +353,18 @@ export function planUninstall({ home = os.homedir(), installDir, keepRegistry =
332
353
  ? { action: 'strip-block', why: 'remove the fenced KLYPIX block; your own guidance stays' }
333
354
  : { action: 'none', why: 'no managed block' }),
334
355
  });
356
+ // 2b) Live projection sections (1.104.0): one per project, in whichever
357
+ // global file Codex loads. Stripped whole; the person's own text stays.
358
+ for (const name of ['AGENTS.override.md', 'AGENTS.md']) {
359
+ const file = path.join(HOME, '.codex', name);
360
+ const raw = readText(file);
361
+ const count = raw === null ? 0 : listLiveSections(raw).length;
362
+ if (!count && name === 'AGENTS.override.md') continue;
363
+ items.push({
364
+ label: 'Live projection sections (Codex global)', rel: `.codex/${name}`, file, kind: 'live-sections',
365
+ ...(count ? { action: 'strip-block', why: `remove ${count} machine-local KLYPIX live section(s); your own guidance stays` } : { action: 'none', why: 'no live section' }),
366
+ });
367
+ }
335
368
  if (codexGlobalInstructionsInstalled(HOME)) {
336
369
  notes.push('~/.codex/AGENTS.md carries no version/hash stamp, so an edit you made INSIDE the KLYPIX block cannot be detected — the .klypix-bak backup is your only copy of it.');
337
370
  }
@@ -432,8 +465,28 @@ function applyOne(item, { root } = {}) {
432
465
  : item.kind === 'codex-hooks' ? null
433
466
  : item.kind === 'codex-agents' ? null
434
467
  : item.kind === 'bundle' ? null
435
- : planOne(item);
468
+ : item.kind === 'live-block' || item.kind === 'live-sections' ? null
469
+ : planOne(item);
436
470
  try {
471
+ if (item.kind === 'live-block') {
472
+ // Both files at once: the project's CLAUDE.local.md block and its section
473
+ // in the Codex global file (the module owns the markers; never a blind delete).
474
+ const projectDir = root || path.dirname(item.file);
475
+ const raw = readText(item.file);
476
+ if (raw === null || !hasLiveBlock(raw, projectDir)) return { ok: true, action: 'none' };
477
+ const backup = backupFile(item.file);
478
+ const res = removeLiveProjection(projectDir, { claudeFile: item.file });
479
+ return { ok: true, action: res.claude === 'file-removed' ? 'delete-file' : res.claude === 'block-removed' ? 'strip-block' : 'none', backup };
480
+ }
481
+ if (item.kind === 'live-sections') {
482
+ const raw = readText(item.file);
483
+ if (raw === null || !listLiveSections(raw).length) return { ok: true, action: 'none' };
484
+ const backup = backupFile(item.file);
485
+ const next = stripLiveBlocks(raw);
486
+ if (next.trim() === '') { deleteFile(item.file); return { ok: true, action: 'delete-file', backup }; }
487
+ writeAtomic(item.file, next);
488
+ return { ok: true, action: 'strip-block', backup };
489
+ }
437
490
  if (item.kind === 'codex-agents') {
438
491
  if (!codexGlobalInstructionsInstalled(path.resolve(item.file, '..', '..'))) return { ok: true, action: 'none' };
439
492
  const res = removeCodexGlobalInstructions(path.resolve(item.file, '..', '..'));