create-walle 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -0,0 +1,798 @@
1
+ 'use strict';
2
+
3
+ // WP-0 (docs/fleet-auto-update-pooled-machines-2026-10-26.html §5.1, §6, §8 WP-0, §9 S-1..S-4):
4
+ // the version truth both sides of the fleet need.
5
+ //
6
+ // * HUB side — `readHubTarget()`: the sha of the hub repo's local `refs/heads/main`, which is the
7
+ // fleet's target (D-1: the user works on local main and never auto-pushes). Memoized for 15 s so
8
+ // a sweep over N boxes runs `git` once, and a git failure reads `''` (never a throw into an HTTP
9
+ // handler).
10
+ // * BOX side — `readFleetStatus()`: what this checkout IS, so a hub can decide eligibility
11
+ // (channel / branch / clean) and so the box's own Machines card can say "current" or "N behind".
12
+ //
13
+ // Eligibility is deliberately NOT a stored setting (lead decision 2026-10-26, dropping doc S-5):
14
+ // a box is eligible iff its checkout is the git channel, on `main`, with a clean tree. There is no
15
+ // `autoUpdate` machine-self flag anywhere in this module or the routes it feeds.
16
+ //
17
+ // Every git call goes through `execFile` (never a shell string, never a `-C` interpolated into a
18
+ // command line) with a 5 s timeout, and `runGit` is injectable so tests drive a real temp repo — or
19
+ // a counter — without a shell. The module owns no timer: the memo is a plain timestamp compare.
20
+
21
+ const { isSystemdService } = require('./systemd-scope');
22
+ const { execFile, spawn } = require('node:child_process');
23
+ // The real launchers again, under a second name: `_spawnFleetApplyDetached`'s parameters are bound to
24
+ // the literal call names (`execFile(...)`/`spawn(...)`) so bin/main-thread-lint.js keeps SEEING both
25
+ // launch sites — a call written through an `*Impl` alias would be invisible to it, and an invisible
26
+ // process launch is exactly what that ledger exists to prevent.
27
+ const defaultExecFile = execFile;
28
+ const defaultSpawn = spawn;
29
+ const fsp = require('node:fs/promises');
30
+ const fs = require('node:fs');
31
+ const http = require('node:http');
32
+ const path = require('node:path');
33
+ // WP-3 (§6.2): the origin half of the status read reuses the landing machine's own readers — the
34
+ // target-tree policy loader and the local tracking-ref status — so the box's card and `land status`
35
+ // can never disagree about ahead/behind or the last sync verdict.
36
+
37
+ /** Planner verdicts (WP-2). Frozen so a completeness test can iterate it. */
38
+ const FLEET_REASONS = Object.freeze([
39
+ 'current',
40
+ 'not_git',
41
+ 'not_main',
42
+ 'dirty',
43
+ 'diverged',
44
+ 'unreachable',
45
+ 'another_box_updating',
46
+ 'already_attempted',
47
+ 'cooldown',
48
+ 'busy',
49
+ 'behind',
50
+ ]);
51
+
52
+ /** The box state machine's states (doc §5.3). A state file naming anything else reads as 'idle'. */
53
+ const FLEET_STATES = Object.freeze(['idle', 'applying', 'waiting_idle', 'restarting', 'rolling_back']);
54
+
55
+ /** The three lockfiles whose change forces a frozen install (D-6); `git diff --name-only` paths. */
56
+ const FLEET_LOCKFILES = Object.freeze([
57
+ 'package-lock.json',
58
+ 'coding-task-manager/package-lock.json',
59
+ 'wall-e/package-lock.json',
60
+ ]);
61
+
62
+ /** 28× the measured worst live gap (2,324,240 B) — the bundle body cap (§9 S-6). */
63
+ const FLEET_BUNDLE_MAX_BYTES = 64 * 1024 * 1024;
64
+
65
+ const GIT_TIMEOUT_MS = 5000;
66
+ /** `git bundle verify` / `git fetch <bundle>` are O(gap size), not O(1); 60 s is the §6 budget. */
67
+ const GIT_BUNDLE_TIMEOUT_MS = 60000;
68
+ const HUB_TARGET_MEMO_MS = 15000;
69
+
70
+ /** The repo root holding `.git` — `lib/` is one level below it. */
71
+ const DEFAULT_REPO_ROOT = path.resolve(__dirname, '..', '..');
72
+
73
+ /**
74
+ * The production `git` runner: `execFile` with the args as an ARRAY (no shell), a 5 s timeout, and
75
+ * stdout trimmed. A non-zero exit rejects, which every caller below catches and reads as "unknown".
76
+ */
77
+ function _execFileGit(args, { cwd, timeout = GIT_TIMEOUT_MS } = {}) {
78
+ return new Promise((resolve, reject) => {
79
+ execFile('git', args, { cwd, timeout }, (error, stdout) => {
80
+ if (error) { reject(error); return; }
81
+ resolve(String(stdout == null ? '' : stdout).trim());
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * One git call reduced to `{ code, stdout, stderr }` — never a throw. `runGit` has two legitimate
88
+ * shapes: the production runner above (rejects on a non-zero exit, with `error.stderr` attached by
89
+ * `execFile`) and a test double that returns stdout. Both are read here, so a caller that must look
90
+ * at an EXIT CODE (`git bundle verify`, `git merge-base --is-ancestor`) does not care which it got.
91
+ */
92
+ async function _runGitResult(runGit, args, cwd, timeout) {
93
+ try {
94
+ const out = await runGit(args, { cwd, timeout });
95
+ if (out && typeof out === 'object' && Number.isInteger(out.code)) {
96
+ return { code: out.code, stdout: String(out.stdout == null ? '' : out.stdout), stderr: String(out.stderr == null ? '' : out.stderr) };
97
+ }
98
+ return { code: 0, stdout: String(out == null ? '' : out), stderr: '' };
99
+ } catch (error) {
100
+ const code = error && Number.isInteger(error.code) ? error.code : 1;
101
+ const stderr = error && (error.stderr != null ? String(error.stderr) : String(error.message || ''));
102
+ return { code, stdout: String((error && error.stdout) || ''), stderr };
103
+ }
104
+ }
105
+
106
+ // Module state by design (same contract as the registry's caches): repoRoot -> { sha, at }. The memo
107
+ // dies with the process, and a stale sha is worthless — it is re-read every 15 s.
108
+ const _hubTarget = new Map();
109
+
110
+ /**
111
+ * D-1: the hub's fleet target — the full sha of its LOCAL `refs/heads/main`.
112
+ *
113
+ * Memoized for 15 s. Any git failure returns `''` and leaves the memo UNset (a zeroed stamp), so the
114
+ * very next call retries instead of serving a failure for the rest of the window. `now` is injectable
115
+ * so a test can move the clock without waiting.
116
+ */
117
+ async function readHubTarget({ repoRoot = DEFAULT_REPO_ROOT, runGit = _execFileGit, now = Date.now } = {}) {
118
+ const at = Number(now()) || 0;
119
+ const memo = _hubTarget.get(repoRoot);
120
+ if (memo && at - memo.at < HUB_TARGET_MEMO_MS) return memo.sha;
121
+ let sha = '';
122
+ try {
123
+ sha = String((await runGit(['rev-parse', '--verify', 'refs/heads/main'], { cwd: repoRoot })) || '').trim();
124
+ } catch {
125
+ // HANDLE: no git, no repo, or no local main — this hub has no target to offer. Report '' rather
126
+ // than a half-truth, and drop the memo so the next read tries again.
127
+ sha = '';
128
+ }
129
+ if (!sha) { _hubTarget.delete(repoRoot); return ''; }
130
+ _hubTarget.set(repoRoot, { sha, at });
131
+ return sha;
132
+ }
133
+
134
+ /**
135
+ * One git read for the status below; a failure degrades THAT field, never the whole response.
136
+ * `runGit` may be the production string runner or a `{ code, stdout, stderr }` one (see
137
+ * `_runGitResult`); both spellings are unwrapped here so a caller can inject either.
138
+ */
139
+ async function _git(runGit, repoRoot, args, fallback) {
140
+ try {
141
+ const out = await runGit(args, { cwd: repoRoot });
142
+ if (out && typeof out === 'object') return String(out.stdout == null ? fallback : out.stdout).trim();
143
+ return out == null ? fallback : String(out).trim();
144
+ } catch {
145
+ return fallback; // HANDLE: a missing/unreadable repo reads as unknown, not as a 500
146
+ }
147
+ }
148
+
149
+ /**
150
+ * S-4: this checkout's fleet-facing truth.
151
+ *
152
+ * channel 'git' iff `git rev-parse --is-inside-work-tree` prints `true`, else 'unknown'. This is
153
+ * the ONLY eligibility fact the lead's decision needs besides branch/clean.
154
+ * head the full sha on disk — differs from `runningSha` until the process restarts onto it.
155
+ * clean `git status --porcelain --untracked-files=no` is empty (untracked files never make a
156
+ * box ineligible: they are not local work the update could overwrite).
157
+ * guard the restart guard's blocking count. Production passes `guardSnapshot` = server.js's
158
+ * `_restartGuardSnapshot` — the exact verdict /api/instance/update-gate and
159
+ * /api/restart/ctm use (hosted sessions + revive preflight included), so the hub and the
160
+ * box can never disagree about idle. The bare summarizer is the unit-test fallback.
161
+ *
162
+ * `state`/`last` are the box state machine's (doc §5.3); WP-0 has no apply path yet, so they read
163
+ * the honest idle/never default.
164
+ *
165
+ * `originSync` (WP-3, docs/fleet-origin-main-sync-skill-2026-10-31.html §5 R-1, §6.2): the origin
166
+ * half of the same truth — `{ configured, follow, ahead, behind, fetchedAt, lastSync }`, read
167
+ * LOCAL-ONLY (a status read never fetches; `land sync` is what moves the tracking ref). A repository
168
+ * whose target tree carries no `landing.publish` block reads exactly `{ configured: false }`: the hub
169
+ * planner turns that fact into the `origin_sync` reason, so a self-syncing box is never bundled to.
170
+ */
171
+ async function readFleetStatus({
172
+ repoRoot = DEFAULT_REPO_ROOT,
173
+ dataDir = '',
174
+ runningSha = '',
175
+ sessions = [],
176
+ runGit = _execFileGit,
177
+ summarizeGuard = require('./restart-guard').summarizeRestartGuard,
178
+ guardSnapshot = null,
179
+ } = {}) {
180
+ const insideWorkTree = await _git(runGit, repoRoot, ['rev-parse', '--is-inside-work-tree'], '');
181
+ const channel = insideWorkTree === 'true' ? 'git' : 'unknown';
182
+ const head = String(await _git(runGit, repoRoot, ['rev-parse', 'HEAD'], '') || '');
183
+ const branch = String(await _git(runGit, repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], '') || '');
184
+ const porcelain = await _git(runGit, repoRoot, ['status', '--porcelain', '--untracked-files=no'], null);
185
+ // null = the status read itself failed. Fail CLOSED (not clean): a box we cannot inspect is never
186
+ // a box we may write over (D-9).
187
+ const clean = porcelain === null ? false : porcelain === '';
188
+ const verdict = (typeof guardSnapshot === 'function' ? guardSnapshot() : summarizeGuard(sessions)) || {};
189
+ const state = await readFleetState(dataDir);
190
+ return {
191
+ channel,
192
+ head,
193
+ runningSha: String(runningSha == null ? '' : runningSha).trim(),
194
+ branch,
195
+ clean,
196
+ guard: { blockingCount: Number(verdict.blockingCount) || 0 },
197
+ state: state.state,
198
+ last: state.last,
199
+ originSync: await readOriginSync(repoRoot, runGit),
200
+ };
201
+ }
202
+
203
+ /**
204
+ * WP-3 (§5 R-1, §6.2): the origin half of `readFleetStatus`. The policy is the `.ctm.json` committed at the
205
+ * checkout's HEAD (a box that reports here is on main); ahead/behind come from the LOCAL tracking ref —
206
+ * this function performs no fetch and writes nothing, and uses only async git/fs (it runs on the request
207
+ * path of GET /api/fleet/status).
208
+ *
209
+ * Every failure collapses to `{ configured: false }`: a directory that is not a repository, a missing
210
+ * target branch, an unreadable/invalid `.ctm.json`, or no `landing.publish` block all mean "this box
211
+ * has no origin contract", which is the honest answer and never a 500 on the status route.
212
+ */
213
+ async function readOriginSync(repoRoot, runGit = _execFileGit) {
214
+ // Async git + fsp only: this runs inside GET /api/fleet/status on CTM's main thread, so it must never
215
+ // block the event loop (the landing CLI's synchronous runner is fine in `land`, not here). Local refs
216
+ // only — never a fetch inside a status read.
217
+ const NOT = { configured: false };
218
+ const raw = await _git(runGit, repoRoot, ['show', 'HEAD:.ctm.json'], '');
219
+ if (!raw) return NOT;
220
+ let landing = null;
221
+ try {
222
+ const parsed = JSON.parse(raw);
223
+ landing = parsed && typeof parsed === 'object' ? parsed.landing : null;
224
+ } catch {
225
+ return NOT; // HANDLE: an unparsable .ctm.json is "not configured", never a 500
226
+ }
227
+ const publish = landing && typeof landing === 'object' ? landing.publish : null;
228
+ if (!publish || typeof publish !== 'object') return NOT;
229
+ const target = typeof landing.target === 'string' && landing.target ? landing.target : 'main';
230
+ const remote = typeof publish.remote === 'string' && publish.remote ? publish.remote : 'origin';
231
+ const branch = typeof publish.branch === 'string' && publish.branch ? publish.branch : 'main';
232
+ const common = await _git(runGit, repoRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'], '');
233
+ let state = {};
234
+ if (common) {
235
+ try {
236
+ const parsed = JSON.parse(await fsp.readFile(path.join(common, 'landing', 'sync-state.json'), 'utf8'));
237
+ if (parsed && typeof parsed === 'object') state = parsed;
238
+ } catch {
239
+ state = {}; // HANDLE: no sync has run yet (or the file is mid-rename) — no fetchedAt/lastSync
240
+ }
241
+ }
242
+ const counts = await _git(runGit, repoRoot,
243
+ ['rev-list', '--left-right', '--count', `refs/heads/${target}...refs/remotes/${remote}/${branch}`], '');
244
+ const [ahead, behind] = String(counts || '').split(/\s+/).map((value) => (value === '' ? NaN : Number(value)));
245
+ return {
246
+ configured: true,
247
+ follow: publish.follow !== false,
248
+ ahead: Number.isFinite(ahead) ? ahead : null,
249
+ behind: Number.isFinite(behind) ? behind : null,
250
+ fetchedAt: state.fetchedAt || null,
251
+ lastSync: state.lastSync || null,
252
+ };
253
+ }
254
+
255
+ // ── WP-1: the box state machine + the apply lock (doc §5.3, §9 S-6/S-7) ─────────────────────────
256
+ //
257
+ // Two files under <dataDir>/fleet carry the box's apply truth, and both are read by readFleetStatus:
258
+ // state.json the state machine the Machines card renders (idle/applying/waiting_idle/restarting/
259
+ // rolling_back) plus the last terminal verdict. Missing or corrupt ⇒ the honest
260
+ // idle/never default — never a throw into an HTTP handler.
261
+ // apply.lock a plain pid lock file (lead decision 2026-10-26, replacing the doc's
262
+ // acquireUpdateLease, which is bound to the npm release layout). `open(…, 'wx')` is
263
+ // the atomic claim; a lock whose pid is dead is stale and is reclaimed.
264
+ //
265
+ // The ROUTE holds the lock across the transfer and releases it just before it spawns the detached
266
+ // helper, which takes the same lock for the apply itself — so "transferring" and "applying" are the
267
+ // same serialization point (D-7), and D-6's one-box-at-a-time is enforced by the kernel, not by a
268
+ // flag.
269
+
270
+ /** `<dataDir>/fleet` — the apply state's one directory. */
271
+ function fleetDir(dataDir) { return path.join(String(dataDir || ''), 'fleet'); }
272
+
273
+ /** `<dataDir>/fleet/state.json` (mode 0600, written atomically). */
274
+ function fleetStatePath(dataDir) { return path.join(fleetDir(dataDir), 'state.json'); }
275
+
276
+ /** `<dataDir>/fleet/apply.lock` — the pid lock file (§3 step 1). */
277
+ function fleetApplyLockPath(dataDir) { return path.join(fleetDir(dataDir), 'apply.lock'); }
278
+
279
+ /**
280
+ * The box state machine as persisted. No `dataDir` (a unit test, or a box that never applied) reads
281
+ * the honest idle/never default; so does a file that is missing, unreadable, not JSON, or names a
282
+ * state this module does not know.
283
+ */
284
+ async function readFleetState(dataDir) {
285
+ if (!dataDir) return { state: 'idle', last: null };
286
+ let parsed = null;
287
+ try {
288
+ parsed = JSON.parse(await fsp.readFile(fleetStatePath(dataDir), 'utf8'));
289
+ } catch {
290
+ // HANDLE: no state file yet, or a torn/unreadable one. "Never applied" is the truthful answer and
291
+ // is what the box reports before its first bundle; a 500 here would break a read-only card.
292
+ return { state: 'idle', last: null };
293
+ }
294
+ if (!parsed || typeof parsed !== 'object') return { state: 'idle', last: null };
295
+ return {
296
+ state: FLEET_STATES.includes(parsed.state) ? parsed.state : 'idle',
297
+ last: parsed.last && typeof parsed.last === 'object' ? parsed.last : null,
298
+ };
299
+ }
300
+
301
+ /** Atomic 0600 write: temp file in the same directory, then `rename` (§3 step 2). */
302
+ async function writeFleetState(dataDir, value) {
303
+ const dir = fleetDir(dataDir);
304
+ await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
305
+ const target = fleetStatePath(dataDir);
306
+ const tmp = path.join(dir, `.state.${process.pid}.${Date.now()}.tmp`);
307
+ await fsp.writeFile(tmp, JSON.stringify(value), { mode: 0o600 });
308
+ await fsp.rename(tmp, target);
309
+ }
310
+
311
+ /** The pid a lock file names, or 0 when it is absent/unreadable/garbage. */
312
+ async function _readLockPid(lockPath) {
313
+ try {
314
+ const pid = Number.parseInt(String(await fsp.readFile(lockPath, 'utf8')).trim(), 10);
315
+ return Number.isInteger(pid) && pid > 0 ? pid : 0;
316
+ } catch {
317
+ return 0; // HANDLE: unreadable lock ⇒ unknown holder; the caller treats pid 0 as dead and reclaims
318
+ }
319
+ }
320
+
321
+ /** Signal 0 probe: EPERM means the pid exists but is not ours — that is still a LIVE holder. */
322
+ function isPidAlive(pid) {
323
+ if (!pid) return false;
324
+ try { process.kill(pid, 0); return true; }
325
+ catch (error) { return !!error && error.code === 'EPERM'; }
326
+ }
327
+
328
+ /**
329
+ * Claim `<dataDir>/fleet/apply.lock`. `open(…, 'wx')` is atomic at the OS level, so two concurrent
330
+ * callers cannot both win; the loser reads the holder's pid and reports `live: true` (a real apply or
331
+ * transfer is in flight). A lock whose holder is DEAD is stale (§3 step 1) and is reclaimed once.
332
+ */
333
+ async function acquireFleetApplyLock(dataDir) {
334
+ const dir = fleetDir(dataDir);
335
+ await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
336
+ const lockPath = fleetApplyLockPath(dataDir);
337
+ for (let attempt = 0; attempt < 2; attempt += 1) {
338
+ try {
339
+ const handle = await fsp.open(lockPath, 'wx', 0o600);
340
+ try { await handle.writeFile(String(process.pid)); } finally { await handle.close(); }
341
+ return { ok: true, path: lockPath, pid: process.pid };
342
+ } catch (error) {
343
+ if (!error || error.code !== 'EEXIST') throw error;
344
+ const pid = await _readLockPid(lockPath);
345
+ if (isPidAlive(pid)) return { ok: false, live: true, pid, path: lockPath };
346
+ // The holder is gone (crash, kill -9, or a torn file): reclaim and retry ONCE.
347
+ try { await fsp.unlink(lockPath); } catch { /* expected: another reclaimer won the race; the retry below reports the truth */ }
348
+ }
349
+ }
350
+ const pid = await _readLockPid(lockPath);
351
+ return { ok: false, live: isPidAlive(pid), pid, path: lockPath };
352
+ }
353
+
354
+ /** Release a claim made by `acquireFleetApplyLock`. Missing is success (idempotent). */
355
+ async function releaseFleetApplyLock(lock) {
356
+ if (!lock || !lock.path) return;
357
+ try { await fsp.unlink(lock.path); }
358
+ catch { /* expected: already released — the stale-reclaim path, or a double release */ }
359
+ }
360
+
361
+ /** An identity-preserving regex test for the `?to=` parameter (a full 40-hex sha). */
362
+ function isFullSha(value) { return typeof value === 'string' && /^[0-9a-f]{40}$/.test(value); }
363
+
364
+ /**
365
+ * Stream a binary request body to `filePath` with a hard byte cap (§9 S-6 — deliberately NOT
366
+ * `_readRawBody` from lib/remote-machine-proxy.js, which decodes the body into a string and is for
367
+ * proxied JSON). Resolves `{ ok: true, bytes }` or `{ ok: false, tooLarge | error }`; on any failure
368
+ * the partial file is unlinked, so a refused bundle never leaves anything behind.
369
+ */
370
+ function _streamBodyToFile(req, filePath, maxBytes) {
371
+ return new Promise((resolve) => {
372
+ const out = fs.createWriteStream(filePath, { mode: 0o600 });
373
+ let bytes = 0;
374
+ let settled = false;
375
+ const settle = (result) => {
376
+ if (settled) return;
377
+ settled = true;
378
+ if (!result.ok) {
379
+ try { out.destroy(); } catch { /* expected: the write already failed and closed the stream */ }
380
+ if (typeof req.destroy === 'function') { try { req.destroy(); } catch { /* expected: the request already ended */ } }
381
+ void fsp.unlink(filePath).catch(() => { /* expected: nothing to unlink — the file was never created */ });
382
+ }
383
+ resolve(result);
384
+ };
385
+ out.on('error', (error) => settle({ ok: false, error, bytes }));
386
+ if (typeof req.on === 'function') {
387
+ req.on('error', (error) => settle({ ok: false, error, bytes }));
388
+ req.on('data', (chunk) => {
389
+ if (settled) return;
390
+ bytes += chunk.length;
391
+ if (bytes > maxBytes) { settle({ ok: false, tooLarge: true, bytes }); return; }
392
+ if (!out.write(chunk)) req.pause();
393
+ });
394
+ out.on('drain', () => { if (!settled && typeof req.resume === 'function') req.resume(); });
395
+ req.on('end', () => { if (!settled) out.end(() => settle({ ok: true, bytes })); });
396
+ }
397
+ });
398
+ }
399
+
400
+ /** The detached helper's log file: one per target, so a failed apply is diagnosable after the fact. */
401
+ function fleetApplyLogPath(dataDir, to) { return path.join(fleetDir(dataDir), `apply-${String(to).slice(0, 12)}.log`); }
402
+
403
+ /** `systemd-run` is a launcher, not the work: if it does not answer quickly, fall back (below). */
404
+ const SYSTEMD_RUN_TIMEOUT_MS = 10000;
405
+
406
+ /**
407
+ * The transient user unit the helper runs under on a systemd host (`ctm-fleet-apply-<to12>`), built as
408
+ * one argv array so the whole command is auditable and nothing goes through a shell (S-6).
409
+ *
410
+ * `local: true` is §8 WP-2b (`land sync` moved THIS box's own main): the flag goes BEFORE the sha, in
411
+ * the helper's own positional contract (`fleet-apply.js [--local] <to-sha>`).
412
+ */
413
+ function systemdRunArgv(to, { repoRoot, dataDir, port, logPath, local = false }) {
414
+ return [
415
+ '--user',
416
+ '--collect',
417
+ '--quiet',
418
+ `--unit=ctm-fleet-apply-${String(to).slice(0, 12)}`,
419
+ `--working-directory=${repoRoot}`,
420
+ `--setenv=CTM_PORT=${port == null ? '' : port}`,
421
+ `--setenv=CTM_DATA_DIR=${dataDir}`,
422
+ '-p', `StandardOutput=append:${logPath}`,
423
+ '-p', `StandardError=append:${logPath}`,
424
+ process.execPath,
425
+ path.join(repoRoot, 'bin', 'fleet-apply.js'),
426
+ ...(local ? ['--local'] : []),
427
+ to,
428
+ ];
429
+ }
430
+
431
+ /**
432
+ * Default `spawnHelper`: start `bin/fleet-apply.js <to>` so it outlives the CTM restart it triggers
433
+ * (D-4), with stdout/stderr on the per-target log file (the pattern lib/api/worktrees.js:193-215 uses
434
+ * for the npx updater). `CTM_PORT`/`CTM_DATA_DIR` are handed to the child so it polls the box it was
435
+ * spawned by.
436
+ *
437
+ * TWO LAUNCH SHAPES, because "detached" means different things to the two supervisors:
438
+ * * systemd (Linux service — measured on the DGX Spark 2026-10-27): `ctm.service` is
439
+ * `KillMode=control-group` / `Restart=on-failure` and EVERY descendant — session hosts, Wall-E,
440
+ * agents — lives in that unit's cgroup. CTM's restart is exit 42, so systemd stops the unit and
441
+ * reaps the WHOLE cgroup: a `detached: true` child is killed mid-apply, before step 7 can confirm
442
+ * or roll back, and the box is left saying `restarting` forever. A transient `systemd-run --user`
443
+ * unit is not in `ctm.service`'s cgroup, so it survives the restart and `--collect` cleans it up.
444
+ * * launchd / anything else: `detached: true` already puts the child in its own process group, which
445
+ * nothing here reaps. Unchanged.
446
+ *
447
+ * A `systemd-run` that cannot start (non-zero exit, ENOENT) is not fatal: we say so in the log and
448
+ * fall back to the detached child, which is still strictly better than not applying at all.
449
+ * `platform`/`env`/`execFileImpl`/`spawnImpl` are injectable so tests pin the exact argv.
450
+ */
451
+ async function _spawnFleetApplyDetached(to, {
452
+ repoRoot,
453
+ dataDir,
454
+ port,
455
+ platform = process.platform,
456
+ env = process.env,
457
+ execFileImpl: execFile = defaultExecFile,
458
+ spawnImpl: spawn = defaultSpawn,
459
+ local = false,
460
+ } = {}) {
461
+ await fsp.mkdir(fleetDir(dataDir), { recursive: true, mode: 0o700 });
462
+ const logPath = fleetApplyLogPath(dataDir, to);
463
+
464
+ // §8 WP-2b — the LOCAL variant (`land sync` moved this box's own main; `--local` before the sha).
465
+ // A SEPARATE BRANCH, deliberately: the two launch shapes below keep their exact text, because
466
+ // bin/main-thread-lint.js keys its process-launch exemptions on the whole source line. Both shapes
467
+ // exist here for the same reason they exist below (see the block comment on this function): on a
468
+ // systemd host a `detached: true` child dies with `ctm.service`'s cgroup mid-apply.
469
+ if (local) {
470
+ if (platform === 'linux' && env && env.INVOCATION_ID) {
471
+ const outcome = await new Promise((resolve) => {
472
+ execFile('systemd-run', systemdRunArgv(to, { repoRoot, dataDir, port, logPath, local: true }),
473
+ { timeout: SYSTEMD_RUN_TIMEOUT_MS },
474
+ (error) => resolve(error || null));
475
+ });
476
+ if (!outcome) return;
477
+ const why = Number.isInteger(outcome.code) ? `exit ${outcome.code}` : String(outcome.message || outcome);
478
+ await fsp.appendFile(logPath,
479
+ `[fleet-update] systemd-run could not start the local apply unit (${why}); falling back to a detached child\n`,
480
+ { mode: 0o600 }).catch(() => { /* expected: an unwritable log must not swallow the apply */ });
481
+ }
482
+ const localHandle = await fsp.open(logPath, 'a', 0o600);
483
+ try {
484
+ const child = spawn(process.execPath, [path.join(repoRoot, 'bin', 'fleet-apply.js'), '--local', to], {
485
+ cwd: repoRoot,
486
+ detached: true,
487
+ stdio: ['ignore', localHandle.fd, localHandle.fd],
488
+ env: { ...env, CTM_PORT: String(port == null ? '' : port), CTM_DATA_DIR: String(dataDir) },
489
+ });
490
+ child.unref();
491
+ } finally {
492
+ await localHandle.close();
493
+ }
494
+ return;
495
+ }
496
+
497
+ // INVOCATION_ID is set by systemd for the unit's own processes: its presence is what proves we are
498
+ // a service rather than a human's shell on a Linux box.
499
+ if (isSystemdService({ platform, env })) {
500
+ const outcome = await new Promise((resolve) => {
501
+ execFile('systemd-run', systemdRunArgv(to, { repoRoot, dataDir, port, logPath }),
502
+ { timeout: SYSTEMD_RUN_TIMEOUT_MS },
503
+ (error) => resolve(error || null));
504
+ });
505
+ if (!outcome) return;
506
+ const why = Number.isInteger(outcome.code) ? `exit ${outcome.code}` : String(outcome.message || outcome);
507
+ await fsp.appendFile(logPath,
508
+ `[fleet-update] systemd-run could not start the apply unit (${why}); falling back to a detached child\n`,
509
+ { mode: 0o600 }).catch(() => { /* expected: an unwritable log must not swallow the apply */ });
510
+ }
511
+
512
+ const handle = await fsp.open(logPath, 'a', 0o600);
513
+ try {
514
+ const child = spawn(process.execPath, [path.join(repoRoot, 'bin', 'fleet-apply.js'), to], {
515
+ cwd: repoRoot,
516
+ detached: true,
517
+ stdio: ['ignore', handle.fd, handle.fd],
518
+ env: { ...env, CTM_PORT: String(port == null ? '' : port), CTM_DATA_DIR: String(dataDir) },
519
+ });
520
+ child.unref();
521
+ } finally {
522
+ await handle.close();
523
+ }
524
+ }
525
+
526
+ /**
527
+ * §5 R-7 / §8 WP-2b: the outcomes `scheduleLocalApply` can report, frozen so a test can iterate the
528
+ * whole set and a reader can tell "refused on purpose" from "silently did nothing".
529
+ */
530
+ const FLEET_LOCAL_RESTART_REASONS = Object.freeze([
531
+ 'no_move', // the sync did not move main (pushed, in_sync, …) — nothing to converge onto
532
+ 'no_env', // CTM_DATA_DIR / CTM_PORT unset: not a CTM-managed box (a human's shell, a test)
533
+ 'dev_instance', // CTM_INSTANCE_TAG starts with `dev-`: a dev instance never restarts itself
534
+ 'foreign_repo', // the synced repository is not the one this launcher ships in (see below)
535
+ 'already_current',// /readyz already reports a build that is this sha
536
+ 'self_restart_off',// this clone's `git config ctm.fleetSelfRestart false`: the box restarts by hand only
537
+ 'spawned', // the helper was launched; it owns the idle verdict, the restart and readyz
538
+ 'spawn_failed', // the launch itself threw — reported, never retried here
539
+ ]);
540
+
541
+ // User ruling 2026-10-31: "for fleet apply, don't restart hub ctm, you can restart the ctm in other
542
+ // boxes". Which box is the hub is a per-machine fact, not repository policy (`.ctm.json` is the same
543
+ // on every box), so it lives in the clone's own git config — the same place the ledger merge drivers
544
+ // live: `git config ctm.fleetSelfRestart false` in the hub's main checkout. Unset (every other box)
545
+ // or any value git does not read as false keeps today's behaviour: the box restarts itself when idle.
546
+ const SELF_RESTART_CONFIG_KEY = 'ctm.fleetSelfRestart';
547
+
548
+ /** True unless `repoRoot`'s git config sets `ctm.fleetSelfRestart` to a false boolean. */
549
+ async function selfRestartAllowed(repoRoot, { runGit = _execFileGit } = {}) {
550
+ const out = await _runGitResult(runGit, ['config', '--type=bool', '--get', SELF_RESTART_CONFIG_KEY], repoRoot);
551
+ // exit 1 = unset; any other failure (a value git cannot parse as a bool) also keeps the default.
552
+ if (out.code !== 0) return true;
553
+ return out.stdout.trim() !== 'false';
554
+ }
555
+
556
+ /** `/readyz` is a local HTTP call inside a sync: 5 s is generous, and a miss means "spawn". */
557
+ const READYZ_TIMEOUT_MS = 5000;
558
+
559
+ /**
560
+ * `GET 127.0.0.1:<port>/readyz` — the identity of the build this box is RUNNING (`build.sha`).
561
+ * Anything unreadable (refused, timed out, not JSON, non-200) resolves `{ ok: false }`, which the
562
+ * caller reads as "spawn": the helper re-reads the box's own state and is the only place that can
563
+ * decide to give up, so a failed probe must never be the reason a box stays stale.
564
+ */
565
+ function defaultReadyzProbe(port) {
566
+ return new Promise((resolve) => {
567
+ const request = http.get({ host: '127.0.0.1', port: Number(port), path: '/readyz', timeout: READYZ_TIMEOUT_MS }, (response) => {
568
+ let body = '';
569
+ response.setEncoding('utf8');
570
+ response.on('data', (chunk) => { body += chunk; });
571
+ response.on('end', () => resolve({ ok: response.statusCode === 200, body }));
572
+ });
573
+ request.on('timeout', () => { request.destroy(); resolve({ ok: false, body: '' }); });
574
+ request.on('error', () => resolve({ ok: false, body: '' }));
575
+ });
576
+ }
577
+
578
+ /** The running build's sha, or `''` for a readyz this code cannot read. */
579
+ function runningShaFrom(response) {
580
+ if (!response || !response.ok || !response.body) return '';
581
+ try {
582
+ const parsed = JSON.parse(response.body);
583
+ const sha = parsed && parsed.build && parsed.build.sha;
584
+ return typeof sha === 'string' ? sha.trim() : '';
585
+ } catch (error) {
586
+ /* expected: a non-JSON /readyz body proves nothing about the running build — the caller then
587
+ spawns, and the helper's own readyz check is the authority */
588
+ return '';
589
+ }
590
+ }
591
+
592
+ /**
593
+ * `realpath` or `null`: a path that does not exist cannot be the repository that ships
594
+ * fleet-apply.js. Async on purpose — `land sync` must not block its own event loop on the filesystem.
595
+ */
596
+ async function realRepoPath(value) {
597
+ if (!value) return null;
598
+ try {
599
+ return await fsp.realpath(value);
600
+ } catch (error) {
601
+ /* expected: a missing/unreadable path is "not my repository", never a throw into a sync */
602
+ return null;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * The MAIN worktree of the repository `dir` belongs to (realpath), or null. A linked worktree's `.git` is
608
+ * a file `gitdir: <common>/worktrees/<name>`, so its main worktree is three levels above that path; a main
609
+ * checkout's `.git` is a directory. `land sync` usually runs from an agent's linked worktree, so guard (c)
610
+ * must compare main worktree to main worktree, never a worktree path to the main checkout.
611
+ */
612
+ async function mainWorktreeOf(dir) {
613
+ const real = await realRepoPath(dir);
614
+ if (!real) return null;
615
+ let stat;
616
+ try {
617
+ stat = await fsp.stat(path.join(real, '.git'));
618
+ } catch (error) {
619
+ /* expected: no .git here means "not a repository root", which guard (c) reports as foreign_repo */
620
+ return null;
621
+ }
622
+ if (stat.isDirectory()) return real;
623
+ let text;
624
+ try {
625
+ text = await fsp.readFile(path.join(real, '.git'), 'utf8');
626
+ } catch (error) {
627
+ /* expected: an unreadable .git file is "not my repository" */
628
+ return null;
629
+ }
630
+ const match = /^gitdir:\s*(.+)$/m.exec(text);
631
+ if (!match) return null;
632
+ const gitdir = path.resolve(real, match[1].trim());
633
+ return realRepoPath(path.resolve(gitdir, '..', '..', '..'));
634
+ }
635
+
636
+ /**
637
+ * §5 R-7 / §8 WP-2b: after `land sync` moved THIS box's own main (pulled or published), schedule ONE
638
+ * idle restart onto the new sha — `bin/fleet-apply.js --local <to>` detached through the launcher
639
+ * above, so the box converges onto its own build without waiting for a hub bundle.
640
+ *
641
+ * SPAWN ONLY when all four guards hold, else `{ spawned: false, reason }` (the reasons above):
642
+ * (a) `CTM_DATA_DIR` and `CTM_PORT` are set — a CTM-managed box, not a human's shell or a test;
643
+ * (b) `CTM_INSTANCE_TAG` does not start with `dev-` — a dev instance never restarts itself;
644
+ * (c) the synced repo's MAIN worktree (the git common dir's parent, resolved) IS the repository
645
+ * this launcher ships in. This is the safety rule that keeps a scratch repo in a test — or
646
+ * another checkout of the same code — from ever restarting a real CTM;
647
+ * (d) `/readyz` does not already report a build that is `to` (an unreadable readyz ⇒ spawn: the
648
+ * helper handles a box that is mid-restart, and refusing there would strand it).
649
+ *
650
+ * `probe` and `spawner` are injectable so the guards are testable without a server or a process.
651
+ */
652
+ async function scheduleLocalApply({
653
+ to,
654
+ syncedRepoRoot = null,
655
+ env = process.env,
656
+ probe = defaultReadyzProbe,
657
+ spawner = null,
658
+ repoRoot = DEFAULT_REPO_ROOT,
659
+ local = true,
660
+ readSelfRestart = selfRestartAllowed,
661
+ } = {}) {
662
+ const refuse = (reason) => ({ spawned: false, reason });
663
+ const dataDir = env ? env.CTM_DATA_DIR : null;
664
+ const port = env ? env.CTM_PORT : null;
665
+ if (!dataDir || !port) return refuse('no_env');
666
+ if (String(env.CTM_INSTANCE_TAG || '').startsWith('dev-')) return refuse('dev_instance');
667
+ const mine = await mainWorktreeOf(repoRoot);
668
+ const synced = await realRepoPath(syncedRepoRoot);
669
+ if (!mine || synced !== mine) return refuse('foreign_repo');
670
+ if (!(await readSelfRestart(mine))) return refuse('self_restart_off');
671
+
672
+ let ready = null;
673
+ try {
674
+ ready = await probe(String(port));
675
+ } catch (error) {
676
+ /* expected: a throwing probe is an unreadable readyz — spawn, and let the helper be the judge */
677
+ ready = null;
678
+ }
679
+ const running = runningShaFrom(ready);
680
+ if (running && String(to).startsWith(running)) return refuse('already_current');
681
+
682
+ try {
683
+ const spawnFn = typeof spawner === 'function'
684
+ ? spawner
685
+ // The helper must be the MAIN checkout's copy: it restarts the CTM that checkout runs, and its own
686
+ // HEAD-is-main check is against the checkout it lives in (a worktree copy would refuse not_main).
687
+ : (sha) => _spawnFleetApplyDetached(sha, { repoRoot: mine, dataDir, port, local });
688
+ await spawnFn(to);
689
+ return { spawned: true, reason: 'spawned' };
690
+ } catch (error) {
691
+ return { spawned: false, reason: `spawn_failed: ${error.message}` };
692
+ }
693
+ }
694
+
695
+ /**
696
+ * POST /api/fleet/bundle (§6, §9 S-6): accept ONE fast-forward bundle, verify it, park it in
697
+ * `refs/fleet/incoming`, and hand the apply to the detached helper. Never merges anything itself —
698
+ * D-4's "the box applies" is the helper's job, so this route's worst case is a refused ref.
699
+ *
700
+ * Returns `{ status, body }`; the caller writes it. Every refusal unlinks the received file, so a
701
+ * rejected bundle leaves no trace beyond the ref it never moved.
702
+ */
703
+ async function acceptFleetBundle(req, {
704
+ to,
705
+ repoRoot = DEFAULT_REPO_ROOT,
706
+ dataDir = '',
707
+ port = null,
708
+ runGit = _execFileGit,
709
+ spawnHelper = null,
710
+ maxBytes = FLEET_BUNDLE_MAX_BYTES,
711
+ } = {}) {
712
+ if (!isFullSha(to)) return { status: 400, body: { error: 'fleet_bad_target' } };
713
+ if (!dataDir) return { status: 500, body: { error: 'fleet_no_data_dir' } };
714
+
715
+ // The transfer holds the SAME lock the helper takes, so a second bundle cannot start while one is
716
+ // in flight (D-7) — and a lock whose holder is dead is reclaimed here, not by a human.
717
+ const lock = await acquireFleetApplyLock(dataDir);
718
+ if (!lock.ok) return { status: 409, body: { error: 'fleet_refused', reason: 'in_progress' } };
719
+
720
+ const bundlePath = path.join(fleetDir(dataDir), 'incoming', `${to}.bundle`);
721
+ let handedOff = false;
722
+ try {
723
+ const status = await readFleetStatus({ repoRoot, runGit });
724
+ if (status.channel !== 'git') return { status: 409, body: { error: 'fleet_refused', reason: 'not_git' } };
725
+ if (status.branch !== 'main') return { status: 409, body: { error: 'fleet_refused', reason: 'not_main' } };
726
+ if (!status.clean) return { status: 409, body: { error: 'fleet_refused', reason: 'dirty' } };
727
+
728
+ await fsp.mkdir(path.dirname(bundlePath), { recursive: true, mode: 0o700 });
729
+ const streamed = await _streamBodyToFile(req, bundlePath, maxBytes);
730
+ if (!streamed.ok) {
731
+ if (streamed.tooLarge) return { status: 413, body: { error: 'fleet_bundle_too_large' } };
732
+ return { status: 400, body: { error: 'fleet_bundle_invalid' } };
733
+ }
734
+
735
+ // A bundle that needs a prerequisite this box does not hold is the DIVERGED case the hub must
736
+ // show as such; anything else is simply not a bundle we can use.
737
+ const verify = await _runGitResult(runGit, ['bundle', 'verify', bundlePath], repoRoot, GIT_BUNDLE_TIMEOUT_MS);
738
+ if (verify.code !== 0) {
739
+ if (/prerequisite/i.test(verify.stderr || '')) return { status: 409, body: { error: 'fleet_refused', reason: 'diverged' } };
740
+ return { status: 400, body: { error: 'fleet_bundle_invalid' } };
741
+ }
742
+
743
+ const fetched = await _runGitResult(runGit, ['fetch', bundlePath, '+refs/heads/main:refs/fleet/incoming'], repoRoot, GIT_BUNDLE_TIMEOUT_MS);
744
+ if (fetched.code !== 0) return { status: 400, body: { error: 'fleet_bundle_invalid' } };
745
+ const incoming = String(await _git(runGit, repoRoot, ['rev-parse', 'refs/fleet/incoming'], '') || '').trim();
746
+ if (incoming !== to) return { status: 400, body: { error: 'fleet_bundle_target_mismatch' } };
747
+
748
+ // D-9: fast-forward only. A `to` this box's HEAD is not an ancestor of is never applied.
749
+ const ancestor = await _runGitResult(runGit, ['merge-base', '--is-ancestor', 'HEAD', to], repoRoot, GIT_TIMEOUT_MS);
750
+ if (ancestor.code !== 0) return { status: 409, body: { error: 'fleet_refused', reason: 'diverged' } };
751
+
752
+ // Hand the lock to the helper (it re-claims it as its own pid) before the detached spawn, so the
753
+ // child never races the route for its own lock file.
754
+ await releaseFleetApplyLock(lock);
755
+ handedOff = true;
756
+ const spawnFn = typeof spawnHelper === 'function'
757
+ ? spawnHelper
758
+ : (sha) => _spawnFleetApplyDetached(sha, { repoRoot, dataDir, port });
759
+ await spawnFn(to);
760
+ return { status: 202, body: { accepted: true, to } };
761
+ } finally {
762
+ if (!handedOff) {
763
+ await releaseFleetApplyLock(lock);
764
+ await fsp.unlink(bundlePath).catch(() => { /* expected: a 400/409 refused before the body was written */ });
765
+ }
766
+ }
767
+ }
768
+
769
+ function _resetFleetUpdateStateForTests() { _hubTarget.clear(); }
770
+
771
+ module.exports = {
772
+ _resetFleetUpdateStateForTests,
773
+ _spawnFleetApplyDetached,
774
+ defaultReadyzProbe,
775
+ runningShaFrom,
776
+ scheduleLocalApply,
777
+ mainWorktreeOf,
778
+ systemdRunArgv,
779
+ FLEET_BUNDLE_MAX_BYTES,
780
+ FLEET_LOCKFILES,
781
+ FLEET_LOCAL_RESTART_REASONS,
782
+ SELF_RESTART_CONFIG_KEY,
783
+ selfRestartAllowed,
784
+ FLEET_REASONS,
785
+ FLEET_STATES,
786
+ acceptFleetBundle,
787
+ acquireFleetApplyLock,
788
+ fleetApplyLockPath,
789
+ fleetDir,
790
+ fleetStatePath,
791
+ isFullSha,
792
+ isPidAlive,
793
+ readFleetState,
794
+ readFleetStatus,
795
+ readHubTarget,
796
+ releaseFleetApplyLock,
797
+ writeFleetState,
798
+ };