projmux 0.16.0 → 0.16.1

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.
@@ -233,8 +233,12 @@ per-launch model or effort row.
233
233
 
234
234
  `window.create` (v0 id `new-window`) and the Window menu's New At End decide the
235
235
  new Window's first Pane before the Window exists, following the saved launch
236
- default -- the same setting the saved-default split key reads (Settings > AI
237
- Settings, stored in `$XDG_CONFIG_HOME/projmux/tmux-ai-split-mode`). The order is:
236
+ default. That default is the TUI file
237
+ `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/tmux-ai-split-mode` (Settings > AI
238
+ Settings, the same setting the saved-default split key reads) when it holds a
239
+ valid mode, then the central `ai-new-window-mode`, then `selective`; see
240
+ [New AI Window Default](configuration.md#new-ai-window-default). The create
241
+ then runs in this order:
238
242
 
239
243
  1. Ask. A picker mode opens its picker on the Pane the key was pressed in; a
240
244
  provider mode and `shell` are already the answer and open nothing.
@@ -249,7 +253,7 @@ The client therefore never sees a shell Pane that is about to be replaced.
249
253
  | --- | --- |
250
254
  | `shell` | the shell Pane the create made; nothing else runs |
251
255
  | `claude`, `codex`, `antigravity` | exactly that Agent Pane, with no picker |
252
- | `selective` (also the unset default) | whatever the `Alt-7` picker chose: that Agent Pane, or the shell Pane for the shell row; its `resume` row opens the resume session list in the same popup |
256
+ | `selective` (also the default when neither `tmux-ai-split-mode` nor `ai-new-window-mode` holds a valid mode) | whatever the `Alt-7` picker chose: that Agent Pane, or the shell Pane for the shell row; its `resume` row opens the resume session list in the same popup |
253
257
  | `resume` | whatever the resume picker chose, on the same terms |
254
258
 
255
259
  Cancelling the picker creates nothing: no Window, and the client stays where it
@@ -605,14 +609,14 @@ still required before ordinary mutation.
605
609
  To roll v2 back to the immediately previous v1 file, restore its pre-v2 backup:
606
610
 
607
611
  ```sh
608
- cp ~/.config/projmux/keymap.toml.pre-v2-<digest>.bak ~/.config/projmux/keymap.toml
612
+ cp "${XDG_CONFIG_HOME:-$HOME/.config}/projmux/keymap.toml.pre-v2-<digest>.bak" "${XDG_CONFIG_HOME:-$HOME/.config}/projmux/keymap.toml"
609
613
  ```
610
614
 
611
615
  For a keymap that entered migration as v0, restore the established pre-v1
612
616
  backup **before** installing a projmux that predates versioned keymaps:
613
617
 
614
618
  ```sh
615
- cp ~/.config/projmux/keymap.toml.pre-v1-<digest>.bak ~/.config/projmux/keymap.toml
619
+ cp "${XDG_CONFIG_HOME:-$HOME/.config}/projmux/keymap.toml.pre-v1-<digest>.bak" "${XDG_CONFIG_HOME:-$HOME/.config}/projmux/keymap.toml"
616
620
  ```
617
621
 
618
622
  A projmux that predates this schema reads `schema_version` as an unsupported
@@ -14,6 +14,10 @@ this package layout:
14
14
  | `@projmux/darwin-x64` | `darwin/amd64` `bin/projmux` |
15
15
  | `@projmux/darwin-arm64` | `darwin/arm64` `bin/projmux` |
16
16
 
17
+ Each platform package also carries `THIRD_PARTY_NOTICES`, the license notices
18
+ of the Go runtime and the Go modules its binary links. The root package has no
19
+ binary and no notices.
20
+
17
21
  The shim sets `PROJMUX_INSTALLER=npm` before executing the real binary so
18
22
  `projmux update status` and the Settings About screen can present
19
23
  npm-specific guidance. npm is only an update/install source label here; the
@@ -16,8 +16,15 @@ no generic metadata map. Runtime lifecycle records add only closed
16
16
  attach, switch, kill, and config apply; codes are stable failure/health
17
17
  classifications and never carry routing identity or subprocess details.
18
18
 
19
- Command and subcommand names come from static allowlists. Unknown argv values,
20
- paths, flags, and arguments are dropped. Messages have control/format
19
+ Command and subcommand names come from static allowlists. The allowlist covers
20
+ every route of the CLI route graph, `internal <namespace>` included, and its
21
+ direct child subcommand; a sweep test over the graph enforces this. Routes
22
+ covered only for this purpose gain error attribution: whether a success is
23
+ recorded still follows the state-changing rules below. An alias records the
24
+ canonical child name (`get project` records `get projects`). Unknown argv stays
25
+ unnamed, and argv values, paths, flags, and arguments are dropped. A log written
26
+ by a newer binary may carry names an older reader does not know; that reader
27
+ skips those lines. Messages have control/format
21
28
  characters removed, whitespace normalized, the current home path abbreviated
22
29
  to `~`, and length capped at 512 Unicode code points. Top-level outcomes never
23
30
  copy `error.Error()` into the journal: their message is one of three stable,
@@ -36,6 +43,59 @@ operation instead of recording nested outcomes. Lifecycle ownership replaces
36
43
  the generic top-level `command.outcome`; it never duplicates it. Start/outcome
37
44
  append failures are ignored and do not change the command result.
38
45
 
46
+ The `lifecycle.outcome` of `operation=tmux.apply` (`config apply` and the
47
+ hidden `internal tmux apply`) also carries a step and Registry lock
48
+ breakdown. It is measurement only: the apply's steps, their order, its
49
+ stdout, stderr, exit status, rollback, and every lock boundary are exactly
50
+ as without it. The record is appended after the apply route returned, so
51
+ after every Registry lock it took was released, and a journal failure
52
+ changes nothing about the apply. Only closed names and integers are added;
53
+ every other event family, including `lifecycle.outcome` of any other
54
+ operation and `lifecycle.start`, rejects each of these fields.
55
+
56
+ - One `step_<name>_ms` field per apply step the apply entered, in order:
57
+ `step_keymap_migration_ms` (keymap migration),
58
+ `step_hook_file_migration_ms` (managed agent hook file migration),
59
+ `step_retired_file_reclaim_ms` (retired snapshot, Codex generation, and
60
+ sidebar startup file reclaim and the status bar default seed),
61
+ `step_route_bind_ms` (binding to the exact live app server),
62
+ `step_bell_hook_migration_ms` (managed tmux bell hook migration),
63
+ `step_config_write_ms` (writing the generated config),
64
+ `step_key_sequence_retire_ms` (retiring recorded key sequence state),
65
+ `step_source_file_ms` (the route guard and `source-file`),
66
+ `step_route_marker_ms` (the logical socket marker),
67
+ `step_exhausted_replay_ms` (replaying retry-exhausted clean exits), and
68
+ `step_converge_ms` (the controller convergence). A step the apply did not
69
+ enter -- `--no-reload`, no live server, or an earlier failure -- is absent.
70
+ Steps are measured on the same clock as `duration_ms`, start no earlier
71
+ than it, and do not overlap; each opens where the previous one closes, so
72
+ the few statements between two steps count toward the earlier one. Each is
73
+ rounded down to whole milliseconds, and `sum(step ms) <= duration_ms`
74
+ always holds on disk: a step set that would exceed it is dropped whole.
75
+ - `lock_acquisition_count`, `lock_wait_total_ms`, and `lock_held_total_ms`
76
+ total every Registry lock acquisition the apply made while it ran, whatever
77
+ site made it, with the wait and hold the Registry Store measured for
78
+ `registry.lock.acquisition`. They are present whenever the breakdown is,
79
+ and zero when the apply took no lock.
80
+ - `longest_lock_kind`, `longest_lock_step`, `longest_lock_wait_ms`, and
81
+ `longest_lock_held_ms` describe the one released acquisition that held the
82
+ lock longest. The kind is the Registry transaction it belonged to:
83
+ `preexisting-dead-agent`, `control-targets`, `mirror-recovery`,
84
+ `binding-converge`, `lifecycle-reconcile`, `session-lower`, or `other` for
85
+ an acquisition no site named. The step is the apply step it ran in, or
86
+ `other` outside every step. All four are absent when no acquisition held
87
+ the lock, and the longest wait and hold never exceed the totals.
88
+ - `longest_lock_observe_ms`, `longest_lock_plan_ms`, `longest_lock_commit_ms`,
89
+ and `longest_lock_store_write_ms` split that hold: live tmux reads and
90
+ their classification, plan and reconcile computation, Registry and tmux
91
+ writes inside the transaction, and from the transaction's callback
92
+ returning to the release being observed (normalize, validate, the durable
93
+ write, the unlock). A phase the transaction did not mark is absent, and the
94
+ grant and the Store's locked read before the first mark are left
95
+ unattributed. `sum(phase ms) <= longest_lock_held_ms` always holds on disk:
96
+ a breakdown that would exceed the hold is dropped whole and the rest of the
97
+ longest lock is kept.
98
+
39
99
  Project lifecycle operator diagnostics also keep plans mutually exclusive:
40
100
  `stop`, `close-window`, `delete-project`, and `fresh` are distinct operation
41
101
  classes. Startup and unregister failures print the closed action, failing
@@ -66,10 +126,11 @@ and the UI new Window, including one whose answer is an Agent), `pane`
66
126
  (`create pane`, the split UI's shell Pane, and the pane-menu split), `agent`
67
127
  (`create agent` and the split UI's Agent Pane, including a resume-picker
68
128
  pick, which creates a new Agent), or `resume` (`agent resume`, including the
69
- resume `agent persona` runs). A successful transaction is `info`/`success`;
129
+ resume `agent relaunch` runs). A successful transaction is `info`/`success`;
70
130
  a failed one, including one that was rolled back, is `error`/`error` with
71
131
  `kind=runtime`, so the support report's existing error-only projection
72
- carries it. The record adds only two timings:
132
+ carries it. The record adds two timings and, inside the lock hold, their
133
+ breakdown:
73
134
 
74
135
  - `duration_ms` runs from entering the create transaction to its return. It
75
136
  includes the runtime route bind, the wait for the Registry lock, the time
@@ -85,14 +146,212 @@ carries it. The record adds only two timings:
85
146
  excludes the wait for the lock and the Registry read before the mutation
86
147
  starts. It is absent when the transaction failed before it entered the
87
148
  mutation, and `0 <= lock_held_ms <= duration_ms` always holds.
149
+ - Six phase fields split `lock_held_ms` into the stages of the mutation, in
150
+ order, each starting where the previous one ended:
151
+ `phase_guard_ms` (the ownership guards' preflight against tmux),
152
+ `phase_first_reconcile_ms` (the reconcile pass before the create's own
153
+ writes), `phase_operation_ms` (the create itself: tmux splits and mirrors,
154
+ the Registry edit, and for an Agent the supervised child spawn),
155
+ `phase_second_reconcile_ms` (the reconcile pass after those writes),
156
+ `phase_reprove_ms` (the re-proof of any route identity the transaction
157
+ reused), and `phase_store_write_ms` (from the mutation callback returning
158
+ to the Registry update returning: normalize, validate, the durable write,
159
+ the unlock, and anything the store does after the unlock before it
160
+ returns). A phase the transaction never reached, because an earlier stage
161
+ failed, is absent; the store-write phase is present whenever the mutation
162
+ was entered. The phases appear only with `lock_held_ms`, and
163
+ `sum(phase ms) <= lock_held_ms` always holds on disk: a breakdown that
164
+ would exceed the hold (a clock that went backwards) is dropped whole.
165
+ - `spawn_to_release_ms` is written for `agent` and `resume` only. It runs
166
+ from the first supervised child spawn -- the split that starts the managed
167
+ Pane's supervisor returning that Pane -- to the Registry update returning:
168
+ how much of the child's own Registry lock budget the creator consumed
169
+ before the child could take the lock. It is absent when no supervised
170
+ child was spawned, and `0 <= spawn_to_release_ms <= lock_held_ms` always
171
+ holds.
88
172
 
89
173
  The record is appended after the transaction returns, so never while the
90
174
  Registry lock is held, and a journal failure never changes the create's
91
175
  result, exit status, stdout, or stderr. It does not replace the invocation's
92
176
  `command.outcome`, and it does not count guard reads or time individual
93
177
  guards. The generated Window rename also runs through the same transaction
94
- and is not recorded, because it is not a create. Every other event family
95
- rejects `lock_held_ms` and the `create` component.
178
+ and is not recorded, because it is not a create. Every event family other
179
+ than `create.outcome` rejects the `create` component, the six phase fields,
180
+ and `spawn_to_release_ms`; every family other than `create.outcome` and
181
+ `registry.lock.acquisition` rejects `lock_held_ms`.
182
+
183
+ Registry lock acquisitions use `component=registry` and
184
+ `event=registry.lock.acquisition`. Every acquisition of the Registry mutation
185
+ lock -- a Registry read, update, convergent update, schema migration, or
186
+ admission barrier -- is measured, and one record is written for an
187
+ acquisition that waited at least one second, held the lock at least one
188
+ second, or timed out; every other acquisition writes nothing. The record is
189
+ appended after the lock is released (or after the acquisition gave up), under
190
+ the invocation's `run_id`, and it is best effort: a journal failure, or any
191
+ failure in the recorder, never changes the Registry operation's result or
192
+ the bytes it wrote. When a create's own acquisition is recorded, that append
193
+ runs after the unlock but before the Registry update returns, so its cost
194
+ falls inside that create's `lock_held_ms` and `phase_store_write_ms`. The
195
+ record carries only:
196
+
197
+ - `command` and `subcommand`: the invocation's catalog command, classified
198
+ like `command.outcome` and absent for an unclassified invocation. No argv
199
+ value, prompt, flag, path, pid, or process name is recorded.
200
+ - `operation`: the Registry entry point that took the lock, one of `update`,
201
+ `update-convergent`, `load`, `migrate`, or `admission-barrier`.
202
+ - `wait_ms`: from just before the acquisition to the grant, or to giving up.
203
+ - `lock_held_ms`: from the grant to just after the release; present only when
204
+ the lock was held.
205
+ - `duration_ms`: always exactly `wait_ms + lock_held_ms`.
206
+ - `result`, `level`, `kind`, and `code`: a held and released lock whose work
207
+ succeeded is `success` with no kind or code, at level `info`, or `warn` when
208
+ it held the lock for five seconds or more. A held lock whose work failed
209
+ (a refused callback, a failed validation or write) is `error`/`error`,
210
+ `kind=runtime`, `code=registry.mutation.failed`. A deadline timeout is
211
+ `error`/`error`, `kind=runtime`, `code=registry.lock.timeout`, and any other
212
+ acquisition failure is `code=registry.lock.acquire-failed`; neither carries
213
+ `lock_held_ms`.
214
+
215
+ `warn` is used by this event alone, and every other event family rejects
216
+ `wait_ms`, the `registry` component, and level `warn`. `diagnostics log
217
+ --level warn` selects these records; the support report's error-only
218
+ projection carries the `error` ones.
219
+
220
+ A Registry lock timeout error names the process the lock marker records as
221
+ holder, when that process is observed running, by the leading command words
222
+ of its `/proc/<pid>/cmdline`: the program's base name followed by the words
223
+ after it while they look like catalog command words (a lowercase letter, then
224
+ lowercase letters, digits, and hyphens), stopping at the first flag, `--`,
225
+ uid, path, number, or prompt, and at four words in all -- for example
226
+ `holder: pid 1234 (projmux create agent), running`. When the command line
227
+ cannot be read or yields no word, the kernel process name is used, and when
228
+ that cannot be read either, the command is reported as unavailable.
229
+
230
+ An accepted `agent message send` whose Claude source Agent's registered
231
+ Claude process is not an ancestor of the sender writes one `component=agent`,
232
+ `event=agent.message.foreign-source` `info`/`success` record. It adds only the
233
+ opaque source `agent_uid` (`agent-…`) and its `pane_uid` (`pane-…`); the
234
+ provider session id, process ids, and environment shown in the stderr warning
235
+ are never recorded, and every family other than the two `agent` events below
236
+ rejects `agent_uid` and the `agent` component. A journal failure never changes
237
+ the send.
238
+
239
+ The Claude messaging endpoint registration writes `component=agent`,
240
+ `event=agent.claude.registration` records. The chain is the SessionStart hook
241
+ `internal claude-endpoint-register`, which builds a bootstrap and starts the
242
+ detached helper `internal claude-endpoint-helper`; the helper claims and
243
+ records the registration in one Registry transaction and becomes Ready. Every
244
+ refusal along it, the helper's Ready, and the end of a Ready helper's serving
245
+ loop each write one record. A record carries only:
246
+
247
+ - `source`: the process that wrote it, `hook` or `helper`.
248
+ - `code`: `claude.registration.<reason>`, one reason of the closed table
249
+ below. A reason the table does not list, or does not allow for that source,
250
+ drops the record.
251
+ - `result`, `level`, and `kind`: a refusal before Ready is
252
+ `error`/`error`, `kind=runtime`. `ready` and the `ended-*` reasons are
253
+ `info`/`success` with no kind: the registration ran, and the `ended-*`
254
+ reason says how its lifetime ended.
255
+ - `duration_ms`: from that process's route entry to the append.
256
+ - `agent_uid` (`agent-…`) and `pane_uid` (`pane-…`): only once the Agent and
257
+ Pane matched the Registry, and omitted when not strictly shaped. The hook
258
+ sets them from the provider process check onward, once its bootstrap matched
259
+ the Pane and its Agent against the Registry, and for every helper start
260
+ refusal. The helper sets them only after its producer check passed: the
261
+ bootstrap then provably came from the live hook that made that Registry
262
+ match, so its UIDs are the Registry-matched ones.
263
+
264
+ The provider session id, the registration nonce (`registrationGeneration`),
265
+ the messaging token and socket path, lease and coordination socket paths,
266
+ process ids and start identities, argv, error text, and the working directory
267
+ are never recorded.
268
+
269
+ | code (`claude.registration.…`) | source | where |
270
+ | --- | --- | --- |
271
+ | `hook-arguments-present` | hook | the hook route got arguments |
272
+ | `registry-path-invalid` | hook | the activation Registry path is set but not the exact shape |
273
+ | `registry-unreadable` | hook, helper | the hook's Registry read failed, or the helper's read after its claim |
274
+ | `hook-input-unreadable` | hook | stdin failed to read or exceeded 64 KiB |
275
+ | `payload-not-session-start` | hook | the payload is not a parsable `SessionStart` |
276
+ | `pane-binding-mismatch` | hook | the Pane, activation generation, or Claude process binding does not match |
277
+ | `agent-mismatch` | hook | the Pane's Agent is not the running Claude Agent that owns it |
278
+ | `provider-process-mismatch` | hook | the hook's parent is not the bound Claude process |
279
+ | `messaging-credential-invalid` | hook | the messaging socket or token is missing or malformed |
280
+ | `session-id-embeds-credential` | hook | the session id contains the token or socket |
281
+ | `messaging-socket-unavailable` | hook, helper | the messaging socket failed inspection |
282
+ | `nonce-unavailable` | hook | no registration nonce could be generated |
283
+ | `authority-invalid` | hook, helper | the registration authority is not valid |
284
+ | `hook-identity-unavailable` | hook | the hook's own process identity is unavailable |
285
+ | `reply-tool-policy-unavailable` | hook | the reply tool policy could not be captured |
286
+ | `helper-executable-unavailable` | hook | the running binary could not be located |
287
+ | `helper-bootstrap-unavailable` | hook | the bootstrap could not be encoded |
288
+ | `helper-ack-pipe-unavailable` | hook | the acknowledgement pipe could not be created |
289
+ | `helper-start-failed` | hook | the helper process did not start |
290
+ | `helper-admission-unconfirmed` | hook | no acknowledgement arrived before the 3s deadline or EOF |
291
+ | `helper-arguments-invalid` | helper | the helper got arguments or inherited a messaging credential variable |
292
+ | `helper-ack-missing` | helper | fd 3 is absent |
293
+ | `helper-ack-not-pipe` | helper | fd 3 is not a pipe |
294
+ | `helper-input-unreadable` | helper | stdin failed to read, exceeded 64 KiB, or is not a bootstrap |
295
+ | `producer-mismatch` | helper | the parent is not the hook that built the bootstrap |
296
+ | `bootstrap-invalid` | helper | the bootstrap Registry path or token is invalid |
297
+ | `helper-identity-unavailable` | helper | the helper's own process identity is unavailable |
298
+ | `lease-unavailable` | helper | the private lease directory, socket, or its mode could not be set up |
299
+ | `coordination-unavailable` | helper | the coordination listener could not be opened |
300
+ | `lease-owner-unavailable` | helper | the lease owner receipt could not be written |
301
+ | `provider-process-gone` | helper | in the transaction, the Claude process is gone |
302
+ | `claim-refused-activation` | helper | in the transaction, the activation is no longer this helper's claim target |
303
+ | `claim-refused-competing` | helper | in the transaction, the same registration generation carries another session or lease |
304
+ | `claim-refused-newer` | helper | in the transaction, a newer SessionStart claimed another generation |
305
+ | `lock-timeout` | helper | the transaction gave up on the Registry lock deadline |
306
+ | `lock-acquire-failed` | helper | the transaction never ran for any other Store reason (the one fallback, below) |
307
+ | `registry-degraded` | helper | the Store refused the transaction on a degraded Registry |
308
+ | `registry-write-failed` | helper | the transaction's validation or durable write failed |
309
+ | `route-mismatch` | helper | the recorded route does not resolve to this registration |
310
+ | `dialogue-broker-unavailable` | helper | the dialogue broker could not start |
311
+ | `reply-tool-gate-unavailable` | helper | the reply tool gate could not be built |
312
+ | `stale-before-ack` | helper | the registration stopped being current before the acknowledgement |
313
+ | `ready` | helper | the acknowledgement byte was written |
314
+ | `ended-context-done` | helper | the serving loop's context ended |
315
+ | `ended-not-current` | helper | the serving loop found the registration no longer current |
316
+ | `ended-accept-failed` | helper | the lease listener failed |
317
+
318
+ A Claude session projmux did not launch writes nothing. The user-wide
319
+ SessionStart hook still runs for it, but with no activation Registry path set
320
+ at all (`PMX_INTERNAL_CLAUDE_REGISTRY_PATH` empty or unset) it is outside any
321
+ managed activation: the hook stops with the closed reason `unmanaged-session`,
322
+ which the recorder drops and every reader rejects, so it is never journaled
323
+ at any level. A set but malformed path is still `registry-path-invalid`. A
324
+ nested unmanaged Claude that inherited a managed Pane's activation environment
325
+ is refused as `provider-process-mismatch`, because its parent is not the bound
326
+ Claude process; that record names the managed Pane's `agent_uid` and
327
+ `pane_uid`, whose activation environment it inherited.
328
+
329
+ `lock-acquire-failed` is the one fallback of the transaction's
330
+ classification: the claim callback never ran, and the Store error is neither
331
+ a lock timeout nor a degraded Registry. That is a failed lock acquisition, or
332
+ a locked Registry read or recovery inspection failure the Store did not
333
+ classify as degraded. A failure after the callback ran is always
334
+ `registry-write-failed`.
335
+
336
+ The hook writes at most one record, and only once its attempt is over: at a
337
+ refusal before it started the helper, or after its acknowledgement wait
338
+ returned, which is `helper-admission-unconfirmed` or a start refusal. It never
339
+ appends between starting the helper and the helper's producer check, and a
340
+ confirmed admission writes nothing from the hook because the helper writes
341
+ `ready`. The helper writes `ready` exactly once, right after the
342
+ acknowledgement byte, and one more record when it returns: the refusal that
343
+ stopped it before Ready, or the `ended-*` reason after it. That last record is
344
+ appended only after the helper closed fd 3, so the hook's EOF never waits on
345
+ the journal. Appends are best effort: a failing or slow journal never changes
346
+ the hook's empty output, its nil result, or the helper's argv, environment,
347
+ and stdin. Every other event family rejects the `hook` and `helper` sources
348
+ and every `claude.registration.*` code.
349
+
350
+ Both routes are classified as the internal-only commands
351
+ `claude-endpoint-register` and `claude-endpoint-helper`, so the helper's slow
352
+ `registry.lock.acquisition` records carry `command=claude-endpoint-helper`.
353
+ Neither is state-changing and both always return nil, so neither writes a
354
+ `command.outcome`.
96
355
 
97
356
  projmux no longer emits `session-state.outcome` records. Project snapshots
98
357
  were removed, and the retained `internal tmux autosave-session-state` route is
@@ -313,7 +572,11 @@ Classification is intentionally conservative for mutation-capable interactive
313
572
  commands: opening session/project/settings/popup flows is treated as changing
314
573
  even when a user cancels. Explicit read variants (`internal status`, `list`,
315
574
  `get`, config rendering, plain welcome, and the diagnostics viewer) remain
316
- read-only. The successful automatic hook/poll paths `internal agent-hook ingest`, `attention
575
+ read-only. Read-only classification governs `command.outcome` only: a
576
+ read-only command whose locked Registry read waited for or held the lock for a
577
+ second or more still writes its `registry.lock.acquisition` measurement, except
578
+ Doctor, the support report, and the retired no-write argv, which never append.
579
+ The successful automatic hook/poll paths `internal agent-hook ingest`, `attention
317
580
  arm`, `attention clear`, `attention window`, `internal tmux autosave-session-state`, and
318
581
  `window record` are also read-only so high-frequency operation does not append
319
582
  to the journal; an error from any of them still records exactly one safe error
package/docs/release.md CHANGED
@@ -34,6 +34,10 @@ then builds the linux/darwin × amd64/arm64 matrix and uploads tarballs to the
34
34
  drafted release (`gh release upload --clobber`). `Build Release` depends on the
35
35
  aggregate rather than on individual shards.
36
36
 
37
+ Each tarball holds the binary, `README.md`, `README-ko.md`, `LICENSE`, and
38
+ `THIRD_PARTY_NOTICES`. The last is generated by `make notices` and committed;
39
+ `make test` fails when it no longer matches what the binary links.
40
+
37
41
  Do not add hardcoded notes back to that workflow. release-please owns the
38
42
  notes.
39
43
 
@@ -400,6 +400,12 @@ platform with no executable link to read declines to drain rather than draining
400
400
  on a guess: `defaultProjmuxImageReplaced` answers false on darwin, and the
401
401
  absence is stated by this table's `unsupported-platform` row.
402
402
 
403
+ A native observer whose first bind receives a drain refusal leaves the Pane on
404
+ hook fallback and keeps retrying that exact Agent and thread. It writes no
405
+ composite authority until a current broker binding opens. When the old broker
406
+ finishes its accepted work and exits, the observer can bind to the replacement
407
+ and publish authority; messages remain refused while the binding is absent.
408
+
403
409
  ### The install pass
404
410
 
405
411
  `projmux internal install-replace` runs as a step of `make install`, immediately
@@ -21,6 +21,7 @@ projmux/
21
21
  state/
22
22
  tools/
23
23
  gendocs/
24
+ gennotices/
24
25
  ui/
25
26
  picker/
26
27
  pickercompat/
@@ -40,6 +41,8 @@ projmux/
40
41
  - `internal/core` contains product behavior that should be testable without tmux.
41
42
  - `internal/tools/gendocs` is a build-time `main` package, not part of the shipped
42
43
  binary. `make docs` runs it to regenerate `docs/cli.md` from the command manifest.
44
+ - `internal/tools/gennotices` is a build-time `main` package too. `make notices` runs it
45
+ to regenerate `THIRD_PARTY_NOTICES` from the modules `./cmd/projmux` links.
43
46
  - `internal/integrations/tmux` should be the only place that knows tmux command strings and output formats.
44
47
  - `internal/ui/picker` and `internal/ui/projmuxpicker` own native picker behavior.
45
48
  - `internal/ui/pickercompat` is an internal compatibility option/result shape for older app call sites. It is not a runtime backend; product code should route through the native picker.
@@ -145,6 +145,10 @@ PROJMUX_RESOURCE_PROJECT_FALLBACK_SMOKE=1 go test \
145
145
  ./internal/integrations/tmux
146
146
  ```
147
147
 
148
+ `PROJMUX_REAL_TMUX_STRICT=1` also turns on both isolated smokes, and the CI
149
+ Unit Tests job sets it, so they run there and a missing tmux fails them
150
+ instead of skipping. The read-only smoke above stays opt-in.
151
+
148
152
  ## Phase 1 inspector
149
153
 
150
154
  The popup retains warming/partial/unavailable and overage states, renders RSS
@@ -17,9 +17,12 @@ registered Project is not asked: it opens fresh, which registers it.
17
17
  the exact old Project UID and its Window/Pane/Agent counts. Declining returns
18
18
  to the startup rows and writes nothing. It does not archive or retain the old
19
19
  generation. Its new Window's first Pane follows the saved launch default
20
- (`tmux-ai-split-mode`), exactly as a Window created from the UI does: the
21
- choice is made before anything is cleared. The first open of an unregistered
22
- root, which resolves to the same fresh start, behaves the same.
20
+ (`tmux-ai-split-mode`, then the central `ai-new-window-mode`, then
21
+ `selective`; see
22
+ [New AI Window Default](configuration.md#new-ai-window-default)), exactly as
23
+ a Window created from the UI does: the choice is made before anything is
24
+ cleared. The first open of an unregistered root, which resolves to the same
25
+ fresh start, behaves the same.
23
26
 
24
27
  Esc/cancel returns to Projects; it is not an action row. Picker failure falls
25
28
  back to the non-destructive `Continue project` action.
@@ -77,10 +80,11 @@ each successful result has exactly one Project claiming the root.
77
80
  The saved launch default is used only when the open carries the exact client
78
81
  that pressed the row. The order is:
79
82
 
80
- 1. Ask. A picker mode (`selective`, the unset default, or `resume`) opens its
81
- picker on the Pane the row was pressed in, before the old layout is cleared
82
- or the new Session exists; a provider mode and `shell` are already the
83
- answer and open nothing.
83
+ 1. Ask. A picker mode (`selective`, the default when neither
84
+ `tmux-ai-split-mode` nor `ai-new-window-mode` holds a valid mode, or
85
+ `resume`) opens its picker on the Pane the row was pressed in, before the
86
+ old layout is cleared or the new Session exists; a provider mode and
87
+ `shell` are already the answer and open nothing.
84
88
  2. Clear the layout and create the new Session with its one shell Pane.
85
89
  3. Fill it: an Agent answer is created in that Window first, and the shell is
86
90
  then removed through the canonical Pane delete.
package/docs/statusbar.md CHANGED
@@ -51,10 +51,11 @@ row 1 [#S] #{pane_current_path} <git> CPU 12% MEM 41%  %H:%M
51
51
  mouse-target context resolves the clicked window directly. All
52
52
  other ranges fall through to `run-shell projmux internal statusbar click
53
53
  ...`, which dispatches by range id. The in-config short-circuit
54
- is required because `#{mouse_window}` is empty for window-list
55
- clicks on tmux 3.4+, so a `run-shell` handler can't recover the
56
- target after the fact — the Go dispatcher's
57
- `isWindowListRangeToken` fallback is now defense-in-depth only.
54
+ is required because tmux has no format variable for the clicked
55
+ window: it is only reachable through tmux's internal mouse target
56
+ (`-t =`), which does not survive `run-shell`, so a handler can't
57
+ recover the target after the fact — the Go dispatcher's
58
+ `window|<idx>` index path is defense-in-depth only.
58
59
  Each window tab reserves a one-cell live pane attention prefix from
59
60
  `projmux attention window #{window_id}` before the index. AI panes use the
60
61
  semantic `@projmux_ai_badge_kind` first: approval/input-required panes use
@@ -72,11 +73,12 @@ row 1 [#S] #{pane_current_path} <git> CPU 12% MEM 41%  %H:%M
72
73
  response-complete live badge, including stale `@projmux_ai_state=waiting`
73
74
  fallback state; action-required and in-progress live badges remain visible.
74
75
  Window-list badges and app pane-border badges use the same semantic priority,
75
- with display style controlled by Settings > Appearance > AI badge style and persisted in
76
- `~/.config/projmux/ai-badge-style`. The default is `dot`; `emoji` renders
77
- `⏳` for approval/input-required, `✅` for response-complete, and `🔄` for
78
- in-progress. `off` (also accepted as `minimal` when read from disk) preserves
79
- the same spacing without drawing a marker.
76
+ with display style controlled by Settings > Appearance > AI badge style and
77
+ persisted in `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/ai-badge-style`.
78
+ The default is `dot`; `emoji` renders `⏳` for approval/input-required,
79
+ `✅` for response-complete, and `🔄` for in-progress. `off` (also accepted as
80
+ `minimal` when read from disk) preserves the same spacing without drawing a
81
+ marker.
80
82
  The session, pwd, and git segments on this row are wrapped
81
83
  in `#[range=user|<id>]` ranges and dispatched through the projmux
82
84
  handler. The standalone config also wraps the right-side `projmux`
@@ -244,11 +246,12 @@ A single tmux bind handles both lines:
244
246
  ```tmux
245
247
  bind-key -n MouseDown1Status if-shell -F "#{==:#{mouse_status_range},window}" \
246
248
  { select-window -t = } \
247
- { run-shell "'<projmux>' internal statusbar click \"#{mouse_status_range}\" --client \"#{client_tty}\" --mouse-window \"#{mouse_window}\"" }
249
+ { run-shell "'<projmux>' internal statusbar click \"#{mouse_status_range}\" --client \"#{client_tty}\"" }
248
250
  ```
249
251
 
250
252
  `MouseDown1Status` fires from any line of a multi-line status bar with
251
- `#{mouse_status_range}` resolving to the range under the cursor.
253
+ `#{mouse_status_range}` resolving to the range under the cursor. The click
254
+ command passes only `#{mouse_status_range}` and `#{client_tty}`.
252
255
 
253
256
  ## Usage element drop order
254
257
 
@@ -403,10 +406,11 @@ Internal notify commands use `NotifySidebar:*` IDs in `keymap.toml`; runtime
403
406
  footers render key guides from the merged keymap and prefer the default alias
404
407
  when it is still configured.
405
408
 
406
- Empty `#{mouse_status_range}` (a click on whitespace) falls through to
407
- `select-window -t @<mouse_window>` when `--mouse-window` is non-empty,
408
- otherwise it is a no-op. Unknown user range ids are non-specialized
409
- placeholder surfaces and no-op until a handler is wired into the dispatcher.
409
+ Empty `#{mouse_status_range}` (a click on whitespace) is a no-op. Unknown
410
+ user range ids are non-specialized placeholder surfaces and no-op until a
411
+ handler is wired into the dispatcher. `--mouse-window <v>` is accepted for
412
+ compatibility with bindings generated by older releases (live on a running
413
+ tmux server until `projmux config apply`) and ignored.
410
414
 
411
415
  ## Keyboard chord
412
416
 
@@ -474,14 +478,14 @@ git-provider, or bell icon), or `emoji`. Git branch decoration follows
474
478
  fox-style mark, and other remotes use a generic git branch mark. Saving
475
479
  visibility or Resources regenerates the app/standalone config and source-loads
476
480
  the generated app config when Settings is running inside tmux. The legacy
477
- `~/.config/projmux/statusbar-decoration` and
481
+ `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/statusbar-decoration` and
478
482
  `@projmux_statusbar_decoration` remain fallback defaults for older configs.
479
483
  Settings > Theme controls the bottom status bar background through
480
484
  `status_background`; `surface` controls popup and native frame backgrounds.
481
485
 
482
- Resources uses `~/.config/projmux/live-resources` as its single saved enabled
483
- state; there is no separate Resources visibility file or duplicate toggle. CPU
484
- sampling state is an internal,
486
+ Resources uses `${XDG_CONFIG_HOME:-$HOME/.config}/projmux/live-resources` as
487
+ its single saved enabled state; there is no separate Resources visibility file
488
+ or duplicate toggle. CPU sampling state is an internal,
485
489
  atomically replaced file under `${XDG_STATE_HOME:-~/.local/state}/projmux/`
486
490
  and is not a user-edited setting.
487
491