create-walle 0.10.2 → 0.10.3

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.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -35,8 +35,19 @@ var _machinesPageState = {
35
35
  loading: false,
36
36
  loaded: false,
37
37
  error: '',
38
+ // WP-4 (docs/remote-session-hub-bridge-2026-10-30.html R-11/R-13, §6.2): the hub-bridge access
39
+ // surface. `access.trust` is the R-11 trust policy (GET /api/hub-bridge/trust), `access.grants`
40
+ // every live grant and `access.pending` every open ask (GET /api/hub-bridge/grants), and
41
+ // `accessAudit` the last 20 audit rows per machine (GET /api/hub-bridge/audit). `accessPendingIndex`
42
+ // is pending-id -> row, so a grant-button click knows the caller session, tier and choice.
43
+ access: null,
44
+ accessAudit: Object.create(null),
45
+ accessPendingIndex: Object.create(null),
38
46
  };
39
47
 
48
+ // pendingId -> the open ask (R-5). Rendered as a banner on the session toolbar the ask names (AC-2g).
49
+ var _machinesPageBanners = Object.create(null);
50
+
40
51
  // AC-5f's seam: the panel's timer and clock are injectable so a test can prove the timer stops
41
52
  // without sleeping. `now` is never called from a hidden path — every caller passes it in or reads it
42
53
  // here, so the tested paths are clock-free.
@@ -118,7 +129,8 @@ function machinesPageToast(text) {
118
129
  var panel = document.getElementById('machines-panel');
119
130
  var node = panel ? panel.querySelector('.machines-page-toast') : null;
120
131
  if (node) { node.textContent = message; node.classList.toggle('show', !!message); }
121
- if (typeof toast === 'function') toast(message);
132
+ // An empty message CLEARS the inline note; it must never pop an empty global toast.
133
+ if (message && typeof toast === 'function') toast(message);
122
134
  return message;
123
135
  }
124
136
 
@@ -156,8 +168,8 @@ function machinesFormatPct(value) {
156
168
  return machineFormatPct(value);
157
169
  }
158
170
 
159
- function machinesAgentText(agents) {
160
- return machineAgentText(agents);
171
+ function machinesAgentText(agents, presence) {
172
+ return machineAgentText(agents, presence);
161
173
  }
162
174
 
163
175
  function machinesTaskLine(tasks) {
@@ -168,6 +180,19 @@ function machinesGpuText(gpu) {
168
180
  return machineGpuText(gpu);
169
181
  }
170
182
 
183
+ function machinesPowerText(sample, history, now) {
184
+ return machinePowerText(sample, history, now);
185
+ }
186
+
187
+ // WP-3 (§8, docs/fleet-auto-update-pooled-machines-2026-10-26.html): the box's update status line.
188
+ // The rule lives in the shared presenter (machineFleetLine); here is only its markup, so the phone
189
+ // card renders the same bytes. '' (no markup at all) when there is nothing honest to say.
190
+ function machinesPageFleetHtml(machine) {
191
+ var fleet = machineFleetLine(machine);
192
+ if (!fleet) return '';
193
+ return '<div class="mpc-fleet" data-fleet-tone="' + mEsc(fleet.tone) + '" title="' + mEsc(fleet.title) + '">' + mEsc(fleet.text) + '</div>';
194
+ }
195
+
171
196
  function machinesCredentialBadge(machine, isSelf) {
172
197
  var credential = String((machine && machine.peerCredential) || (isSelf ? 'self' : 'missing'));
173
198
  if (isSelf) return { html: '<span class="mpc-cred" data-cred="self" title="This Mac runs the sessions itself">This Mac</span>', action: '' };
@@ -616,17 +641,20 @@ function machineCardHtml(machine, history) {
616
641
  var cpuNow = sample && sample.cpuPct != null ? Number(sample.cpuPct) : machinesLastValue(cpuSeries);
617
642
  var memNow = machinesLastValue(memSeries);
618
643
  var credential = machinesCredentialBadge(m, false);
619
- var agents = machinesAgentText(m.agents);
644
+ var agents = machinesAgentText(m.agents, m.agentPresence);
620
645
  var facts = ''
621
646
  + machinesStatBlock('Load', sample && sample.load1 != null ? mEsc(Number(sample.load1).toFixed(2)) : '')
622
647
  + machinesStatBlock('Cores', sample && sample.cores != null ? mEsc(String(sample.cores)) : '')
623
648
  + machinesStatBlock('Disk free', sample ? mEsc(machinesFormatBytes(sample.diskFreeBytes)) : '')
624
649
  + machinesStatBlock('GPU', mEsc(machinesGpuText(sample && sample.gpu)))
650
+ + machinesStatBlock('Power', mEsc(machinesPowerText(sample, machinesHistoryRows(history), machinesPageNow())))
625
651
  + machinesStatBlock('Tasks', mEsc(machinesTaskLine(sample && sample.tasks)))
626
652
  + machinesStatBlock('Agents', mEsc(agents));
627
653
  var staleNote = stale && (sample || statsAt)
628
654
  ? '<span class="mpc-stale" title="The hub re-reads a box at most once a minute; this sample is older than three">stats stale</span>'
629
655
  : '';
656
+ // WP-3 rev 3 (docs/off-lan-hub-to-box-access-2026-10-31.html §13.6 D-17): an unreachable box whose every dial URL is local-only says WHY — `awayReason` is the registry's verdict, and anything else renders no note at all.
657
+ var awayNote = m.awayReason === 'no_away_path' ? '<div class="machines-page-note machine-card-away-note">' + mEsc('Not reachable away from home — no away path is set up on this hub (bin/away-path.sh).') + '</div>' : (m.awayReason === 'away_path_down' ? '<div class="machines-page-note machine-card-away-note">' + mEsc("Not reachable — the away path is set up, but this box's tunnel or forward is down. Run: bin/away-path.sh doctor") + '</div>' : '');
630
658
  return '<article class="machine-page-card" data-machine-id="' + mEsc(id) + '" data-health="' + mEsc(health) + '">'
631
659
  + '<div class="mpc-head">'
632
660
  + '<span class="mpc-dot" data-health="' + mEsc(health) + '" title="' + mEsc(health) + '"></span>'
@@ -636,13 +664,16 @@ function machineCardHtml(machine, history) {
636
664
  + credential.html
637
665
  + '</div>'
638
666
  + '<div class="mpc-meta">' + mEsc([machineHealthLabel(m), machineLastSeenText(m), [m.os, m.arch].filter(Boolean).join('/')].filter(Boolean).join(' · '))
639
- + ' ' + staleNote + '</div>'
667
+ + ' ' + staleNote + '</div>' + awayNote
668
+ + machinesPageFleetHtml(m)
640
669
  + '<div class="mpc-sparks">'
641
670
  + '<div class="mpc-spark"><span class="mpc-spark-label">CPU</span>' + sparklineSvg(cpuSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(cpuNow)) + '</span></div>'
642
671
  + '<div class="mpc-spark"><span class="mpc-spark-label">MEM</span>' + sparklineSvg(memSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(memNow)) + '</span></div>'
643
672
  + '</div>'
644
673
  + '<dl class="mpc-facts">' + facts + '</dl>'
645
674
  + machinesPageSignInHtml(m)
675
+ + machinesPageSkillsRowHtml(m)
676
+ + machinesPageModelsRowHtml(m)
646
677
  + '<div class="mpc-actions">'
647
678
  + '<button type="button" class="mpc-btn mpc-fav" aria-pressed="' + (m.favorite ? 'true' : 'false') + '"'
648
679
  + ' title="Favorite — shown first in the picker" onclick="machinesPageToggle(' + mEsc(JSON.stringify(id)) + ',\'favorite\')">'
@@ -654,6 +685,7 @@ function machineCardHtml(machine, history) {
654
685
  + '<button type="button" class="mpc-btn mpc-danger" onclick="machinesPageConfirmRemove(' + mEsc(JSON.stringify(id)) + ')">Remove</button>'
655
686
  + '</div>'
656
687
  + machinesPageSignInRowHtml(m)
688
+ + machinesPageAccessBlockHtml(m)
657
689
  + '</article>';
658
690
  }
659
691
 
@@ -674,6 +706,7 @@ function machinesSelfCardHtml() {
674
706
  + machinesStatBlock('Cores', latest && latest.cores != null ? mEsc(String(latest.cores)) : '')
675
707
  + machinesStatBlock('Disk free', latest ? mEsc(machinesFormatBytes(latest.diskFreeBytes)) : '')
676
708
  + machinesStatBlock('GPU', mEsc(machinesGpuText(latest && latest.gpu)))
709
+ + machinesStatBlock('Power', mEsc(machinesPowerText(latest, machinesHistoryRows(history), machinesPageNow())))
677
710
  + machinesStatBlock('Tasks', mEsc(machinesTaskLine(latest && latest.tasks)))
678
711
  + machinesStatBlock('Memory', latest ? mEsc(machinesFormatBytes(latest.memUsedBytes) + ' / ' + machinesFormatBytes(latest.memTotalBytes)) : '');
679
712
  var poolNote = _machinesPageState.machines.length
@@ -712,6 +745,7 @@ function machinesPageHeaderHtml() {
712
745
  + 'Names, favorites and visibility live on the machine they describe.</p>'
713
746
  + '</div>'
714
747
  + '<div class="machines-page-actions">'
748
+ + machinesPageTrustAllHtml()
715
749
  + '<button type="button" class="mpc-btn mpc-primary" onclick="openAddMachineDialog()">+ Add machine</button>'
716
750
  + '<button type="button" class="mpc-btn" onclick="openShowCodeDialog()">Show a code for this machine</button>'
717
751
  + (inPool ? '<button type="button" class="mpc-btn mpc-danger" onclick="machinesPageLeavePool()">Leave pool</button>' : '')
@@ -777,11 +811,17 @@ function loadMachinesPanel() {
777
811
  machinesPageJson(machinesPageUrl('/api/machines')),
778
812
  machinesPageJson(machinesPageUrl('/api/machine-self')),
779
813
  machinesPageJson(machinesPageUrl('/api/host/stats', machinesPageHistoryQuery('', now))),
814
+ // R-11/R-13: the trust policy and the live grants/pending rows that fill every card's Access block.
815
+ machinesPageJson(machinesPageUrl('/api/hub-bridge/trust')),
816
+ machinesPageJson(machinesPageUrl('/api/hub-bridge/grants')),
780
817
  ]).then(function (results) {
781
818
  var list = results[0];
782
819
  var self = results[1];
783
820
  var stats = results[2];
784
821
  _machinesPageState.loading = false;
822
+ // A hub without the WP-2 routes (an older server) 404s: the access surface is simply empty, and
823
+ // the Machines page renders exactly as it did before this work.
824
+ machinesPageApplyAccess(results[3], results[4]);
785
825
  if (!list.ok) {
786
826
  _machinesPageState.error = machinesPageServerError(list.data, list.status);
787
827
  renderMachinesPage();
@@ -799,7 +839,7 @@ function loadMachinesPanel() {
799
839
  _machinesPageState.origins = machinesPageReadPoolOrigins();
800
840
  _machinesPageState.loaded = true;
801
841
  renderMachinesPage();
802
- return machinesPageLoadHistories(now).then(function () {
842
+ return Promise.all([machinesPageLoadHistories(now), machinesPageLoadAccessAudits()]).then(function () {
803
843
  renderMachinesPage();
804
844
  return true;
805
845
  });
@@ -883,10 +923,48 @@ function machinesPanelHidden() {
883
923
  return machinesPageStopPolling();
884
924
  }
885
925
 
886
- // navTo('machines') entry point (mirrors showWorktreesPanel).
926
+ // navTo('machines') entry point (mirrors showWorktreesPanel). `opts.revealMachine` /
927
+ // `opts.revealMachineName` come from a `.machine-chip` click (AC-4a): after the cards exist, the
928
+ // named card is scrolled into view and flashed, exactly once per click.
887
929
  function showMachinesPanel(opts) {
888
930
  if (typeof activateTab === 'function') activateTab('machines');
889
- return loadMachinesPanel();
931
+ var options = opts || {};
932
+ var pending = loadMachinesPanel();
933
+ if (!options.revealMachine && !options.revealMachineName) return pending;
934
+ return pending.then(function (ok) {
935
+ machinesPageRevealCard(String(options.revealMachine || ''), String(options.revealMachineName || ''));
936
+ return ok;
937
+ });
938
+ }
939
+
940
+ /** AC-4a: scroll the named machine's card into view and flash it (the chip click-through target). */
941
+ function machinesPageRevealCard(machineId, machineName) {
942
+ if (typeof document === 'undefined' || !document || typeof document.querySelector !== 'function') return false;
943
+ var id = String(machineId || '');
944
+ var name = String(machineName || '');
945
+ var escaped = (typeof window !== 'undefined' && window.CSS && typeof CSS.escape === 'function')
946
+ ? CSS.escape(id)
947
+ : id.replace(/["\\]/g, '\\$&');
948
+ var card = id ? document.querySelector('.machine-page-card[data-machine-id="' + escaped + '"]') : null;
949
+ if (!card && name && typeof document.querySelectorAll === 'function') {
950
+ var cards = document.querySelectorAll('.machine-page-card') || [];
951
+ for (var i = 0; i < cards.length; i += 1) {
952
+ var node = cards[i];
953
+ var nameNode = typeof node.querySelector === 'function' ? node.querySelector('.mpc-name') : null;
954
+ if (nameNode && String(nameNode.value || '') === name) { card = node; break; }
955
+ }
956
+ }
957
+ if (!card) return false;
958
+ if (typeof card.scrollIntoView === 'function') {
959
+ try { card.scrollIntoView({ block: 'center' }); } catch { card.scrollIntoView(); }
960
+ }
961
+ if (card.classList && typeof card.classList.add === 'function') {
962
+ card.classList.add('machine-card-highlight');
963
+ if (typeof setTimeout === 'function') {
964
+ setTimeout(function () { card.classList.remove('machine-card-highlight'); }, 2000);
965
+ }
966
+ }
967
+ return true;
890
968
  }
891
969
 
892
970
  // ── AC-5d: keep the picker grid and the sidebar chip in step without a reload ───────────────────
@@ -1250,3 +1328,1245 @@ function openShowCodeDialog() {
1250
1328
  function machinesPageJoinPool(instanceId, url) {
1251
1329
  return openAddMachineDialog({ url: url || '' });
1252
1330
  }
1331
+
1332
+ // ── WP-9: the Skills line + the Review dialog (§9 S-9, AC-9a..AC-9d) ────────────────────────────
1333
+ // One line per machine row ("Skills: 12 sent · 2 held back · 1 new on spark — Review"), and the
1334
+ // dialog it opens: what the hub held back, what the machine has of its own, and which plugins this
1335
+ // hub is told not to send it. PHONE = DESKTOP PARITY — public/m/machines.js renders the same states
1336
+ // with the same wording from the same row shape, and neither surface gets an action the other lacks.
1337
+ //
1338
+ // AC-9c: a machine whose row carries `skills: null` (a box this hub never pushed to and never read)
1339
+ // renders EXACTLY the card it rendered before this work — every builder below returns '' for null, so
1340
+ // the row's bytes cannot move. tests/machines-page.test.js pins that byte-for-byte.
1341
+
1342
+ var MACHINES_PAGE_SKILL_REASONS = {
1343
+ platform: 'macOS-only',
1344
+ hub_home_path: 'points at a path that exists only on this laptop',
1345
+ too_large: 'too large to send',
1346
+ unreadable: 'could not be read',
1347
+ };
1348
+
1349
+ var MACHINES_PAGE_SKILL_STATES = {
1350
+ new_on_box: 'new on this machine',
1351
+ same: 'same as the hub',
1352
+ shadows_hub: 'this machine’s copy wins',
1353
+ changed_on_box: 'changed on this machine',
1354
+ conflict: 'both sides changed',
1355
+ };
1356
+
1357
+ // The states that mean "this machine has its own copy" — `same` is the hub's, already on the list.
1358
+ var MACHINES_PAGE_BOX_LOCAL_STATES = ['new_on_box', 'changed_on_box', 'conflict', 'shadows_hub'];
1359
+
1360
+ /** The row's `skills` block, or null. Everything below reads through this one accessor. */
1361
+ function machinesPageSkills(machine) {
1362
+ var skills = machine && machine.skills;
1363
+ return skills && typeof skills === 'object' ? skills : null;
1364
+ }
1365
+
1366
+ function machinesPageSkillReasonLabel(reason) {
1367
+ var key = String(reason || '');
1368
+ return MACHINES_PAGE_SKILL_REASONS[key] || key || 'held back';
1369
+ }
1370
+
1371
+ function machinesPageSkillStateLabel(state) {
1372
+ var key = String(state || '');
1373
+ return MACHINES_PAGE_SKILL_STATES[key] || key || 'differs';
1374
+ }
1375
+
1376
+ function machinesPageSkillsHeldBack(machine) {
1377
+ var skills = machinesPageSkills(machine);
1378
+ return skills && Array.isArray(skills.heldBack) ? skills.heldBack : [];
1379
+ }
1380
+
1381
+ function machinesPageSkillsOnBox(machine) {
1382
+ var skills = machinesPageSkills(machine);
1383
+ return skills && Array.isArray(skills.onBox) ? skills.onBox : [];
1384
+ }
1385
+
1386
+ function machinesPageSkillsPlugins(machine) {
1387
+ var skills = machinesPageSkills(machine);
1388
+ return skills && Array.isArray(skills.plugins) ? skills.plugins : [];
1389
+ }
1390
+
1391
+ function machinesPageSkillsExcluded(machine) {
1392
+ var skills = machinesPageSkills(machine);
1393
+ return skills && Array.isArray(skills.excludePlugins) ? skills.excludePlugins : [];
1394
+ }
1395
+
1396
+ /** How many of this machine's own skills the hub does not have (or has a different copy of). */
1397
+ function machinesPageSkillsBoxLocalCount(machine) {
1398
+ return machinesPageSkillsOnBox(machine).filter(function (item) {
1399
+ return item && MACHINES_PAGE_BOX_LOCAL_STATES.indexOf(String(item.state)) !== -1;
1400
+ }).length;
1401
+ }
1402
+
1403
+ function machinesPageSkillsMachineName(machine) {
1404
+ var m = machine || {};
1405
+ return String(m.displayName || m.hostname || m.instanceId || 'this machine');
1406
+ }
1407
+
1408
+ /**
1409
+ * The row's one Skills line, or '' when the row has no skills block (AC-9c).
1410
+ * "sent" is always shown; every other segment appears only when it is non-zero.
1411
+ */
1412
+ function machinesPageSkillsLineText(machine) {
1413
+ if (!machinesPageSkills(machine)) return '';
1414
+ var parts = [(Number((machine.skills || {}).sentCount) || 0) + ' sent'];
1415
+ var heldBack = machinesPageSkillsHeldBack(machine).length;
1416
+ if (heldBack) parts.push(heldBack + ' held back');
1417
+ var local = machinesPageSkillsBoxLocalCount(machine);
1418
+ if (local) parts.push(local + ' new on ' + machinesPageSkillsMachineName(machine));
1419
+ var text = 'Skills: ' + parts.join(' · ');
1420
+ var error = (machine.skills || {}).pushError;
1421
+ if (error && (error.message || error.code)) {
1422
+ text += ' · last push failed: ' + String(error.message || error.code);
1423
+ }
1424
+ return text;
1425
+ }
1426
+
1427
+ /** §9 S-9: Review is offered when there is something to review — held-back items, this machine's own
1428
+ * skills, or a plugin this hub ships that is not excluded yet. */
1429
+ function machinesPageSkillsNeedsReview(machine) {
1430
+ if (!machinesPageSkills(machine)) return false;
1431
+ if (machinesPageSkillsHeldBack(machine).length) return true;
1432
+ if (machinesPageSkillsBoxLocalCount(machine)) return true;
1433
+ var excluded = machinesPageSkillsExcluded(machine);
1434
+ return machinesPageSkillsPlugins(machine).some(function (plugin) { return excluded.indexOf(plugin) === -1; });
1435
+ }
1436
+
1437
+ function machinesPageSkillsRowHtml(machine) {
1438
+ if (!machinesPageSkills(machine)) return ''; // AC-9c: no block, no bytes
1439
+ var id = String((machine && machine.instanceId) || '');
1440
+ var review = machinesPageSkillsNeedsReview(machine)
1441
+ ? '<button type="button" class="mpc-btn mpc-skills-review" onclick="machinesPageOpenReview('
1442
+ + mEsc(JSON.stringify(id)) + ')">Review</button>'
1443
+ : '';
1444
+ return '<div class="mpc-skills"><span class="mpc-skills-line">' + mEsc(machinesPageSkillsLineText(machine))
1445
+ + '</span>' + review + '</div>';
1446
+ }
1447
+
1448
+ // ── WP-3: the Models line + the Review dialog's Models section (§6.2, §8 WP-3) ───────────────────
1449
+ // The row's `models` block is the BOX's own answer about its own routes (lib/machine-registry.js
1450
+ // `_modelsBlockFrom`, read on the existing 60 s probe tick): how many of its enabled routes it can
1451
+ // actually use, which ones it cannot get to, and what its two /delegate picks name. `null` = never
1452
+ // read, or a box that predates WP-1 — then this card renders EXACTLY the bytes it rendered before
1453
+ // this work (AC-3a), so every builder below returns '' for null, exactly like the Skills builders.
1454
+ // Nothing here promises an action: "Remove from <box>" arrives in WP-4, "Use on <box>" in WP-5.
1455
+ // PHONE = DESKTOP PARITY: public/m/machines.js renders the same states with the same wording.
1456
+
1457
+ /** The row's `models` block (§6.2), or null. Everything below reads through this one accessor. */
1458
+ function machinesPageModels(machine) {
1459
+ var models = machine && machine.models;
1460
+ return models && typeof models === 'object' ? models : null;
1461
+ }
1462
+
1463
+ function machinesPageModelsUnreachable(machine) {
1464
+ var models = machinesPageModels(machine);
1465
+ return models && Array.isArray(models.unreachable) ? models.unreachable : [];
1466
+ }
1467
+
1468
+ function machinesPageModelsDelegate(machine) {
1469
+ var models = machinesPageModels(machine);
1470
+ return models && Array.isArray(models.delegate) ? models.delegate : [];
1471
+ }
1472
+
1473
+ /** The unreachable rows are worded ONCE here — the line, the /delegate warning and the dialog all
1474
+ * read the same sentence. A local runtime has nothing listening, which is a different fact. */
1475
+ function machinesPageModelsUnreachableLabel(machine, item) {
1476
+ if (String((item && item.type) || '').toLowerCase() === 'ollama') return 'nothing is running there';
1477
+ return "can't be reached from " + machinesPageSkillsMachineName(machine);
1478
+ }
1479
+
1480
+ /** Whether one /delegate default's route is reachable on the box, said plainly (§8 WP-3). */
1481
+ function machinesPageModelsReachLabel(machine, reach) {
1482
+ return (String(reach || '') === 'reachable' ? 'can be reached from ' : "can't be reached from ")
1483
+ + machinesPageSkillsMachineName(machine);
1484
+ }
1485
+
1486
+ /** §8 WP-3: one warning per /delegate default whose route the box itself cannot get to. */
1487
+ function machinesPageModelsDelegateWarnings(machine) {
1488
+ return machinesPageModelsDelegate(machine).filter(function (entry) {
1489
+ var reach = String((entry && entry.reach) || '');
1490
+ return reach === 'unreachable' || reach === 'no_server';
1491
+ }).map(function (entry) {
1492
+ return '/delegate on ' + machinesPageSkillsMachineName(machine) + ' uses ' + String((entry && entry.model) || '')
1493
+ + ' through a route ' + machinesPageSkillsMachineName(machine) + " can't reach.";
1494
+ });
1495
+ }
1496
+
1497
+ /**
1498
+ * The row's one Models line, or '' when the row has no models block (AC-3a).
1499
+ * "usable" is always shown; the unreachable segment appears only when there is one.
1500
+ */
1501
+ function machinesPageModelsLineText(machine) {
1502
+ var models = machinesPageModels(machine);
1503
+ if (!models) return '';
1504
+ var parts = [(Number(models.usable) || 0) + ' usable'];
1505
+ var unreachable = machinesPageModelsUnreachable(machine).length;
1506
+ if (unreachable) {
1507
+ parts.push(unreachable + " can't be reached from " + machinesPageSkillsMachineName(machine));
1508
+ }
1509
+ return 'Models: ' + parts.join(' · ');
1510
+ }
1511
+
1512
+ /** §8 WP-3: Review is offered when the block has something to show — a route the box cannot get to,
1513
+ * or a /delegate default that names one. */
1514
+ function machinesPageModelsNeedsReview(machine) {
1515
+ if (!machinesPageModels(machine)) return false;
1516
+ if (machinesPageModelsUnreachable(machine).length) return true;
1517
+ return machinesPageModelsDelegateWarnings(machine).length > 0;
1518
+ }
1519
+
1520
+ function machinesPageModelsRowHtml(machine) {
1521
+ if (!machinesPageModels(machine)) return ''; // AC-3a: no block, no bytes
1522
+ var id = String((machine && machine.instanceId) || '');
1523
+ var review = machinesPageModelsNeedsReview(machine)
1524
+ ? '<span class="mpc-models-sep">—</span>'
1525
+ + '<button type="button" class="mpc-btn mpc-models-review" onclick="machinesPageOpenReview('
1526
+ + mEsc(JSON.stringify(id)) + ')">Review</button>'
1527
+ : '';
1528
+ var warnings = machinesPageModelsDelegateWarnings(machine).map(function (text) {
1529
+ return '<span class="mpc-models-warn">' + mEsc(text) + '</span>';
1530
+ }).join('');
1531
+ return '<div class="mpc-models"><span class="mpc-models-line">' + mEsc(machinesPageModelsLineText(machine))
1532
+ + '</span>' + review + warnings + '</div>';
1533
+ }
1534
+
1535
+ /** WP-9 title, extended by WP-3: the dialog names whichever of the two blocks the row actually has. */
1536
+ function machinesPageReviewTitle(machine) {
1537
+ var name = machinesPageSkillsMachineName(machine);
1538
+ var hasSkills = !!machinesPageSkills(machine);
1539
+ var hasModels = !!machinesPageModels(machine);
1540
+ if (hasSkills && hasModels) return 'Skills and models on ' + name;
1541
+ if (hasModels) return 'Models on ' + name;
1542
+ return 'Skills on ' + name;
1543
+ }
1544
+
1545
+ /** WP-3: a row is reviewable while EITHER block has something to show. A section whose block is
1546
+ * absent renders nothing, so a models-only row opens a Models-only dialog. */
1547
+ function machinesPageReviewOpenable(machine) {
1548
+ return !!machinesPageSkills(machine) || machinesPageModelsNeedsReview(machine);
1549
+ }
1550
+
1551
+ // ── the Review dialog's sections (pure presenters; the wiring mounts them) ───────────────────────
1552
+
1553
+ function machinesPageReviewHeldBackHtml(machine) {
1554
+ if (!machinesPageSkills(machine)) return ''; // no block, no section (§8 WP-3)
1555
+ var items = machinesPageSkillsHeldBack(machine);
1556
+ var body = items.length
1557
+ ? items.map(function (item) {
1558
+ var where = item && item.file
1559
+ ? '<span class="mpc-review-where">' + mEsc(String(item.file) + (item.line ? ':' + item.line : '')) + '</span>'
1560
+ : '';
1561
+ return '<li class="mpc-review-item" data-skill="' + mEsc(String((item && item.name) || '')) + '"'
1562
+ + ' data-key="' + mEsc(machinesPageReviewItemKey(item && item.agent, item && item.name)) + '">'
1563
+ + '<span class="mpc-review-name">' + mEsc(String((item && item.name) || '')) + '</span>'
1564
+ + '<span class="mpc-review-meta">' + mEsc([String((item && item.agent) || ''), machinesPageSkillReasonLabel(item && item.reason)].filter(Boolean).join(' · ')) + '</span>'
1565
+ + where + '</li>';
1566
+ }).join('')
1567
+ : '<li class="mpc-review-empty">Nothing was held back.</li>';
1568
+ return '<section class="mpc-review-section" data-section="held-back"><h4>Held back</h4><ul>'
1569
+ + body + '</ul></section>';
1570
+ }
1571
+
1572
+ /** The buttons one on-`machine` item offers, by state (§9 S-9). `new_on_box` has no laptop copy to
1573
+ * fall back to, so "Use the laptop's copy" is absent there — the one state where the pair differs. */
1574
+ function machinesPageReviewItemActionsHtml(instanceId, item) {
1575
+ var state = String((item && item.state) || '');
1576
+ var args = mEsc(JSON.stringify(String(instanceId))) + ',' + mEsc(JSON.stringify(String((item && item.agent) || '')))
1577
+ + ',' + mEsc(JSON.stringify(String((item && item.name) || ''))) + ',' + mEsc(JSON.stringify(String((item && item.boxHash) || '')));
1578
+ var out = '';
1579
+ if (state !== 'shadows_hub') {
1580
+ out += '<button type="button" class="mpc-btn mpc-primary" onclick="machinesPageReviewAction(' + args + ',\'import\',this)">Import</button>';
1581
+ }
1582
+ if (state !== 'new_on_box') {
1583
+ out += '<button type="button" class="mpc-btn" onclick="machinesPageReviewAction(' + args + ',\'retire\',this)">Use the laptop’s copy</button>';
1584
+ }
1585
+ out += '<button type="button" class="mpc-btn" onclick="machinesPageReviewSkip(' + args + ',this)">Skip</button>';
1586
+ return out;
1587
+ }
1588
+
1589
+ function machinesPageReviewOnBoxHtml(machine) {
1590
+ if (!machinesPageSkills(machine)) return ''; // no block, no section (§8 WP-3)
1591
+ var id = String((machine && machine.instanceId) || '');
1592
+ var items = machinesPageSkillsOnBox(machine).filter(function (item) { return item && String(item.state) !== 'same'; });
1593
+ var body = items.length
1594
+ ? items.map(function (item) {
1595
+ var key = String((item && item.name) || '');
1596
+ return '<li class="mpc-review-item" data-skill="' + mEsc(key) + '"'
1597
+ + ' data-key="' + mEsc(machinesPageReviewItemKey(item && item.agent, key)) + '">'
1598
+ + '<span class="mpc-review-name">' + mEsc(key) + '</span>'
1599
+ + '<span class="mpc-review-meta">' + mEsc([String((item && item.agent) || ''), machinesPageSkillStateLabel(item && item.state)].filter(Boolean).join(' · ')) + '</span>'
1600
+ + '<span class="mpc-review-actions">'
1601
+ + machinesPageReviewItemActionsHtml(id, item)
1602
+ + '<button type="button" class="mpc-btn" onclick="machinesPageShowDiff(' + mEsc(JSON.stringify(id)) + ','
1603
+ + mEsc(JSON.stringify(String((item && item.agent) || ''))) + ',' + mEsc(JSON.stringify(key)) + ',this)">Show diff</button>'
1604
+ + '</span>'
1605
+ + '<div class="mpc-review-diff" hidden></div>'
1606
+ + '</li>';
1607
+ }).join('')
1608
+ : '<li class="mpc-review-empty">This machine has no skills of its own.</li>';
1609
+ return '<section class="mpc-review-section" data-section="on-box"><h4>On '
1610
+ + mEsc(machinesPageSkillsMachineName(machine)) + '</h4><ul>' + body + '</ul></section>';
1611
+ }
1612
+
1613
+ function machinesPageReviewPluginsHtml(machine) {
1614
+ if (!machinesPageSkills(machine)) return ''; // no block, no section (§8 WP-3)
1615
+ var plugins = machinesPageSkillsPlugins(machine);
1616
+ var excluded = machinesPageSkillsExcluded(machine);
1617
+ var body = plugins.length
1618
+ ? plugins.map(function (plugin) {
1619
+ var id = String(plugin);
1620
+ return '<label class="mpc-review-plugin"><input type="checkbox" value="' + mEsc(id) + '"'
1621
+ + (excluded.indexOf(id) !== -1 ? ' checked' : '') + '> ' + mEsc(id) + '</label>';
1622
+ }).join('')
1623
+ : '<p class="mpc-review-empty">The hub sends no plugins to this machine.</p>';
1624
+ return '<section class="mpc-review-section" data-section="plugins"><h4>Plugins</h4>'
1625
+ + '<p class="mpc-review-note">Checked plugins are not sent to this machine.</p>'
1626
+ + '<div class="mpc-review-plugins">' + body + '</div>'
1627
+ + '<div class="mpc-review-actions"><button type="button" class="mpc-btn mpc-primary"'
1628
+ + ' onclick="machinesPageReviewSavePlugins(' + mEsc(JSON.stringify(String((machine && machine.instanceId) || '')))
1629
+ + ',this)">Save</button></div></section>';
1630
+ }
1631
+
1632
+ /**
1633
+ * §8 WP-3 + WP-4: the Review dialog's Models section — one row per route the box cannot get to (id,
1634
+ * type, origin, and what that means for THIS machine), then the box's own /delegate defaults with the
1635
+ * verdict of the route each one names. WP-4 puts ONE control on the unreachable rows (a checkbox: the
1636
+ * route ids are the box's own, and only the box can delete them) and the matching "Remove from
1637
+ * <machine>" button under the list — never on the /delegate rows, which name no route of their own.
1638
+ */
1639
+ function machinesPageReviewModelsHtml(machine) {
1640
+ if (!machinesPageModels(machine)) return ''; // no block, no section
1641
+ var name = machinesPageSkillsMachineName(machine);
1642
+ var unreachable = machinesPageModelsUnreachable(machine);
1643
+ var rows = unreachable.map(function (item) {
1644
+ var id = String((item && item.id) || '');
1645
+ return '<li class="mpc-review-item" data-route="' + mEsc(id) + '">'
1646
+ + '<label class="mpc-review-pick"><input type="checkbox" data-role="models-pick" value="' + mEsc(id) + '"'
1647
+ + ' aria-label="' + mEsc('Remove ' + id + ' from ' + name) + '"'
1648
+ + ' onchange="machinesPageReviewModelsPicked()">'
1649
+ + '<span class="mpc-review-name">' + mEsc(id) + '</span></label>'
1650
+ + '<span class="mpc-review-meta">'
1651
+ + mEsc([String((item && item.type) || ''), String((item && item.origin) || '')].filter(Boolean).join(' · '))
1652
+ + '</span>'
1653
+ + '<span class="mpc-review-where">' + mEsc(machinesPageModelsUnreachableLabel(machine, item)) + '</span>'
1654
+ + '</li>';
1655
+ });
1656
+ if (!rows.length) rows.push('<li class="mpc-review-empty">Every route on this machine can be reached.</li>');
1657
+ machinesPageModelsDelegate(machine).forEach(function (entry) {
1658
+ var route = String((entry && entry.routeId) || '');
1659
+ var meta = [String((entry && entry.model) || ''), route ? 'route ' + route : ''].filter(Boolean).join(' · ');
1660
+ rows.push('<li class="mpc-review-item" data-agent="' + mEsc(String((entry && entry.agent) || '')) + '">'
1661
+ + '<span class="mpc-review-name">' + mEsc(String((entry && entry.agent) || '')) + '</span>'
1662
+ + '<span class="mpc-review-meta">' + mEsc(meta) + '</span>'
1663
+ + '<span class="mpc-review-where">' + mEsc(machinesPageModelsReachLabel(machine, entry && entry.reach)) + '</span>'
1664
+ + '</li>');
1665
+ });
1666
+ // WP-4: nothing unreachable → nothing to remove → no control at all (the row line is the whole
1667
+ // block). With rows, the button is disabled until a row is ticked.
1668
+ var remove = unreachable.length
1669
+ ? '<div class="mpc-review-models-remove"><button type="button" class="mpc-btn mpc-danger" data-role="models-remove"'
1670
+ + ' onclick="machinesPageReviewRemoveModels('
1671
+ + mEsc(JSON.stringify(String((machine && machine.instanceId) || ''))) + ',this)" disabled>'
1672
+ + 'Remove from ' + mEsc(name) + '</button></div>'
1673
+ : '';
1674
+ return '<section class="mpc-review-section" data-section="models"><h4>Models on '
1675
+ + mEsc(name) + '</h4><ul>' + rows.join('') + '</ul>' + remove
1676
+ + machinesPageReviewFromHubHtml(machine) + '</section>';
1677
+ }
1678
+
1679
+ /** WP-4: the route ids ticked in the OPEN dialog — the only truth a Remove POST may be built from. */
1680
+ function machinesPageReviewModelsChecked() {
1681
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1682
+ var picked = [];
1683
+ if (overlay && typeof overlay.querySelectorAll === 'function') {
1684
+ Array.prototype.slice.call(overlay.querySelectorAll('[data-role="models-pick"]') || []).forEach(function (box) {
1685
+ if (box && box.checked) picked.push(String(box.value == null ? '' : box.value));
1686
+ });
1687
+ }
1688
+ return picked;
1689
+ }
1690
+
1691
+ /** WP-4: one tick or untick → the button is live exactly while something is ticked. Returns the count. */
1692
+ function machinesPageReviewModelsPicked() {
1693
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1694
+ var count = machinesPageReviewModelsChecked().length;
1695
+ var button = overlay && typeof overlay.querySelector === 'function'
1696
+ ? overlay.querySelector('[data-role="models-remove"]') : null;
1697
+ if (button) button.disabled = count === 0;
1698
+ return count;
1699
+ }
1700
+
1701
+ /**
1702
+ * §8 WP-5: the "From this hub" subsection — the routes THIS hub holds that the machine does not, one
1703
+ * row each, each row carrying its OWN "Use on <name>" button. There is no checkbox and no bulk
1704
+ * control here (AC-5c: nothing in this dialog can copy more than one route). The list is read ONCE
1705
+ * per open (machinesPageReviewFetchFromHub) and read again after a copy or a remove, so a route that
1706
+ * just travelled leaves the list — and a route that just arrived joins it.
1707
+ */
1708
+ function machinesPageReviewFromHubState(machine) {
1709
+ var id = String((machine && machine.instanceId) || '');
1710
+ var store = _machinesPageReview ? _machinesPageReview.fromHub : null;
1711
+ // Nothing stored for THIS machine (or the dialog has moved on) reads as "still looking": the open
1712
+ // path fetches straight away, so the loading line is what the first paint legitimately shows.
1713
+ if (!store || String(store.instanceId || '') !== id) return { status: 'loading' };
1714
+ if (store.error) return { status: 'error', error: String(store.error) };
1715
+ return { status: 'ready', routes: Array.isArray(store.routes) ? store.routes : [] };
1716
+ }
1717
+
1718
+ function machinesPageReviewFromHubItemsHtml(machine) {
1719
+ if (!machinesPageModels(machine)) return ''; // no block, no subsection
1720
+ var name = machinesPageSkillsMachineName(machine);
1721
+ var id = String((machine && machine.instanceId) || '');
1722
+ var view = machinesPageReviewFromHubState(machine);
1723
+ if (view.status === 'loading') {
1724
+ return '<li class="mpc-review-empty">' + mEsc('Looking for routes this hub has that ' + name + ' lacks…') + '</li>';
1725
+ }
1726
+ if (view.status === 'error') return '<li class="mpc-review-note">' + mEsc(view.error) + '</li>';
1727
+ if (!view.routes.length) {
1728
+ return '<li class="mpc-review-empty">' + mEsc('This hub has no other routes ' + name + ' could use.') + '</li>';
1729
+ }
1730
+ return view.routes.map(function (route) {
1731
+ var routeId = String((route && route.id) || '');
1732
+ var label = String((route && route.name) || '') || routeId;
1733
+ var origin = String((route && route.origin) || '');
1734
+ var meta = [String((route && route.type) || ''), origin].filter(Boolean).join(' · ');
1735
+ // The button names its route and the origin the row was PAINTED with: the copy carries that
1736
+ // origin as `expectOrigin`, so a route that moved on the hub is refused instead of copied.
1737
+ return '<li class="mpc-review-item" data-hub-route="' + mEsc(routeId) + '">'
1738
+ + '<span class="mpc-review-name">' + mEsc(label) + '</span>'
1739
+ + '<span class="mpc-review-meta">' + mEsc(meta) + '</span>'
1740
+ + '<span class="mpc-review-actions"><button type="button" class="mpc-btn mpc-primary" data-role="models-copy"'
1741
+ + ' data-route-id="' + mEsc(routeId) + '" data-origin="' + mEsc(origin) + '"'
1742
+ + ' onclick="machinesPageReviewCopyModel(' + mEsc(JSON.stringify(id)) + ',this)">'
1743
+ + 'Use on ' + mEsc(name) + '</button></span></li>';
1744
+ }).join('');
1745
+ }
1746
+
1747
+ function machinesPageReviewFromHubHtml(machine) {
1748
+ if (!machinesPageModels(machine)) return '';
1749
+ return '<div class="mpc-review-from-hub" data-section="from-hub"><h5>From this hub</h5>'
1750
+ + '<ul class="mpc-review-list" data-role="from-hub">' + machinesPageReviewFromHubItemsHtml(machine) + '</ul></div>';
1751
+ }
1752
+
1753
+ /** The subsection's live node in the OPEN dialog — null when the dialog is gone or is not a models one. */
1754
+ function machinesPageReviewFromHubNode() {
1755
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1756
+ if (!overlay || typeof overlay.querySelector !== 'function') return null;
1757
+ return overlay.querySelector('[data-role="from-hub"]');
1758
+ }
1759
+
1760
+ /** Repaint ONLY the list: the rest of the dialog (and the note the action just wrote) stays put. */
1761
+ function machinesPageReviewFromHubPaint() {
1762
+ var machine = machinesPageMachineById(_machinesPageReview && _machinesPageReview.instanceId);
1763
+ var node = machinesPageReviewFromHubNode();
1764
+ if (!node || !machinesPageModels(machine)) return false;
1765
+ node.innerHTML = machinesPageReviewFromHubItemsHtml(machine);
1766
+ return true;
1767
+ }
1768
+
1769
+ /**
1770
+ * WP-5: ONE read of `models/copyable` per open, stored in the open-dialog state under this machine's
1771
+ * id. A refusal is the server's own code and is SHOWN, never retried — there is no timer here.
1772
+ */
1773
+ function machinesPageReviewFetchFromHub(instanceId) {
1774
+ var id = String(instanceId == null ? '' : instanceId);
1775
+ if (!id) return Promise.resolve(false);
1776
+ _machinesPageReview.fromHub = { instanceId: id, loading: true };
1777
+ machinesPageReviewFromHubPaint();
1778
+ return machinesPageJson(machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/models/copyable'))
1779
+ .then(function (result) {
1780
+ if (String(_machinesPageReview.instanceId) !== id) return false; // the dialog moved on meanwhile
1781
+ _machinesPageReview.fromHub = result.ok
1782
+ ? { instanceId: id, routes: (result.data && Array.isArray(result.data.routes)) ? result.data.routes : [] }
1783
+ : { instanceId: id, error: machinesPageServerError(result.data, result.status) };
1784
+ machinesPageReviewFromHubPaint();
1785
+ return !!result.ok;
1786
+ })
1787
+ .catch(function (error) {
1788
+ // REPORT: the subsection says why it is empty; the dialog stays open and nothing is retried.
1789
+ if (String(_machinesPageReview.instanceId) !== id) return false;
1790
+ _machinesPageReview.fromHub = {
1791
+ instanceId: id,
1792
+ error: 'Could not reach the hub: ' + ((error && error.message) || 'fetch failed'),
1793
+ };
1794
+ machinesPageReviewFromHubPaint();
1795
+ return false;
1796
+ });
1797
+ }
1798
+
1799
+ /** The name a row was painted with — the success note says what travelled, in the user's own words. */
1800
+ function machinesPageReviewFromHubRouteName(routeId) {
1801
+ var store = _machinesPageReview ? _machinesPageReview.fromHub : null;
1802
+ var routes = (store && Array.isArray(store.routes)) ? store.routes : [];
1803
+ var match = routes.filter(function (route) {
1804
+ return String((route && route.id) || '') === String(routeId == null ? '' : routeId);
1805
+ })[0];
1806
+ return String((match && match.name) || '') || String(routeId == null ? '' : routeId);
1807
+ }
1808
+
1809
+ function machinesPageReviewDialogHtml(machine) {
1810
+ return '<div class="machine-modal mpc-review-modal" role="dialog" aria-modal="true" aria-labelledby="mpc-review-title">'
1811
+ + '<h3 id="mpc-review-title">' + mEsc(machinesPageReviewTitle(machine)) + '</h3>'
1812
+ + '<p class="machine-modal-sub">'
1813
+ + mEsc(machinesPageSkillsLineText(machine) || machinesPageModelsLineText(machine)) + '</p>'
1814
+ + '<div class="mpc-review-note" data-role="review-note" role="status" aria-live="polite"></div>'
1815
+ + machinesPageReviewHeldBackHtml(machine)
1816
+ + machinesPageReviewOnBoxHtml(machine)
1817
+ + machinesPageReviewPluginsHtml(machine)
1818
+ + machinesPageReviewModelsHtml(machine)
1819
+ + '<div class="machine-modal-actions"><button type="button" class="mpc-btn" onclick="closeMachinesPageDialog()">Close</button></div>'
1820
+ + '</div>';
1821
+ }
1822
+
1823
+ // ── diff rendering (the LCS line diff is public/js/prompt-diff.js — never a second copy) ────────
1824
+ var MACHINES_PAGE_DIFF_LINE_CAP = 400;
1825
+
1826
+ function machinesPageDiffOps(hubText, boxText) {
1827
+ var diff = typeof PromptDiff !== 'undefined' && PromptDiff && typeof PromptDiff.diffLines === 'function'
1828
+ ? PromptDiff
1829
+ : (typeof window !== 'undefined' ? window.PromptDiff : null);
1830
+ if (!diff || typeof diff.diffLines !== 'function') return null;
1831
+ return diff.diffLines(hubText == null ? '' : String(hubText), boxText == null ? '' : String(boxText));
1832
+ }
1833
+
1834
+ function machinesPageDiffFileHtml(boxFile, hubFile) {
1835
+ var filePath = String((boxFile && boxFile.path) || (hubFile && hubFile.path) || '');
1836
+ var size = Number((boxFile && boxFile.size) || (hubFile && hubFile.size) || 0);
1837
+ var head = '<p class="mpc-diff-file">' + mEsc(filePath) + '</p>';
1838
+ if ((boxFile && boxFile.binary) || (hubFile && hubFile.binary)) {
1839
+ return head + '<p class="mpc-diff-binary">' + mEsc('binary file, ' + machinesFormatBytes(size)) + '</p>';
1840
+ }
1841
+ var ops = machinesPageDiffOps(hubFile ? hubFile.text : '', boxFile ? boxFile.text : '');
1842
+ if (!ops) return head + '<p class="mpc-diff-binary">' + mEsc('binary file, ' + machinesFormatBytes(size)) + '</p>';
1843
+ var shown = ops.slice(0, MACHINES_PAGE_DIFF_LINE_CAP);
1844
+ var lines = shown.map(function (op) {
1845
+ var cls = op.type === 'add' ? 'mpc-diff-add' : (op.type === 'del' ? 'mpc-diff-del' : 'mpc-diff-eq');
1846
+ var sign = op.type === 'add' ? '+' : (op.type === 'del' ? '-' : ' ');
1847
+ return '<span class="' + cls + '">' + mEsc(sign + ' ' + op.text) + '</span>';
1848
+ }).join('');
1849
+ var more = ops.length > shown.length
1850
+ ? '<p class="mpc-review-note">' + mEsc('… ' + (ops.length - shown.length) + ' more lines') + '</p>'
1851
+ : '';
1852
+ return head + '<div class="mpc-diff">' + lines + '</div>' + more;
1853
+ }
1854
+
1855
+ /** A preview payload → the per-file diff markup. Pure, so the dialog is testable without a socket. */
1856
+ function machinesPageDiffHtml(preview) {
1857
+ var boxFiles = (preview && preview.box && Array.isArray(preview.box.files)) ? preview.box.files : [];
1858
+ var hubFiles = (preview && preview.hub && Array.isArray(preview.hub.files)) ? preview.hub.files : [];
1859
+ var hubByName = {};
1860
+ hubFiles.forEach(function (file) { if (file && file.path) hubByName[String(file.path)] = file; });
1861
+ var seen = {};
1862
+ var out = boxFiles.map(function (file) {
1863
+ var key = String((file && file.path) || '');
1864
+ seen[key] = true;
1865
+ return machinesPageDiffFileHtml(file, hubByName[key] || null);
1866
+ });
1867
+ hubFiles.forEach(function (file) {
1868
+ var key = String((file && file.path) || '');
1869
+ if (seen[key]) return;
1870
+ out.push('<p class="mpc-diff-file">' + mEsc(key) + '</p><p class="mpc-diff-binary">'
1871
+ + mEsc('only on this hub — importing replaces it (a backup is kept)') + '</p>');
1872
+ });
1873
+ return out.join('') || '<p class="mpc-review-empty">No files on either side.</p>';
1874
+ }
1875
+
1876
+ // ── the Review dialog's wiring (AC-9a: an action re-reads /api/machines and repaints IN PLACE) ───
1877
+ // Every write here is one of the three WP-8 routes; the dialog never holds a copy of the list, so a
1878
+ // successful action cannot leave the row showing yesterday's numbers. Nothing calls location.reload().
1879
+ var _machinesPageReview = { instanceId: '', skipped: {}, fromHub: null };
1880
+
1881
+ function machinesPageMachineById(instanceId) {
1882
+ var id = String(instanceId == null ? '' : instanceId);
1883
+ return _machinesPageState.machines.find(function (machine) {
1884
+ return String((machine && machine.instanceId) || '') === id;
1885
+ }) || null;
1886
+ }
1887
+
1888
+ function machinesPageReviewNote(text, kind) {
1889
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1890
+ var node = overlay && typeof overlay.querySelector === 'function'
1891
+ ? overlay.querySelector('[data-role="review-note"]') : null;
1892
+ if (!node) return;
1893
+ node.textContent = String(text || '');
1894
+ node.classList.toggle('bad', kind !== 'ok');
1895
+ }
1896
+
1897
+ /** Buttons are disabled while their own request is in flight (a double-tap must not double-post). */
1898
+ function _machinesPageReviewBusy(button, busy) {
1899
+ if (!button || typeof button !== 'object') return;
1900
+ button.disabled = !!busy;
1901
+ if (typeof button.setAttribute === 'function') button.setAttribute('aria-busy', busy ? 'true' : 'false');
1902
+ }
1903
+
1904
+ function machinesPageReviewItemKey(agent, name) {
1905
+ return String(agent || '') + ':' + String(name || '');
1906
+ }
1907
+
1908
+ /** Client-only Skip: the item leaves this dialog and stays gone across a repaint — no server call. */
1909
+ function machinesPageReviewSkip(instanceId, agent, name) {
1910
+ var key = machinesPageReviewItemKey(agent, name);
1911
+ _machinesPageReview.skipped[key] = true;
1912
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1913
+ if (overlay && typeof overlay.querySelector === 'function') {
1914
+ var item = overlay.querySelector('.mpc-review-item[data-key="' + key.replace(/"/g, '\\"') + '"]');
1915
+ if (item) item.hidden = true;
1916
+ }
1917
+ return key;
1918
+ }
1919
+
1920
+ function _machinesPageReviewApplySkips() {
1921
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1922
+ if (!overlay || typeof overlay.querySelectorAll !== 'function') return;
1923
+ var items = overlay.querySelectorAll('.mpc-review-item[data-key]');
1924
+ Array.prototype.slice.call(items || []).forEach(function (item) {
1925
+ var key = typeof item.getAttribute === 'function' ? item.getAttribute('data-key') : null;
1926
+ if (key && _machinesPageReview.skipped[key]) item.hidden = true;
1927
+ });
1928
+ }
1929
+
1930
+ function machinesPageReviewPaint() {
1931
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
1932
+ if (!overlay) return null;
1933
+ var machine = machinesPageMachineById(_machinesPageReview.instanceId);
1934
+ if (!machinesPageReviewOpenable(machine)) { closeMachinesPageDialog(); return null; }
1935
+ overlay.innerHTML = machinesPageReviewDialogHtml(machine);
1936
+ _machinesPageReviewApplySkips(overlay);
1937
+ return machine;
1938
+ }
1939
+
1940
+ function machinesPageOpenReview(instanceId) {
1941
+ var id = String(instanceId == null ? '' : instanceId);
1942
+ var machine = machinesPageMachineById(id);
1943
+ if (!machinesPageReviewOpenable(machine)) return null;
1944
+ closeMachinesPageDialog();
1945
+ _machinesPageReview = { instanceId: id, skipped: {}, fromHub: null };
1946
+ var overlay = _machinesPageMountDialog(machinesPageReviewDialogHtml(machine));
1947
+ _machinesPageDialog = { overlay: overlay, review: true };
1948
+ // WP-5: one read of the hub's copyable routes per open — the dialog is already painted, so the
1949
+ // subsection shows "Looking for…" until this lands.
1950
+ machinesPageReviewFetchFromHub(id);
1951
+ return _machinesPageDialog;
1952
+ }
1953
+
1954
+ /** AC-9a: re-read the list, repaint the row, and repaint the OPEN dialog — never a page reload. */
1955
+ function machinesPageReviewRefresh() {
1956
+ return machinesPageJson(machinesPageUrl('/api/machines')).then(function (list) {
1957
+ if (list && list.ok) {
1958
+ _machinesPageState.machines = (list.data && Array.isArray(list.data.machines)) ? list.data.machines : [];
1959
+ _machinesPageState.hubNetworkSuspect = !!(list.data && list.data.hubNetworkSuspect);
1960
+ renderMachinesPage();
1961
+ }
1962
+ machinesPageReviewPaint();
1963
+ return true;
1964
+ }).catch(function () {
1965
+ // REPORT: the dialog says why it could not refresh; it stays open with what it has.
1966
+ machinesPageReviewNote('Could not re-read the machine list — close and reopen Review.');
1967
+ return false;
1968
+ });
1969
+ }
1970
+
1971
+ /**
1972
+ * AC-8c/AC-9a: Import (POST .../skills/import) or "Use the laptop's copy" (POST .../skills/retire).
1973
+ * `expectBoxHash` is the hash the user is looking at, so the server can refuse a box that moved on.
1974
+ */
1975
+ function machinesPageReviewAction(instanceId, agent, name, boxHash, action, button) {
1976
+ var id = String(instanceId || '');
1977
+ var verb = String(action) === 'retire' ? 'retire' : 'import';
1978
+ var body = { agent: String(agent || ''), name: String(name || '') };
1979
+ if (boxHash) body.expectBoxHash = String(boxHash);
1980
+ _machinesPageReviewBusy(button, true);
1981
+ return machinesPageJson(
1982
+ machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/skills/' + verb),
1983
+ { method: 'POST', body: body },
1984
+ ).then(function (result) {
1985
+ _machinesPageReviewBusy(button, false);
1986
+ if (!result.ok) return _machinesPageReviewActionFailed(result);
1987
+ machinesPageToast(verb === 'import'
1988
+ ? 'Imported ' + String(name || '') + ' from this machine.'
1989
+ : 'This machine now uses the laptop’s copy of ' + String(name || '') + '.');
1990
+ delete _machinesPageReview.skipped[machinesPageReviewItemKey(agent, name)];
1991
+ return machinesPageReviewRefresh();
1992
+ }).catch(function (error) {
1993
+ // REPORT: a transport failure is shown in the dialog, which stays open for a retry.
1994
+ _machinesPageReviewBusy(button, false);
1995
+ machinesPageReviewNote('Could not reach the hub: ' + ((error && error.message) || 'fetch failed'));
1996
+ return false;
1997
+ });
1998
+ }
1999
+
2000
+ function _machinesPageReviewActionFailed(result) {
2001
+ var data = result && result.data;
2002
+ var code = String((data && (data.error || data.code)) || '');
2003
+ if (Number(result && result.status) === 409 || code.indexOf('agent_skill_changed') !== -1) {
2004
+ machinesPageReviewNote('This skill changed on the box since you opened Review — reopen to see the new version.');
2005
+ return machinesPageReviewRefresh();
2006
+ }
2007
+ machinesPageReviewNote(machinesPageServerError(data, result && result.status));
2008
+ return false;
2009
+ }
2010
+
2011
+ /**
2012
+ * §8 WP-4: "Remove from <machine>" — the box deletes these routes and the keys it holds for them.
2013
+ * The hub keeps its own copies, which is exactly what the confirm says before anything is sent; a
2014
+ * cancelled confirm is not a request. Only the TICKED ids travel, and the list is re-read after.
2015
+ */
2016
+ function machinesPageReviewRemoveModels(instanceId, button) {
2017
+ var id = String(instanceId == null ? '' : instanceId);
2018
+ var name = machinesPageSkillsMachineName(machinesPageMachineById(id));
2019
+ var routeIds = machinesPageReviewModelsChecked();
2020
+ if (!routeIds.length) return Promise.resolve(false); // nothing ticked → nothing to send
2021
+ if (typeof confirm === 'function'
2022
+ && !confirm('Deletes these routes and their keys from ' + name + '. This hub keeps its own copies.')) {
2023
+ return Promise.resolve(false);
2024
+ }
2025
+ _machinesPageReviewBusy(button, true);
2026
+ return machinesPageJson(
2027
+ machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/models/remove'),
2028
+ { method: 'POST', body: { routeIds: routeIds } },
2029
+ ).then(function (result) {
2030
+ _machinesPageReviewBusy(button, false);
2031
+ if (!result.ok) { machinesPageReviewNote(machinesPageServerError(result.data, result.status)); return false; }
2032
+ var data = result.data || {};
2033
+ var removed = Array.isArray(data.removed) ? data.removed.length : routeIds.length;
2034
+ var missing = Array.isArray(data.missing) ? data.missing.length : 0;
2035
+ // The note goes AFTER the reload: the reload repaints the dialog and would wipe a note set first.
2036
+ // WP-5: the removals also change what this hub can offer, hence the reload of both lists.
2037
+ return machinesPageReviewReload().then(function (reloaded) {
2038
+ machinesPageReviewNote('Removed ' + removed + ' from ' + name + (missing ? ' · ' + missing + ' already gone' : ''), 'ok');
2039
+ return reloaded;
2040
+ });
2041
+ }).catch(function (error) {
2042
+ // REPORT: a transport failure is shown in the dialog, which stays open for a retry.
2043
+ _machinesPageReviewBusy(button, false);
2044
+ machinesPageReviewNote('Could not reach the hub: ' + ((error && error.message) || 'fetch failed'));
2045
+ return false;
2046
+ });
2047
+ }
2048
+
2049
+ /** WP-5: after a copy or a remove, re-read BOTH the list and the routes this hub could still offer. */
2050
+ function machinesPageReviewReload() {
2051
+ var id = String((_machinesPageReview && _machinesPageReview.instanceId) || '');
2052
+ return machinesPageReviewRefresh().then(function () {
2053
+ return machinesPageReviewFetchFromHub(id);
2054
+ });
2055
+ }
2056
+
2057
+ /**
2058
+ * §8 WP-5 / AC-5c: "Use on <name>" — copies THIS one route and its key to the machine, and nothing
2059
+ * else; there is no control anywhere in this dialog that copies two routes at once. The confirm says
2060
+ * exactly that before anything is sent, and `expectOrigin` is the origin the row was painted with, so
2061
+ * a route that moved on the hub is refused instead of copied.
2062
+ */
2063
+ function machinesPageReviewCopyModel(instanceId, button) {
2064
+ var id = String(instanceId == null ? '' : instanceId);
2065
+ var name = machinesPageSkillsMachineName(machinesPageMachineById(id));
2066
+ var attr = function (key) {
2067
+ return button && typeof button.getAttribute === 'function' ? String(button.getAttribute(key) || '') : '';
2068
+ };
2069
+ var routeId = attr('data-route-id');
2070
+ var origin = attr('data-origin');
2071
+ if (!routeId) return Promise.resolve(false); // no route on the row → nothing to copy
2072
+ var label = machinesPageReviewFromHubRouteName(routeId);
2073
+ if (typeof confirm === 'function'
2074
+ && !confirm('Copies this route and its key to ' + name + '. Only this route is copied.')) {
2075
+ return Promise.resolve(false);
2076
+ }
2077
+ _machinesPageReviewBusy(button, true);
2078
+ return machinesPageJson(
2079
+ machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/models/copy'),
2080
+ { method: 'POST', body: { routeId: routeId, expectOrigin: origin } },
2081
+ ).then(function (result) {
2082
+ _machinesPageReviewBusy(button, false);
2083
+ if (!result.ok) { machinesPageReviewNote(machinesPageServerError(result.data, result.status)); return false; }
2084
+ // After the reload, which repaints the dialog (a note set first would be wiped).
2085
+ return machinesPageReviewReload().then(function (reloaded) {
2086
+ machinesPageReviewNote('Copied ' + label + ' to ' + name, 'ok');
2087
+ return reloaded;
2088
+ });
2089
+ }).catch(function (error) {
2090
+ // REPORT: a transport failure is shown in the dialog, which stays open for a retry.
2091
+ _machinesPageReviewBusy(button, false);
2092
+ machinesPageReviewNote('Could not reach the hub: ' + ((error && error.message) || 'fetch failed'));
2093
+ return false;
2094
+ });
2095
+ }
2096
+
2097
+ /** §9 S-9: checked = excluded. PUT replaces the list wholesale (the route's own contract). */
2098
+ function machinesPageReviewSavePlugins(instanceId, button) {
2099
+ var id = String(instanceId || '');
2100
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
2101
+ var excluded = [];
2102
+ if (overlay && typeof overlay.querySelectorAll === 'function') {
2103
+ Array.prototype.slice.call(overlay.querySelectorAll('.mpc-review-plugins input') || []).forEach(function (box) {
2104
+ if (box && box.checked) excluded.push(String(box.value == null ? '' : box.value));
2105
+ });
2106
+ }
2107
+ _machinesPageReviewBusy(button, true);
2108
+ return machinesPageJson(
2109
+ machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/skills/exclude-plugins'),
2110
+ { method: 'PUT', body: { plugins: excluded } },
2111
+ ).then(function (result) {
2112
+ _machinesPageReviewBusy(button, false);
2113
+ if (!result.ok) { machinesPageReviewNote(machinesPageServerError(result.data, result.status)); return false; }
2114
+ machinesPageToast(excluded.length ? 'Plugins saved — ' + excluded.length + ' not sent to this machine.' : 'Plugins saved — every plugin is sent to this machine.');
2115
+ return machinesPageReviewRefresh();
2116
+ }).catch(function (error) {
2117
+ _machinesPageReviewBusy(button, false);
2118
+ machinesPageReviewNote('Could not reach the hub: ' + ((error && error.message) || 'fetch failed'));
2119
+ return false;
2120
+ });
2121
+ }
2122
+
2123
+ function _machinesPageReviewDiffNode(agent, name) {
2124
+ var overlay = _machinesPageDialog && _machinesPageDialog.overlay;
2125
+ if (!overlay || typeof overlay.querySelector !== 'function') return null;
2126
+ var key = machinesPageReviewItemKey(agent, name).replace(/"/g, '\\"');
2127
+ var item = overlay.querySelector('.mpc-review-item[data-key="' + key + '"]');
2128
+ return item && typeof item.querySelector === 'function' ? item.querySelector('.mpc-review-diff') : null;
2129
+ }
2130
+
2131
+ /** AC-9b: the diff is the hub's copy vs the box's, rendered by public/js/prompt-diff.js (desktop) —
2132
+ * the same module the phone lazy-loads, never a second algorithm. */
2133
+ function machinesPageShowDiff(instanceId, agent, name, button) {
2134
+ var id = String(instanceId || '');
2135
+ var node = _machinesPageReviewDiffNode(agent, name);
2136
+ if (node && !node.hidden) { node.hidden = true; return Promise.resolve(true); }
2137
+ _machinesPageReviewBusy(button, true);
2138
+ return machinesPageJson(machinesPageUrl(
2139
+ '/api/machines/' + encodeURIComponent(id) + '/skills/preview?agent=' + encodeURIComponent(String(agent || ''))
2140
+ + '&name=' + encodeURIComponent(String(name || '')),
2141
+ )).then(function (result) {
2142
+ _machinesPageReviewBusy(button, false);
2143
+ node = _machinesPageReviewDiffNode(agent, name);
2144
+ if (!node) return false;
2145
+ node.hidden = false;
2146
+ node.innerHTML = result.ok
2147
+ ? machinesPageDiffHtml(result.data)
2148
+ : '<p class="mpc-review-note">' + mEsc(machinesPageServerError(result.data, result.status)) + '</p>';
2149
+ return !!result.ok;
2150
+ }).catch(function (error) {
2151
+ _machinesPageReviewBusy(button, false);
2152
+ node = _machinesPageReviewDiffNode(agent, name);
2153
+ if (node) {
2154
+ node.hidden = false;
2155
+ node.innerHTML = '<p class="mpc-review-note">' + mEsc('Could not reach the hub: ' + ((error && error.message) || 'fetch failed')) + '</p>';
2156
+ }
2157
+ return false;
2158
+ });
2159
+ }
2160
+
2161
+ // ── WP-4: hub-bridge access (R-11 trust, R-4 grants, R-5 pending, R-9 audit, R-13 the UI) ─────────
2162
+ // Everything here is the Machines page's half of docs/remote-session-hub-bridge-2026-10-30.html: the
2163
+ // per-card "Can use this Mac: Ask · Always · Never" select (R-11), the card's Access block (active
2164
+ // grants with [Revoke], open asks with the 4-way grant card + [Deny], and the last 20 audit lines), the page-level
2165
+ // "Trust all my machines" switch, and the pane-overlay banner an open ask paints (AC-2g). Every
2166
+ // route is user-principal only (§6.2, no peerControl), so a 403 here is the server refusing a
2167
+ // non-admin session — shown as-is, never retried.
2168
+ //
2169
+ // No innerHTML of a server string: every value goes through mEsc.
2170
+
2171
+ // The Access block's HTML, the effective-policy rule and the request bodies are ONE copy, in
2172
+ // public/js/machine-pool-shared.js (loaded before this file): the phone Machines card calls the same
2173
+ // builders with its own escaper and its own `mp-*` class names. What is left here is this page's
2174
+ // state, its reads and its delegated wiring.
2175
+ var MACHINES_PAGE_ACCESS_OPTS = { esc: mEsc };
2176
+
2177
+ /** R-11: this page's effective policy for a machine (its own row, else the file default). */
2178
+ function machinesPageTrustValueFor(instanceId) {
2179
+ return machineEffectiveTrust(_machinesPageState.access, instanceId);
2180
+ }
2181
+
2182
+ /** Fold the two GET bodies into state. A failed/missing read leaves the surface empty, never stale. */
2183
+ function machinesPageApplyAccess(trustResult, grantsResult) {
2184
+ var trust = (trustResult && trustResult.ok && trustResult.data) ? trustResult.data : null;
2185
+ var payload = (grantsResult && grantsResult.ok && grantsResult.data) ? grantsResult.data : null;
2186
+ _machinesPageState.access = trust
2187
+ ? {
2188
+ v: 1,
2189
+ default: MACHINES_PAGE_TRUST_VALUES.indexOf(String(trust.default || '')) !== -1 ? String(trust.default) : MACHINES_PAGE_TRUST_DEFAULT,
2190
+ machines: (trust.machines && typeof trust.machines === 'object') ? trust.machines : {},
2191
+ }
2192
+ : null;
2193
+ var grants = (payload && Array.isArray(payload.grants)) ? payload.grants : [];
2194
+ var pending = (payload && Array.isArray(payload.pending)) ? payload.pending : [];
2195
+ _machinesPageState.accessPendingIndex = Object.create(null);
2196
+ for (var i = 0; i < pending.length; i += 1) {
2197
+ var row = pending[i];
2198
+ if (row && row.id) _machinesPageState.accessPendingIndex[String(row.id)] = row;
2199
+ }
2200
+ _machinesPageState.accessGrants = grants;
2201
+ _machinesPageState.accessPending = pending;
2202
+ return true;
2203
+ }
2204
+
2205
+ function machinesPageGrantsFor(instanceId) {
2206
+ return machineAccessRowsFor(_machinesPageState.accessGrants, instanceId);
2207
+ }
2208
+
2209
+ function machinesPagePendingFor(instanceId) {
2210
+ return machineAccessRowsFor(_machinesPageState.accessPending, instanceId);
2211
+ }
2212
+
2213
+ /** R-13: the last 20 audit lines for every member, in one round of reads (never This Mac's own). */
2214
+ function machinesPageLoadAccessAudits() {
2215
+ var rows = _machinesPageState.machines.filter(function (machine) { return machine && machine.instanceId; });
2216
+ return Promise.all(rows.map(function (machine) {
2217
+ var id = String(machine.instanceId);
2218
+ return machinesPageJson(machinesPageUrl('/api/hub-bridge/audit', 'machineId=' + encodeURIComponent(id) + '&limit=' + MACHINES_PAGE_AUDIT_LIMIT))
2219
+ .then(function (result) {
2220
+ _machinesPageState.accessAudit[id] = (result.ok && result.data && Array.isArray(result.data.entries)) ? result.data.entries : [];
2221
+ return null;
2222
+ })
2223
+ .catch(function () {
2224
+ // HANDLE: one machine's history is decoration; its card still renders from the other reads.
2225
+ _machinesPageState.accessAudit[id] = [];
2226
+ return null;
2227
+ });
2228
+ }));
2229
+ }
2230
+
2231
+ /** Re-read trust + grants + audit and repaint (after any action, and on both WS frames). */
2232
+ function machinesPageRefreshAccess() {
2233
+ var panel = (typeof document !== 'undefined' && document) ? document.getElementById('machines-panel') : null;
2234
+ if (!panel || typeof fetch !== 'function') return Promise.resolve(false);
2235
+ return Promise.all([
2236
+ machinesPageJson(machinesPageUrl('/api/hub-bridge/trust')),
2237
+ machinesPageJson(machinesPageUrl('/api/hub-bridge/grants')),
2238
+ ]).then(function (results) {
2239
+ machinesPageApplyAccess(results[0], results[1]);
2240
+ return machinesPageLoadAccessAudits();
2241
+ }).then(function () {
2242
+ renderMachinesPage();
2243
+ return true;
2244
+ }).catch(function () {
2245
+ // REPORT: a transport fault is not a silent success; the page keeps its last honest paint.
2246
+ machinesPageToast('hub_bridge_unavailable');
2247
+ return false;
2248
+ });
2249
+ }
2250
+
2251
+ // ── the trust select + the page switch (R-11, R-13) ───────────────────────────────────────────────
2252
+ function machinesPageTrustAllHtml() {
2253
+ return machineTrustAllHtml(_machinesPageState.access, MACHINES_PAGE_ACCESS_OPTS);
2254
+ }
2255
+
2256
+ function machinesPageTrustSelectHtml(instanceId) {
2257
+ return machineTrustSelectHtml(instanceId, _machinesPageState.access, MACHINES_PAGE_ACCESS_OPTS);
2258
+ }
2259
+
2260
+ /** R-11: PUT one machine's row. The server validates the value; a refusal is shown, never swallowed. */
2261
+ function machinesPageTrustChange(instanceId, value, select) {
2262
+ var id = String(instanceId || '');
2263
+ var body = hubBridgeTrustBody(id, value);
2264
+ return machinesPageJson(machinesPageUrl('/api/hub-bridge/trust'), { method: 'PUT', body: body })
2265
+ .then(function (result) {
2266
+ if (!result.ok) {
2267
+ machinesPageToast(machinesPageServerError(result.data, result.status));
2268
+ if (select) {
2269
+ // Put the control back where the server still says it is (the last honest policy).
2270
+ select.value = machinesPageTrustValueFor(id);
2271
+ }
2272
+ return false;
2273
+ }
2274
+ machinesPageToast('');
2275
+ return machinesPageRefreshAccess();
2276
+ })
2277
+ .catch(function (error) {
2278
+ // REPORT: an unreachable hub must not look like a saved policy.
2279
+ machinesPageToast((error && error.message) || 'trust_write_failed');
2280
+ return false;
2281
+ });
2282
+ }
2283
+
2284
+ /** R-13: the page switch writes the trust file DEFAULT ('always' on, 'ask' off). */
2285
+ function machinesPageTrustAll(on) {
2286
+ return machinesPageJson(machinesPageUrl('/api/hub-bridge/trust'), { method: 'PUT', body: hubBridgeTrustAllBody(on) })
2287
+ .then(function (result) {
2288
+ if (!result.ok) {
2289
+ machinesPageToast(machinesPageServerError(result.data, result.status));
2290
+ return false;
2291
+ }
2292
+ machinesPageToast('');
2293
+ return machinesPageRefreshAccess();
2294
+ })
2295
+ .catch(function (error) {
2296
+ machinesPageToast((error && error.message) || 'trust_write_failed');
2297
+ return false;
2298
+ });
2299
+ }
2300
+
2301
+ // ── the Access block (R-13) ───────────────────────────────────────────────────────────────────────
2302
+ function machinesPageAccessBlockHtml(machine) {
2303
+ var m = machine || {};
2304
+ var id = String(m.instanceId || '');
2305
+ if (!id) return '';
2306
+ return machineAccessBlockHtml(id, {
2307
+ trust: _machinesPageState.access,
2308
+ grants: machinesPageGrantsFor(id),
2309
+ pending: machinesPagePendingFor(id),
2310
+ audit: Array.isArray(_machinesPageState.accessAudit[id]) ? _machinesPageState.accessAudit[id] : [],
2311
+ now: machinesPageNow(),
2312
+ }, MACHINES_PAGE_ACCESS_OPTS);
2313
+ }
2314
+
2315
+ // ── the three writes (R-4/R-5) ────────────────────────────────────────────────────────────────────
2316
+ /** The pending row's own machine/session/tier is what is granted — never the card's id. */
2317
+ function machinesPagePendingRow(pendingId) {
2318
+ var key = String(pendingId || '');
2319
+ if (!key) return null;
2320
+ return _machinesPageState.accessPendingIndex[key] || _machinesPageBanners[key] || null;
2321
+ }
2322
+
2323
+ /** Answer the ask: POST the grant its own tier names, in the scope and for the duration it names (D-1). */
2324
+ function machinesPageAccessAllow(instanceId, pendingId, grant) {
2325
+ var row = machinesPagePendingRow(pendingId);
2326
+ if (!row) {
2327
+ machinesPageToast('pending_not_found');
2328
+ return Promise.resolve(false);
2329
+ }
2330
+ var call = hubBridgeAnswerCall(row, instanceId, grant);
2331
+ return machinesPageJson(machinesPageUrl(call[0]), call[1]).then(function (result) {
2332
+ if (!result.ok) {
2333
+ machinesPageToast(machinesPageServerError(result.data, result.status));
2334
+ return false;
2335
+ }
2336
+ machinesPageToast('');
2337
+ delete _machinesPageBanners[String(pendingId)];
2338
+ machinesPagePaintHubBridgeBanners();
2339
+ return machinesPageRefreshAccess();
2340
+ }).catch(function (error) {
2341
+ machinesPageToast((error && error.message) || 'grant_failed');
2342
+ return false;
2343
+ });
2344
+ }
2345
+
2346
+ function machinesPageAccessDeny(pendingId) {
2347
+ var key = String(pendingId || '');
2348
+ if (!key) return Promise.resolve(false);
2349
+ return machinesPageJson(machinesPageUrl(hubBridgeDenyPath(key)), { method: 'POST', body: {} })
2350
+ .then(function (result) {
2351
+ if (!result.ok) {
2352
+ machinesPageToast(machinesPageServerError(result.data, result.status));
2353
+ return false;
2354
+ }
2355
+ machinesPageToast('');
2356
+ delete _machinesPageBanners[key];
2357
+ machinesPagePaintHubBridgeBanners();
2358
+ return machinesPageRefreshAccess();
2359
+ })
2360
+ .catch(function (error) {
2361
+ machinesPageToast((error && error.message) || 'deny_failed');
2362
+ return false;
2363
+ });
2364
+ }
2365
+
2366
+ /** DELETE with the (machine, session, tier) triple — the server drops that tier and everything below it. */
2367
+ function machinesPageAccessRevoke(instanceId, callerSessionId, tier) {
2368
+ return machinesPageJson(machinesPageUrl('/api/hub-bridge/grants'), {
2369
+ method: 'DELETE',
2370
+ body: hubBridgeRevokeBody(instanceId, callerSessionId, tier),
2371
+ }).then(function (result) {
2372
+ if (!result.ok) {
2373
+ machinesPageToast(machinesPageServerError(result.data, result.status));
2374
+ return false;
2375
+ }
2376
+ machinesPageToast('');
2377
+ return machinesPageRefreshAccess();
2378
+ }).catch(function (error) {
2379
+ machinesPageToast((error && error.message) || 'revoke_failed');
2380
+ return false;
2381
+ });
2382
+ }
2383
+
2384
+ // ── AC-2g: the pane overlay an open ask paints (docs/hub-bridge-approval-banner-cutoff-2026-10-31.html)
2385
+ // Absolute in the session's .term-container — never a toolbar / tab-strip row (that crushed the tabs).
2386
+ /** The banner's markup is the shared one (js/machine-pool-shared.js), so the phone says the same thing. */
2387
+ function hubBridgePendingBannerHtml(row) {
2388
+ return hubBridgeBannerHtml(row, MACHINES_PAGE_ACCESS_OPTS);
2389
+ }
2390
+
2391
+ function machinesPageBannerAllow(pendingId, grant) {
2392
+ return machinesPageAccessAllow('', pendingId, grant);
2393
+ }
2394
+
2395
+ function machinesPageBannerDeny(pendingId) {
2396
+ return machinesPageAccessDeny(pendingId);
2397
+ }
2398
+
2399
+ /** Is this session one the page shows? (the banner belongs to a mirror row the user can see) */
2400
+ function machinesPageSessionShown(sessionId) {
2401
+ var sid = String(sessionId || '');
2402
+ if (!sid) return false;
2403
+ if (typeof state === 'undefined' || !state || !state.sessions) return false;
2404
+ var sessions = state.sessions;
2405
+ if (sessions && typeof sessions.has === 'function') return sessions.has(sid);
2406
+ if (Array.isArray(sessions)) return sessions.some(function (row) { return String((row && row.id) || '') === sid; });
2407
+ return false;
2408
+ }
2409
+
2410
+ /** The pane the asking session's banner overlays: its own .term-container, wherever its toolbar is. */
2411
+ function machinesPageBannerHostFor(sessionId) {
2412
+ var sid = String(sessionId || '');
2413
+ if (!sid) return null;
2414
+ if (typeof document === 'undefined' || !document || typeof document.querySelector !== 'function') return null;
2415
+ var escaped = (typeof window !== 'undefined' && window.CSS && typeof CSS.escape === 'function')
2416
+ ? CSS.escape(sid)
2417
+ : sid.replace(/["\\]/g, '\\$&');
2418
+ return document.querySelector('.term-container[data-session-id="' + escaped + '"]');
2419
+ }
2420
+
2421
+ /**
2422
+ * Repaint every banner: one per shown session with an open ask, removed for every session whose ask
2423
+ * was answered or cleared. Idempotent — safe to call from the seed, a frame, or after an action.
2424
+ */
2425
+ function machinesPagePaintHubBridgeBanners() {
2426
+ if (typeof document === 'undefined' || !document) return 0;
2427
+ var bySession = Object.create(null);
2428
+ Object.keys(_machinesPageBanners).forEach(function (id) {
2429
+ var row = _machinesPageBanners[id];
2430
+ var sid = row && row.callerSessionId ? String(row.callerSessionId) : '';
2431
+ if (!sid || !machinesPageSessionShown(sid)) return;
2432
+ if (!bySession[sid]) bySession[sid] = row;
2433
+ });
2434
+ var painted = 0;
2435
+ Object.keys(bySession).forEach(function (sid) {
2436
+ var host = machinesPageBannerHostFor(sid);
2437
+ if (!host) return;
2438
+ var node = (typeof host.querySelector === 'function') ? host.querySelector(':scope > .hub-bridge-banner') : null;
2439
+ if (!node) {
2440
+ if (typeof document.createElement !== 'function') return;
2441
+ node = document.createElement('div');
2442
+ node.className = 'hub-bridge-banner';
2443
+ node.dataset.sessionId = sid;
2444
+ node.setAttribute('role', 'alert');
2445
+ if (typeof host.appendChild === 'function') host.appendChild(node);
2446
+ }
2447
+ node.innerHTML = hubBridgePendingBannerHtml(bySession[sid]);
2448
+ painted += 1;
2449
+ });
2450
+ // Drop the banners whose ask is gone (or whose session the page no longer shows).
2451
+ if (typeof document.querySelectorAll === 'function') {
2452
+ var nodes = document.querySelectorAll('.hub-bridge-banner');
2453
+ for (var i = 0; i < (nodes ? nodes.length : 0); i += 1) {
2454
+ var banner = nodes[i];
2455
+ var bannerId = String((banner.dataset && banner.dataset.sessionId) || '');
2456
+ if (bySession[bannerId]) continue;
2457
+ if (banner.parentNode && typeof banner.parentNode.removeChild === 'function') banner.parentNode.removeChild(banner);
2458
+ }
2459
+ }
2460
+ return painted;
2461
+ }
2462
+
2463
+ /** AC-2g: a `hub-bridge-pending` frame paints the banner on the session it names. */
2464
+ function onHubBridgePending(frame) {
2465
+ var row = frame && frame.pending;
2466
+ if (!row || !row.id) return false;
2467
+ _machinesPageBanners[String(row.id)] = row;
2468
+ machinesPagePaintHubBridgeBanners();
2469
+ if (machinesPagePanelOpen()) machinesPageRefreshAccess();
2470
+ return true;
2471
+ }
2472
+
2473
+ /** AC-2g: a `hub-bridge-pending-cleared` frame hides every banner it names. */
2474
+ function onHubBridgePendingCleared(frame) {
2475
+ var ids = (frame && Array.isArray(frame.ids)) ? frame.ids : [];
2476
+ if (!ids.length) return false;
2477
+ for (var i = 0; i < ids.length; i += 1) delete _machinesPageBanners[String(ids[i] || '')];
2478
+ machinesPagePaintHubBridgeBanners();
2479
+ if (machinesPagePanelOpen()) machinesPageRefreshAccess();
2480
+ return true;
2481
+ }
2482
+
2483
+ function machinesPagePanelOpen() {
2484
+ if (typeof document === 'undefined' || !document || typeof document.getElementById !== 'function') return false;
2485
+ var panel = document.getElementById('machines-panel');
2486
+ return !!(panel && panel.style && panel.style.display !== 'none');
2487
+ }
2488
+
2489
+ // ── the delegated wiring (one pair of listeners for every render) ─────────────────────────────────
2490
+ // The shared builders mark every control with `data-hub-bridge-*` instead of an inline handler, so a
2491
+ // card repaint, the toolbar banner and the audit repaint all keep working without rebinding. Capture
2492
+ // phase, so a control inside a card never also opens the card.
2493
+ function machinesPageAccessBind(doc) {
2494
+ var target = doc || ((typeof document !== 'undefined') ? document : null);
2495
+ if (!target || typeof target.addEventListener !== 'function') return false;
2496
+ if (target.__ctmMachinesAccessBound) return false;
2497
+ target.__ctmMachinesAccessBound = true;
2498
+ target.addEventListener('change', function (event) {
2499
+ var node = (event.target && typeof event.target.closest === 'function')
2500
+ ? event.target.closest(HUB_BRIDGE_TRUST_HOOK) : null;
2501
+ if (!node || typeof node.getAttribute !== 'function') return;
2502
+ if (node.hasAttribute && node.hasAttribute('data-hub-bridge-trust-all')) {
2503
+ machinesPageTrustAll(!!event.target.checked);
2504
+ return;
2505
+ }
2506
+ machinesPageTrustChange(node.getAttribute('data-hub-bridge-trust'), event.target.value, event.target);
2507
+ }, true);
2508
+ target.addEventListener('click', function (event) {
2509
+ var node = (event.target && typeof event.target.closest === 'function')
2510
+ ? event.target.closest(HUB_BRIDGE_ACTION_HOOK) : null;
2511
+ if (!node || typeof node.getAttribute !== 'function') return;
2512
+ if (typeof event.stopPropagation === 'function') event.stopPropagation();
2513
+ var action = node.getAttribute('data-hub-bridge');
2514
+ if (action === 'allow') machinesPageBannerAllow(node.getAttribute('data-pending-id'), node.getAttribute('data-grant'));
2515
+ else if (action === 'deny') machinesPageBannerDeny(node.getAttribute('data-pending-id'));
2516
+ else if (action === 'revoke') {
2517
+ machinesPageAccessRevoke(node.getAttribute('data-machine-id'), node.getAttribute('data-session-id'), node.getAttribute('data-tier'));
2518
+ }
2519
+ }, true);
2520
+ return true;
2521
+ }
2522
+
2523
+ /** One entry for both hub-bridge frames (public/js/ws/live-dispatch.js calls this). */
2524
+ function machinesPageHubBridgeOnFrame(msg) {
2525
+ var type = String((msg && msg.type) || '');
2526
+ if (type === 'hub-bridge-pending') return onHubBridgePending(msg);
2527
+ if (type === 'hub-bridge-pending-cleared') return onHubBridgePendingCleared(msg);
2528
+ return false;
2529
+ }
2530
+
2531
+ // The live WS dispatcher (js/ws/live-dispatch.js) hands both frames here; it is loaded after this
2532
+ // file, so the assignment is the contract between them and not a load-order accident.
2533
+ if (typeof globalThis !== 'undefined' && globalThis) {
2534
+ globalThis.CtmHubBridgeAccess = { onFrame: machinesPageHubBridgeOnFrame };
2535
+ }
2536
+
2537
+ /** AC-2g: on page load, seed the banners from the hub's own list of open asks. */
2538
+ function machinesPageSeedHubBridgeBanners() {
2539
+ // A slice without a transport (a unit realm) simply seeds nothing; the page always has fetch.
2540
+ if (typeof fetch !== 'function') return Promise.resolve(false);
2541
+ return machinesPageJson(machinesPageUrl('/api/hub-bridge/grants')).then(function (result) {
2542
+ if (!result.ok) return false;
2543
+ var pending = (result.data && Array.isArray(result.data.pending)) ? result.data.pending : [];
2544
+ _machinesPageBanners = Object.create(null);
2545
+ for (var i = 0; i < pending.length; i += 1) {
2546
+ var row = pending[i];
2547
+ if (row && row.id) _machinesPageBanners[String(row.id)] = row;
2548
+ }
2549
+ machinesPagePaintHubBridgeBanners();
2550
+ return true;
2551
+ }).catch(function () {
2552
+ // HANDLE: a hub that cannot be read seeds nothing; the next frame or page load tries again.
2553
+ return false;
2554
+ });
2555
+ }
2556
+
2557
+ // The page-load seed: run it now when the document is already parsed, else on DOMContentLoaded.
2558
+ if (typeof document !== 'undefined' && document && typeof document.addEventListener === 'function') {
2559
+ var _machinesPageSeedBanners = function () {
2560
+ try {
2561
+ machinesPageAccessBind();
2562
+ machinesPageSeedHubBridgeBanners();
2563
+ } catch {
2564
+ // expected: a slice realm with no transport seeds nothing; the page always has fetch.
2565
+ }
2566
+ };
2567
+ if (String(document.readyState || '') === 'loading') {
2568
+ document.addEventListener('DOMContentLoaded', _machinesPageSeedBanners);
2569
+ } else {
2570
+ _machinesPageSeedBanners();
2571
+ }
2572
+ }