klypix-mcp 1.102.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 +44 -2
- package/bin/klypix-install.mjs +1 -1
- package/bin/klypix-worker.mjs +13 -2
- package/package.json +2 -2
- package/src/agent-presence.mjs +87 -2
- package/src/agent-rules.mjs +10 -8
- package/src/codex-brain-hook.mjs +63 -4
- package/src/global-brain-hook.mjs +104 -7
- package/src/klypix-core.mjs +89 -10
- package/src/klypix-format.mjs +10 -3
- package/src/live-projection.mjs +286 -0
- package/src/mcp-presence.mjs +1 -1
- package/src/session-handoff.mjs +276 -17
- package/src/uninstall.mjs +54 -1
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:
|
|
@@ -702,6 +729,18 @@ until a session takes it (`{action:"take", id}`) and finishes it (`{action:"done
|
|
|
702
729
|
archives it). A later handoff can name the earlier one it `continues`. The per-prompt hook nudges
|
|
703
730
|
the agent to write one when the person says *handoff* or names the limit.
|
|
704
731
|
|
|
732
|
+
**Drafted by KLYPIX — when the session never got to write one.** A usage limit, a crash or a
|
|
733
|
+
closed window leaves no time for `brain_handoff`. Since 1.103.0 the brain writes the card itself:
|
|
734
|
+
at the next `brain_sync` start or `brain_handoff list` of *any* session, a record that qualifies —
|
|
735
|
+
the session is closed or quiet for 30 minutes, left real work (its own commits, or touched files
|
|
736
|
+
and a reply tail), has no card and no taker, and its task was a human's, not an app's — becomes a
|
|
737
|
+
🤝 HANDOFF card on that session's behalf, reason *interrupted*. The card says `Drafted: by KLYPIX`
|
|
738
|
+
on its own line, lists only what git proved under *Done*, tells the reader to confirm the state
|
|
739
|
+
with `git status` before touching anything, and carries the session's last words verbatim. The
|
|
740
|
+
record and every list say the session wrote none. If the session resumes and completes its task,
|
|
741
|
+
the drafted card is archived at the next pass. Three drafts per pass at most; a draft can never
|
|
742
|
+
fail the sync that triggered it.
|
|
743
|
+
|
|
705
744
|
Durable decisions still go in the brain as cards — a handoff is the one card about *work in
|
|
706
745
|
flight*, and it closes when the work lands.
|
|
707
746
|
|
|
@@ -1017,7 +1056,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
1017
1056
|
| `brain_doctor` | Self-diagnosis: version, core/enhanced host adapters, active sessions, tool count, projection drift |
|
|
1018
1057
|
| `brain_message` | Session-to-session coordination notes — to a live session, or queued for one that is not running until it next starts — with a fixed send-time audience and per-recipient pending / offer / acknowledgement / consumption / failure receipts (a directed note is kept 7 days, a broadcast 24h; never written into the brain) |
|
|
1019
1058
|
| `brain_message_receipt` | Explicitly record model-side consumption using the exact message id and per-recipient offer token; acknowledgement alone never consumes a note |
|
|
1020
|
-
| `brain_handoff` | The session handoff. `write` puts a 🤝 card in the brain — task, done, next in order, files, verify — when a session stops before the work is done; the next session of any agent sees it at its first `brain_sync` and `take`s it, then marks it `done`. `list` also shows the **unfinished work** KLYPIX recorded automatically for every session (its last reply, files and
|
|
1059
|
+
| `brain_handoff` | The session handoff. `write` puts a 🤝 card in the brain — task, done, next in order, files, verify — when a session stops before the work is done; the next session of any agent sees it at its first `brain_sync` and `take`s it, then marks it `done`. `list` also shows the **unfinished work** KLYPIX recorded automatically for every session (its last reply, files, own commits, and since 1.103.0 the directories it worked in with the uncommitted state of each checkout, so a fix left in a side checkout is named, not searched for), which can be taken over by session id. Since 1.103.0 a session that stopped without writing a card (a usage limit, a crash) gets one **drafted by KLYPIX** from that record at the next `brain_sync` start or `list` — marked `Drafted:`, reason *interrupted*, archived if the session later completes its task |
|
|
1021
1060
|
| `brain_reopen` | Reopen a closed Claude Code or Codex session so a note waiting for it gets there now — only after the person answers *Reopen* in an in-chat prompt or KLYPIX's own dialog; opens a new, visible terminal in that session's folder with the host's resume command |
|
|
1022
1061
|
| `brain_sync` | Context Gateway: task capsule, active-task peers, exact-file overlap, one-time alerts, timing, and optional result-manifest reconciliation |
|
|
1023
1062
|
| `brain_connect` | Find and draw related-but-unlinked cards |
|
|
@@ -1338,7 +1377,10 @@ It strips only KLYPIX's own entries — every other hook and setting in
|
|
|
1338
1377
|
`~/.claude/settings.json` stays — backs up each file it edits, and **never deletes a `.klypix`**.
|
|
1339
1378
|
Removal requires `--yes`. Without it, the command prints the plan and changes nothing;
|
|
1340
1379
|
there is no interactive confirmation prompt. Run the project-local form in each project you
|
|
1341
|
-
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.
|
|
1342
1384
|
|
|
1343
1385
|
Your `brain.klypix` is yours — it is a plain ZIP and stays readable with or without this package.
|
|
1344
1386
|
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -495,7 +495,7 @@ try {
|
|
|
495
495
|
// a newer klypix-format cannot. merge-brains and the driver ship here so
|
|
496
496
|
// brain-history's restore merge, the KLYPIX core and the git driver all
|
|
497
497
|
// find one engine in this directory.
|
|
498
|
-
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', '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/bin/klypix-worker.mjs
CHANGED
|
@@ -1409,7 +1409,13 @@ server.registerTool('brain_sync', {
|
|
|
1409
1409
|
let handoffData = null;
|
|
1410
1410
|
if (phase === 'start' && include_context !== false && report.structured?.brain) {
|
|
1411
1411
|
try {
|
|
1412
|
-
const h = await opBrainHandoff({ vault: mcpPresence.vault, canvas: report.structured.brain, action: 'list', from: { sessionId: extra?.klypixRequestIdentity?.sessionId || mcpPresence.id } });
|
|
1412
|
+
const h = await opBrainHandoff({ vault: mcpPresence.vault, canvas: report.structured.brain, action: 'list', from: { sessionId: extra?.klypixRequestIdentity?.sessionId || mcpPresence.id, client: extra?.klypixRequestIdentity?.clientInfo?.client || extra?.klypixClientName || 'unknown' } });
|
|
1413
|
+
// 1.103.0: the list may have WRITTEN the brain (a card drafted for a dead
|
|
1414
|
+
// session, or a drafted card archived) — an atomic replace that changes
|
|
1415
|
+
// the brain inode captured at sync. Refresh the binding from the
|
|
1416
|
+
// unchanged canonical root, as the tool wrapper does after any managed
|
|
1417
|
+
// write, or the session's very next tool is rejected as project-changed.
|
|
1418
|
+
if ((h?.structured?.autoDrafted || []).length || (h?.structured?.autoClosed || []).length) mcpPresence.refreshCurrentProjectBinding();
|
|
1413
1419
|
const cards = h?.structured?.handoffs || [];
|
|
1414
1420
|
// 1.99.1: the AUTOMATIC lane records are history, not a lock (bench S5:
|
|
1415
1421
|
// an ended session's intent on a file made Claude and Codex defer, 0/5).
|
|
@@ -1438,14 +1444,19 @@ server.registerTool('brain_sync', {
|
|
|
1438
1444
|
.map(({ r }) => r);
|
|
1439
1445
|
if (!h?.isError && (cards.length || lane.length)) {
|
|
1440
1446
|
const now = Date.now();
|
|
1447
|
+
const drafted = Array.isArray(h?.structured?.autoDrafted) ? h.structured.autoDrafted : [];
|
|
1441
1448
|
handoffText = [
|
|
1449
|
+
// 1.103.0: a card KLYPIX drafted in THIS pass from a dead session's
|
|
1450
|
+
// record is announced as such — a lead to verify, not the session's words.
|
|
1451
|
+
drafted.length ? `🤝 KLYPIX drafted ${drafted.length} handoff card${drafted.length > 1 ? 's' : ''} just now from the record of a session that stopped without writing one: ${drafted.map((d) => `[${d.id}] “${d.title}”`).join('; ')}. A drafted card is a lead, not a verified state — confirm it with git before relying on it.` : '',
|
|
1442
1452
|
formatHandoffCards(cards, { now, max: 5 }),
|
|
1443
1453
|
formatUnfinishedWork(lane, { now, max: 4, ranked: wantsContinue && words.size > 0 }),
|
|
1444
1454
|
].filter(Boolean).join('\n\n');
|
|
1445
1455
|
handoffData = {
|
|
1446
1456
|
cards: cards.length,
|
|
1447
1457
|
unfinished: lane.length,
|
|
1448
|
-
handoffs: cards.map((c) => ({ id: c.id, title: c.title, from: c.from, at: c.at, reason: c.reason, state: c.state, next: c.next, files: c.files })),
|
|
1458
|
+
handoffs: cards.map((c) => ({ id: c.id, title: c.title, from: c.from, at: c.at, reason: c.reason, state: c.state, next: c.next, files: c.files, auto: c.auto === true })),
|
|
1459
|
+
...(Array.isArray(h?.structured?.autoDrafted) && h.structured.autoDrafted.length ? { autoDrafted: h.structured.autoDrafted } : {}),
|
|
1449
1460
|
sessions: lane.slice(0, 4).map((r) => ({ id: r.id, client: r.client, intent: r.intent, state: r.state, working: false, holdsFiles: false, note: r.note, status: r.status, lastSeen: r.lastSeen, lastPrompt: r.lastPrompt || null, lastWords: r.lastWords || null, files: r.files, commits: r.commits, handoffCardId: r.handoffCardId, takenBy: r.takenBy })),
|
|
1450
1461
|
};
|
|
1451
1462
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/src/agent-presence.mjs
CHANGED
|
@@ -512,6 +512,37 @@ const endedMatches = (row, id) => {
|
|
|
512
512
|
// without `work` is byte-stable through a touch that changes nothing.
|
|
513
513
|
export const WORK_LAST_WORDS_MAX = 700;
|
|
514
514
|
export const WORK_COMMITS_MAX = 6;
|
|
515
|
+
// Where the session actually worked (1.103.0, field case 2026-10-15): the
|
|
516
|
+
// directories its shell commands moved into (`workdirs`) and a bounded `git
|
|
517
|
+
// status` of each checkout at Stop (`checkouts`). Codex had left a fix
|
|
518
|
+
// UNCOMMITTED on a branch in a side checkout (C:\w\facts) that `git worktree
|
|
519
|
+
// list` in the project does not show; its last words, files and commits were
|
|
520
|
+
// all recorded and still nobody could find the work.
|
|
521
|
+
export const WORK_WORKDIRS_MAX = 4;
|
|
522
|
+
export const WORK_CHECKOUTS_MAX = 4;
|
|
523
|
+
export const WORK_CHECKOUT_PATHS_MAX = 12;
|
|
524
|
+
// Newest last; a directory used again moves to the tail (LRU, like observedFiles).
|
|
525
|
+
const normalizeDirList = (raw, max) => {
|
|
526
|
+
const cleaned = (Array.isArray(raw) ? raw : []).map((d) => String(d || '').trim().replace(/[\\/]+$/, '').slice(0, 400)).filter(Boolean);
|
|
527
|
+
const out = [];
|
|
528
|
+
const seen = new Set();
|
|
529
|
+
for (const d of cleaned.reverse()) { if (seen.has(d)) continue; seen.add(d); out.unshift(d); }
|
|
530
|
+
return out.slice(-max);
|
|
531
|
+
};
|
|
532
|
+
export function normalizeCheckout(raw) {
|
|
533
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
534
|
+
const dir = String(raw.dir || '').trim().replace(/[\\/]+$/, '').slice(0, 400);
|
|
535
|
+
if (!dir) return null;
|
|
536
|
+
const paths = [...new Set((Array.isArray(raw.paths) ? raw.paths : []).map((p) => String(p || '').trim().slice(0, 200)).filter(Boolean))].slice(0, WORK_CHECKOUT_PATHS_MAX);
|
|
537
|
+
const changed = Math.max(Number.isFinite(Number(raw.changed)) ? Math.round(Number(raw.changed)) : 0, paths.length);
|
|
538
|
+
const head = String(raw.head || '').trim().slice(0, 12);
|
|
539
|
+
const branch = String(raw.branch || '').replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
540
|
+
const at = Number.isFinite(Number(raw.at)) && Number(raw.at) > 0 ? Number(raw.at) : null;
|
|
541
|
+
return {
|
|
542
|
+
dir, branch: branch || null, head: /^[0-9a-f]{4,12}$/i.test(head) ? head : null, changed, paths,
|
|
543
|
+
shared: Boolean(raw.shared), ...(raw.statusUnknown ? { statusUnknown: true } : {}), ...(at ? { at } : {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
515
546
|
export function normalizeWork(raw) {
|
|
516
547
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
517
548
|
const out = {};
|
|
@@ -539,6 +570,17 @@ export function normalizeWork(raw) {
|
|
|
539
570
|
// hooks or not — stamps when it was declared, so a Cursor / Gemini / VS Code
|
|
540
571
|
// session that stopped mid-task is still found by the next session.
|
|
541
572
|
if (ms(raw.declaredAt)) out.declaredAt = ms(raw.declaredAt);
|
|
573
|
+
// 1.103.0: where it worked — directories beyond cwd, and each checkout's
|
|
574
|
+
// uncommitted state at the last Stop (replaced whole on every Stop).
|
|
575
|
+
const workdirs = normalizeDirList(raw.workdirs, WORK_WORKDIRS_MAX);
|
|
576
|
+
if (workdirs.length) out.workdirs = workdirs;
|
|
577
|
+
if (Array.isArray(raw.checkouts)) {
|
|
578
|
+
const checkouts = raw.checkouts.map(normalizeCheckout).filter(Boolean).slice(0, WORK_CHECKOUTS_MAX);
|
|
579
|
+
if (checkouts.length) out.checkouts = checkouts;
|
|
580
|
+
}
|
|
581
|
+
// 1.103.0: the card at handoffCardId was DRAFTED BY KLYPIX from this record
|
|
582
|
+
// (the session never wrote one) — every renderer says so.
|
|
583
|
+
if (raw.handoffAuto === true) out.handoffAuto = true;
|
|
542
584
|
return Object.keys(out).length ? out : null;
|
|
543
585
|
}
|
|
544
586
|
export function mergeWork(previous, patch) {
|
|
@@ -546,6 +588,12 @@ export function mergeWork(previous, patch) {
|
|
|
546
588
|
if (patch === null) return null;
|
|
547
589
|
const prev = normalizeWork(previous) || {};
|
|
548
590
|
const next = patch && typeof patch === 'object' ? { ...patch } : {};
|
|
591
|
+
// `addWorkdirs` APPENDS (one tool call at a time on Codex; a transcript walk
|
|
592
|
+
// on Claude), newest last, de-duplicated, capped like `workdirs`.
|
|
593
|
+
if (Array.isArray(next.addWorkdirs)) {
|
|
594
|
+
next.workdirs = normalizeDirList([...(prev.workdirs || []), ...next.addWorkdirs], WORK_WORKDIRS_MAX);
|
|
595
|
+
delete next.addWorkdirs;
|
|
596
|
+
}
|
|
549
597
|
// `addCommits` APPENDS (Codex records one commit per tool result; the
|
|
550
598
|
// Claude Stop hook replaces the full list with `commits`), newest last,
|
|
551
599
|
// de-duplicated by sha, capped like `commits`.
|
|
@@ -2940,7 +2988,37 @@ export const UNFINISHED_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
|
2940
2988
|
// Evidence of real work, from ANY host: a reply tail (Claude and Codex Stop
|
|
2941
2989
|
// hooks), own commits (Claude transcript, Codex tool results), a handoff card
|
|
2942
2990
|
// (brain_handoff), or a task declared through brain_sync (every MCP host).
|
|
2943
|
-
|
|
2991
|
+
// 1.103.0: uncommitted changes in a checkout it worked in are evidence too — a
|
|
2992
|
+
// session cut off before its first reply tail still left work on disk.
|
|
2993
|
+
export const hasUncommittedWork = (work) => Boolean(work && Array.isArray(work.checkouts) && work.checkouts.some((c) => c && Number(c.changed) > 0));
|
|
2994
|
+
export const hasWorkEvidence = (work) => Boolean(work && (work.lastWords || (Array.isArray(work.commits) && work.commits.length) || work.handoffCardId || work.declaredAt || hasUncommittedWork(work)));
|
|
2995
|
+
// The "where it worked" lines of one record — shared by formatUnfinishedWork,
|
|
2996
|
+
// the brain_handoff take reply and (as a twin) the Claude hook's footer. A
|
|
2997
|
+
// separate checkout is named in full with its branch, head and uncommitted
|
|
2998
|
+
// paths: the next session must not have to search for it. The project
|
|
2999
|
+
// checkout is shared by every session, so only the files THIS session touched
|
|
3000
|
+
// are reported there.
|
|
3001
|
+
export function formatCheckoutLines(work, { cwd = null } = {}) {
|
|
3002
|
+
const lines = [];
|
|
3003
|
+
const checkouts = Array.isArray(work?.checkouts) ? work.checkouts : [];
|
|
3004
|
+
const named = new Set();
|
|
3005
|
+
for (const c of checkouts) {
|
|
3006
|
+
if (!c || !c.dir) continue;
|
|
3007
|
+
named.add(String(c.dir).replace(/\\/g, '/').toLowerCase());
|
|
3008
|
+
const more = Number(c.changed) > c.paths.length ? ` …and ${Number(c.changed) - c.paths.length} more` : '';
|
|
3009
|
+
if (c.shared) {
|
|
3010
|
+
if (Number(c.changed) > 0) lines.push(` uncommitted in the project checkout (files this session touched): ${c.paths.join(', ')}${more}`);
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
const where = [c.branch ? `branch ${c.branch}` : 'detached HEAD', ...(c.head ? [`@ ${c.head}`] : [])].join(' ');
|
|
3014
|
+
const state = c.statusUnknown ? 'git status unavailable' : (Number(c.changed) > 0 ? `${c.changed} uncommitted: ${c.paths.join(', ')}${more}` : 'clean');
|
|
3015
|
+
lines.push(` worked in a separate checkout: ${c.dir} (${where}; ${state}) — not the project folder; git worktree list may not show it`);
|
|
3016
|
+
}
|
|
3017
|
+
const extra = (Array.isArray(work?.workdirs) ? work.workdirs : [])
|
|
3018
|
+
.filter((d) => !named.has(String(d).replace(/\\/g, '/').toLowerCase()) && (!cwd || String(d).replace(/\\/g, '/').toLowerCase() !== String(cwd).replace(/\\/g, '/').toLowerCase()));
|
|
3019
|
+
if (extra.length) lines.push(` also ran commands in: ${extra.join(', ')}`);
|
|
3020
|
+
return lines;
|
|
3021
|
+
}
|
|
2944
3022
|
export function clientWord(client) {
|
|
2945
3023
|
const key = String(client || '').toLowerCase();
|
|
2946
3024
|
if (key === 'claude-code' || key === 'claude') return 'Claude Code';
|
|
@@ -3010,7 +3088,11 @@ export function listUnfinishedWork({ brainPath, home, now = Date.now(), selfId =
|
|
|
3010
3088
|
lastWords: work.lastWords || '',
|
|
3011
3089
|
lastWordsAt: work.lastWordsAt || null,
|
|
3012
3090
|
commits: Array.isArray(work.commits) ? work.commits : [],
|
|
3091
|
+
// 1.103.0: where it worked — so a fix left in a side checkout is found.
|
|
3092
|
+
workdirs: Array.isArray(work.workdirs) ? work.workdirs : [],
|
|
3093
|
+
checkouts: Array.isArray(work.checkouts) ? work.checkouts : [],
|
|
3013
3094
|
handoffCardId: work.handoffCardId || null,
|
|
3095
|
+
handoffAuto: work.handoffAuto === true,
|
|
3014
3096
|
takenBy: work.takenBy
|
|
3015
3097
|
? { ...work.takenBy, live: Boolean(taker), status: taker ? (sessionStatusLabel(taker, now) || 'live') : 'not running' }
|
|
3016
3098
|
: null,
|
|
@@ -3068,10 +3150,13 @@ export function formatUnfinishedWork(records, { now = Date.now(), max = 5, cardT
|
|
|
3068
3150
|
lines.push(`- ${who} · “${flat(r.intent).slice(0, 120)}” · ${bits.join(' · ')}`);
|
|
3069
3151
|
if (r.commits.length) lines.push(` commits: ${r.commits.map((c) => `${c.sha} ${c.subject}`).join('; ').slice(0, 320)}`);
|
|
3070
3152
|
if (r.files.length) lines.push(` files it touched (free — not reserved): ${r.files.slice(-8).join(', ')}`);
|
|
3153
|
+
lines.push(...formatCheckoutLines(r, { cwd: r.cwd }));
|
|
3071
3154
|
if (r.lastWords) lines.push(` its last words: “${flat(r.lastWords).slice(0, 420)}”`);
|
|
3072
3155
|
if (r.handoffCardId) {
|
|
3073
3156
|
const title = cardTitles && cardTitles.get ? cardTitles.get(r.handoffCardId) : null;
|
|
3074
|
-
lines.push(
|
|
3157
|
+
lines.push(r.handoffAuto
|
|
3158
|
+
? ` 🤝 KLYPIX drafted a handoff card from this record (the session wrote none): ${r.handoffCardId}${title ? ` — “${flat(title).slice(0, 100)}”` : ''} (brain_handoff {action:"list"} prints it in full)`
|
|
3159
|
+
: ` 🤝 it left a handoff card in the brain: ${r.handoffCardId}${title ? ` — “${flat(title).slice(0, 100)}”` : ''} (brain_handoff {action:"list"} prints it in full)`);
|
|
3075
3160
|
}
|
|
3076
3161
|
if (r.takenBy) lines.push(` taken over by ${clientWord(r.takenBy.client)} ${String(r.takenBy.id).slice(0, 8)} ${r.takenBy.at ? agoLabel(now - r.takenBy.at) : ''} (${r.takenBy.status}) — coordinate with brain_message before duplicating it`);
|
|
3077
3162
|
if (r.resumeCommand) lines.push(` its own transcript, should the human want it: ${r.resumeCommand}`);
|
package/src/agent-rules.mjs
CHANGED
|
@@ -510,19 +510,21 @@ is near, the day ends, you are blocked, you are switching tool) Call \`brain_han
|
|
|
510
510
|
{action:"write"} with the task, what is done, what is NEXT in order, the files and how to verify.
|
|
511
511
|
It is a 🤝 card in this brain the human can see and correct, and the next session on this project —
|
|
512
512
|
any agent — sees it at its first \`brain_sync\` / session start and takes it with {action:"take"},
|
|
513
|
-
then {action:"done"} when finished. KLYPIX also records every session's last reply, files
|
|
514
|
-
commits
|
|
515
|
-
|
|
516
|
-
|
|
513
|
+
then {action:"done"} when finished. KLYPIX also records every session's last reply, files,
|
|
514
|
+
commits and the checkouts it worked in (with their uncommitted files) automatically, so
|
|
515
|
+
\`brain_sync\` may list **unfinished work** another session left without a card: if the human asks
|
|
516
|
+
you to continue it, take it with \`brain_handoff\` {action:"take", session:"<id>"} first, and look
|
|
517
|
+
first in the checkout the record names — it may be a side folder, not the project. That list is history, not a lock — a stopped session holds no claim on its
|
|
517
518
|
files. Never ask the human to paste a summary into the next session.
|
|
518
519
|
|
|
519
520
|
**A note that asks agents to keep off a file**, whatever its wording (here, in AGENTS.md / CLAUDE.md or any
|
|
520
521
|
notes file) can outlive the work it protected. Before you obey one, or before you edit the file
|
|
521
522
|
anyway, call \`brain_sync\` with that file in \`files\`: KLYPIX checks the note against live sessions,
|
|
522
|
-
unmerged local branches and registered worktrees.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
523
|
+
unmerged local branches and registered worktrees. Decide which kind of note it is: a temporary
|
|
524
|
+
reservation for someone's in-flight change ends when that work ends; a permanent rule (generated
|
|
525
|
+
file, policy, legal or human restriction) always holds, whatever the evidence says. If a temporary
|
|
526
|
+
reservation's work has ended and your task needs the file, change it there and tell the human so
|
|
527
|
+
they can remove the note — never delete the note yourself. An incomplete check says UNKNOWN.
|
|
526
528
|
|
|
527
529
|
**Don't** hand-edit \`brain.klypix\` (it's a packaged canvas — use the tools) or dump file contents into it; capture the *decision*, not the file.`;
|
|
528
530
|
|
package/src/codex-brain-hook.mjs
CHANGED
|
@@ -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.
|
|
@@ -495,6 +511,20 @@ async function main() {
|
|
|
495
511
|
? touchedFiles(input, projectDir)
|
|
496
512
|
: undefined;
|
|
497
513
|
const files = humanTurn ? [] : undefined;
|
|
514
|
+
const workPatch = codexWorkPatch({ event, humanTurn, prompt, input }) || {};
|
|
515
|
+
// Where it works (1.103.0): the directory a tool call ran in (Codex's exec
|
|
516
|
+
// carries `workdir`; a command may `cd` or `git -C` elsewhere). Dynamic +
|
|
517
|
+
// guarded so a stale bundle without the helper still records the rest.
|
|
518
|
+
let whereLib = null;
|
|
519
|
+
if (event === 'PostToolUse' || event === 'Stop') {
|
|
520
|
+
try { whereLib = await import(new URL('./session-handoff.mjs', import.meta.url).href); } catch { whereLib = null; }
|
|
521
|
+
}
|
|
522
|
+
if (event === 'PostToolUse' && typeof whereLib?.workdirsFromToolInput === 'function') {
|
|
523
|
+
try {
|
|
524
|
+
const dirs = whereLib.workdirsFromToolInput(toolInput(input));
|
|
525
|
+
if (dirs.length) workPatch.addWorkdirs = dirs;
|
|
526
|
+
} catch { /* best-effort */ }
|
|
527
|
+
}
|
|
498
528
|
const sessions = upsertSession({
|
|
499
529
|
brainPath,
|
|
500
530
|
id: sessionId,
|
|
@@ -517,8 +547,29 @@ async function main() {
|
|
|
517
547
|
// human prompt on a human turn, and at Stop the reply tail WHEN the host
|
|
518
548
|
// hands it over (Codex's Stop payload may carry `last_assistant_message`;
|
|
519
549
|
// this adapter never reads Codex's transcript). Absent = keep the known.
|
|
520
|
-
...(
|
|
550
|
+
...(Object.keys(workPatch).length ? { work: workPatch } : {}),
|
|
521
551
|
});
|
|
552
|
+
// At Stop, a bounded git status of every checkout this session worked in
|
|
553
|
+
// (cwd first, then the workdirs its tool calls recorded) — the fix left
|
|
554
|
+
// uncommitted in a side checkout is what the 2026-10-15 field case lost.
|
|
555
|
+
// A second touch, so it sees the workdirs the first one merged.
|
|
556
|
+
if (event === 'Stop' && typeof whereLib?.snapshotCheckouts === 'function') {
|
|
557
|
+
try {
|
|
558
|
+
const me = sessions.find((session) => session.id === sessionId);
|
|
559
|
+
const checkouts = whereLib.snapshotCheckouts({
|
|
560
|
+
dirs: [cwd, ...(Array.isArray(me?.work?.workdirs) ? me.work.workdirs : [])],
|
|
561
|
+
projectDir,
|
|
562
|
+
sessionFiles: [...new Set([...(me?.files || []), ...(me?.observedFiles || [])])],
|
|
563
|
+
});
|
|
564
|
+
if (checkouts.length) {
|
|
565
|
+
upsertSession({
|
|
566
|
+
brainPath, id: sessionId, client: 'codex', event, channel: 'lifecycle', cwd,
|
|
567
|
+
...(HOST_PID.pid ? { hostPid: HOST_PID.pid, hostPidSource: HOST_PID.source } : {}),
|
|
568
|
+
logicalSessionId: sessionId, identitySource: 'codex-lifecycle', work: { checkouts },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} catch { /* best-effort */ }
|
|
572
|
+
}
|
|
522
573
|
|
|
523
574
|
const ignored = event === 'PostToolUse' ? ownBrainMessageText(input) : [];
|
|
524
575
|
// Stop can show a UI warning but cannot add model context. Never advance a
|
|
@@ -552,6 +603,8 @@ async function main() {
|
|
|
552
603
|
} catch { return ''; }
|
|
553
604
|
})();
|
|
554
605
|
const lockNotes = await lockNotesBlock(projectDir, brainPath, sessionId, sessions);
|
|
606
|
+
const unfinished = unfinishedWorkBlock(brainPath, sessionId);
|
|
607
|
+
await projectLiveSafe({ projectDir, sessions, sessionId, lockNotes, unfinished });
|
|
555
608
|
emitSystemMessage([
|
|
556
609
|
formatPresenceMessage(sessions, sessionId, { includeSolo: true }),
|
|
557
610
|
lockNotes,
|
|
@@ -565,10 +618,16 @@ async function main() {
|
|
|
565
618
|
// ⏸️ Unfinished work other sessions left (session handoff, 1.99.0): the
|
|
566
619
|
// same engine block brain_sync prints, so a Codex session asked to
|
|
567
620
|
// "continue" what a Claude session stopped in starts from its record.
|
|
568
|
-
|
|
621
|
+
unfinished,
|
|
569
622
|
], event);
|
|
570
623
|
return;
|
|
571
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
|
+
}
|
|
572
631
|
if (event === 'UserPromptSubmit') {
|
|
573
632
|
const me = sessions.find((session) => session.id === sessionId);
|
|
574
633
|
// A STRONG status question is answered from computed state and REPLACES
|