projmux 0.10.0 → 0.11.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.
@@ -31,7 +31,6 @@ Adapters for external systems.
31
31
 
32
32
  Initial adapters:
33
33
  - tmux
34
- - kubeconfig per-session state
35
34
  - filesystem
36
35
  - git metadata for preview enrichment
37
36
 
@@ -76,7 +75,6 @@ Candidate areas:
76
75
  - default home-like roots
77
76
  - preview preferences
78
77
  - session naming exceptions
79
- - kube session settings
80
78
  - ephemeral session retention defaults
81
79
 
82
80
  ## State model
@@ -103,6 +101,654 @@ Resource snapshots are not Session State and are never saved or restored. See
103
101
  [resource-attribution.md](resource-attribution.md) for metric, partial-state,
104
102
  host-remainder, privacy, and measurement contracts.
105
103
 
104
+ ## Resource metadata model
105
+
106
+ `projmux` owns a persistent resource model that is independent of tmux
107
+ lifecycle. It is the storage, ownership, and name-allocation foundation for the
108
+ CLI information architecture v2 resource routes.
109
+
110
+ Packages:
111
+
112
+ - `internal/core/metadata` is pure: the resource model, validation, name
113
+ allocation, schema migration, snapshot reconciliation, and the operation
114
+ transaction. It performs no I/O; the clock, uid source, and root-directory
115
+ probe are injected through `Mutator`.
116
+ - `internal/integrations/metadata` owns the registry file (lock, atomic write,
117
+ migration) and the tmux transport mirror.
118
+ - `internal/integrations/tmuxopts` is a dependency-free leaf holding the
119
+ canonical spelling of every projmux-owned tmux option name, so the generated
120
+ tmux config, session-state replay, and the resource mirror cannot drift.
121
+
122
+ Resources and ownership:
123
+
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.
128
+ - A persistent tmux **Session is not a resource**. It is a 1:1 runtime
129
+ projection of a Project recorded in `Project.status.session` with a `live`
130
+ flag, and it owns no uid, name, or ownerRef. Auto-attach ephemeral sessions
131
+ live only in runtime inventory, outside the Project hierarchy.
132
+ - `Window` and `Pane` carry **no stored liveness field**, deliberately. Their
133
+ `status` block holds observed conditions only; live/offline is derived from a
134
+ live tmux observation at read time. See *Runtime observation and resource
135
+ 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.
140
+
141
+ Identity and naming:
142
+
143
+ - `metadata.uid` is opaque, immutable, and independent of tmux lifecycle. It
144
+ survives snapshot/restore, runtime creation, and root rebind.
145
+ - `metadata.name` is the stable unique-within-scope query key. Project names
146
+ are unique across the registry; Window, Pane, and Agent names are unique
147
+ within their `ownerRef` scope.
148
+ - `metadata.displayName` may duplicate and is never a selector, an ownerRef, or
149
+ identity. `metadata.labels` is key/value classification;
150
+ `metadata.annotations` is non-identifying metadata such as an AI topic.
151
+ - Name bases are assigned once, at create or migration time, and are never
152
+ re-derived. A declared Window create uses explicit name → initial command
153
+ basename → configured shell basename → `window`; a Window newly imported
154
+ from tmux always uses the literal `window` allocator base. Its observed
155
+ `window_name`, Pane label, provider, command, shell, topic, and title are all
156
+ excluded from stable identity. Shell Pane uses command basename → shell
157
+ basename → `pane`; a Pane managed by an Agent uses `<agent-name>-pane`; Agent
158
+ uses explicit `--name` → normalized provider id → `agent`.
159
+ - For a Window, the observed tmux `window_name` is
160
+ `metadata.displayName`: duplicate-allowed, visible in `describe window`, and
161
+ never a selector, ownerRef, or identity input. Existing Window uids, names,
162
+ owners, and name reservations are preserved; there is no bulk naming
163
+ migration.
164
+ - Automatic collisions take the lowest free suffix (`Projmux-1`, `codex-1`)
165
+ from the persisted `nameReservations` table, scanning integer suffixes rather
166
+ than resource or map iteration order. An **explicit** `--name` or rename
167
+ collision never receives an implicit suffix: it fails with exit code 2 and
168
+ zero mutations.
169
+
170
+ Root lifecycle:
171
+
172
+ - `spec.root` is an absolute path. Rebind changes only `spec.root`, atomically,
173
+ never moves files, and never changes the uid. Rebinding onto a root already
174
+ bound to another Project fails with exit code 2 and zero mutations.
175
+ - uids are never merged heuristically. Basename, git origin, inode, and scan
176
+ order are not consulted; only an exact saved root that reappears reuses its
177
+ uid.
178
+ - A disappeared root records a `MissingRoot` condition with its first-observed
179
+ timestamp and preserves both the metadata and the name reservations. A
180
+ returning root recovers the same uid and clears the condition.
181
+
182
+ Agent lifecycle:
183
+
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.
188
+
189
+ Agent provider session ref:
190
+
191
+ - `status.sessionRef` is the durable pointer from an Agent to the provider
192
+ conversation it belongs to. It is in **status, not spec**, because nothing
193
+ declares it: a provider hook reports it after the fact, exactly like
194
+ `status.paneRef`.
195
+ - The two status refs have deliberately different lifetimes. `status.paneRef` is
196
+ the *current* managed-Pane binding and is cleared by `ReleaseAgentPane`,
197
+ `DeletePane`, and every non-`Running` transition. `status.sessionRef` is
198
+ cleared by none of them: an `Offline` Agent that has lost its Pane still knows
199
+ which conversation it is.
200
+ - It is **not** a duplicate of the tmux pane option `@projmux_ai_session_id`,
201
+ and that option is not going away. The pane option is the *live routing
202
+ index*: hook ingest scans the live pane list and matches on it to decide which
203
+ pane an incoming event belongs to, so following pane lifetime is correct for
204
+ it. `status.sessionRef` answers "which conversation is this Agent" and must
205
+ outlive the Pane. Ingest writes both.
206
+ - The shape is a **per-provider discriminated union**, not one flat string:
207
+ `provider` is the discriminator and exactly one of `claude`, `codex`, or
208
+ `antigravity` is populated. Providers disagree on what identifies a
209
+ conversation — Claude reports a session id plus a transcript path, Codex a
210
+ thread id and a session id, Antigravity a single conversation id — and
211
+ flattening them would assert a false equivalence between a Codex thread id and
212
+ a Claude session id.
213
+ - Codex's turn id is deliberately **not** stored. A turn addresses one turn
214
+ inside the conversation and changes on every hook event, so it is not a
215
+ pointer to the conversation.
216
+ - Transcript **paths** are recorded as the hook reported them. Nothing reads
217
+ provider config files or transcript **contents**; that is permanently out of
218
+ scope.
219
+ - The field is additive inside `schemaVersion: 1`. It is an optional pointer
220
+ with `omitempty`, so a registry written before it existed decodes with a nil
221
+ ref, validates, and re-encodes byte-identically. Bumping the envelope would
222
+ make every already-installed build refuse the file fail-closed with
223
+ `ErrSchemaTooNew`, which is a hard downgrade break bought for nothing.
224
+ - **"One conversation ↔ at most one live Agent" is deliberately NOT enforced.**
225
+ Two Agents may carry the same conversation, and `get agents` / `describe agent`
226
+ will show both. Enforcing it would mean a best-effort hook observation could be
227
+ *refused*, making the registry describe a world that does not exist: the same
228
+ conversation really can be attached twice (a manual resume of the same session
229
+ id in a second pane already does that), and an `Offline` Agent keeps its ref
230
+ forever, so a later Agent observing the same conversation would be permanently
231
+ unable to record it. Choosing between several Agents that point at one
232
+ conversation is a resume-time decision and belongs to the resume
233
+ materialization Phase, not to an observation write.
234
+ - The write is narrow and idempotent: it touches `status.sessionRef` and nothing
235
+ else — not the phase, not `lastTransitionAt`, not `paneRef` — and
236
+ re-observing the same conversation opens no registry transaction at all. A
237
+ hook whose provider contradicts the Agent's `spec.provider` is refused with
238
+ zero mutations.
239
+
240
+ Agent resume:
241
+
242
+ - `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
245
+ `metadata.uid` and `metadata.name` do not change, `status.phase` becomes
246
+ `Running`, and `status.paneRef` points at the new Pane. `status.sessionRef`
247
+ itself is read and never rewritten by resume.
248
+ - **A resume that cannot happen fails; it never becomes a create.** `create agent`
249
+ always mints a new uid and `agent resume` always reuses one, and the two are
250
+ not one code path: the resume route holds a launch seam whose only argv builder
251
+ takes a conversation id, so there is no fresh-start argv it can produce. Every
252
+ refusal below is decided against a read-only registry snapshot, so it opens
253
+ zero registry transactions and issues zero `split-window` calls.
254
+ - The refusals are: a `Running` Agent (usage error naming `focus pane`); any
255
+ phase other than `Offline`/`Failed`; **an Agent with no `sessionRef` at all**,
256
+ which is the normal state of an Agent whose provider hook never ran and which
257
+ names `create agent` rather than performing it; a ref whose conversation id the
258
+ provider's own resume builder rejects; a ref contradicting a declared
259
+ `spec.provider`; a provider disabled in Settings; a provider binary that is not
260
+ installed; a `MissingRoot` Project; and a Window with no resolvable anchor Pane.
261
+ - **Several Agents may point at one conversation, and the conversation is never a
262
+ selector.** Resume rebinds exactly the Agent the reference resolves to and
263
+ never searches the registry by conversation id to choose a different one, so
264
+ duplicates neither redirect nor block a rebind — refusing them would make the
265
+ state the observation write deliberately allows permanently unusable. The
266
+ duplicates are disclosed on stderr in uid order, which makes the disclosure
267
+ byte-identical regardless of registry order.
268
+ - **`observedAt` is not a resume gate.** It records when projmux last *saw* the
269
+ conversation, not a provider timestamp, so it cannot answer "is this ref
270
+ stale": a conversation untouched for a month is perfectly resumable and one
271
+ observed a minute ago may already be deleted. The only authority is the
272
+ provider, and reading its store is permanently out of scope, so projmux checks
273
+ what it can see and hands the rest to the provider's resume argv.
274
+ - Resume is **conversation-granularity for every provider**. Codex's turn id is
275
+ not stored and `codex resume <thread-id>` has no turn slot, so turn-level
276
+ resume is not something this surface can express today.
277
+
278
+ Registry file and schema:
279
+
280
+ - 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.
286
+ - Everything else fails closed: the file is refused as unreadable and **no
287
+ write happens at all** — no rewrite, no backup, no staged temp file. This
288
+ covers a **newer** schemaVersion (which would destroy state a newer build
289
+ owns), malformed JSON, and a document that parses but carries **no**
290
+ `schemaVersion`. An absent field decodes as version `0`, which means unknown
291
+ rather than pre-release: migrating it would rewrite a corrupt or foreign file
292
+ at the registry path, which is exactly the write-on-unknown-input that
293
+ fail-closed exists to prevent. The registry is deliberately not quarantined
294
+ 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.
306
+ - **Field spelling:** the registry file intentionally uses the resource-model
307
+ camelCase spelling (`apiVersion`, `schemaVersion`, `metadata`, `displayName`,
308
+ `ownerRef`, `primaryPaneRef`, `spec`, `status`) rather than the snake_case
309
+ used by the older projmux on-disk JSON. The two spellings coexist on purpose:
310
+ existing snake_case files are **not** retro-changed, and the resource registry
311
+ follows the resource-model contract.
312
+
313
+ Session State interoperability:
314
+
315
+ - Session snapshots carry resource identity through additive `omitempty`
316
+ `metadata` blocks at the unchanged snapshot `version: 1` — one for the owning
317
+ Project at the top level, one per Window, and one per Pane, each with
318
+ `uid`, `name`, `labels`, `owner_kind`, and `owner_uid` in the snapshot's own
319
+ snake_case spelling. No schema bump was needed, and a snapshot written
320
+ 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.
325
+
326
+ tmux transport mirror:
327
+
328
+ - Live resources mirror identity into tmux options: `@projmux_project_uid` and
329
+ `@projmux_project_name` on the session, the new window-scoped
330
+ `@projmux_window_uid` and `@projmux_window_name`, and pane-scoped
331
+ `@projmux_pane_uid` plus the existing `@projmux_pane_label` as the Pane
332
+ **name** mirror. These are the first window-scoped projmux options; every
333
+ earlier one was pane-, session-, or global-scoped.
334
+ - `rename pane` changes `Pane.metadata.name` and its `@projmux_pane_label`
335
+ mirror only. It never writes the raw tmux `pane_title`.
336
+ - `rename window` is the explicit stable-identity path: it changes only
337
+ `Window.metadata.name`, its scoped name reservation, and the exact live
338
+ `@projmux_window_name` transport mirror. It does not
339
+ change `metadata.displayName` or tmux `window_name`.
340
+ - `rename project` likewise writes only `Project.metadata.name` and the exact
341
+ live session's `@projmux_project_name`; it never renames the tmux session.
342
+ `rebind project` preserves the Project uid and session name while updating
343
+ `spec.root` and the exact live session's `@projmux_project_path`. Neither
344
+ operation moves files.
345
+ - `rename agent` changes only the Window-scoped Agent `metadata.name` and its
346
+ reservation. Agent topic annotations, provider, lifecycle status, and the
347
+ managed Pane's name and raw title are independent and receive no tmux write.
348
+ - Rename/rebind commits the authoritative Registry transaction before its
349
+ field-specific live projection. Immediate projection is enabled only inside
350
+ tmux from an inherited absolute socket path; every inventory and write is
351
+ routed through that exact `-S` socket, and Project/Window writes target stable
352
+ `$N`/`@N` handles rather than mutable names or indices. Outside tmux no server
353
+ is probed and the operation is Registry-only. If no exact UID target is
354
+ observed, including when the exact inventory is unavailable, the resource is
355
+ treated as offline and the durable drift is left for a later explicit-socket
356
+ reconciliation. Once an exact target is found, a write failure or duplicate
357
+ UID claim is nonzero and reports that Registry state committed plus
358
+ `projmux reconcile resources` as the retry boundary.
359
+ - The configured `window.rename` action (`Ctrl-M` by default) is the runtime
360
+ display path and invokes tmux `rename-window` directly. Reconciliation
361
+ observes that `window_name` back into `metadata.displayName` without changing
362
+ stable identity.
363
+ - Registry-managed Windows are set to `automatic-rename off` so a focused-Pane
364
+ change cannot overwrite the Window name. The **global** `automatic-rename on`
365
+ plus visible-pane-label `automatic-rename-format` default in the generated
366
+ app config is unchanged, so unmanaged windows keep their existing behavior.
367
+ - Legacy import gives every newly discovered Window the literal `window` base,
368
+ uniquified in Project scope (`window`, `window-1`, ...), and projects its
369
+ current `window_name` into duplicate-allowed `metadata.displayName` before
370
+ switching it to `automatic-rename off`. Re-observing an existing Window may
371
+ refresh only that display field; it never changes the uid, stable name,
372
+ ownerRef, or name reservation. An existing `@projmux_pane_label` remains the
373
+ migration seed and transport mirror for the Pane **name**; Pane naming is a
374
+ separate contract and is unchanged here.
375
+ - `Pane.metadata.name` is the primary pane display source. The derived
376
+ `Pane.status.displayTitle` (Agent topic → known shell → raw pane title) is
377
+ secondary and is never a selector, an identity, or a Window name source.
378
+
379
+ Runtime observation and resource status:
380
+
381
+ - **Status is an observation; spec is stored and authoritative.** A read verb
382
+ never trusts a stored liveness value. `Window` and `Pane` status is derived
383
+ per invocation from a live tmux snapshot: a resource is `live` only while a
384
+ live tmux object still mirrors its `@projmux_window_uid` /
385
+ `@projmux_pane_uid`. A registry object bound to nothing live is an **orphan**,
386
+ and an orphan is not live.
387
+ - `selector.ObservedStatus(missingRoot, bound)` is the **single** derivation
388
+ rule in the codebase, and every kind goes through it. `missing-root` outranks
389
+ everything, then `bound` decides `live` vs `offline`. The MissingRoot
390
+ precedence contract is unchanged, and it now applies to a Window or Pane whose
391
+ owning Project lost its root even while tmux is still running them.
392
+ - A **Project** is the one kind whose runtime object is a tmux *session*, which
393
+ has no `@projmux` uid of its own, so Project status still reads
394
+ `status.session` as refreshed by the reconciler.
395
+ - An **Agent** owns no tmux object of its own — there is no `@projmux_agent_uid`
396
+ and there must not be one, because an Agent outlives the managed Pane it is
397
+ bound to. Its runtime object is **that managed Pane**, named by
398
+ `status.paneRef`, and that is what is observed: an Agent is `live` only while
399
+ a live tmux pane still mirrors the uid its `paneRef` points at. An empty
400
+ `paneRef` — the state of every released, pending, or failed Agent — is
401
+ `offline`, and `missing-root` still outranks both.
402
+ - Agent status is **not** inherited from the owning Window. It used to be, and
403
+ that was the last surviving inheritance path: once one Window was adopted and
404
+ went live, every Agent under it read `live` whether or not it had a pane, so
405
+ `get agents` said `live` for a resource `describe agent` reported as `Offline`
406
+ with no managed pane. The Window now contributes exactly one fact — whether
407
+ the owning Project carries `MissingRoot` — and specifically not its liveness.
408
+ - `status.phase` is **not** an input to status either. Phase is lifecycle (a
409
+ stored value, owned by the Agent liveness rules) and Status is observation;
410
+ folding a stored value back into the observation is what the contract forbids.
411
+ They cannot contradict anyway: every non-`Running` transition clears
412
+ `paneRef`, so a non-`Running` Agent has no runtime object to observe.
413
+ - The observation is taken **at the command entrypoint, once per process
414
+ invocation**, and costs exactly two reads: `list-panes -a` and
415
+ `list-windows -a` (~3ms each). It is lazy, so a route that never renders
416
+ status never pays for it. It is **not** cached and **not** persisted: closing
417
+ a pane must make the *next* query report it offline, and a TTL would defeat
418
+ exactly that. It is also not a per-route reconcile, because the read verbs
419
+ load the registry read-only and must never materialize
420
+ `<state>/projmux/metadata/`.
421
+ - A failed inventory query yields an **empty** observation, never a fallback to
422
+ a stored value. Empty can only downgrade a resource to offline; it can never
423
+ invent a live one, and "nothing is live" is the truthful answer for a machine
424
+ whose tmux server is not up.
425
+ - The reconciler runs the same diff on the mutation routes and records **why** a
426
+ runtime object went away as a `MissingRuntime` condition
427
+ (`reason: RuntimeUnbound`) on the Window or Pane, with `firstObservedAt`
428
+ preserved across repeat observations and cleared when the object rebinds.
429
+ `describe` renders it. This is an inventory diff, not an event handler, so it
430
+ **converges with no hook firing**: the pane-exit hooks only accelerate the
431
+ read verbs, which never reconcile.
432
+ - **A vanished runtime never deletes, prunes, or re-identifies a resource, and
433
+ never releases a name reservation** — the same preservation contract
434
+ `MissingRoot` established for a Project whose root disappeared. There is no
435
+ auto-prune.
436
+ - The inventory is a pure **read**. It never writes, re-mirrors, or adopts a uid
437
+ onto a live tmux object; reattaching a lost binding belongs to the reconciler
438
+ (see *Binding reapply and adoption* below). After a tmux server restart the
439
+ objects survive but the options do not, so everything reads offline until the
440
+ next mutation route reconciles.
441
+ - The observation shells out as bare `tmux`, like every other mirror read, so
442
+ inside a client `$TMUX` selects the projmux socket. Introducing a second
443
+ socket convention for this one query would let the observation disagree with
444
+ the mirror writes it is diffed against.
445
+
446
+ Binding reapply and adoption:
447
+
448
+ - The `@projmux_*_uid` tmux options are the binding store, and they used to be
449
+ written **once**, at legacy-session import time. A tmux server restart, an
450
+ option reset, or a registry written before the mirror existed leaves live
451
+ windows and panes carrying no uid at all, and nothing ever put one back. The
452
+ measured symptom: `projmux delete pane` with no selector fails with *the
453
+ active tmux pane carries no `@projmux_pane_uid`* in almost every pane on the
454
+ machine, making the shipped "omit the selector, act on the active target"
455
+ behavior unreachable.
456
+ - Reconcile now **reapplies** bindings. Every live window and pane that resolves
457
+ to a Project gets its uid options written again, through the same
458
+ `Mirror.MirrorWindow` / `Mirror.MirrorPane` path an imported object uses.
459
+ There is one write convention, not a uid-only variant: a reattached object
460
+ must end up configured exactly like an imported one.
461
+ - The old import guard (*skip a Project that already owns Windows*) is gone. It
462
+ **avoided** duplicates instead of repairing drift, so once the registry and
463
+ the machine disagreed no later pass could bring them back together. Each
464
+ observed tmux window now resolves into one of four outcomes — **rebound**
465
+ (still carries a uid the Project owns), **adopted** (blank, pairs with the
466
+ next unbound registry Window), **created** (blank, no candidate left), or
467
+ **refused**. Panes cascade the same way inside a window that was itself
468
+ matched. A drifted registry converges in a single pass.
469
+ - **The matching key is structural, never content.** Two layers: the Project
470
+ scope, then ordinal alignment inside it — the session's tmux windows in
471
+ `window_index` order against that Project's registry Windows in creation
472
+ order, and panes the same way inside an adopted Window. That is the alignment
473
+ the import path already created and `mirrorImported` already maps back
474
+ through; adoption restores it rather than inventing one. `window_name`,
475
+ `@projmux_window_name`, `@projmux_pane_label`, pane cwd, basename, git origin,
476
+ and inode are explicitly **not** matching keys. Names are worthless here: the
477
+ registry carries the Window name `zsh` across nine different Projects.
478
+ - **Two ways a session resolves to a Project, and they are disjoint.**
479
+ `@projmux_project_path` is the *import* key — it is what turns an unknown
480
+ session into a Project, and it is written only at session creation, so a
481
+ session older than it has none. For those, reconcile uses the
482
+ Project↔session-name edge it already maintains: a Project's session name is
483
+ `status.session.name` when set, otherwise the name `sessionNameFor` gives its
484
+ root. That is used **forward only** — compute the expected name from the
485
+ Project and compare. Parsing a session name back into a path would be the
486
+ heuristic. The session-name path never creates a **Window**; only the anchored
487
+ import path does.
488
+ - **An orphan live pane is registered, and that is the one thing the
489
+ session-name path creates.** Adoption needs an existing registry Pane to adopt
490
+ *into*, and a pane produced by the earlier non-resource direct Agent bridge
491
+ has none, so it stayed unbound forever and
492
+ `projmux delete pane` with no selector kept refusing in the operator's own
493
+ active pane. Reconcile therefore mints a **shell-role Pane owned by the
494
+ already-paired Window** for every live pane inside it that matches nothing, and
495
+ mirrors that Pane's uid back through the same `Mirror.MirrorPane` write. The
496
+ Project boundary is structural rather than checked: the Window was paired
497
+ earlier in the same walk and belongs to the single Project the session name
498
+ resolved to, so there is no code path from a live pane to a Project it does not
499
+ already sit under. A live *window* that matches nothing still creates nothing,
500
+ and neither does a **refused** pane — a refusal means a real registry Pane sits
501
+ on the other side of the ambiguity, so minting beside it would leave two Panes
502
+ describing one tmux pane.
503
+ - The registered Pane is named from `FallbackPaneNameBase` (`pane`, `pane-1`, …)
504
+ through the registry's own allocator, never from `pane_current_command`:
505
+ `metadata.name` is not derived from a runtime attribute, and the command
506
+ changes the moment the operator runs something else. The runtime reading goes
507
+ to `status.displayTitle` instead. The mint itself never creates an Agent: it
508
+ adds one Pane and stops. Linking that Pane to an Agent is the separate step
509
+ below, which runs on the Pane the walk just settled on.
510
+ - **Nothing is ever re-identified.** Adoption changes no uid, merges no uid, and
511
+ reassigns no uid; it only decides which registry object a live tmux object is
512
+ the runtime of, and then writes that object's existing uid. Adopted objects
513
+ are reported to the reconciler so their bindings get written, but they are not
514
+ recorded as *created* in the transaction — rolling an operation back must not
515
+ delete an object that predates it.
516
+ - **Everything ambiguous is refused, and a refusal writes nothing.** The session
517
+ resolves to no Project, or to more than one. The live object carries a uid
518
+ another Project — or a sibling Window — owns. A candidate is already the
519
+ binding of a different live tmux object, so the walk moves on rather than
520
+ stealing it. Two live objects claim one uid. The parent window was not
521
+ matched, so none of its panes are considered.
522
+ - A uid the registry has **never heard of** is its own case. It is never
523
+ adopted: pointing an existing registry object at it would be re-identification
524
+ off a failed lookup. The anchored import path still *mints* a new object for
525
+ it, because projmux itself produces unknown uids — a reconcile rolled back by
526
+ a pre-create hook refusal has already written its allocated uids onto tmux,
527
+ and tmux options are not transactional — and refusing outright would leave
528
+ those windows permanently unmanageable. Minting changes no existing uid. The
529
+ session-name repair path, which has no anchor, mints only the Pane case and
530
+ skips the window.
531
+ - The "already bound elsewhere" set is one observation taken **before** the pass
532
+ writes anything, shared by both binding steps, so "already bound" means "bound
533
+ before we got here". The binding writes land before the runtime-observation
534
+ step, or that step would stamp `MissingRuntime` on a Window this same pass
535
+ just reattached.
536
+
537
+ Managed runtime binding convergence:
538
+
539
+ - Binding repair has two explicit mutation boundaries. A normal
540
+ `projmux config apply --socket <name>` first completes config preflight and a
541
+ successful `source-file`, then runs the existing registry reconciler against
542
+ that same exact `tmux -L <name>` server. The app-generated config also owns
543
+ synchronous `after-new-window` and `after-split-window` hooks. Each hook
544
+ expands tmux's absolute `#{socket_path}` and passes it to hidden internal
545
+ plumbing that routes every reconciler read and mirror write through
546
+ `tmux -S <absolute-path>`. Neither path falls back to the default socket or
547
+ inherited `$TMUX`.
548
+ - The lifecycle hooks are synchronous so a newly bindable Window or Pane has a
549
+ registry binding before the creating tmux command returns and before the next
550
+ implicit read can run. Mirror writes use `set-option` and `rename-window`, not
551
+ creation commands, so they cannot recursively fire either creation hook.
552
+ - A canonical resource create already owns the registry transaction while it
553
+ issues `new-window` or `split-window`. It therefore installs a private,
554
+ session-scoped create lease before the mutation (`new-session -e` on first
555
+ materialization), and the exact-socket hook inspects that lease using the
556
+ expanded `#{session_id}`. Only a live lease defers the hook's registry entry;
557
+ stale or malformed leases are cleared and normal synchronous convergence
558
+ continues. The create path explicitly mirrors its objects, reconciles the
559
+ resulting runtime again inside the same transaction, then ownership-checks
560
+ and clears its lease after commit or rollback. This avoids lock reentry
561
+ without weakening standalone lifecycle ordering.
562
+ - A tmux creation may return non-zero after the object exists when a later
563
+ synchronous hook fails. Materialization retains the combined output, accepts
564
+ a reported `@N` or `%N` only when it was absent from the before-inventory and
565
+ present in the same target's after-inventory, mirrors its operation uid, and
566
+ rolls it back in reverse order. Ambiguous handles and changed ownership fail
567
+ closed: unrelated objects are preserved and residual drift is reported.
568
+ - `config apply --no-reload` stops before any live-server query, and config or
569
+ keymap preflight failure does the same. A server on a second socket is never
570
+ inventoried or mutated. `get`, `describe`, and implicit active-target
571
+ resolution remain read-only and never invoke convergence or open a registry
572
+ transaction.
573
+ - The existing `BindingMatcher`, `registryReconciler`, and metadata `Mirror`
574
+ remain the only matcher, orchestrator, and tmux uid writer. Missing bindings
575
+ take the existing complete mirror path; ambiguous and foreign objects keep
576
+ the existing refusal rules. A resource already carrying its exact registry
577
+ uid skips the mirror, and the convergent store suppresses an atomic registry
578
+ replace when normalization finds no semantic change. Repeating apply or the
579
+ lifecycle boundary therefore issues no `set-option`/`rename-window` writes
580
+ and performs no registry byte write.
581
+ - This boundary adds no public command, option, environment variable, or
582
+ registry schema. It does not add persistent Project scope, matching by name,
583
+ cwd, or a new ordinal heuristic, uid merge/reassignment, pruning, or forced
584
+ adoption. The Project scope remains derived from the active binding on read.
585
+
586
+ Public resource reconciliation:
587
+
588
+ - `projmux reconcile resources` exposes the same Registry matcher, mutator,
589
+ reconciler, and tmux Mirror as a deliberate operator repair boundary. A
590
+ shadow tmux runner delegates reads to one exact server, records mirror writes,
591
+ and overlays only those recorded UID values for the reconciler's final
592
+ observation. Planning therefore executes production convergence on a cloned
593
+ Registry while writing zero Registry, tmux, or filesystem bytes.
594
+ - Plan item identity is stable by resource kind, live target or Registry scope,
595
+ and action. Opaque UIDs allocated while planning are display details
596
+ normalized to deterministic placeholders; they are not matching keys and do
597
+ not obscure owner or target identity. Human and JSON output share the same
598
+ sorted items and missing/stale/foreign/orphan vocabulary.
599
+ - Execute rebuilds the plan from the locked current Registry. Runtime
600
+ observation is limited to the Registry Project graphs safely attributable to
601
+ sessions on the selected socket; absence there never marks another socket's
602
+ graph missing or releases its Agents. The desired
603
+ Registry is validated and committed before any non-transactional tmux mirror
604
+ write, keeping Registry identity authoritative and retryable if a later live
605
+ step fails. After commit, every planned live write is guarded by re-reading
606
+ its target's Project, Window, or Pane UID binding from the exact socket; all
607
+ guards and planned before-values must still match before the first write. A
608
+ recycled or raced handle therefore causes zero live writes.
609
+ - A Registry commit failure performs no tmux mutation. A partial tmux failure
610
+ leaves the durable Registry identity in place, replans current drift, and
611
+ reports completed stages, remaining items, and the exact retry command.
612
+ Repeating after success plans no writes and does not replace `registry.json`.
613
+ - `--socket` is exact `-L`; absolute `--socket-path` and inherited `$TMUX` are
614
+ exact `-S`. No-flag use outside tmux is rejected before planning. No default
615
+ socket, fallback server, config reload, state-loss recovery, or heuristic UID
616
+ merge exists on this route.
617
+ - Public repair is stricter than lifecycle compatibility convergence for
618
+ foreign state. An unknown, duplicate, or wrong-owner live UID makes its
619
+ session diagnostic-only so later ordinal rows cannot slide onto a different
620
+ Registry object. Safe drift elsewhere may converge; the refused item remains
621
+ explicit and nonzero. `get`, `describe`, and `doctor` never enter this path.
622
+ - A known, unique `@projmux_project_uid` is the authority for Project rebind
623
+ drift: the old non-empty `@projmux_project_path` does not turn that session
624
+ foreign. The planner emits only the path-option repair, guards it with the
625
+ unchanged Project UID, and becomes a no-op after convergence. Unknown or
626
+ duplicate Project UID claims remain refused.
627
+
628
+ Agent runtime linkage:
629
+
630
+ - Once a live tmux pane has settled on a registry Pane, reconcile decides which
631
+ **Agent** that Pane is the managed Pane of. Without this step the registry had
632
+ running agents with no Agent resource and Agent resources with no
633
+ `status.paneRef`, so `get panes` printed an empty AGENT column for every row
634
+ and `get agents` listed finished conversations while hiding running ones.
635
+ - **The evidence is authorship, not a command name.** `pane_current_command ==
636
+ claude` says a process called claude is running; it is equally true of a pane
637
+ the operator typed `claude` into by hand, and nothing here reads it. The
638
+ evidence is `@projmux_ai_agent`, the pane option the AI routes write when
639
+ *projmux itself* launches an agent into a pane. A pane without it gets no
640
+ Agent — Phase 1's refuse rule, unchanged. The legacy import path already
641
+ trusted exactly this option to mint an Agent on its create path; linkage makes
642
+ the adopt and rebind paths agree with it.
643
+ - **Which Agent, in order.** (1) The Pane is already Agent-owned: that Agent is
644
+ the answer and only `status.paneRef` is repaired. (2) An Agent in the same
645
+ Window already records the same provider conversation in `status.sessionRef`
646
+ as the pane carries in `@projmux_ai_session_id` / `@projmux_ai_thread_id` —
647
+ an exact identifier equality on a value both sides got from the provider, with
648
+ `provider` compared too so a Codex thread id is never equated with a Claude
649
+ session id. (3) Otherwise a new Agent is minted, named through the registry's
650
+ own allocator over the provider name base, with the observed topic going to the
651
+ non-identifying `projmux.io/agent-topic` annotation.
652
+ - **Ambiguity mints rather than guesses.** Two Agents recording one conversation
653
+ is legal registry state, so an ambiguous conversation match cannot be resolved
654
+ to "the first one"; taking a binding that might belong to the other Agent is
655
+ the mistake no later pass can undo, while an extra Agent is inert and visible.
656
+ A candidate already claimed by this pass, or one whose `paneRef` is some other
657
+ Pane, is not a candidate at all — one Agent is the runtime owner of at most one
658
+ live pane. The candidate set is the paired Window's Agents and nothing else, so
659
+ the Project boundary is structural here too.
660
+ - **A linked Pane is promoted, and that is the one rewrite of an existing
661
+ resource.** Its `spec.role` becomes `agent` and its `ownerRef` moves from the
662
+ Window to the Agent, with its name reservation following it into the Agent's
663
+ scope. The uid does not change, the name does not change, and the Project and
664
+ Window it sits under do not change — the Agent is owned by the very Window that
665
+ owned the Pane. `ownerRef` is the single edge every other reader resolves
666
+ Agent↔Pane through (the AGENT column, hook session-ref attribution, cascading
667
+ delete), so expressing the link only in `status.paneRef` would create a second,
668
+ disagreeing source of truth.
669
+ - `status.sessionRef` is **not** written here. The pane option is a live routing
670
+ index and the durable conversation pointer belongs to hook ingest, which
671
+ reaches the Agent on its own once the Pane is Agent-owned.
672
+ - **A promoted Pane joins the managed-Pane lifecycle**, which is a visible
673
+ consequence: the dead-agent-pane sweep releases an Agent whose managed Pane
674
+ died and removes that Pane row, where a Window-owned shell Pane was never in
675
+ its reach. That is the existing managed-Pane contract applying to panes that
676
+ genuinely are managed. The **Agent** is preserved either way — same uid, same
677
+ name, same `sessionRef`, still resumable.
678
+ - Linkage is idempotent: the next pass finds the Pane already Agent-owned and
679
+ reasserts nothing, so a reconciler that runs on every mutation route converges
680
+ instead of accumulating Agents. It is tolerant like the writes around it — a
681
+ link that cannot be made writes nothing, does not cost the pane the binding
682
+ that already succeeded, and does not fail the operator's command.
683
+ - Reapply stays a **mutation-route** concern, like the rest of reconcile. Read
684
+ verbs still `LoadReadOnly` and still never materialize the registry. A tmux
685
+ server that is absent or erroring still fails closed with no error, and a
686
+ binding write or an orphan registration that fails is skipped rather than
687
+ escalated — the next pass sees the same drift and tries again. It is
688
+ maintenance riding along inside somebody else's transaction: one pane it cannot
689
+ register must not fail the `create` that happened to trigger it.
690
+
691
+ Selector and the implicit active target:
692
+
693
+ - A selector value is either `uid:<uid>` or a `metadata.name`. There is no
694
+ bare-uid form; `displayName`, `spec.root`, and tmux `%N`/`@N`/`$N` handles are
695
+ structurally unmatchable. `--project` is at-most-once and fixes the scope,
696
+ `--window`/`--pane` repeat and union, `--selector key=value` repeats and ANDs,
697
+ and how many targets a `<verb, kind>` pair accepts comes from one declared
698
+ cardinality matrix rather than per-route rules.
699
+ - Inside tmux, an invocation of a **singular read or rename verb** that carries
700
+ no selector at all resolves the **active tmux target**: `get pane`,
701
+ `describe project|window|pane|agent`, `rename project|window|pane`, and
702
+ `rebind project`. Any reference, scope flag, or label keeps the pre-existing
703
+ singular-target meaning; `create` and the destructive routes are unaffected.
704
+ - Project is also the namespace-like default scope of the plural registry reads
705
+ `get windows|panes|agents`. When `--project` is absent inside tmux, the active
706
+ Window uid mirror and its registry owner chain derive a Project on every
707
+ invocation. This narrows the Window universe only; it never chooses one
708
+ Window, Pane, or Agent for the operator. `get projects` is above that scope,
709
+ while notifications and snapshots belong to separate stores, so all three
710
+ remain global.
711
+ - `--all-projects` is the explicit registry-wide escape for those three reads.
712
+ It is deliberately different from destructive `delete --all`, whose existing
713
+ whole-registry compatibility meaning is unchanged. A bare `--all` is not a
714
+ read flag. Explicit `--project` keeps its prior result and cannot be combined
715
+ with `--all-projects`.
716
+ - Outside tmux, an omitted Project scope keeps the historical whole-registry
717
+ inventory. Inside tmux, a missing Window binding or broken Project owner chain
718
+ is a usage refusal with zero stdout, never a silent global fallback. The
719
+ selector engine's `windowScope` is the single choice point for explicit
720
+ Project, active-derived default, or global scope, shared by Window, Pane, and
721
+ Agent resolution.
722
+ - There is **no sentinel value token**. `current` and `active` pass
723
+ `ValidateName`, so `--pane current` would shadow a resource that legitimately
724
+ carries that name. Omission is the only spelling. If an explicit one is ever
725
+ needed, `.` (reserved by `ValidateName`) and `@` (a forbidden name rune) are
726
+ the two collision-free candidates; neither is claimed today.
727
+ - Being inside tmux is decided from `$TMUX_PANE` plus `$TMUX`, and that pane id
728
+ is passed as an explicit `-t` target. It is deliberately **not** decided by
729
+ whether a tmux command succeeds: a bare `display-message -p` from outside a
730
+ client still answers, for the most-recently-used session, which would silently
731
+ select a wrong target.
732
+ - Only two options are read: `@projmux_pane_uid` on the active pane and
733
+ `@projmux_window_uid` on its window (window-scoped options resolve through a
734
+ pane target). Every ancestor above them comes from `ownerRef` — the Project is
735
+ the owner of the active Window, the Agent is the owner of the active Pane.
736
+ The session-scoped `@projmux_project_uid` is **not** consulted: it is
737
+ measurably empty on live sessions, so trusting it would refuse targets the
738
+ owner chain resolves.
739
+ - There is no persistent, queryable scope and no `set-context` equivalent. The
740
+ observation is re-read on every invocation, which costs one `display-message`
741
+ and leaves nothing to go stale. `describe pane` with no selector is itself the
742
+ preview of what the singular family will act on; the plural-read Project
743
+ default consumes the same observation and owner chain without choosing an
744
+ individual resource.
745
+ - An active target that maps onto no registry resource is a **refusal**, not a
746
+ fallthrough: exit `2`, zero stdout, no resource selected, nothing written, and
747
+ a message naming what was inspected. It is deliberately not the
748
+ `matched N ..., want exactly one` cardinality error, because an unmanaged pane
749
+ carrying no `@projmux_pane_uid` is the common case and presenting it as
750
+ ambiguity would hide the cause.
751
+
106
752
  ## Naming metadata model
107
753
 
108
754
  Projmux keeps visible naming separate from source metadata:
@@ -143,36 +789,36 @@ Projmux keeps visible naming separate from source metadata:
143
789
  following XDG). Writes go through an `O_CREATE|O_EXCL` lock file
144
790
  (`notify.json.lock`) with bounded retry + jittered backoff so the queue
145
791
  is safe across concurrent producers (the AI flow, the manual `attention
146
- toggle`, the `notify push` CLI) on a local filesystem.
792
+ toggle`, the `create notification` CLI) on a local filesystem.
147
793
 
148
794
  Attention and notify are intentionally separate surfaces: attention is live
149
795
  tmux pane state, while notify is the explicit-ack pending queue derived from
150
796
  AI reply panes and explicit pushes. The queue helps clicks route to work; it
151
797
  does not own the truth of every live badge.
152
798
 
153
- - **Push** — `projmux notify push` (or the in-process producer in
799
+ - **Push** — `projmux create notification` (or the in-process producer in
154
800
  `internal/app/notify_producer.go`) appends an entry. Entries carry a
155
801
  stable id (caller-supplied or `ai:<session>:<pane>` for the producer
156
802
  path), text (capped at 80 runes), severity (`info|warn|critical`),
157
803
  source (`ai|k8s|git|external`), TTL freshness metadata (default 600s), and a
158
804
  `Target{Socket, Session, Window, Pane}`. Re-pushing an existing id
159
805
  refreshes the entry's text and timestamp.
160
- - **List** — `projmux notify list` returns newest-first without mutating the
161
- queue. TTL alone is not a removal condition. `projmux notify list --live` adds a
806
+ - **List** — `projmux get notifications` returns newest-first without mutating the
807
+ queue. TTL alone is not a removal condition. `projmux get notifications --live` adds a
162
808
  read-only comparison against live pane state, explaining manual reply
163
809
  badges without queue entries, live AI replies with/missing queue entries,
164
810
  and inactive (`queue-stale`) `ai:` entries.
165
- - **Ack** — `projmux notify ack <id>` removes one entry; `--all`
811
+ - **Ack** — `projmux notification ack <id>` removes one entry; `--all`
166
812
  flushes everything. Interactive focus/click handlers ack after successful
167
813
  focus, and gone/unroutable targets clean up without focusing.
168
- - **Reconcile** — `projmux notify reconcile` walks
814
+ - **Reconcile** — `projmux notification reconcile` walks
169
815
  `tmux list-panes -a` and back-fills entries for panes whose
170
816
  attention state is `reply` AND whose AI agent option is set,
171
817
  reporting inactive `ai:` entries that no longer match a live reply+agent pane without
172
818
  acking them. It then removes rows only when they are both TTL-expired and
173
819
  gone from the real pane/session inventory, and enforces a 256-row hard cap
174
820
  by evicting oldest overflow. Live rows otherwise remain explicit-ack-only.
175
- `make install` and `projmux upgrade` invoke it so the queue
821
+ `make install` and `projmux update apply` invoke it so the queue
176
822
  recovers from any drift introduced by a lost daemon.
177
823
 
178
824
  The producer is wired to the attention state machine: a pane
@@ -186,7 +832,7 @@ See [notify-queue.md](notify-queue.md) for the full reference.
186
832
 
187
833
  ## Usage snapshots
188
834
 
189
- `projmux usage` and `projmux status usage` share a single `Manager`
835
+ `projmux agent usage` and `projmux internal status usage` share a single `Manager`
190
836
  that walks two registered adapters (Claude, Codex) and persists the
191
837
  result to `<state>/projmux/usage/snapshots.json` (or
192
838
  `PROJMUX_USAGE_STATE_DIR`). The cache file is the authoritative source
@@ -195,7 +841,7 @@ network call.
195
841
 
196
842
  - **Per-adapter throttle** — Claude reports a 5-minute hint via the
197
843
  `ThrottleHinter` interface; Codex falls through to the global
198
- `30s` floor used by `status usage`. `MaybeCollect` only invokes an
844
+ `30s` floor used by `internal status usage`. `MaybeCollect` only invokes an
199
845
  adapter when `now - last_collect >= throttle`. `--force` bypasses the
200
846
  gate.
201
847
  - **429 backoff** — Claude implements `BackoffStater`. On HTTP 429
@@ -216,22 +862,21 @@ refresh, rollout schema).
216
862
  ## Two-line clickable status bar
217
863
 
218
864
  projmux configures tmux with `status 2`. Line 0 is the existing
219
- session/window/path/git/kube/clock row. Line 1 splits the notification bar
865
+ session/window/path/git/clock row. Line 1 splits the notification bar
220
866
  (left half, capped at 80 cells) and the AI usage HUD (right half, capped at
221
867
  120 cells) using tmux `#[align=left]` / `#[align=right]`. Each clickable
222
868
  segment is wrapped in a tmux user-defined range (`#[range=user|<id>]...
223
- #[norange]`) and dispatched through `projmux statusbar click <range-id>`. A
869
+ #[norange]`) and dispatched through `projmux internal statusbar click <range-id>`. A
224
870
  single `bind -n MouseDown1Status` covers both lines because tmux fires
225
871
  `MouseDown1Status` from any line of a multi-line status bar with
226
872
  `#{mouse_status_range}` resolving to whichever range the cursor was over.
227
873
 
228
874
  | Range id | Line | Click action | Keybinding |
229
875
  |----------|------|-------------------------------------------|--------------|
230
- | session | 0 | popup `projmux sessions --ui=popup` | prefix+s s |
876
+ | session | 0 | popup `projmux runtime sessions --ui=popup` | prefix+s s |
231
877
  | pwd | 0 | show pane_current_path in a display-only path popup | prefix+s p |
232
- | kube | 0 | popup `projmux switch --ui=popup` | prefix+s k |
233
878
  | git | 0 | popup `projmux switch --ui=popup` | prefix+s g |
234
- | usage | 1 | popup `projmux usage` | prefix+s u |
879
+ | usage | 1 | popup `projmux agent usage` | prefix+s u |
235
880
  | notify | 1 | focus origin pane of newest notification | prefix+s n |
236
881
 
237
882
  The keyboard chord uses `bind-key s switch-client -T projmux-status` so the
@@ -242,6 +887,23 @@ The hardcoded `prefix s r` sibling is usage-specific: it runs the existing
242
887
  throttled collector and then reopens the same display-only usage popup from
243
888
  cache.
244
889
 
890
+ ## Related design and inventory notes
891
+
892
+ Contributor-facing companions to this document. They are design records and
893
+ inventories rather than user documentation, so they are linked from here rather
894
+ than from the README docs index.
895
+
896
+ - [globalization.md](globalization.md) — the globalization contract: which
897
+ user-facing string families are translatable and how they are classified.
898
+ - [migration-plan.md](migration-plan.md) — the standalone plan the shell-to-Go
899
+ migration follows, slice by slice.
900
+ - [settings-ia.md](settings-ia.md) — the Settings information architecture:
901
+ section ownership, row density, and feedback rules.
902
+ - [shell-autostart.md](shell-autostart.md) — shell auto-start integration and
903
+ its opt-out behavior.
904
+ - [tmux-surface-inventory.md](tmux-surface-inventory.md) — the inventory of tmux
905
+ options, hooks, and bindings projmux owns.
906
+
245
907
  ## Non-goals
246
908
 
247
909
  - replacing tmux