create-walle 0.10.0 → 0.10.2

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 (381) hide show
  1. package/README.md +88 -41
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +7 -5
  6. package/package.json +10 -4
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
  11. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  12. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  13. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  14. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  16. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  17. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  18. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  21. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  22. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  23. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  24. package/template/coding-task-manager/db.js +705 -286
  25. package/template/coding-task-manager/git-utils.js +44 -0
  26. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  27. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  28. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  29. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  30. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  31. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  32. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  33. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  34. package/template/coding-task-manager/lib/api/machines.js +461 -0
  35. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  36. package/template/coding-task-manager/lib/api/providers.js +53 -6
  37. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  38. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  39. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  40. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  41. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  42. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  43. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  44. package/template/coding-task-manager/lib/auth-context.js +32 -2
  45. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  46. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  47. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  48. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  49. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  50. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  51. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  52. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  53. package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
  54. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  55. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  56. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  57. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  58. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  59. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  60. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  61. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  62. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  63. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  64. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  65. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  66. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  67. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  68. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  69. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  70. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  71. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  72. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  73. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  74. package/template/coding-task-manager/lib/db-domains.js +21 -2
  75. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  76. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  77. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  78. package/template/coding-task-manager/lib/feature-manifest.js +1 -3
  79. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  80. package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
  81. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  82. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  83. package/template/coding-task-manager/lib/headless-term-service.js +60 -23
  84. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  85. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  86. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  87. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  88. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  89. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  90. package/template/coding-task-manager/lib/host-stats.js +328 -0
  91. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  92. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  93. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  94. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  95. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  96. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  97. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  98. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  99. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  100. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  101. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  102. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  103. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  104. package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
  105. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  106. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  107. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  108. package/template/coding-task-manager/lib/login-tripwire.js +18 -0
  109. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  110. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  111. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  112. package/template/coding-task-manager/lib/machine-self.js +66 -0
  113. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
  114. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  115. package/template/coding-task-manager/lib/media-journal.js +84 -27
  116. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  117. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  118. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  119. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  120. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  121. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  122. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  123. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  124. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  125. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  126. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  127. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  128. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  129. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  130. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  131. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  132. package/template/coding-task-manager/lib/native-history-control.js +62 -14
  133. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  134. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  135. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  136. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  137. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  138. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  139. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  140. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  141. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  142. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  143. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  144. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  145. package/template/coding-task-manager/lib/read-pool-client.js +36 -24
  146. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  147. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  148. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  149. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  150. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  151. package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
  152. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  153. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  154. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  155. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  156. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  157. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  158. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  159. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  160. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  161. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  162. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  163. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  164. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  165. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  166. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  167. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  168. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  169. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  170. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  171. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  172. package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
  173. package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
  174. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  175. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  176. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  177. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  178. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  179. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  180. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  181. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  182. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  183. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  184. package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
  185. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  186. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  187. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  188. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  189. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  190. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  191. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  192. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  193. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  194. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  195. package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
  196. package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
  197. package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
  198. package/template/coding-task-manager/lib/session-standup.js +67 -4
  199. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  200. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  201. package/template/coding-task-manager/lib/session-status.js +2 -2
  202. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  203. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  204. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  205. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  206. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  207. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  208. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  209. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  210. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  211. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  212. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  213. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  214. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  215. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  216. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  217. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  218. package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
  219. package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
  220. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  221. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  222. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  223. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  224. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  225. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  226. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  227. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  228. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  229. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  230. package/template/coding-task-manager/lib/ws/input.js +1 -0
  231. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  232. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  233. package/template/coding-task-manager/lib/ws-router.js +68 -1
  234. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  235. package/template/coding-task-manager/oauth-proxy.js +99 -55
  236. package/template/coding-task-manager/package-lock.json +24 -0
  237. package/template/coding-task-manager/package.json +3 -0
  238. package/template/coding-task-manager/protocol/message-table.json +0 -10
  239. package/template/coding-task-manager/protocol/messages.js +0 -10
  240. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  241. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  242. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  243. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  244. package/template/coding-task-manager/public/css/app.css +279 -2
  245. package/template/coding-task-manager/public/css/reviews.css +4 -0
  246. package/template/coding-task-manager/public/index.html +297 -240
  247. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  248. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  249. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  250. package/template/coding-task-manager/public/js/background-work.js +13 -7
  251. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  252. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  253. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  254. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  255. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  256. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  257. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  258. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  259. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  260. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  261. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  262. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  263. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
  264. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  265. package/template/coding-task-manager/public/js/protocol.js +0 -10
  266. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  267. package/template/coding-task-manager/public/js/reviews.js +7 -3
  268. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  269. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  270. package/template/coding-task-manager/public/js/row-join.js +18 -3
  271. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  272. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  273. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  274. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  275. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  276. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  277. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  278. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  279. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  280. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  281. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  282. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  283. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  284. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  285. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
  286. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  287. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  288. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  289. package/template/coding-task-manager/public/m/app.css +226 -8
  290. package/template/coding-task-manager/public/m/app.js +502 -9
  291. package/template/coding-task-manager/public/m/index.html +38 -1
  292. package/template/coding-task-manager/public/m/machines.js +1313 -0
  293. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  294. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  295. package/template/coding-task-manager/public/m/sw.js +5 -0
  296. package/template/coding-task-manager/queue-engine.js +13 -0
  297. package/template/coding-task-manager/server/approvals.js +5 -6
  298. package/template/coding-task-manager/server/boot.js +136 -17
  299. package/template/coding-task-manager/server.js +964 -425
  300. package/template/coding-task-manager/session-integrity.js +7 -9
  301. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  302. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  303. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  304. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  305. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  306. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  307. package/template/package-lock.json +2 -2
  308. package/template/package.json +1 -1
  309. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  310. package/template/shared/conversation-storage-v2-contract.js +42 -6
  311. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  312. package/template/shared/git-remote-key.js +83 -0
  313. package/template/shared/host-identity.js +98 -2
  314. package/template/shared/owner-write-safety.js +135 -0
  315. package/template/shared/public-runtime.js +13 -5
  316. package/template/shared/release-schema.json +3 -2
  317. package/template/shared/report-citations.js +14 -0
  318. package/template/shared/sqlite-write-lock.js +361 -223
  319. package/template/shared/storage/contract.js +35 -3
  320. package/template/wall-e/agent.js +21 -0
  321. package/template/wall-e/api-walle.js +5 -0
  322. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  323. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  324. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  325. package/template/wall-e/brain/migrations.js +51 -22
  326. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  327. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  328. package/template/wall-e/brain/schema-ensures.js +22 -0
  329. package/template/wall-e/brain.js +1 -1
  330. package/template/wall-e/coding/host-capabilities.js +2 -2
  331. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  332. package/template/wall-e/coding/session-journal.js +28 -40
  333. package/template/wall-e/coding-orchestrator.js +23 -26
  334. package/template/wall-e/fly.toml +1 -1
  335. package/template/wall-e/http/auth.js +18 -1
  336. package/template/wall-e/http/blog-views-api.js +318 -0
  337. package/template/wall-e/http/helper-rules-api.js +321 -0
  338. package/template/wall-e/http/model-proxy-providers.js +3 -1
  339. package/template/wall-e/http/telemetry-api.js +12 -0
  340. package/template/wall-e/lib/blog-view-policy.js +88 -0
  341. package/template/wall-e/lib/blog-view-store.js +169 -0
  342. package/template/wall-e/lib/client-config-lock.js +2 -1
  343. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  344. package/template/wall-e/lib/error-kinds.js +10 -1
  345. package/template/wall-e/lib/feature-manifest.js +1 -1
  346. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  347. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  348. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  349. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  350. package/template/wall-e/lib/helper-rules/label.js +247 -0
  351. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  352. package/template/wall-e/lib/helper-rules/store.js +667 -0
  353. package/template/wall-e/lib/mcp-integration.js +5 -6
  354. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  355. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  356. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  357. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  358. package/template/wall-e/llm/default-fallback.js +4 -0
  359. package/template/wall-e/llm/model-catalog.js +13 -4
  360. package/template/wall-e/llm/model-roles.js +6 -0
  361. package/template/wall-e/llm/portkey-http.js +150 -0
  362. package/template/wall-e/llm/portkey-sync.js +15 -145
  363. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  364. package/template/wall-e/llm/worker-route.js +43 -1
  365. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  366. package/template/wall-e/mcp-protocol-contract.js +41 -1
  367. package/template/wall-e/mcp-server.js +13 -2
  368. package/template/wall-e/mcp-worker-schemas.js +20 -2
  369. package/template/wall-e/memory/ctm-context-client.js +25 -5
  370. package/template/wall-e/shared/release-schema.json +3 -2
  371. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  372. package/template/wall-e/shared/storage/contract.js +35 -3
  373. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  374. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  375. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  376. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  377. package/template/wall-e/tools/local-tools.js +12 -5
  378. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  379. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  380. package/template/wall-e/workers/telemetry-worker.js +8 -0
  381. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -160,6 +160,13 @@
160
160
  itemElements: [],
161
161
  },
162
162
  projects: [],
163
+ // WP-5 (S-16/AC-5c): the machine registry, mirrored field for field from the desktop client —
164
+ // same GET /api/machines rows, same reachable/unknown/unhealthy card states, same chip facts.
165
+ // Empty until the hub answers; with zero machines nothing machine-shaped renders at all.
166
+ machines: [],
167
+ // The project picked on this Mac before a box was chosen in the new-session sheet: the key the
168
+ // same-repo prefill matches, and what "This Mac" restores.
169
+ newSessionLocalCwd: '',
163
170
  pendingStepUp: null,
164
171
  workflowRoute: { pending: null },
165
172
  homeSearchQuery: '',
@@ -386,7 +393,7 @@
386
393
  || !mobilePrivateStateRuntime.matchesAuth(operation.principalAuth, lease)) {
387
394
  if (mobilePrivatePreparationOperation !== operation) return null;
388
395
  identityCache.clear(); mobilePrivateLease = null; mobilePrivateReady = false; mobilePrivateCache = null; // D-7: an unattached lease keeps no identity bytes
389
- state.sessions = []; state.sessionsLoaded = true; state.remoteOutbox = [];
396
+ state.sessions = []; state.sessionsLoaded = true; state.remoteOutbox = []; syncPoolOutboxPendingMarker();
390
397
  state.privateScopeError = mobilePrivateStateRuntime.snapshot()?.code || 'private_scope_unavailable';
391
398
  setConnection('offline', 'Private data paused');
392
399
  render();
@@ -420,7 +427,7 @@
420
427
  const staleSocket = state.ws; state.ws = null; state.wsReady = false; clearWsReconnectTimer(); clearWsLivenessTimers(); try { staleSocket?.close(); } catch { /* expected: invalidation can race socket close */ }
421
428
  clearTimeout(state.remoteOutboxTimer); state.remoteOutboxTimer = null;
422
429
  clearTimeout(state.remoteOutboxStorage.persistTimer); state.remoteOutboxStorage.persistTimer = null;
423
- state.remoteOutbox = []; state.remoteOutboxDraining = false; state.sessions = []; state.sessionsLoaded = true; state.sessionsFromCache = false;
430
+ state.remoteOutbox = []; syncPoolOutboxPendingMarker(); state.remoteOutboxDraining = false; state.sessions = []; state.sessionsLoaded = true; state.sessionsFromCache = false;
424
431
  state.activeSession = null; state.detail.sessionId = null; state.detail.messages = []; state.remoteSubmissions.clear();
425
432
  state.remote = null; state.remoteStatusPromise = null; state.remoteStatusLoading = false; remoteStatusLease = null;
426
433
  state.remoteOutboxStorage.persistInFlight = null; remoteOutboxMirrorDeferred = false;
@@ -2458,6 +2465,7 @@
2458
2465
  <span class="session-title-line">
2459
2466
  <span class="session-title">${esc(card.title || card.id || 'Session')}</span>
2460
2467
  ${opts.worker === true ? cardModelChip(card) : ''}
2468
+ ${phoneMachineChip(card)}
2461
2469
  ${pill}
2462
2470
  </span>
2463
2471
  <span class="session-meta"><span class="session-meta-text">${esc(meta)}</span>${sessionDiffMark(card)}</span>
@@ -4099,6 +4107,10 @@
4099
4107
  .then((mod) => {
4100
4108
  if (container) container.dataset.moreLoaded = '1';
4101
4109
  mod.renderMore(moreContext());
4110
+ // WP-5 (D-11/S-16): the Machines card is part of More on the phone, exactly as it is part of
4111
+ // Settings on the desktop. more.js owns #more-content and rewrites it wholesale, so the card
4112
+ // is (re)appended after each of its renders.
4113
+ renderPhoneMachinesCard();
4102
4114
  })
4103
4115
  .catch((err) => {
4104
4116
  if (container) {
@@ -5616,7 +5628,25 @@
5616
5628
  }
5617
5629
  }
5618
5630
 
5631
+ // WP-4 (docs/dgx-spark-coding-readiness-2026-10-24.html) — phone = desktop parity: the desktop
5632
+ // shows a MACHINE-scoped failure frame verbatim in a toast (public/index.html:4471-4481), while
5633
+ // this dispatcher used to drop every 'error' frame, so a hub refusal (machine_agent_signed_out,
5634
+ // machine_unreachable, …) was invisible on the phone. Only machine failures are shown, through
5635
+ // the app's existing toast helper (:13662); every other error frame keeps today's behaviour.
5636
+ function showMobileMachineError(msg) {
5637
+ const code = String((msg && msg.code) || '');
5638
+ if (!/^machine_/.test(code) && !(msg && msg.machine)) return false;
5639
+ toast(String((msg && (msg.message || msg.error)) || code || 'Machine error'));
5640
+ return true;
5641
+ }
5642
+
5619
5643
  function dispatchMobileWsMessage(msg) {
5644
+ if (msg.type === 'error') {
5645
+ // A machine create refused at the hub names the box and the page that fixes it; show it and
5646
+ // stop. 'hello'/'pong'/… are unaffected, and no other frame type is touched.
5647
+ showMobileMachineError(msg);
5648
+ return;
5649
+ }
5620
5650
  if (msg.type === 'hello') {
5621
5651
  handleServerHello(msg);
5622
5652
  return;
@@ -5667,9 +5697,16 @@
5667
5697
  }
5668
5698
  if (msg.type === 'sessions' || msg.type === 'data-changed') {
5669
5699
  if (msg.type === 'data-changed' && ['models', 'model-registry', 'providers'].includes(msg.resource)) phoneModelPicker?.invalidateCatalog?.(); // D-6: as desktop onDataChanged
5700
+ // R-3: a queue-draft echo reloads the open idle builder's draft (reloadIfIdle guards
5701
+ // mid-operation panes); scheduleRefresh below is unchanged.
5702
+ if (msg.type === 'data-changed' && msg.resource === 'queue-draft' && msg.id) {
5703
+ privateQueueActions.reloadIfIdle(msg.id).catch(window.ClientErrors.tail('mobile:queue-draft-load'));
5704
+ }
5670
5705
  scheduleRefresh(350);
5671
5706
  return;
5672
5707
  }
5708
+ // R-3: delivery transitions (server.js broadcastQueueState) were dropped on the phone.
5709
+ if (msg.type === 'queue-state' && msg.sessionId) privateQueueActions.reloadIfIdle(msg.sessionId).catch(window.ClientErrors.tail('mobile:queue-draft-load'));
5673
5710
  if (msg.type === 'session-activity') { applySessionActivity(msg); return; }
5674
5711
  if (msg.type === 'tunnel-economy') { phonePresence.handleTunnelEconomy(msg); return; } // D-8: the lane governor's economy notice drives the passive #detail-tunnel-economy chip
5675
5712
  if (msg.type === 'session.status') {
@@ -5814,7 +5851,28 @@
5814
5851
  }, 0);
5815
5852
  }
5816
5853
 
5854
+ // Plain window.localStorage, or null when the context blocks it (private mode throws on the getter
5855
+ // itself). The outward mirror of the queue is the only thing in this file that uses plain storage.
5856
+ function phonePlainStorage() {
5857
+ try {
5858
+ return window.localStorage || null;
5859
+ } catch (error) {
5860
+ return null; // expected: a blocked/opaque context throws on the localStorage getter itself
5861
+ }
5862
+ }
5863
+
5864
+ // P-7 boot seam: public/m/pool-failover.js probes the other members before this app has a private
5865
+ // lease, and cannot read the outbox (which is leased). So every persist and every clear mirrors ONLY
5866
+ // the queue's presence into `ctm.pool.outboxPending` — '1' or absent, never an entry, an id or a
5867
+ // count — and attemptPhonePoolFailover() reads it back through the shared module.
5868
+ function syncPoolOutboxPendingMarker() {
5869
+ const failover = window.CtmPoolFailover;
5870
+ if (!failover?.writeOutboxPendingMarker) return false;
5871
+ return failover.writeOutboxPendingMarker(phonePlainStorage(), state.remoteOutbox.length > 0);
5872
+ }
5873
+
5817
5874
  function persistRemoteOutbox(options = {}) {
5875
+ syncPoolOutboxPendingMarker();
5818
5876
  const captured = capturedPrivateStorage(options.lease || mobilePrivateLease);
5819
5877
  if (!captured || !remoteOutboxPersistence) return false;
5820
5878
  if (pendingDurableOutboxWrites.size && options.includePending !== true) {
@@ -6087,6 +6145,7 @@
6087
6145
  } catch (err) {
6088
6146
  const index = state.remoteOutbox.findIndex((item) => item === entry && item.id === entry.id);
6089
6147
  if (index >= 0) state.remoteOutbox.splice(index, 1);
6148
+ syncPoolOutboxPendingMarker();
6090
6149
  throw err;
6091
6150
  } finally { finishPendingDurableOutboxWrite(entry.id, lease); }
6092
6151
  if (typeof opts.isStatusCurrent !== 'function' || opts.isStatusCurrent()) setComposerStatus(remoteOutboxQueuedStatus(entry), '');
@@ -13284,19 +13343,357 @@
13284
13343
  if (state.activeTab === 'search') renderSearch();
13285
13344
  }
13286
13345
 
13287
- function openNewSessionSheet() {
13346
+ // ── WP-5 machine surfaces (docs/remote-machine-hub-sessions-2026-10-22.html §6.6, D-11, S-16) ──
13347
+ // The desktop presenter's field-for-field twin (public/js/core/session-commands.js): the same
13348
+ // /api/machines rows, the same card-state table, the same chip text and the same payload field.
13349
+ // Phone = desktop parity: no phone-only machine feature and no desktop-only machine feature.
13350
+ const PHONE_MACHINE_CARD_STATES = Object.freeze(['reachable', 'unknown', 'unhealthy']);
13351
+
13352
+ function phoneMachineFacts(row) {
13353
+ const meta = (row && (row.meta || row)) || {};
13354
+ return {
13355
+ machine: String(meta.machine || '').trim(),
13356
+ name: String(meta.machineName || meta.machine_name || '').trim(),
13357
+ health: String(meta.machineHealth || meta.machine_health || '').trim(),
13358
+ };
13359
+ }
13360
+
13361
+ // '' for a local session, so local rows are byte-identical to their pre-hub render (AC-5b).
13362
+ function phoneMachineChip(row) {
13363
+ const facts = phoneMachineFacts(row);
13364
+ if (!facts.machine) return '';
13365
+ const name = facts.name || facts.machine;
13366
+ const unreachable = facts.health === 'unreachable';
13367
+ const title = unreachable ? `Runs on ${name} — unreachable (showing its last known state)` : `Runs on ${name}`;
13368
+ return `<span class="machine-chip${unreachable ? ' unreachable' : ''}" title="${esc(title)}" aria-label="${esc(title)}"`
13369
+ + ` data-machine-name="${esc(name)}" data-machine-health="${esc(facts.health)}">${esc((unreachable ? '⚠ ' : '◈ ') + name)}</span>`;
13370
+ }
13371
+
13372
+ // WP-4 (docs/dgx-spark-coding-readiness-2026-10-24.html): the same rule as the desktop presenter —
13373
+ // a reachable box whose Claude/Codex CLI is signed out names the agents it needs signed in
13374
+ // (`agents` = the hub's last-read {claude, codex, checkedAt} verdict). The option stays ENABLED:
13375
+ // shell sessions still work there and the hub refuses a signed-out agent create itself.
13376
+ const PHONE_MACHINE_AGENT_LABELS = { claude: 'Claude', codex: 'Codex' };
13377
+
13378
+ function phoneMachineSignedOutAgents(machine) {
13379
+ const agents = machine && machine.agents;
13380
+ if (!agents || typeof agents !== 'object') return [];
13381
+ return ['claude', 'codex']
13382
+ .filter((key) => agents[key] === 'signed_out')
13383
+ .map((key) => PHONE_MACHINE_AGENT_LABELS[key]);
13384
+ }
13385
+
13386
+ // WP-4: the machine's NAME, read from the registry row (state.machines, the same array
13387
+ // phoneMachineOptionsHtml is called with — desktop machineSelectionName() does exactly this),
13388
+ // never from the option's rendered text, which carries the "◈ " glyph and may carry a
13389
+ // "— sign in: …" suffix. An id the list does not know falls back to the raw id.
13390
+ function phoneMachineDisplayName(machineId) {
13391
+ const id = String(machineId || '').trim();
13392
+ if (!id) return '';
13393
+ const row = (Array.isArray(state.machines) ? state.machines : [])
13394
+ .find((machine) => machine && String(machine.instanceId || '') === id);
13395
+ return String((row && (row.displayName || row.url)) || id);
13396
+ }
13397
+
13398
+ // AC-5a: reachable is selectable; unknown/unhealthy render DISABLED with their reason;
13399
+ // unreachable is ABSENT. The phone expresses the same three states as <option> attributes.
13400
+ function phoneMachineCardView(machine) {
13401
+ const health = String((machine && machine.health) || '').trim();
13402
+ if (PHONE_MACHINE_CARD_STATES.indexOf(health) === -1) return null;
13403
+ const id = String((machine && machine.instanceId) || '');
13404
+ if (!id) return null;
13405
+ const name = machine.displayName || machine.url || id;
13406
+ const reason = health === 'reachable'
13407
+ ? 'reachable'
13408
+ : health === 'unknown'
13409
+ ? 'health unknown — the box is not answering probes'
13410
+ : 'unhealthy — the box is up but its hub is degraded';
13411
+ const view = { id, name, health, reason, disabled: health !== 'reachable' };
13412
+ // WP-4: only a reachable box names an agent — a disabled option keeps its single reason.
13413
+ if (health === 'reachable') {
13414
+ const signInNeeded = phoneMachineSignedOutAgents(machine);
13415
+ if (signInNeeded.length) view.signInNeeded = signInNeeded;
13416
+ }
13417
+ return view;
13418
+ }
13419
+
13420
+ // The hub's registry: the phone reads the same GET /api/machines the desktop does. A hub-less
13421
+ // (older) server 404s and the phone simply has no machines (no grid, no chip, no card).
13422
+ async function loadMachines() {
13423
+ try {
13424
+ const payload = await api('/api/machines');
13425
+ state.machines = Array.isArray(payload?.machines) ? payload.machines.filter((m) => m && m.instanceId) : [];
13426
+ } catch {
13427
+ state.machines = [];
13428
+ }
13429
+ return state.machines;
13430
+ }
13431
+
13432
+ // The desktop rule (session-commands.js machinesWritableHere): registration is loopback-only
13433
+ // server-side, so off loopback the phone shows the list read-only instead of a form that 403s.
13434
+ // The phone's own loopback signal is the auth principal's isLoopback.
13435
+ function machinesWritableHere() {
13436
+ return state.auth?.isLoopback === true;
13437
+ }
13438
+
13439
+ // WP-6 (brief item 2): the phone Machines page is public/m/machines.js — the twin of the desktop
13440
+ // machines-page.js (This Mac + every registry member, the same groups/badges/sparklines/actions,
13441
+ // the same Add dialog, Show a code, Leave pool, Join pool, New session here, "Sign in to <member>
13442
+ // too"). The old phone card here (register form, remove button, health line) is DELETED, not kept
13443
+ // beside it: the register form's legacy URL path lives in the Add dialog on both surfaces, exactly
13444
+ // as WP-5 has it on the desktop.
13445
+ let phoneMachinesSurface = null;
13446
+
13447
+ function phoneMachines() {
13448
+ if (phoneMachinesSurface) return phoneMachinesSurface;
13449
+ phoneMachinesSurface = window.CtmPhoneMachines?.create?.({
13450
+ $,
13451
+ document,
13452
+ esc,
13453
+ api,
13454
+ toast,
13455
+ loadMachines: (options) => loadMachines(options),
13456
+ openNewSessionSheet: (instanceId) => openNewSessionMachine(instanceId),
13457
+ confirm: (message) => window.confirm(message),
13458
+ now: () => Date.now(),
13459
+ }) || null;
13460
+ return phoneMachinesSurface;
13461
+ }
13462
+
13463
+ // Renders (or re-renders) the Machines section inside #more-content. more.js owns that container and
13464
+ // rewrites it wholesale, so this runs again after each of its renders and the section node is reused.
13465
+ function renderPhoneMachinesCard() {
13466
+ return phoneMachines()?.render($('more-content')) || null;
13467
+ }
13468
+
13469
+ // ── WP-6 item 3: the picker — <select> at N <= 4, the search sheet above it ─────────────────────
13470
+ // The one clock read for the picker (D-12 staleness). Named, so a test drives it instead of the
13471
+ // page's real clock.
13472
+ function phoneMachinesNow() {
13473
+ return Date.now();
13474
+ }
13475
+ // N = This Mac + the visible registry rows (D-1). At or below MACHINE_PICKER_CARD_LIMIT the phone
13476
+ // keeps the one-tap <select>: its option text carries the D-12 load line when the hub has a fresh
13477
+ // sample, because a phone option row is short enough for it ("◈ Spark — CPU 12% · 3 running");
13478
+ // when the sample is stale or absent the option keeps the pre-WP-6 "◈ Name" text, so a hub that
13479
+ // sends no samples renders exactly what it rendered before. Above the limit the picker is the
13480
+ // search sheet (D-2 search, D-3 groups, offline disabled with "last seen …").
13481
+ function phoneMachineOptionsHtml(machines, now) {
13482
+ const views = (Array.isArray(machines) ? machines : []).filter((m) => m && m.instanceId).map(phoneMachineCardView).filter(Boolean);
13483
+ const items = ['<option value="">This Mac</option>']
13484
+ .concat(views.map((view) => {
13485
+ // '' when nothing is signed out, so the option text is byte-identical to its pre-WP-4 self.
13486
+ const signIn = !view.disabled && Array.isArray(view.signInNeeded) && view.signInNeeded.length
13487
+ ? ` — sign in: ${view.signInNeeded.join(', ')}`
13488
+ : '';
13489
+ const machine = (Array.isArray(machines) ? machines : []).find((m) => String(m?.instanceId || '') === view.id);
13490
+ // machineLoadLine is the shared copy (public/js/machine-pool-shared.js), loaded by
13491
+ // public/m/index.html before this file — the same global the desktop picker calls.
13492
+ const load = !view.disabled && machine ? machineLoadLine(machine, now) : '';
13493
+ // A stale/absent sample yields 'stats stale' or '' — neither earns a place in a one-line option.
13494
+ const loadText = load && load !== 'stats stale' ? ` — ${load}` : '';
13495
+ return `<option value="${esc(view.id)}"${view.disabled ? ' disabled' : ''}>${esc(view.disabled ? `◈ ${view.name} — ${view.reason}` : `◈ ${view.name}${loadText}${signIn}`)}</option>`;
13496
+ }));
13497
+ return { views, html: items.join('') };
13498
+ }
13499
+
13500
+ function phoneMachinePickerMode(machines) {
13501
+ return window.CtmPhoneMachines?.pickerMode?.(machines) || 'select';
13502
+ }
13503
+
13504
+ async function openNewSessionSheet() {
13288
13505
  if (!state.auth?.scopes?.includes('create') && !state.auth?.scopes?.includes('admin')) {
13289
13506
  toast('This phone token needs create scope. Upgrade it on the Mac.');
13290
13507
  return;
13291
13508
  }
13292
13509
  const select = $('new-session-project');
13293
- const options = state.projects.length
13294
- ? state.projects.map((project) => `<option value="${esc(project.path || project.cwd || project.name)}">${esc(project.name || compactPath(project.path || project.cwd))}</option>`)
13295
- : ['<option value="">Home</option>'];
13296
- select.innerHTML = options.join('');
13510
+ select.innerHTML = phoneProjectOptionsHtml(state.projects);
13511
+ state.newSessionLocalCwd = '';
13512
+ phoneMachineSetHint('');
13513
+ // WP-5 (S-16/AC-5c): the machine choice is fetched per open, exactly like the desktop modal.
13514
+ // With zero renderable machines the field stays hidden and the sheet is unchanged (AC-5a).
13515
+ // WP-6 (D-1/D-2/D-3): above MACHINE_PICKER_CARD_LIMIT machines the field becomes the search sheet
13516
+ // — the <select> stays as the value store (hidden), and the row above it opens the sheet.
13517
+ const machineField = $('new-session-machine-field');
13518
+ const machineSelect = $('new-session-machine');
13519
+ const machineRow = $('new-session-machine-row');
13520
+ if (machineSelect) machineSelect.innerHTML = '<option value="">This Mac</option>';
13521
+ if (machineField) machineField.hidden = true;
13522
+ const machines = await loadMachines();
13523
+ if (machineSelect) {
13524
+ const { views, html } = phoneMachineOptionsHtml(machines, phoneMachinesNow());
13525
+ machineSelect.innerHTML = html;
13526
+ if (machineField) machineField.hidden = views.length === 0;
13527
+ phoneMachineSetPickerMode(views.length ? phoneMachinePickerMode(machines) : 'select');
13528
+ }
13529
+ if (machineRow) machineRow.hidden = true;
13297
13530
  $('new-session-sheet').hidden = false;
13298
13531
  }
13299
13532
 
13533
+ // The picker's mode decides which of the two controls is visible; the <select> is always the value
13534
+ // holder, so createSession's `$('new-session-machine').value` read never changes.
13535
+ function phoneMachineSetPickerMode(mode) {
13536
+ const search = mode === 'search';
13537
+ const machineSelect = $('new-session-machine');
13538
+ const machineRow = $('new-session-machine-row');
13539
+ if (machineSelect) machineSelect.hidden = search;
13540
+ if (machineRow) {
13541
+ machineRow.hidden = !search;
13542
+ machineRow.textContent = phoneMachineSelectedLabel();
13543
+ }
13544
+ return search;
13545
+ }
13546
+
13547
+ function phoneMachineSelectedLabel() {
13548
+ const id = String($('new-session-machine')?.value || '').trim();
13549
+ return id ? `◈ ${phoneMachineDisplayName(id)}` : '◈ This Mac';
13550
+ }
13551
+
13552
+ // WP-6 item 4 / D-1: "New session here" from the Machines page opens the EXISTING new-session sheet
13553
+ // and selects that machine. One create path, exactly as the desktop page's machinesPageNewSessionHere.
13554
+ async function openNewSessionMachine(instanceId) {
13555
+ await openNewSessionSheet();
13556
+ const machineSelect = $('new-session-machine');
13557
+ const id = String(instanceId || '').trim();
13558
+ if (!machineSelect || machineSelect.hidden || !id) return false;
13559
+ machineSelect.value = id;
13560
+ phoneMachineSetPickerMode(phoneMachinePickerMode(state.machines));
13561
+ await onPhoneMachineChange();
13562
+ return true;
13563
+ }
13564
+
13565
+ // ── WP-6 item 3: the machine search sheet (N > MACHINE_PICKER_CARD_LIMIT) ───────────────────────
13566
+ let phoneMachineSheetOpen = false;
13567
+
13568
+ function renderPhoneMachineSearchSheet(query) {
13569
+ const list = $('machine-search-list');
13570
+ if (!list) return false;
13571
+ const now = phoneMachinesNow();
13572
+ const selected = String($('new-session-machine')?.value || '').trim();
13573
+ list.innerHTML = window.CtmPhoneMachines?.sheetListHtml?.(state.machines, query, selected, now) || '';
13574
+ const count = $('machine-search-count');
13575
+ if (count) {
13576
+ const groups = window.CtmPhoneMachines?.sheetGroups?.(state.machines, query, now) || [];
13577
+ const total = groups.reduce((sum, group) => sum + group.rows.length, 0);
13578
+ count.textContent = total
13579
+ ? `${total} machine${total === 1 ? '' : 's'}`
13580
+ : 'No machine matches that search.';
13581
+ }
13582
+ return true;
13583
+ }
13584
+
13585
+ function openPhoneMachineSearchSheet() {
13586
+ const backdrop = $('machine-search-sheet');
13587
+ if (!backdrop) return false;
13588
+ const input = $('machine-search-input');
13589
+ if (input) input.value = '';
13590
+ renderPhoneMachineSearchSheet('');
13591
+ backdrop.hidden = false;
13592
+ phoneMachineSheetOpen = true;
13593
+ if (input && typeof input.focus === 'function') input.focus();
13594
+ return true;
13595
+ }
13596
+
13597
+ function closePhoneMachineSearchSheet() {
13598
+ const backdrop = $('machine-search-sheet');
13599
+ if (backdrop) backdrop.hidden = true;
13600
+ phoneMachineSheetOpen = false;
13601
+ return true;
13602
+ }
13603
+
13604
+ function bindPhoneMachineSearchSheet() {
13605
+ const input = $('machine-search-input');
13606
+ if (input) input.addEventListener('input', () => renderPhoneMachineSearchSheet(input.value));
13607
+ $('machine-search-close')?.addEventListener('click', () => closePhoneMachineSearchSheet());
13608
+ $('machine-search-sheet')?.addEventListener('click', (event) => {
13609
+ if (event.target === $('machine-search-sheet')) closePhoneMachineSearchSheet();
13610
+ });
13611
+ $('machine-search-list')?.addEventListener('click', async (event) => {
13612
+ const row = event.target?.closest?.('[data-machine-pick]');
13613
+ if (!row || row.disabled) return;
13614
+ event.preventDefault();
13615
+ const machineSelect = $('new-session-machine');
13616
+ if (!machineSelect) return;
13617
+ machineSelect.value = row.getAttribute('data-machine-pick') || '';
13618
+ phoneMachineSetPickerMode('search');
13619
+ closePhoneMachineSearchSheet();
13620
+ await onPhoneMachineChange();
13621
+ });
13622
+ $('new-session-machine-row')?.addEventListener('click', (event) => {
13623
+ event.preventDefault();
13624
+ openPhoneMachineSearchSheet();
13625
+ });
13626
+ }
13627
+
13628
+ function phoneProjectOptionsHtml(projects) {
13629
+ const rows = Array.isArray(projects) ? projects : [];
13630
+ return rows.length
13631
+ ? rows.map((project) => `<option value="${esc(project.path || project.cwd || project.name)}">${esc(project.name || compactPath(project.path || project.cwd))}</option>`).join('')
13632
+ : '<option value="">Home</option>';
13633
+ }
13634
+
13635
+ function phoneMachineSetHint(text, warn) {
13636
+ const hint = $('new-session-machine-hint');
13637
+ if (!hint) return;
13638
+ hint.textContent = text || '';
13639
+ hint.hidden = !text;
13640
+ hint.classList.toggle('warn', !!warn);
13641
+ }
13642
+
13643
+ // The box checkout of the project picked on this Mac: the box row whose gitRemoteKey equals the
13644
+ // local project's key ('' = that project has no remote, so nothing can match).
13645
+ function phoneSameRepoProject(localProjects, localCwd, boxRows) {
13646
+ const local = (Array.isArray(localProjects) ? localProjects : []).find((project) => project && (project.path || project.cwd) === localCwd);
13647
+ const localKey = String(local?.gitRemoteKey || '');
13648
+ if (!localKey) return '';
13649
+ const match = (Array.isArray(boxRows) ? boxRows : []).find((row) => row && (row.path || row.cwd) && String(row.gitRemoteKey || '') === localKey);
13650
+ return match ? String(match.path || match.cwd) : '';
13651
+ }
13652
+
13653
+ // WP-5 (S-13, desktop _selectMachineCard + nsMachineAfterProjectsLoaded): the project list
13654
+ // belongs to the selected machine. A box lists the BOX's projects (GET /api/projects?machine=)
13655
+ // and prefills the checkout whose gitRemoteKey equals the key of the project picked on this Mac
13656
+ // (plain equality: the server normalizes); "This Mac" restores the local list and project. An
13657
+ // answer for a machine that is no longer selected is dropped, so a box path never lands under
13658
+ // "This Mac" (or under another box).
13659
+ async function onPhoneMachineChange() {
13660
+ const machineSelect = $('new-session-machine');
13661
+ const select = $('new-session-project');
13662
+ if (!machineSelect || !select) return;
13663
+ const machine = String(machineSelect.value || '').trim();
13664
+ if (!machine) {
13665
+ select.innerHTML = phoneProjectOptionsHtml(state.projects);
13666
+ if (state.newSessionLocalCwd) select.value = state.newSessionLocalCwd;
13667
+ state.newSessionLocalCwd = '';
13668
+ phoneMachineSetHint('');
13669
+ return;
13670
+ }
13671
+ if (!state.newSessionLocalCwd) state.newSessionLocalCwd = String(select.value || '');
13672
+ // The registry row's own name ('' only for an unknown id, which falls back to the id itself):
13673
+ // never the option's rendered text, which carries "◈ " and possibly "— sign in: …".
13674
+ const name = phoneMachineDisplayName(machine);
13675
+ select.innerHTML = '';
13676
+ phoneMachineSetHint(`Loading projects on ${name}…`);
13677
+ let rows = [];
13678
+ try {
13679
+ const data = await api(`/api/projects?machine=${encodeURIComponent(machine)}`);
13680
+ rows = Array.isArray(data) ? data : (Array.isArray(data?.projects) ? data.projects : []);
13681
+ } catch (err) {
13682
+ if (String($('new-session-machine')?.value || '').trim() !== machine) return;
13683
+ phoneMachineSetHint(`Could not list projects on ${name}: ${err?.message || err}`, true);
13684
+ return;
13685
+ }
13686
+ if (String($('new-session-machine')?.value || '').trim() !== machine) return;
13687
+ select.innerHTML = phoneProjectOptionsHtml(rows);
13688
+ const match = phoneSameRepoProject(state.projects, state.newSessionLocalCwd, rows);
13689
+ if (match) {
13690
+ select.value = match;
13691
+ phoneMachineSetHint(`Same repository found on ${name}.`);
13692
+ } else {
13693
+ phoneMachineSetHint(`No checkout on ${name} carries this project's git remote — pick a folder on that box.`, true);
13694
+ }
13695
+ }
13696
+
13300
13697
  async function createSession(event) {
13301
13698
  event.preventDefault();
13302
13699
  const lease = mobilePrivateLease;
@@ -13317,11 +13714,15 @@
13317
13714
  ? 'walle'
13318
13715
  : 'claude';
13319
13716
  const args = prompt && !isWalle ? [prompt] : [];
13320
- const request = Object.freeze({ cwd, agent, agentLabel, prompt, isWalle, cmd, args: Object.freeze(args.slice()) });
13717
+ // WP-5 (S-12/S-16): the machine chosen in the sheet travels with the create frame as `machine`;
13718
+ // '' is this Mac and the field is omitted, so a local create frame is unchanged.
13719
+ const machine = ($('new-session-machine')?.value || '').trim();
13720
+ const request = Object.freeze({ cwd, agent, agentLabel, prompt, isWalle, cmd, args: Object.freeze(args.slice()), machine });
13321
13721
  const action = async () => {
13322
13722
  if (!privateLeaseCurrent(lease) || state.pendingStepUp?.action !== action) throw privateScopeChangedError();
13323
13723
  const id = randomId();
13324
13724
  const payload = { type: 'create', id, cwd: request.cwd, cmd: request.cmd, args: request.args.slice() };
13725
+ if (request.machine) payload.machine = request.machine;
13325
13726
  if (request.isWalle) {
13326
13727
  Object.assign(payload, {
13327
13728
  label: 'Wall-E Coding', agentType: 'walle', agentMode: 'coding', agentKind: 'walle-coding',
@@ -13368,6 +13769,7 @@
13368
13769
  { ...hiddenSheet('stepup-sheet'), close: () => $('stepup-cancel')?.click() },
13369
13770
  { ...hiddenSheet('workflow-route-sheet'), close: () => closeWorkflowRouteSheet('cancel') },
13370
13771
  { ...hiddenSheet('model-picker-sheet'), close: () => closeModelPicker() },
13772
+ { ...hiddenSheet('machine-search-sheet'), close: () => closePhoneMachineSearchSheet() },
13371
13773
  { ...hiddenSheet('review-sheet'), close: () => closeReviewSheet() },
13372
13774
  { ...hiddenSheet('review-comment-sheet'), close: () => { $('review-comment-sheet').hidden = true; } },
13373
13775
  { ...hiddenSheet('queue-builder-sheet'), close: () => closeQueueBuilder() },
@@ -13768,6 +14170,8 @@
13768
14170
  $('new-session-btn').addEventListener('click', openNewSessionSheet);
13769
14171
  $('new-session-cancel').addEventListener('click', () => { $('new-session-sheet').hidden = true; });
13770
14172
  $('new-session-form').addEventListener('submit', createSession);
14173
+ $('new-session-machine')?.addEventListener('change', () => { onPhoneMachineChange().catch(() => { /* expected: onPhoneMachineChange reports its own failures in the hint */ }); });
14174
+ bindPhoneMachineSearchSheet();
13771
14175
  $('status-content')?.addEventListener('click', (event) => {
13772
14176
  if (event.target.closest('[data-private-scope-retry]')) { refreshPhoneHealth({ force: true }).catch(() => {}); retryPrivateStatePreparation().catch(() => {}); return; } // D-7: an explicit re-check ignores the TTL
13773
14177
  handleStatusSummaryClick(event);
@@ -13858,6 +14262,66 @@
13858
14262
  if (hash.startsWith('#walle/')) setTab('walle');
13859
14263
  }
13860
14264
 
14265
+ // ── WP-6 item 5 / P-7: the boot-failure seam ───────────────────────────────────────────────────
14266
+ // THE seam: init() below. The first API call of a boot is `prepareMobilePrivateState`'s private
14267
+ // scope read (public/js/private-state-scope.js runPreparation: GET /api/auth/me, GET
14268
+ // /api/instance/manifest, GET /api/auth/me again). When that read does not reach this origin, the
14269
+ // runtime records it in the snapshot as `httpStatus`: 0 for a rejected fetch / aborted body (the
14270
+ // transport outage P-7 is about) and the HTTP status when a response did arrive — so a 5xx from the
14271
+ // relay/tunnel is the same "the box behind this origin is not answering" case, while a 4xx means
14272
+ // this origin IS answering and the phone must not navigate away from it.
14273
+ function phoneBootOriginUnreachable() {
14274
+ const snapshot = mobilePrivateStateRuntime?.snapshot?.() || null;
14275
+ const status = Number(snapshot?.httpStatus);
14276
+ if (status === 0) return true;
14277
+ return Number.isFinite(status) && status >= 500;
14278
+ }
14279
+
14280
+ // The phone's own identity for the failover payloads: the dial-cache row for THIS origin (written by
14281
+ // the last successful /api/machines load), falling back to the origin itself. The cache holds the
14282
+ // five roster fields and no credential, so this is never a secret.
14283
+ function phoneSelfOriginRow() {
14284
+ const origin = String(window.location?.origin || '').replace(/\/+$/, '');
14285
+ const rows = window.CtmPoolFailover?.readPoolOrigins?.() || [];
14286
+ const mine = (Array.isArray(rows) ? rows : []).find((row) => String(row?.origin || '').replace(/\/+$/, '') === origin);
14287
+ return { instanceId: String(mine?.instanceId || ''), displayName: String(mine?.displayName || ''), origin };
14288
+ }
14289
+
14290
+ // Fire-and-forget: boot continues rendering the local shell whatever this decides. It navigates
14291
+ // only when another pool member answers (public/m/pool-failover.js), and asks first when this phone
14292
+ // still holds unsent outbox items — the outbox is per origin and would otherwise be stranded.
14293
+ function attemptPhonePoolFailover() {
14294
+ const failover = window.CtmPoolFailover;
14295
+ if (!failover?.failoverOnBootFailure) return null;
14296
+ // Offline means no member can answer: probing would burn four seconds per member to prove nothing.
14297
+ if (typeof navigator !== 'undefined' && navigator.onLine === false) return null;
14298
+ if (!phoneBootOriginUnreachable()) return null;
14299
+ return failover.failoverOnBootFailure({
14300
+ // The leased outbox is not readable yet, so this is the content-free presence marker app.js keeps
14301
+ // in plain localStorage; the in-memory length is ORed in so the prompt can never under-report.
14302
+ hasUnsentOutbox: !!failover.readOutboxPendingMarker?.(phonePlainStorage()) || state.remoteOutbox.length > 0,
14303
+ selfInstanceId: phoneSelfOriginRow().instanceId,
14304
+ renderList: (html) => { mountPhonePoolOriginsList(html); },
14305
+ }).catch(() => null); // HANDLE: a failover that cannot even probe leaves the user on the boot screen
14306
+ }
14307
+
14308
+ // The "no member answered" roster, mounted into the phone's own status surface (the P-7 list).
14309
+ function mountPhonePoolOriginsList(html) {
14310
+ const host = $('pool-failover-list') || $('pool-from-banner');
14311
+ if (!host) return null;
14312
+ host.innerHTML = String(html || '');
14313
+ return host;
14314
+ }
14315
+
14316
+ // AC-6e: arriving here with ?poolFrom=<id> means another member handed this phone over. Both names
14317
+ // come from the stored roster; every string is escaped inside the module.
14318
+ function showPhonePoolFromBanner() {
14319
+ const failover = window.CtmPoolFailover;
14320
+ if (!failover?.showPoolFromBanner) return null;
14321
+ const self = phoneSelfOriginRow();
14322
+ return failover.showPoolFromBanner({ self, selfInstanceId: self.instanceId });
14323
+ }
14324
+
13861
14325
  async function init() {
13862
14326
  const operation = {};
13863
14327
  mobilePrivatePreparationOperation = operation;
@@ -13885,7 +14349,14 @@
13885
14349
  state.sessionsLoaded = true;
13886
14350
  if (!preparationRetry.isPending()) setConnection('offline', state.auth?.authenticated ? 'Private data paused' : 'Sign in');
13887
14351
  render();
13888
- }
14352
+ // P-7 (WP-6 item 5): the boot API call did not reach this origin. A scheduled retry means the
14353
+ // origin is answering (the runtime asked us to come back), so only an unreachable origin tries
14354
+ // the other pool members. Nothing here blocks the local shell: the switch is a navigation.
14355
+ if (!preparationRetry.isPending()) attemptPhonePoolFailover();
14356
+ }
14357
+ // AC-6e: if we arrived on a ?poolFrom=<id> hand-over, say who handed us over — over the shell, on
14358
+ // every tab, whether or not the private lease came up.
14359
+ showPhonePoolFromBanner();
13889
14360
  if ('serviceWorker' in navigator && !window.__CTM_MOBILE_TEST_DISABLE_SW) {
13890
14361
  registerMobileServiceWorker().catch(() => {});
13891
14362
  }
@@ -13993,6 +14464,28 @@
13993
14464
  itemsContext: state.skillPicker.itemsContext ? { ...state.skillPicker.itemsContext } : null,
13994
14465
  }),
13995
14466
  selectMobileSkillItemForTest: (index, control) => selectMobileSkillItem(index, control),
14467
+ // WP-5 (S-11/S-16 AC-5a/AC-5c): the machine surfaces, so the phone spec can assert the same
14468
+ // contract the desktop spec does — zero machines renders nothing, the create payload carries
14469
+ // `machine`, and a remote row carries the chip.
14470
+ loadMachines,
14471
+ openNewSessionSheet,
14472
+ createSession,
14473
+ machineOptionsHtml: (machines, now) => phoneMachineOptionsHtml(machines, now),
14474
+ machinePickerMode: (machines) => phoneMachinePickerMode(machines),
14475
+ machineSearchSheetHtml: (machines, query, selectedId, now) => window.CtmPhoneMachines?.sheetListHtml?.(machines, query, selectedId, now) || '',
14476
+ // WP-6: the Machines page itself now lives in public/m/machines.js (its presenters are exported
14477
+ // there); this renders the page's list for a given registry, so the phone spec can still assert
14478
+ // the card contract from the app realm without reaching into the module.
14479
+ machinesCardHtml: (machines) => {
14480
+ const surface = phoneMachines();
14481
+ if (!surface) return '';
14482
+ if (Array.isArray(machines)) surface.state.machines = machines.map((machine) => ({ ...machine }));
14483
+ return surface.listHtml();
14484
+ },
14485
+ machineState: () => state.machines.map((machine) => ({ ...machine })),
14486
+ sessionRowHtml: (card) => sessionRow(card),
14487
+ sameRepoProject: (localProjects, localCwd, boxRows) => phoneSameRepoProject(localProjects, localCwd, boxRows),
14488
+ machineChipHtml: (card) => phoneMachineChip(card),
13996
14489
  };
13997
14490
  }
13998
14491