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
@@ -142,6 +142,106 @@ function createSessionModelControl(sessionId) {
142
142
  return control;
143
143
  }
144
144
 
145
+ // ── WP-5 machine chip (docs/remote-machine-hub-sessions-2026-10-22.html §6.6, §8 S-14/S-15) ──
146
+ // The branch badge's sibling: ONE presenter, rendered from the session row's own facts
147
+ // (`machine` / `machineName` / `machineHealth`, served by the hub) and looked up GLOBALLY by
148
+ // data-session-id so every refresh path keeps working after the toolbar reparent (§6.6,
149
+ // S-15 — the same rule as _findSessionModelChip). A local session (`machine === ''`) gets NO
150
+ // chip at all, so local rows render exactly as they did before this work (AC-5b).
151
+ const MACHINE_HEALTH_UNREACHABLE = 'unreachable';
152
+ // Card states that still render in the new-session grid: reachable is selectable, the other two
153
+ // render DISABLED with their reason. `unreachable` machines are ABSENT from the grid (AC-5a).
154
+ const MACHINE_CARD_STATES = ['reachable', 'unknown', 'unhealthy'];
155
+
156
+ // Machine facts off either a `sessions`-frame row or a client session object (`s.meta`).
157
+ function sessionMachineFacts(row) {
158
+ const meta = (row && row.meta) || row || {};
159
+ return {
160
+ machine: String(meta.machine || '').trim(),
161
+ name: String(meta.machineName || meta.machine_name || '').trim(),
162
+ health: String(meta.machineHealth || meta.machine_health || '').trim(),
163
+ };
164
+ }
165
+
166
+ // Pure presentation: null for a local session, else the label/title/health the chip renders.
167
+ function machineChipPresentation(row) {
168
+ const facts = sessionMachineFacts(row);
169
+ if (!facts.machine) return null;
170
+ const name = facts.name || facts.machine;
171
+ const unreachable = facts.health === MACHINE_HEALTH_UNREACHABLE;
172
+ const title = unreachable
173
+ ? `Runs on ${name} — unreachable (showing its last known state)`
174
+ : `Runs on ${name}`;
175
+ return { name, health: facts.health, unreachable, title, label: (unreachable ? '⚠ ' : '◈ ') + name };
176
+ }
177
+
178
+ // HTML twin, for the row templates that build markup as strings.
179
+ function machineChipHtml(row) {
180
+ const view = machineChipPresentation(row);
181
+ if (!view) return '';
182
+ const classes = 'machine-chip' + (view.unreachable ? ' unreachable' : '');
183
+ const id = _machineRowId(row);
184
+ return `<span class="${classes}" title="${escHtml(view.title)}" aria-label="${escHtml(view.title)}"`
185
+ + (id ? ` data-session-id="${escHtml(id)}"` : '')
186
+ + ` data-machine-name="${escHtml(view.name)}" data-machine-health="${escHtml(view.health)}">${escHtml(view.label)}</span>`;
187
+ }
188
+
189
+ // Node twin, for the tab chrome (built with createElement, like the branch badge).
190
+ function machineSessionChipNode(row, sessionId) {
191
+ const view = machineChipPresentation(row);
192
+ if (!view) return null;
193
+ const chip = document.createElement('span');
194
+ chip.className = 'machine-chip' + (view.unreachable ? ' unreachable' : '');
195
+ chip.textContent = view.label;
196
+ chip.title = view.title;
197
+ chip.setAttribute('aria-label', view.title);
198
+ chip.dataset.machineName = view.name;
199
+ chip.dataset.machineHealth = view.health;
200
+ const id = String(sessionId || _machineRowId(row) || '');
201
+ if (id) chip.dataset.sessionId = id;
202
+ return chip;
203
+ }
204
+
205
+ function _machineRowId(row) {
206
+ if (!row) return '';
207
+ const meta = row.meta || {};
208
+ return String(row._id || row.sessionId || row.session_id || row.id || meta.id || meta.sessionId || '');
209
+ }
210
+
211
+ // The global lookup by data-session-id (same rule as the model chip): an active session's toolbar
212
+ // is reparented into #tabbar-session-controls, so its container no longer holds the chip.
213
+ function _findSessionMachineChip(sessionId) {
214
+ if (!sessionId) return null;
215
+ const escaped = (window.CSS && CSS.escape) ? CSS.escape(String(sessionId)) : String(sessionId);
216
+ return document.querySelector('.machine-chip[data-session-id="' + escaped + '"]');
217
+ }
218
+
219
+ // Repaint an already-mounted chip from fresh facts (health flips reachable -> unreachable without
220
+ // a tab rebuild, since the tab unit's structural hash does not carry machine health).
221
+ function refreshSessionMachineChip(row) {
222
+ const view = machineChipPresentation(row);
223
+ if (!view) return false;
224
+ const chip = _findSessionMachineChip(_machineRowId(row));
225
+ if (!chip) return false;
226
+ chip.textContent = view.label;
227
+ chip.title = view.title;
228
+ chip.setAttribute('aria-label', view.title);
229
+ chip.dataset.machineName = view.name;
230
+ chip.dataset.machineHealth = view.health;
231
+ chip.classList.toggle('unreachable', view.unreachable);
232
+ return true;
233
+ }
234
+
235
+ function refreshSessionMachineChips(rows) {
236
+ if (!Array.isArray(rows)) return 0;
237
+ let changed = 0;
238
+ for (const row of rows) {
239
+ if (!sessionMachineFacts(row).machine) continue;
240
+ if (refreshSessionMachineChip(row)) changed += 1;
241
+ }
242
+ return changed;
243
+ }
244
+
145
245
  function createSessionsOverviewTab() {
146
246
  const tab = document.createElement('div');
147
247
  tab.className = `tab pinned-tab sessions-overview-tab ${isSessionsOverviewActive() ? 'active' : ''}`;
@@ -267,6 +367,11 @@ function _createStreamChromeSessionTab(unit) {
267
367
  : null;
268
368
  if (trackingBadge) tab.appendChild(trackingBadge);
269
369
 
370
+ // WP-5 (S-15/AC-5b): the machine chip sits beside the branch badge, built by the same
371
+ // presenter the sidebar rows use. Local sessions produce null and the tab is unchanged.
372
+ const machineChip = machineSessionChipNode(unit.session, id);
373
+ if (machineChip) tab.appendChild(machineChip);
374
+
270
375
  const lifecycleTemplate = document.createElement('template');
271
376
  lifecycleTemplate.innerHTML = lifecycleHtml;
272
377
  const lifecycleChip = lifecycleTemplate.content.firstElementChild;
@@ -479,6 +584,9 @@ function createSession() {
479
584
  var cwd = document.getElementById('ns-cwd').value || undefined;
480
585
  var label = document.getElementById('ns-label').value;
481
586
  var useWorktree = document.getElementById('ns-worktree').checked;
587
+ // WP-5 (S-12/§6.3): the selected machine travels with the create frame as `machine`; '' is this
588
+ // machine and the field is omitted, so the local create frame is byte-identical to today.
589
+ var machine = machineSelection();
482
590
  // WP-2 (design doc agent-context-budget-claude-codex-2026-09-26.html §5 C-1, §8 WP-2):
483
591
  // send the selected MCP profile for every agent (the server only applies it to
484
592
  // Claude/Codex, simpler than special-casing here) and remember it per project.
@@ -497,6 +605,7 @@ function createSession() {
497
605
 
498
606
  function _doCreate(finalCwd) {
499
607
  var msg = { type: 'create', cwd: finalCwd, label: label || undefined, mcp_profile: mcpProfile };
608
+ if (machine) msg.machine = machine;
500
609
  if (wantCreateCwd && !useWorktree && finalCwd) msg.createCwd = true;
501
610
  // Spawn the PTY at the terminal's real size so the agent's first paint
502
611
  // matches the final height (no 120x30→fit reflow that strands Codex's cursor).
@@ -538,6 +647,10 @@ function createSession() {
538
647
  }
539
648
  var createBtn = document.querySelector('#new-session-modal .btn.primary');
540
649
  if (createBtn) { createBtn.disabled = true; createBtn.textContent = 'Creating worktree...'; }
650
+ // WP-5 (S-12/§6.5): the worktree is created ON the selected machine, so `cwd` is the
651
+ // box-local path the picker returned and `machine` routes the request (the hub proxies
652
+ // POST /api/worktrees/create?machine=<id>). No local source field is ever named here —
653
+ // the box refuses `machine_source_mixed` if one is.
541
654
  _wtCreateWithProgress({
542
655
  name: wtName,
543
656
  cwd: cwd,
@@ -548,6 +661,7 @@ function createSession() {
548
661
  branch: wtName,
549
662
  subtitle: 'CTM is creating the isolated checkout before launching the session.',
550
663
  showAfterMs: 900,
664
+ machine: machine,
551
665
  })
552
666
  .then(function(d) {
553
667
  if (d.error) {
@@ -922,3 +1036,565 @@ function closeSessionPalette() {
922
1036
  _sessionPaletteIdx = 0;
923
1037
  _sessionPaletteItems = [];
924
1038
  }
1039
+
1040
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
1041
+ // WP-5 machine surfaces (docs/remote-machine-hub-sessions-2026-10-22.html §6.5, §6.6, D-11)
1042
+ // S-11/S-13/AC-5a new-session machine cards (agent-grid pattern) + per-machine path truth
1043
+ // §6.5 box picker routing (every request carries ?machine=<id>) and the
1044
+ // same-repo prefill — plain gitRemoteKey equality, never client-side
1045
+ // normalization (the server normalizes both ends)
1046
+ // Settings register / list / remove machines (loopback-only writable, D-11)
1047
+ // No feature flag: with zero machines the card grids do not render and the modal is identical
1048
+ // to its pre-hub self (AC-5a).
1049
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
1050
+
1051
+ // `''` is this machine — the default, and the only selection when the hub has no machines.
1052
+ // `renderedAt` is the clock the last card render judged D-12 staleness against, so a re-render
1053
+ // (a popover pick) cannot move the numbers under the user; `pickerMachine` is the D-1 row picked
1054
+ // from the popover, kept for THIS modal session only.
1055
+ const _nsMachineState = {
1056
+ machines: [], localRepoKeyPath: null, localKeysRequested: false, localCwd: '',
1057
+ renderedAt: 0, pickerMachine: '',
1058
+ };
1059
+
1060
+ function machineSelection() {
1061
+ const el = document.getElementById('ns-machine');
1062
+ return el ? String(el.value || '') : '';
1063
+ }
1064
+
1065
+ function machineSelectionName() {
1066
+ const id = machineSelection();
1067
+ if (!id) return 'This Mac';
1068
+ const machine = _nsMachineState.machines.find(m => m && m.instanceId === id);
1069
+ return (machine && (machine.displayName || machine.url)) || id;
1070
+ }
1071
+
1072
+ // WP-4 (docs/dgx-spark-coding-readiness-2026-10-24.html): a box can be REACHABLE and still have a
1073
+ // coding CLI that is signed out, so a create would open at a login prompt. The hub refuses that
1074
+ // create on its own (lib/remote-machine-create.js → machine_agent_signed_out), so the card only
1075
+ // has to SAY it: the box stays selectable because shell sessions still work there. `agents` is the
1076
+ // hub's last-read verdict ({claude, codex, checkedAt}); a missing field, or any value that is not
1077
+ // exactly 'signed_out' (signed_in / unknown), adds nothing — the view is then byte-identical to
1078
+ // its pre-WP-4 self.
1079
+ // WP-6: the labels themselves are the ONE shared copy, MACHINE_AGENT_LABELS in
1080
+ // public/js/machine-pool-shared.js, which public/index.html loads before this file (the phone reads
1081
+ // the same pair through public/m/index.html). A second declaration here is exactly what S-18 forbids.
1082
+ function machineSignedOutAgents(machine) {
1083
+ const agents = machine && machine.agents;
1084
+ if (!agents || typeof agents !== 'object') return [];
1085
+ return ['claude', 'codex']
1086
+ .filter((key) => agents[key] === 'signed_out')
1087
+ .map((key) => MACHINE_AGENT_LABELS[key]);
1088
+ }
1089
+
1090
+ // AC-5a: reachable is selectable; unknown/unhealthy render DISABLED carrying their reason;
1091
+ // unreachable (and any state the hub may add later) is ABSENT from the grid.
1092
+ function machineCardView(machine, now) {
1093
+ const health = String((machine && machine.health) || '').trim();
1094
+ if (MACHINE_CARD_STATES.indexOf(health) === -1) return null;
1095
+ const id = String((machine && machine.instanceId) || '');
1096
+ if (!id) return null;
1097
+ const name = (machine.displayName || machine.url || id);
1098
+ const note = health === 'reachable'
1099
+ ? (machine.url || '')
1100
+ : health === 'unknown'
1101
+ ? 'Health unknown — the box is not answering probes'
1102
+ : 'Unhealthy — the box is up but its hub is degraded';
1103
+ const view = { id, name, health, sub: health === 'reachable' ? 'reachable' : note, disabled: health !== 'reachable', note };
1104
+ // WP-4: only a reachable box can name an agent — a disabled card keeps its single reason.
1105
+ if (health === 'reachable') {
1106
+ const signInNeeded = machineSignedOutAgents(machine);
1107
+ if (signInNeeded.length) view.signInNeeded = signInNeeded;
1108
+ }
1109
+ // D-12: the box's live numbers are the card's third line. A box the hub has never sampled adds
1110
+ // NOTHING (the field is absent, never ''), so today's two-line card stays byte-identical and the
1111
+ // existing "no new field at all" pin keeps holding.
1112
+ const load = machineLoadLine(machine, now);
1113
+ if (load) view.load = load;
1114
+ return view;
1115
+ }
1116
+
1117
+ function _nsMachineCardNode(view) {
1118
+ const card = document.createElement('div');
1119
+ card.className = 'ns-agent-card ns-machine-card' + (view.disabled ? ' disabled' : '');
1120
+ card.dataset.value = view.id;
1121
+ card.dataset.health = view.health;
1122
+ card.style.setProperty('--ns-card-accent', view.disabled ? 'var(--warning, #e0af68)' : '#7aa2f7');
1123
+ // WP-4: the card stays clickable and keeps its accent — only the sub line and the tooltip name
1124
+ // the signed-out agents. '' here (nothing signed out) reproduces the pre-WP-4 strings exactly.
1125
+ const signIn = !view.disabled && Array.isArray(view.signInNeeded) && view.signInNeeded.length
1126
+ ? view.signInNeeded.join(', ')
1127
+ : '';
1128
+ // WP-3 (docs/remote-agent-sign-in-from-hub-browser-2026-10-25.html §9 S-14): the tooltip points
1129
+ // at the Machines page, which signs in a box's coding agent through the hub's own proxy — the
1130
+ // box's local page ("open <url> → AI Providers → Coding Agents → Sign in") is not reachable from
1131
+ // a phone, and no longer the instruction on either surface.
1132
+ card.title = view.disabled
1133
+ ? view.name + ': ' + view.note
1134
+ : signIn
1135
+ ? 'Create this session on ' + view.name + ' — not signed in there: ' + signIn + ' (Sign in from the Machines page)'
1136
+ : 'Create this session on ' + view.name;
1137
+ const icon = document.createElement('div');
1138
+ icon.className = 'ns-card-icon';
1139
+ icon.style.background = view.disabled ? 'var(--warning, #e0af68)' : '#7aa2f7';
1140
+ icon.textContent = view.id ? '◈' : '⌘';
1141
+ const name = document.createElement('div');
1142
+ name.className = 'ns-card-name';
1143
+ name.textContent = view.name;
1144
+ const sub = document.createElement('div');
1145
+ sub.className = 'ns-card-sub';
1146
+ sub.textContent = view.disabled ? 'unavailable' : (signIn ? 'sign in: ' + signIn : 'ready');
1147
+ card.appendChild(icon);
1148
+ card.appendChild(name);
1149
+ card.appendChild(sub);
1150
+ // D-12: the load line is the card's THIRD line and only exists when the hub holds a sample.
1151
+ if (view.load) {
1152
+ const load = document.createElement('div');
1153
+ load.className = 'ns-card-load';
1154
+ load.textContent = view.load;
1155
+ card.appendChild(load);
1156
+ }
1157
+ if (!view.disabled) card.onclick = function() { _selectMachineCard(view.id); };
1158
+ return card;
1159
+ }
1160
+
1161
+ // ── WP-4 picker (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §5 D-1/D-2/D-3/D-12) ──
1162
+ // D-1: while N = 1 + the visible (non-hidden) registry rows is at most MACHINE_PICKER_CARD_LIMIT the
1163
+ // grid stays the one-click card row it has always been; above the limit it keeps This Mac + the top
1164
+ // three and adds a fifth "All machines (N)…" card. That card opens the APG combobox that already
1165
+ // ships as window.CtmModelPicker — P4: one combobox, never a fifth copy.
1166
+ //
1167
+ // WP-6: MACHINE_PICKER_CARD_LIMIT, MACHINE_STATS_STALE_MS, MACHINE_RECENT_MS and the presenters
1168
+ // below (machinePickerOrder / machineSearchText / machineLoadLine / machineIsOffline /
1169
+ // machineIsFavorite / machinePickerGroupRows / machineHealthLabel / machineLastSeenText) now live in
1170
+ // the ONE shared copy, public/js/machine-pool-shared.js, which public/index.html loads before this
1171
+ // file and public/m/index.html loads for the phone. This file keeps only the desktop grid/combobox.
1172
+ // The sentinel this card's data-value carries. It is never '' — an empty value would make
1173
+ // _selectMachineCard paint the "All machines" card as the selected machine.
1174
+ const MACHINE_ALL_CARD_VALUE = '__all_machines__';
1175
+ // WP-5 (D-14): the machine pool is a first-class page — nav target `machines`, hash `#machines`
1176
+ // (js/app/machines-page.js). index.html's router rewrites the old `#setup&tab=machines` deep link
1177
+ // onto it, so bookmarks keep working.
1178
+ const MACHINE_MANAGE_HREF = '#machines';
1179
+ const MACHINE_MANAGE_LABEL = 'Manage machines';
1180
+ const MACHINE_MANAGE_ID = 'ns-machine-manage';
1181
+
1182
+ // The picker component, or null on a page that never loaded it (an older boot snapshot): the card
1183
+ // row still renders, the "All machines" card just does nothing.
1184
+ function _machinePickerComponent() {
1185
+ if (typeof window !== 'undefined' && window && window.CtmModelPicker) return window.CtmModelPicker;
1186
+ if (typeof CtmModelPicker !== 'undefined' && CtmModelPicker) return CtmModelPicker;
1187
+ return null;
1188
+ }
1189
+
1190
+ // D-1: the machine a row picked this modal session, moved into the THIRD top card. Cleared by
1191
+ // loadNewSessionMachines, so it can never outlive the modal it was chosen in.
1192
+ function _machineTopCardOrder(order) {
1193
+ const forced = String(_nsMachineState.pickerMachine || '');
1194
+ if (!forced) return order;
1195
+ const chosen = order.find((m) => m.instanceId === forced);
1196
+ if (!chosen) return order;
1197
+ const rest = order.filter((m) => m.instanceId !== forced);
1198
+ const head = rest.slice(0, MACHINE_PICKER_CARD_LIMIT - 2);
1199
+ return head.concat([chosen], rest.slice(MACHINE_PICKER_CARD_LIMIT - 2));
1200
+ }
1201
+
1202
+ // D-1: the top three cards, in order, skipping any row that produces no card at all (an unreachable
1203
+ // box stays out of the row — HUB AC-5a — but keeps its place in the popover's Offline group).
1204
+ function _nsMachineTopViews(order, now) {
1205
+ const views = [];
1206
+ for (const machine of _machineTopCardOrder(order)) {
1207
+ if (views.length >= MACHINE_PICKER_CARD_LIMIT - 1) break;
1208
+ const view = machineCardView(machine, now);
1209
+ if (view) views.push(view);
1210
+ }
1211
+ return views;
1212
+ }
1213
+
1214
+ // D-3 groups. Favorites and Recent are filtered views of All (a box may legitimately appear in two
1215
+ // groups); Offline carries the rows HUB AC-5a keeps out of the card row, disabled with the reason a
1216
+ // "where is my machine" search is looking for. WP-6: the group SET is the shared
1217
+ // machinePickerGroupRows (public/js/machine-pool-shared.js), so the phone's sheet lists the same
1218
+ // groups in the same order; this function only turns each row into a CtmModelPicker row.
1219
+ function _machinePickerGroups(now) {
1220
+ const groups = machinePickerGroupRows(_nsMachineState.machines, now)
1221
+ .map((group) => ({
1222
+ id: group.id,
1223
+ label: group.label,
1224
+ rows: group.rows.map((machine) => _machinePickerRow(machine, group.id, now)),
1225
+ }));
1226
+ return {
1227
+ groups,
1228
+ emptyMessage: 'No machines yet',
1229
+ noMatchMessage: 'No machine matches that search',
1230
+ };
1231
+ }
1232
+
1233
+ function _machinePickerRow(machine, groupId, now) {
1234
+ const load = machineLoadLine(machine, now);
1235
+ const row = {
1236
+ // Group-scoped key: the same box may be listed in two groups, and only the option with this key
1237
+ // is ever marked selected.
1238
+ key: groupId + ':' + machine.instanceId,
1239
+ label: machine.displayName || machine.hostname || machine.url || machine.instanceId,
1240
+ sublabel: load,
1241
+ search: machineSearchText(machine),
1242
+ current: String(machine.instanceId) === machineSelection(),
1243
+ data: { instanceId: machine.instanceId },
1244
+ };
1245
+ if (machineIsOffline(machine)) {
1246
+ row.disabled = { reason: machineLastSeenText(machine) };
1247
+ row.badges = [{ text: 'offline', tone: 'warn' }];
1248
+ }
1249
+ return row;
1250
+ }
1251
+
1252
+ function _machinePickerDetail(row, now) {
1253
+ const pane = document.createElement('div');
1254
+ const machine = row && row.data
1255
+ ? _nsMachineState.machines.find((m) => m && m.instanceId === row.data.instanceId)
1256
+ : null;
1257
+ if (!machine) {
1258
+ pane.className = 'walle-model-config-provider';
1259
+ pane.textContent = 'Pick a machine to see where the session will run.';
1260
+ return pane;
1261
+ }
1262
+ const eyebrow = document.createElement('div');
1263
+ eyebrow.className = 'walle-model-config-eyebrow';
1264
+ eyebrow.textContent = 'Machine';
1265
+ const title = document.createElement('div');
1266
+ title.className = 'walle-model-config-title';
1267
+ title.textContent = machine.displayName || machine.hostname || machine.url || machine.instanceId;
1268
+ const meta = document.createElement('div');
1269
+ meta.className = 'walle-model-config-provider';
1270
+ meta.textContent = [
1271
+ machineHealthLabel(machine),
1272
+ machineLastSeenText(machine),
1273
+ machineLoadLine(machine, now),
1274
+ [machine.os, machine.arch].filter(Boolean).join('/'),
1275
+ machine.url,
1276
+ ].filter(Boolean).join(' · ');
1277
+ pane.appendChild(eyebrow);
1278
+ pane.appendChild(title);
1279
+ pane.appendChild(meta);
1280
+ return pane;
1281
+ }
1282
+
1283
+ // D-1: choosing a row makes it the selected card for this modal session and goes through
1284
+ // _selectMachineCard, so the per-machine path truth (S-13) is re-keyed exactly as a card click does.
1285
+ function _machinePickerChoose(row) {
1286
+ const id = row && row.data ? String(row.data.instanceId || '') : '';
1287
+ const machine = id ? _nsMachineState.machines.find((m) => m && m.instanceId === id) : null;
1288
+ if (!machine || machineIsOffline(machine)) return false;
1289
+ _nsMachineState.pickerMachine = id;
1290
+ renderNewSessionMachineCards(_nsMachineState.machines, _nsMachineState.renderedAt);
1291
+ _selectMachineCard(id);
1292
+ return true;
1293
+ }
1294
+
1295
+ // The "All machines (N)…" card. It is a real button (role=button + tabindex + aria-haspopup), so
1296
+ // the APG keyboard contract holds: Enter/Space opens the popover and focus lands back here on close.
1297
+ function _nsMachineAllCardNode(count) {
1298
+ const card = document.createElement('div');
1299
+ card.className = 'ns-agent-card ns-machine-card ns-machine-all';
1300
+ card.dataset.value = MACHINE_ALL_CARD_VALUE;
1301
+ card.setAttribute('role', 'button');
1302
+ card.setAttribute('tabindex', '0');
1303
+ card.setAttribute('aria-haspopup', 'dialog');
1304
+ card.title = 'Show all ' + count + ' machines';
1305
+ const icon = document.createElement('div');
1306
+ icon.className = 'ns-card-icon';
1307
+ icon.style.background = '#7aa2f7';
1308
+ icon.textContent = '⋯';
1309
+ const name = document.createElement('div');
1310
+ name.className = 'ns-card-name';
1311
+ name.textContent = 'All machines (' + count + ')…';
1312
+ card.appendChild(icon);
1313
+ card.appendChild(name);
1314
+ card.onclick = function() { openAllMachinesPicker(card); };
1315
+ card.addEventListener('keydown', function(event) {
1316
+ const key = String((event && event.key) || '');
1317
+ if (key !== 'Enter' && key !== ' ' && key !== 'Spacebar') return;
1318
+ if (typeof event.preventDefault === 'function') event.preventDefault();
1319
+ openAllMachinesPicker(card);
1320
+ });
1321
+ return card;
1322
+ }
1323
+
1324
+ // The popover's anchor, re-resolved because choosing a row re-renders the grid (the node we opened
1325
+ // from is detached) and focus must land on the live card (APG, AC-4c).
1326
+ function _nsMachineAllCardAnchor() {
1327
+ const grid = document.getElementById('ns-machine-grid');
1328
+ if (!grid || typeof grid.querySelectorAll !== 'function') return null;
1329
+ const cards = grid.querySelectorAll('.ns-machine-all');
1330
+ return cards && cards.length ? cards[0] : null;
1331
+ }
1332
+
1333
+ function openAllMachinesPicker(anchor, now) {
1334
+ const picker = _machinePickerComponent();
1335
+ if (!picker || typeof picker.open !== 'function') return null;
1336
+ const at = Number(now == null ? Date.now() : now);
1337
+ return picker.open({
1338
+ anchor: anchor,
1339
+ title: 'Choose machine',
1340
+ tabs: [{
1341
+ id: 'machines',
1342
+ label: 'Machines',
1343
+ searchPlaceholder: 'Search machines',
1344
+ loadRows: function() { return Promise.resolve(_machinePickerGroups(at)); },
1345
+ detail: function(row) { return _machinePickerDetail(row, at); },
1346
+ actions: [{ id: 'choose', label: 'Choose this machine', primary: true, disabled: (row) => !row || !!row.disabled }],
1347
+ onApply: function(row) {
1348
+ // An offline row can never be committed: the hub would have nowhere to put the session.
1349
+ if (!_machinePickerChoose(row)) return Promise.resolve({ ok: false, message: 'That machine cannot be selected.' });
1350
+ return Promise.resolve({ ok: true });
1351
+ },
1352
+ }],
1353
+ onClose: function(reason) {
1354
+ // APG AC-4c: focus returns to the "All machines" card. CtmModelPicker focuses the anchor it was
1355
+ // given, but a pick has re-rendered the grid, so re-resolve the live card and fall back to it.
1356
+ if (reason === 'replaced') return;
1357
+ const back = _nsMachineAllCardAnchor() || anchor;
1358
+ if (back && typeof back.focus === 'function') back.focus();
1359
+ },
1360
+ });
1361
+ }
1362
+
1363
+ // Item 7: the "Manage machines" link under the grid. There is no slot in index.html for it (that
1364
+ // file is WP-5's), so it is created once and mounted as the grid's next sibling — the modal is its
1365
+ // parent, and no markup change is needed to place it.
1366
+ function _nsMachineManageLink() {
1367
+ let link = document.getElementById(MACHINE_MANAGE_ID);
1368
+ if (!link) {
1369
+ link = document.createElement('a');
1370
+ link.id = MACHINE_MANAGE_ID;
1371
+ link.className = 'ns-machine-manage';
1372
+ link.href = MACHINE_MANAGE_HREF;
1373
+ link.textContent = MACHINE_MANAGE_LABEL;
1374
+ link.title = 'Open the Machines page';
1375
+ }
1376
+ return link;
1377
+ }
1378
+
1379
+ function _nsMachineMountManageLink(grid) {
1380
+ const link = _nsMachineManageLink();
1381
+ const parent = grid && grid.parentNode;
1382
+ // Nothing to mount into (a bare DOM stub, or a detached grid): the caller still gets the node so
1383
+ // it can flip its visibility.
1384
+ if (!link || !parent || typeof parent.insertBefore !== 'function') return link;
1385
+ if (link.parentNode !== parent) parent.insertBefore(link, grid.nextSibling || null);
1386
+ return link;
1387
+ }
1388
+
1389
+ function _nsMachineShowManageLink(link, on) {
1390
+ if (link && link.style) link.style.display = on ? '' : 'none';
1391
+ }
1392
+
1393
+ // AC-5a: with nothing renderable the block is removed from the DOM entirely (no grid, no
1394
+ // hidden input value), which is what keeps the zero-machine modal pixel-identical to today.
1395
+ // WP-4 D-1: above MACHINE_PICKER_CARD_LIMIT the grid keeps This Mac + the top three and adds the
1396
+ // "All machines (N)…" card, which is the only way to reach a box that never renders a card.
1397
+ function renderNewSessionMachineCards(machines, now) {
1398
+ const grid = document.getElementById('ns-machine-grid');
1399
+ const label = document.getElementById('ns-machine-label');
1400
+ const hint = document.getElementById('ns-machine-hint');
1401
+ const hidden = document.getElementById('ns-machine');
1402
+ if (!grid || !label || !hidden) return false;
1403
+ const at = Number(now == null ? Date.now() : now);
1404
+ _nsMachineState.machines = (Array.isArray(machines) ? machines : []).filter(m => m && m.instanceId);
1405
+ _nsMachineState.renderedAt = at;
1406
+ const visible = _nsMachineState.machines.filter(m => !m.hidden);
1407
+ // Registry order, filtered to the rows that render — today's grid, unchanged (AC-4a).
1408
+ const views = visible.map(m => machineCardView(m, at)).filter(Boolean);
1409
+ const manage = _nsMachineMountManageLink(grid);
1410
+ if (!views.length) {
1411
+ grid.textContent = '';
1412
+ grid.style.display = 'none';
1413
+ label.style.display = 'none';
1414
+ hidden.value = '';
1415
+ _nsMachineShowManageLink(manage, false);
1416
+ if (hint) { hint.textContent = ''; hint.style.display = 'none'; }
1417
+ return false;
1418
+ }
1419
+ // D-1: the switch to the "All machines" card counts This Mac plus each row that renders a card — an
1420
+ // unreachable box renders none (HUB AC-5a), so below the limit today's grid is unchanged. The card's
1421
+ // label counts every visible row, offline included, so it matches the popover's All group it opens.
1422
+ const total = 1 + views.length;
1423
+ label.style.display = '';
1424
+ grid.style.display = '';
1425
+ grid.textContent = '';
1426
+ grid.appendChild(_nsMachineCardNode({ id: '', name: 'This Mac', health: 'reachable', sub: 'ready', disabled: false }));
1427
+ if (total > MACHINE_PICKER_CARD_LIMIT) {
1428
+ _nsMachineTopViews(machinePickerOrder(_nsMachineState.machines), at).forEach(view => grid.appendChild(_nsMachineCardNode(view)));
1429
+ grid.appendChild(_nsMachineAllCardNode(1 + visible.length));
1430
+ } else {
1431
+ views.forEach(view => grid.appendChild(_nsMachineCardNode(view)));
1432
+ }
1433
+ _nsMachineShowManageLink(manage, true);
1434
+ hidden.value = '';
1435
+ _selectMachineCard('');
1436
+ _nsMachineSetHint('');
1437
+ return true;
1438
+ }
1439
+
1440
+ function _nsMachineSetHint(text, warn) {
1441
+ const hint = document.getElementById('ns-machine-hint');
1442
+ if (!hint) return;
1443
+ hint.textContent = String(text || '');
1444
+ hint.style.display = text ? '' : 'none';
1445
+ hint.classList.toggle('warn', !!warn);
1446
+ }
1447
+
1448
+ function _selectMachineCard(value) {
1449
+ const hidden = document.getElementById('ns-machine');
1450
+ const next = String(value || '');
1451
+ const previous = hidden ? String(hidden.value || '') : '';
1452
+ if (hidden) hidden.value = next;
1453
+ const grid = document.getElementById('ns-machine-grid');
1454
+ if (grid) {
1455
+ grid.querySelectorAll('.ns-machine-card').forEach(function(card) {
1456
+ card.classList.toggle('selected', String(card.dataset.value || '') === next);
1457
+ });
1458
+ }
1459
+ _nsMachineSetHint(next ? 'Sessions will spawn on ' + machineSelectionName() + ' — pick a folder on that box.' : '');
1460
+ // S-13: host-path truth belongs to exactly one machine. Re-run the picker reset on every
1461
+ // change — it rebinds the host probe (which now carries ?machine=) and forgets the previous
1462
+ // machine's project list — so a box answer can never land on a local path field.
1463
+ if (previous !== next && typeof _nsPathReset === 'function') {
1464
+ // The same-repo prefill matches THIS project (the local cwd the user had when they picked
1465
+ // the box), never just any repo both machines happen to share.
1466
+ const cwdInput = document.getElementById('ns-cwd');
1467
+ if (!previous) _nsMachineState.localCwd = cwdInput ? String(cwdInput.value || '').trim() : '';
1468
+ if (typeof _nsPathPickerState !== 'undefined') {
1469
+ _nsPathPickerState.machine = next;
1470
+ _nsPathPickerState.projectsLoaded = false;
1471
+ _nsPathPickerState.projects = [];
1472
+ }
1473
+ _nsPathReset(next ? '' : (typeof getLastSessionCwd === 'function' ? getLastSessionCwd() : ''));
1474
+ }
1475
+ return next;
1476
+ }
1477
+
1478
+ // The ?machine= fragment the picker routes carry (§6.5). '' produces nothing, so every local
1479
+ // request keeps its exact current URL.
1480
+ function machineQueryParam() {
1481
+ const id = machineSelection();
1482
+ return id ? '&machine=' + encodeURIComponent(id) : '';
1483
+ }
1484
+
1485
+ // The header form of the same routing, for POSTs that already own their query string
1486
+ // (/api/path-status). A local selection adds nothing (§6.5: routing is by query/header ONLY,
1487
+ // so no request body ever carries the machine).
1488
+ function machineScopedHeaders(baseHeaders) {
1489
+ const headers = Object.assign({}, baseHeaders || {});
1490
+ const id = machineSelection();
1491
+ if (id) headers['X-CTM-Machine'] = id;
1492
+ return headers;
1493
+ }
1494
+
1495
+ // The key of the local project that contains `cwd` (the longest project path that is cwd or an
1496
+ // ancestor of it), '' when none does. Shared shape with the phone's localProjectKeyFor.
1497
+ function localProjectKeyFor(cwd, keyPath) {
1498
+ const target = String(cwd || '').replace(/\/+$/, '');
1499
+ let best = '';
1500
+ let bestLen = -1;
1501
+ Object.keys(keyPath || {}).forEach(function(key) {
1502
+ const root = String(keyPath[key] || '').replace(/\/+$/, '');
1503
+ if (!root || (target !== root && target.indexOf(root + '/') !== 0)) return;
1504
+ if (root.length > bestLen) { best = key; bestLen = root.length; }
1505
+ });
1506
+ return best;
1507
+ }
1508
+
1509
+ // Local gitRemoteKey -> path, so the same-repo prefill is a plain key equality against the
1510
+ // box's own /api/projects rows. Normalization stays server-side (§6.5).
1511
+ function loadLocalRepoKeys() {
1512
+ if (_nsMachineState.localKeysRequested) return;
1513
+ _nsMachineState.localKeysRequested = true;
1514
+ const token = (typeof state !== 'undefined' && state.token) || '';
1515
+ fetch('/api/projects?token=' + encodeURIComponent(token))
1516
+ .then(function(r) { return r.ok ? r.json() : null; })
1517
+ .then(function(rows) {
1518
+ const map = Object.create(null);
1519
+ (Array.isArray(rows) ? rows : []).forEach(function(row) {
1520
+ if (row && row.path && row.gitRemoteKey) map[String(row.gitRemoteKey)] = row.path;
1521
+ });
1522
+ _nsMachineState.localRepoKeyPath = map;
1523
+ })
1524
+ .catch(function() { /* expected: without the local key list the picker simply browses */ });
1525
+ }
1526
+
1527
+ // Called by _nsPathLoadProjects once the SELECTED machine's projects answer arrives.
1528
+ function nsMachineAfterProjectsLoaded(pickerState) {
1529
+ const machine = machineSelection();
1530
+ if (!machine) return false;
1531
+ const rows = (pickerState && Array.isArray(pickerState.projects)) ? pickerState.projects : [];
1532
+ const localKey = localProjectKeyFor(_nsMachineState.localCwd, _nsMachineState.localRepoKeyPath);
1533
+ const match = localKey ? rows.find(function(row) {
1534
+ return row && row.path && String(row.gitRemoteKey || '') === localKey;
1535
+ }) : null;
1536
+ const input = document.getElementById('ns-cwd');
1537
+ if (!input) return false;
1538
+ if (!match) {
1539
+ _nsMachineSetHint('No checkout on ' + machineSelectionName() + " carries this project's git remote — browse to a folder on that box.", true);
1540
+ return false;
1541
+ }
1542
+ input.value = match.path;
1543
+ if (pickerState) {
1544
+ pickerState.prefillCwd = match.path;
1545
+ pickerState.autoActivePath = match.path;
1546
+ }
1547
+ _nsMachineSetHint('Same repository found on ' + machineSelectionName() + '.');
1548
+ if (typeof _nsPathRenderSuggestions === 'function') _nsPathRenderSuggestions();
1549
+ if (typeof _nsPathUpdateValidation === 'function') _nsPathUpdateValidation();
1550
+ return true;
1551
+ }
1552
+
1553
+ // Opened with the modal, so the machine list is fresh every time. A hub-less (older) server
1554
+ // answers 404 and the modal stays exactly as it was (AC-5a).
1555
+ function loadNewSessionMachines() {
1556
+ // D-1: a fresh modal starts from the plain top three — the popover pick belongs to the session it
1557
+ // was made in, never to the next one.
1558
+ _nsMachineState.pickerMachine = '';
1559
+ renderNewSessionMachineCards([]);
1560
+ const token = (typeof state !== 'undefined' && state.token) || '';
1561
+ fetch('/api/machines?token=' + encodeURIComponent(token))
1562
+ .then(function(r) { return r.ok ? r.json() : null; })
1563
+ .then(function(data) {
1564
+ const shown = renderNewSessionMachineCards(data && data.machines);
1565
+ if (shown) loadLocalRepoKeys();
1566
+ })
1567
+ .catch(function() { /* expected: no /api/machines on this server — nothing to render */ });
1568
+ }
1569
+
1570
+ // Machine health / last-seen labels — the ONE copy lives in public/js/machine-pool-shared.js
1571
+ // (machineHealthLabel / machineLastSeenText), loaded by public/index.html before this file and by
1572
+ // public/m/index.html for the phone; the WP-4 picker cards, the WP-5 machines page and the WP-6
1573
+ // phone page all read that copy.
1574
+
1575
+ // ── AC-6d (client half): "Open on <machine>" on a resume-blocked card ────────────────────────
1576
+ // A resume-blocked wire that is parked on another machine MAY carry `openOnMachine`; the action
1577
+ // sends this session's normal resume, which WP-6 routes to that machine. Absent the field the
1578
+ // card's actions are untouched. The button is appended to the card runtime-20 painted (never a
1579
+ // second renderer) and is idempotent across repaints.
1580
+ // Called by runtime-20's resume-blocked painter on EVERY paint (first paint and remount both rebuild
1581
+ // the actions row from s._resumeBlockedMsg), so the button can never be lost to a repaint.
1582
+ function resumeBlockedOpenOnMachineAction(msg, actions, s) {
1583
+ const target = msg && msg.openOnMachine;
1584
+ if (!target || !target.instanceId || !actions || !s) return false;
1585
+ const previous = actions.querySelector('.machine-open-action');
1586
+ if (previous) previous.remove();
1587
+ const name = target.displayName || target.instanceId;
1588
+ const btn = document.createElement('button');
1589
+ btn.type = 'button';
1590
+ btn.className = 'snapshot-loading-btn machine-open-action';
1591
+ btn.textContent = 'Open on ' + name;
1592
+ btn.title = 'Resume this session on ' + name;
1593
+ btn.onclick = function() {
1594
+ if (typeof _retryBlockedResume !== 'function') return;
1595
+ btn.textContent = 'Opening on ' + name + '…';
1596
+ _retryBlockedResume(s);
1597
+ };
1598
+ actions.appendChild(btn);
1599
+ return true;
1600
+ }