projmux 0.14.2 → 0.15.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.
@@ -0,0 +1,339 @@
1
+ # Heterogeneous Dialogue Canary
2
+
3
+ This opt-in Linux canary exercises current Claude 2.1.263 and Codex 0.153.2
4
+ through the public reply-only activation. The required selectorless E2E remains
5
+ one offline L20 scenario. An actual provider run requires the roadmap owner's
6
+ reviewed R1 plan and explicit execution gate; these scripts do not grant it.
7
+ The first actual run covers qualification followed by one idle request. It does
8
+ not certify active-tool overlap, human overlap, recovery or installed behavior.
9
+
10
+ ## Public reply-only activation
11
+
12
+ On Linux, explicitly opt one next activation into the restricted transport profile:
13
+
14
+ ```sh
15
+ projmux create agent --provider claude --dialogue-reply-only --project <project> --window <window>
16
+ # After normal exit, resume the same Agent UID and provider conversation:
17
+ projmux agent resume uid:<same-agent> --dialogue-reply-only
18
+ # From the exact current Codex source activation:
19
+ projmux agent message qualify uid:<claude-agent> --confirm-isolated-provider-push -o json
20
+ ```
21
+
22
+ The flag changes only that activation. It does not persist an Agent default,
23
+ change a running Agent, or enable ordinary Claude sessions implicitly. Each new
24
+ activation requires a fresh opt-in and exact-version qualification. Without
25
+ this profile or another explicitly validated execution guard, inbound eligibility
26
+ remains unqualified; an ordinary `integrate`/resume alone does not create the guard.
27
+
28
+ This headless profile starts one fixed `Reply READY.` turn, enables only Bash,
29
+ and enforces its exact public reply command through the pinned execution gate.
30
+ It uses restricted mode, no Chrome or slash commands/prompt suggestions, empty
31
+ settings sources, strict empty MCP configuration and owned exec-form hooks.
32
+ The public init must confirm Bash alone, no MCP/plugins and the current version.
33
+ Normal SessionStart, UserPromptSubmit and Stop state callbacks retain badge
34
+ ownership; Stop never publishes a peer reply. Provider session persistence stays
35
+ on so a normal same-UID resume can retain its conversation.
36
+
37
+ The pane is an activation status/EOF surface. Typed terminal text is discarded;
38
+ it is **not** model-visible human input. Ctrl-D closes provider stdin and lets the
39
+ current turn finish before normal exit. The owned observer validates public
40
+ stdout/stderr in memory and forwards only bounded shape/effect assertions to
41
+ the existing coordination helper. Raw text, reasoning, signatures and messaging
42
+ credentials are never evidence files. EOF, stderr, unknown output or observer
43
+ replacement invalidates inbound and issued reply actions. The supervisor requires
44
+ exact observer birth and pidfd exit readiness before removing its private profile;
45
+ uncertain exit retains the profile and reports cleanup failure.
46
+
47
+ The public qualifier obtains fresh observed init evidence from that exact helper
48
+ when `--evidence` is omitted. A supplied evidence file cannot override a live
49
+ profile's missing/invalid observer. Qualification still requires the current
50
+ Codex source, confirmation and a broker-owned explicit reply challenge; observed
51
+ init alone does not admit general inbound traffic. Actual model execution,
52
+ human overlap, active-tool ordering and installed same-UID recovery remain
53
+ separate R2 evidence requirements; these deterministic tests do not prove them.
54
+
55
+ The existing helper's read-only profile evidence also reports bounded observed
56
+ model tool IDs, their parsed original request/target, paired result and public
57
+ reply ref. It separately compares the guard's exact selection and the successful
58
+ broker commit. An issued/consumed permit or a model result alone does not prove
59
+ that commit. Missing, mismatched or late observations cannot grant execution;
60
+ raw commands, bodies, reasoning, signatures and tickets are omitted. A currently
61
+ alive execution PID is only a point-in-time observation, not proof that a second
62
+ push overlapped an active tool. Actual model action and exact Codex claim still
63
+ require the revised owned runner and owner gate.
64
+
65
+ ## Exact execution boundary
66
+
67
+ The owned exec-form PreToolUse hook permits only Bash's literal public
68
+ `<candidate> agent message send uid:<original-source> --reply-to <original-ref> -- <one text argument>`.
69
+ The documented [shell prefix](https://code.claude.com/docs/en/env-vars) receives
70
+ one opaque shell invocation. The pinned candidate extracts one canonical
71
+ one-use ticket and directly executes its approved argv; it never evaluates the
72
+ carrier. Exact provider ancestry/birth, session, route, tool ID and image remain
73
+ required at preparation, consumption and commit. Missing, foreign, reused or
74
+ changed tickets/images fail before execution. An observed tool action grants no
75
+ permission. Private coord v5 fences earlier helpers; the frozen provider frame
76
+ is unchanged.
77
+
78
+ ## Genuine source transaction
79
+
80
+ The maintained setup, source action, bounded observation reader and parent
81
+ cleanup are connected and have offline regression coverage. The prior
82
+ payload-free source recipe is superseded. **Actual execution is not approved.**
83
+ The current runner now checks public config/read values/origins before source
84
+ creation and release, with offline fixtures. Actual current-version policy
85
+ responses and observation shapes remain unverified. Neither config generation
86
+ nor command-item observation proves that no earlier endpoint-startup effect
87
+ occurred. Those actual boundaries remain open R1 review items,
88
+ not completed acceptance evidence or permission to run the command below.
89
+ Historical failed attempts and their receipts remain unchanged.
90
+
91
+ `scripts/agent-dialogue-canary-setup.py` is the single transaction entrypoint.
92
+ It invokes prepare, starts the exact owned direct app-server endpoint, creates
93
+ the two public Agents, transfers observation to the parent runner and owns
94
+ partial-setup cleanup. Do not run prepare and then create actors manually, run
95
+ the source action from an operator shell, or invoke the run stage separately.
96
+ Those sequences do not establish genuine model-tool origin or the full cleanup
97
+ boundary.
98
+
99
+ Before owner approval, pin the reviewed clean commit and a separately built
100
+ 0755 candidate, both provider executables, every runner/schema file, the exact
101
+ generated prompt/config, a fresh short root, external receipt and external audit.
102
+ Use the public current versions Claude 2.1.263 and Codex 0.153.2. The candidate
103
+ and direct Codex image must be regular, owned executable files without group or
104
+ world write. Authentication inputs must be explicitly selected regular files,
105
+ owned by the current UID with mode 0600. Pin their paths and metadata only;
106
+ never record their contents or hashes. Runtime UIDs, process births and native
107
+ thread/turn/item IDs are obtained after approved creation and frozen before the
108
+ first Claude push; they are not fabricated in the pre-run packet.
109
+
110
+ After that separate approval, the reviewed invocation has this form. Each
111
+ uppercase input below is a concrete value from the approved packet; the root,
112
+ receipt and audit must all be absent before invocation:
113
+
114
+ ```sh
115
+ env -i PATH=/usr/local/bin:/usr/bin:/bin HOME="$HOME" \
116
+ PMX_DIALOGUE_LIVE_CANARY=1 \
117
+ PMX_DIALOGUE_CANDIDATE_HEAD="$REVIEWED_HEAD" \
118
+ PMX_DIALOGUE_PROJMUX_BIN="$CANDIDATE_BINARY" \
119
+ PMX_DIALOGUE_REAL_CLAUDE_BIN="$CLAUDE_BINARY" \
120
+ PMX_DIALOGUE_REAL_CODEX_BIN="$CODEX_BINARY" \
121
+ PMX_DIALOGUE_CLAUDE_CREDENTIAL_FILE="$CLAUDE_CREDENTIAL_FILE" \
122
+ PMX_DIALOGUE_CODEX_AUTH_FILE="$CODEX_AUTH_FILE" \
123
+ PMX_DIALOGUE_CANARY_ROOT="$FRESH_ROOT" \
124
+ PMX_DIALOGUE_CANARY_RECEIPT="$EXTERNAL_RECEIPT" \
125
+ PMX_DIALOGUE_MESSAGE_REF="$IDLE_MESSAGE_REF" \
126
+ python3 "$REVIEWED_CHECKOUT/scripts/agent-dialogue-canary-setup.py"
127
+ ```
128
+
129
+ The audit path is exactly `${EXTERNAL_RECEIPT}.audit.jsonl`, exclusive mode
130
+ 0600 outside the disposable root. It records bounded stage/exit/byte counts,
131
+ closed source freeze/completion facts and captured writer exit proof before
132
+ root removal. It contains no raw provider output, command, reasoning or auth
133
+ values. An absent success receipt is a failed/incomplete transaction even if
134
+ cleanup removed the root. No failure or unknown write outcome permits resend
135
+ or another transaction under the old approval.
136
+
137
+ ## Source action and first-push proof
138
+
139
+ The parent uses the fixed public default socket constructor
140
+ `<root>/codex-home/app-server-control/app-server-control.sock`; its encoded path
141
+ must be shorter than 100 bytes. It directly launches the pinned Codex executable
142
+ with `app-server --listen unix://<socket>`, retains that child handle and checks
143
+ its UID, PID/birth, executable, argv and socket/kernel peer. It does not start
144
+ or stop an ambient daemon service. The existing public Project projection
145
+ selects the tmux session name. A separate TMUX_TMPDIR, unique `-L`, observed
146
+ socket_path and exact socket identity scope both actor creation and cleanup.
147
+
148
+ The existing public source create receives the genuine approved task:
149
+
150
+ ```text
151
+ <candidate> create agent --provider codex --project uid:<P> --window uid:<W> -o pane-id -- <one genuine source prompt argument>
152
+ ```
153
+
154
+ The prompt is generated by `source_prompt(root)` and contains exactly the one
155
+ `source_command(root)` invocation:
156
+
157
+ ```text
158
+ python3 <root>/bin/agent-dialogue-source-action.py <root>
159
+ ```
160
+
161
+ The source model is asked to execute that command once from `<root>/work`,
162
+ wait for its result, then reply DONE. It is explicitly told not to execute other
163
+ commands, inspect authentication/config/history, create subagents, resend or
164
+ clean up. This is a genuine initial Codex user task, with normal private original
165
+ thread/tool-result writes expected. Claude's reply-only activation separately
166
+ starts the fixed `Reply READY.` turn. Neither startup turn is peer-to-user-turn
167
+ relay; the runner performs no direct peer history/input API writes.
168
+
169
+ The source action writes its independently observed process identity and waits.
170
+ Before release the parent verifies both runtime-first Agent/Pane chains, exact
171
+ current routes/composite source authority and the guarded Claude public init.
172
+ It freezes the actual native thread/turn/in-progress command item and separately
173
+ matches the action PID/birth, exact script argv and ancestry to the owned native
174
+ child. Inherited tmux context or a provider `processId` alone is not origin proof.
175
+ The source action builds its public CLI environment from the frozen own Pane,
176
+ server and socket; it does not use an inherited create-anchor context.
177
+
178
+ Only after release does the source action use existing public `agent message`
179
+ commands: qualify the exact receiver with `--confirm-isolated-provider-push`,
180
+ claim that reply from the original Codex inbox, send one independent idle request,
181
+ then claim its correlated reply. Empty follow-up claims check claim-once. Frozen
182
+ live routes are rechecked before dispatch. Claude tool/result/guarded-commit
183
+ proof is separate from full-frame delivered status. Missing or ambiguous proof
184
+ fails without resend. The parent never executes these coordination commands.
185
+
186
+ Only bounded correlation facts are returned to the original source model. The
187
+ parent then compares the completed command's closed result with the independent
188
+ qualification/idle proofs and separately observes original-turn completion.
189
+ Started-item freeze precedes the first push; completed-item proof follows the
190
+ result. The action never cleans up its own provider or parent, and parent cleanup
191
+ cannot stand in for a successfully returned source tool result.
192
+
193
+ ## Observation and requested private policy
194
+
195
+ `agent-dialogue-codex-observation.py` validates the frozen public 0.153.2
196
+ `thread/read(includeTurns=true)` and `item/started`/`item/completed` schemas.
197
+ The four exports in `scripts/agent-dialogue-codex-schema/` retain their original
198
+ hashes, also checked by the reader. `agent-dialogue-native-source.py` connects to
199
+ the pinned owned endpoint and performs bounded non-experimental initialization;
200
+ the reader sends only `thread/read`. Neither component starts/resumes a thread,
201
+ subscribes a relay, sends a user turn, or copies native history to evidence.
202
+
203
+ Observation is bounded to 1 MiB per frame, 8 MiB/128 frames per reader and 32 items
204
+ in the sole expected turn. HTTP upgrade and initialization are each bounded to 16 KiB/five seconds;
205
+ read connections have a ten-second deadline. The source release wait, action
206
+ result wait and completed-result observation are separately bounded. Unknown
207
+ fields/effects, incomplete views, absent source, changed items/routes, wrong
208
+ results, EOF or bounds violations fail closed. Explicitly observed non-userShell
209
+ source enums are checked against the prepared allowlist; no schema default is
210
+ applied. `processId` is never treated as an OS PID. Raw text/command/output,
211
+ reasoning and auth data stay out of the returned facts and external audit.
212
+
213
+ The generated [public configuration](https://developers.openai.com/codex/config-reference)
214
+ requests file authentication, never approvals, workspace-write with the owned
215
+ root as an additional writable root, `/tmp` and TMPDIR expansion excluded,
216
+ sandbox network disabled, web search disabled and no startup update check.
217
+ Fresh HOME/CODEX_HOME/CODEX_SQLITE_HOME/XDG paths are passed to the direct child;
218
+ ambient config/history and keyring policy are not copied. Both auth inputs are
219
+ copied only during the approved transaction to private mode-0600 files.
220
+
221
+ The private Unix listener carries RFC6455 WebSocket messages. The maintained
222
+ `agent-dialogue-websocket.py` first validates HTTP 101/accept, masks every client
223
+ frame and reads bounded server text messages. It does not send raw JSONL onto
224
+ the socket. Fragmented text and interleaved ping/pong retain their message
225
+ boundary; invalid masks/opcodes/UTF-8, extensions, EOF or limits fail closed.
226
+ Each connection permits at most 128 total frames and 8 MiB total wire bytes,
227
+ including control traffic; each text message is at most 1 MiB. The adapter
228
+ opens no socket or listener and retains the existing kernel peer checks.
229
+
230
+ The pinned public v1 InitializeParams/InitializeResponse schemas require
231
+ `codexHome`, `platformFamily`, `platformOs` and `userAgent` in the response.
232
+ The harness compares `codexHome` to the exact owned CODEX_HOME in memory before
233
+ sending `initialized` or config/read. Raw initialization values are discarded.
234
+ Schema-invalid responses and a foreign home are refused. The same framed
235
+ transport serves read-only config/read and thread/read; method permissions,
236
+ source release, policy, route and cleanup fences remain in force.
237
+
238
+ `agent-dialogue-native-policy.py` uses the two original public 0.153.2
239
+ ConfigRead schemas under `scripts/agent-dialogue-config-schema/`. On the same
240
+ owned endpoint it requests only `config/read` with the exact work cwd and
241
+ `includeLayers=false`. It requires the prepared approval, sandbox/network,
242
+ writable-root, web-search, file-auth and update values. Each selected dotted
243
+ leaf must have an explicit origin in the owned user config; a missing origin
244
+ is not inferred from its parent or default. Other reported origins must be
245
+ that same user file or packaged defaults under the pinned distribution.
246
+ Managed, project, session, unknown or mismatched origins fail closed. Nonempty
247
+ MCP/plugin/hook/instruction inputs and returned raw layers are refused.
248
+
249
+ Only those closed values, origin classes/counts and boolean assertions are
250
+ retained, including in the external audit. Arbitrary additional config fields,
251
+ origin revision strings, instructions and secrets are discarded. The response
252
+ has a one-MiB/five-second bound. The parent checks policy before public source
253
+ creation, then rereads it on the exact current peer immediately before release
254
+ and requires identical facts and an unchanged owned config/socket/process.
255
+ An otherwise-valid source action receives no release when policy changes.
256
+
257
+ A failed pre-source read preserves one `policy-failure` audit event before
258
+ cleanup. Its code identifies the rejecting boundary: `policy-schema` for
259
+ pinned schemas/JSON/public schema validation, `policy-request` for initialization
260
+ or config/read framing/transport, `policy-value` for schema-valid policy or layer
261
+ mismatches, `policy-origin` for origin constraints, `policy-config` for the owned
262
+ config file fence, and `policy-socket` for the endpoint/process/image fence.
263
+ These fixed codes contain no field names, raw values, layers, exception strings,
264
+ paths, instructions or secrets. They identify a validation boundary, not the
265
+ provider's underlying cause. Earlier failures without a code remain unknown.
266
+ Both consumed v2 and v3 `policy-request` failures retain unknown finer predicates;
267
+ later offline findings do not establish the cause of either actual failure.
268
+ The event now also records closed `substage` and `rejectionKind` labels. Substages
269
+ separate connect/current/socket/peer checks, WebSocket upgrade, initialize
270
+ schema/write/read/decode/envelope/result, initialized notification write,
271
+ config/read params/write/read/decode/envelope/result, and post-read socket/config
272
+ checks. Rejection kinds distinguish deadline, I/O, EOF/close, size bounds, UTF-8,
273
+ frame or HTTP validation, identity, and envelope shape/ID/error/notification/request.
274
+ Unclassified failures remain `unknown`; no native error code, message, method,
275
+ dynamic response field names or exception text is exported. Error/notification/request envelopes
276
+ still stop the transaction. No notification is skipped to search for a success.
277
+ The existing product and pinned public JSON-RPC contracts agree on
278
+ initialize → matching response → initialized → config/read, with `id`/`result`
279
+ success envelopes and no required `jsonrpc` member. This comparison found no
280
+ transport or admission mismatch after v3. The classifier now distinguishes a
281
+ public server request (including optional bounded trace context) from a generic
282
+ shape refusal; malformed error or method types remain generic refusals. Pinned
283
+ public `JSONRPCRequest`, `JSONRPCResponse`, `JSONRPCNotification` and `JSONRPCError`
284
+ schemas anchor the offline fixtures. These are closed known-field subsets:
285
+ unknown fields and mixed forms still refuse, and the expected-ID fence runs
286
+ first. A request is neither answered nor skipped. Its method, params, trace and
287
+ error data never become evidence. The consumed v4 and v5 `config-read-envelope /
288
+ envelope-shape` results do not establish which public or unknown form arrived.
289
+ No new admission or transport mismatch was established by the bounded v5 review.
290
+
291
+ At an initialize/config-read envelope rejection, an optional `envelopeFacts`
292
+ projection records all remaining structural predicates together. Its thirteen
293
+ fixed fields describe the top-level JSON type, ID presence/type/equality,
294
+ result/params presence, method type/emptiness, error object/code/message types,
295
+ trace object/member-value types, and booleans for unknown top-level/error/trace
296
+ members. Simultaneous defects remain visible without copying any unknown names,
297
+ IDs, numeric error codes, method strings, trace values or nested payloads. The
298
+ audit sink rejects missing/extra projection fields, non-boolean flags, unknown
299
+ enum values, and projections outside the two envelope rejection boundaries
300
+ before writing. The existing exact `{id,result}` success allowlist and expected
301
+ integer ID fence remain independent of these diagnostic facts. Offline fixtures
302
+ cover every combination of known top-level fields, malformed nested types,
303
+ otherwise-valid replies with unknown fields, privacy, and once cleanup after
304
+ the external facts are written; they establish no actual v5 response contents.
305
+ Audit write failure still runs the one owned writer cleanup and both credential
306
+ finally paths, retains the root, and cannot produce a successful receipt.
307
+
308
+ This is resolved config evidence, not a ThreadStartResponse observation. The
309
+ existing public create sends cwd and runtime workspace roots; this reader adds
310
+ no dummy thread/start, resume, policy mutation or relay to obtain more evidence.
311
+ The original thread's actual policy/override boundary and the current public
312
+ origin representation remain explicit review/actual-verification limits.
313
+ Endpoint-startup effects preceding the read are not retroactively certified.
314
+ Offline fixtures prove rejection/ordering and data minimization, not actual
315
+ provider execution or a waiver of that remaining boundary.
316
+
317
+ ## Parent cleanup and remaining acceptance evidence
318
+
319
+ The parent owns one cleanup transaction on every partial failure and after the
320
+ source tool result returns. It captures owned writer births before teardown,
321
+ including the direct daemon, source action and exact kernel peers of the private
322
+ broker discovery directory. Broker credential records are not read. Signals use
323
+ pidfds for only validated captured roles, alongside exact public Project/tmux
324
+ teardown. The broker's 30-second idle default is not treated as proof of exit
325
+ within the existing 20-second writer deadline.
326
+
327
+ Both owned auth copies are removed on success and retained-root failure paths.
328
+ The exact-root writer exit proof is preserved in the external audit before one
329
+ root removal. Unknown writer exit, audit failure or removal failure remains
330
+ failure and preserves evidence; there is no fixed-sleep proof, broad signal,
331
+ retry removal or manual-cleanup PASS. No protected/shared process is an owned
332
+ seed. Auth source files are not modified. A successful external receipt requires
333
+ closed model/tool/claim proofs and automatic cleanup; it does not certify other
334
+ acceptance cases.
335
+
336
+ The selectorless E2E remains the single deterministic L20 scenario. Actual
337
+ active-tool/human overlap, multiple ordinary requests, same-UID recovery and
338
+ installed smoke remain separate unverified evidence. They are not run by this
339
+ qualification-plus-one-idle transaction and are not inferred from its result.
package/docs/hooks.md CHANGED
@@ -229,6 +229,39 @@ amber-orange status role, response-complete panes use success green, and
229
229
  in-progress panes use progress yellow. They do not inherit the critical queue
230
230
  severity, and permission/input status badges do not use red.
231
231
 
232
+ ## Hook Pane Identity
233
+
234
+ Every provider hook projmux installs is handed the Pane it belongs to:
235
+
236
+ ```
237
+ --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-}
238
+ ```
239
+
240
+ projmux plants that activation envelope on the process it launches in the Pane,
241
+ so the value reaches the hook without depending on the tmux environment. That
242
+ matters because the process that actually runs a provider hook is often not the
243
+ one projmux started in the pane: an app-server can be launched with no `TMUX` and
244
+ no `TMUX_PANE` at all, and a hook spawned from it inherits nothing tmux knows.
245
+
246
+ The argument is resolved against the Registry, never trusted on its own. The
247
+ Pane must exist, hold a live runtime handle, and round-trip through the Agent
248
+ that owns it. When it does, that Pane is the answer and no other evidence is
249
+ consulted — falling through to a working-directory match is exactly how an event
250
+ lands on somebody else's Pane. When it does not, the event is not attributed and
251
+ `ai-ingest.log` records which step failed.
252
+
253
+ An app-server shared by several Panes carries no envelope, so the argument
254
+ expands to a bare `--pane=`. That is a valid answer meaning "nothing was handed
255
+ over", and attribution falls back to the established ladder: `TMUX_PANE`, then a
256
+ working-directory match against the live pane list, then a thread or session id
257
+ match. Nothing about any of this is provider-specific.
258
+
259
+ Attribution failures are a closed vocabulary in `projmux diagnostics agent-hook`:
260
+ `pane inventory unavailable` (no live pane list could be read), `no matching
261
+ pane` (the list was read and nothing matched), `pane registry unavailable`,
262
+ `explicit pane is not registered`, `explicit pane has no live runtime`, and
263
+ `explicit pane binding is stale`.
264
+
232
265
  ## Codex Hooks Engine
233
266
 
234
267
  `projmux doctor` reports Codex hooks-engine wiring separately from legacy
@@ -282,49 +315,49 @@ hooks = true
282
315
  matcher = "*"
283
316
  [[hooks.PreToolUse.hooks]]
284
317
  type = "command"
285
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
318
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
286
319
 
287
320
  [[hooks.PermissionRequest]]
288
321
  matcher = "*"
289
322
  [[hooks.PermissionRequest.hooks]]
290
323
  type = "command"
291
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
324
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
292
325
 
293
326
  [[hooks.PostToolUse]]
294
327
  matcher = "*"
295
328
  [[hooks.PostToolUse.hooks]]
296
329
  type = "command"
297
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
330
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
298
331
 
299
332
  [[hooks.PreCompact]]
300
333
  matcher = "*"
301
334
  [[hooks.PreCompact.hooks]]
302
335
  type = "command"
303
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
336
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
304
337
 
305
338
  [[hooks.PostCompact]]
306
339
  matcher = "*"
307
340
  [[hooks.PostCompact.hooks]]
308
341
  type = "command"
309
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
342
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
310
343
 
311
344
  [[hooks.SessionStart]]
312
345
  matcher = "*"
313
346
  [[hooks.SessionStart.hooks]]
314
347
  type = "command"
315
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
348
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
316
349
 
317
350
  [[hooks.UserPromptSubmit]]
318
351
  matcher = "*"
319
352
  [[hooks.UserPromptSubmit.hooks]]
320
353
  type = "command"
321
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
354
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
322
355
 
323
356
  [[hooks.Stop]]
324
357
  matcher = "*"
325
358
  [[hooks.Stop.hooks]]
326
359
  type = "command"
327
- command = "projmux internal agent-hook ingest codex-hook >/dev/null 2>&1 || true"
360
+ command = "projmux internal agent-hook ingest codex-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true"
328
361
  ```
329
362
 
330
363
  Repeated installs are idempotent and preserve unrelated Codex config, including
@@ -543,7 +576,7 @@ an observability hook:
543
576
  "hooks": [
544
577
  {
545
578
  "type": "command",
546
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
579
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
547
580
  }
548
581
  ]
549
582
  }
@@ -553,7 +586,7 @@ an observability hook:
553
586
  "hooks": [
554
587
  {
555
588
  "type": "command",
556
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
589
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
557
590
  }
558
591
  ]
559
592
  }
@@ -563,7 +596,7 @@ an observability hook:
563
596
  "hooks": [
564
597
  {
565
598
  "type": "command",
566
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
599
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
567
600
  }
568
601
  ]
569
602
  }
@@ -573,7 +606,7 @@ an observability hook:
573
606
  "hooks": [
574
607
  {
575
608
  "type": "command",
576
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
609
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
577
610
  }
578
611
  ]
579
612
  }
@@ -583,7 +616,7 @@ an observability hook:
583
616
  "hooks": [
584
617
  {
585
618
  "type": "command",
586
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
619
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
587
620
  }
588
621
  ]
589
622
  }
@@ -593,7 +626,7 @@ an observability hook:
593
626
  "hooks": [
594
627
  {
595
628
  "type": "command",
596
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
629
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
597
630
  }
598
631
  ]
599
632
  }
@@ -603,7 +636,7 @@ an observability hook:
603
636
  "hooks": [
604
637
  {
605
638
  "type": "command",
606
- "command": "projmux internal agent-hook ingest claude-hook >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
639
+ "command": "projmux internal agent-hook ingest claude-hook --pane=${PMX_INTERNAL_ACTIVATION_PANE_UID:-} >/dev/null 2>&1 || true # projmux-managed:claude-hook:v1"
607
640
  }
608
641
  ]
609
642
  }
@@ -20,6 +20,37 @@ npm-specific guidance. npm is only an update/install source label here; the
20
20
  keybinding flow remains `projmux shell` first, then `projmux setup` and
21
21
  `projmux setup terminal` only for terminals that swallow shortcuts.
22
22
 
23
+ ## Install residue notice
24
+
25
+ There is no `postinstall` script in this package, and there is no plan to add
26
+ one. A lifecycle script is skipped by `--ignore-scripts` and by many global and
27
+ CI installs, and npm buffers or reorders its output unless
28
+ `--foreground-scripts`. The bin shim cannot be skipped — it *is* the entrypoint
29
+ — and it writes straight to the user's terminal.
30
+
31
+ So the shim carries the install residue notice
32
+ ([operational-diagnostics.md](operational-diagnostics.md#install-residue-ledger)).
33
+ `npm install -g projmux` and `npm update` delete and rewrite the package
34
+ directory, so a missing `npm/.install-residue-reported` watermark inside that
35
+ directory *is* the "this install is new" signal — no fingerprinting, no second
36
+ copy of the Go side's XDG path logic in JavaScript, and nothing written outside
37
+ the package directory. The watermark is not in the published `files` list, so
38
+ it never ships in a tarball.
39
+
40
+ On the first run after an install, if stderr is a TTY, the shim creates the
41
+ watermark and then — **after** the user's command has finished, so the notice is
42
+ the last thing on screen and never delays or interleaves with the real command
43
+ — runs `projmux internal install-residue`. A non-TTY run (a hook, CI, a pipe)
44
+ does nothing and leaves the watermark missing, so the next interactive run is
45
+ the one that reports; the notice should land on a run a human is looking at. If
46
+ the watermark cannot be written the notice is never shown, so an unwritable
47
+ package directory cannot produce it on every invocation forever. None of this
48
+ can change the shim's exit code.
49
+
50
+ The trade-off is deliberate: for npm the notice appears on the first
51
+ interactive run after the install rather than during `npm install` itself. The
52
+ ledger's `at` timestamp is the install-detection moment either way.
53
+
23
54
  ## Local Packaging
24
55
 
25
56
  Build and dry-run pack all npm packages: