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.
- package/README-ko.md +58 -114
- package/README.md +49 -110
- package/docs/agent-workflow.md +1420 -37
- package/docs/architecture.md +95 -70
- package/docs/assets/projmux-ai-attention-ko.gif +0 -0
- package/docs/assets/projmux-ai-attention.gif +0 -0
- package/docs/assets/projmux-overview-ko.gif +0 -0
- package/docs/assets/projmux-overview.gif +0 -0
- package/docs/assets/projmux-three-pane-workflow-ko.gif +0 -0
- package/docs/assets/projmux-three-pane-workflow.gif +0 -0
- package/docs/claude-coordination-endpoints.md +254 -0
- package/docs/cli-guide.md +283 -39
- package/docs/cli.md +2059 -56
- package/docs/codex-generation-pool.md +618 -0
- package/docs/codex-installed-compatibility.md +69 -0
- package/docs/codex-native-required-migration.md +98 -9
- package/docs/column-profiles.md +83 -0
- package/docs/configuration.md +43 -4
- package/docs/heterogeneous-dialogue-canary.md +339 -0
- package/docs/hooks.md +48 -15
- package/docs/npm-distribution.md +31 -0
- package/docs/operational-diagnostics.md +125 -0
- package/docs/pr-guideline.md +3 -2
- package/docs/replacement-contract.md +555 -0
- package/docs/session-restore.md +15 -5
- package/docs/settings-ia.md +5 -4
- package/docs/testing.md +12 -7
- package/docs/troubleshooting.md +19 -0
- package/docs/upgrading.md +48 -3
- package/npm/projmux.js +65 -0
- package/package.json +5 -5
package/docs/architecture.md
CHANGED
|
@@ -313,29 +313,42 @@ Identity and naming:
|
|
|
313
313
|
- `metadata.uid` is opaque, immutable, and independent of tmux lifecycle. It
|
|
314
314
|
survives snapshot/restore, runtime creation, and root rebind.
|
|
315
315
|
- `metadata.name` is the stable unique-within-scope query key. Project names
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
collision
|
|
338
|
-
zero
|
|
316
|
+
and ControlSession names are unique within their own root kind across the
|
|
317
|
+
registry. Window, Pane, and Agent names are unique by
|
|
318
|
+
`(rootOwnerUID, kind, name)`, where the root is the Project or ControlSession
|
|
319
|
+
reached through the direct `ownerRef` chain. Different roots and different
|
|
320
|
+
kinds may use the same spelling.
|
|
321
|
+
- Automatic creation mints the opaque UID first and stores that exact full UID,
|
|
322
|
+
including its kind prefix and complete payload, as `metadata.name`. If that
|
|
323
|
+
name slot is already occupied, the unpublished UID is discarded and reminted
|
|
324
|
+
up to 100 times. There is no semantic stem, prefix truncation, sibling scan,
|
|
325
|
+
or numeric suffix allocator. An explicit `--name` keeps its original spelling
|
|
326
|
+
after validation; explicit create and rename collisions fail with exit code 2
|
|
327
|
+
and zero Registry, tmux, or provider writes.
|
|
328
|
+
- `create agent` supplies an **explicit** name for the Pane its Agent owns:
|
|
329
|
+
`<agent-name>-pane`, derived from the Agent's own name. That used to be a
|
|
330
|
+
documented follow-up `rename pane` a launcher had to remember, so a caller
|
|
331
|
+
that did not know the convention left the managed Pane addressed by a raw
|
|
332
|
+
UID. It is not a new automatic-name rule -- it goes through the same explicit
|
|
333
|
+
name path an operator's `--name` does, so a Pane with no Agent still gets its
|
|
334
|
+
exact full UID. Length is the only fallback: an Agent name may be the full 128
|
|
335
|
+
bytes, and `<agent-name>-pane` would then be 133 and invalid, so the Pane
|
|
336
|
+
falls back to automatic naming rather than refusing a create that works today.
|
|
337
|
+
A *collision* on the derived name never falls back; it is the same typed exit
|
|
338
|
+
2 with zero writes an explicit `--name` collision produces, because the whole
|
|
339
|
+
metadata phase runs before the first tmux or provider call.
|
|
340
|
+
- The derivation runs once, at create, and is deliberately not an invariant.
|
|
341
|
+
`rename agent` does not follow the Pane and `rename pane` is neither forbidden
|
|
342
|
+
nor restricted, so the Pane name keeps exactly one owner and `rename agent`
|
|
343
|
+
keeps its `cardinality=exact-one` effect tuple. A launcher that still issues
|
|
344
|
+
the old follow-up `rename pane <agent-name>-pane` gets a successful no-op,
|
|
345
|
+
because a reservation slot the same uid already holds is not a conflict.
|
|
346
|
+
- Schema v4 stores no `metadata.displayName`, `status.displayTitle`, or renamed
|
|
347
|
+
presentation replacement. Human context is projected for one invocation from
|
|
348
|
+
the Project root, topic/provider/command, and exact live tmux title. That
|
|
349
|
+
context may duplicate and is never a selector, reservation, ownerRef, or
|
|
350
|
+
durable identity input. `metadata.labels` remains key/value classification;
|
|
351
|
+
`metadata.annotations` remains non-identifying metadata such as an AI topic.
|
|
339
352
|
|
|
340
353
|
Root lifecycle:
|
|
341
354
|
|
|
@@ -656,10 +669,9 @@ Registry file and schema:
|
|
|
656
669
|
bounded retry and stale-lock breaking, matching the notify queue and
|
|
657
670
|
recent-windows stores. Explicit Registry repair uses its own recovery lock;
|
|
658
671
|
see the recovery boundary below.
|
|
659
|
-
- The envelope carries `schemaVersion:
|
|
660
|
-
envelope projmux wrote; this build migrates v1 through v2 and
|
|
661
|
-
|
|
662
|
-
`interrupted/control-action` evidence.
|
|
672
|
+
- The envelope carries `schemaVersion: 4`. Version 1 is the first Registry
|
|
673
|
+
envelope projmux wrote; this build migrates v1 through v2 and v3 before the
|
|
674
|
+
v3 → v4 root-scoped naming and presentation-field cutover.
|
|
663
675
|
- Everything else fails closed: the file is refused as unreadable and **no
|
|
664
676
|
write happens at all** — no rewrite, no backup, no staged temp file. This
|
|
665
677
|
covers a **newer** schemaVersion (which would destroy state a newer build
|
|
@@ -673,9 +685,9 @@ Registry file and schema:
|
|
|
673
685
|
registry yet" case **only before the first successful write**; see the durable
|
|
674
686
|
envelope below. Only a file with actual content and no usable `schemaVersion`
|
|
675
687
|
is refused as unknown.
|
|
676
|
-
- A normal locked `Load` of v1 or
|
|
688
|
+
- A normal locked `Load` of v1, v2, or v3 runs the production migration chain,
|
|
677
689
|
validates the repaired graph, writes the versioned backup, and publishes the
|
|
678
|
-
|
|
690
|
+
v4 bytes through the existing temp-file atomic replace. A failed migration
|
|
679
691
|
leaves the source bytes unchanged. Every successful first migrator (`Load`,
|
|
680
692
|
`Update`, `UpdateConvergent`, or explicit `Migrate`) also atomically publishes
|
|
681
693
|
a 0600 `<exact-backup>.migration-report.json` beside the versioned backup
|
|
@@ -685,11 +697,21 @@ Registry file and schema:
|
|
|
685
697
|
A failed migration removes any staged/published report before returning while
|
|
686
698
|
leaving the source bytes unchanged. `LoadWithMigrationResult` and `Migrate`
|
|
687
699
|
additionally return both exact paths from the same locked transaction. A
|
|
688
|
-
second pass sees
|
|
689
|
-
existing invalid
|
|
700
|
+
second pass sees v4 and writes neither Registry, backup, nor report bytes. An
|
|
701
|
+
existing invalid v4 document is validated and refused byte-identically even
|
|
690
702
|
when explicit `Migrate` has no version step to run.
|
|
691
703
|
Explicit read-only inspection migrates only its returned in-memory view and
|
|
692
704
|
never publishes it.
|
|
705
|
+
- The v3 → v4 step validates the UID/direct-owner graph, rebuilds reservations
|
|
706
|
+
from that graph, and changes descendant scope to the owning Project or
|
|
707
|
+
ControlSession. Every member of a root-wide same-kind duplicate group moves
|
|
708
|
+
to its exact UID name. If one of those destinations is held by a resource
|
|
709
|
+
outside the set, the holder is added until destination closure reaches a
|
|
710
|
+
fixed point; every unique name outside that closure is preserved. The sorted
|
|
711
|
+
name mapping and content-free receipts for removed `metadata.displayName` and
|
|
712
|
+
`status.displayTitle` keys are recorded beside the exact-byte v3 backup. A
|
|
713
|
+
present empty key records zero bytes and SHA-256 of empty content without
|
|
714
|
+
being counted as information loss.
|
|
693
715
|
- The v1 repair is deterministic over Registry order apart from injected opaque
|
|
694
716
|
uid generation. It preserves every existing uid, ownerRef, reserved name, and
|
|
695
717
|
Agent conversation/session pointer. A valid Project anchor is never
|
|
@@ -717,8 +739,8 @@ Registry file and schema:
|
|
|
717
739
|
Mixed legacy/final authority is refused; the final writer emits no
|
|
718
740
|
`primaryPaneRef`; and a second final-v2 pass writes zero bytes.
|
|
719
741
|
- **Field spelling:** the registry file intentionally uses the resource-model
|
|
720
|
-
camelCase spelling (`apiVersion`, `schemaVersion`, `metadata`, `
|
|
721
|
-
`
|
|
742
|
+
camelCase spelling (`apiVersion`, `schemaVersion`, `metadata`, `ownerRef`,
|
|
743
|
+
`anchorPaneRef`, `defaultShellPaneRef`, `spec`, `status`) rather than the snake_case
|
|
722
744
|
used by the older projmux on-disk JSON. The two spellings coexist on purpose:
|
|
723
745
|
existing snake_case files are **not** retro-changed, and the resource registry
|
|
724
746
|
follows the resource-model contract.
|
|
@@ -831,11 +853,14 @@ Registry recovery boundary (`projmux reconcile registry`):
|
|
|
831
853
|
`ownerRef`, or a broken name reservation are all refused. Restoring an
|
|
832
854
|
unverified source would replace a known-damaged registry with an
|
|
833
855
|
unknown-damaged one, and the second state is worse because it looks healthy.
|
|
834
|
-
- **Byte-semantic restore
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
856
|
+
- **Byte-semantic current restore and canonical v3 import.** A verified v4
|
|
857
|
+
source is published verbatim. A verified v3 source is never made live in its
|
|
858
|
+
legacy form: its exact bytes are first written to a versioned backup, a
|
|
859
|
+
checksum-bearing content-free migration report is published beside that
|
|
860
|
+
backup, and the same deterministic v3 → v4 migration used by normal Registry
|
|
861
|
+
loading supplies the staged live bytes. Repeat restore compares the live
|
|
862
|
+
Registry with that canonical publish checksum, so it writes no new Registry,
|
|
863
|
+
backup, or report bytes.
|
|
839
864
|
- **The bytes being replaced are kept.** A restore copies the current registry to
|
|
840
865
|
`recovery/replaced-<stamp>-<seq>.json` before replacing it, and unlike the
|
|
841
866
|
write-side copy it keeps content that does **not** verify — that damaged
|
|
@@ -974,15 +999,15 @@ tmux transport mirror:
|
|
|
974
999
|
- `rename pane` changes `Pane.metadata.name` and its `@projmux_pane_label`
|
|
975
1000
|
mirror only. It never writes the raw tmux `pane_title`.
|
|
976
1001
|
- `rename window` is the explicit stable-identity path: it changes only
|
|
977
|
-
`Window.metadata.name`, its scoped name reservation, and the
|
|
978
|
-
`@projmux_window_name` transport mirror. It does not
|
|
979
|
-
|
|
1002
|
+
`Window.metadata.name`, its root-scoped same-kind name reservation, and the
|
|
1003
|
+
exact live `@projmux_window_name` transport mirror. It does not change tmux
|
|
1004
|
+
`window_name`.
|
|
980
1005
|
- `rename project` likewise writes only `Project.metadata.name` and the exact
|
|
981
1006
|
live session's `@projmux_project_name`; it never renames the tmux session.
|
|
982
1007
|
`rebind project` preserves the Project uid and session name while updating
|
|
983
1008
|
`spec.root` and the exact live session's `@projmux_project_path`. Neither
|
|
984
1009
|
operation moves files.
|
|
985
|
-
- `rename agent` changes only the
|
|
1010
|
+
- `rename agent` changes only the root-scoped Agent `metadata.name` and its
|
|
986
1011
|
reservation. Agent topic annotations, provider, lifecycle status, and the
|
|
987
1012
|
managed Pane's name and raw title are independent and receive no tmux write.
|
|
988
1013
|
- Rename/rebind commits the authoritative Registry transaction before its
|
|
@@ -997,24 +1022,23 @@ tmux transport mirror:
|
|
|
997
1022
|
UID claim is nonzero and reports that Registry state committed plus
|
|
998
1023
|
`projmux reconcile resources` as the retry boundary.
|
|
999
1024
|
- The configured `window.rename` action (`Ctrl-M` by default) is the runtime
|
|
1000
|
-
display path and invokes tmux `rename-window` directly.
|
|
1001
|
-
|
|
1002
|
-
|
|
1025
|
+
display path and invokes tmux `rename-window` directly. Reads may observe that
|
|
1026
|
+
exact live `window_name` as invocation-scoped context; reconciliation never
|
|
1027
|
+
persists it as a Registry address or presentation field.
|
|
1003
1028
|
- Registry-managed Windows are set to `automatic-rename off` so a focused-Pane
|
|
1004
1029
|
change cannot overwrite the Window name. The **global** `automatic-rename on`
|
|
1005
1030
|
plus visible-pane-label `automatic-rename-format` default in the generated
|
|
1006
1031
|
app config is unchanged, so unmanaged windows keep their existing behavior.
|
|
1007
|
-
- Legacy import gives every newly discovered Window
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
ownerRef,
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
secondary and is never a selector, an identity, or a Window name source.
|
|
1032
|
+
- Legacy import gives every newly discovered Window, Pane, and Agent its exact
|
|
1033
|
+
minted UID as the automatic Registry name and switches managed Windows to
|
|
1034
|
+
`automatic-rename off`. Observed `window_name`, Pane label/title, command,
|
|
1035
|
+
shell, provider, and topic are never persisted as an address or presentation
|
|
1036
|
+
field. Re-observing an existing resource preserves its UID, stable name,
|
|
1037
|
+
ownerRef, and reservation. `@projmux_window_name` and
|
|
1038
|
+
`@projmux_pane_label` remain live mirrors of the durable name.
|
|
1039
|
+
- `Pane.metadata.name` is the stable Pane address. Human-readable Pane context
|
|
1040
|
+
is derived per invocation from the owner Agent topic/provider, command, and
|
|
1041
|
+
exact live title; it is never a selector, identity, or Window name source.
|
|
1018
1042
|
|
|
1019
1043
|
Runtime observation and resource status:
|
|
1020
1044
|
|
|
@@ -1154,13 +1178,12 @@ Binding reapply and adoption:
|
|
|
1154
1178
|
and neither does a **refused** pane — a refusal means a real registry Pane sits
|
|
1155
1179
|
on the other side of the ambiguity, so minting beside it would leave two Panes
|
|
1156
1180
|
describing one tmux pane.
|
|
1157
|
-
- The registered Pane
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
to
|
|
1162
|
-
|
|
1163
|
-
below, which runs on the Pane the walk just settled on.
|
|
1181
|
+
- The registered Pane uses its exact minted UID as `metadata.name`, never
|
|
1182
|
+
`pane_current_command`, the Pane label/title, or a numeric suffix. Those
|
|
1183
|
+
runtime values may contribute only to invocation-scoped context. The mint
|
|
1184
|
+
itself never creates an Agent: it adds one Pane and stops. Linking that Pane
|
|
1185
|
+
to an Agent is the separate step below, which runs on the Pane the walk just
|
|
1186
|
+
settled on.
|
|
1164
1187
|
- **Nothing is ever re-identified.** Adoption changes no uid, merges no uid, and
|
|
1165
1188
|
reassigns no uid; it only decides which registry object a live tmux object is
|
|
1166
1189
|
the runtime of, and then writes that object's existing uid. Adopted objects
|
|
@@ -1673,7 +1696,8 @@ Explicit Registry topology materialization:
|
|
|
1673
1696
|
desired Windows remain Agent-anchored and acquire a shell only through the
|
|
1674
1697
|
ordinary materializer. Source snapshot bytes and unrelated roots are never
|
|
1675
1698
|
rewritten, and a second projection is byte-stable.
|
|
1676
|
-
- `
|
|
1699
|
+
- `Recreate Project` replaces the exact same-root Project graph, after an
|
|
1700
|
+
explicit confirmation, in one Registry
|
|
1677
1701
|
commit. It always allocates a new Project UID plus one new canonical Window
|
|
1678
1702
|
and direct shell UID, whether the old Project retained Windows or had zero.
|
|
1679
1703
|
The preimage remains the durable recovery state when the replacement commit
|
|
@@ -1849,11 +1873,12 @@ Resource-first create:
|
|
|
1849
1873
|
through the same `@projmux_window_uid` mirror and registry `ownerRef` chain
|
|
1850
1874
|
the read verbs use.
|
|
1851
1875
|
- **Window and anchor follow the whole scope, not the Project flag.** They are
|
|
1852
|
-
derived only when the argv named no `--project`, `--window`, `--pane`,
|
|
1853
|
-
`--selector` at all. That keeps a
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1876
|
+
derived only when the argv named no `--project`, `--window`, `--pane`,
|
|
1877
|
+
`--selector`, `--all-windows`, and no `--primary-window` at all. That keeps a
|
|
1878
|
+
bare `create pane --placement right` -- the generated keybinding body -- a
|
|
1879
|
+
split of the Window the operator is looking at, rather than of the Project's
|
|
1880
|
+
primary Window, which is what the same route resolves once a `--project` scope
|
|
1881
|
+
is typed, while one explicit occurrence still fixes the whole target set. An explicit `--pane` or popup
|
|
1857
1882
|
origin is the exact split anchor. Only a scope with no Pane consumes the
|
|
1858
1883
|
target Window's role-agnostic `spec.anchorPaneRef`; a missing, stale, dead, or
|
|
1859
1884
|
cross-Window ref refuses with no alternate-live-Pane inference.
|
|
@@ -1880,7 +1905,7 @@ Resource-first create:
|
|
|
1880
1905
|
Selector and the implicit active target:
|
|
1881
1906
|
|
|
1882
1907
|
- A selector value is either `uid:<uid>` or a `metadata.name`. There is no
|
|
1883
|
-
bare-uid form;
|
|
1908
|
+
bare-uid form; ephemeral context, `spec.root`, and tmux `%N`/`@N`/`$N` handles are
|
|
1884
1909
|
structurally unmatchable. `--project` is at-most-once and fixes the scope,
|
|
1885
1910
|
`--window`/`--pane` repeat and union, `--selector key=value` repeats and ANDs,
|
|
1886
1911
|
and how many targets a `<verb, kind>` pair accepts comes from one declared
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Claude coordination endpoint registration
|
|
2
|
+
|
|
3
|
+
Phase 1 binds a Claude-created endpoint to a managed Agent activation. It does
|
|
4
|
+
not implement message delivery, a provider message schema, a message/wait parser,
|
|
5
|
+
or a new public provider command. `agent capabilities` reports the exact Agent's
|
|
6
|
+
`runtimeEligibility.coordination` readiness and evidence. Static provider queries
|
|
7
|
+
remain static, and existing action cells stay unchanged.
|
|
8
|
+
|
|
9
|
+
`AgentRouteRef` shares stable Agent UID, Pane UID, and activation generation.
|
|
10
|
+
Its sealed provider authority union consumes the existing Codex thread and
|
|
11
|
+
`CodexAuthorityRef` composite unchanged. Claude authority instead consists of the
|
|
12
|
+
actual public SessionStart session ID, kernel process birth identity, a fresh
|
|
13
|
+
registration generation, and the kernel identity of the helper retaining its
|
|
14
|
+
registration lease. No Claude connection/binding epochs are synthesized.
|
|
15
|
+
|
|
16
|
+
Managed Agent/Pane names and Claude session titles are not route authority.
|
|
17
|
+
Renaming an unchanged UID/activation preserves its registration. Phase 1 does
|
|
18
|
+
not discover unmanaged provider names or observe live Claude `/rename` events.
|
|
19
|
+
Competing session/process/helper claims for an exact activation are refused;
|
|
20
|
+
the exact child's next SessionStart atomically claims a new registration
|
|
21
|
+
generation before launching its helper. Delayed admission and cleanup from an
|
|
22
|
+
older generation cannot overwrite or clear that newer registration.
|
|
23
|
+
|
|
24
|
+
The activation gate records the actual child PID and kernel birth identity
|
|
25
|
+
before replacing itself with Claude. The separate managed SessionStart hook
|
|
26
|
+
uses `exec`, making the registered provider its direct parent. The helper
|
|
27
|
+
verifies the complete helper → hook → provider process chain while the hook
|
|
28
|
+
waits for a bounded startup acknowledgement. A nested unmanaged Claude cannot
|
|
29
|
+
register its own endpoint through inherited activation environment variables.
|
|
30
|
+
The creator-selected Registry path travels only in private Claude activation
|
|
31
|
+
context, independent of a tmux server's older XDG environment.
|
|
32
|
+
|
|
33
|
+
The hook obtains the socket and credential solely from Claude's documented
|
|
34
|
+
`CLAUDE_CODE_MESSAGING_SOCKET` and `CLAUDE_CODE_MESSAGING_TOKEN` environment.
|
|
35
|
+
These values pass to the helper over an anonymous pipe and remain in process
|
|
36
|
+
memory. They are absent from Registry, helper argv, helper environment, public
|
|
37
|
+
capabilities, diagnostic messages, and the content-free cleanup receipt.
|
|
38
|
+
Projmux does not predict, issue, read a vendor registration file for, connect
|
|
39
|
+
to, or write to the Claude inbox.
|
|
40
|
+
|
|
41
|
+
Readiness validates exact Registry ownership/generation, provider and helper
|
|
42
|
+
kernel birth identity, socket type/owner/mode and unchanged inode. The read-only
|
|
43
|
+
capability query probes only a short, private Projmux readiness socket and
|
|
44
|
+
authenticates its kernel peer PID and birth. Linux birth identity includes boot
|
|
45
|
+
ID and start ticks; Darwin uses kernel process start time. The private socket
|
|
46
|
+
path fits both supported Unix socket limits and does not depend on TMPDIR.
|
|
47
|
+
|
|
48
|
+
The helper invalidates on provider exit, socket replacement, or loss of exact
|
|
49
|
+
Registry authority. The supervisor independently detects helper death while
|
|
50
|
+
the provider remains alive, clears only that exact lease, and removes orphaned
|
|
51
|
+
helper files. Provider exit keeps the existing lock-free termination journal
|
|
52
|
+
boundary; it never waits for the cleanup watcher's Registry transaction.
|
|
53
|
+
Provider sockets are never removed by Projmux. Generation replacement and
|
|
54
|
+
termination convergence discard old registration observations.
|
|
55
|
+
|
|
56
|
+
The required deterministic process integration is
|
|
57
|
+
`test/integration/claude-endpoint-binding.sh`. It is included in
|
|
58
|
+
`make test-integration`. The opt-in `TestClaudeEndpointInstalledSourceGate`
|
|
59
|
+
requires `PMX_TEST_CLAUDE_ENDPOINT_BIN`, `PMX_TEST_REAL_CLAUDE_BIN`, and a prepared
|
|
60
|
+
disposable authenticated `PMX_TEST_REAL_CLAUDE_CONFIG_DIR`. The harness creates
|
|
61
|
+
its own provider config with an authentication symlink; it never reads or copies
|
|
62
|
+
the caller's credentials. Its one-shot uses
|
|
63
|
+
strict empty MCP, empty tools, empty setting sources, and no session persistence;
|
|
64
|
+
it checks public init tools/MCP/plugins and tool-use are all zero. Raw public
|
|
65
|
+
stream and messaging credentials stay in memory; only allowlisted receipts
|
|
66
|
+
are reported. The token scan covers all owned files, including provider source
|
|
67
|
+
state. Upstream may store its own public locator; the test observes only a
|
|
68
|
+
boolean and removes the entire owned provider config before reporting cleanup.
|
|
69
|
+
Projmux Registry, helper files, diagnostics, and evidence contain neither value.
|
|
70
|
+
Inbound cross-session messaging is explicitly refused during the one-shot.
|
|
71
|
+
This is historical Phase 1 registration-source evidence only. It does not
|
|
72
|
+
establish or relax Phase 4 qualification: safe mode disables hooks and cannot
|
|
73
|
+
substitute for the separately owned, long-lived, hook-enabled current-version
|
|
74
|
+
public-init and marker gate.
|
|
75
|
+
|
|
76
|
+
The live canary accepts only paired, zero-output lifecycle events for its two
|
|
77
|
+
owned SessionStart callbacks before init, correlated by exact session, hook ID,
|
|
78
|
+
name, and event. It strips the empty output fields before storing evidence.
|
|
79
|
+
Unobserved hook names, Setup, plugin installation, unknown fields/events,
|
|
80
|
+
nonzero output, and incomplete pairs fail closed before any peer push. This
|
|
81
|
+
startup ordering follows the [public stream contract](https://code.claude.com/docs/en/headless#read-session-metadata)
|
|
82
|
+
and [hook lifecycle schemas](https://code.claude.com/docs/en/agent-sdk/typescript#sdkhookstartedmessage).
|
|
83
|
+
The observed 2.1.263 public init also carries `capabilities` (protocol feature
|
|
84
|
+
identifiers), `fast_mode_disabled_reason` (`sdk_opt_in_required`), and boolean
|
|
85
|
+
`analytics_disabled` / `product_feedback_disabled` metadata. These fields are
|
|
86
|
+
validated by type and known shape; none grants tool, plugin, or messaging
|
|
87
|
+
authority. The owned wrapper sets the documented
|
|
88
|
+
[`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`](https://code.claude.com/docs/en/data-usage)
|
|
89
|
+
opt-out. A feedback metadata boolean is not a record of external traffic.
|
|
90
|
+
The observed `system/thinking_tokens` event carries only numeric progress
|
|
91
|
+
estimates. The collector accepts its exact public shape after init for the same
|
|
92
|
+
session, validates finite nonnegative numbers, and gives it no reply or tool
|
|
93
|
+
authority. It does not carry or collect thinking text.
|
|
94
|
+
Public assistant `request_id` and timestamp are validated metadata, never reply
|
|
95
|
+
selectors. The observed `context_management`, `diagnostics`, and `stop_details`
|
|
96
|
+
fields are accepted only as null. A public Messages API thinking block is
|
|
97
|
+
validated in memory, then replaced by an omitted-content marker before disk;
|
|
98
|
+
neither reasoning text nor signature is retained or sent to the provider. Only
|
|
99
|
+
text blocks can provide semantic reply evidence; tool and unknown blocks fail
|
|
100
|
+
closed. See the public [Messages schema](https://platform.claude.com/docs/en/api/http/beta/messages/create)
|
|
101
|
+
and [ThinkingBlock schema](https://platform.claude.com/docs/en/api/typescript/messages).
|
|
102
|
+
|
|
103
|
+
## Phase 4 push ingress and heterogeneous dialogue
|
|
104
|
+
|
|
105
|
+
The registration helper owns the existing private `coord-*` Unix socket for
|
|
106
|
+
the same lease. Its address is derived from the exact `AgentRouteRef`; names,
|
|
107
|
+
tmux `%N`, the provider socket, and the provider token do not enter that address
|
|
108
|
+
or its protocol. Every operation revalidates Agent UID, Pane UID, activation
|
|
109
|
+
generation, provider/helper process births, registration generation, and route
|
|
110
|
+
incarnation. Normal exit and cleanup unlink only the exact owned coord socket
|
|
111
|
+
inode. Projmux never creates a replacement provider listener or relay.
|
|
112
|
+
|
|
113
|
+
Claude ingress is immediate push. There is no receiver waiter, pending ingress
|
|
114
|
+
queue, `asyncRewake`, `begin-handoff`, or `no-waiter` state. The helper retains
|
|
115
|
+
the opaque `CLAUDE_CODE_MESSAGING_SOCKET` and token only from the original
|
|
116
|
+
anonymous bootstrap pipe, removes both keys from its environment, validates the
|
|
117
|
+
unchanged 0600 provider socket and exact provider peer process, then performs
|
|
118
|
+
one write containing the documented auth line followed by the single frozen
|
|
119
|
+
current-version user frame:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"type":"user","message":{"role":"user","content":"..."}}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
No control, status, reply, or inferred vendor frame is implemented. A known
|
|
126
|
+
failure before any byte is a non-ambiguous `provider-write-zero`; partial bytes,
|
|
127
|
+
a write error after bytes, or a lost helper response is ambiguous and has
|
|
128
|
+
`autoResend=false`. A complete write plus helper return is only a transport
|
|
129
|
+
handoff, not proof that Claude parsed, displayed, processed, or answered it.
|
|
130
|
+
|
|
131
|
+
The frozen frame is closed to Claude Code `2.1.263`. A different provider
|
|
132
|
+
version, helper replacement, provider restart, registration replacement, or
|
|
133
|
+
activation restart inherits no qualification. An ordinary send cannot qualify
|
|
134
|
+
the endpoint. The dedicated opt-in command requires a private 0600 sanitized
|
|
135
|
+
collector record bound to the same public init session/process/Pane/generation
|
|
136
|
+
and helper route, exact version, empty tools/MCP/plugins, zero pre-marker tool
|
|
137
|
+
use/stderr, frozen stream, and inbound policy `accept`:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
projmux agent message qualify uid:<claude-agent> \
|
|
141
|
+
--evidence /absolute/owned/private-init.json \
|
|
142
|
+
--confirm-isolated-provider-push -o json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The helper pushes a unique non-secret qualification marker. Only the exact
|
|
146
|
+
ordinary official `Stop.last_assistant_message` at the unchanged safe-boundary
|
|
147
|
+
epoch opens helper-memory eligibility. A missing/mismatched/recursive Stop,
|
|
148
|
+
`UserPromptSubmit`, timeout, target exit, or post-write uncertainty closes the
|
|
149
|
+
attempt as ambiguous/no-auto-resend. Late Stop cannot reopen it; retry requires
|
|
150
|
+
a new explicit command. A version string supplied by the caller is never proof.
|
|
151
|
+
|
|
152
|
+
After qualification, `agent message send` persists the exact immutable broker
|
|
153
|
+
handoff before any provider byte. A missing, mismatched, or unwritable durable
|
|
154
|
+
record therefore writes zero. The pushed content is a structured
|
|
155
|
+
`projmux-coordination` object with `untrusted-coordination-only` authority and
|
|
156
|
+
the exact source/target routes, `messageRef`, `conversationRef`, `replyTo`, and
|
|
157
|
+
payload. It cannot start or steer a user turn, answer an approval, interrupt a
|
|
158
|
+
turn, execute a tool, call a connector, or write Codex app-server/model history.
|
|
159
|
+
|
|
160
|
+
Reply egress uses only documented official `Stop.last_assistant_message` plus
|
|
161
|
+
one delivered Projmux-owned pending record at the same boundary. Push ingress
|
|
162
|
+
correlates only an ordinary Stop (`stop_hook_active=false`); recursive Stop,
|
|
163
|
+
`UserPromptSubmit`, multiple candidates, or authority drift fails closed.
|
|
164
|
+
Assistant wording is never a selector. The reply reverses the routes, preserves
|
|
165
|
+
`conversationRef`, sets `replyTo`, and completes only when the original exact
|
|
166
|
+
Codex Agent self-claims it.
|
|
167
|
+
|
|
168
|
+
| Receiver state or transition | Product result |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| Before readiness or qualification | Refused/failed terminal, held count zero, provider writes zero |
|
|
171
|
+
| Ready and idle | Immediate one-frame push; model visibility is separate evidence |
|
|
172
|
+
| Active tool/turn | Push never interrupts; next official human boundary makes reply correlation ambiguous |
|
|
173
|
+
| Provider exit or activation restart | Old generation writes and claims zero |
|
|
174
|
+
| Same-generation registration/helper replacement | Old incarnation writes zero; fresh exact-version qualification required |
|
|
175
|
+
| Provider version replacement | Old qualification is cleared; unqualified writes zero |
|
|
176
|
+
| Codex endpoint replacement | Old incarnation self-claim zero; exact new incarnation may claim |
|
|
177
|
+
|
|
178
|
+
Message state is receipt-only. It performs no Registry Agent-interaction or
|
|
179
|
+
tmux badge write, so it cannot overwrite `in_progress`, approval-required, or
|
|
180
|
+
their badges. Public terminal states remain terminal-once.
|
|
181
|
+
|
|
182
|
+
Explicit integration installs the SessionStart registration hook, one short
|
|
183
|
+
synchronous Stop reply hook, and one short synchronous UserPromptSubmit boundary
|
|
184
|
+
hook. It installs no long-lived ingress waiter and does not use `asyncRewake`.
|
|
185
|
+
Capability reads never modify settings. Preview, enable, and remove only the
|
|
186
|
+
owned hook entries:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
projmux agent integrate claude --dry-run
|
|
190
|
+
projmux agent integrate claude
|
|
191
|
+
projmux agent integrate claude --remove
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
For a pre-install Running Claude whose activation has no registration, install
|
|
195
|
+
the hooks, let that process exit normally, then run
|
|
196
|
+
`projmux agent resume uid:<same-agent-uid>`. Resume preserves the same Agent UID
|
|
197
|
+
and creates no replacement Agent. Delivery remains zero until the resumed
|
|
198
|
+
process has a current registration and fresh exact-version qualification.
|
|
199
|
+
`agent capabilities uid:<agent> -o json` reports source registration readiness
|
|
200
|
+
separately from target `runtimeEligibility.coordination` and includes the exact
|
|
201
|
+
recovery action and non-secret route incarnation.
|
|
202
|
+
|
|
203
|
+
The only selectorless required E2E is offline scenario `L20`. It uses one
|
|
204
|
+
synthetic auth+frozen-frame fixture, official-hook-shaped Stop input, semantic
|
|
205
|
+
barriers, exact structured receipts, and no model or installed provider. The
|
|
206
|
+
real-provider observation is opt-in through
|
|
207
|
+
[`heterogeneous-dialogue-canary.md`](heterogeneous-dialogue-canary.md) and
|
|
208
|
+
`scripts/agent-dialogue-live-canary.sh`; each version-stress row must qualify
|
|
209
|
+
independently.
|
|
210
|
+
|
|
211
|
+
Provider sources: [SessionStart and Stop hooks](https://code.claude.com/docs/en/hooks)
|
|
212
|
+
and [cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging).
|
|
213
|
+
|
|
214
|
+
Once a delivered message has unresolved reply ambiguity (an overlapping human
|
|
215
|
+
turn, multiple pending messages, expiry, or an uncertain write/reply outcome),
|
|
216
|
+
a later idle Stop cannot restore automatic reply correlation. The helper keeps
|
|
217
|
+
push ingress available but refuses automatic replies for that incarnation.
|
|
218
|
+
Use the documented public recovery on the same Agent UID and qualify its new
|
|
219
|
+
activation before resuming automatic dialogue. Source and target routes are
|
|
220
|
+
revalidated after durable handoff and immediately before the provider write.
|
|
221
|
+
|
|
222
|
+
The final source check also asks the existing Codex broker to verify the exact
|
|
223
|
+
runtime, connection and binding lease. This read-only IPC observation binds
|
|
224
|
+
nothing and sends no provider request. An older broker that does not support
|
|
225
|
+
this observation refuses coordination; it is never restarted implicitly.
|
|
226
|
+
Helper store lock contention fails immediately. Concurrent official hooks
|
|
227
|
+
invalidate reply correlation without waiting for another hook to finish.
|
|
228
|
+
|
|
229
|
+
The live harness begins with the benign `Reply READY.` control. The later broker
|
|
230
|
+
payload contains its own exact acknowledgement request; the initial user turn
|
|
231
|
+
does not authorize hypothetical future messages. Qualification keeps the same
|
|
232
|
+
frozen user frame and its existing one-shot marker. Observed rate/result metadata
|
|
233
|
+
is validated by closed shape and reduced before persistence. A refusal, API
|
|
234
|
+
error, queued user turn, permission denial, or nonzero subagent activity closes
|
|
235
|
+
the pre-inbound gate. Neither rate status nor timing/cost metadata is authority.
|
|
236
|
+
|
|
237
|
+
Usage is validated and discarded before persistence. Server tool counters must
|
|
238
|
+
be zero and tool/subagent iterations empty. After the semantic reply, the harness
|
|
239
|
+
requires three successful results in the same session, the public assistant
|
|
240
|
+
reply marker, and empty provider/collector stderr through owned cleanup.
|
|
241
|
+
|
|
242
|
+
Public model IDs and usage labels are bounded strings, including provider aliases
|
|
243
|
+
and context suffixes. They are discarded without imposing identifier syntax or
|
|
244
|
+
using pricing metadata as a permission or correlation source.
|
|
245
|
+
|
|
246
|
+
Failed live-canary cleanup captures exact Linux process births before unregister
|
|
247
|
+
and tmux teardown, including pane supervisors, their descendants, and the
|
|
248
|
+
detached registration helper. Supervisors write termination/operation receipts
|
|
249
|
+
after their provider exits. Both success and failure cleanup wait for captured
|
|
250
|
+
pidfds to report exit before removing the owned root. An unproven or stubborn
|
|
251
|
+
writer leaves the root for diagnosis and fails cleanup; no quiet-file interval
|
|
252
|
+
or process-name kill substitutes for exit proof. The owned credential copy is
|
|
253
|
+
removed even when cleanup cannot complete. The actual reply-origin qualification
|
|
254
|
+
blocker is unchanged by this cleanup guarantee.
|