@staix/agent-hub 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ # COMPAT receipt audit for CI automation (#1248 T0)
2
+
3
+ Date: 2026-11-07. Task #761. Receipt pointers were read from `src/adapters/compat.ts` at `3440e7b10dc6b4537d56541774436aa4f7d49a92`. The data source is [manifest.json](../../scripts/compat-receipts/manifest.json). This audit runs no agent CLI, installer, model, TUI or workflow and changes no verified version.
4
+
5
+ `autoAdmit` means policy eligibility after every required leg and artifact binding passes. It is not an implemented driver or a current pass. Missing work is not-run. One Mac arm64 job can repeat an eligible id's complete procedure, including its portable checks; the Linux category does not require an extra matrix job. Native macOS arm64 qualification is retained; a portable Linux fixture or source comparison does not qualify a Linux-only rerun. Qwen explicitly rejected Linux-only evidence. Optional, explicitly unverified model/TUI follow-ups do not become new admission requirements.
6
+
7
+ Source comparison can produce hashes and diffs on Linux. Changed security/contract regions still require scoped human review: diff availability alone is never a passing review leg. T1 must report such work as not-run until reviewed.
8
+
9
+ ## Per-id scheduling and eligibility
10
+
11
+ | COMPAT id | Model-free repeat | Remaining required scope | Installation | autoAdmit |
12
+ | --- | --- | --- | --- | --- |
13
+ | `pi` | Linux host permission/version fixtures; Mac RPC extension, SDK and tool receipts | Mac arm64 native | npm exact | `true` |
14
+ | `claude-implementer` | Mac Write/Bash/rewrite transport and wrapper controls, synthetic provider | Mac arm64 native | npm exact; native-installer receipt boundary | `true` |
15
+ | `codex-review` | Linux source comparison; Mac production metadata and native sandbox controls | Person-authorized model-backed tool call | npm exact | `false` |
16
+ | `kimi-review` | Linux source comparison; Mac private config/broker/native allowlist controls | Mac arm64 native | GitHub asset + SHA-256 | `true` |
17
+ | `jcode` | Linux source comparison; Mac serve/ACP/MCP and fixed-stream steering | Mac arm64 native | GitHub asset + SHA256SUMS | `true` |
18
+ | `agy` | Mac CLI, synthetic MCP/hooks and Seatbelt controls | Interactive TUI/launcher nonce; no public install source | Unavailable | `false` |
19
+ | `qwen` | Mac ACP/permission and install replay; recorded Linux run is additional | Mac arm64 repetition required | npm exact | `true` |
20
+ | `qwen-dual` | Version/file-source checks are partial only | Real gateway model and PTY; OS not recorded | npm exact; historical artifact gap | `false` |
21
+
22
+ The macOS requirement follows each receipt's native artifact/host boundary; the manifest does not claim those native procedures have passed on Linux. `qwen-dual`'s OS remains unspecified because its current receipt does not name one. `codex-review` stays false even with successful model-free preparation; its separately authorized 0.167.0 model-backed attempt has an unknown outcome. `agy` stays false independently of model-free sublegs because there is no public install source.
23
+
24
+ ## Installation evidence and boundaries
25
+
26
+ Install methods describe exact future candidate acquisition, with no global installer. npm candidates must bind to their exact-version `dist.integrity`; release candidates must verify the versioned checksum and GitHub asset digest before bounded extraction. Reference hashes in the manifest are observed metadata or receipt bindings, not a new execution result.
27
+
28
+ - Claude 2.1.319 was observed through the native installer while npm lagged. The exact npm version is now publicly available and its SRI was read without downloading or executing it. That does not equate its tarball with the receipt's native binary SHA.
29
+ - Jcode's receipt identifies an installed release build. The v0.99.1 release API now supplies `jcode-macos-aarch64.tar.gz`, an asset SHA-256 and `SHA256SUMS`; that future acquisition recipe is not retrospective archive-execution proof.
30
+ - Kimi's reference receipt already binds its official macOS arm64 archive to the GitHub digest and sibling checksum.
31
+ - Codex's native binary hashes and qwen-dual's changing fnm global install are not npm SRI evidence. Public exact-version npm metadata was read separately.
32
+ - No npm/GitHub method is invented for agy. Its unavailable install method makes an automated run not-run.
33
+
34
+ ## pi
35
+
36
+ Current receipt: [2026-11-03-pi-150-compat.md](2026-11-03-pi-150-compat.md). `autoAdmit: true`. The admitted procedure is model-free; real inference and interactive coexistence are explicitly unverified follow-ups.
37
+
38
+ ### Recorded receipt legs
39
+
40
+ - **native-identity**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Native identity".
41
+ - **cli-rpc-session**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Real CLI extension load and RPC session identity".
42
+ - **sdk-registration**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Installed SDK extension registration".
43
+ - **tool-replay**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Managed tool callback and receipt replay".
44
+ - **failed-tool**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Failed tool result".
45
+ - **zero-provider-requests**: `macos-runner`, required, runner OS `macos`. [## Results](2026-11-03-pi-150-compat.md): "Zero requests reached the loopback provider".
46
+ - **host-permission-modes**: `linux-ci`, required, runner OS `linux, macos`. [## Results](2026-11-03-pi-150-compat.md): "Host permission modes".
47
+ - **version-admission**: `linux-ci`, required, runner OS `linux, macos`. [## Results](2026-11-03-pi-150-compat.md): "Version admission".
48
+ - **isolated-home-cleanup**: `macos-runner`, required, runner OS `macos`. [## Identity and isolation](2026-11-03-pi-150-compat.md): "9551 entries, identical.".
49
+
50
+ ### Explicitly unverified follow-ups
51
+
52
+ - **real-inference-tools-quota**: `account-or-model`, not required by the current admission receipt. [## Not verified](2026-11-03-pi-150-compat.md): "Account-backed inference, a model-generated tool request".
53
+ - **interactive-coexistence**: `tui-desktop`, not required by the current admission receipt. [## Not verified](2026-11-03-pi-150-compat.md): "interactive TUI coexistence.".
54
+ - **native-linux-intel**: `linux-ci`, not required by the current admission receipt. [## Not verified](2026-11-03-pi-150-compat.md): "Linux and Intel macOS.".
55
+
56
+ ## claude-implementer
57
+
58
+ Current receipt: [2026-11-06-claude-implementer-2.1.319.md](2026-11-06-claude-implementer-2.1.319.md). `autoAdmit: true`. Permission transport and wrapper controls use a synthetic provider, with no real account or model required.
59
+
60
+ ### Recorded receipt legs
61
+
62
+ - **write-allow**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
63
+ - **write-deny**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
64
+ - **write-missing-tool**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
65
+ - **write-hook-deny**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
66
+ - **write-hook-allow**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
67
+ - **write-mcp-error**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
68
+ - **write-disconnect**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
69
+ - **write-pending**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
70
+ - **write-deadline**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Write transport (allow, deny, missing tool, hook deny/allow, MCP error, disconnect, pending, deadline)".
71
+ - **bash-allow**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
72
+ - **bash-deny**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
73
+ - **bash-missing-tool**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
74
+ - **bash-hook-deny**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
75
+ - **bash-hook-allow**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
76
+ - **bash-mcp-error**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
77
+ - **bash-disconnect**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
78
+ - **bash-pending**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
79
+ - **bash-deadline**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash transport, production server and tool names, `tool_use_id` required".
80
+ - **bash-rewrite-allow**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash `updatedInput` command replacement (allow, deny)".
81
+ - **bash-rewrite-deny**: `macos-runner`, required, runner OS `macos`. [## Runs](2026-11-06-claude-implementer-2.1.319.md): "Bash `updatedInput` command replacement (allow, deny)".
82
+ - **wrapper-cwd**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Persistent-shell control reports `sub` twice".
83
+ - **wrapper-root**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Fresh rewritten input executes the fixed root".
84
+ - **wrapper-executable**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Target identity equals the absolute host-selected executable".
85
+ - **wrapper-poison-env**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Injected native GIT_DIR and RIPGREP_CONFIG_PATH are absent".
86
+ - **wrapper-host-env**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Target HOME/PATH/TERM and peer identity equal the host fixture values".
87
+ - **wrapper-tmp**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Owned TMPDIR replaces native state".
88
+ - **wrapper-replay**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Same ticket's second execution returns the fixed refusal".
89
+ - **wrapper-effect-once**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Effect counter remains exactly 1 after replay".
90
+ - **wrapper-quoted-paths**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Spaces and apostrophes in root/executable paths".
91
+ - **wrapper-cleanup**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Native exit 0, host stopped, no remaining live native group members".
92
+ - **wrapper-binary-fence**: `macos-runner`, required, runner OS `macos`. [## Native observations](2026-10-26-headless-claude-shell-context.md): "Binary unchanged across the case".
93
+ - **upstream-impact-review**: `linux-ci`, required, runner OS `linux, macos`. [## Upstream changes since 2.1.318](2026-11-06-claude-implementer-2.1.319.md): "This receipt therefore rests on observed behavior only".
94
+
95
+ ### Explicitly unverified follow-ups
96
+
97
+ - **real-provider-model**: `account-or-model`, not required by the current admission receipt. [## Evidence boundaries](2026-11-06-claude-implementer-2.1.319.md): "no real provider authentication, model tool selection".
98
+ - **person-approval**: `tui-desktop`, not required by the current admission receipt. [## Evidence boundaries](2026-11-06-claude-implementer-2.1.319.md): "person approval interaction".
99
+ - **native-task-resume-binding**: `account-or-model`, not required by the current admission receipt. [## Evidence boundaries](2026-11-06-claude-implementer-2.1.319.md): "task-session resume or production-daemon task binding is proved".
100
+ - **host-seatbelt-policy**: `macos-runner`, not required by the current admission receipt. [## Host fixtures and ceilings](2026-10-26-headless-claude-shell-context.md): "The executable task-code profile is host policy verified separately by Darwin fixtures.".
101
+ - **native-internal-timeout**: `macos-runner`, not required by the current admission receipt. [## Observations](2026-10-23-headless-claude-permissions.md): "native internal timeout not established".
102
+
103
+ ## codex-review
104
+
105
+ Current receipt: [2026-11-06-codex-0167-reviewer-isolation.md](2026-11-06-codex-0167-reviewer-isolation.md). `autoAdmit: false`. Admission requires a successful separately authorized model-backed hub tool call, even when every model-free preparation leg passes.
106
+
107
+ ### Recorded receipt legs
108
+
109
+ - **source-impact**: `linux-ci`, required, runner OS `linux, macos`. [## Source identity and re-read regions](2026-11-06-codex-0167-reviewer-isolation.md): "Each of the seven files read for 0.166.0".
110
+ - Changed source: not-run until the changed security/contract regions have been reviewed; never infer a pass from diff availability.
111
+ - **tag-binding**: `linux-ci`, required, runner OS `linux, macos`. [## Source identity and re-read regions](2026-11-06-codex-0167-reviewer-isolation.md): "annotated tag object".
112
+ - **version-negative**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "Refused only at version admission".
113
+ - **empty-home-prepare**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "empty homes | All production preparation legs pass".
114
+ - **ambient-prepare**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "synthetic ambient config | All production preparation legs pass".
115
+ - **control**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "Control 0.166.0, empty homes".
116
+ - **model-free-mcp-inventory-disabled-ambient**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "model-free-mcp-inventory-disabled-ambient".
117
+ - **hub-tools-exact-allowlist**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "hub-tools-exact-allowlist".
118
+ - **plugins-and-hooks-disabled**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "plugins-and-hooks-disabled".
119
+ - **legacy-notify-disabled**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "legacy-notify-disabled".
120
+ - **apps-disabled-effective-feature-readback**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "apps-disabled-effective-feature-readback".
121
+ - **web-search-disabled**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "web-search-disabled".
122
+ - **project-untrusted**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "project-untrusted".
123
+ - **native-public-read-private-deny-write-deny**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "native-public-read-private-deny-write-deny".
124
+ - **native-glob-and-symlink-private-deny**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "native-glob-and-symlink-private-deny".
125
+ - **portable-core-name-and-state-deny**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "portable-core-name-and-state-deny".
126
+ - **standard-host-credential-roots-denied**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "standard-host-credential-roots-denied".
127
+ - **binary-config-stable**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "binary-config-stable".
128
+ - **binary-hash-once-metadata-fence**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "binary-hash-once-metadata-fence".
129
+ - **session-ephemeral**: `macos-runner`, required, runner OS `macos`. [## Native production preparation](2026-11-06-codex-0167-reviewer-isolation.md): "session-ephemeral".
130
+ - **model-backed-hub-call**: `account-or-model`, required, runner OS `macos`. [## Reviewers](../agent-notes/adapters.md): "before marking a reviewer backend/version verified, record a successful model-backed hub tool call".
131
+
132
+ ### Explicitly unverified follow-ups
133
+
134
+ - **effective-network-web**: `account-or-model`, not required by the current admission receipt. [## Compatibility and evidence boundaries](2026-11-06-codex-0167-reviewer-isolation.md): "The network setting is configured disabled without a separate probe.".
135
+ - **default-profile-selection**: `account-or-model`, not required by the current admission receipt. [## Compatibility and evidence boundaries](2026-11-06-codex-0167-reviewer-isolation.md): "exec selecting the named default profile".
136
+ - **native-linux**: `linux-ci`, not required by the current admission receipt. [## Compatibility and evidence boundaries](2026-11-06-codex-0167-reviewer-isolation.md): "No native Linux result is claimed.".
137
+
138
+ ## kimi-review
139
+
140
+ Current receipt: [2026-11-06-kimi-251-reviewer-compat.md](2026-11-06-kimi-251-reviewer-compat.md). `autoAdmit: true`. The required native allowlist/broker controls use fixed loopback replies and synthetic credentials, without real inference.
141
+
142
+ ### Recorded receipt legs
143
+
144
+ - **prepare**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Production preparation, exact version/current/config, replace environment".
145
+ - **inventory**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Exactly two MCP tools, empty subagents and skills, config mode 0600".
146
+ - **broker-refusals**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Wrong task, third tool, missing authorization and revoked broker refusals".
147
+ - **native-tool-loop**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Actual native two-tool loop, fixed loopback responses".
148
+ - **write-absent**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Injected Write absent from inventory and native tool-not-found result".
149
+ - **write-positive-control**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Explicit Write positive control".
150
+ - **environment-decoy**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Trust-workspace environment decoy removed from launch environment".
151
+ - **cleanup**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "Original synthetic config/credential bytes, owned-group and temporary cleanup".
152
+ - **network-guard-control**: `macos-runner`, required, runner OS `macos`. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "A separate preload positive control blocked a".
153
+ - **source-review**: `linux-ci`, required, runner OS `linux, macos`. [## Source review](2026-11-06-kimi-251-reviewer-compat.md): "Model-free native exact-two-tool inventories across all fixed-response requests are still required".
154
+ - Changed source: not-run until the changed security/contract regions have been reviewed; never infer a pass from diff availability.
155
+
156
+ ### Explicitly unverified follow-ups
157
+
158
+ - **real-oauth-provider**: `account-or-model`, not required by the current admission receipt. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "login/provider readiness, real OAuth refresh".
159
+ - **model-quota**: `account-or-model`, not required by the current admission receipt. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "model
160
+ behavior/quality and quota remain unverified.".
161
+ - **tui-coexistence**: `tui-desktop`, not required by the current admission receipt. [## Isolated production and native controls](2026-11-06-kimi-251-reviewer-compat.md): "attached-TUI coexistence".
162
+ - **other-platforms**: `linux-ci`, not required by the current admission receipt. [## Evidence and decision](2026-11-06-kimi-251-reviewer-compat.md): "Other platforms, future versions and model-backed
163
+ behavior are not inferred.".
164
+
165
+ ## jcode
166
+
167
+ Current receipt: [2026-10-26-jcode-0.99.1.md](2026-10-26-jcode-0.99.1.md). `autoAdmit: true`. ACP, serve, MCP and running-turn steering use an isolated fixed-stream loopback provider; the real TUI/model follow-ups are not current admission legs.
168
+
169
+ ### Recorded receipt legs
170
+
171
+ - **isolation**: `macos-runner`, required, runner OS `macos`. [## Isolation](2026-10-26-jcode-0.99.1.md): "Every process ran under an isolated `HOME` and `JCODE_HOME`".
172
+ - **serve-cleanup**: `macos-runner`, required, runner OS `macos`. [## Contract legs (idle server, no model)](2026-10-26-jcode-0.99.1.md): "The process group is gone after SIGTERM.".
173
+ - **acp-initialize-new-load-mode-ceilings**: `macos-runner`, required, runner OS `macos`. [## Contract legs (idle server, no model)](2026-10-26-jcode-0.99.1.md): "`session/load` returns `configOptions` and `models` only, with no `sessionId`.".
174
+ - **mcp-owned-environment**: `macos-runner`, required, runner OS `macos`. [## Contract legs (idle server, no model)](2026-10-26-jcode-0.99.1.md): "its environment held exactly `AGENTHUB_PEER_ID` and `AGENTHUB_LAUNCH_ID`".
175
+ - **telemetry**: `macos-runner`, required, runner OS `macos`. [## Contract legs (idle server, no model)](2026-10-26-jcode-0.99.1.md): "no telemetry file appeared in the isolated home.".
176
+ - **active-turn-soft-interrupt**: `macos-runner`, required, runner OS `macos`. [## Steering leg (the 0.99.0 check, now on a running turn)](2026-10-26-jcode-0.99.1.md): "No real provider, key or model was involved.".
177
+ - **source-comparison**: `linux-ci`, required, runner OS `linux, macos`. [## Source comparison v0.99.0..v0.99.1](2026-10-26-jcode-0.99.1.md): "232 commits, 300 files, read only through the GitHub compare API (nothing built).".
178
+ - Changed source: not-run until the changed security/contract regions have been reviewed; never infer a pass from diff availability.
179
+
180
+ ### Explicitly unverified follow-ups
181
+
182
+ - **real-provider-model-tool-call**: `account-or-model`, not required by the current admission receipt. [## Not verified](2026-10-26-jcode-0.99.1.md): "A real account, provider or model, and a model turn that calls a hub tool".
183
+ - **tui-repeat**: `tui-desktop`, not required by the current admission receipt. [## Not verified](2026-10-26-jcode-0.99.1.md): "The TUI form (`jcode --resume`)".
184
+ - **linux-intel-second-instance**: `linux-ci`, not required by the current admission receipt. [## Not verified](2026-10-26-jcode-0.99.1.md): "A second instance running beside the base peer, and Linux or Intel macOS builds.".
185
+
186
+ ## agy
187
+
188
+ Current receipt: [2026-11-03-agy-1.4.12-launcher-nonce.md](2026-11-03-agy-1.4.12-launcher-nonce.md). `autoAdmit: false`. No public installation source is available to the watcher; the current procedure also requires interactive TUI/launcher controls.
189
+
190
+ ### Recorded receipt legs
191
+
192
+ - **isolation-seatbelt-cleanup**: `macos-runner`, required, runner OS `macos`. [## Environment and isolation](2026-11-03-agy-1.4.12-launcher-nonce.md): "No account and no cloud model call.".
193
+ - **cli-contract**: `macos-runner`, required, runner OS `macos`. [# Antigravity 1.4.12 actual-hub nonce leg through the `ahub agy` launcher (#888)](2026-11-03-agy-1.4.12-launcher-nonce.md): "passed the CLI surface, the synthetic plugin MCP and hook contract and the normal TUI close".
194
+ - **synthetic-plugin-mcp-hooks**: `macos-runner`, required, runner OS `macos`. [## Synthetic plugin MCP and hook contract: passed](2026-11-03-agy-1.4.12.md): "## Synthetic plugin MCP and hook contract: passed".
195
+ - **normal-tui-close**: `tui-desktop`, required, runner OS `macos`. [## Result](2026-11-03-agy-1.4.12-launcher-nonce.md): "0, after `/exit` and Enter; agy closed by itself and printed its resume line".
196
+ - **launcher-actual-hub-nonce**: `tui-desktop`, required, runner OS `macos`. [## Result](2026-11-03-agy-1.4.12-launcher-nonce.md): "the nonce from the scratch bus preface appeared in a provider request after the tool call".
197
+
198
+ ### Explicitly unverified follow-ups
199
+
200
+ - **real-account-model-permission**: `account-or-model`, not required by the current admission receipt. [## Not verified](2026-11-03-agy-1.4.12-launcher-nonce.md): "A real account, a real model's tool choices and agy's own permission prompts".
201
+ - **ide-linux-other-tui-tools**: `tui-desktop`, not required by the current admission receipt. [## Not verified](2026-11-03-agy-1.4.12-launcher-nonce.md): "`hub_send` and board calls through the TUI, the IDE form and Linux.".
202
+
203
+ ## qwen
204
+
205
+ Current receipt: [2026-11-06-qwen-0268-acp.md](2026-11-06-qwen-0268-acp.md). `autoAdmit: true`. The admitted ACP/permission procedure uses no prompts or real model; macOS arm64 repetition remains required.
206
+
207
+ ### Recorded receipt legs
208
+
209
+ - **artifact-private-install-integrity**: `macos-runner`, required, runner OS `macos`. [## Artifact and isolation](2026-11-06-qwen-0268-acp.md): "npm installed into a new private prefix with scratch HOME, cache and tmp, empty user/global npm configs".
210
+ - **reviewed-optional-lifecycle-replay**: `macos-runner`, required, runner OS `macos`. [## Artifact and isolation](2026-11-06-qwen-0268-acp.md): "Fresh ACP and permission probes after the replay passed again.".
211
+ - **native-acp-auth-session-modes**: `macos-runner`, required, runner OS `macos`. [## Native ACP, no prompts](2026-11-06-qwen-0268-acp.md): "ACP prompts sent: 0.".
212
+ - **actual-published-permission-handler**: `macos-runner`, required, runner OS `macos`. [## Actual published permission handler](2026-11-06-qwen-0268-acp.md): "without a model-generated tool turn.".
213
+ - **preload-network-cleanup**: `macos-runner`, required, runner OS `macos`. [## Artifact and isolation](2026-11-06-qwen-0268-acp.md): "This is preload evidence, not kernel network isolation.".
214
+ - **native-macos-repetition**: `macos-runner`, required, runner OS `macos`. [## Actual macOS arm64 repetition](2026-11-06-qwen-0268-acp.md): "The Linux-only receipt did not prove admission on this Mac.".
215
+ - **additional-linux**: `linux-ci`, recorded, not required for admission, runner OS `linux`. [# Qwen Code 0.26.8 ACP receipt](2026-11-06-qwen-0268-acp.md): "The earlier Linux x86_64 run is additional evidence.".
216
+
217
+ ### Explicitly unverified follow-ups
218
+
219
+ - **real-model-microphone-tui-intel**: `account-or-model`, not required by the current admission receipt. [## Not verified](2026-11-06-qwen-0268-acp.md): "Microphone functionality, Intel macOS, a real login/provider, model-generated permission turns, model behavior, token/quota behavior and TUI qualification.".
220
+
221
+ ## qwen-dual
222
+
223
+ Current receipt: [2026-10-25-qwen-dual-output-t0.md](2026-10-25-qwen-dual-output-t0.md). `autoAdmit: false`. The current receipt uses a real gateway model and an interactive PTY; model-free automation cannot admit a version.
224
+
225
+ ### Recorded receipt legs
226
+
227
+ - **real-model**: `account-or-model`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "`flashnext/qwen3.8-flash-next` through the campus gateway".
228
+ - **pty-harness**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "the TUI ran on a pseudo-terminal (120x40).".
229
+ - **Q2-submit-stream-queue**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "queued, not interleaved".
230
+ - **Q3-permissions-native-enter-deny-ceiling**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "A tool that needs approval emits `control_request` `can_use_tool`".
231
+ - **Q4-known-watchdog-ceiling**: `tui-desktop`, recorded, not required for admission, runner OS `unspecified`. [# Qwen dual-output TUI amendment, 2026-10-25 (#286 Part 3)](../specs/2026-09-19-agent-hub-design.md): "Q2, Q3, Q5 and Q6 pass; Q4's limit is stated".
232
+ - Recorded accepted watchdog limitation, not a passing cancellation capability; preserve it in future receipts.
233
+ - **Q5-unread-fifo**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "With `--json-file` a FIFO that nobody opened for reading".
234
+ - **Q6-resume-no-replay**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "did not replay any earlier `submit`, and appended to the event file.".
235
+ - **Q7-version-identity**: `tui-desktop`, required, runner OS `unspecified`. [# Qwen dual-output TUI verification, 2026-10-25](2026-10-25-qwen-dual-output-t0.md): "`qwen --version` under the isolated environment: 0.26.1.".
236
+
237
+ ### Explicitly unverified follow-ups
238
+
239
+ - None separately identified.
240
+
241
+ ## How an automation-opened PR gets checks
242
+
243
+ Token choice remains with the person. This audit enables neither route. Current [GitHub trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow) describes approval-required runs for GITHUB_TOKEN-created opened/synchronize/reopened PR events; dispatch events remain an exception. This qualifies the issue's older no-trigger shorthand and does not establish unattended PR CI. This is documentation readback, not a live token experiment.
244
+
245
+ | Route | Benefit | Trade-off and integration work |
246
+ | --- | --- | --- |
247
+ | GITHUB_TOKEN plus explicit `check.yml` dispatch on `compat/<id>-<version>` | Avoids introducing a person-supplied App private key; dispatch can start the full check independently of PR-event approval | Publish job needs dispatch permission; capture and validate the run's exact head SHA. `check.yml` already accepts workflow_dispatch with normal inputs. Dispatch shares its existing check-main queue. It does not itself run the separate closing-refs or routing-receipt workflows; T2 must cover those gates or retain required approval. |
248
+ | Person-provided GitHub App installation token for branch/PR writes | Normal PR events can start all existing checks automatically | The person must provide a narrowly scoped installation and App credentials, with rotation/revocation ownership. Credentials belong only in the publish job, never the third-party package/harness job. Normal review and merge gates remain. |
249
+
250
+ The [workflow dispatch API](https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event) accepts a branch/tag ref and requires Actions write permission for fine-grained tokens. Run evidence must bind to the PR's actual head, rather than merely a branch name. [GitHub App authentication](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow) documents creating an installation token from the App credentials.
251
+
252
+ Repository readback: `.github/workflows/check.yml` has workflow_dispatch, reads the dispatched SHA when there is no pull_request head, and uses check-main concurrency for ordinary dispatches. `.github/workflows/closing-refs.yml` and `.github/workflows/routing-receipt.yml` currently have only pull_request triggers. No workflow file changed here.
253
+
254
+ ## Scope and handoff
255
+
256
+ T0 delivers data, provenance and a coverage test only. T1 still implements bounded isolated installation/probes and fail/not-run reporting; T2 still integrates the watcher, no-secret/read-only harness, artifact-only publisher and the person's chosen check-trigger mechanism. No token has been selected or provisioned, and no admission PR is auto-merged.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
5
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
6
6
  "paths": [
7
7
  "package.json",
8
8
  "src/",
@@ -11,14 +11,14 @@
11
11
  ]
12
12
  },
13
13
  "docs/security.md": {
14
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
14
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
21
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
22
22
  "paths": [
23
23
  "package.json",
24
24
  "src/",
@@ -35,7 +35,7 @@
35
35
  ]
36
36
  },
37
37
  "docs/quickstart.md": {
38
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
38
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
39
39
  "paths": [
40
40
  "package.json",
41
41
  "src/",
@@ -43,7 +43,7 @@
43
43
  ]
44
44
  },
45
45
  "docs/agent-notes/adapters.md": {
46
- "verifiedAgainst": "10efdc8b0ea114ae858fff4ede65de09f6c55604",
46
+ "verifiedAgainst": "d31d7503fce542f37ed9cdacb3c40fc86786cb09",
47
47
  "paths": [
48
48
  "src/adapters/",
49
49
  "src/pi/",
@@ -132,7 +132,7 @@
132
132
  ]
133
133
  },
134
134
  "docs/agent-notes/daemon.md": {
135
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
135
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
136
136
  "paths": [
137
137
  "src/hub/daemon.ts",
138
138
  "src/hub/control-client.ts",
@@ -243,7 +243,7 @@
243
243
  ]
244
244
  },
245
245
  "docs/agent-notes/tasks.md": {
246
- "verifiedAgainst": "10efdc8b0ea114ae858fff4ede65de09f6c55604",
246
+ "verifiedAgainst": "d31d7503fce542f37ed9cdacb3c40fc86786cb09",
247
247
  "paths": [
248
248
  "src/hub/tasks.ts",
249
249
  "src/hub/board.ts",
@@ -291,7 +291,7 @@
291
291
  ]
292
292
  },
293
293
  "docs/agent-notes/tests.md": {
294
- "verifiedAgainst": "ab8ae6f400d741ee9060d3087489e59996a72086",
294
+ "verifiedAgainst": "fc75a18dc2fa92b8372c19371c23ac6088d21ca3",
295
295
  "paths": [
296
296
  "test/",
297
297
  "scripts/check.sh",
@@ -329,7 +329,7 @@
329
329
  ]
330
330
  },
331
331
  "src/cli/help.ts": {
332
- "verifiedAgainst": "af37f8fd8b44a2c566aa47629ec5b02ee9f91d69",
332
+ "verifiedAgainst": "d31d7503fce542f37ed9cdacb3c40fc86786cb09",
333
333
  "paths": [
334
334
  "src/cli/main.ts",
335
335
  "src/cli/launch.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",
@@ -10248,7 +10248,7 @@ var package_default;
10248
10248
  var init_package = __esm(() => {
10249
10249
  package_default = {
10250
10250
  name: "@staix/agent-hub",
10251
- version: "0.33.0",
10251
+ version: "0.34.1",
10252
10252
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
10253
10253
  license: "MIT",
10254
10254
  type: "module",
@@ -1,5 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { stablePackageRoot } from "../../cli/install-paths.ts";
3
5
  import { realPath } from "../../hub/project.ts";
4
6
  import { VERSION } from "../../version.ts";
5
7
 
@@ -53,11 +55,11 @@ export type PluginState =
53
55
  | { state: "stale"; version: string; why: string; notices?: string[] };
54
56
 
55
57
  const safeDisplayValue = (value: unknown): string | undefined =>
56
- typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) ? value : undefined;
58
+ typeof value === "string" && value.length <= 4096 && value.isWellFormed() && !/[\p{Cc}\p{Cf}]/u.test(value) ? value : undefined;
57
59
 
58
60
  function nonUserPluginNotices(plugins: InstalledPlugin[]): string[] {
59
61
  return plugins.filter(plugin => plugin.id === PLUGIN && plugin.scope !== "user").map(plugin => {
60
- const scope = plugin.scope === "project" || plugin.scope === "local" ? plugin.scope : "unknown-scope";
62
+ const scope = safeDisplayValue(plugin.scope) ?? "unknown-scope";
61
63
  const path = safeDisplayValue(plugin.projectPath) ?? safeDisplayValue(plugin.installPath);
62
64
  const version = safeDisplayValue(plugin.version) ?? "unknown";
63
65
  return `${scope} plugin row preserved${path ? ` at ${path}` : ""} (version ${version}); setup never removes non-user scopes`;
@@ -90,14 +92,21 @@ export interface SetupStep {
90
92
  * The next thing `ahub setup` has to do, or nothing. One step at a time: the state is read again after every step,
91
93
  * because what a step leaves behind is Claude Code's business (removing a marketplace may uninstall its plugins).
92
94
  */
93
- export function nextStep(plugins: InstalledPlugin[], marketplaces: Marketplace[], packageRoot: string): SetupStep | undefined {
95
+ export function nextStep(plugins: InstalledPlugin[], marketplaces: Marketplace[], packageRoot: string, personHome = homedir()): SetupStep | undefined {
94
96
  const root = real(packageRoot);
95
97
  const market = marketplaces.find((m) => m.name === MARKETPLACE);
96
98
  if (market && real(market.path ?? "") !== root) {
97
- if (plugins.some(plugin => plugin.id === PLUGIN && plugin.scope !== "user")) return { why: `the ${MARKETPLACE} marketplace points at another source; removing it may remove non-user plugin rows`, argv: [], blocked: true };
99
+ const blockers = plugins.filter(plugin => plugin.id === PLUGIN && plugin.scope !== "user");
100
+ if (blockers.length) {
101
+ const display = (value: unknown) => JSON.stringify(safeDisplayValue(value) ?? "unknown");
102
+ const rows = blockers.map(plugin => `scope=${display(plugin.scope)}, projectPath=${display(plugin.projectPath)}, version=${display(plugin.version)}, installPath=${display(plugin.installPath)}`).join("; ");
103
+ const homeProject = blockers.some(plugin => safeDisplayValue(plugin.projectPath) !== undefined && real(plugin.projectPath!) === real(personHome));
104
+ const danger = homeProject ? ` Do not run 'claude plugin uninstall --scope project' from $HOME (${display(personHome)}): its project settings file ~/.claude/settings.json (${display(join(personHome, ".claude/settings.json"))}) is also the default home's user settings.` : "";
105
+ return { why: `the ${MARKETPLACE} marketplace points at another source; removing it may remove non-user plugin rows. Blocking rows: ${rows}. Hub-launched Claude sessions load their plugin from ~/.agenthub/releases (${display(join(personHome, ".agenthub/releases"))}), not from these plugin-cache rows. Standalone Claude sessions may still use them.${danger} Next: back up the selected Claude home's plugins/installed_plugins.json, review every listed row, and manually remove only rows confirmed stale and unused, preserving every other row; then rerun ahub setup. Setup never removes these rows automatically`, argv: [], blocked: true };
106
+ }
98
107
  return { why: `the ${MARKETPLACE} marketplace points at ${market.path ?? "another source"}, not at this package`, argv: ["claude", "plugin", "marketplace", "remove", MARKETPLACE] };
99
108
  }
100
- if (!market) return { why: "register this package as a Claude Code plugin marketplace", argv: ["claude", "plugin", "marketplace", "add", root] };
109
+ if (!market) return { why: "register this package as a Claude Code plugin marketplace", argv: ["claude", "plugin", "marketplace", "add", stablePackageRoot(root)] };
101
110
  const state = pluginState(plugins, root);
102
111
  if (state.state === "missing") return { why: "install the user-scope channel plugin", argv: ["claude", "plugin", "install", PLUGIN, "--scope", "user"] };
103
112
  // Uninstall rather than update: with an unchanged version number an update has nothing to do.
@@ -22,12 +22,21 @@ export async function usedClaudeHomes(registry: Registry, home: string, env: Nod
22
22
  return { ...found, complete: found.complete && !notices.length, notices: [...notices, ...found.notices] };
23
23
  }
24
24
  export async function autonomyHomeStatus(registry: Registry, home: string, env: NodeJS.ProcessEnv, packageRoot: string, probe: ClaudeHomeProbe) {
25
- const found = await usedClaudeHomes(registry, home, env), deadline = performance.now() + 6000;
25
+ const found = await usedClaudeHomes(registry, home, env);
26
+ // Doctor also inspects the existing default home; setup's selected write inventory stays unchanged.
27
+ const defaults = await discoverClaudeHomes({ home, env: { ...env, CLAUDE_CONFIG_DIR: undefined } });
28
+ // Discovery canonicalizes existing homes through realPath, so lexical aliases share one inspection.
29
+ const inspected = new Map(found.homes.map(selected => [selected.path, selected]));
30
+ for (const selected of defaults.homes.filter(selected => selected.exists)) {
31
+ const previous = inspected.get(selected.path);
32
+ inspected.set(selected.path, previous ? { ...previous, sources: [...new Set([...previous.sources, ...selected.sources])] } : selected);
33
+ }
34
+ const notices = [...new Set([...found.notices, ...defaults.notices])], deadline = performance.now() + 6000;
26
35
  const homes = [];
27
- for (const selected of found.homes) {
36
+ for (const selected of inspected.values()) {
28
37
  const remaining = Math.floor(deadline - performance.now());
29
38
  if (remaining < 1) homes.push({ home: selected, plugin: { state: "unknown", reason: "The machine profile inventory time bound was reached." }, hook: checkClaudeProfileHook(selected) });
30
39
  else homes.push(await checkClaudeHome(selected, packageRoot, env, probe, Math.min(remaining, 6000)));
31
40
  }
32
- return { homes, notices: found.notices, complete: found.complete && performance.now() <= deadline };
41
+ return { homes, notices, complete: found.complete && defaults.complete && performance.now() <= deadline };
33
42
  }
@@ -3,7 +3,7 @@ import { isAbsolute, join } from "node:path";
3
3
  import { realPath } from "../hub/project.ts";
4
4
  import { launcherOf, recordPath, MAX_COMMAND_MS, type ProcessIdentity } from "./terminal-recovery.ts";
5
5
  import { CLAUDE_PLUGIN_INVENTORY, PLUGIN, nextStep, pluginState, type InstalledPlugin, type Marketplace, type PluginState, type SetupStep } from "./setup.ts";
6
- import { checkClaudeProfileHook, type ClaudeProfileHookCheck } from "./claude-profile-hook.ts";
6
+ import { checkClaudeProfileHook, type ClaudeProfileHookCheck, type ClaudeProfileHookOptions } from "./claude-profile-hook.ts";
7
7
 
8
8
  export type ClaudeHomeSource = "environment" | "default" | "recorded-launch";
9
9
  export interface ClaudeHome { path: string; exists: boolean; sources: ClaudeHomeSource[] }
@@ -93,12 +93,12 @@ export interface ClaudeHomeCheck {
93
93
  }
94
94
 
95
95
  /** Read-only per-home plugin inspection. Installation never proves that a live permission hook is present. */
96
- export async function checkClaudeHome(home: ClaudeHome, packageRoot: string, env: NodeJS.ProcessEnv, probe: ClaudeHomeProbe, timeoutMs = MAX_COMMAND_MS, probeMissingDefault = false): Promise<ClaudeHomeCheck> {
96
+ export async function checkClaudeHome(home: ClaudeHome, packageRoot: string, env: NodeJS.ProcessEnv, probe: ClaudeHomeProbe, timeoutMs = MAX_COMMAND_MS, probeMissingDefault = false, hookOptions?: ClaudeProfileHookOptions): Promise<ClaudeHomeCheck> {
97
97
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_COMMAND_MS) throw new Error("invalid Claude plugin probe time bound");
98
- const hook = checkClaudeProfileHook(home);
98
+ const hook = checkClaudeProfileHook(home, hookOptions);
99
99
  const unknown = (): ClaudeHomeCheck => ({ home, plugin: { state: "unknown", reason: "Claude plugin inventory is unavailable or invalid for this home." }, hook });
100
100
  if (hook.state === "refused") return unknown();
101
- if (!home.exists && !(probeMissingDefault && home.sources.includes("default"))) return { home, plugin: { state: "missing" }, next: nextStep([], [], packageRoot), hook };
101
+ if (!home.exists && !(probeMissingDefault && home.sources.includes("default"))) return { home, plugin: { state: "missing" }, next: nextStep([], [], packageRoot, validPath(env.HOME) ? env.HOME : undefined), hook };
102
102
  const scoped = claudeHomeEnvironment(home, env), deadline = performance.now() + timeoutMs;
103
103
  const read = async (argv: readonly string[]): Promise<unknown[] | undefined> => {
104
104
  const remaining = Math.floor(deadline - performance.now());
@@ -118,6 +118,6 @@ export async function checkClaudeHome(home: ClaudeHome, packageRoot: string, env
118
118
  (row.version === undefined || typeof row.version === "string" && row.version.length <= 64 && (row.id !== PLUGIN ? !/[\u0000-\u001f\u007f-\u009f]/.test(row.version) : /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(row.version))) &&
119
119
  (row.installPath === undefined || validPath(row.installPath))) ||
120
120
  !marketplaces.every(row => object(row) && typeof row.name === "string" && row.name.length <= 256 && (row.path === undefined || validPath(row.path)))) return unknown();
121
- return { home, plugin: pluginState(plugins as InstalledPlugin[], packageRoot), next: nextStep(plugins as InstalledPlugin[], marketplaces as Marketplace[], packageRoot), hook };
121
+ return { home, plugin: pluginState(plugins as InstalledPlugin[], packageRoot), next: nextStep(plugins as InstalledPlugin[], marketplaces as Marketplace[], packageRoot, validPath(env.HOME) ? env.HOME : undefined), hook };
122
122
  } catch { return unknown(); }
123
123
  }
@@ -1,9 +1,9 @@
1
- import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync, type Stats } from "node:fs";
1
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, rmSync, statSync, writeFileSync, type Stats } from "node:fs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
- import { canonicalJson } from "../hub/packs.ts";
5
4
  import { factsHook } from "./facts-hook.ts";
6
5
  import { shellQuote } from "./shell-quote.ts";
6
+ import { stableBunRuntime, stablePackageRoot } from "./install-paths.ts";
7
7
  import type { ClaudeHome } from "./claude-homes.ts";
8
8
 
9
9
  export interface ClaudeProfileHookOptions { script?: string; runtime?: string }
@@ -25,11 +25,29 @@ interface HookPlan {
25
25
  }
26
26
 
27
27
  function hookEntry(options: ClaudeProfileHookOptions): Record<string, unknown> {
28
- const script = options.script ?? join(import.meta.dir, "claude-profile-hook.ts"), runtime = options.runtime ?? process.execPath;
28
+ const script = options.script ?? join(stablePackageRoot(join(import.meta.dir, "../..")), "src/cli/claude-profile-hook.ts"), runtime = options.runtime ?? stableBunRuntime();
29
29
  if (!validPath(script) || !validPath(runtime)) throw new Error("invalid profile hook command");
30
30
  return { matcher: "*", hooks: [{ type: "command", command: `${shellQuote(runtime)} ${shellQuote(script)} --run`, timeout: 5 }] };
31
31
  }
32
32
 
33
+ interface ManagedProfileHook { entry: Record<string, unknown>; hook: Record<string, unknown>; runtime: string; script: string }
34
+ /** Own only the generated command grammar; matcher, timeout and caller metadata remain theirs. */
35
+ function managedProfileHook(candidate: unknown): ManagedProfileHook | undefined {
36
+ if (!object(candidate) || !Array.isArray(candidate.hooks) || candidate.hooks.length !== 1) return;
37
+ const hook = candidate.hooks[0];
38
+ if (!object(hook) || hook.type !== "command" || typeof hook.command !== "string") return;
39
+ const match = /^'((?:[^']|'\\'')*)' '((?:[^']|'\\'')*)' --run$/.exec(hook.command);
40
+ if (!match) return;
41
+ const runtime = match[1]!.replace(/'\\''/g, "'"), script = match[2]!.replace(/'\\''/g, "'");
42
+ if (!validPath(runtime) || !validPath(script) || !script.endsWith("/src/cli/claude-profile-hook.ts")
43
+ || hook.command !== `${shellQuote(runtime)} ${shellQuote(script)} --run`) return;
44
+ return { entry: candidate, hook, runtime, script };
45
+ }
46
+
47
+ function hookPathPresent(file: string): boolean {
48
+ try { return statSync(file).isFile(); } catch { return false; }
49
+ }
50
+
33
51
  function settingsSource(file: string): { value: Record<string, unknown>; source?: HookPlan["source"] } {
34
52
  let fd: number | undefined;
35
53
  try {
@@ -67,13 +85,24 @@ function planClaudeProfileHook(home: ClaudeHome, options: ClaudeProfileHookOptio
67
85
  if (value.hooks !== undefined && !object(value.hooks)) return refuse("Claude's hook configuration is malformed.");
68
86
  const hooks = value.hooks ?? {}, existing = hooks.PreToolUse ?? [];
69
87
  if (!Array.isArray(existing)) return refuse("Claude's PreToolUse hook list is malformed.");
70
- const entry = hookEntry(options), expected = canonicalJson(entry);
71
- const matches = existing.filter(candidate => canonicalJson(candidate) === expected).length;
72
- const next = matches === 1 ? undefined : `${JSON.stringify({ ...value, hooks: { ...hooks, PreToolUse: [...existing.filter(candidate => canonicalJson(candidate) !== expected), entry] } }, null, 2)}\n`;
88
+ const entry = hookEntry(options), expectedCommand = (entry.hooks as { command: string }[])[0]!.command;
89
+ const managed = existing.map(managedProfileHook), first = managed.find(candidate => candidate !== undefined);
90
+ const count = managed.filter(candidate => candidate !== undefined).length;
91
+ const stale = managed.some(candidate => candidate && (!hookPathPresent(candidate.runtime) || !hookPathPresent(candidate.script)));
92
+ const configured = count === 1 && first?.hook.command === expectedCommand && !stale;
93
+ const normalized = existing.flatMap((candidate, index) => {
94
+ const owned = managed[index];
95
+ if (!owned) return [candidate];
96
+ return owned === first ? [{ ...owned.entry, hooks: [{ ...owned.hook, command: expectedCommand }] }] : [];
97
+ });
98
+ if (!first) normalized.push(entry);
99
+ const next = configured ? undefined : `${JSON.stringify({ ...value, hooks: { ...hooks, PreToolUse: normalized } }, null, 2)}\n`;
73
100
  if (next !== undefined && Buffer.byteLength(next, "utf8") > MAX_SETTINGS_BYTES) return refuse("Claude's settings would exceed the bounded profile settings size.");
74
- return { file, home: directory, parent, source, next, check: matches === 1
75
- ? { state: "installed", reason: "The project-neutral profile hook is configured; this does not prove live permission enforcement." }
76
- : { state: "missing", reason: matches ? "Duplicate managed profile hooks need normalization." : "The project-neutral profile hook is not configured." } };
101
+ let reason = "The project-neutral profile hook is not configured for this install; run ahub setup.";
102
+ if (count > 1) reason = "Duplicate managed profile hooks need normalization; run ahub setup.";
103
+ if (stale) reason = "A managed profile hook script or runtime is missing; run ahub setup to refresh the selected home's hook.";
104
+ if (configured) reason = "The project-neutral profile hook is configured; this does not prove live permission enforcement.";
105
+ return { file, home: directory, parent, source, next, check: { state: configured ? "installed" : "missing", reason } };
77
106
  } catch { return refuse("Claude profile settings could not be safely inspected; no settings were changed."); }
78
107
  }
79
108