projmux 0.15.1 → 0.15.2

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.
@@ -188,6 +188,8 @@
188
188
  - `make test` also covers keybinding surface tier catalog rules, action-centered `keymap.toml` `keys = [...]` multi-alias parsing/writing, quoted internal `Surface:Action` tables, legacy popup action ID aliases, canonical popup toggle names, AI split popup-vs-direct action labels, generated tmux config multi-alias rendering, global/direct conflict detection, surface-scoped native picker command conflict detection, Settings Keybindings list/search/detail surface-aware picker-local labels, compact action-list state copy, flat single-key/sequence action detail rows, key detail remove/test/replace rows, one visible Add binding flow, and one typed binding surface; the unified native recorder's 0–4 stroke reducer, length-driven single-versus-sequence save, empty Enter/Backspace no-op, last-stroke pop, Esc zero-mutation cancellation, four-stroke cap, continuous native byte-stream comma draft rendering with canonical space-form results, conflict retry, and one-reader/no-event-leak lifecycle; capture/typed comma-or-legacy-space normalization and byte-identical save/apply output, atomic single↔sequence replacement, comma list/detail/diagnostic display with space route/disk/runtime parity, and negative guards against the retired Add sequence, Record next stroke, selectable stroke-remove, and Sequence Editor authoring routes; the reserved Enter/Escape/Tab/Backspace/Delete/navigation base-key classifier across tmux aliases and modifiers, identical capture/typed/final-save rejection before schema/file/config/live-tmux writes, later-printable and first-stroke/raw/conflict parity, shipped PlainChord/PlainChords/PrefixChord/Sequences protected-action inventory, reasoned whole-editor read-only rendering, forged dispatch/direct-helper no-write, retained delivery tests/navigation, and existing schema-v2/default/runtime parse-render-bind parity; collapsed Troubleshooting entry copy, Settings delivery diagnostics that split logical key/raw bytes/tmux received key while distinguishing missing, ambiguous, adapter-needed, and delivered states, safe direct key pool and risky/reserved diagnostic-only copy, Projmux-owned Ghostty/Windows Terminal advanced delivery copy, raw escape/CSI-u/xterm modified/UserKey/UserSequence rejection from primary keymap storage, Labs compatibility redirect to the Keybindings root, existing macOS capture/add-key flows, unsafe raw capture, reset behavior, stale guide docs guards, and welcome/runtime footer copy that avoids hardcoded launch-key guides.
189
189
  - `make test` also covers known retired live no-prefix cleanup for the old `C-t` pane-label binding: cleanup renders before current binds, explicit current `C-t` reassignment wins, repeated apply is byte-idempotent, and unrelated bindings remain unchanged. `TestTmuxApplyRetiresStalePaneLabelChordAndMigratesKeymapOnce` additionally pins that the first apply migrates the keymap to `schema_version = 2` and the second writes no bytes and leaves exactly one backup.
190
190
  - `make test` / `make test-integration` / `make test-e2e`: keymap multi-stroke Phase 0 pins v2 `sequences` parse/render/merge, marker-only v1 migration with pre-v2 backup and rollback, repeat no-op bytes, deterministic two-to-four-stroke trie/table generation, shared prefixes across standalone/app actions, duplicate/strict-prefix/single-first-stroke conflict rejection, rejection of the undeliverable `C-m`/`C-i`/`C-[` spellings a terminal reports as `Enter`/`Tab`/`Escape`, unsafe and picker-local/command/output/default-sequence negatives, apply-ordered retire of the recorded roots/tables strictly before `source-file` (the generated config records that state but never removes it, because a `run-shell` loop is not ordered against a sourced file), native modified-stroke transport allowlisting without independent dispatch, and zero-write config/live failure ordering. The isolated real-tmux paths prove single dispatch for shared-prefix `mouse.toggle`/`window.create`, Escape and unknown continuation cancellation to `root` with zero raw pane input, terminal and native `send-keys -K -c` logical-stream parity, repeated apply idempotence, sequence removal cleanup, and unchanged single-chord behavior.
191
+ - `make test` / `make test-integration` / `make test-e2e`: Orphaned Registry Window Phase 2 managed close keys route the app config's `prefix x` and `prefix &` through canonical delete. `TestManagedDeleteKeyBindingsRenderMirrorGuardedCanonicalRouteWithStockFallback` pins the app-scoped `pane.delete`/`window.delete` catalog actions and their exact `if-shell -F "#{@projmux_pane_uid}"`/`"#{@projmux_window_uid}"` bindings: the mirrored branch is only `internal tmux delete-confirm`, the else branch is tmux 3.6's stock `confirm-before` body byte for byte, and the standalone snippet keeps both keys stock. `TestManagedDeleteKeymapChangeAndDisableReachGeneratedConfig` moves and disables them through `keymap.toml` `prefix`, adds a root key through `keys`, rejects two managed actions on one prefix key, and proves a vacated stock key is unbound and then re-bound to tmux's stock body before any managed bind. `TestManagedDeleteCatalogSurfaceRowsMapBothDirections` ties `catalog.pane.delete`/`catalog.window.delete` to `mutationKillPane`/`mutationKillWindow` in both directions. `TestTmuxDeleteConfirmIssuesOneLocalizedPromptToTheExactClient` issues exactly one en-US/ko-KR `confirm-before -b -t <client>` whose command is the Pane menu Kill route or `internal tmux window-delete` with the press-time anchor, performs zero deletes before the answer, and resolves to one guarded route and one runtime ledger row; `TestTmuxManagedDeleteIntentsRefuseMalformedArgvWithZeroTmuxCalls`, `TestTmuxWindowDeleteIntentReportsCanonicalResultToTheExactClient`, and `TestTmuxWindowDeleteIntentRefusalIsShownWithoutRawFallback` cover the canonical Window intent. `TestDefaultCatalogManagedDeleteConfirmParity` holds en/ko parity for the Registry deletion and the Agent-does-not-return-on-Continue impact. `TestGeneratedWindowLifecycleActionsReachTypedHandlersWithoutRawManagedVerbs`, `TestGeneratedCatalogMutationAndNavigationArtifactsHaveOneSurfaceRow`, and `TestFullRenderedTmuxConfigsHaveClosedGeneratedMutationSurfaces` now admit `kill-pane`/`kill-window` only as those exact stock bodies, counted per rendered config including a close-key override config. The integration smoke reads both applied `list-keys -T prefix` bindings. The e2e pane-menu leg presses `C-b x`/`C-b &` with `send-keys -K` on the attached client for a managed Pane, an unmanaged Pane, a managed Window, a stale-mirror Window refusal, and the Project's last Window. Raw-kill `killed` retention, Continue, and teardown tests are unchanged.
192
+ - `make test` / `make test-integration` / `make test-e2e`: Orphaned Registry Window Phase 3 routes tmux's stock menu Kill entries for managed resources through canonical delete. The app config unbinds and replaces tmux's stock `prefix <`, `MouseDown3Status`, `M-MouseDown3Status` Window menus and `prefix >`, `M-MouseDown3Pane` Pane menus; the standalone snippet keeps all five stock. `TestManagedStockMenusRenderInAppConfigOnlyAfterUnbindingTmuxStock` pins that scope, unbind-before-bind, the unchanged `MouseDown3Pane` in both configs, and that a keymap-assigned `prefix <` still wins. `TestManagedStockMenusKeepTmux36ItemsExceptManagedRoutes` parses tmux 3.6's stock menus spelled as literals and requires every generated item to equal them except Kill (`if-shell -F "#{@projmux_window_uid}"` to `internal tmux window-delete` with no `delete-confirm`, or `"#{@projmux_pane_uid}"` to the Pane menu Kill route, each with tmux's stock `kill-window`/`kill-pane` as the else branch), Rename (`internal tmux window-rename`), New At End (`internal tmux window-create`), Horizontal/Vertical Split (`internal tmux pane-menu split-right|split-down`), and the removed Respawn and New After. `TestManagedMenuKillCarriesTmuxKillOnlyAsTheMirrorAbsentBranch` holds all six Kill items, including `MouseDown3Pane`, whose unmanaged Kill now runs tmux's kill instead of refusing. `TestManagedWindowMenuRunsTheCatalogCreateAndRenameCommands` ties New At End and Rename to the catalog renderer and proves the Rename prompt template is `##`-escaped: measured on tmux 3.6, a menu item is format-expanded when the menu opens, and an expanded `%N` Pane handle in a `command-prompt` template is a response placeholder. `TestGeneratedPaneMenuArtifactsHaveOneClosedSurfaceRow` maps every item of all six menus to exactly one `window-menu.*`/`pane-menu.*` surface row and back, admits `swap-window`/`swap-pane`/`resize-pane` only as the exact stock presentation items and `kill-window`/`kill-pane` only as the Kill else branch; `TestFullRenderedTmuxConfigsHaveClosedGeneratedMutationSurfaces` counts those per rendered config. `TestGeneratedPaneMenuGolden`, `TestGeneratedPaneMenuOmitsRawTmuxMutationVerbsAndRespawn`, `TestTmuxPrintConfigBindsPaneContextMenu`, and the `TestBrightPhase2GeneratedConfigByteIdentity` digests change only for the Kill guard and the new menus. The run-shell ledger adds a `window-menu` surface. The integration smoke reads the applied `list-keys` of the five menus and `MouseDown3Pane`. The e2e pane-menu leg drives the real menus on the attached client with `send-keys -K` and SGR right-clicks: `prefix <` New At End, `MouseDown3Status` Rename and Kill on a non-current managed Window, `M-MouseDown3Status` Kill on an unmanaged Window with the Registry unchanged, a stale-mirror refusal with no raw kill, `prefix >` Horizontal Split and `M-MouseDown3Pane` Kill on a non-active managed Pane, `MouseDown3Pane` Kill on an unmanaged Pane, and a second Project's last Window leaving a zero-Window Project.
191
193
  - `make test` / `make test-integration` / `make test-e2e`: keymap multi-stroke Phase 1 Settings authoring pins one shared 1–4 stroke Add/replace recorder, picker-local sequence exclusion, en-US/ko-KR locale and navigation golden reachability, shared reserved-key policy with first-plain rejection and later-plain acceptance, typed/captured canonical v2 keymap and generated-config byte parity, length-driven single/sequence add and atomic cross-kind replace plus strict-prefix conflict recovery, saved/prepared/running-session reporting including no-live-tmux recovery, native-macOS-on/off versus Linux/WSL logical authoring parity with transport differences confined to Delivery diagnostics, and zero keymap/generated-config/live-server mutation from navigation, cancellation, Backspace editing, or delivery tests.
192
194
  - `make test`: Settings IA re-cut Phase 3 versioned keymap action-ID migration. The action catalog carries a `CanonicalID` beside the runtime `ID`, and `keymapActionManifest` derives the exhaustive old→canonical table from it, so `TestKeymapManifestCoversEveryCatalogActionExactlyOnce` fails on any action without a v1 spelling or any manifest row that resolves to no action, `TestKeymapCanonicalIDsAreUniqueAndDistinctFromLegacyIDs` rejects a canonical id that duplicates another action's canonical or legacy id, `TestKeymapRetiredIDsAreDisjointFromTheManifest` keeps retired ids out of the rename table and requires a remediation for each, `TestKeymapCanonicalIDsUseTheDottedSchema` pins the dotted lower-case shape and that the writer quotes it, and `TestKeymapDispositionIsExactlyOnePerTable` proves every table gets exactly one of canonical/coalesce/preserve-unknown/retired-remediation. Parser coverage adds the root `schema_version` marker with duplicate, forward-version and unknown-root-key refusals, and rejects an unquoted dotted table because that is a nested TOML table rather than a dotted key (`TestKeymapV1FileParsesAndRoundTrips`, `TestKeymapRejectsUnquotedDottedTableAndFutureSchema`). `TestKeymapMigrationPreservesEffectiveBindings` migrates a fixture carrying a custom multi-alias, `keys = []`, a transport-dependent additive alias, a legacy `plain` and a legacy `prefix` and requires the merged chord table to be identical before and after. The apply half is covered by `TestKeymapMigrationCoalescesIdenticalDualTables`, `TestKeymapMigrationRefusesConflictingDualTablesWithZeroWrites` (no keymap write, no backup, no tmux call), `TestKeymapMigrationPreservesUnknownTables`, `TestKeymapMigrationRepeatRunIsAByteWriteFreeNoOp` (unchanged bytes *and* unchanged mtime), `TestKeymapMigrationOnAbsentFileIsANoOp`, `TestKeymapMigrationBackupIsDigestNamedExclusiveAndReused`, `TestKeymapMigrationRefusesToClobberAForeignBackup`, `TestKeymapMigrationFailsClosedWhenTheDirectoryIsReadOnly`, `TestKeymapMigrationPreservesFileModeAndSymlinkTarget` (writes through the link, keeps 0600), `TestKeymapRollbackRestoresV0` and `TestKeymapRollbackRefusesACorruptBackup`. Convergence and the write boundary are pinned by `TestSettingsKeySaveConvergesAV0KeymapToV2`, `TestSettingsKeySaveAbortsWhenMigrationConflicts` (Schema/Saved/Prepared/Running-session stages), `TestTmuxApplyRefusesToApplyWhenTheKeymapMigrationFails`, `TestTmuxApplyNoReloadMigratesWithoutTouchingTheServer`, `TestConfigRenderReportsMigrationOnStderrAndWritesNoKeymap` (the preflight never reaches the generated artifact on stdout) and `TestReadOnlyRoutesNeverRewriteTheKeymap`, which drives the real dispatcher against an isolated HOME/XDG for `help`, `version`, `get`, `describe`, `agent usage`, `doctor --help`, both `config render` artifacts and `update apply --dry-run`. `TestUpdateDryRunPreviewsTheMigrationStageWithoutPromisingADiff` requires the preview to name the stage and leak no canonical id, and `TestPostUpdateApplyArgsAlwaysReachTheNewBinary` pins that `--no-apply` becomes `config apply --no-reload` so an installer suppresses the reload without skipping the migration. `TestKeymapAndRegistrySchemaMarkersAreIndependent` and `TestKeymapMigrationFailureLeavesTheRegistryUntouched` keep the keymap `schema_version` domain separate from the resource registry `apiVersion`/`schemaVersion` envelope, with separate backups and no shared transaction.
193
195
  - `make test`: Go updater exact-target atomic replacement. `update apply` on the `go` backend no longer runs `go install` and then converges with the path it resolved beforehand: it stages a scratch `GOBIN` beside the active executable, publishes into it, checks the artifact is a regular executable file, replaces the exact active executable through the same `atomicBinaryReplacer` the github-release backend uses, and runs the post-publication `config apply` against that replaced target. `TestUpdateApplyGoReplacesTheExactActiveExecutable` requires the target file to hold the published bytes for both `apply` and `--no-apply` and names the replaced path in the output; `TestUpdateApplyGoPublishesIntoAScratchGobinBesideTheTarget` pins that the publication carries exactly one environment entry — an absolute `GOBIN` that is a sibling of the target, so the user's real `GOBIN` is never written and the rename stays within one filesystem; `TestUpdateApplyGoRefusesArtifactsThatAreNotRegularExecutables` covers a missing artifact, a directory, a symlink and a non-executable file, and requires the target untouched, the scratch removed, and no post-publication convergence; `TestUpdateApplyGoCleansScratchAndLeavesAUsableBinaryOnEveryFailure` walks a missing toolchain, a failing `go install`, a failing replacement and a failing convergence, requiring the recovery command in every message and either the old or the new binary at the target, never a partial write; `TestUpdateApplyGoDryRunReplacesNothing` pins the preview lines and zero execution; and `TestRunUpdateExternalWithEnvLayersOverTheCallerEnvironment` holds the memoization boundary by requiring `PROJMUX_PROJDIR` to reach the child alongside the `GOBIN` override. `TestUpdateApplyRunsGoUpgradeInPublicationOrder` and `TestUpdateApplyRunsGoUpgradeNoApply` keep the published stage order unchanged.
@@ -311,7 +313,7 @@
311
313
 
312
314
  - `make test`: Declarative contract stabilization Phase 2 adds the Registry materialization invariant audit to `projmux doctor` as the `registry` section, present in the default report and in `--section registry`, in both text and `--json`. The audit is the consumer predicate rather than a second opinion about it: every Registry Project is planned through the shipped `planRegistryTopology` with no observed sessions, and `refusalScope` supplies the fatal/skipped split, so `TestDoctorRegistryInvariantAuditReusesTheMaterializePredicate` fails if the reported counts ever diverge from the planner's own refusals over the same Registry. After schema v2, `TestDoctorRegistryInvariantAuditReportsTheFieldFailureStructure` proves deleting a Window's last shell installs a canonical replacement and therefore emits no skipped-Window finding; the remaining gone-root Project is still reported as `registry.materialize.fatal.project`. `TestDoctorRegistryInvariantAuditStatesTheZeroCase` requires a clean Registry to report `registry.materialize.clean` and the number of Projects planned without `--verbose`, because a silent clean audit is indistinguishable from a section that never ran, and `TestDoctorRegistryInvariantAuditGoldens` pins the zero and N section output in text and JSON. `TestDoctorRegistryInvariantAuditWritesNothing` runs the production zero-write snapshot read under an isolated `HOME`/`XDG_STATE_HOME`/`XDG_CONFIG_HOME` on a machine that never created a Project and requires an unchanged filesystem tree and no state directory; `TestDoctorRegistryInvariantAuditNeverReachesATmuxServer` pins that the runner handed to the planner refuses every call, and `TestDoctorRegistryInvariantAuditDegradesWhenTheRegistryCannotBeRead` keeps an unreadable Registry a `registry.materialize.unavailable` finding rather than a failed diagnostic. `TestSupportReportCarriesRegistryInvariantCountsWithoutReasonsOrPaths` requires the archive's `doctor.json` to carry the same codes, kinds, and counts and neither the planner's reason wording nor the fixture's absolute paths, and `TestDoctorRegistryInvariantCodesStayInsideThePublishedInventory` keeps every emitted code and remediation inside `doctorFindingCodeInventory`/`doctorFindingRemediationInventory` so nothing reaches a support archive as an opaque hash. The audit performs no repair, schema migration, or display-layer diagnosis, and adds no second diagnostic command.
313
315
  - `make test` / `make test-integration` / `make test-e2e`: Declarative contract stabilization Phase 3 raises the Registry envelope to schema v2 and ships the atomic v1 → v2 invariant repair. `TestRegistryV2GenerationMigrationGoldens` labels the 0.10/0.11/0.12 source generations and pins raw SHA-256 bytes for every source/output pair while comparing generated output directly to the raw golden bytes and asserting repair and information-loss counts, identity/name/owner/name-reservation preservation, Agent conversation-pointer preservation, graph validity, and a no-op second pass. `TestV1MigrationCreatesCanonicalShellChainForAZeroWindowProject` covers additive opaque Window/Pane creation, and `TestValidV2CanonicalAnchorRoundTripsWithZeroDiff` fixes a valid anchor at zero diff. `TestEveryFirstMigratorLeavesDurableV012RepairAndLossEvidenceOnce` drives normal `Load`, `Update`, and explicit `Migrate` as the first writer and requires one private report adjacent to the exact versioned backup, with that backup's absolute path/SHA-256 and exact repair/loss detail; returned paths match the durable pair where the API returns evidence, and the next migration pass writes zero Registry, backup, or report bytes. Failure-injection rows require a report published before Registry staging to be removed on every failed ordering. `TestMigrateRejectsAnInvalidCurrentRegistryWithZeroWrite` prevents a current-version no-op from bypassing validation, and future/unversioned envelopes remain byte-identical refusals with no evidence report. `TestV012MigrationLeavesNoMaterializeRefusals` runs the shipped materialize predicate over the repaired 0.12 sample and requires zero fatal and zero skipped items plus the exact Project → Window → direct shell Pane chain. Current-schema validation globally enforces that chain, and last-shell deletion installs a replacement bare shell. Phase 15 consumes this canonical anchor as the only identity retained by `Open fresh`; schema-v2 validation remains the pre-commit refusal boundary for an invalid anchor.
314
- - `make test` / `make test-e2e`: Declarative contract stabilization Phase 5 moves the generated `MouseDown3Pane` Horizontal/Vertical Split and Kill entries onto managed resource intents. The generated-config byte golden fixes the exact `internal tmux pane-menu --client #{client_tty} <action> #{pane_id}` actions, and a negative audit requires zero `split-window`, `kill-pane`, `respawn-pane`, and `Respawn` literals in that menu. Unit tables prove both split directions emit one `agentPaneIntent` with the exact clicked `%N` in `anchorPaneID`, so `createFromIntent` and the popup-origin `anchoredActiveTargetLookup` remain the only scope and mutation definitions; Kill resolves through the same anchor and calls canonical `delete pane`, preserves its full output, and displays its summary on the originating client. Injected create and delete refusals are displayed on that client and trigger no fallback mutation. Respawn is removed from the generated menu and its internal action is rejected; no managed respawn/replace intent is introduced. The isolated real-tmux leg strips inherited `TMUX`/`TMUX_PANE`, uses a run-unique `TMUX_TMPDIR` and socket, audits the installed `MouseDown3Pane` binding and selects Split/Kill actions through a real `display-menu` on an attached client, verifies split uid/Registry visibility and canonical Kill removal/result, and validates then records the exact socket cleanup target under its smoke root.
316
+ - `make test` / `make test-e2e`: Declarative contract stabilization Phase 5 moves the generated `MouseDown3Pane` Horizontal/Vertical Split and Kill entries onto managed resource intents. The generated-config byte golden fixes the exact `internal tmux pane-menu --client #{client_tty} <action> #{pane_id}` actions, and a negative audit requires zero `split-window`, `respawn-pane`, and `Respawn` literals in that menu (since Orphaned Registry Window Phase 3 its one `kill-pane` is tmux's stock item as the mirror-absent branch of Kill). Unit tables prove both split directions emit one `agentPaneIntent` with the exact clicked `%N` in `anchorPaneID`, so `createFromIntent` and the popup-origin `anchoredActiveTargetLookup` remain the only scope and mutation definitions; Kill resolves through the same anchor and calls canonical `delete pane`, preserves its full output, and displays its summary on the originating client. Injected create and delete refusals are displayed on that client and trigger no fallback mutation. Respawn is removed from the generated menu and its internal action is rejected; no managed respawn/replace intent is introduced. The isolated real-tmux leg strips inherited `TMUX`/`TMUX_PANE`, uses a run-unique `TMUX_TMPDIR` and socket, audits the installed `MouseDown3Pane` binding and selects Split/Kill actions through a real `display-menu` on an attached client, verifies split uid/Registry visibility and canonical Kill removal/result, and validates then records the exact socket cleanup target under its smoke root.
315
317
  - `make test` / `make test-integration` / `make test-e2e`: Declarative contract stabilization Phase 6 makes termination evidence survive the exit boundary without taking the Registry lock. The supervisor writes one bounded JSON row, framed by leading and trailing delimiters in a single `O_APPEND` call to `termination-receipts.jsonl`; the leading delimiter terminates any prior process's partial tail. Controller convergence reads the append snapshot outside the Registry transaction, absorbs generation-guarded receipts before absence projection, tolerates malformed/partial rows without disabling later convergence, and relies on duplicate/stale receipts as no-ops rather than truncating or garbage-collecting the journal. `TestSupervisorReceiptPrewriteDoesNotWaitForRegistryLock` proves the prewrite never enters a deliberately blocked Registry update, `TestMalformedTerminationJournalRowCannotDisableConvergence` pins recovery from a truly unterminated tail, and `TestInvalidTerminationJournalRowCannotDisableLaterValidReceipt` proves a well-formed but contract-invalid row cannot poison later valid evidence. The generated hooks use three distinct reasons: `pane-exited` carries exact `#{hook_pane}` and reobserves only the matching stored activation handle; `pane-killed` carries no invented pane and retains whole-host reobservation; `window-unlinked` carries exact `#{hook_window}` as explicit event evidence while retaining whole-host fresh reobservation. Phase 6 had no persisted runtime/liveness mapping; Phase 3 now persists only the exact last-positive Window owner handles required for causal teardown. `TestLifecycleHookRoutesCarryExactPaneAndWindowEvidence` and `TestWindowUnlinkedRecordsKillEvidenceWithoutPersistedRuntimeState` pin the Phase 6 boundaries and kill-window evidence recording. `TestPaneKilledCoalescedBehindExactPaneExitWidensEveryLaterPass` pins that a narrow pane-exited lease holder widens after any coalesced event and stays whole-host through its final no-op verification, so a kill or different-pane event cannot be acknowledged under the first Pane's selector. `TestPaneExitedAbsorbsLateSupervisorReceiptAfterUnknownProjection` covers the fast-exit ordering where runtime-created first records unknown: exact pane-exited still treats later journal absorption as work, refines the released same-generation Agent's evidence to normal or killed, and repeats with zero transactions; `TestLateSupervisorRefinementNeverFollowsAResumedBinding` keeps a new non-empty Pane binding strictly outside that exception. The closed classification vocabulary adds `killed`: only a supervisor's SIGHUP wait status may claim it, it means external kill rather than intent, and a same-generation `control-action/intentional` receipt remains sticky when the resulting HUP arrives. `TestHookWaitStatusReceiptDecisionTableIsClosed` covers pane exit, pane kill, standalone window kill, last-pane clean exit, paired control receipt, and evidence-free absence; `TestTerminationPairingRejectsUnentitledClaims` proves supervisor cannot claim intent, control action cannot claim observed outcomes, and reconcile can claim only unknown. `FuzzTerminationEventSequencePreservesCanonicalDeleteAndHookRows` fixes the authority boundary over arbitrary event order: canonical delete removes the Pane row and every repeat is byte-identical, while hook-driven observation changes evidence/status, clears the Agent binding, preserves Pane/Agent rows, and a repeated reconciliation opens zero transactions.
316
318
 
317
319
  - `make test`: Phase 6 kill/reap race enforcement keeps external-kill evidence from settling as unknown merely because tmux's 50ms hook beat the supervisor append. `TestPaneKilledWaitsPastHookDelayForSupervisorReceipt` delays the SIGHUP receipt past 50ms and requires killed evidence; `TestPaneKilledWaitRefinesRacingUnknownWithDelayedSupervisorReceipt` starts from a racing reconcile/unknown and requires the same killed refinement. `TestPaneKilledReceiptWaitIsBoundedBeforeUnknownProjection` proves a genuinely receipt-free kill waits only for the fixed bound and then remains honest unknown. Phase 2 extends the same lock-free bounded wait to the exact `pane-exited` activation only: `TestExactPaneExitedReceiptWaitIsBoundedBeforeUnknownProjection` proves a receipt-free timeout still retains the resource with honest unknown evidence, while integration/e2e prove a racing same-generation normal supervisor row enables deletion. The wait reads only the exact-host inventory, Registry snapshot, and lock-free journal before convergence; it takes no Registry write lock, invents no intent, keeps exact pane exit narrowed to its `%N`, and leaves kill/window paths whole-host. At the process boundary, `TestRunSupervisedChildRetainsRelayedHUPEvidenceWhenChildExits129` proves a provider that catches a successfully relayed HUP and exits 129 still records external-kill evidence, while `TestRunSupervisedChildDoesNotInferHUPFromExit129` proves the same numeric exit without an observed relay remains abnormal.
@@ -517,6 +519,7 @@
517
519
  - `make test`: One oversized answer no longer cycles the Codex app-server control plane. The broker multiplexes one upstream connection across every bound thread, and `codexappserver.Client.readLoop` used to treat an inbound message past the 1 MiB frame bound as a fatal connection error. `thread/read` with `includeTurns=true` returns a thread's whole transcript, so a long-lived Agent's own lifecycle snapshot crosses that bound on its own: the connection died, `Broker.revoke` suspended **every** binding at once, each observer's next epoch re-issued the same read, and the cycle sustained itself -- measured on this machine at 0.86-1.35 connection epochs per second for hours, with `bind` never dipping. The framing layer now reads an over-long message to its exact byte count and drops it, so the stream stays framed; the client answers only the requests that were waiting with `ErrPayloadTooLarge` and keeps the connection; and the broker classifies that as `indeterminate` under its own `payload-too-large` reason rather than collapsing it onto `disconnect-boundary`, because the connection survived and an operator reading the two alike looks for a broker fault that is not there. `TestClientDropsAnOversizedAnswerAndKeepsTheConnection` requires the dropped answer to reach its caller as `ErrPayloadTooLarge`, a following notification to still arrive on the same connection, and the drop to be counted. `TestClientKeepsServingRequestsAfterAnOversizedAnswer` proves the drop is request-scoped by serving the next request on that connection. `TestWebsocketStreamDropsAnOversizedMessageAndStaysFramed` reads the whole message behind a dropped one, which is what a discard that consumed the wrong byte count would break. `TestOversizedAnswerEndsOneMutationAndNotTheConnectionEpoch` holds the blast radius: five oversized answers leave the connection epoch, the disconnect count, the binding count, and a neighbour binding's live fence and stream all untouched. `TestOversizedAnswerIsToldApartFromADisconnectBoundary` keeps the two indeterminate reasons distinct. Beyond the frame bound the message is still refused, because reading it out is itself the denial of service the bound exists to stop.
518
520
 
519
521
  - `make test` / `make test-integration`: Window teardown decision journal Phase 0 records each consumed lifecycle teardown decision at the app layer (never inside the pure core planner) as one closed `topology.teardown.decision` event. `TestTeardownReasonJournalMappingCoversEveryCoreReason` and `TestTeardownVocabularyMatchesCoreDecisionTable` read `internal/core/metadata` source for every `TeardownReason`, `TeardownAction`, and `TerminationClassification` constant and fail on any unmapped or unallowlisted value; the first also round-trips every code through the real store and every conflict reason through its unchanged error bytes. `TestTeardownDecisionEventClosedShapes` and `TestTeardownRecorderAppendsWithoutOutcomeOwnership` pin the closed shape, strict `win-`/`pane-` UIDs, rejection of the new fields on every other family, and best-effort appends that never own the top-level outcome. `TestTeardownJournalCleanLastPanePairRecordsOneDeleteWindow`, `TestTeardownJournalKilledLastPaneRecordsRetainAndKeepsRegistry`, `TestTeardownJournalCleanupRetryRecordsRetainOnce`, and `TestTeardownJournalAuthorityConflictRecordsOneRefusal` run each fixture beside an unjournaled twin and require identical results, errors, and Registry bytes with exactly one decision per evaluation; `TestTeardownJournalWindowUnlinkRecordsFirstWaitAndExhaustion` requires one `retain`/`awaiting-pane-exit` on the hook's first wait, zero during carried causal retries, and one at exhaustion; `TestTeardownJournalKillWindowShapeFirstWaitCarriesWindowPaneAndKilled` drives the real controller body for a `kill-window`-shaped Registry and requires that first record to carry the exact Window UID, Pane UID, and `killed` with unchanged Registry bytes, while `TestTeardownJournalAmbiguousUnlinkSubjectOmitsUIDs` and `TestLifecycleWindowUnlinkAwaitingSubjectOmitsAmbiguousPane` omit any ambiguous UID. The integration termination smoke asserts one `delete-window`/`window-teardown` record for the exact closed Window UID, the first-wait plus exactly one exhaustion record for a replayed unpaired `window-unlinked`, and one `awaiting-pane-exit` record with the exact Window UID for a real `tmux kill-window` whose Window row is retained. Its read-only status/viewer and automatic-path journal line-count windows, which follow hook-driven session stop/unregister/prune closes, exclude only the asynchronously appended `topology.teardown.decision` lines, so any record the measured command itself appends still fails.
522
+ - `make test`: Orphaned Registry Window Phase 1 records the live `$N/@N` of every Window a Project start/Continue topology replay materializes, in the replay commit itself, through the same `ObserveWindowRuntimeBinding` writer a canonical Window create uses. `TestTopologyReplayCommitsLiveBindingForEveryMaterializedWindowOnly` requires the new-session-adopted first Window and the new-window ones to carry their live pair with MissingRuntime cleared at the single replay write, while a Window outside the replay keeps its runtime ids byte-for-byte; `TestTopologyReplayUnderLiveSessionLeavesPresetLiveWindowBindingsUntouched` re-creates a raw-killed first Window under the existing session and leaves every preset-live Window row, including a wrong sentinel binding, byte-identical. `TestReplayedWindowCleanLastPaneExitAndLiveUnlinkDeleteWindowAndJournalOnce` pairs the replayed sole Pane's clean `pane-exited` with the live `window-unlinked`, deletes the Window and Pane, keeps the Project, journals one `delete-window`/`window-teardown`, and shows the stale pre-fix row shape does not pair; `TestReplayedWindowKillWindowRetainsWindowAndJournalsAwaitingPaneExitKilled` keeps a raw-killed replayed Window and journals `retain`/`awaiting-pane-exit` with `killed` and the exact Window UID; `TestReplayedWindowNonLastAgentPaneCleanExitRemovesOnlyPaneAndKeepsAgentOffline` removes only the Agent Pane and keeps the Agent Offline with its conversation; `TestPastOfflineWindowWithUnpairedTeardownEvidenceSurvivesReplayAndOtherPairs` keeps a past offline Window with MissingRuntime and unpaired evidence byte-identical across the replay and another Window's causal pair. Pairing conditions, teardown decisions, and Windows the replay does not materialize are unchanged.
520
523
 
521
524
  ## When To Update This List
522
525
 
@@ -2059,6 +2062,12 @@ separate decision this measurement exists to inform.
2059
2062
  `TestClaudeDialogueStreamRejectsUnknownEffectsAndBoundsState` cover transient
2060
2063
  public JSON validation, action/result pairing, zero pre-inbound tool effects,
2061
2064
  unknown metadata refusal and finite hook/tool state.
2065
+ - `TestClaudeDialogueStreamReplaysObservedFrameCorpus` replays
2066
+ `internal/app/testdata/claude-dialogue-observed-frames.json`, the top-level
2067
+ key shapes preserved from a Claude Code 2.1.263 run, through a fresh stream in
2068
+ each item's declared state. Items carry provenance labels and the measured
2069
+ verdict. A reject must stop once its recorded rule keys are removed, so any
2070
+ validator change that moves a recorded verdict fails with the item name and rule.
2062
2071
  - `TestClaudeDialoguePipeEOFClosesInputButWaitsCurrentTurn`,
2063
2072
  `TestClaudeDialoguePipeDiagnosticLossFailsClosedWithoutBusyLoop`,
2064
2073
  `TestClaudeDialogueCleanupWaitsExactWriterAndHandlesZombieWithoutSignals`, and
@@ -208,6 +208,21 @@ real-provider observation is opt-in through
208
208
  `scripts/agent-dialogue-live-canary.sh`; each version-stress row must qualify
209
209
  independently.
210
210
 
211
+ `TestClaudeDialogueStreamReplaysObservedFrameCorpus` pins the reply-only
212
+ validator's verdict on preserved real frame shapes. Neither it nor L20 proves
213
+ installed-provider compatibility, value drift (the corpus keeps top-level key
214
+ shape with placeholder values), or acceptance of unknown frames and fields (the
215
+ closed vocabulary rejects them by design). The L20 fixture emits none of the
216
+ corpus's dropped side frames. Two recorded real shapes are rejected today:
217
+ `result-success-25-keys` (the result allowlist lacks `origin`) and
218
+ `system-init-allowlist-diff` (the init allowlist lacks `memory_paths` and
219
+ `terminal_slash_commands`). Shapes without preserved evidence are gaps, not
220
+ items: `command_lifecycle` keys and values, a non-null assistant
221
+ `context_management`, `system` `thinking_tokens`, hook and user `tool_result`
222
+ frames, the exact real init key set, every nested value, `rate_limit_event`
223
+ envelope keys beyond `type` and `rate_limit_info`, and the lost earlier shape
224
+ inventories.
225
+
211
226
  Provider sources: [SessionStart and Stop hooks](https://code.claude.com/docs/en/hooks)
212
227
  and [cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging).
213
228
 
package/docs/cli-guide.md CHANGED
@@ -376,10 +376,16 @@ The generated `MouseDown3Pane` menu treats its Horizontal Split, Vertical
376
376
  Split, and Kill entries as Projmux resource actions. Both splits pass the exact
377
377
  clicked pane through the popup-origin anchor above and reach the same canonical
378
378
  `create pane` materializer as the CLI, so the new pane receives a Registry uid.
379
- Kill resolves that anchor's mirrored uid and reaches canonical `delete pane`,
380
- including its printed delete result. The menu never falls back to a raw tmux
381
- mutation when either route refuses. The reason is displayed on the exact client
382
- that opened the menu instead of being lost as a `run-shell` exit code.
379
+ Kill on a pane with `@projmux_pane_uid` resolves that anchor's mirrored uid and
380
+ reaches canonical `delete pane`, including its printed delete result; a pane
381
+ without the mirror runs tmux's own `kill-pane` instead of being refused. The
382
+ menu never falls back to a raw tmux mutation when either route refuses. The
383
+ reason is displayed on the exact client that opened the menu instead of being
384
+ lost as a `run-shell` exit code. The app config's `prefix >` and
385
+ `M-MouseDown3Pane` Pane menus follow the same rules, and its `prefix <`,
386
+ `MouseDown3Status`, and `M-MouseDown3Status` Window menus route Kill on a
387
+ mirrored Window to canonical `delete window`; see
388
+ [Managed menus](keybindings.md#managed-menus).
383
389
 
384
390
  tmux Respawn has no equivalent in the current resource model: it preserves the
385
391
  same pane handle, layout, Registry uid, and original command, while canonical
@@ -389,6 +395,23 @@ menu therefore omits Respawn entirely and does not expose a refusal handler or
389
395
  invent a replace operation. User-authored tmux bindings remain outside this
390
396
  managed-menu contract.
391
397
 
398
+ #### Managed close keys
399
+
400
+ The generated app config routes tmux's `prefix x` and `prefix &` through the
401
+ same resource routes whenever the target carries a Registry identity mirror.
402
+ `prefix x` on a Pane with `@projmux_pane_uid` confirms on the exact client, then
403
+ runs the Pane menu Kill route above: canonical `delete pane` with an
404
+ `intentional` termination receipt. The Agent that owned the Pane stays in the
405
+ Registry as Offline with a `deleted` exit, which Continue never replays.
406
+ `prefix &` on a Window with
407
+ `@projmux_window_uid` confirms, then runs `internal tmux window-delete`, which is
408
+ canonical `delete window` on that exact Window; a Project's last Window leaves a
409
+ zero-Window Project. A refusal is shown on that client and never falls back to
410
+ a raw kill. A target without the mirror keeps tmux's stock `confirm-before`
411
+ prompt and kill, with no Registry write. See
412
+ [Managed close keys](keybindings.md#managed-close-keys) for changing or
413
+ disabling the keys.
414
+
392
415
  ### Rename and rebind live convergence
393
416
 
394
417
  `rename project|window|pane` commits the selected Registry `metadata.name` and
@@ -537,7 +560,9 @@ generated `pane-exited` hook. The supervisor must have durably journaled a
537
560
  same-generation `normal` receipt, the hook must name the exact `%N` Pane and
538
561
  the owner Window must carry its exact last-positive `$N/@N` binding on the same
539
562
  socket, and fresh preflight plus locked observations must still resolve the
540
- same Registry owner chain. A non-last Pane is removed while its directly owning
563
+ same Registry owner chain. This includes Windows re-materialized by Project
564
+ start/Continue topology replay: the replay records each Window it materializes
565
+ with its live `$N/@N` in the same commit. A non-last Pane is removed while its directly owning
541
566
  Agent is retained Offline with its conversation identity. For a last Pane, that evidence is retained until a
542
567
  matching `window-unlinked` hook removes the Window; a final Project Window also
543
568
  removes its Window descendants while retaining the exact Project uid, root,
@@ -337,3 +337,26 @@ The selectorless E2E remains the single deterministic L20 scenario. Actual
337
337
  active-tool/human overlap, multiple ordinary requests, same-UID recovery and
338
338
  installed smoke remain separate unverified evidence. They are not run by this
339
339
  qualification-plus-one-idle transaction and are not inferred from its result.
340
+
341
+ `TestClaudeDialogueStreamReplaysObservedFrameCorpus` replays the preserved
342
+ Claude Code 2.1.263 frame shapes through the reply-only validator and fails when
343
+ a recorded verdict moves. Neither it nor L20 proves:
344
+
345
+ - compatibility with the installed provider, which only this canary observes;
346
+ - value drift, because the corpus keeps top-level key shape and fills values
347
+ with placeholders;
348
+ - acceptance of unknown frames or fields, which the closed vocabulary rejects
349
+ by design.
350
+
351
+ The L20 fixture emits none of the corpus's dropped side frames. Recorded real
352
+ shapes the validator rejects today:
353
+
354
+ - `result-success-25-keys`: the result allowlist lacks `origin`;
355
+ - `system-init-allowlist-diff`: the init allowlist lacks `memory_paths` and
356
+ `terminal_slash_commands`.
357
+
358
+ Shapes without preserved evidence are gaps, not corpus items: `command_lifecycle`
359
+ keys and values; a non-null assistant `context_management` value; `system`
360
+ `thinking_tokens` frames; hook and user `tool_result` frames; the exact real init
361
+ key set; nested values of every frame; `rate_limit_event` envelope keys beyond
362
+ `type` and `rate_limit_info`; and earlier shape inventories, which were lost.
@@ -217,6 +217,91 @@ without storing or replacing the transport default. Rename actions no longer
217
217
  have a built-in terminal fallback; use tmux's prefix rename flow or configure
218
218
  an explicit safe key where the action is editable.
219
219
 
220
+ ## Managed close keys
221
+
222
+ The generated app config replaces tmux's stock `prefix x` (kill Pane) and
223
+ `prefix &` (kill Window) with two catalog actions, `pane.delete` (v0 id
224
+ `delete-pane`) and `window.delete` (v0 id `delete-window`). Each binding
225
+ branches on the target's Registry identity mirror:
226
+
227
+ | Key | Target carries the mirror | Target has no mirror |
228
+ | --- | --- | --- |
229
+ | `prefix x` | `@projmux_pane_uid`: confirm, then the Pane menu Kill route (canonical `delete pane`) | tmux stock `confirm-before -p "kill-pane #P? (y/n)" kill-pane` |
230
+ | `prefix &` | `@projmux_window_uid`: confirm, then canonical `delete window` | tmux stock `confirm-before -p "kill-window #W? (y/n)" kill-window` |
231
+
232
+ A managed target is always confirmed first. The prompt is localized (en-US and
233
+ ko-KR) when the key is pressed. It says the resource is deleted from the
234
+ Registry and what happens to Agents: deleting a Pane leaves its owning Agent in
235
+ the Registry as Offline with a `deleted` exit, and deleting a Window deletes the
236
+ Agents in it. Neither comes back on Continue. Answering `y` deletes the target
237
+ with an `intentional` termination receipt, so a later Project start does not
238
+ re-materialize it. Deleting a Project's last Window follows `delete window` and
239
+ leaves a zero-Window Project. When the canonical route refuses, the reason is
240
+ shown on the client that pressed the key and nothing falls back to a raw tmux
241
+ kill.
242
+
243
+ A target without the mirror gets tmux's own stock prompt and kill, unchanged,
244
+ and projmux writes nothing to the Registry. The projmux process never issues
245
+ `kill-pane` or `kill-window` for either key: the stock body is tmux's binding,
246
+ run by tmux. The standalone `~/.tmux.conf` snippet does not carry these
247
+ actions, so a plain tmux server keeps both keys stock.
248
+
249
+ Both actions follow the ordinary keymap rules. In `keymap.toml`,
250
+ `prefix = "<key>"` moves the managed binding, `prefix = ""` disables it, and
251
+ `keys` adds root-table keys that run the same branching binding:
252
+
253
+ ```toml
254
+ [bindings."pane.delete"]
255
+ prefix = "X"
256
+
257
+ [bindings."window.delete"]
258
+ prefix = ""
259
+ ```
260
+
261
+ When an action no longer owns its stock key, because it was disabled or moved,
262
+ the generated config unbinds that key and binds tmux's stock body back onto it.
263
+ `prefix x` and `prefix &` therefore return to tmux's own behavior on the next
264
+ apply, including on a server that is already running, instead of being left
265
+ with no binding at all. Two managed actions on one prefix key are rejected.
266
+ Settings > Keybindings adds and removes root-table keys for these actions; their
267
+ prefix key is changed in `keymap.toml`.
268
+
269
+ ## Managed menus
270
+
271
+ The generated app config also replaces tmux's stock Window and Pane menus. Each
272
+ generated menu keeps tmux's title, position, item names, key shortcuts, and dim
273
+ conditions, and changes only the items that alter topology or identity:
274
+
275
+ | Menu | Bindings | Kill on a target with the mirror | Kill on a target without it | Typed items | Removed |
276
+ | --- | --- | --- | --- | --- | --- |
277
+ | Window | `prefix <`, `MouseDown3Status`, `M-MouseDown3Status` | `@projmux_window_uid`: `internal tmux window-delete`, canonical `delete window` | tmux stock `kill-window` | Rename: `internal tmux window-rename`; New At End: `internal tmux window-create` | Respawn, New After |
278
+ | Pane | `prefix >`, `M-MouseDown3Pane` | `@projmux_pane_uid`: the Pane menu Kill route, canonical `delete pane` | tmux stock `kill-pane` | Horizontal Split / Vertical Split: `internal tmux pane-menu split-right` / `split-down` | Respawn |
279
+ | Pane context | `MouseDown3Pane` | same as the Pane menus | tmux stock `kill-pane` | Horizontal Split / Vertical Split, as above | Respawn |
280
+
281
+ Swap Left, Swap Right, Swap Marked, Swap Up, Swap Down, Mark, Zoom, and the
282
+ copy-mode items (Go To Top, Go To Bottom, Search For, Type, Copy, Copy Line)
283
+ are presentation and run tmux's own commands. `MouseDown3Pane` also keeps its
284
+ AI Resume Picker entry.
285
+
286
+ Selecting Kill is the confirmation, as it is in tmux's own menus, so a managed
287
+ Kill asks nothing more. It deletes the target with an `intentional` termination
288
+ receipt: a Pane's owning Agent stays in the Registry as Offline with a `deleted`
289
+ exit, the Agents of a deleted Window are deleted with it, and deleting a
290
+ Project's last Window leaves a zero-Window Project. When the canonical route
291
+ refuses, for example because the mirror names no Registry resource, the reason
292
+ is shown on the client that opened the menu and nothing falls back to a raw
293
+ tmux kill. A target without the mirror gets tmux's own kill with no Registry
294
+ write; the projmux process never issues `kill-pane` or `kill-window`.
295
+
296
+ A mouse menu acts on what was clicked, not on what is focused: Kill, Rename, and
297
+ New At End in the status-line Window menu act on the clicked Window, and the
298
+ Rename prompt starts with that Window's name.
299
+
300
+ The standalone `~/.tmux.conf` snippet keeps tmux's stock `prefix <`, `prefix >`,
301
+ `MouseDown3Status`, `M-MouseDown3Status`, and `M-MouseDown3Pane` menus; it
302
+ carries only the `MouseDown3Pane` menu. A `keymap.toml` that assigns a managed
303
+ action to `prefix <` or `prefix >` keeps that key.
304
+
220
305
  ## Product Requirements
221
306
 
222
307
  Settings > Keybindings stays a discovery surface. It must continue to expose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projmux",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "tmux project session manager",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/crevissepartners/projmux#readme",
@@ -28,9 +28,9 @@
28
28
  "package:npm:pack": "scripts/package-npm.sh --pack"
29
29
  },
30
30
  "optionalDependencies": {
31
- "@projmux/linux-x64": "0.15.1",
32
- "@projmux/linux-arm64": "0.15.1",
33
- "@projmux/darwin-x64": "0.15.1",
34
- "@projmux/darwin-arm64": "0.15.1"
31
+ "@projmux/linux-x64": "0.15.2",
32
+ "@projmux/linux-arm64": "0.15.2",
33
+ "@projmux/darwin-x64": "0.15.2",
34
+ "@projmux/darwin-arm64": "0.15.2"
35
35
  }
36
36
  }