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.
- package/README-ko.md +90 -72
- package/README.md +24 -16
- package/docs/agent-workflow.md +86 -23
- package/docs/ai-agent-shortcuts.md +63 -278
- package/docs/architecture.md +678 -16
- package/docs/cli-guide.md +1367 -0
- package/docs/cli.md +1239 -1091
- package/docs/configuration.md +185 -136
- package/docs/globalization.md +16 -12
- package/docs/hooks.md +90 -67
- package/docs/install.md +22 -6
- package/docs/keybindings.md +223 -60
- package/docs/legacy-cli-retirement.md +64 -0
- package/docs/legacy-diagnostics-inventory.md +23 -0
- package/docs/migration-plan.md +6 -3
- package/docs/notify-os-focus-poc.md +36 -15
- package/docs/notify-queue.md +21 -19
- package/docs/operational-diagnostics.md +190 -11
- package/docs/repo-layout.md +4 -1
- package/docs/resource-attribution.md +16 -0
- package/docs/session-restore.md +27 -9
- package/docs/settings-ia.md +232 -162
- package/docs/statusbar.md +228 -48
- package/docs/testing.md +9 -9
- package/docs/theme-palette.md +0 -3
- package/docs/tmux-surface-inventory.md +2 -2
- package/docs/upgrading.md +138 -23
- package/docs/usage-tracking.md +120 -34
- package/package.json +5 -5
package/docs/architecture.md
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
161
|
-
queue. TTL alone is not a removal condition. `projmux
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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`
|
|
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`
|
|
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
|