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
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WP-4 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-6/D-7/D-8, §6.4): the BOX side of "the
5
+ * session spawns with the hub's bundle".
6
+ *
7
+ * A hub-created session has a `sessions/<id>.json` record (WP-3) and the hub's bundle is materialized
8
+ * in the store (WP-2). A box with EXACTLY ONE hub pointer also applies that hub's bundle to a session
9
+ * with no record at all — the pre-fix hub-created sessions (and box-local creates on a one-hub box)
10
+ * cannot otherwise be told apart from a local create (rev 3 D-10). This module turns the store's
11
+ * resolution of that bundle into argv/env for ONE spawn. Nothing here is persisted: the `--add-dir`
12
+ * pair and CLAUDE_CODE_PLUGIN_DIRS live only in the argv/env handed to `_spawnSessionPty`, so a
13
+ * restore re-resolves the hub's CURRENT tree instead of replaying a stale path (D-7, AC-4c/AC-4d).
14
+ *
15
+ * Measured on the box (design §5 decision table, 2026-10-25):
16
+ * `claude --add-dir <treeDir>/claude` loads `<treeDir>/claude/.claude/skills/*` bare-named
17
+ * `CLAUDE_CODE_PLUGIN_DIRS=<treeDir>/plugins` loads every plugin folder under it (parent form)
18
+ * A missing `<treeDir>/plugins` is harmless to Claude (measured), so nothing here stats the
19
+ * filesystem — the transform stays PURE and unit-testable.
20
+ *
21
+ * D-8 (argv placement): `--add-dir` is VARIADIC — it swallows every following positional until the
22
+ * next option (tests/worker-launch-argv.test.js:206-208,231). Inserted before a positional prompt it
23
+ * would eat that prompt, so the pair goes immediately before the FIRST option token of the argv (and
24
+ * at the end when the argv has no option token at all).
25
+ */
26
+
27
+ const path = require('node:path');
28
+
29
+ /** Everything from the first `-`-prefixed token on is "option territory" as far as D-8 is concerned. */
30
+ function _isOptionToken(value) {
31
+ return typeof value === 'string' && value.startsWith('-');
32
+ }
33
+
34
+ /**
35
+ * D-8: `launchArgs` with `['--add-dir', dir]` inserted before its first option token (appended when
36
+ * there is none). Every original token keeps its relative order; a fresh array is always returned.
37
+ *
38
+ * @param {string[]} launchArgs
39
+ * @param {string} dir
40
+ * @returns {string[]}
41
+ */
42
+ function insertAddDir(launchArgs, dir) {
43
+ const args = Array.isArray(launchArgs) ? launchArgs : [];
44
+ const tokens = ['--add-dir', dir];
45
+ const at = args.findIndex(_isOptionToken);
46
+ return at === -1 ? [...args, ...tokens] : [...args.slice(0, at), ...tokens, ...args.slice(at)];
47
+ }
48
+
49
+ /**
50
+ * §6.4/D-8: the bundle's argv/env contribution for THIS spawn.
51
+ *
52
+ * PURE: the inputs are never mutated and the identity (`applied:false`, the caller's own objects) is
53
+ * returned for every case that is not a Claude spawn with a resolved bundle — a `null` bundle, and
54
+ * every non-Claude agent (Codex has no session-scoped skills lever; WP-6 installs instead, D-14).
55
+ *
56
+ * @param {{launchArgs?: string[], spawnEnv?: object, agentType?: string, bundle?: ?{treeDir: string}}} spawn
57
+ * @returns {{launchArgs: string[], spawnEnv: object, applied: boolean}}
58
+ */
59
+ function applyAgentBundleToSpawn({ launchArgs, spawnEnv, agentType, bundle }) {
60
+ if (agentType !== 'claude') return { launchArgs, spawnEnv, applied: false };
61
+ const treeDir = bundle && typeof bundle === 'object' ? String(bundle.treeDir == null ? '' : bundle.treeDir) : '';
62
+ if (!treeDir) return { launchArgs, spawnEnv, applied: false };
63
+ const env = spawnEnv && typeof spawnEnv === 'object' ? spawnEnv : {};
64
+ const existing = String(env.CLAUDE_CODE_PLUGIN_DIRS == null ? '' : env.CLAUDE_CODE_PLUGIN_DIRS).trim();
65
+ const pluginDirs = path.join(treeDir, 'plugins');
66
+ return {
67
+ launchArgs: insertAddDir(launchArgs, path.join(treeDir, 'claude')),
68
+ // Ours FIRST: the hub's plugins win a name collision against anything the box already had (D-8).
69
+ spawnEnv: { ...env, CLAUDE_CODE_PLUGIN_DIRS: existing ? `${pluginDirs}:${existing}` : pluginDirs },
70
+ applied: true,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * D-6/D-7: the bundle for `sessionId`, or null. A bundle can never fail, delay or change a spawn.
76
+ *
77
+ * `options.report(event, details)` is the caller's diagnostic seam (wired to `recordSessionDiagnostic`
78
+ * at the spawn site) and is called with exactly one of:
79
+ * `agent-bundle-resolve-failed` the store could not answer (REPORT, then spawn without the overlay)
80
+ * `agent-bundle-missing` this session WAS created by a hub, but no tree resolves for it —
81
+ * deleted/GC'd, or the hub has never pushed (AC-4e)
82
+ * A session with no hub record at all reports nothing: there was never a bundle to miss. On a
83
+ * one-hub box (rev 3 D-10) such a session still RESOLVES — `store.resolveSpawnBundle` falls back to
84
+ * the sole hub — so this path only stays silent when the box has zero or several hub pointers. When a
85
+ * sole hub exists but its tree is gone, `resolveSpawnBundle` returns null and `sessionHubRecord` is
86
+ * null too, so nothing is reported (acceptable: with no record there is no hub to name).
87
+ *
88
+ * @param {string} sessionId
89
+ * @param {{store?: object, report?: (event: string, details: object) => void, dataDir?: string}} [options]
90
+ * @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
91
+ */
92
+ async function resolveSpawnBundleForSession(sessionId, options = {}) {
93
+ const store = options.store || require('./store');
94
+ const report = typeof options.report === 'function' ? options.report : () => undefined;
95
+ // createProviderSession has no data dir in scope, so the canonical resolver is required lazily here
96
+ // rather than threaded through every caller.
97
+ const dataDir = options.dataDir || require('../data-dir').getDataDir();
98
+ let bundle = null;
99
+ try {
100
+ bundle = await store.resolveSpawnBundle(dataDir, sessionId);
101
+ } catch (error) {
102
+ // REPORT: an unreadable store is not a spawn failure — the session starts without the overlay.
103
+ report('agent-bundle-resolve-failed', { message: String((error && error.message) || error).slice(0, 200) });
104
+ return null;
105
+ }
106
+ if (bundle) return bundle;
107
+ let record = null;
108
+ try {
109
+ record = await store.sessionHubRecord(dataDir, sessionId);
110
+ } catch (error) {
111
+ // REPORT: bundle bookkeeping never blocks a launch (D-6/D-7).
112
+ report('agent-bundle-resolve-failed', { message: String((error && error.message) || error).slice(0, 200) });
113
+ return null;
114
+ }
115
+ if (record) report('agent-bundle-missing', { hubPeerId: record.hubPeerId });
116
+ return null;
117
+ }
118
+
119
+ module.exports = { applyAgentBundleToSpawn, insertAddDir, resolveSpawnBundleForSession };
@@ -0,0 +1,568 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WP-2 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-1/D-2/D-5, §6.3): the BOX side of the
5
+ * hub's agent-skill bundle — a content-addressed blob store plus a manifest materializer.
6
+ *
7
+ * Layout under `<dataDir>/agent-bundles/`:
8
+ * blobs/<hh>/<hex> content store, 0444, hh = first two hex chars
9
+ * trees/<treeHashHex>/{claude,plugins,codex}/… materialized read-only view, files 0444/0555,
10
+ * dirs 0555 (the entry paths already carry the
11
+ * `claude/`, `plugins/`, `codex/` prefixes)
12
+ * trees/<treeHashHex>.manifest.json WP-5: the manifest that tree was built from, the
13
+ * GC's blob-reference source (never a tree itself)
14
+ * hubs/<peerInstanceId>.json { treeHash, at } — the hub's CURRENT pointer
15
+ * sessions/<ctmSessionId>.json { hubPeerId, createdTreeHash, at }
16
+ *
17
+ * D-1: nothing is ever written into the box user's `~/.claude`; a bundle is a session overlay that
18
+ * lives only here. D-2: a bundle is addressed by `treeHash = computeTreeHash(entries)` (WP-1's
19
+ * builder), so identical content yields an identical directory on every hub. D-5: the box verifies
20
+ * what it is given — every blob is re-hashed on receipt, the manifest is accepted only when the
21
+ * recomputed tree hash matches the URL and every referenced blob is present at the declared size,
22
+ * and materialization is temp-dir + rename so a partially written tree is never visible.
23
+ *
24
+ * Identity rules (the reason `AgentBundleError` exists): `hex` must be 64 lowercase hex chars and
25
+ * `treeHash` the `sha256:<64 hex>` form (URL-encoded or raw — the URL carries it), so neither can
26
+ * ever name a file outside this store. `peerInstanceId` / `ctmSessionId` become file names and are
27
+ * held to `/^[A-Za-z0-9._-]{1,128}$/`.
28
+ */
29
+
30
+ const crypto = require('crypto');
31
+ const fsp = require('fs/promises');
32
+ const path = require('path');
33
+
34
+ const { computeTreeHash } = require('./build');
35
+ const { atomicWriteFile } = require('../../atomic-write');
36
+
37
+ const HEX_RE = /^[0-9a-f]{64}$/;
38
+ const TREE_HASH_RE = /^sha256:([0-9a-f]{64})$/;
39
+ /** The id shape shared by peerInstanceId / ctmSessionId — they are file names, nothing more. */
40
+ const ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
41
+ /** = build.js's MAX_ENTRIES: the hub cannot produce more, so the box will not accept more. */
42
+ const MAX_ENTRIES = 20000;
43
+ /** D-5: files are one of exactly two modes; dirs are always 0555. */
44
+ const FILE_MODES = new Set([0o444, 0o555]);
45
+ const DIR_MODE = 0o555;
46
+ /** D-5 / WP-1: every entry path starts with one of these three roots. */
47
+ const PATH_PREFIXES = ['claude/.claude/skills/', 'plugins/', 'codex/'];
48
+
49
+ class AgentBundleError extends Error {
50
+ constructor(code, status, details = {}) {
51
+ super(code);
52
+ this.name = 'AgentBundleError';
53
+ this.code = code;
54
+ this.status = status;
55
+ if (Array.isArray(details.missing)) this.missing = details.missing;
56
+ }
57
+ }
58
+
59
+ function storeRoot(dataDir) {
60
+ return path.join(String(dataDir == null ? '' : dataDir), 'agent-bundles');
61
+ }
62
+
63
+ function _invalid() {
64
+ return new AgentBundleError('agent_bundle_path_invalid', 422);
65
+ }
66
+
67
+ /** A blob hex, or `agent_bundle_path_invalid`. Never normalizes case: the name IS the hash. */
68
+ function _requireHex(value) {
69
+ const hex = String(value == null ? '' : value).trim();
70
+ if (!HEX_RE.test(hex)) throw _invalid();
71
+ return hex;
72
+ }
73
+
74
+ /** `sha256:<64 hex>`, accepted URL-encoded (`sha256%3A…`) or raw, or `agent_bundle_path_invalid`. */
75
+ function _requireTreeHash(value) {
76
+ const raw = String(value == null ? '' : value).trim();
77
+ let decoded = raw;
78
+ if (raw.includes('%')) {
79
+ try {
80
+ decoded = decodeURIComponent(raw);
81
+ } catch {
82
+ // expected: a malformed percent-escape is just an invalid tree hash, not a server fault
83
+ throw _invalid();
84
+ }
85
+ }
86
+ const match = TREE_HASH_RE.exec(decoded);
87
+ if (!match) throw _invalid();
88
+ return { treeHash: `sha256:${match[1]}`, hex: match[1] };
89
+ }
90
+
91
+ function _requireId(value) {
92
+ const id = String(value == null ? '' : value).trim();
93
+ if (!ID_RE.test(id)) throw _invalid();
94
+ return id;
95
+ }
96
+
97
+ function _blobPath(root, hex) {
98
+ return path.join(root, 'blobs', hex.slice(0, 2), hex);
99
+ }
100
+
101
+ async function _statOrNull(target) {
102
+ try {
103
+ return await fsp.stat(target);
104
+ } catch (error) {
105
+ if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ async function _exists(target) {
111
+ return (await _statOrNull(target)) !== null;
112
+ }
113
+
114
+ async function _unlinkQuiet(target) {
115
+ try {
116
+ await fsp.unlink(target);
117
+ } catch {
118
+ /* expected: the temp file was never created, or the rename already consumed it */
119
+ }
120
+ }
121
+
122
+ async function _readJsonOrNull(target) {
123
+ let text;
124
+ try {
125
+ text = await fsp.readFile(target, 'utf8');
126
+ } catch (error) {
127
+ if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
128
+ throw error;
129
+ }
130
+ try {
131
+ const parsed = JSON.parse(text);
132
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
133
+ } catch {
134
+ // expected: a torn/garbled pointer is a missing piece of information, not a reason to fail the
135
+ // spawn — D-7 keeps bundle work unable to block or fail a session.
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /** D-5: a relative POSIX path under one of the three bundle roots, with no way to escape the tree. */
141
+ function _validateEntryPath(value) {
142
+ if (typeof value !== 'string' || value.length === 0) throw _invalid();
143
+ if (value.startsWith('/')) throw _invalid();
144
+ if (value.includes('\\')) throw _invalid();
145
+ if (/[\u0000-\u001f]/.test(value)) throw _invalid();
146
+ const segments = value.split('/');
147
+ for (const segment of segments) {
148
+ if (segment === '' || segment === '.' || segment === '..') throw _invalid();
149
+ }
150
+ if (!PATH_PREFIXES.some((prefix) => value.startsWith(prefix))) throw _invalid();
151
+ return value;
152
+ }
153
+
154
+ function _normalizeEntries(manifest, canonicalTreeHash) {
155
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw _invalid();
156
+ if (manifest.schema !== 1) throw _invalid();
157
+ if (!Array.isArray(manifest.entries)) throw _invalid();
158
+ if (manifest.entries.length > MAX_ENTRIES) throw _invalid();
159
+ const seen = new Set();
160
+ const entries = [];
161
+ for (const raw of manifest.entries) {
162
+ if (!raw || typeof raw !== 'object') throw _invalid();
163
+ const rel = _validateEntryPath(raw.path);
164
+ if (seen.has(rel)) throw _invalid();
165
+ seen.add(rel);
166
+ if (!FILE_MODES.has(raw.mode)) throw _invalid();
167
+ if (!Number.isInteger(raw.size) || raw.size < 0) throw _invalid();
168
+ const sha256 = String(raw.sha256 == null ? '' : raw.sha256);
169
+ if (!HEX_RE.test(sha256)) throw _invalid();
170
+ entries.push({ path: rel, mode: raw.mode, sha256, size: raw.size });
171
+ }
172
+ if (computeTreeHash(entries) !== canonicalTreeHash) {
173
+ throw new AgentBundleError('agent_bundle_tree_hash_mismatch', 422);
174
+ }
175
+ return entries;
176
+ }
177
+
178
+ /** Every directory in a freshly written tree, the root included (breadth-first, unordered). */
179
+ async function _collectDirs(rootDir) {
180
+ const dirs = [rootDir];
181
+ for (let index = 0; index < dirs.length; index += 1) {
182
+ const dirents = await fsp.readdir(dirs[index], { withFileTypes: true });
183
+ for (const dirent of dirents) {
184
+ if (dirent.isDirectory()) dirs.push(path.join(dirs[index], dirent.name));
185
+ }
186
+ }
187
+ return dirs;
188
+ }
189
+
190
+ /** Recursive remove that first drops read-only dirs back to writable (trees are 0555). */
191
+ async function _removeTree(target) {
192
+ const stack = [target];
193
+ while (stack.length) {
194
+ const dir = stack.pop();
195
+ let dirents;
196
+ try {
197
+ dirents = await fsp.readdir(dir, { withFileTypes: true });
198
+ } catch (error) {
199
+ if (error && error.code === 'ENOENT') continue;
200
+ throw error;
201
+ }
202
+ try {
203
+ await fsp.chmod(dir, 0o700);
204
+ } catch (error) {
205
+ if (!error || error.code !== 'ENOENT') throw error;
206
+ /* expected: the dir vanished between readdir and chmod — the removal below is already done */
207
+ }
208
+ for (const dirent of dirents) {
209
+ if (dirent.isDirectory()) stack.push(path.join(dir, dirent.name));
210
+ }
211
+ }
212
+ await fsp.rm(target, { recursive: true, force: true });
213
+ }
214
+
215
+ /**
216
+ * §6.3: build `trees/<hex>` from the blobs. Copies (not hardlinks) so the 0444 inode is never a
217
+ * shared, mutable-looking object. The lock order is everything BELOW the temp root (0555,
218
+ * deepest-first) → `rename(tmp, treeDir)` → the published root itself (0555); see
219
+ * docs/agent-bundle-darwin-readonly-rename-2026-10-27.html D-1: Darwin's rename(2) needs write on
220
+ * `old/..`, so a 0555 temp ROOT makes the rename itself fail with EACCES on macOS ≤15, while the
221
+ * root stays owner-writable across the rename and is locked only once it is in its final place.
222
+ * A rename race (another request materialized the same tree first) is not an error: `materialized:false`.
223
+ */
224
+ async function _materialize(root, hex, treeDir, entries) {
225
+ const tmp = path.join(root, 'trees', `.tmp-${hex}-${crypto.randomBytes(6).toString('hex')}`);
226
+ try {
227
+ await fsp.mkdir(tmp, { recursive: true });
228
+ for (const entry of entries) {
229
+ const target = path.join(tmp, ...entry.path.split('/'));
230
+ await fsp.mkdir(path.dirname(target), { recursive: true });
231
+ await fsp.copyFile(_blobPath(root, entry.sha256), target);
232
+ await fsp.chmod(target, entry.mode);
233
+ }
234
+ // D-5: EVERY directory in the tree is 0555, not just the entry folders — mkdir's defaults are
235
+ // whatever the umask says, and a writable intermediate would let a later spawn write into the
236
+ // store. Deepest-first so a parent is never locked while a child still needs creating.
237
+ // The temp ROOT is excluded (D-1, docs/agent-bundle-darwin-readonly-rename-2026-10-27.html): the
238
+ // rename below needs write on `old/..`, so the root is locked only after it is published.
239
+ const dirs = (await _collectDirs(tmp)).filter((dir) => dir !== tmp);
240
+ dirs.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
241
+ for (const dir of dirs) await fsp.chmod(dir, DIR_MODE);
242
+ await fsp.rename(tmp, treeDir); // D-1: the root is still owner-writable here
243
+ await fsp.chmod(treeDir, DIR_MODE); // D-1: a throw here propagates; D-3 re-locks on the next PUT
244
+ return true;
245
+ } catch (error) {
246
+ await _removeTree(tmp);
247
+ if (error && (error.code === 'EEXIST' || error.code === 'ENOTEMPTY')) return false;
248
+ throw error; // RETHROW: a failed materialization must not be reported as "already present"
249
+ }
250
+ }
251
+
252
+ async function _writeHubPointer(root, peerInstanceId, treeHash) {
253
+ const dir = path.join(root, 'hubs');
254
+ await fsp.mkdir(dir, { recursive: true });
255
+ await atomicWriteFile(
256
+ path.join(dir, `${peerInstanceId}.json`),
257
+ JSON.stringify({ treeHash, at: Date.now() }),
258
+ );
259
+ }
260
+
261
+ /** §6.3: the hexes from `hashes` that this box does not hold, in input order, deduped. */
262
+ async function missingBlobs(dataDir, hashes) {
263
+ const root = storeRoot(dataDir);
264
+ const unique = [];
265
+ const seen = new Set();
266
+ for (const value of Array.isArray(hashes) ? hashes : []) {
267
+ const hex = _requireHex(value);
268
+ if (seen.has(hex)) continue;
269
+ seen.add(hex);
270
+ unique.push(hex);
271
+ }
272
+ const missing = [];
273
+ for (const hex of unique) {
274
+ if (!(await _exists(_blobPath(root, hex)))) missing.push(hex);
275
+ }
276
+ return missing;
277
+ }
278
+
279
+ // gc.js's blob sweep rmdirs every shard it leaves empty, and it runs unawaited after a materializing
280
+ // manifest PUT -- so a shard this write just created can vanish before the temp file is opened in it.
281
+ // The writer owns recreating its directory (git's object writer does the same): mkdir and retry the
282
+ // open on ENOENT. The recursive mkdir itself can throw ENOENT too -- it answers an existing shard with
283
+ // EEXIST and then stats it, and the sweep can remove it in between. Once the temp file exists the
284
+ // shard is non-empty and the sweep's rmdir cannot win.
285
+ const SHARD_OPEN_ATTEMPTS = 3;
286
+
287
+ async function _openInShard(dir, tmp) {
288
+ for (let attempt = 1; ; attempt += 1) {
289
+ try {
290
+ await fsp.mkdir(dir, { recursive: true });
291
+ return await fsp.open(tmp, 'w', 0o600);
292
+ } catch (error) {
293
+ if (!error || error.code !== 'ENOENT' || attempt >= SHARD_OPEN_ATTEMPTS) throw error; // RETHROW
294
+ /* expected: a GC sweep removed the empty shard during mkdir or before open -- recreate it */
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * §6.2/AC-2a: store one blob. The hash is recomputed FIRST — a wrong-hash body stores nothing — and
301
+ * an existing blob is left untouched (`{ stored: false }`), which is what makes a re-push cheap.
302
+ */
303
+ async function putBlob(dataDir, hex, buffer) {
304
+ const clean = _requireHex(hex);
305
+ const data = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer == null ? '' : buffer);
306
+ if (crypto.createHash('sha256').update(data).digest('hex') !== clean) {
307
+ throw new AgentBundleError('agent_bundle_blob_hash_mismatch', 422);
308
+ }
309
+ const root = storeRoot(dataDir);
310
+ const target = _blobPath(root, clean);
311
+ if (await _exists(target)) return { stored: false };
312
+ const dir = path.dirname(target);
313
+ const tmp = path.join(dir, `.${clean}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
314
+ try {
315
+ const handle = await _openInShard(dir, tmp);
316
+ try {
317
+ await handle.writeFile(data);
318
+ await handle.sync();
319
+ } finally {
320
+ await handle.close();
321
+ }
322
+ await fsp.chmod(tmp, 0o444);
323
+ await fsp.rename(tmp, target);
324
+ } catch (error) {
325
+ await _unlinkQuiet(tmp);
326
+ throw error; // RETHROW: a failed write must not look like a stored blob
327
+ }
328
+ return { stored: true };
329
+ }
330
+
331
+ /**
332
+ * §6.2/D-5: accept a manifest and materialize its tree, then move this hub's pointer.
333
+ *
334
+ * The peer identity is required BEFORE any write: a box keys bundles by the AUTHENTICATED hub
335
+ * (`req.ctmAuth.peerInstanceId`), never by a field in the body, so an unidentified caller cannot
336
+ * create state on this machine.
337
+ *
338
+ * @returns {Promise<{treeHash: string, materialized: boolean}>}
339
+ */
340
+ async function putManifest(dataDir, treeHash, manifest, options = {}) {
341
+ const peerRaw = String(options.peerInstanceId == null ? '' : options.peerInstanceId).trim();
342
+ if (!peerRaw) throw new AgentBundleError('agent_bundle_peer_unidentified', 403);
343
+ const peerInstanceId = _requireId(peerRaw);
344
+ const canonical = _requireTreeHash(treeHash);
345
+ const root = storeRoot(dataDir);
346
+ const entries = _normalizeEntries(manifest, canonical.treeHash);
347
+
348
+ const missing = new Set();
349
+ for (const entry of entries) {
350
+ if (!(await _exists(_blobPath(root, entry.sha256)))) missing.add(entry.sha256);
351
+ }
352
+ if (missing.size) {
353
+ throw new AgentBundleError('agent_bundle_blobs_missing', 409, { missing: [...missing].sort() });
354
+ }
355
+ // D-5: presence is not enough — the bytes on disk must be the size the manifest declares.
356
+ for (const entry of entries) {
357
+ const stat = await _statOrNull(_blobPath(root, entry.sha256));
358
+ if (stat && stat.size !== entry.size) {
359
+ throw new AgentBundleError('agent_bundle_tree_hash_mismatch', 422);
360
+ }
361
+ }
362
+
363
+ await fsp.mkdir(path.join(root, 'trees'), { recursive: true });
364
+ const treeDir = path.join(root, 'trees', canonical.hex);
365
+ const existing = await _statOrNull(treeDir);
366
+ const materialized = existing && existing.isDirectory()
367
+ // D-3 (docs/agent-bundle-darwin-readonly-rename-2026-10-27.html): re-lock a root a crash between
368
+ // D-1's rename and its chmod left writable — idempotent, and a throw here is a real 500.
369
+ ? (await fsp.chmod(treeDir, DIR_MODE), false)
370
+ : await _materialize(root, canonical.hex, treeDir, entries);
371
+
372
+ // WP-5: GC needs to know which blobs a tree still needs, so the manifest is copied next to the
373
+ // tree it names (the tree directory is the durable truth; nothing else remembers it). Written
374
+ // when the tree is created AND when it already exists without a copy — a tree materialized before
375
+ // this WP picks one up on the hub's next push of the same content.
376
+ const manifestCopy = path.join(root, 'trees', `${canonical.hex}.manifest.json`);
377
+ if (!(await _exists(manifestCopy))) {
378
+ await atomicWriteFile(manifestCopy, JSON.stringify({ schema: 1, treeHash: canonical.treeHash, entries }));
379
+ }
380
+
381
+ // AC-2e: the pointer moves even when the tree was already there — it names the hub's CURRENT
382
+ // bundle, not the one that happened to be materialized first.
383
+ await _writeHubPointer(root, peerInstanceId, canonical.treeHash);
384
+ return { treeHash: canonical.treeHash, materialized };
385
+ }
386
+
387
+ /** The hub pointer file this store keys by peer id — one read site for both callers below. */
388
+ function _hubPointerPath(root, peerId) {
389
+ return path.join(root, 'hubs', `${peerId}.json`);
390
+ }
391
+
392
+ /**
393
+ * WP-3: the hub's CURRENT tree pointer for `peerInstanceId`, or null when that hub never pushed
394
+ * (or its pointer is unreadable). Used at create-record time so `sessions/<id>.json` carries the
395
+ * tree that was current then; `resolveSpawnBundle` re-reads the pointer at spawn regardless, so a
396
+ * null here costs nothing.
397
+ *
398
+ * @returns {Promise<string|null>} `sha256:<64 hex>` or null
399
+ */
400
+ async function currentHubTree(dataDir, peerInstanceId) {
401
+ const peer = String(peerInstanceId == null ? '' : peerInstanceId).trim();
402
+ if (!ID_RE.test(peer)) return null;
403
+ const pointer = await _readJsonOrNull(_hubPointerPath(storeRoot(dataDir), peer));
404
+ if (!pointer) return null;
405
+ try {
406
+ return _requireTreeHash(pointer.treeHash).treeHash;
407
+ } catch (error) {
408
+ if (error instanceof AgentBundleError) return null;
409
+ throw error; // RETHROW: an unexpected read fault must not be reported as "no pointer"
410
+ }
411
+ }
412
+
413
+ /**
414
+ * §6.3: remember which hub started this session, and the tree hash as of that create.
415
+ *
416
+ * WP-3: `treeHash` may be NULL — that is the honest record for a hub that had not pushed yet at
417
+ * create time (D-7 reads the hub's CURRENT pointer at spawn, so the create-time value is
418
+ * bookkeeping). A non-null value is still validated into the canonical `sha256:<hex>` form.
419
+ */
420
+ async function recordSessionHub(dataDir, ctmSessionId, hubPeerId, treeHash) {
421
+ const sessionId = _requireId(ctmSessionId);
422
+ const hubPeerIdClean = _requireId(hubPeerId);
423
+ const canonical = treeHash === null || treeHash === undefined || String(treeHash).trim() === ''
424
+ ? null
425
+ : _requireTreeHash(treeHash).treeHash;
426
+ const dir = path.join(storeRoot(dataDir), 'sessions');
427
+ await fsp.mkdir(dir, { recursive: true });
428
+ await atomicWriteFile(
429
+ path.join(dir, `${sessionId}.json`),
430
+ JSON.stringify({ hubPeerId: hubPeerIdClean, createdTreeHash: canonical, at: Date.now() }),
431
+ );
432
+ }
433
+
434
+ /**
435
+ * The pointer -> materialized-tree tail shared by both `resolveSpawnBundle` paths: read
436
+ * `hubs/<hubPeerId>.json`, canonicalize its tree hash, stat `trees/<hex>`. Returns null for every
437
+ * missing/unusable piece (no pointer, a torn pointer, a malformed hash, no tree directory) and
438
+ * rethrows an unexpected read fault. Factored out so the record path and the sole-hub path cannot
439
+ * drift apart (WP-5 step 4).
440
+ *
441
+ * @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
442
+ */
443
+ async function _bundleForHub(root, hubPeerId) {
444
+ const hub = await _readJsonOrNull(_hubPointerPath(root, hubPeerId));
445
+ if (!hub) return null;
446
+ let hex;
447
+ try {
448
+ hex = _requireTreeHash(hub.treeHash).hex;
449
+ } catch (error) {
450
+ if (error instanceof AgentBundleError) return null;
451
+ throw error; // RETHROW: an unexpected read fault must not be reported as "no bundle"
452
+ }
453
+ const treeDir = path.join(root, 'trees', hex);
454
+ const stat = await _statOrNull(treeDir);
455
+ if (!stat || !stat.isDirectory()) return null;
456
+ return { treeDir, treeHash: `sha256:${hex}`, hubPeerId };
457
+ }
458
+
459
+ /**
460
+ * rev 3 D-10: the peer id of the box's ONLY hub, or null. A box with exactly one `hubs/<peer>.json`
461
+ * pointer has exactly one hub, so a session with no `sessions/<id>.json` record (created before the
462
+ * hub sent its peer id, or by a box-local path) is attributed to that hub. Several pointers (or none)
463
+ * stay null — the box cannot guess.
464
+ *
465
+ * Only `<peer>.json` names whose basename is a valid id count; temp files (`hubs/.tmp-*`) and anything
466
+ * else are ignored. ENOENT (no `hubs/` dir at all) is the honest "no hub" -> null; any other read
467
+ * fault is RETHROWN for the caller to REPORT (spawn.js turns it into `agent-bundle-resolve-failed`).
468
+ *
469
+ * @returns {Promise<string|null>}
470
+ */
471
+ async function _soleHubPeerId(root) {
472
+ let names;
473
+ try {
474
+ names = await fsp.readdir(path.join(root, 'hubs'));
475
+ } catch (error) {
476
+ if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
477
+ throw error; // RETHROW: a store we cannot read is REPORTED, never silently "no hub"
478
+ }
479
+ const peers = [];
480
+ for (const name of names) {
481
+ if (!name.endsWith('.json')) continue;
482
+ const peer = name.slice(0, -'.json'.length);
483
+ if (!ID_RE.test(peer)) continue;
484
+ peers.push(peer);
485
+ }
486
+ return peers.length === 1 ? peers[0] : null;
487
+ }
488
+
489
+ /**
490
+ * §6.3: the bundle this session should spawn with, read through the hub's CURRENT pointer so a skill
491
+ * edit on the hub applies at the next spawn (D-7's "session overlay", not a snapshot).
492
+ *
493
+ * rev 3 D-10: a `sessions/<id>.json` record still wins when present — resolution follows THAT record's
494
+ * hub pointer only, with no fallback even when the pointer is gone or its tree was GC'd. With no
495
+ * record, the box applies its single hub's bundle when it has exactly one hub pointer
496
+ * (`_soleHubPeerId`); zero or several pointers stay null, so the pre-fix hub-created sessions on a
497
+ * one-hub box (`hubs/ctm-…-3456-1a2c9c.json`) get the hub's skills where nothing else could tell them
498
+ * apart from box-local creates (docs/remote-skills-hub-peer-identity-2026-10-26.html rev 3 D-10).
499
+ *
500
+ * @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
501
+ */
502
+ async function resolveSpawnBundle(dataDir, ctmSessionId) {
503
+ const sessionId = String(ctmSessionId == null ? '' : ctmSessionId).trim();
504
+ if (!ID_RE.test(sessionId)) return null;
505
+ const root = storeRoot(dataDir);
506
+ const session = await _readJsonOrNull(path.join(root, 'sessions', `${sessionId}.json`));
507
+ if (session) {
508
+ const hubPeerId = String(session.hubPeerId == null ? '' : session.hubPeerId).trim();
509
+ if (!ID_RE.test(hubPeerId)) return null;
510
+ // The record wins: no fallback to a sole hub, so 'sess-orphan' / 'sess-gc' keep resolving to null.
511
+ return _bundleForHub(root, hubPeerId);
512
+ }
513
+ // No record (D-10): the box's only hub, if it has exactly one.
514
+ const sole = await _soleHubPeerId(root);
515
+ if (!sole) return null;
516
+ return _bundleForHub(root, sole);
517
+ }
518
+
519
+ /**
520
+ * WP-4 (§6.4, AC-4e): the `sessions/<id>.json` record itself — "was this session created by a hub at
521
+ * all, and which one" — without following the hub's pointer.
522
+ *
523
+ * `resolveSpawnBundle` answers "what should this spawn load" (null covers both "no hub" and "no
524
+ * tree"); this answers "was there ever a hub", which is what tells a tree that vanished (report
525
+ * `agent-bundle-missing`) from a local create (report nothing).
526
+ *
527
+ * ENOENT → null (the honest "no record"); any other read fault is RETHROWN for the caller to REPORT —
528
+ * a bundle read never decides whether a session spawns (D-6/D-7).
529
+ *
530
+ * @returns {Promise<null | {hubPeerId: string, createdTreeHash: string|null}>}
531
+ */
532
+ async function sessionHubRecord(dataDir, ctmSessionId) {
533
+ const sessionId = String(ctmSessionId == null ? '' : ctmSessionId).trim();
534
+ if (!ID_RE.test(sessionId)) return null;
535
+ const record = await _readJsonOrNull(path.join(storeRoot(dataDir), 'sessions', `${sessionId}.json`));
536
+ if (!record) return null;
537
+ const hubPeerId = String(record.hubPeerId == null ? '' : record.hubPeerId).trim();
538
+ if (!ID_RE.test(hubPeerId)) return null;
539
+ let createdTreeHash = null;
540
+ if (record.createdTreeHash !== null && record.createdTreeHash !== undefined) {
541
+ try {
542
+ createdTreeHash = _requireTreeHash(record.createdTreeHash).treeHash;
543
+ } catch (error) {
544
+ if (!(error instanceof AgentBundleError)) throw error; // RETHROW: a real read fault is not "no tree"
545
+ createdTreeHash = null;
546
+ }
547
+ }
548
+ return { hubPeerId, createdTreeHash };
549
+ }
550
+
551
+ module.exports = {
552
+ storeRoot,
553
+ missingBlobs,
554
+ putBlob,
555
+ putManifest,
556
+ recordSessionHub,
557
+ // WP-3: the hub's current pointer, read at create-record time (a null record is fine — the
558
+ // spawn-time resolver re-reads the pointer itself).
559
+ currentHubTree,
560
+ resolveSpawnBundle,
561
+ // WP-4: the raw session record, for the "hub-created but no tree" diagnostic (AC-4e).
562
+ sessionHubRecord,
563
+ // WP-5: the read-only-tree remover, exported so lib/agent-bundle/gc.js removes a tree the same
564
+ // way materialization's own failure path does (chmod 0700 down the tree, then rm -r) instead of
565
+ // growing a second copy of that chmod dance.
566
+ removeTree: _removeTree,
567
+ AgentBundleError,
568
+ };