projmux 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/upgrading.md CHANGED
@@ -44,15 +44,253 @@ still requires an explicit `PROJMUX_INSTALLER=github-release`.
44
44
 
45
45
  ## Behavior Changes
46
46
 
47
+ ### Registry schema v2 final Window anchors
48
+
49
+ The unreleased intermediate schema-v2 Window field `primaryPaneRef` has been
50
+ replaced by the final v2 shape:
51
+
52
+ - `anchorPaneRef` is required and names a same-Window shell or managed Agent
53
+ Pane.
54
+ - `defaultShellPaneRef` is optional; when present it names a direct
55
+ Window-owned shell Pane.
56
+
57
+ The on-disk `schemaVersion` remains `2` because the intermediate shape was not
58
+ published. A schema-v1 Registry migrates directly to these fields. An
59
+ intermediate-v2 Registry is identified by raw field presence and normalized
60
+ under the Registry lock. The migration publishes an exact mode-0600 backup and
61
+ a checksum/repair report before staging and atomically replacing the Registry.
62
+ Mixed legacy/final fields, mixed Window authorities, dangling or cross-Window
63
+ anchors, and invalid default shells fail closed without changing the source.
64
+ Repeating migration on final-v2 is a byte-level no-op.
65
+
66
+ The final writer never emits `primaryPaneRef`. A matching intermediate
67
+ pre-release validator therefore sees final-v2 as invalid because its required
68
+ legacy authority is absent. Not every old read surface necessarily runs that
69
+ validator: an old command may ignore the new fields and return a partial view.
70
+ That is evidence of incompatibility, not permission to proceed. Operators must
71
+ refuse a binary-only downgrade before installation and must not permit the old
72
+ binary to write final-v2 bytes. To roll back during the prerelease window, stop
73
+ the final writer, restore the exact pre-normalization Registry backup, and
74
+ restore the matching intermediate binary as a pair. Snapshots are not migration
75
+ or rollback inputs and are never rewritten by this normalization.
76
+
77
+ Rollback rehearsal is byte-oriented: record the backup path, mode, SHA-256,
78
+ and matching pre-release binary revision; stop every final-v2 writer; atomically
79
+ restore those exact intermediate-v2 bytes; verify their checksum; install the
80
+ recorded matching binary; then run its read-only validation before permitting a
81
+ write. Installing only the intermediate binary while leaving final-v2 Registry
82
+ bytes in place is deliberately rejected and is not a rollback procedure.
83
+
84
+ ### `create` is resource-backed on every spelling
85
+
86
+ **Breaking.** `create pane`, `create agent`, and the `create
87
+ codex|claude|antigravity` shortcuts no longer have two product models behind the
88
+ presence of `--project`. Previously an invocation *with* `--project` created
89
+ Registry resources and materialized them detached, while an invocation *without*
90
+ it ran a runtime-only split of the current tmux window that created no Projmux
91
+ resource and moved the client. That second model is gone.
92
+
93
+ What changes for an existing invocation:
94
+
95
+ | Invocation | Before | Now |
96
+ | --- | --- | --- |
97
+ | `create codex --placement right` inside a managed Project | Runtime-only split of the current tmux window; no Registry resource; the client followed the new pane | Creates an Agent and its managed Pane below the **active managed Project's active Window**, anchored on the active Pane, materialized detached; the client does not move |
98
+ | `create codex -w hi --create-window` | `flag provided but not defined: -w` | Creates Window `hi` under the active managed Project, then the Agent and its managed Pane inside it |
99
+ | `create pane --placement down` inside a managed Project | Runtime-only shell split | Creates a Pane resource below the active Window and splits it detached |
100
+ | Any `create` inside Home, a control session, an unattributed or foreign pane | Runtime-only split | Exit `2` naming `--project`, with zero Registry writes and zero tmux mutations |
101
+ | Any `create` from outside tmux with no `--project` | Runtime-only split against the default server | Exit `2` naming `--project`; no server is probed |
102
+ | Any `create` with an explicit `--project` | Resource-backed | Unchanged |
103
+ | `create pane -o uid\|name\|ref\|metadata\|json` with no `--project` | Exit `2` saying the compatibility split creates no resource | Works: the projections resolve the created resource |
104
+
105
+ Two consequences are worth calling out:
106
+
107
+ - **The client no longer follows the new pane.** Every create is detached. Use
108
+ `projmux focus pane uid:<uid>` — or `-o pane-id` and your own `select-pane` —
109
+ when you want to move there.
110
+ - **Panes created this way are now managed.** They appear in `get panes`,
111
+ in the primary navigation surface, and in `reconcile resources`. Nothing
112
+ adopts the runtime-only panes created by older releases; they stay visible in
113
+ the Runtime diagnostics surface and are never imported automatically.
114
+
115
+ The default keybindings whose bodies are `create codex|claude|pane --placement
116
+ right|down` keep their spelling and their "split where I am" meaning, and now
117
+ produce Registry resources. `internal agent-pane launch-default` — the saved
118
+ default split mode, bound to Ctrl-Shift-R/L in the terminal adapters — kept its
119
+ spelling and its meaning, and now produces the same Registry resources: see
120
+ [Every split surface is Registry-first](#every-split-surface-is-registry-first).
121
+
122
+ The generated tmux config changed shape to support that. Every projmux
123
+ `run-shell` binding is now rendered as `run-shell "TMUX_PANE=#{pane_id} <bin>
124
+ ..."`, because tmux exports `$TMUX` to a `run-shell` child but never
125
+ `$TMUX_PANE`, and without the exact pane a binding cannot tell where it was
126
+ pressed. `projmux config apply` — which `make install` and `projmux update
127
+ apply` run for you — rewrites the generated file. If you hand-copied a projmux
128
+ `run-shell` line into your own `~/.tmux.conf`, add the same prefix.
129
+
130
+ If you need a raw, unmanaged tmux split, use tmux itself (`split-window`).
131
+ projmux does not spell that as a resource verb.
132
+
133
+ ### Every split surface is Registry-first
134
+
135
+ The saved-default split binding, the `Alt-7` provider picker, and the resume
136
+ picker used to open a pane by calling tmux's `split-window` directly. Those panes
137
+ were runtime-only: no uid, no owner Window, no Agent row, and no row in `get
138
+ panes` or the primary navigation surface. They now go through the same canonical
139
+ `create` route the `create codex|claude|pane` bindings and typed commands use, so
140
+ every pane a Projmux surface opens is a managed resource.
141
+
142
+ What changes for you:
143
+
144
+ - **Panes from the picker and the saved default are managed.** They appear in
145
+ `get panes`, `get agents`, the primary navigation surface, and `reconcile
146
+ resources`, and they are deleted with `projmux delete pane|agent`. Panes those
147
+ surfaces created in older releases are not adopted; they stay visible in the
148
+ Runtime diagnostics surface.
149
+ - **A launch needs a resolvable Project.** These surfaces create resources, so
150
+ they follow [Create scope](cli-guide.md#create-scope) like any other create. A
151
+ UI action taken outside a managed Project fails with the same
152
+ `pass --project <ref>` guidance instead of opening an unmanaged pane.
153
+ - **The legacy `ai split` route is gone**, along with its `--agent`,
154
+ `--force-agent`, and `--print-pane-id` flags. The `ai` root itself was retired
155
+ earlier; nothing reaches that handler now. Automation that wants the new pane's
156
+ handle uses `-o pane-id` on a canonical create — see
157
+ [AI Agent Shortcuts](ai-agent-shortcuts.md).
158
+ - **The AI title watcher no longer starts automatically.** The canonical create
159
+ route never started it, and these surfaces now behave the same way. Pane
160
+ titles, topics, and status come from provider hooks and from `projmux agent
161
+ topic`. `projmux internal agent-hook watch-title` still exists for the hook
162
+ contract.
163
+
164
+ If you need a raw, unmanaged tmux split, use tmux itself (`split-window`).
165
+
166
+ ### One lifecycle trigger route
167
+
168
+ The generated tmux config used to install two lifecycle routes: `internal tmux
169
+ reconcile-bindings` on `after-new-window`/`after-split-window`, and `internal tmux
170
+ release-dead-agent-panes` on `pane-exited`/`after-kill-pane`. Both answered the
171
+ same question with a different subset of one reconciliation, so a pane exit
172
+ inside a session that had also just gained a window paid for two registry
173
+ transactions to reach the state one pass reaches.
174
+
175
+ All four hooks now invoke one hidden route, `internal tmux converge
176
+ --socket-path <path> --session <id> --reason <config-apply|runtime-created|runtime-exited>`,
177
+ and so does `projmux config apply`. At most one convergence worker runs per exact
178
+ tmux server, so a burst of hooks costs one pass instead of one per event. Which
179
+ hooks each config installs is unchanged: the app config carries all four, and the
180
+ standalone snippet carries the two pane-exit hooks only, so a raw `new-window` in
181
+ a session projmux does not own still stays unmanaged.
182
+
183
+ `projmux config apply` — which `make install` and `projmux update apply` run for
184
+ you — rewrites the generated file. A tmux server still holding config from an
185
+ older binary keeps invoking the two retired routes, which now fail; the hooks
186
+ guard every invocation with `>/dev/null 2>&1`, so nothing surfaces, and the next
187
+ `config apply` replaces them. If you hand-copied a projmux `set-hook` line into
188
+ your own `~/.tmux.conf`, re-copy it from `projmux config render standalone`.
189
+
190
+ ### Discovery no longer registers Projects
191
+
192
+ **Breaking.** A directory found under a discovery root used to become a Registry
193
+ Project on its own. Any mutation route ran a reconcile prelude that walked the
194
+ configured workdirs and registered every child it did not recognize, so a single
195
+ `projmux create pane` in one repository could add a Project for every sibling
196
+ directory beside it. Scanning a directory is now a scan and nothing else.
197
+
198
+ Three collections, three authorities:
199
+
200
+ | Thing | What it is | What it is not |
201
+ | --- | --- | --- |
202
+ | Workdirs / `PROJMUX_MANAGED_ROOTS` / `PROJMUX_PROJDIR` | Scan roots to look inside | Not a statement that anything inside them is a Project |
203
+ | A discovered child | An unregistered candidate | Not managed identity, however often it is scanned |
204
+ | A Registry Project | Managed identity, with a stable uid | Not derived from a path, a name, or a scan |
205
+
206
+ What registers a Project now:
207
+
208
+ ```sh
209
+ projmux create project --root /abs/path/to/repo
210
+ ```
211
+
212
+ …or opening that one candidate from the Projects sidebar, which performs the same
213
+ registration for that exact path. Both are idempotent: a root an existing Project
214
+ already claims writes nothing at all. Sibling candidates under the same discovery
215
+ root stay unregistered.
216
+
217
+ What changes for an existing invocation:
218
+
219
+ | Invocation | Before | Now |
220
+ | --- | --- | --- |
221
+ | `create window --project <name>` where `<name>` is a discovered-but-unregistered directory | The reconcile prelude registered it (and every other discovered child) first, then the create succeeded | Exits non-zero naming the exact `--root` to register, or open it once from the sidebar |
222
+ | Any mutation route, with N children under a scan root | Up to N Projects appeared | Zero Projects appear |
223
+ | `switch open <unregistered directory>` | Opened a session; the Project appeared later, as a side effect of some unrelated mutation | Registers that one path, then opens it exactly as before |
224
+
225
+ If you relied on the old behavior to populate the Registry, register the roots
226
+ you actually want once:
227
+
228
+ ```sh
229
+ for root in ~/src/*; do projmux create project --root "$root"; done
230
+ projmux get projects
231
+ ```
232
+
233
+ ### Pins are typed, and migrate on request
234
+
235
+ **Breaking.** `~/.config/projmux/pins` held one absolute path per line, which
236
+ could not say whether the path was a Project. It is now a typed envelope:
237
+
238
+ ```text
239
+ projmux-pins v2
240
+ project proj-kwo4qozry2sr2ycij2g45zyvam
241
+ candidate /home/dev/src/scratch
242
+ ```
243
+
244
+ A `project` pin references a Registry Project uid; its displayed root and name
245
+ are projected from the Registry on every read, so the pin survives a rebind, a
246
+ rename, and a missing root. A `candidate` pin references a path that no Project
247
+ claims and stays a path.
248
+
249
+ Nothing migrates behind your back. Reads project a pre-v2 file in memory, so the
250
+ sidebar looks the same before and after; the file is rewritten only when you ask:
251
+
252
+ ```sh
253
+ projmux pin project migrate --dry-run # report only
254
+ projmux pin project migrate # store the typed form
255
+ ```
256
+
257
+ Per line, migration has exactly three outcomes:
258
+
259
+ | Registry Projects claiming the path | Result |
260
+ | --- | --- |
261
+ | exactly one | becomes that Project's `project <uid>` pin |
262
+ | none | stays a `candidate <path>` pin |
263
+ | two or more | the **entire** migration is refused; the pin file and the Registry keep their bytes, and the message names the repair |
264
+
265
+ Repair an ambiguous pin with `projmux rebind project` so one Project claims the
266
+ path, or pin the Project you meant directly with `projmux pin project add
267
+ uid:<uid>`, then re-run the migration. A corrupt file, or one written by a newer
268
+ projmux, is refused rather than partially read.
269
+
270
+ `pin project add|remove|toggle <dir>` is unchanged in argv and now resolves to a
271
+ typed pin under the same rule (one Project → managed, none → candidate, more than
272
+ one → refused). `pin project list` gained a kind column and a `--kind
273
+ project|candidate` filter; the first mutation after an upgrade migrates the file
274
+ first, so it can refuse for the reasons above.
275
+
276
+ Settings > Projects shows the three collections separately: **Additional
277
+ discovery roots** (scan roots), **Pinned Projects** (managed, by uid, with rebind
278
+ and unpin), and **Candidate Pins** (unregistered paths, with register and unpin).
279
+ On Windows the discovery roots stay OS-native paths; drive-letter, case, and
280
+ separator differences are folded only when matching a candidate or migrating a
281
+ legacy pin, never to mint or merge a Project uid.
282
+
47
283
  ### Legacy compatibility routes removed
48
284
 
49
285
  This release removes the human-facing compatibility argv and old
50
286
  pre-namespace internal aliases listed in the [retirement ledger](legacy-cli-retirement.md).
51
- Removed public argv exits 2 with its exact canonical replacement, no stdout,
52
- and no side effect. Removed internal aliases (`tmux`, `status`, `statusbar`,
53
- `preview`, `session-popup`, `key-broker`, and `popup-wait-key`) are unknown
54
- top-level commands and exit 1. Use `internal ...` for generated plumbing and
55
- `config render|apply` for public configuration work.
287
+ Rejected compatibility argv below a surviving mixed root exits 2 with its exact
288
+ canonical replacement, no stdout, and no side effect. Fully removed human roots
289
+ (`current`, `kill`, `notify`, `sessions`, `session-state`, `tag`, `upgrade`, and
290
+ `usage`) and removed internal aliases (`tmux`, `status`, `statusbar`, `preview`,
291
+ `session-popup`, `key-broker`, and `popup-wait-key`) are unknown top-level
292
+ commands and exit 1. Use `internal ...` for generated plumbing and `config
293
+ render|apply` for public configuration work.
56
294
 
57
295
  The mixed roots retain only `attach project`, `focus project|window|pane`, `pin
58
296
  project`, and `prune project|snapshot`. Shortcuts and singular/plural resource
@@ -75,7 +75,31 @@ floor.
75
75
 
76
76
  ### Codex (`internal/core/usage/adapters/codex`)
77
77
 
78
- Local rollout JSONL parser. No network calls.
78
+ The native Codex app-server account API is primary:
79
+
80
+ - An explicit `projmux agent usage` request reuses the app-server
81
+ ensure-ready lifecycle, then opens one initialized official stdio-proxy
82
+ connection and calls `account/rateLimits/read` with a bounded deadline.
83
+ Automatic HUD refreshes probe/read an already-running daemon but do not gain
84
+ daemon-start authority.
85
+ - When `rateLimitsByLimitId` is present it is the authoritative multi-bucket
86
+ view; the backward-compatible `rateLimits` mirror is not duplicated. Each
87
+ bucket preserves its map key, nullable `limitId`, nullable `limitName`,
88
+ primary/secondary slot, percentage, nullable reset, and nullable cadence.
89
+ `300` and `10080` minutes project to `5h` and `weekly`. Missing or unknown
90
+ positive cadences remain lossless `quota` rows instead of disappearing.
91
+ - `account/rateLimits/updated` is merged sparsely into the just-read native
92
+ snapshot during one bounded event-settle window. Null account metadata does
93
+ not clear a prior label or identity. The event-refreshed rows then use the
94
+ same Manager throttle, replace, store, and last-known-good path as a read.
95
+ - Validation is row-level. A malformed bucket/window is dropped with a bounded,
96
+ field-only warning while valid siblings remain native. Native and rollout
97
+ rows are never combined in one invocation.
98
+
99
+ When app-server is unavailable, unsupported, disconnected, timed out, or the
100
+ account exposes no supported rate-limit bucket (including API-key-style
101
+ accounts), the adapter invokes the existing newest-rollout collector exactly
102
+ once without attempting login, logout, token refresh, or config writes:
79
103
 
80
104
  - Walks `${HOME}/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl`
81
105
  newest-first by mtime (NOT filename — Dropbox-synced rollouts can
@@ -99,8 +123,10 @@ Local rollout JSONL parser. No network calls.
99
123
  dropped and reported rather than projected as a genuine `0%`.
100
124
 
101
125
  Codex shares the manager's default `30s` throttle (no
102
- `ThrottleHinter`). It does not implement `BackoffStater` — local-only
103
- read.
126
+ `ThrottleHinter`) and does not implement `BackoffStater`. A successful
127
+ rollout fallback row records `source=rollout` plus the closed fallback reason.
128
+ If neither lane produces rows, Manager preserves the previous source/value and
129
+ adds a closed `stale_reason` to the last-known-good row.
104
130
 
105
131
  ### Antigravity (`internal/core/usage/adapters/antigravity`)
106
132
 
@@ -163,13 +189,17 @@ For `--model all`, calls `Manager.Collect` (or `ForceCollect` with
163
189
  Enabled agents, filters by window, and renders the tab-aligned table:
164
190
 
165
191
  ```
166
- MODEL WINDOW PCT RESETS_AT RESET_IN STALE
192
+ MODEL WINDOW PCT RESETS_AT RESET_IN STALE SOURCE REASON
193
+ codex 5h/codex · General 12% 2026-05-07T14:00:00+09:00 - app-server
167
194
  claude 5h 80% 2026-05-07T14:00:00+09:00 -
168
195
  antigravity quota/gemini-weekly 6% 2026-07-06T16:50:32+09:00 560580s
169
196
  claude quota/group-redacted · Model Redacted Alpha 38% 2031-02-03T15:05:06+09:00 - *
170
197
  ```
171
198
 
172
- `STALE` is `*` when `now - UpdatedAt > 10m`. A `--json` payload returns
199
+ `SOURCE`/`REASON` are included when source-aware rows are present. Native
200
+ Codex rows report `app-server`; fresh rollout fallback and retained
201
+ last-known-good rows report their closed fallback/stale reasons. `STALE` is
202
+ `*` when `now - UpdatedAt > 10m`. A `--json` payload returns
173
203
  the `Snapshot` array; if any adapter is in backoff, the wrapper
174
204
  `{snapshots, backoff: {model: {until, consecutive}}}` is emitted
175
205
  instead. A backoff note is appended to the human table:
@@ -215,6 +245,17 @@ excluded; only its aggregate official `5h` and `weekly` rows reach the HUD.
215
245
  The Settings provider list consumes `aiprovider.UsageSupported()` order, but a
216
246
  window toggle exists only when this same projection seam declares it. A future
217
247
  provider or an opaque bucket cannot manufacture a window row.
248
+ For native Codex multi-bucket rows, the exact `codex` bucket wins the HUD
249
+ projection, then the legacy empty bucket, then lexical bucket order. The HUD
250
+ compact identity is derived from that same row: a healthy authoritative
251
+ `app-server` row is simply `Codex`, a fresh rollout row is
252
+ `Codex [fallback]`, and a retained last-known-good row is `Codex [stale]`.
253
+ Blank, malformed, or future non-stale provenance also fails conservatively to
254
+ the existing `[fallback]` identity rather than looking native or expanding the
255
+ compact vocabulary. The exact raw source and closed fallback/stale reason stay
256
+ in `agent usage --model codex` table/JSON output and in
257
+ `projmux diagnostics log --component usage`; compact labels never replace
258
+ those fields.
218
259
 
219
260
  Settings > Appearance > Status Bar > Agent Usage HUD can hide the whole HUD,
220
261
  a provider, or one supported window. Parent off states preserve child saved
@@ -250,8 +291,10 @@ The `~` / `~~` markers are the legacy stale vocabulary, carried inside the
250
291
  indicator while the age text is still rendered and glued to the label once the
251
292
  drop order has shed it. Staleness stays muted however far the segment has
252
293
  degraded: warning and critical colors are reserved for usage thresholds, not
253
- cache age. Codex opts out of the indicator because the rollout file is always
254
- near-current (no throttle gap to report).
294
+ cache age. A healthy Codex row does not need a cosmetic age indicator, while a
295
+ retained Codex last-known-good row opts in and carries the compact `[stale]`
296
+ identity. Its exact closed stale reason remains available on the full table,
297
+ JSON, and diagnostics surfaces.
255
298
 
256
299
  The statusbar usage **popup**'s sync line is a different surface with a
257
300
  different meaning (last successful collect, 60s amber threshold) and is
@@ -284,10 +327,13 @@ When a collection fails, the failure is visible in three places:
284
327
  projmux diagnostics log --component usage --tail 20
285
328
  ```
286
329
 
287
- The row carries the provider and a closed failure enum and nothing else:
330
+ The row carries the provider and closed source/failure enums and nothing else:
288
331
  `collect-failed` (whole-adapter failure, `level=error`) or `rows-skipped`
289
- (partial failure, `level=info`). A successful collection writes no row at
290
- all. Identical `(provider, failure)` tuples are recorded at most once per
332
+ (partial failure, `level=info`). Codex rollout fallback records
333
+ `source=rollout` plus its closed fallback reason; retained data records
334
+ `source=last-known-good` plus its closed stale reason. A healthy native
335
+ collection writes no row at all. Identical `(provider, source, failure)`
336
+ tuples are recorded at most once per
291
337
  process run — the same suppression the notify/focus recorder uses — so a
292
338
  repeating failure cannot flood the bounded journal. Journal writes are
293
339
  best-effort and never change what the usage command returns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projmux",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "tmux project session manager",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/crevissepartners/projmux#readme",
@@ -28,9 +28,9 @@
28
28
  "package:npm:pack": "scripts/package-npm.sh --pack"
29
29
  },
30
30
  "optionalDependencies": {
31
- "@projmux/linux-x64": "0.12.2",
32
- "@projmux/linux-arm64": "0.12.2",
33
- "@projmux/darwin-x64": "0.12.2",
34
- "@projmux/darwin-arm64": "0.12.2"
31
+ "@projmux/linux-x64": "0.13.0",
32
+ "@projmux/linux-arm64": "0.13.0",
33
+ "@projmux/darwin-x64": "0.13.0",
34
+ "@projmux/darwin-arm64": "0.13.0"
35
35
  }
36
36
  }