projmux 0.12.2 → 0.13.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.
@@ -39,6 +39,40 @@ Responsibilities:
39
39
  - parse command output
40
40
  - convert failures into typed errors
41
41
 
42
+ #### Codex app-server compatibility and lifecycle bridge
43
+
44
+ `internal/integrations/agents/codexappserver` is a Codex-only vertical slice.
45
+ It owns the headerless JSON-RPC request, response, and notification wire types,
46
+ the newline-delimited direct-stdio framing limit, request IDs,
47
+ initialize/initialized handshake, local cancellation, and connection
48
+ replacement. The local proxy transport performs the required HTTP Upgrade and
49
+ bounded RFC6455 WebSocket framing (including masked client frames) before
50
+ carrying those JSON-RPC messages. Core metadata and UI packages receive only
51
+ its closed, content-free health result; they do not import app-server request
52
+ or event types.
53
+
54
+ The compatibility probe runs the fixed read-only bridge `codex app-server
55
+ proxy` against the local control socket and sends only `initialize` plus
56
+ `initialized`. Doctor, Settings, and support-report triggers remain probe-only
57
+ and never mutate daemon state. A future native user-action trigger may enter the
58
+ lifecycle seam, but only the exact closed `daemon-not-running` classification
59
+ (the official local socket is missing or refuses a local connection) may invoke
60
+ the installed CLI's idempotent `codex app-server daemon start`, at most once for
61
+ the shared in-flight attempt in this process. The start and readiness retry are
62
+ bounded, each caller can cancel its own wait, and readiness must still complete
63
+ the proxy initialize handshake. All other executable, timeout, unsupported,
64
+ protocol, and endpoint failures stay on the existing fallback without a start
65
+ attempt.
66
+
67
+ The bridge discards command output and reports only closed, content-free health
68
+ and lifecycle reasons; prompts, tokens, paths, and process output do not cross
69
+ the integration boundary. It does not install, bootstrap, restart, or stop the
70
+ daemon, change Codex configuration, perform login, manage a custom socket, or
71
+ accept remote WebSocket control. Projmux shutdown does not stop the shared
72
+ daemon. No existing Agent create/resume, hook, review, catalog, model, or usage
73
+ consumer uses the native source in this phase. Settings displays the decision
74
+ as a read-only state row; it is not a user-selectable authority.
75
+
42
76
  ### 3. UI orchestration
43
77
  Picker data is modeled independently from row rendering. The app builds
44
78
  backend-neutral `picker.Item` values (`Title`, `Value`, `SearchText`,
@@ -71,7 +105,7 @@ Responsibilities that remain outside `projmux`:
71
105
  Config should be explicit and file-backed.
72
106
 
73
107
  Candidate areas:
74
- - managed roots
108
+ - managed roots (scan roots for candidate discovery; never managed identity)
75
109
  - default home-like roots
76
110
  - preview preferences
77
111
  - session naming exceptions
@@ -80,7 +114,7 @@ Candidate areas:
80
114
  ## State model
81
115
 
82
116
  Persistent state:
83
- - pins
117
+ - pins (typed: managed Project uid, or unregistered candidate path)
84
118
  - lightweight user preferences
85
119
 
86
120
  Ephemeral runtime state:
@@ -113,30 +147,166 @@ Packages:
113
147
  allocation, schema migration, snapshot reconciliation, and the operation
114
148
  transaction. It performs no I/O; the clock, uid source, and root-directory
115
149
  probe are injected through `Mutator`.
150
+ - `internal/core/resourcegraph` is pure: the resolved resource graph that joins
151
+ the Registry's desired topology to one exact tmux server, the typed
152
+ session/window/pane inventory it is resolved against, the closed attribution
153
+ and status vocabularies, and the transport descriptor. It performs no I/O.
154
+ - `internal/core/runtimediag` is pure: the read-only projection of one resolved
155
+ graph's runtime half -- every observed tmux object with its attribution, its
156
+ exact coordinate, and the Registry resource it is bound to, plus the scopes
157
+ that could not be observed. It performs no I/O and re-derives no attribution.
158
+ - `internal/core/registryview` is pure: the primary navigation view model. It
159
+ projects a resolved graph plus the caller's filesystem discovery onto the rows
160
+ the Projects, Sessions, and Recent Windows surfaces list -- Registry resources
161
+ in Registry order, discovered directories in their own section, and one Runtime
162
+ link -- with a status overlay and the actions each resource state is eligible
163
+ for. It performs no I/O.
164
+ - `internal/core/controller` is pure: the command-scoped controller kernel. It
165
+ owns the closed intent x attribution authority table, the guard evidence, and
166
+ the totally ordered plan every convergence producer is authorized through. It
167
+ performs no I/O and holds no tmux dependency; the guard field spellings are
168
+ supplied by the caller.
116
169
  - `internal/integrations/metadata` owns the registry file (lock, atomic write,
117
- migration) and the tmux transport mirror.
170
+ migration), the tmux transport mirror, and the bounded observation adapter that
171
+ fills a `resourcegraph.Inventory` from one exact server.
118
172
  - `internal/integrations/tmuxopts` is a dependency-free leaf holding the
119
173
  canonical spelling of every projmux-owned tmux option name, so the generated
120
174
  tmux config, session-state replay, and the resource mirror cannot drift.
121
175
 
122
176
  Resources and ownership:
123
177
 
124
- - Kinds are `Project`, `Window`, `Pane`, and `Agent`, stamped with
125
- `apiVersion: projmux.io/v1alpha1`.
126
- - `ownerRef` runs Project → Window → (shell Pane | Agent), and an Agent owns
127
- its current managed Pane.
178
+ - Kinds are `Project`, `Window`, `Pane`, `Agent`, and `ControlSession`, stamped
179
+ with `apiVersion: projmux.io/v1alpha1`.
180
+ - `ownerRef` runs (Project | ControlSession) → Window → (shell Pane | Agent),
181
+ and an Agent owns its current managed Pane. A Window's allowed owner set is
182
+ exactly those two root kinds; every other ownerRef kind is refused.
183
+ - A `ControlSession` is the app-owned control session -- the Home session
184
+ `projmux shell` opens -- as a Registry root. It exists so Home's Windows and
185
+ Panes have an owner chain at all: before it, pane `%0` of Home carried no
186
+ `@projmux_window_uid`, so every route that resolves "the active target"
187
+ refused there.
188
+ - **A ControlSession owns no filesystem path, and `ControlSessionSpec` has no
189
+ field that could hold one.** `spec.session` names the exact tmux session and
190
+ nothing else. That is the structural guarantee behind "$HOME is never a
191
+ Project": managed roots, trust, rebind, cwd defaults, and `ProjectByRoot` all
192
+ read `Project.spec.root`, so a control session cannot leak into any of them
193
+ even by accident. `$HOME` is never registered as a Project and never added to
194
+ managed roots.
195
+ - A ControlSession is recognized as one only on evidence, never by name: the
196
+ server must carry `@projmux_app=1` and the exact session's
197
+ `@projmux_session_role` must be exactly `control`. `@projmux_ephemeral=1`
198
+ together with a control role fails closed on both sides -- the reader refuses
199
+ the pair and the writer refuses to produce it.
200
+ - Control identity is one declarative controller plan, not an install-time
201
+ migration. The canonical shell lifecycle declares one exact socket/session;
202
+ `config apply` declares the canonical Home target on the exact `-L` server it
203
+ just reloaded; and later lifecycle triggers may continue only an exact
204
+ ControlSession identity already stored in the Registry. For those inputs the
205
+ root, control role, and every Window/Pane uid owner-chain mirror converge from
206
+ any partial state, and a second pass performs no Registry or tmux write.
207
+ Foreign or duplicate claimants and any Project uid claim refuse the whole
208
+ plan before its first write. Session-name resemblance, cwd, commands,
209
+ display names, and the app marker by itself are never promotion evidence.
210
+ - ControlSession names share the registry-wide scope with Project names but not
211
+ a reservation slot, because `nameReservations` is keyed by kind as well as
212
+ scope. A Project named `home` and a ControlSession named `home` coexist.
128
213
  - A persistent tmux **Session is not a resource**. It is a 1:1 runtime
129
214
  projection of a Project recorded in `Project.status.session` with a `live`
130
215
  flag, and it owns no uid, name, or ownerRef. Auto-attach ephemeral sessions
131
- live only in runtime inventory, outside the Project hierarchy.
216
+ live only in runtime inventory, outside the Project hierarchy. A
217
+ ControlSession is not a counter-example: it is a root resource that *names* a
218
+ session, and the session still carries no identity of its own.
132
219
  - `Window` and `Pane` carry **no stored liveness field**, deliberately. Their
133
220
  `status` block holds observed conditions only; live/offline is derived from a
134
221
  live tmux observation at read time. See *Runtime observation and resource
135
222
  status* below.
136
- - Every Window owns an initial Pane and stores its uid in
137
- `spec.primaryPaneRef`. Project registration creates this topology **offline**,
138
- with no tmux involvement, so Project and Window metadata stays queryable
139
- while tmux is down.
223
+ - Every non-empty Project stores one exact canonical Window and every Window
224
+ stores a role-independent Pane anchor. `Project.spec.primaryWindowRef`
225
+ resolves to a Project-owned Window whenever the Project owns any Windows;
226
+ it is empty only for the valid closed zero-Window state. Final schema v2 requires
227
+ `Window.spec.anchorPaneRef`; it may resolve through the same Window ancestry
228
+ to either a direct `role=shell` Pane or an Agent-owned `role=agent` Pane.
229
+ `Window.spec.defaultShellPaneRef` is optional; when present it resolves only
230
+ to a directly Window-owned `role=shell` Pane. Project registration creates
231
+ both refs on the same initial shell **offline**, with no tmux involvement,
232
+ so Project and Window metadata stays queryable while tmux is down.
233
+ Phase-2 consumers resolve `anchorPaneRef` as the stable role-agnostic split
234
+ target. An explicit Pane selector or popup origin wins over that stored ref;
235
+ the stored anchor is consulted only when the invocation scopes no Pane.
236
+ Shell-required offline creation may adopt or lazily allocate the optional
237
+ default shell without replacing an Agent anchor. Consumers never write the
238
+ removed intermediate `primaryPaneRef` field.
239
+ Canonical deletion preserves the same invariant: deleting the primary Window
240
+ reanchors to the first existing valid sibling, while deleting the last Window
241
+ leaves the existing Project with an empty `primaryWindowRef`, a non-live
242
+ session observation, and no replacement allocation. The requested Window and
243
+ all descendants are removed exactly; only explicit Project deletion removes
244
+ the owning root itself.
245
+
246
+ Home and root kinds:
247
+
248
+ This is the one place that answers "what is Home". The word names three
249
+ different things and they are not interchangeable.
250
+
251
+ - **The Home tmux session is a `ControlSession` root, and it is not a Project.**
252
+ `projmux shell` opens it, the convergence pass marks it
253
+ `@projmux_session_role=control` on an `@projmux_app=1` server, and
254
+ `BindControlSession` records it as a Registry root that owns Windows and
255
+ Panes. It has **no path**: `ControlSessionSpec` holds `spec.session`, the
256
+ exact tmux session name, and has no field that could hold a root. `$HOME` is
257
+ therefore never a Project root, never a managed root, never a rebind target,
258
+ and never returned by `ProjectByRoot`, and no route registers it as one.
259
+ - **`$HOME` the directory is a discovery candidate like any other.** If
260
+ filesystem discovery offers it, it is an unregistered bootstrap candidate. It
261
+ becomes a Project only if an operator explicitly opens it, and doing so
262
+ creates an ordinary Project that has nothing to do with the ControlSession.
263
+ - **The sidebar Home chrome row is neither of the above.** It is a synthesized
264
+ navigation row for the operator's own root: it carries no uid, no
265
+ `resourceRef`, and no managed identity, it is not a reconcile or create
266
+ target, and it disappears entirely when discovery does not offer `$HOME`. It
267
+ leads the Projects list because it is where the surface starts from, not
268
+ because it is a member of what the surface orders. Home's *Windows and Panes*,
269
+ by contrast, are managed rows -- they are owned by the ControlSession root --
270
+ while the Home session row itself stays classified `control` with no
271
+ `resourceRef`. See *Registry-first primary navigation* below for the row-level detail.
272
+
273
+ The root kinds may retain the same preferred tmux session name in stored state,
274
+ but that does not make the name an identity edge. An exact
275
+ `ControlSession.spec.session` claim wins before any Project session-name
276
+ fallback. If an explicitly opened Project would otherwise project onto that
277
+ physical session, its stable runtime name is `<preferred>--<full Project uid>`;
278
+ the Registry Project uid, root, and owner chain remain unchanged. A Project
279
+ uid/root observed on the exact control-owned session is D4 contamination, not
280
+ permission to adopt or rewrite the control-owned descendants. Observation
281
+ failures are quarantined as reason-bearing D6 items so an unrelated session can
282
+ still reconcile; only exact socket evidence authorizes runtime writes.
283
+
284
+ The consequence for every consumer is one rule: **the Registry has two root
285
+ kinds and a projection that walks roots has to walk both.** A traversal that
286
+ reads `registry.Projects` as if it were the whole root set will drop, refuse,
287
+ or fail to report whatever a ControlSession owns. Kind-scoped reads are still
288
+ fine and are the common case -- a Project root path, a Project session claim, a
289
+ Project pin -- but they are scoped on purpose, not by omission. The classified
290
+ list of every root traversal in the tree, and which of the two kinds each one
291
+ handles, is maintained as an executable table in
292
+ `internal/app/resource_reconcile_root_kind_test.go`; it is re-derived from the
293
+ source on every test run, so a traversal added or moved fails until it is
294
+ classified.
295
+
296
+ Divergence taxonomy:
297
+
298
+ Reconciliation uses one closed, additive classification for every plan and
299
+ refusal item. The item's human-readable `reason` remains separate from its
300
+ machine-readable `divergence` label: `D1-unrealized` is declared Registry state
301
+ with no realization, `D2-unattributed` is observed state with no exact resource
302
+ attribution, `D3-orphan-mirror` is a runtime mirror whose uid is absent from the
303
+ Registry, `D4-contamination` is a conflicting Registry identity or contradictory
304
+ exact evidence, `D5-drift` is a bound resource whose declared and observed
305
+ fields differ, and `D6-unknown` is the fail-closed remainder. This taxonomy does
306
+ not replace the resolver's managed/control/ephemeral/recoverable/unattributed/
307
+ foreign/conflict runtime classes or the reconciler's missing/stale/foreign drift
308
+ vocabulary. Doctor and dry-run reports expose counts for all six labels;
309
+ support reports expose those counts but redact item reasons and identifiers.
140
310
 
141
311
  Identity and naming:
142
312
 
@@ -181,10 +351,186 @@ Root lifecycle:
181
351
 
182
352
  Agent lifecycle:
183
353
 
184
- - The phase set is exactly `Pending`, `Running`, `Offline`, `Failed`. A normal
185
- managed-Pane exit or an explicit pane deletion resolves to `Offline`; a launch
186
- failure or an abnormal exit resolves to `Failed`. The Agent survives its Pane
187
- as a resumable resource.
354
+ - The phase set is exactly `Pending`, `Running`, `Offline`, `Failed`. An
355
+ abnormal exit resolves to `Failed`, while killed or unexplained disappearance
356
+ resolves to `Offline` and retains the Agent/Pane rows for diagnosis and
357
+ explicit recovery. A same-generation supervisor exit 0 paired with exact
358
+ `pane-exited` evidence is different: a non-last Pane is removed while its
359
+ Agent is retained Offline. For a last Pane, the complete Window subtree stays
360
+ pending until the exact causal `window-unlinked` half arrives.
361
+ - `Offline` for an unexplained disappearance rather than `Failed` is a deliberate
362
+ asymmetry. The phase is what an operator reads to decide whether to resume, and
363
+ an unproven `Failed` is worse for that decision than an honest `Offline`; the
364
+ fact that the answer is unproven is carried by `status.lastTermination`, where
365
+ it can be read without being mistaken for a diagnosis.
366
+
367
+ Termination evidence transport:
368
+
369
+ - A managed Pane's `status.activation` names one **materialization** of that
370
+ Pane, not the Pane. It carries an opaque `generation` minted per launch,
371
+ resume, and topology materialization, the exact `%N` handle it landed on, the
372
+ owning Agent uid for an Agent-managed Pane, and the operation id that issued
373
+ it. The uid survives kill/recreate and resume; the generation does not, and
374
+ that is what lets a receipt from a replaced process be recognized as stale
375
+ instead of applied to the Pane that now holds the uid.
376
+ - Every managed launch execs `projmux internal supervise --pane-uid <uid>
377
+ --generation <gen> [--agent-uid <uid>] -- <command>...`. The supervisor gives
378
+ the child this pane's exact stdin/stdout/stderr -- the pty tmux allocated, not
379
+ a pipe -- puts it in its own process group, and makes that group the
380
+ terminal's foreground group, so job control works and
381
+ `#{pane_current_command}` keeps naming the child. argv, cwd, and the
382
+ inherited environment are untouched except for two private `PMX_INTERNAL_*`
383
+ capability values carrying this Pane uid and activation generation to the
384
+ provider's own hook children. They are not public `PROJMUX_*` hook API and
385
+ are accepted only together with the Registry binding and exact recorded `%N`
386
+ runtime handle. tmux-side signals aimed at the pane process are relayed to
387
+ the child's group, because the pane pid and the child pid used to be the same
388
+ process. The foreground handoff is attempted and retried without
389
+ it rather than probed for: there is no portable way to ask "is fd 0 my
390
+ controlling terminal" without an ioctl, and a start that fails forks no
391
+ surviving child.
392
+ - A managed shell Pane -- one created with no command of its own -- is
393
+ supervised over the process tmux itself would have started: `default-command`
394
+ run by `default-shell` when it is set, and a **login** shell (argv[0] prefixed
395
+ with `-`) when it is empty. Both values are read from the same exact server
396
+ the pane is created on.
397
+ - `status.lastTermination` on the Pane, mirrored onto the owning Agent, is the
398
+ minimal durable receipt: closed `source` and `classification` vocabularies,
399
+ `observedAt`, the Pane uid, the optional Agent uid, the generation, and either
400
+ an exit code or a signal name, plus the operation id. It carries no command
401
+ text, no pane content, and no provider conversation data. Like
402
+ `status.sessionRef` it is an optional pointer with `omitempty` and additive
403
+ inside `schemaVersion: 1`.
404
+ - The classification vocabulary is four **kinds of proof**, not a severity
405
+ ladder. `intentional` is a canonical control action's own written record and
406
+ may only come from `source: control-action`. `normal` and `abnormal` mean a
407
+ supervisor actually reaped the child: exit 0 and everything else,
408
+ respectively. `unknown` is an explicitly evidence-free record. **Exit 0 is
409
+ never promoted to intent**: a provider that exits because the operator quit
410
+ and one that exits because it finished a batch produce byte-identical wait
411
+ statuses.
412
+ - Receipts are applied under a generation guard: the Pane must still exist, the
413
+ receipt's generation must be the Pane's current one, a receipt naming an Agent
414
+ must name the Agent that owns the Pane and still binds it, a receipt the
415
+ registry already stores verbatim is a no-op, and recorded intent is sticky for
416
+ its generation. The last rule is load bearing -- a canonical delete records
417
+ intent and then kills the pane, and the supervisor watching it reports the
418
+ resulting signal; letting the observation win would turn every deliberate
419
+ deletion into a crash report.
420
+ - Canonical `delete window|pane|agent` commits its intentional receipt in **its
421
+ own transaction, before the first live mutation**. A failure to make that
422
+ evidence durable aborts with zero tmux mutations. Every refusal after it
423
+ withdraws the receipt again, scoped by the operation id so it can only remove
424
+ what it wrote; a partial delete that really did kill something keeps the
425
+ evidence that explains it.
426
+ - Exit reconciliation is what consumes a receipt; see below.
427
+ - The lock-free `termination-receipts.jsonl` row precedes a clean process exit
428
+ and therefore outlives a qualifying Pane/Agent Registry deletion. That bounded
429
+ receipt is the post-delete diagnostic: source, classification, observed time,
430
+ Pane/Agent uid, generation, and wait status only. No command, pane content,
431
+ prompt, transcript, or provider payload is recorded.
432
+ - The supervisor resolves its state paths from the pane's own inherited
433
+ environment, which is the tmux **server's** environment rather than the
434
+ environment of the CLI call that created the pane. That is the correct
435
+ production binding -- the server is started from the operator's session -- and
436
+ it is why an isolated test has to start its server with the same state root it
437
+ reads the receipts back from.
438
+ - Losing a receipt is a supported outcome, not a failure mode. A supervisor
439
+ killed with `SIGKILL`, a lost tmux server, an unwritable registry, and a pane
440
+ whose supervisor could not be constructed all leave no receipt, and the pane
441
+ behaves exactly as it did before supervision existed. An absent receipt is the
442
+ input that resolves to `unknown`; it is never read as a normal exit.
443
+ - A managed process that dies before the create transaction that launched it
444
+ commits is a real edge exit reconciliation owns: the reconciliation runs inside
445
+ the next mutation's transaction and can retire the Pane before the supervisor's
446
+ receipt arrives. The receipt is then refused as stale, which is the correct
447
+ outcome -- the Pane it describes is gone -- and the Agent converges on
448
+ `Offline` with `unknown` evidence rather than on invented evidence.
449
+ - A Pane **adopted** from a runtime object created for another reason -- the
450
+ first pane a `new-session` brings with it -- carries no generation until it is
451
+ relaunched. Adoption is not supervision: the process was already running, so
452
+ there is nothing to have launched it with.
453
+
454
+ Exit reconciliation and lifecycle projection:
455
+
456
+ - A **lifecycle dirty event** is one exact-host statement that a managed runtime
457
+ object's lifecycle may have changed. `pane-exited` carries tmux's exact
458
+ `#{hook_pane}`. Its current-context session/window formats may already name a
459
+ survivor, so the owner `$N/@N` comes from the Window's last live Registry
460
+ observation; `window-unlinked` carries exact `#{hook_session}` and
461
+ `#{hook_window}` for the dead Window. `after-kill-pane` carries neither because
462
+ tmux leaves `#{hook_pane}` empty there. Whole-host and coalesced events remain
463
+ advisory projection inputs and never acquire delete authority.
464
+ - The event is advisory. The reconciliation re-observes the **final** snapshot of
465
+ that same exact host and re-reads the registry, so a stale event, a duplicate
466
+ event, and an event for a pane that has since come back all converge on the
467
+ same state as no event at all.
468
+ - The observation is the same mirrored-uid read (`list-panes -a -F
469
+ '#{@projmux_pane_uid}...'`) the reconciler and the active-target fallback
470
+ already share, routed through the event's exact target: an explicit `-L/-S`
471
+ addresses that server only, and a zero target routes through the inherited
472
+ client, which is the absolute socket in `$TMUX`. There is no default-socket
473
+ fallback -- a reconciliation that summed two servers could never report a death
474
+ at all, and a sibling server carrying the same `%N` handles or the same
475
+ mirrored uid receives zero calls.
476
+ - The retained-state transition is derived from the receipt the Pane already
477
+ stores. `abnormal` lands the Agent in `Failed`; `killed` and an evidence-free
478
+ disappearance land it in `Offline`. A `normal` receipt alone is still only
479
+ evidence. It becomes Pane/Agent delete authority only when the same controller
480
+ pass also has the exact hook Pane and Window, a non-empty exact-socket
481
+ inventory, and a current generation/owner chain.
482
+ - An absence with no receipt **records** an `unknown` one, with
483
+ `source: reconcile`. That is what makes the reconciliation idempotent: a second
484
+ pass finds the same document already stored, recording it is a no-op, and the
485
+ registry is left byte-identical. An absence with no stored value would be
486
+ re-projected on every pane exit in every session, forever.
487
+ - `source: reconcile` and `classification: unknown` may only appear together.
488
+ Unknown is a statement that nothing was observed, and letting a supervisor that
489
+ read a wait status or a control action that stated its intent file one would let
490
+ either of them erase evidence with it.
491
+ - A qualifying exact clean non-last exit removes only the Pane and leaves its
492
+ Agent Offline in the same locked Registry transaction. The owning Window,
493
+ Project/ControlSession, sibling Panes and sibling Agents are unchanged. A
494
+ shell and a provider are intentionally indistinguishable here: both are
495
+ supervised wait-status 0; no command or `/exit` text participates.
496
+ - Abnormal, killed, unknown, whole-host absence, missing/empty server inventory,
497
+ permission failure, foreign Window observation, stale generation, and an
498
+ Agent that now binds a resumed Pane all produce delete-plan zero. They keep the
499
+ retained lifecycle projection and canonical explicit Offline delete recovery.
500
+ - The closed Agent transition table stays the authority. An Agent that may not
501
+ reach the implied phase keeps its phase, its `paneRef`, and its managed Pane;
502
+ only the evidence is recorded. A refused transition is not a reason to discard
503
+ what was observed.
504
+ - Cost is measured in transactions. The projection set is computed against a
505
+ read-only snapshot and the write lock is taken only when something is
506
+ outstanding -- unrecorded evidence, or an Agent still bound to a dead Pane that
507
+ can still move -- so a reconciled disappearance costs zero transactions on every
508
+ later pass. Inside the lock the host is re-observed and the set recomputed,
509
+ because the registry may have gained a freshly created Agent while the event
510
+ waited, and applying the pre-lock observation to that newer registry would
511
+ release the new Agent and delete its still-live Pane.
512
+ - It fails closed. An observation that could not be taken is indistinguishable
513
+ from one that found nothing, and reading it as empty would file an `unknown`
514
+ termination against every managed Pane on a machine whose tmux server simply is
515
+ not up. It is also not an error: the reconciliation rides along inside other
516
+ operations and must never fail them.
517
+ - Ordering: a supervisor writes its receipt before its own process exits, so the
518
+ journal evidence is durable before tmux tears the pane down. The controller
519
+ absorbs it, re-resolves the exact `%N`/`@N` owner on the event socket, and then
520
+ repeats both inventory and owner/generation checks under the Registry lock.
521
+ Duplicate and permuted receipt delivery is idempotent. A late old-generation
522
+ receipt or a resume that wins the lock cannot follow the new binding.
523
+ - The reconciliation performs no runtime call beyond that one observation. It
524
+ never resumes an Agent, starts an offline resource, materializes a replacement
525
+ Pane, deletes an unmanaged object, or adopts one. An observation is not an
526
+ activation authority.
527
+ - `get pane|agent` renders the stored receipt in a `TERMINATION` column --
528
+ `<classification>/<source>` with the exit status when one was read, plus a
529
+ relative age -- and `describe pane|agent` renders the classification, source,
530
+ observed instant, exit code or signal, Pane ref, generation, and operation id as
531
+ their own rows. The Registry-first navigation carries the same receipt on its
532
+ Pane and Agent rows. All three are pure projections: a read verb never consumes
533
+ a receipt, advances a phase, or writes to the registry.
188
534
 
189
535
  Agent provider session ref:
190
536
 
@@ -237,11 +583,39 @@ Agent provider session ref:
237
583
  hook whose provider contradicts the Agent's `spec.provider` is refused with
238
584
  zero mutations.
239
585
 
586
+ Agent launch argv (workspace / task boundary):
587
+
588
+ - One Agent launch hands the provider CLI two independent things in a single
589
+ argv: the **workspace** (`--cwd` and every `--add-dir` the create validated)
590
+ and the **initial task payload** given after `--`. Where the workspace stops
591
+ is a property of the provider's own parser, so the boundary is provider
592
+ grammar data in `internal/app/agent_launch_argv.go`, not a concatenation at
593
+ the call site. `create agent` and `agent resume` read that one grammar, so a
594
+ provider's option arity cannot be spelled two ways.
595
+ - Claude's `--add-dir <directories...>` is **variadic**: it consumes every
596
+ following operand until an option-looking token or `--` stops it. So every
597
+ root travels in one occurrence and the payload is introduced by `--`. A
598
+ payload appended straight after the roots is parsed as one more directory, and
599
+ the session then starts with no task at all — an installed regression that is
600
+ invisible in the argv and surfaces only as an unacknowledged activation.
601
+ - Codex's `-C <DIR>` and `--add-dir <DIR>` each take exactly one value, so roots
602
+ repeat the option and no payload can be absorbed. Codex's argv is deliberately
603
+ left byte-identical, which is also why a Codex prompt beginning with `-` is
604
+ still read in option position, exactly as before.
605
+ - projmux gives additional roots only to Codex and Claude. A stored root for any
606
+ other provider is refused at launch construction rather than translated into a
607
+ flag this seam never validated, so an Agent never starts with access narrower
608
+ than what it records.
609
+ - An empty payload contributes nothing, so the interactive create and the resume
610
+ argv (where the provider's own conversation option, not a terminator, ends the
611
+ variadic root option) are unchanged.
612
+
240
613
  Agent resume:
241
614
 
242
615
  - `agent resume <ref>` rebinds an existing Agent: it builds the provider's
243
- **resume** argv from `status.sessionRef`, splits a new managed Pane detached on
244
- the target Window's `spec.primaryPaneRef`, and attaches it to that Agent. The
616
+ **resume** argv from `status.sessionRef`, resolves the target Window's
617
+ exact role-agnostic `anchorPaneRef`, splits a new managed Pane detached, and
618
+ attaches it to that Agent. The
245
619
  `metadata.uid` and `metadata.name` do not change, `status.phase` becomes
246
620
  `Running`, and `status.paneRef` points at the new Pane. `status.sessionRef`
247
621
  itself is read and never rewritten by resume.
@@ -278,11 +652,12 @@ Agent resume:
278
652
  Registry file and schema:
279
653
 
280
654
  - The registry lives at `<state>/projmux/metadata/registry.json` (0600 below a
281
- 0700 directory) behind an `O_CREATE|O_EXCL` lock file with bounded retry and
282
- stale-lock breaking, matching the notify queue and recent-windows stores.
283
- - The envelope carries `schemaVersion: 1`. **v1 is the first envelope projmux
284
- has ever written, and no migration step ships today**, so the current version
285
- is the only version the registry accepts.
655
+ 0700 directory). Ordinary mutations use an `O_CREATE|O_EXCL` lock file with
656
+ bounded retry and stale-lock breaking, matching the notify queue and
657
+ recent-windows stores. Explicit Registry repair uses its own recovery lock;
658
+ see the recovery boundary below.
659
+ - The envelope carries `schemaVersion: 2`. Version 1 is the first Registry
660
+ envelope projmux wrote and is the only older version this build migrates.
286
661
  - Everything else fails closed: the file is refused as unreadable and **no
287
662
  write happens at all** — no rewrite, no backup, no staged temp file. This
288
663
  covers a **newer** schemaVersion (which would destroy state a newer build
@@ -292,24 +667,269 @@ Registry file and schema:
292
667
  at the registry path, which is exactly the write-on-unknown-input that
293
668
  fail-closed exists to prevent. The registry is deliberately not quarantined
294
669
  or reset the way a corrupt recent-windows file is.
295
- - A file that is absent, empty, or whitespace-only is still the legitimate
296
- "no registry yet" case and yields a fresh empty registry. Only a file with
297
- actual content and no usable `schemaVersion` is refused.
298
- - The migration machinery is generic and version-indexed, ready for the first
299
- real schema bump: a registered older step is applied with backup → temp
300
- write → validate → atomic replace, so an interrupted or failing migration
301
- leaves either the original file or the fully migrated file, never a partial
302
- one. Downgrade writes are unsupported. Because production registers no step,
303
- that path is proven by tests that register one into a private migration set
304
- (`MigrationSet`, `ClassifySchemaVersionWith`, `MigrateRegistryWith`, and the
305
- store's private migration override) rather than by shipping a migration.
670
+ - A file that is absent, empty, or whitespace-only is the legitimate "no
671
+ registry yet" case **only before the first successful write**; see the durable
672
+ envelope below. Only a file with actual content and no usable `schemaVersion`
673
+ is refused as unknown.
674
+ - A normal locked `Load` of v1 runs the production 1 → 2 migration, validates
675
+ the repaired graph, writes the versioned backup, and publishes the v2 bytes
676
+ through the existing temp-file atomic replace. A failed repair leaves the v1
677
+ source bytes unchanged. Every successful first migrator (`Load`, `Update`,
678
+ `UpdateConvergent`, or explicit `Migrate`) also atomically publishes a 0600
679
+ `<exact-backup>.migration-report.json` beside the versioned backup before the
680
+ Registry replace. That durable evidence records the exact absolute backup
681
+ path, SHA-256 of its byte-identical v1 contents, version pair, repair/loss
682
+ counts, and every repair detail. It is outside rolling recovery retention.
683
+ A failed migration removes any staged/published report before returning while
684
+ leaving the source bytes unchanged. `LoadWithMigrationResult` and `Migrate`
685
+ additionally return both exact paths from the same locked transaction. A
686
+ second pass sees v2 and writes neither Registry, backup, nor report bytes. An
687
+ existing invalid v2 document is validated and refused byte-identically even
688
+ when explicit `Migrate` has no version step to run.
689
+ Explicit read-only inspection migrates only its returned in-memory view and
690
+ never publishes it.
691
+ - The v1 repair is deterministic over Registry order apart from injected opaque
692
+ uid generation. It preserves every existing uid, ownerRef, reserved name, and
693
+ Agent conversation/session pointer. A valid Project anchor is never
694
+ reselected. Otherwise it selects the first valid Project-owned Window and
695
+ direct shell-Pane chain; a Window with no valid direct shell promotes its
696
+ first direct shell or receives one bare shell, and a Project with no Window
697
+ receives the minimum Window/Pane chain. A non-empty shell cwd that no longer
698
+ names a directory is downgraded to the Project root. Every repair is recorded
699
+ in `MigrationReport`; replaced declared fields are separately marked as
700
+ information loss, while created Window/Pane resources are additive repair.
701
+ Production and golden tests call the same pure repair algorithm with injected
702
+ directory-existence and uid adapters.
703
+ - The canonical anchor is a schema-v2 write invariant. Until the separately
704
+ planned Project-start projection lands, the legacy `New` startup path's
705
+ prune-to-zero transaction fails validation and commits zero Registry bytes.
706
+ The Registry verdict precedes snapshot deletion, so that rejection also
707
+ preserves the latest snapshot byte-for-byte and performs no tmux mutation.
708
+ This fail-closed ordering is not Phase 3 authority to redesign Project start.
709
+ - Downgrade writes remain unsupported. Unversioned, malformed, and future
710
+ envelopes still fail closed before backup, staging, or replace.
711
+ - **Final schema-v2 Window shape:** the first public v2 contract has required
712
+ `anchorPaneRef` and optional `defaultShellPaneRef`. Unpublished v2 files with
713
+ `primaryPaneRef` are normalized under the Registry lock after an exact backup
714
+ and durable checksum report. A v1 file migrates directly to this final shape.
715
+ Mixed legacy/final authority is refused; the final writer emits no
716
+ `primaryPaneRef`; and a second final-v2 pass writes zero bytes.
306
717
  - **Field spelling:** the registry file intentionally uses the resource-model
307
718
  camelCase spelling (`apiVersion`, `schemaVersion`, `metadata`, `displayName`,
308
- `ownerRef`, `primaryPaneRef`, `spec`, `status`) rather than the snake_case
719
+ `ownerRef`, `anchorPaneRef`, `defaultShellPaneRef`, `spec`, `status`) rather than the snake_case
309
720
  used by the older projmux on-disk JSON. The two spellings coexist on purpose:
310
721
  existing snake_case files are **not** retro-changed, and the resource registry
311
722
  follows the resource-model contract.
312
723
 
724
+ Durable recovery envelope:
725
+
726
+ - The registry is the source of truth for managed identity and desired topology,
727
+ so `registry.json` is not the whole state: beside it the store keeps
728
+ `registry.initialized`, the marker that records a completed write, and
729
+ `recovery/`, a bounded set of the bytes replaced by semantic writes. The marker
730
+ and every copy are 0600, `recovery/` is 0700 like the directory above it, and
731
+ **no read creates any of them**.
732
+ - **First use versus state loss.** Before the first successful write there is no
733
+ marker, and an absent, empty, or whitespace-only registry is the empty
734
+ first-use registry — the zero-write read contract is unchanged, including the
735
+ `LoadReadOnly` short-circuit that must not materialize
736
+ `<state>/projmux/metadata/` for an operator who has never registered a
737
+ resource. Once the marker exists, the same content-free registry is
738
+ `ErrRegistryStateLost` on ordinary loads and mutations. Recovery inspection
739
+ still classifies the missing or empty state and stays available to the repair
740
+ route. Answering an empty registry there would hide the loss of every uid,
741
+ name reservation, and offline resource, and the next mutation would mint a
742
+ second identity domain on top of it. A registry written before the marker
743
+ existed is ordinary state, not a loss, and gains the marker on its next write.
744
+ - **Rolling recovery copies.** A same-version semantic write copies the bytes it
745
+ is about to replace to `recovery/registry-<stamp>-<seq>.json` before the
746
+ replace. Only *verified* bytes are copied: an absent, empty, or
747
+ structurally invalid prior file yields no copy. An invalid Registry blocks
748
+ every ordinary write; only the separately validated, explicitly sourced
749
+ recovery route may replace it. Retention keeps the newest five and removes the
750
+ rest deterministically — names sort chronologically, and the sequence
751
+ continues past the newest name rather than reusing one retention freed. A
752
+ migration keeps its own versioned `.bak` instead, so it never spends a
753
+ recovery slot on bytes that already have a backup.
754
+ - **A convergent no-op writes nothing at all.** `UpdateConvergent` on an
755
+ unchanged registry takes no recovery copy, publishes no marker, and leaves the
756
+ registry's bytes, mtime, and inode untouched. Convergence agreeing with stored
757
+ state is not a reason to replace it.
758
+ - **Write sequence.** Stage into a temp file in the same directory → `fsync` it →
759
+ re-read and validate it → copy the prior verified bytes → publish the marker if
760
+ absent → `fsync` the directory → atomic `rename` → `fsync` the directory again.
761
+ The live registry is only ever touched by that rename, and every step before it
762
+ is undone on failure, so an injected or real failure at any step leaves the
763
+ prior registry byte-identical with no staged file, no orphan copy, and no
764
+ half-created marker. Directory `fsync` is best effort for filesystems that
765
+ reject it (DrvFs and friends, the same ones that reject the permission repair),
766
+ because losing the ability to write state there would be worse than losing the
767
+ ordering guarantee.
768
+ - The marker is published **before** the rename so that its own failure cannot
769
+ leave a replaced registry behind. The cost is a crash window of one rename: a
770
+ hard crash between the marker and the very first registry rename leaves a
771
+ marker with no registry, which reads as state loss rather than first use. That
772
+ direction is deliberate — it asks the operator instead of silently starting
773
+ over — and the diagnostic names the marker so it can be removed to accept an
774
+ empty registry.
775
+ - **Distinct diagnostics.** Missing-after-initialization
776
+ (`ErrRegistryStateLost`), malformed (`ErrMalformedRegistry`), too new
777
+ (`ErrSchemaTooNew`), and unreadable (`ErrRegistryPermission`) stay four
778
+ separate causes classified with `errors.Is`, because they ask for four
779
+ different repairs. None of them creates an empty registry or a uid.
780
+ - **Restore is a separate operation.** Producing and bounding the copies is a
781
+ property of a write; selecting one and putting it back is an operator decision,
782
+ so it lives in the recovery boundary below rather than in the write path.
783
+
784
+ Degraded Registry mode:
785
+
786
+ - `valid` and a legitimate `first-use` are the only states from which an
787
+ ordinary mutation may begin. Missing or empty state after initialization,
788
+ malformed JSON, unsupported/newer schema, an invalid resource graph, and an
789
+ unreadable Registry enter degraded mode. This is a command-scoped
790
+ classification, not a sticky process flag: a successful repair makes the next
791
+ mutation healthy again.
792
+ - Ordinary writes classify before entering the normal mutation lock and repeat
793
+ the graph guard after acquiring it. A degraded refusal wraps the existing
794
+ typed cause, states that ordinary mutations are disabled, and ends with the
795
+ exact no-write next command: `projmux reconcile registry --dry-run`. It never
796
+ leaves a raw validation error as the whole diagnosis and never chooses a
797
+ recovery source for the operator.
798
+ - Public resource reads explicitly opt into the degraded decode path, so a
799
+ decodable invalid graph remains available without weakening ordinary
800
+ low-level Store loads. Recovery inspection can
801
+ diagnose even malformed, missing, empty, unsupported, or graph-invalid bytes.
802
+ `projmux reconcile registry` is the only write allowed to replace degraded
803
+ Registry bytes. Other reconcile, create, rename, rebind, delete, lifecycle,
804
+ and convergence writes stay on the ordinary gate.
805
+
806
+ Registry recovery boundary (`projmux reconcile registry`):
807
+
808
+ - **Two operations with deliberately different powers.** Planning classifies the
809
+ current registry and every bounded candidate and writes nothing at all — no
810
+ lock, no permission repair, no directory creation, no tmux mutation. Restoring
811
+ publishes exactly one source the operator named. There is no "just fix it"
812
+ mode: which copy is the truth is a judgment about which mutations were wanted,
813
+ and the command never makes it.
814
+ - **A separate repair transaction.** Restore serializes on
815
+ `registry.json.repair.lock`, never acquires or waits for the ordinary
816
+ `registry.json.lock`, and therefore remains available when a failed or stale
817
+ ordinary writer holds that lock. This is not a validation bypass: the operator
818
+ must name one source; the source is classified and graph-validated before and
819
+ under the recovery lock; the staged bytes are classified and graph-validated
820
+ again; and source/current checksums are rechecked immediately before publish.
821
+ - **Classification, not authority.** A plan reports the current registry and each
822
+ candidate as `valid`, `first-use`, `missing`, `empty`, `malformed`,
823
+ `schema-too-new`, `invalid`, or `unreadable`, with a `sha256:` digest of the
824
+ exact bytes, size, mtime, and the resource/reservation counts a verified
825
+ envelope holds. Only `valid` is restorable. The same classifier runs at publish
826
+ time, so a source is never previewed one way and validated another.
827
+ - **Fail-closed on the source.** Malformed JSON, an empty file, an envelope newer
828
+ than this build, and a graph that decodes but holds a duplicate uid, a dangling
829
+ `ownerRef`, or a broken name reservation are all refused. Restoring an
830
+ unverified source would replace a known-damaged registry with an
831
+ unknown-damaged one, and the second state is worse because it looks healthy.
832
+ - **Byte-semantic restore.** The verified bytes are published verbatim rather than
833
+ re-encoded, so uids, owner relations, and name reservations are preserved
834
+ exactly, a repeat restore is a byte comparison instead of a normalization
835
+ argument, and an older-but-known schema stays readable through the existing safe
836
+ read and migrates on the next semantic write.
837
+ - **The bytes being replaced are kept.** A restore copies the current registry to
838
+ `recovery/replaced-<stamp>-<seq>.json` before replacing it, and unlike the
839
+ write-side copy it keeps content that does **not** verify — that damaged
840
+ registry is the only remaining evidence if the restore turns out to be the wrong
841
+ call. Replaced copies are their own bounded family, so a restore never consumes
842
+ the automatic write history and never grows without bound.
843
+ - **Race guards.** `--expect-source-checksum` and `--expect-current-checksum` tie
844
+ a restore to the plan it was read from, and the preview prints the exact guarded
845
+ command. Underneath, the source is re-read and re-verified under the recovery
846
+ lock, the staged copy is re-validated, and both inputs are re-hashed immediately
847
+ before the single rename. Anything that moved refuses with the registry
848
+ byte-identical and tells the operator to re-run the preview.
849
+ - **A repeat restore is a byte no-op.** Bytes already equal to the source mean no
850
+ rename, no preserved copy, and no marker write.
851
+ - **Restore establishes the boundary.** Restoring into a state directory with no
852
+ marker publishes one, so a later loss on that machine reads as state loss rather
853
+ than as a fresh first use.
854
+ - **The live tmux mirror is evidence, never a source.** When no verified copy
855
+ exists, the plan reports what identity the *exact* server can still testify to —
856
+ mirrored Project/Window/Pane uids, names, the Project root, and containment
857
+ resolved from stable tmux ids — beside a fixed statement of what no mirror can
858
+ return: offline resources, every Agent (no tmux option carries an Agent uid),
859
+ an Agent-owned Pane's `ownerRef`, the name reservation table,
860
+ `spec.anchorPaneRef`, `spec.defaultShellPaneRef`, and labels/annotations/timestamps/status. A pane carrying
861
+ a provider option is counted as proof that an Agent existed whose own uid is
862
+ nowhere on the server. Nothing is imported and no registry is generated:
863
+ rebuilding from fragments would convert a visible loss into an invisible one.
864
+ - **No transport is a reason, not an error.** A restore is a filesystem
865
+ operation, so planning works outside tmux; the mirror section simply reports
866
+ that it has no exact target. The diagnostic is also skipped entirely when a
867
+ verified copy exists or the registry is healthy, so it never answers a question
868
+ nobody asked.
869
+
870
+ Resolved resource graph (`internal/core/resourcegraph`):
871
+
872
+ - **One join, consumed by everything.** The Registry is the source of truth for
873
+ managed identity and logical desired topology; a runtime observation is a status
874
+ overlay. `Resolve(registry, inventory)` produces the typed read model that the
875
+ controller, the runtime diagnostics surface, and the primary UI all consume, so
876
+ "is this Window live" and "may I mutate this pane" have one answer instead of
877
+ one per call site.
878
+ - **Rows come from the Registry, objects come from the machine.** Every Registry
879
+ row is emitted whatever the observation said, and every observed tmux object is
880
+ named and classified even when projmux owns none of it. Neither direction can
881
+ delete or invent the other's members.
882
+ - **Exact evidence only.** Attribution uses mirrored uids, the mirrored owner uid,
883
+ the exact session role value, and the stable containment ids tmux itself
884
+ reports. Session name, working directory, and running command are never
885
+ ownership keys: a heuristic merge here would attach an operator's unrelated
886
+ shell to a managed resource, and a wrong identity is worse than an unattributed
887
+ object.
888
+ - **Closed attribution set.** `managed` is a Registry resource, or the object bound
889
+ to one; `recoverable` mirrors a uid this Registry does not contain; `control` is
890
+ an app-owned session carrying the exact `@projmux_session_role=control` marker;
891
+ `ephemeral` is an auto-attach scratch session; `unattributed` has no mirrored
892
+ identity but sits inside a managed enclosure or on a server projmux started;
893
+ `foreign` has neither and belongs to the operator's own tmux; `conflict` is
894
+ evidence that contradicts itself.
895
+ - **Contradiction refuses to bind.** One uid claimed by two live objects, a uid
896
+ mirrored onto the wrong kind of object, and a claim whose live containment names
897
+ a different owner than the Registry does are all recorded as conflicts with both
898
+ tmux handles, and the row is never reported live and never handed a transport
899
+ handle. Absent containment evidence is not a contradiction: a session that lost
900
+ its Project option says nothing about ownership, so the object's own exact uid
901
+ still binds. A binding that would cross a Project boundary is impossible by
902
+ construction.
903
+ - **Status is derived, never stored.** `missing-root` outranks every runtime
904
+ answer, a bound handle is `live`, a scope that could not be observed is
905
+ `unknown` with a stated reason, and only a readable observation with no handle is
906
+ `offline`. An empty or failed observation can only downgrade a row; it can never
907
+ invent a live one. An Agent has no tmux object of its own, so its status is its
908
+ current managed Pane's status and its phase is reported from the Registry
909
+ verbatim.
910
+ - **Partial failure stays partial.** The host-ownership probe and the three list
911
+ queries are independent scopes. A failed windows query leaves Window rows
912
+ `unknown` while Pane rows keep their own observation, because a pane that is
913
+ provably gone is still offline. A socket with no server behind it is different
914
+ again: that is definite knowledge that nothing is live, so rows read `offline`
915
+ and only host ownership is unavailable.
916
+ - **Both hosts, one identity.** `@projmux_app=1` on the server is the only proof
917
+ of an app-owned host; anything else is a standalone host projmux is a guest on.
918
+ The same Registry and the same objects produce identical managed rows under both,
919
+ and a control-role marker on a server projmux does not own is refused, because
920
+ any process can set an option on the operator's tmux.
921
+ - **Explicit transport or none.** An observation is routed through exactly one
922
+ `-L <name>` or `-S <absolute path>`, resolved from the explicit socket flags
923
+ first and the inherited `$TMUX` socket path second. There is no implicit
924
+ default-server probe: with no transport the graph is a Registry-only snapshot
925
+ whose runtime answers are all `unknown`, and a sibling socket is never read.
926
+ - **Bounded and pure.** One observation costs one option probe plus three list
927
+ queries whatever the size of the server, is memoized for the invocation rather
928
+ than cached with a TTL — closing a pane must make the *next* command report it
929
+ offline — and issues no write verb. `Resolve` itself touches no filesystem, no
930
+ process, and no tmux, so the same inputs always produce byte-identical output
931
+ and a read can never materialize state.
932
+
313
933
  Session State interoperability:
314
934
 
315
935
  - Session snapshots carry resource identity through additive `omitempty`
@@ -318,10 +938,13 @@ Session State interoperability:
318
938
  `uid`, `name`, `labels`, `owner_kind`, and `owner_uid` in the snapshot's own
319
939
  snake_case spelling. No schema bump was needed, and a snapshot written
320
940
  without resource metadata still serializes byte-identically to the older form.
321
- - Snapshots written before resource metadata existed still load and reconcile
322
- deterministically: the Project is matched by session projection and then by
323
- root, and Windows and Panes are matched positionally against the registry
324
- topology in insertion order.
941
+ - Snapshots written before resource metadata existed still project
942
+ deterministically into an explicitly selected, closed Registry Project:
943
+ existing Windows and Panes are reused positionally in Registry order and any
944
+ additional descendants receive new stable identities. Restore validates a
945
+ pure Project-scoped plan, atomically commits that desired subtree, and only
946
+ then invokes the ordinary Project materializer. It never directly replays
947
+ snapshot topology into tmux and never replaces the global Registry.
325
948
 
326
949
  tmux transport mirror:
327
950
 
@@ -331,6 +954,21 @@ tmux transport mirror:
331
954
  `@projmux_pane_uid` plus the existing `@projmux_pane_label` as the Pane
332
955
  **name** mirror. These are the first window-scoped projmux options; every
333
956
  earlier one was pane-, session-, or global-scoped.
957
+ - Opening an unregistered directory is the gesture that mints a Project, and the
958
+ same flow finishes that Project's identity mirror. The first open takes the
959
+ shipped `EnsureSession` path -- which writes only the `@projmux_project_path`
960
+ anchor -- so the open itself writes `@projmux_project_uid` and
961
+ `@projmux_project_name` onto the session it just created, after the session
962
+ exists and before the client moves. It uses the same `MirrorProject` writer
963
+ every other mirror goes through, on the same plain `tmux` transport the session
964
+ was created on. The write is gated strictly on "this open registered the
965
+ Project": every already-registered Project converges through the Registry
966
+ topology engine, including desired state previously committed from a snapshot,
967
+ and opening `$HOME` mints no managed identity at all, so neither writes a mirror
968
+ option through this first-open gate. That gate is also what makes repeating an
969
+ open write nothing. Repairing a session that is already live without its
970
+ identity mirror is not this path's job: `projmux reconcile resources` is the
971
+ recovery route.
334
972
  - `rename pane` changes `Pane.metadata.name` and its `@projmux_pane_label`
335
973
  mirror only. It never writes the raw tmux `pane_title`.
336
974
  - `rename window` is the explicit stable-identity path: it changes only
@@ -440,6 +1078,13 @@ Runtime observation and resource status:
440
1078
  exact mirror is killed before the Registry commit; duplicate, foreign,
441
1079
  stale-owner, inventory-failure, and plan-to-execution race states remain
442
1080
  fail-closed. An implicit active Window is never treated as offline.
1081
+ - **`delete window|pane|agent` names its server the same way `reconcile
1082
+ resources` does**: explicit `--socket <name>`, explicit `--socket-path
1083
+ <absolute>`, or the inherited absolute `$TMUX`, and outside tmux with no flag
1084
+ it refuses. There used to be a fourth branch -- a hardcoded `-L projmux` --
1085
+ which meant a delete issued against an isolated server inventoried one host
1086
+ and killed objects on another. Refusing is the only remaining honest answer,
1087
+ and it names the two flags that fix it.
443
1088
  - The inventory is a pure **read**. It never writes, re-mirrors, or adopts a uid
444
1089
  onto a live tmux object; reattaching a lost binding belongs to the reconciler
445
1090
  (see *Binding reapply and adoption* below). After a tmux server restart the
@@ -541,20 +1186,94 @@ Binding reapply and adoption:
541
1186
  step, or that step would stamp `MissingRuntime` on a Window this same pass
542
1187
  just reattached.
543
1188
 
544
- Managed runtime binding convergence:
545
-
546
- - Binding repair has two explicit mutation boundaries. A normal
547
- `projmux config apply --socket <name>` first completes config preflight and a
548
- successful `source-file`, then runs the existing registry reconciler against
549
- that same exact `tmux -L <name>` server. The app-generated config also owns
550
- synchronous `after-new-window` and `after-split-window` hooks. Each hook
551
- expands tmux's absolute `#{socket_path}` and passes it to hidden internal
552
- plumbing that routes every reconciler read and mirror write through
553
- `tmux -S <absolute-path>`. Neither path falls back to the default socket or
554
- inherited `$TMUX`.
555
- - The lifecycle hooks are synchronous so a newly bindable Window or Pane has a
1189
+ Lifecycle trigger convergence:
1190
+
1191
+ - Every mutation and lifecycle producer reaches **one** entrypoint. A producer
1192
+ states a reason from a closed set (`config-apply`, `runtime-created`,
1193
+ `runtime-exited`) and one exact tmux server; it does not choose which stages
1194
+ run or in what order. `projmux config apply --socket <name>` reaches it after
1195
+ config preflight and a successful `source-file`; the generated config's
1196
+ `after-new-window`, `after-split-window`, `pane-exited`, and `after-kill-pane`
1197
+ hooks reach it through the hidden `internal tmux converge` route, which is the
1198
+ only lifecycle route there is.
1199
+ - The two exit hooks are in both generated configs; the two creation hooks are
1200
+ app-config only, and that asymmetry is the adoption boundary rather than an
1201
+ oversight. A convergence caused by a *new* runtime object mints and rebinds, so
1202
+ it adopts an unmarked window inside a managed enclosure. On the app-owned server
1203
+ every session is projmux's own and there is nothing to adopt by accident; the
1204
+ standalone snippet is sourced from the operator's `~/.tmux.conf` and therefore
1205
+ runs on every server they start, where a raw `new-window` in a session projmux
1206
+ does not own has to stay an unmanaged runtime object that only the Runtime
1207
+ diagnostics surface shows.
1208
+ - A hook states that something on one exact server may have changed. Exact
1209
+ `pane-exited` additionally carries tmux's `%N` Pane and `@N` Window handles;
1210
+ kill and coalesced triggers deliberately carry no invented identity. Every
1211
+ hook expands tmux's own absolute `#{socket_path}` and `#{session_id}`, so
1212
+ neither the route nor the convergence it drives falls back to the default
1213
+ socket or inherited `$TMUX`.
1214
+ - One convergence pass is one locked reconciliation followed by an exit-half
1215
+ reobservation. The reconciliation imports the live sessions it can attribute,
1216
+ reapplies the bindings it can prove, projects the lifecycle of every managed
1217
+ Pane whose runtime object died, and records why a Window or Pane lost one --
1218
+ all inside one registry transaction against one observation taken inside the
1219
+ lock. The projection has to run *after* the binding steps of the same pass:
1220
+ those steps mirror the uids the observation is diffed against, so an exit stage
1221
+ placed first would file an unknown termination against every Pane the pass was
1222
+ on its way to binding.
1223
+ - At most one worker converges one exact server at a time, held as an advisory
1224
+ whole-file lease under `<state>/projmux/controller/`. Not because two would
1225
+ corrupt anything -- the registry's own lock prevents that -- but because both
1226
+ pane-exit hooks fire on every pane exit in every session, and a fleet of
1227
+ workers contending for one registry lock is how a burst becomes lock-attempt
1228
+ exhaustion instead of a convergence. A producer that loses the lease records
1229
+ its dirty event and exits successfully; the holder has not acknowledged that
1230
+ event yet, so it runs a further pass for it. The lease is `flock` rather than a
1231
+ timestamped lockfile so a worker that is killed or panics leaves nothing to
1232
+ break.
1233
+ - The pass repeats until one of them writes nothing. That final no-op pass is the
1234
+ reobservation: a write that landed and did not converge -- a second client
1235
+ racing the same repair, a hook that rewrote an option back -- is exactly what a
1236
+ report claiming success must not hide. The loop is bounded; stopping early is
1237
+ safe in a way a lost event is not, because convergence is derived from the
1238
+ machine rather than from the event log.
1239
+ - Project runtime stop and fresh identity separation Phase 1 pairs a qualifying
1240
+ last-Pane `pane-exited` with the exact matching `window-unlinked` by socket,
1241
+ `$N` session, `@N` Window, `%N` Pane, Registry
1242
+ owner chain, and activation generation. The first event stores only bounded
1243
+ teardown evidence; the second re-observes every Window in that exact session,
1244
+ including unmirrored siblings, before deleting anything. The guarded
1245
+ transaction deletes exactly that Window, its Panes, its owned Agents, and
1246
+ their reservations. A non-last Project Window reanchors to its existing
1247
+ sibling. The last Project Window leaves the exact Project uid, root,
1248
+ reservation, pins, and snapshot bytes in the valid zero-Window state. A
1249
+ ControlSession likewise loses only the Window and keeps its root uid.
1250
+ Abnormal/killed/unknown exits, stale generations, unpaired or foreign handles,
1251
+ unavailable/empty observations, and missing-server or permission failures
1252
+ retain the graph. A historical offline Window without the stored causal Pane
1253
+ receipt is never deleted from absence alone; its fixed diagnostic recovery is
1254
+ an exact canonical `delete window uid:<window-uid>` on the named socket. No
1255
+ pane content, command, prompt, history, or transcript is an authority input.
1256
+ - Phase 2 gives Project startup and stop one closed lifecycle table. The input
1257
+ states are retained-window, zero-window, and deleted; the actions are Stop,
1258
+ Continue, Fresh, and explicit Project delete. Stop writes only the exact
1259
+ managed runtime and preserves every desired UID. Continue on retained-window
1260
+ writes only runtime and materializes the same descendant UIDs; Continue on
1261
+ zero-window atomically allocates one canonical Window/shell below the same
1262
+ Project UID before runtime materialization. The deleted+Continue cell accepts
1263
+ only a usable-snapshot precondition, then atomically creates a new Project UID
1264
+ and restores new descendant UIDs from that snapshot; without the precondition
1265
+ the same cell is an unavailable zero-write refusal and never falls back to
1266
+ Fresh. Fresh atomically replaces either
1267
+ registered state with a new Project/Window/shell UID chain and exactly one
1268
+ same-root claimant. Within this runtime/startup lifecycle table, canonical
1269
+ `delete project --yes` alone unregisters the Project graph; the separately
1270
+ scoped filesystem-missing `prune project` administrative policy is unchanged.
1271
+ Ordinary close-window is a separate operation class, so no one plan can also
1272
+ be stop, Fresh, or Project delete.
1273
+ - The creation hooks stay synchronous so a newly bindable Window or Pane has a
556
1274
  registry binding before the creating tmux command returns and before the next
557
- implicit read can run. Mirror writes use `set-option` and `rename-window`, not
1275
+ implicit read can run; the exit hooks stay backgrounded so closing a pane never
1276
+ waits on convergence. Mirror writes use `set-option` and `rename-window`, not
558
1277
  creation commands, so they cannot recursively fire either creation hook.
559
1278
  - A canonical resource create already owns the registry transaction while it
560
1279
  issues `new-window` or `split-window`. It therefore installs a private,
@@ -589,6 +1308,276 @@ Managed runtime binding convergence:
589
1308
  registry schema. It does not add persistent Project scope, matching by name,
590
1309
  cwd, or a new ordinal heuristic, uid merge/reassignment, pruning, or forced
591
1310
  adoption. The Project scope remains derived from the active binding on read.
1311
+ - There is no daemon and no auto-start. A trigger never resumes an Agent, never
1312
+ materializes an offline resource, and never adopts or deletes an unmanaged
1313
+ runtime object. Read verbs start no controller at all: `get`, `describe`, and
1314
+ implicit active-target resolution neither converge nor open a registry
1315
+ transaction, and they leave no controller event or lease behind.
1316
+
1317
+ Projmux split UI:
1318
+
1319
+ - Every split producer carries the exact popup origin as a typed canonical
1320
+ create intent. The origin must resolve through the mirrored Pane uid to its
1321
+ owner Window uid and then to exactly one Project or ControlSession uid; those
1322
+ Phase 11 declaration, root, role, Window, and Pane mirrors are the complete
1323
+ identity evidence. A ControlSession Pane's cwd is launch workspace only and
1324
+ never participates in root identity. If the origin disappears or its owner
1325
+ chain conflicts, canonical create performs no Registry or tmux write and
1326
+ projects the exact refusal to the originating tmux client.
1327
+ - The default `ai-split-right/down` binding, the `Alt-7` provider picker, the
1328
+ resume picker, and the provider and shell direct actions all produce a
1329
+ canonical create intent -- which provider, which side, and for a resume which
1330
+ conversation -- and hand it to the same `create` route a typed command reaches.
1331
+ The provider and shell branches render the exact argv an operator would type,
1332
+ so a UI action and a typed command cannot disagree about what `--placement
1333
+ down` means.
1334
+ - Only the materializer runs `split-window`. Before this convergence the saved
1335
+ default and both pickers descended into a legacy split that called tmux
1336
+ directly, so a pane opened from the UI was a runtime object the Registry had
1337
+ never heard of: no uid, no owner Window, no Agent row, and a Main UI row only
1338
+ once something else happened to reconcile. A raw unmanaged split now exists
1339
+ only where the operator makes one -- typing `tmux split-window`, or tmux's own
1340
+ pane-context-menu entries.
1341
+ - The saved split mode is the one piece of hidden state the split UI reads, which
1342
+ is why the canonical `create agent` route refuses to read it: a canonical route
1343
+ whose result depends on state the operator cannot see in the argv is not
1344
+ canonical. A saved mode that names no launch opens the picker; a saved provider
1345
+ that Settings has since disabled fails clearly, before the intent exists, with
1346
+ zero Registry and zero tmux mutations.
1347
+ - The resume picker joins a conversation the machine already has by reaching the
1348
+ same Agent allocation with the provider's *resume* argv substituted for its
1349
+ fresh-start argv. It is not `agent resume`: that verb rebinds an existing
1350
+ Registry Agent and must never fall through to a fresh conversation, while this
1351
+ interactive path may, because the operator picked a row and has already been
1352
+ told it could not be resumed.
1353
+
1354
+ Command-scoped controller kernel:
1355
+
1356
+ - One seam runs the whole sequence: observe one exact server, resolve it into a
1357
+ `resourcegraph.Graph`, plan, commit the Registry, guard tmux, execute, and
1358
+ reobserve. It is command-scoped and event-triggerable; there is no daemon.
1359
+ - Authority is a closed table over intent x attribution plus one explicit grant,
1360
+ not a predicate. The grant is `OperatorTargeted`: this invocation names one
1361
+ exact server the operator chose. `reconcile resources` cannot run without such
1362
+ a target, and that selection -- nothing else -- is what makes an unmarked
1363
+ object on a host projmux does not own repairable. Without the grant `foreign`
1364
+ is refused, and with it every lifecycle intent still is.
1365
+ `start`, `import`, and `delete` are refused for every class, so an offline
1366
+ resource, Home, an ephemeral session, and an unattributed Pane cannot be
1367
+ created, adopted, or removed by convergence. Repair is allowed on `managed`
1368
+ and on `unattributed` -- an unmarked object inside projmux's own runtime world
1369
+ carries no competing identity, so restoring a Registry-owned mirror overwrites
1370
+ nobody. `recoverable`, `foreign`, and `conflict` are refused; `control` and
1371
+ `ephemeral` are observe-only. An unknown class fails closed.
1372
+ - A planned write must also carry one of the two convergence verbs,
1373
+ `set-option` or `rename-window`. The verb gate is what makes "convergence
1374
+ never created or killed a runtime object" structural rather than a property of
1375
+ which candidates happen to exist today.
1376
+ - The plan is totally ordered: registry surface before tmux surface, then
1377
+ outermost containment first, then by stable key. Containment order is load
1378
+ bearing -- a Pane uid written into a Window that does not yet carry its own uid
1379
+ is attributable to nothing, and the next pass reads it as a Pane outside its
1380
+ owner scope.
1381
+ - Guards are exact evidence captured at observation time and re-proved
1382
+ immediately before the first live write, all or nothing: the server's own
1383
+ `#{socket_path}`, the target's mirrored uid, and the containing object's id.
1384
+ A stale guard aborts having written nothing and reports the exact retry.
1385
+ - After a run that changed anything, the kernel replans against fresh bytes and
1386
+ reports whether a repeat would write. Convergence is observed, not assumed.
1387
+ - Explicit topology materialization keeps its own engine and its own
1388
+ plan-time guard, because it plans against objects it is about to create, which
1389
+ no prior observation can have seen.
1390
+
1391
+ Runtime diagnostics escape hatch:
1392
+
1393
+ - A Registry-first surface is not an inventory, and that is the point of this
1394
+ one. The managed UI lists Registry resources, so an operator's own shell, the
1395
+ Home control session, a scratch session, and anything on a server projmux is a
1396
+ guest on are all correctly absent from it -- and "correctly absent" is
1397
+ indistinguishable from "lost" without a surface that shows the machine as it
1398
+ is. `projmux get runtime sessions|windows|panes` and the `projmux runtime
1399
+ diagnostics` picker are that surface.
1400
+ - It is a projection of `resourcegraph`, not a second join. Every row comes from
1401
+ the resolved graph, which already decided attribution from exact uid, owner,
1402
+ and role evidence; nothing here re-derives a class and nothing here consults a
1403
+ session name, a working directory, or a running command. Every observed object
1404
+ is emitted, managed ones included, because a managed object that needs no
1405
+ repair is exactly the row an operator looks for when the managed UI shows it
1406
+ and the machine seems not to.
1407
+ - Two handles per row, and they are not interchangeable. The stable tmux id is
1408
+ the only thing worth storing; the qualified coordinate -- a session name,
1409
+ `<session>:@N`, `<session>:@N.%N` -- is what an operator and the focus route
1410
+ address the object by. The session half of a coordinate degrades from the
1411
+ observed name to the `$N` id, and an object whose enclosing session cannot be
1412
+ resolved gets no coordinate at all rather than an unqualified handle the focus
1413
+ grammar would read as a session name.
1414
+ - One exact host, and no transport is an answer. The routing is an explicit
1415
+ `--socket`/`--socket-path` or the inherited `$TMUX` socket path, never a
1416
+ default-server probe and never a second socket. Outside tmux the read succeeds
1417
+ and reports every scope unavailable with a stated reason, where `reconcile
1418
+ resources` refuses the same case because it is about to write.
1419
+ - The whole surface is read-only. The Registry is opened without creating it,
1420
+ the observation is the bounded four-query adapter that owns no write verb, and
1421
+ the projection is pure, so a refresh is indistinguishable from not having run
1422
+ it. An empty item list next to a populated unavailability list is a different
1423
+ answer from an empty item list beside none.
1424
+ - The picker's actions are forwards, not features. `focus` moves a client and
1425
+ never materializes, `attach project` is the outside-tmux Project entry point,
1426
+ and the Resource Inspector is read-only; each is offered only where it
1427
+ applies, and where it does not the row states why. There is deliberately no
1428
+ adopt, import, rename, or kill: a diagnostic surface that could adopt what it
1429
+ found would be the heuristic merge the resolved graph refuses, wearing a menu.
1430
+ - `projmux runtime diagnostics` stays separate from `projmux runtime sessions`.
1431
+ That picker lists recent sessions to open one; this one lists every object on
1432
+ the server to explain what it is. Merging them would put an operator's own
1433
+ shell into the open-a-session list.
1434
+
1435
+ Registry-first primary navigation:
1436
+
1437
+ - The primary surfaces enumerate the Registry, not the machine. `internal/core/
1438
+ registryview` builds their rows from a resolved graph, so a Project is a row
1439
+ because the Registry contains it and not because a tmux session exists. The
1440
+ runtime contributes a status -- live, offline, missing-root, or unknown -- and
1441
+ an exact handle, and nothing else.
1442
+ - Identity is the Registry's. Membership and order in `registryview` are the
1443
+ Registry's own slice order, which is insertion order, and the pure view model
1444
+ applies no preference of its own: the same Registry projects the same rows in
1445
+ the same order on an app-owned server, on a standalone server, and outside tmux
1446
+ entirely.
1447
+ - Presentation order is the sidebar's, and only the sidebar's. The Projects list
1448
+ projects the managed rows onto three tiers -- pinned, then live, then closed --
1449
+ and preserves Registry order inside each tier as a stable tie-break. Pinned
1450
+ outranks live because a pin is a stated preference and liveness is an accident
1451
+ of the moment, so a pinned offline Project stays above an unpinned live one. The
1452
+ live tier is an overlay of one exact host, which makes two things contractual:
1453
+ the tier of a row may differ between hosts and between refreshes, and the
1454
+ selection may not follow a position. It follows the Project uid -- the old
1455
+ selection is resolved to its Project and that Project back to whatever row it
1456
+ renders as now -- so a tier change moves the row and not the resource the cursor
1457
+ is on. Nothing about a tier reaches the Registry: it is not stored, not
1458
+ reconciled, and not part of desired topology.
1459
+ - Row identity is the resource uid. A managed Project's *selection* is still its
1460
+ `spec.root` so the shipped open flow is unchanged, except for a Project whose
1461
+ root is gone: that row carries `uid:<uid>` and selecting it opens the read-only
1462
+ resource surface, which is where rebind is stated. Before this, such a row
1463
+ failed the whole picker on directory validation.
1464
+ - Filesystem discovery is kept and demoted. A discovered directory that no
1465
+ Project root claims is an unregistered bootstrap candidate in its own section;
1466
+ one that is already a Project root is dropped rather than listed twice with a
1467
+ second set of actions. Opening a candidate is the explicit gesture that
1468
+ registers it -- see the authority split below.
1469
+ - Home is chrome, not a Project, and the three senses of "Home" stay separate;
1470
+ *Home and root kinds* under the resource metadata model is the canonical
1471
+ statement and this row-level detail follows from it. The
1472
+ Home *control session* is never a managed row: the tmux session itself is app
1473
+ control runtime with no `resourceRef`, the only evidence that a session is one
1474
+ is the exact `@projmux_session_role` value the graph reads, and a session named
1475
+ `home` with no marker is honestly unattributed. The marker is written by the
1476
+ canonical `projmux shell` entry, for the app-session target only, and the same
1477
+ pass mirrors Home's Window and Pane identity -- so Home's *windows and panes*
1478
+ are managed rows owned by a `ControlSession`, while the session row itself
1479
+ stays `control`. Home is still not a Project and never appears in
1480
+ `get projects`. The Home *navigation row* is the operator's own root as
1481
+ filesystem discovery offers it, and it leads the Projects list because it is
1482
+ where the surface starts from rather than a member of what the surface orders.
1483
+ It is synthesized from nothing: it carries no managed identity, it is not a
1484
+ reconcile or create target, and if discovery does not offer `$HOME` there is no
1485
+ Home row.
1486
+ - The Sessions and Recent Windows surfaces list managed rows only, attributed by
1487
+ tmux's own `$N` and `@N` ids rather than by a name join, and carry the Registry
1488
+ resource name beside the exact tmux handle their actions target. What they
1489
+ withhold is tallied by class on a Runtime link that forwards to the escape
1490
+ hatch above.
1491
+ - The Projects sidebar's Runtime link is conditional, and only the link is.
1492
+ `Settings > Projects > Project Sidebar > Runtime diagnostics` chooses between
1493
+ `Always`, which is the shipped behavior, and `When needed`, which is the
1494
+ read-time default with nothing saved and no install migrated to it. `When
1495
+ needed` offers the row when the refused classes -- `Unattributed`, `Foreign`,
1496
+ `Recoverable`, `Conflict` -- sum above zero, or when the observation could not
1497
+ be taken: no transport, or any scope the inventory marked unavailable. Not
1498
+ being able to look is not the same as nothing being there, so a failed
1499
+ observation keeps the escape hatch reachable rather than hiding it.
1500
+ `Control` and `Ephemeral` are deliberately outside that sum: the app's own
1501
+ control session and a scratch session are what a healthy host looks like, and
1502
+ counting them would put the row back on every render. The decision is purely
1503
+ presentational -- `registryview` still emits a complete Runtime row and a
1504
+ complete class tally, a visible row carries its exact shipped label and tally,
1505
+ the Sessions and Recent Windows links are untouched, and `projmux runtime
1506
+ diagnostics` and `projmux get runtime ...` never read the preference. Hiding a
1507
+ row is not disabling a capability. An unreadable or unrecognized saved value
1508
+ resolves to `When needed` without writing anything and says so in Settings.
1509
+ - Every action forwards to a route that already owns it: `focus` for a live row,
1510
+ `attach project` for an offline Project -- the one shipped route that
1511
+ materializes one -- and `agent resume` for an Agent. Rebind and delete are
1512
+ listed as eligible with the exact command that performs them rather than
1513
+ executed from a read surface.
1514
+ - A navigation refresh is a read. It opens the Registry read-only, takes the
1515
+ bounded four-query observation through one exact socket, and projects it
1516
+ purely: no Registry or tmux write, no reconcile, no materialize, and no
1517
+ default-server probe when there is no transport.
1518
+
1519
+ Project discovery and pin authority:
1520
+
1521
+ Five things used to share two files, and each of them answered a different
1522
+ question wrongly as a result. Workdirs were a scan source *and* the thing that
1523
+ decided which Projects existed. The pin file was a presentation preference *and*
1524
+ a discovery input *and* the only record that a directory mattered. They are five
1525
+ separate authorities now, and the boundaries are the point.
1526
+
1527
+ - **Workdirs and project roots are scan roots.** `PROJMUX_MANAGED_ROOTS`,
1528
+ `PROJMUX_PROJDIR` and `~/.config/projmux/workdirs` name directories to look
1529
+ inside. Looking inside a directory registers nothing. On Windows they are
1530
+ OS-native paths and stay OS-native paths; nothing normalizes them into identity.
1531
+ - **A discovered child is an unregistered candidate.** It is a filesystem fact
1532
+ with no uid, no name reservation, and no Registry row. It stays one until
1533
+ something explicitly registers it, however many times it is scanned, rendered,
1534
+ or reconciled.
1535
+ - **The Registry is managed identity.** `projmux create project --root <path>` is
1536
+ the canonical bootstrap, and opening a candidate from the Projects sidebar
1537
+ performs the same registration for that one exact path. Both go through one
1538
+ transaction and both are idempotent: a root an existing Project already claims
1539
+ is answered from the Registry and writes nothing. Nothing else registers a
1540
+ Project. In particular the reconcile prelude no longer walks the discovery
1541
+ roots, so `create pane` in one repository cannot add a Project for every
1542
+ sibling directory under a scan root -- which is exactly what it used to do.
1543
+ `--project <name>` naming an unregistered candidate is a refusal that names the
1544
+ exact `--root` and the route that would register it.
1545
+ - **A managed pin is a Registry Project uid.** Its displayed root and name are
1546
+ projected from the Registry on every render, so the pin survives a rebind, a
1547
+ rename, and a `MissingRoot` condition. The sidebar tier reads the uid, never the
1548
+ path.
1549
+ - **A candidate pin is a path no Project claims.** It is a preference about a
1550
+ directory, kept as one. Rendering it, listing it, and pinning it never mint a
1551
+ Project.
1552
+
1553
+ Storage and migration:
1554
+
1555
+ - The pin file is a typed envelope: a `projmux-pins v2` header followed by
1556
+ `project <uid>` and `candidate <path>` lines. The kind is stored, not inferred,
1557
+ which is what lets one file hold both collections without either surface having
1558
+ to guess.
1559
+ - Reading never writes. Every rendering surface projects a pre-v2 file in memory
1560
+ through the same resolution a migration would persist, so the sidebar is
1561
+ identical before and after `projmux pin project migrate`.
1562
+ - Migration is per-line and atomic as a whole. A path exactly one Project's root
1563
+ claims becomes that uid; a path no Project claims stays a candidate; a path more
1564
+ than one Project claims refuses the entire migration with the pin file and the
1565
+ Registry byte-identical, and names the repair. A corrupt or newer-version
1566
+ envelope is refused rather than partially parsed, because a wrong guess about
1567
+ which resource a preference points at is worse than declining to load one.
1568
+ - Path folding is confined to two questions: candidate exact-match, and legacy
1569
+ path-to-uid migration. `candidates.MatchKeyFor` resolves symlinks on every
1570
+ platform and additionally folds separator, case, and drive-letter case on
1571
+ Windows, so `C:\Users\dev\src` and `c:/users/dev/src` are one candidate. It is
1572
+ never an identity operation: no amount of path agreement mints a Project uid or
1573
+ merges two, and the Windows rules are frozen by a compatibility table that a
1574
+ Linux test run asserts.
1575
+ - `pin project add|remove|toggle <dir>` keeps working unchanged and now resolves
1576
+ to a typed pin under one rule -- exactly one Project with that root makes the pin
1577
+ managed, none makes it a candidate, more than one is refused -- with
1578
+ `uid:<uid>` available when an operator wants to be explicit. Settings shows the
1579
+ three collections as three collections: Additional discovery roots, Pinned
1580
+ Projects, and Candidate Pins.
592
1581
 
593
1582
  Public resource reconciliation:
594
1583
 
@@ -603,16 +1592,22 @@ Public resource reconciliation:
603
1592
  normalized to deterministic placeholders; they are not matching keys and do
604
1593
  not obscure owner or target identity. Human and JSON output share the same
605
1594
  sorted items and missing/stale/foreign/orphan vocabulary.
606
- - Execute rebuilds the plan from the locked current Registry. Runtime
607
- observation is limited to the Registry Project graphs safely attributable to
608
- sessions on the selected socket; absence there never marks another socket's
609
- graph missing or releases its Agents. The desired
610
- Registry is validated and committed before any non-transactional tmux mirror
611
- write, keeping Registry identity authoritative and retryable if a later live
612
- step fails. After commit, every planned live write is guarded by re-reading
613
- its target's Project, Window, or Pane UID binding from the exact socket; all
614
- guards and planned before-values must still match before the first write. A
615
- recycled or raced handle therefore causes zero live writes.
1595
+ - Execute runs through the controller kernel. It rebuilds the plan from the
1596
+ locked current Registry and authorizes every runtime write against the graph
1597
+ resolved from the pre-lock observation. Runtime observation is limited to the
1598
+ Registry Project graphs safely attributable to sessions on the selected
1599
+ socket; absence there never marks another socket's graph missing or releases
1600
+ its Agents. The desired Registry is validated and committed before any
1601
+ non-transactional tmux mirror write, keeping Registry identity authoritative
1602
+ and retryable if a later live step fails. After commit, the socket identity
1603
+ and every planned write's uid and containment guards are re-proved from the
1604
+ exact socket; all of them must still match before the first write. A recycled,
1605
+ moved, or raced handle therefore causes zero live writes.
1606
+ - The report is one projection consumed by both renderers. Alongside the sorted
1607
+ items it carries the observed host mode, the authority rows the run
1608
+ exercised -- including the start, import, and delete refusals that are the
1609
+ evidence nothing was activated or adopted -- and the post-execute
1610
+ reobservation.
616
1611
  - A Registry commit failure performs no tmux mutation. A partial tmux failure
617
1612
  leaves the durable Registry identity in place, replans current drift, and
618
1613
  reports completed stages, remaining items, and the exact retry command.
@@ -638,17 +1633,57 @@ Explicit Registry topology materialization:
638
1633
  one Registry Project and uses a separate pure plan. The default reconciliation
639
1634
  shadow never calls the materializer, and the materialization plan never runs
640
1635
  blank adoption, orphan minting, or Agent phase observation. Registry insertion
641
- order determines session/Window/Window-owned shell Pane creation order; report
642
- keys provide a separately stable rendering order.
643
- - Registry presence is desired topology. Missing runtime sessions, Windows, and
644
- Window-owned `role=shell` Panes are drift; canonical Registry deletion removes
645
- that desire. Exact uid/name/owner mirrors are retained. Stored Pane CWD drives
646
- only that Pane's detached runtime cwd, while Project root remains the session
647
- path anchor and `PROJMUX_CWD` hook value. `Pane.spec.command`, Agent-owned
648
- Panes, Agent providers, snapshots, notifications, and ephemeral sessions are
649
- never execution inputs.
1636
+ order determines session/Window/Window-owned shell Pane/Agent creation order;
1637
+ report keys provide a separately stable rendering order. Agents are created
1638
+ last inside their Window. A shell or managed-Agent `anchorPaneRef` must be
1639
+ proven on the exact Window; no alternate live Pane is inferred.
1640
+ - Registry presence is desired topology. Missing runtime sessions, Windows,
1641
+ Window-owned `role=shell` Panes, and Agents are drift; canonical Registry
1642
+ deletion removes that desire. Exact uid/name/owner mirrors are retained. Stored
1643
+ Pane CWD drives only that Pane's detached runtime cwd, while Project root
1644
+ remains the session path anchor and `PROJMUX_CWD` hook value.
1645
+ `Pane.spec.command`, snapshot recipes, notifications, and ephemeral sessions
1646
+ are never execution inputs.
1647
+ - An Agent whose managed Pane is not live is replayed into a new managed Pane on
1648
+ its Window's proven anchor, through the same allocation, activation ledger,
1649
+ ownership-checked adoption, and rollback the shell half uses. The **only**
1650
+ replay identifier is Registry `status.sessionRef`: no provider conversation
1651
+ store is read, `ClaudeSessionRef.TranscriptPath` in particular is never
1652
+ consulted, and snapshot recipe `resumeID` is a separate value that never feeds
1653
+ this path. The launch argv comes from the two seams `create agent` already
1654
+ owns -- `PlanAgentResume` for a ref that names a conversation, `PlanAgentLaunch`
1655
+ with no payload otherwise -- so the topology engine holds no launch builder of
1656
+ its own and the Settings enabled-agents gate still applies. An Agent that
1657
+ cannot rejoin its conversation comes back on a *new* one and the reason is
1658
+ disclosed; an Agent that cannot be launched at all is disclosed and skipped.
1659
+ Neither aborts the materialization, and neither is ever silent. A stale managed
1660
+ Pane row is released only after the server-wide uid preflight proves its uid is
1661
+ live nowhere on the exact socket.
1662
+ - An offline Agent-only Window plans a visible `allocate default shell` Registry
1663
+ item, authors that direct shell under the same convergent transaction, creates
1664
+ the Window from it, and then replays the anchor Agent while preserving the
1665
+ Agent Pane uid. The default shell is bootstrap, not a replacement anchor. A
1666
+ successful repeat is a Registry-write-free and topology-write-free no-op.
1667
+ - Snapshot restore is a target-Project subtree projection, never a Registry
1668
+ restore. Metadata-bearing v1 snapshots preserve surviving final-v2
1669
+ anchor/default refs; metadata-free snapshots choose the first Window-local
1670
+ Pane as anchor and the first direct shell as optional default. Agent-only
1671
+ desired Windows remain Agent-anchored and acquire a shell only through the
1672
+ ordinary materializer. Source snapshot bytes and unrelated roots are never
1673
+ rewritten, and a second projection is byte-stable.
1674
+ - `Open fresh` replaces the exact same-root Project graph in one Registry
1675
+ commit. It always allocates a new Project UID plus one new canonical Window
1676
+ and direct shell UID, whether the old Project retained Windows or had zero.
1677
+ The preimage remains the durable recovery state when the replacement commit
1678
+ fails, and successful validation requires exactly one same-root claimant.
1679
+ - Exact-socket reconciliation merges scoped results by UID at their existing
1680
+ global Registry positions. Positive mirrored evidence may change only the
1681
+ selected socket's owned rows; sibling sockets and other-host-only desired
1682
+ refs/status retain both values and byte order. Absence on the selected host
1683
+ is never re-anchor, status-clear, or delete authority.
650
1684
  - Preflight rejects a missing/invalid root or Pane CWD, a zero-Window Project,
651
- a primary ref that is not a direct Window-owned shell Pane, and foreign,
1685
+ an anchor ref that is neither an exact same-Window shell nor the owning
1686
+ Agent's current managed Pane, a live Window whose exact anchor is dead, and foreign,
652
1687
  duplicate, wrong-owner, or ambiguous live claims before the first create.
653
1688
  Execute rechecks the same plan under the Registry lock. A server-wide uid
654
1689
  preflight runs first, *before* the selected Project session is created,
@@ -678,6 +1713,55 @@ Explicit Registry topology materialization:
678
1713
  Only the selected exact socket is claimed and mutated; sibling sockets are
679
1714
  tested unchanged, and no global uniqueness across unknown sockets is claimed.
680
1715
 
1716
+ Plan-only runtime mutation boundary:
1717
+
1718
+ - Lifecycle and topology changes owned by the app materializer and Pane-delete
1719
+ runtime are values before they are commands. The closed action inventory
1720
+ records a stable target, a typed guard with the exact expected evidence, a
1721
+ total order, expected effect, and typed executable operands; its JSON
1722
+ projection is deterministic. The argv seam rejects an operand target that
1723
+ does not match the printable stable target.
1724
+ Session/Window/Pane creation, identity and create-operation lease writes,
1725
+ layout writes, ownership-checked rollback, exact Pane kill, pre-commit
1726
+ tombstone/restore, and post-result-flush self-kill queueing all enter the same
1727
+ plan -> printable target/route guard -> effect reobserve/replan -> semantic
1728
+ guard -> execute -> effect reobserve/replan boundary. Before an already
1729
+ satisfied row may disappear, the executor binds its printed logical/physical
1730
+ socket and server-generation authority to the captured route; semantic
1731
+ pre-write guards still run together before the first live write.
1732
+ - Materialization is a sequence of dynamically replanned stages because exact
1733
+ Window and Pane handles do not exist until the preceding create effect is
1734
+ reobserved. Each stage is nevertheless a complete printable plan with a
1735
+ total order; the next stage is built only from the preceding stage's observed
1736
+ exact effect. Every production row carries `-L=<name>` or the exact
1737
+ `-S=<absolute path>`, the independently observed physical socket, and a
1738
+ printable route receipt. App-owned receipts pin `#{pid}` plus ownership and
1739
+ logical markers; inherited standalone receipts pin the exact server pid and
1740
+ originating `$N`/`@N`/`%N` containment while requiring both app markers
1741
+ blank. The public controller's explicit `--socket-path` grant is narrower:
1742
+ it prints the operator-selected path/PID blank-marker class and relies on
1743
+ each planned action's real UID plus session/window guards; it never infers an
1744
+ arbitrary Pane as invocation evidence. Generated popup/menu producers pass an exact Pane anchor which is
1745
+ reobserved on that same socket rather than trusting a targetless current
1746
+ Pane. Before a stage writes, the same `-S` runner refuses path, generation,
1747
+ class, or containment drift. Only a create-session
1748
+ stage may accept the typed no-server observation, because its explicit route
1749
+ and absent-session ownership preflight are the facts required to create the
1750
+ first server.
1751
+ - A guard refusal writes nothing and asks the caller to observe and plan again.
1752
+ Reobservation is explicit: known achieved effects remove their rows, so a
1753
+ successful repeat is an empty plan; an unavailable observation is unknown and
1754
+ can neither synthesize a Registry deletion nor authorize a runtime kill.
1755
+ Partial execution rolls back only actions carrying an ownership-backed undo,
1756
+ in reverse application order. Existing desired Registry state, foreign or
1757
+ sibling objects, and other sockets have no rollback authority.
1758
+ - Pane deletion keeps the exact routed socket plus Session, Window, Pane, root
1759
+ kind/root uid, and current Pane mirror in every executable guard. A
1760
+ caller-containing delete still commits the Registry and flushes the complete
1761
+ result before its self-target kill is queued. The AI picker/default/resume and
1762
+ shell split producers remain canonical create-intent producers; they do not
1763
+ gain a second tmux mutation path.
1764
+
681
1765
  Agent runtime linkage:
682
1766
 
683
1767
  - Once a live tmux pane has settled on a registry Pane, reconcile decides which
@@ -693,6 +1777,14 @@ Agent runtime linkage:
693
1777
  Agent — Phase 1's refuse rule, unchanged. The legacy import path already
694
1778
  trusted exactly this option to mint an Agent on its create path; linkage makes
695
1779
  the adopt and rebind paths agree with it.
1780
+ - **The canonical default shell remains Registry-owned.** A generic
1781
+ `@projmux_ai_agent` marker on the direct Window-owned `role=shell` Pane named
1782
+ by `Window.spec.defaultShellPaneRef` is reported as reason-bearing D2 and
1783
+ performs no Agent mint, Pane reparent, or reservation move. Runtime metadata
1784
+ cannot invalidate the Registry's canonical shell chain. This exception is
1785
+ deliberately exact: an anchor-only shell that is not the default shell keeps
1786
+ the existing linkage behavior, whose promotion semantics belong to the
1787
+ separate anchor/primary-shell track.
696
1788
  - **Which Agent, in order.** (1) The Pane is already Agent-owned: that Agent is
697
1789
  the answer and only `status.paneRef` is repaired. (2) An Agent in the same
698
1790
  Window already records the same provider conversation in `status.sessionRef`
@@ -741,6 +1833,48 @@ Agent runtime linkage:
741
1833
  maintenance riding along inside somebody else's transaction: one pane it cannot
742
1834
  register must not fail the `create` that happened to trigger it.
743
1835
 
1836
+ Resource-first create:
1837
+
1838
+ - **One parser, one product model.** `create window|pane|agent|<provider>` share
1839
+ a single argv surface and a single resource-backed implementation.
1840
+ `--project` is a scope flag, never a mode selector, so no flag chooses between
1841
+ two meanings of the same command. The runtime-only "split the current window"
1842
+ half that used to sit behind an absent `--project` is removed; a raw,
1843
+ unmanaged split is tmux's own verb, not a projmux resource verb.
1844
+ - **Scope resolution has exactly two branches.** An explicit `--project`/`-p`
1845
+ wins inside and outside tmux and suppresses the active-target read entirely.
1846
+ With no `--project`, the Project is derived from the active exact runtime
1847
+ through the same `@projmux_window_uid` mirror and registry `ownerRef` chain
1848
+ the read verbs use.
1849
+ - **Window and anchor follow the whole scope, not the Project flag.** They are
1850
+ derived only when the argv named no `--project`, `--window`, `--pane`, and no
1851
+ `--selector` at all. That keeps a bare `create pane --placement right` -- the
1852
+ generated keybinding body -- a split of the Window the operator is looking at,
1853
+ instead of a fan-out over every Window of the Project, while one explicit
1854
+ occurrence still fixes the whole target set. An explicit `--pane` or popup
1855
+ origin is the exact split anchor. Only a scope with no Pane consumes the
1856
+ target Window's role-agnostic `spec.anchorPaneRef`; a missing, stale, dead, or
1857
+ cross-Window ref refuses with no alternate-live-Pane inference.
1858
+ - **Refusals cost nothing.** Home, control, unattributed, foreign, a mirrored
1859
+ uid the Registry does not hold, a Window whose Project is gone, and every
1860
+ outside-tmux invocation with no `--project` are usage errors naming
1861
+ `--project`. They are raised before the registry transaction opens, so they
1862
+ are measurably zero Registry writes and zero tmux calls. Nothing falls back to
1863
+ a runtime-only split, nothing invents a Project from `$HOME`, a session name,
1864
+ or a cwd, and no default server is probed.
1865
+ - **Host neutrality is transport-level, not policy-level.** Inside an app-owned
1866
+ or a standalone server the create mutates only the inherited exact socket,
1867
+ because every tmux call it issues inherits `$TMUX` and it never enumerates
1868
+ siblings. Outside tmux an explicit Project is the gate before anything live is
1869
+ touched.
1870
+ - **Everything is detached.** No create path issues `switch-client`,
1871
+ `select-window`, `select-pane`, or `attach-session`. `focus pane` and
1872
+ `-o pane-id` are how a caller ends up in the new pane.
1873
+ - **Focus is navigation-only.** `focus project|window|pane` reads live tmux
1874
+ inventory and may move an existing client, but has no Registry store and
1875
+ issues no session/Window/Pane creation, identity-marker, rename, respawn, or
1876
+ deletion write. An offline target remains offline and exits unresolved.
1877
+
744
1878
  Selector and the implicit active target:
745
1879
 
746
1880
  - A selector value is either `uid:<uid>` or a `metadata.name`. There is no
@@ -752,26 +1886,49 @@ Selector and the implicit active target:
752
1886
  - Inside tmux, an invocation of a **singular read or rename verb** that carries
753
1887
  no selector at all resolves the **active tmux target**: `get pane`,
754
1888
  `describe project|window|pane|agent`, `rename project|window|pane`, and
755
- `rebind project`. Any reference, scope flag, or label keeps the pre-existing
756
- singular-target meaning; `create` and the destructive routes are unaffected.
757
- - Project is also the namespace-like default scope of the plural registry reads
758
- `get windows|panes|agents`. When `--project` is absent inside tmux, the active
759
- Window uid mirror and its registry owner chain derive a Project on every
760
- invocation. This narrows the Window universe only; it never chooses one
761
- Window, Pane, or Agent for the operator. `get projects` is above that scope,
762
- while notifications and snapshots belong to separate stores, so all three
763
- remain global.
1889
+ `rebind project`. Any reference, scope flag, or label keeps picking the target
1890
+ itself; the destructive routes are unaffected. `create` reads the same seam
1891
+ under its own rule, described below.
1892
+ - The plural registry reads `get windows|panes|agents` use the active Window's
1893
+ exact Registry owner as their default managed root. A Project-owned Window
1894
+ exposes only that Project's descendants; a ControlSession-owned Window (the
1895
+ Home control surface) exposes only that ControlSession's descendants. Name
1896
+ and label selectors continue to filter inside that default root. An explicit
1897
+ `uid:` selector is already opaque Registry-global authority, so it bypasses
1898
+ active-root observation and narrowing, including from a foreign tmux Pane.
1899
+ An in-tmux Window with no exact existing Project or ControlSession owner is a
1900
+ usage refusal with zero stdout only when no explicit Project, whole-set, or
1901
+ uid authority bypasses the default; it never silently falls back.
1902
+ - An **explicit singular reference** on `describe window|pane|agent` remains
1903
+ Project-namespaced. Generic `rename window|pane|agent` instead derives the
1904
+ exact Project or ControlSession that owns the active Window. When
1905
+ `--project` is absent inside a managed Project, both families derive the
1906
+ Project from the active Window uid mirror and Registry owner chain. This
1907
+ narrows the Window universe only; it never
1908
+ chooses one Window, Pane, or Agent for the operator, so a same-named pair
1909
+ inside the one Project stays the ordinary bounded exact-one ambiguity and a
1910
+ `uid:` reference outside the scope is a no-match rather than a cross-Project
1911
+ hit. The describe family remains the intentional Project-only difference;
1912
+ Phase 14 extends only the generic rename family to ControlSession.
1913
+ `get projects`, `describe|rename project`, `delete`, `rebind`, and `agent
1914
+ resume` are outside that reference scope, and notifications and snapshots
1915
+ belong to separate stores. Delete's exact live preflight nevertheless follows
1916
+ either root kind through the selected descendant's owner chain.
764
1917
  - `--all-projects` is the explicit registry-wide escape for those three reads.
765
1918
  It is deliberately different from destructive `delete --all`, whose existing
766
1919
  whole-registry compatibility meaning is unchanged. A bare `--all` is not a
767
1920
  read flag. Explicit `--project` keeps its prior result and cannot be combined
768
1921
  with `--all-projects`.
769
- - Outside tmux, an omitted Project scope keeps the historical whole-registry
770
- inventory. Inside tmux, a missing Window binding or broken Project owner chain
771
- is a usage refusal with zero stdout, never a silent global fallback. The
772
- selector engine's `windowScope` is the single choice point for explicit
773
- Project, active-derived default, or global scope, shared by Window, Pane, and
774
- Agent resolution.
1922
+ - Outside tmux, an omitted root scope keeps the historical whole-registry
1923
+ inventory and its ambiguity, for a plural read and for a reference alike.
1924
+ Inside tmux, a missing Window binding or broken managed-root owner chain is a
1925
+ usage refusal with zero stdout, never a silent global fallback. The selector
1926
+ engine's `windowScope` is the single choice point for explicit Project,
1927
+ active-derived Project/ControlSession root, or global scope, shared by Window,
1928
+ Pane, and Agent resolution. The plural default and the Project-only singular
1929
+ namespace both fill `Query.DefaultRoot`; only the former can carry a
1930
+ ControlSession kind. The default is consulted only after ruling out explicit
1931
+ `--project`, `--all-projects`/`-A`, and any applicable `uid:` occurrence.
775
1932
  - There is **no sentinel value token**. `current` and `active` pass
776
1933
  `ValidateName`, so `--pane current` would shadow a resource that legitimately
777
1934
  carries that name. Omission is the only spelling. If an explicit one is ever
@@ -784,8 +1941,9 @@ Selector and the implicit active target:
784
1941
  select a wrong target.
785
1942
  - Only two options are read: `@projmux_pane_uid` on the active pane and
786
1943
  `@projmux_window_uid` on its window (window-scoped options resolve through a
787
- pane target). Every ancestor above them comes from `ownerRef` — the Project is
788
- the owner of the active Window, the Agent is the owner of the active Pane.
1944
+ pane target). Every ancestor above them comes from `ownerRef` — the Project or
1945
+ ControlSession is the owner of the active Window, and the Agent is the owner
1946
+ of the active Pane.
789
1947
  The session-scoped `@projmux_project_uid` is **not** consulted: it is
790
1948
  measurably empty on live sessions, so trusting it would refuse targets the
791
1949
  owner chain resolves.
@@ -800,7 +1958,10 @@ Selector and the implicit active target:
800
1958
  a message naming what was inspected. It is deliberately not the
801
1959
  `matched N ..., want exactly one` cardinality error, because an unmanaged pane
802
1960
  carrying no `@projmux_pane_uid` is the common case and presenting it as
803
- ambiguity would hide the cause.
1961
+ ambiguity would hide the cause. An undecidable *namespace* refuses with its
1962
+ own message rather than that one, because "no selector was given" is false on
1963
+ an invocation that carried a reference and would send the operator after the
1964
+ wrong cause.
804
1965
 
805
1966
  ## Naming metadata model
806
1967
 
@@ -908,6 +2069,13 @@ network call.
908
2069
  - **Failure preservation** — adapter failures do not erase prior
909
2070
  rows. The Manager merges new snapshots over the on-disk slice, so a
910
2071
  transient 429 keeps the last known good numbers visible.
2072
+ - **Codex native source selection** — the Codex adapter alone owns one
2073
+ invocation's source decision. It normalizes native
2074
+ `account/rateLimits/read` plus bounded sparse update events into snapshots;
2075
+ only unavailable/unsupported/account-empty outcomes invoke the newest
2076
+ rollout parser once. Native and rollout rows are never synthesized together.
2077
+ Optional snapshot provenance preserves source, fallback/stale reason, and
2078
+ native bucket label/cadence through Store and all public read surfaces.
911
2079
 
912
2080
  See [usage-tracking.md](usage-tracking.md) for adapter detail (token
913
2081
  refresh, rollout schema).
@@ -942,6 +2110,39 @@ cache.
942
2110
 
943
2111
  ## Related design and inventory notes
944
2112
 
2113
+ ### Plan-only managed runtime mutation
2114
+
2115
+ Managed lifecycle/topology changes are printable `runtimeMutationPlan` rows.
2116
+ Each row carries an exact invocation route, immutable observed socket path,
2117
+ printable server-generation authority, a stable tmux handle and Registry
2118
+ UID/owner chain, a closed guard, total order,
2119
+ expected effect, and printable typed operands bound to that handle. Execution
2120
+ validates printable target/route authority before pre-effect reobservation and
2121
+ every pending semantic guard before the first write;
2122
+ owned rollback runs in reverse order. Materialization is intentionally staged:
2123
+ after each dynamic handle is returned, it is reobserved and the next stage is
2124
+ planned, so no later action guesses a Window or Pane handle. A successful
2125
+ reobserve/replan is empty; an unknown observation authorizes no delete or kill.
2126
+ App-owned execution requires exact path/pid/app/logical evidence. An inherited
2127
+ standalone route is separately closed by exact `TMUX=path,pid,index` plus a
2128
+ producer-verified Pane receipt and prints/executes through `-S`; partial app
2129
+ markers never downgrade to standalone. Explicit controller reconciliation may
2130
+ instead use an operator-selected `--socket-path` plus PID/blank-marker receipt,
2131
+ but only action-specific UID and containment guards authorize its writes.
2132
+ Fresh app bootstrap is the only
2133
+ pre-server declaration without a generation receipt, and binds path/pid/$@%
2134
+ before its route marker and all later rows.
2135
+
2136
+ The maintained product table in `internal/app/runtime_mutation_surface.go` maps
2137
+ generated catalog/menu producers, native provider/resume picker selections,
2138
+ sidebar/session-picker stops, and app lifecycle entrypoints in both directions
2139
+ to their handler and plan verb. It also records exact semantic exemptions for
2140
+ focus, labels, operator-requested layout, mouse forwarding, snapshot replay,
2141
+ ephemeral maintenance, app quit, and human runtime maintenance. Managed argv
2142
+ verbs are selected only by the typed executor seam; generated Window
2143
+ create/rename, Pane-menu create/delete, and automatic post-split layout writes
2144
+ reach typed intent/operand routes rather than embedding tmux lifecycle commands.
2145
+
945
2146
  Contributor-facing companions to this document. They are design records and
946
2147
  inventories rather than user documentation, so they are linked from here rather
947
2148
  than from the README docs index.