create-walle 0.10.1 → 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 (681) hide show
  1. package/README.md +7 -1
  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 +11 -8
  6. package/package.json +1 -1
  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 +55 -29
  10. package/template/coding-task-manager/bin/claude-hook.js +62 -4
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  18. package/template/coding-task-manager/bin/land.js +2069 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/memory-census.js +4 -2
  21. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  22. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  23. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  24. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  25. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  26. package/template/coding-task-manager/db.js +860 -317
  27. package/template/coding-task-manager/git-utils.js +44 -0
  28. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  29. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  30. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  31. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  32. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  33. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  34. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  35. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  36. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  37. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  38. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -7
  39. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  40. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  41. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  42. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  43. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  44. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  45. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  46. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  47. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  48. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  49. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  50. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  51. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  52. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  53. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  54. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  55. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  56. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  57. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  58. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  59. package/template/coding-task-manager/lib/agent-presets.js +99 -1
  60. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  61. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  62. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  63. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  64. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  65. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  66. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  67. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  68. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  69. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  70. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  71. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  72. package/template/coding-task-manager/lib/api/ingress.js +43 -12
  73. package/template/coding-task-manager/lib/api/landing.js +403 -0
  74. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  75. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  76. package/template/coding-task-manager/lib/api/machines.js +1150 -0
  77. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  78. package/template/coding-task-manager/lib/api/misc-ops.js +25 -5
  79. package/template/coding-task-manager/lib/api/providers.js +53 -6
  80. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  81. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  82. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  83. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  84. package/template/coding-task-manager/lib/api/sessions.js +237 -59
  85. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  86. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  87. package/template/coding-task-manager/lib/api/setup.js +829 -10
  88. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  89. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  90. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  91. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  92. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  93. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  94. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  95. package/template/coding-task-manager/lib/auth-context.js +32 -2
  96. package/template/coding-task-manager/lib/auth-rate-limit.js +64 -1
  97. package/template/coding-task-manager/lib/auth-rules.js +474 -62
  98. package/template/coding-task-manager/lib/background-stop.js +369 -0
  99. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  100. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  101. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  102. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  103. package/template/coding-task-manager/lib/broadcast-serialize.js +7 -1
  104. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  105. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  106. package/template/coding-task-manager/lib/build-info.js +4 -2
  107. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  108. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  109. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  110. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  111. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  112. package/template/coding-task-manager/lib/client-diag-handler.js +69 -2
  113. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  114. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  115. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  116. package/template/coding-task-manager/lib/codex-harness-qualification.js +55 -20
  117. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  118. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  119. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  120. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  121. package/template/coding-task-manager/lib/coding-agent-models.js +48 -42
  122. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  123. package/template/coding-task-manager/lib/coding-session-model-adapters.js +70 -4
  124. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  125. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  126. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  127. package/template/coding-task-manager/lib/coding-session-model-runtime.js +24 -6
  128. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  129. package/template/coding-task-manager/lib/coding-session-model-state.js +17 -0
  130. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  131. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  132. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  133. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  134. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  135. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  136. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  137. package/template/coding-task-manager/lib/create/resolve-launch-params.js +104 -13
  138. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  139. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  140. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  141. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  142. package/template/coding-task-manager/lib/db-domain-tables.js +8 -0
  143. package/template/coding-task-manager/lib/db-domains.js +22 -2
  144. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  145. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  146. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  147. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  148. package/template/coding-task-manager/lib/db-schema-source-files.js +6 -0
  149. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  150. package/template/coding-task-manager/lib/feature-manifest.js +5 -1
  151. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  152. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  153. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  154. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  155. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  156. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  157. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  158. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  159. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  160. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  161. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  162. package/template/coding-task-manager/lib/fs-watcher.js +61 -44
  163. package/template/coding-task-manager/lib/harness-history-transition.js +52 -11
  164. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  165. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  166. package/template/coding-task-manager/lib/headless-term-service.js +139 -25
  167. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  168. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  169. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  170. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  171. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  172. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  173. package/template/coding-task-manager/lib/host-handoff.js +50 -15
  174. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  175. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  176. package/template/coding-task-manager/lib/host-stats.js +475 -0
  177. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  178. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  179. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  180. package/template/coding-task-manager/lib/hosted-sync-worker.js +141 -310
  181. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  182. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  183. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  184. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  185. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  186. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  187. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  188. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +12 -0
  189. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  190. package/template/coding-task-manager/lib/instance-manifest.js +18 -2
  191. package/template/coding-task-manager/lib/instance-sync-status.js +33 -8
  192. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  193. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  194. package/template/coding-task-manager/lib/landing/config.js +536 -0
  195. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  196. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  197. package/template/coding-task-manager/lib/landing/git.js +344 -0
  198. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  199. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  200. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  201. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  202. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  203. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  204. package/template/coding-task-manager/lib/landing/state.js +263 -0
  205. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  206. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  207. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  208. package/template/coding-task-manager/lib/landing/train.js +885 -0
  209. package/template/coding-task-manager/lib/landing/window.js +57 -0
  210. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  211. package/template/coding-task-manager/lib/launch-model-override.js +193 -8
  212. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  213. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  214. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  215. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  216. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  217. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  218. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +409 -72
  219. package/template/coding-task-manager/lib/lead-model-switch.js +115 -10
  220. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  221. package/template/coding-task-manager/lib/lead-model-transition-store.js +85 -9
  222. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  223. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  224. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  225. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  226. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  227. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  228. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  229. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  230. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  231. package/template/coding-task-manager/lib/machine-registry.js +1494 -0
  232. package/template/coding-task-manager/lib/machine-self.js +66 -0
  233. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  234. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +485 -57
  235. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  236. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  237. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  238. package/template/coding-task-manager/lib/media-journal.js +84 -27
  239. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  240. package/template/coding-task-manager/lib/memory-join.js +337 -0
  241. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  242. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  243. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  244. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  245. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  246. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  247. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  248. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  249. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  250. package/template/coding-task-manager/lib/model-availability.js +727 -0
  251. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  252. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  253. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  254. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +8 -0
  255. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  256. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +6 -0
  257. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  258. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  259. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  260. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  261. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  262. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  263. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  264. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  265. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  266. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  267. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  268. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  269. package/template/coding-task-manager/lib/model-proxy/tokens.js +147 -5
  270. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +18 -0
  271. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  272. package/template/coding-task-manager/lib/native-history-control.js +7 -5
  273. package/template/coding-task-manager/lib/native-lead-settings-client.js +46 -5
  274. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  275. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +776 -75
  276. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  277. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  278. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  279. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  280. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  281. package/template/coding-task-manager/lib/owner-action-manifest-data.json +15 -1
  282. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  283. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  284. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  285. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  286. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  287. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  288. package/template/coding-task-manager/lib/project-config.js +7 -0
  289. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  290. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  291. package/template/coding-task-manager/lib/provider-login-broker.js +264 -24
  292. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  293. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  294. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  295. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  296. package/template/coding-task-manager/lib/read-pool-client.js +94 -4
  297. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  298. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  299. package/template/coding-task-manager/lib/recent-catalog-query.js +41 -0
  300. package/template/coding-task-manager/lib/recent-session-catalog-response.js +76 -10
  301. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  302. package/template/coding-task-manager/lib/relay-client.js +57 -3
  303. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  304. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  305. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  306. package/template/coding-task-manager/lib/remote-machine-catalog.js +1357 -0
  307. package/template/coding-task-manager/lib/remote-machine-create.js +1233 -0
  308. package/template/coding-task-manager/lib/remote-machine-link.js +1426 -0
  309. package/template/coding-task-manager/lib/remote-machine-proxy.js +654 -0
  310. package/template/coding-task-manager/lib/remote-session-forward.js +364 -0
  311. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  312. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  313. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  314. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  315. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  316. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  317. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  318. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  319. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  320. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  321. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  322. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  323. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  324. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  325. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  326. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  327. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  328. package/template/coding-task-manager/lib/review-repo-registry.js +74 -18
  329. package/template/coding-task-manager/lib/root-delivery-runtime.js +74 -17
  330. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  331. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  332. package/template/coding-task-manager/lib/root-prompt-flow.js +168 -34
  333. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  334. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  335. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  336. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  337. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  338. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  339. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  340. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  341. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  342. package/template/coding-task-manager/lib/session-create/delegate-worker.js +179 -20
  343. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  344. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  345. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  346. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  347. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  348. package/template/coding-task-manager/lib/session-create/prepared.js +201 -9
  349. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  350. package/template/coding-task-manager/lib/session-create/provider-process.js +193 -64
  351. package/template/coding-task-manager/lib/session-create/response.js +37 -19
  352. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  353. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  354. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  355. package/template/coding-task-manager/lib/session-create/transcript.js +14 -5
  356. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  357. package/template/coding-task-manager/lib/session-create/worker-continuation.js +50 -2
  358. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  359. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  360. package/template/coding-task-manager/lib/session-create/worker-observability.js +101 -9
  361. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  362. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  363. package/template/coding-task-manager/lib/session-empty.js +13 -10
  364. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  365. package/template/coding-task-manager/lib/session-host-manager.js +212 -206
  366. package/template/coding-task-manager/lib/session-jobs.js +20 -10
  367. package/template/coding-task-manager/lib/session-lifecycle.js +28 -4
  368. package/template/coding-task-manager/lib/session-model-action-preconditions.js +15 -2
  369. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  370. package/template/coding-task-manager/lib/session-park.js +738 -0
  371. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  372. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  373. package/template/coding-task-manager/lib/session-standup.js +67 -4
  374. package/template/coding-task-manager/lib/session-status-ledger.js +147 -6
  375. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  376. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  377. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  378. package/template/coding-task-manager/lib/session-transport.js +17 -0
  379. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  380. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  381. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  382. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  383. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  384. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  385. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  386. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  387. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  388. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  389. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +25 -23
  390. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  391. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  392. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  393. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  394. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  395. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  396. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  397. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  398. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  399. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  400. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  401. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  402. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  403. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  404. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  405. package/template/coding-task-manager/lib/transcript-frame-cache.js +20 -5
  406. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  407. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  408. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  409. package/template/coding-task-manager/lib/transcript-source.js +44 -7
  410. package/template/coding-task-manager/lib/transcript-store.js +49 -0
  411. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  412. package/template/coding-task-manager/lib/typing-interaction-trace.js +198 -3
  413. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  414. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  415. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  416. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  417. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  418. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  419. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  420. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  421. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  422. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  423. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  424. package/template/coding-task-manager/lib/wake-skip-reasons.js +47 -0
  425. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  426. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  427. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  428. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  429. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  430. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  431. package/template/coding-task-manager/lib/walle-supervisor.js +130 -27
  432. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  433. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  434. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  435. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  436. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  437. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  438. package/template/coding-task-manager/lib/ws/state-sync.js +125 -6
  439. package/template/coding-task-manager/lib/ws/terminal-attach.js +123 -1
  440. package/template/coding-task-manager/lib/ws-router.js +68 -1
  441. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  442. package/template/coding-task-manager/oauth-proxy.js +211 -170
  443. package/template/coding-task-manager/package-lock.json +34 -94
  444. package/template/coding-task-manager/package.json +7 -3
  445. package/template/coding-task-manager/protocol/message-table.json +10 -0
  446. package/template/coding-task-manager/protocol/messages.js +10 -0
  447. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  448. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  449. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  450. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  451. package/template/coding-task-manager/public/css/app.css +449 -1
  452. package/template/coding-task-manager/public/css/reviews.css +4 -0
  453. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  454. package/template/coding-task-manager/public/index.html +766 -465
  455. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  456. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  457. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  458. package/template/coding-task-manager/public/js/app/machines-page.js +2572 -0
  459. package/template/coding-task-manager/public/js/app/models-tab.js +658 -73
  460. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  461. package/template/coding-task-manager/public/js/app/worktrees-panel.js +191 -7
  462. package/template/coding-task-manager/public/js/background-work.js +203 -28
  463. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +181 -13
  464. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  465. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  466. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  467. package/template/coding-task-manager/public/js/core/session-commands.js +847 -5
  468. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  469. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  470. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  471. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  472. package/template/coding-task-manager/public/js/history-app-seam.js +48 -4
  473. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  474. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  475. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  476. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  477. package/template/coding-task-manager/public/js/machine-pool-shared.js +991 -0
  478. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  479. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  480. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  481. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  482. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  483. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  484. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  485. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  486. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  487. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  488. package/template/coding-task-manager/public/js/prompts.js +11 -10
  489. package/template/coding-task-manager/public/js/protocol.js +10 -0
  490. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  491. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  492. package/template/coding-task-manager/public/js/reviews.js +7 -3
  493. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  494. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  495. package/template/coding-task-manager/public/js/row-join.js +4 -2
  496. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  497. package/template/coding-task-manager/public/js/session-search-utils.js +64 -21
  498. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  499. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  500. package/template/coding-task-manager/public/js/state-sync-client.js +14 -1
  501. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  502. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  503. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  504. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  505. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  506. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  507. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  508. package/template/coding-task-manager/public/js/terminal/runtime-12.js +60 -3
  509. package/template/coding-task-manager/public/js/terminal/runtime-16.js +16 -12
  510. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  511. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  512. package/template/coding-task-manager/public/js/terminal/runtime-20.js +211 -74
  513. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  514. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  515. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  516. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  517. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  518. package/template/coding-task-manager/public/js/terminal-wheel.js +62 -5
  519. package/template/coding-task-manager/public/js/walle/chat-conversation.js +15 -7
  520. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  521. package/template/coding-task-manager/public/js/walle.js +6 -0
  522. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  523. package/template/coding-task-manager/public/m/app.css +366 -8
  524. package/template/coding-task-manager/public/m/app.js +1006 -74
  525. package/template/coding-task-manager/public/m/index.html +55 -1
  526. package/template/coding-task-manager/public/m/machines.js +2147 -0
  527. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  528. package/template/coding-task-manager/public/m/private-api.js +31 -1
  529. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  530. package/template/coding-task-manager/public/m/sw.js +5 -0
  531. package/template/coding-task-manager/public/usage.html +1 -0
  532. package/template/coding-task-manager/queue-engine.js +13 -0
  533. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  534. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  535. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  536. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  537. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  538. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  539. package/template/coding-task-manager/server/approvals.js +5 -6
  540. package/template/coding-task-manager/server/boot.js +136 -17
  541. package/template/coding-task-manager/server.js +2819 -11885
  542. package/template/coding-task-manager/session-integrity.js +71 -13
  543. package/template/coding-task-manager/workers/db-owner-worker.js +72 -9
  544. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  545. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  546. package/template/coding-task-manager/workers/read-pool-worker.js +93 -12
  547. package/template/coding-task-manager/workers/session-host-pool-process.js +24 -10
  548. package/template/coding-task-manager/workers/session-host-process.js +19 -8
  549. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  550. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  551. package/template/coding-task-manager/workers/worktree-worker.js +202 -33
  552. package/template/ctm-relay/lib/protocol.js +14 -1
  553. package/template/package-lock.json +2 -2
  554. package/template/package.json +1 -1
  555. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  556. package/template/shared/backup-retention.js +82 -0
  557. package/template/shared/conversation-admission.js +2 -0
  558. package/template/shared/conversation-provenance.js +3 -7
  559. package/template/shared/conversation-storage-v2-contract.js +47 -6
  560. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  561. package/template/shared/git-remote-key.js +83 -0
  562. package/template/shared/host-identity.js +98 -2
  563. package/template/shared/linked-worktree.js +51 -0
  564. package/template/shared/macos-fda-target.js +57 -0
  565. package/template/shared/orphan-parents.js +55 -0
  566. package/template/shared/owner-write-safety.js +135 -0
  567. package/template/shared/public-runtime.js +13 -5
  568. package/template/shared/release-schema.json +3 -2
  569. package/template/shared/report-citations.js +14 -0
  570. package/template/shared/sqlite-write-lock.js +361 -223
  571. package/template/shared/storage/contract.js +38 -3
  572. package/template/wall-e/adapters/imessage.js +12 -0
  573. package/template/wall-e/agent.js +92 -4
  574. package/template/wall-e/api-walle.js +26 -1
  575. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  576. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  577. package/template/wall-e/bin/walle-mcp-stdio.js +14 -8
  578. package/template/wall-e/brain/brain-join.js +859 -0
  579. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  580. package/template/wall-e/brain/brain-sync-store.js +131 -77
  581. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  582. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  583. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  584. package/template/wall-e/brain/deterministic-id.js +51 -0
  585. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  586. package/template/wall-e/brain/memories.js +2 -2
  587. package/template/wall-e/brain/migrations.js +91 -28
  588. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  589. package/template/wall-e/brain/model-identity-migration.js +1 -1
  590. package/template/wall-e/brain/owner-call-whitelist.js +39 -7
  591. package/template/wall-e/brain/schema-ensures.js +22 -0
  592. package/template/wall-e/brain/schema.js +45 -85
  593. package/template/wall-e/brain.js +172 -36
  594. package/template/wall-e/chat.js +3 -3
  595. package/template/wall-e/coding/agent-shell-env.js +7 -2
  596. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  597. package/template/wall-e/coding/prompt-bundle.js +66 -21
  598. package/template/wall-e/coding/session-journal.js +28 -40
  599. package/template/wall-e/coding-orchestrator.js +23 -26
  600. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  601. package/template/wall-e/http/admin-api.js +37 -0
  602. package/template/wall-e/http/auth.js +18 -1
  603. package/template/wall-e/http/blog-views-api.js +318 -0
  604. package/template/wall-e/http/gws-api.js +4 -0
  605. package/template/wall-e/http/helper-rules-api.js +327 -0
  606. package/template/wall-e/http/model-admin.js +8 -1
  607. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  608. package/template/wall-e/http/model-proxy-providers.js +177 -2
  609. package/template/wall-e/http/model-route-health.js +157 -0
  610. package/template/wall-e/http/sync-api.js +278 -464
  611. package/template/wall-e/http/telemetry-api.js +12 -0
  612. package/template/wall-e/jobs/job-runner.js +28 -1
  613. package/template/wall-e/lib/blog-view-policy.js +88 -0
  614. package/template/wall-e/lib/blog-view-store.js +169 -0
  615. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  616. package/template/wall-e/lib/client-config-lock.js +2 -1
  617. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  618. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  619. package/template/wall-e/lib/derivation-home.js +147 -0
  620. package/template/wall-e/lib/error-kinds.js +10 -1
  621. package/template/wall-e/lib/feature-manifest.js +1 -1
  622. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  623. package/template/wall-e/lib/helper-rules/evaluate.js +398 -0
  624. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  625. package/template/wall-e/lib/helper-rules/grade.js +320 -0
  626. package/template/wall-e/lib/helper-rules/label.js +247 -0
  627. package/template/wall-e/lib/helper-rules/lead-call-hook.js +555 -0
  628. package/template/wall-e/lib/helper-rules/store.js +669 -0
  629. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  630. package/template/wall-e/lib/mcp-client-config.js +133 -14
  631. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  632. package/template/wall-e/lib/mcp-integration.js +63 -19
  633. package/template/wall-e/lib/parent-death.js +7 -1
  634. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  635. package/template/wall-e/lib/scheduler-worker-jobs.js +12 -4
  636. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  637. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  638. package/template/wall-e/llm/default-fallback.js +4 -0
  639. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  640. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  641. package/template/wall-e/llm/model-catalog.js +13 -4
  642. package/template/wall-e/llm/model-roles.js +7 -1
  643. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  644. package/template/wall-e/llm/portkey-http.js +150 -0
  645. package/template/wall-e/llm/portkey-sync.js +15 -145
  646. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  647. package/template/wall-e/llm/worker-mcp-routing.js +117 -7
  648. package/template/wall-e/llm/worker-route.js +43 -1
  649. package/template/wall-e/loops/brain-custom-writes.js +64 -0
  650. package/template/wall-e/loops/ingest.js +17 -1
  651. package/template/wall-e/loops/think.js +19 -4
  652. package/template/wall-e/mcp-contract.js +222 -6
  653. package/template/wall-e/mcp-protocol-contract.js +77 -2
  654. package/template/wall-e/mcp-server.js +141 -31
  655. package/template/wall-e/mcp-worker-schemas.js +25 -7
  656. package/template/wall-e/memory/ctm-context-client.js +110 -5
  657. package/template/wall-e/memory/ctm-session-context.js +9 -3
  658. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  659. package/template/wall-e/memory/replica-origin.js +56 -0
  660. package/template/wall-e/memory/session-pool.js +135 -0
  661. package/template/wall-e/runtime/live-capabilities.js +8 -3
  662. package/template/wall-e/server.js +4 -2
  663. package/template/wall-e/shared/release-schema.json +3 -2
  664. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  665. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  666. package/template/wall-e/shared/storage/contract.js +38 -3
  667. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  668. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +13 -1
  669. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +11 -6
  670. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +13 -8
  671. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  672. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  673. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  674. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  675. package/template/wall-e/tools/chrome-automation.js +6 -0
  676. package/template/wall-e/tools/local-tools.js +6 -4
  677. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  678. package/template/wall-e/workers/brain-owner-worker.js +27 -4
  679. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  680. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
  681. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,2069 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // WP-1 (docs/local-landing-queue-multi-session-integration-2026-10-26.html §6.2, §5 D-1..D-11,
4
+ // §8 WP-1): the `land` CLI.
5
+ //
6
+ // One queue per repository, no daemon (D-1/D-2). `land submit` writes a job, then tries to become
7
+ // the runner by claiming the single per-repository lease; a submitter that wins the lease drains the
8
+ // queue before exiting, a submitter that loses it prints `runner: 'other'` and exits in well under a
9
+ // second, spawning nothing. `land run [--once]` is the same loop without a submission, for CTM's
10
+ // button and for cron-less recovery.
11
+ //
12
+ // WP-1 (§7 "Header rule", docs/fleet-origin-main-sync-skill-2026-10-31.html): the queue runs
13
+ // `git push` only for a `publish` job, only to `landing.publish.remote/branch`, only fast-forward.
14
+ // Nothing else here pushes: every other git child is `--no-optional-locks` with the §4 GIT_*
15
+ // list scrubbed (`lib/landing/git.js`), never runs add/commit/checkout/stash outside its own train
16
+ // worktree (D-11), and never merges into a session's worktree.
17
+ //
18
+ // Exit codes (§6.2): 0 ok · 1 typed refusal/failure · 2 usage · 75 timeout.
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { spawn } = require('child_process');
23
+
24
+ const {
25
+ landingError,
26
+ childEnv,
27
+ toplevel,
28
+ commonDir,
29
+ revParse,
30
+ revParseOrNull,
31
+ runGitRaw,
32
+ mergeTree,
33
+ } = require('../lib/landing/git');
34
+ const { intFlag, strFlag } = require('../lib/env-flags');
35
+ const lease = require('../lib/landing/lease');
36
+ const queue = require('../lib/landing/queue');
37
+ const train = require('../lib/landing/train');
38
+ const gate = require('../lib/landing/gate');
39
+ const proof = require('../lib/landing/proof');
40
+ const mergeDelta = require('../lib/landing/merge-delta');
41
+ const batchRunner = require('../lib/landing/batch-runner');
42
+ const stop = require('../lib/landing/stop');
43
+ const radar = require('../lib/landing/radar');
44
+ const sync = require('../lib/landing/sync');
45
+ const syncCli = require('../lib/landing/sync-cli');
46
+ const resolveCli = require('../lib/landing/resolve-cli');
47
+ const { writeJsonAtomic } = require('../lib/landing/state');
48
+ const { loadLandingConfig, loadTargetLandingConfig, resolveMainBranch, gatesHash } = require('../lib/landing/config');
49
+
50
+ const CONTRACT = 1;
51
+ // `--wait` and `land wait` are bounded (the doc's 4 h); a queue that has not decided by then is a
52
+ // timeout (exit 75), never an endless poll.
53
+ const WAIT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
54
+ const WAIT_TIMEOUT_S = WAIT_TIMEOUT_MS / 1000;
55
+ // Poll the job file at ≤ 1 s (§6.2), never a busy loop.
56
+ const POLL_MS = 200;
57
+ // §6.2 `land cancel --force` on a running job: how long the canceling process waits for the runner
58
+ // to observe the stop request and settle the job. The runner's heartbeat is 10 s and the SIGKILL
59
+ // grace is another 10 s, so 30 s covers the worst case with room to spare — and a runner that never
60
+ // settles is reported (exit 75), never waited on forever.
61
+ const CANCEL_WAIT_MS = 30000;
62
+ const CANCEL_POLL_MS = 250;
63
+
64
+ // D-10: the marked block written into the repository's agent file. Its content must be exactly
65
+ // `lib/landing/agent-rule.txt` with `<target>` substituted (WP-5's E-4 lint checks byte identity).
66
+ const MARK_BEGIN = '<!-- land:begin -->';
67
+ const MARK_END = '<!-- land:end -->';
68
+ const MARK_RE = /<!-- land:begin -->[\s\S]*?<!-- land:end -->/;
69
+ const AGENT_RULE_FILE = path.join(__dirname, '..', 'lib', 'landing', 'agent-rule.txt');
70
+ const AGENT_FILES = Object.freeze(['CLAUDE.md', 'AGENTS.md']);
71
+
72
+ const USAGE = [
73
+ 'usage: land <command> [options]',
74
+ '',
75
+ ' land init [--guard] [--agent-file CLAUDE.md|AGENTS.md]',
76
+ ' land guard install',
77
+ ' land submit [--branch <name>] [--title <text>] [--priority normal|high|urgent] [--reason <text>] [--queue-anyway] [--wait] [--json]',
78
+ ' land run [--once]',
79
+ ' land priority <jobId> normal|high|urgent [--reason <text>] [--json]',
80
+ ' land status [--json] [--repo <path>] [--radar-for <branch>[,<branch>]]',
81
+ ` land wait <jobId> [--timeout-s ${WAIT_TIMEOUT_S}]`,
82
+ ' land inspect <jobId>',
83
+ ' land cancel <jobId> [--force]',
84
+ ' land conflicts [--with <branch>] [--json]',
85
+ ' land claim <path>... [--ttl-h 4] [--session <id>] [--json]',
86
+ ' land unclaim [--session <id>] [--json]',
87
+ ' land record-proof --tree <oid> --plan-base <rev> --log <path> --seconds <n> [--selection <json-file>] [--commit <sha>] [--json]',
88
+ ' land report merge-delta [--json]',
89
+ ' land sync [--wait] [--pull-only] [--json]',
90
+ ' land resolve <jobId> --worktree <dir> [--wait] [--json]',
91
+ ' land resolve <jobId> --claim [--session <id>] [--json]',
92
+ ' land doctor [--json]',
93
+ ].join('\n');
94
+
95
+ const FLAG_SPECS = Object.freeze({
96
+ init: { bool: ['guard'], value: ['agent-file'] },
97
+ guard: { bool: [], value: [] },
98
+ submit: { bool: ['wait', 'json', 'queue-anyway'], value: ['branch', 'title', 'priority', 'reason'] },
99
+ run: { bool: ['once'], value: [] },
100
+ priority: { bool: ['json'], value: ['reason'] },
101
+ status: { bool: ['json'], value: ['repo', 'radar-for'] },
102
+ wait: { bool: [], value: ['timeout-s'] },
103
+ inspect: { bool: [], value: [] },
104
+ cancel: { bool: ['force'], value: [] },
105
+ conflicts: { bool: ['json'], value: ['with'] },
106
+ claim: { bool: ['json'], value: ['ttl-h', 'session'] },
107
+ unclaim: { bool: ['json'], value: ['session'] },
108
+ 'record-proof': { bool: ['json'], value: ['tree', 'plan-base', 'log', 'seconds', 'selection', 'commit'] },
109
+ report: { bool: ['json'], value: [] },
110
+ sync: { bool: ['wait', 'json', 'pull-only'], value: [] },
111
+ resolve: { bool: ['wait', 'json', 'claim'], value: ['worktree', 'session'] },
112
+ doctor: { bool: ['json'], value: [] },
113
+ });
114
+
115
+ // --- errors and output -------------------------------------------------------------------------
116
+
117
+ function usageError(message) {
118
+ const error = new Error(message);
119
+ error.code = 'usage';
120
+ return error;
121
+ }
122
+
123
+ function progress(line) {
124
+ process.stderr.write(`land: ${line}\n`);
125
+ }
126
+
127
+ function emitJson(payload) {
128
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
129
+ }
130
+
131
+ function emitLine(line) {
132
+ process.stdout.write(`${line}\n`);
133
+ }
134
+
135
+ // A real timer, never `state.sleepSync`: the runner's heartbeat is an interval, and blocking the
136
+ // event loop is exactly what it must not do.
137
+ function delay(ms) {
138
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
139
+ }
140
+
141
+ function reportFailure(error, wantsJson) {
142
+ const code = typeof error.code === 'string' ? error.code : 'unexpected';
143
+ const message = error && error.message ? error.message : String(error);
144
+ if (code === 'usage') {
145
+ process.stderr.write(`land: ${message}\n\n${USAGE}\n`);
146
+ return 2;
147
+ }
148
+ // D-4 (§6.2): a submit refused because the branch conflicts with the target on a path the queue
149
+ // cannot regenerate is its OWN exit code (3), not the generic 1 — the caller has one action to
150
+ // take (`merge <target> into the branch`, or `--queue-anyway`) and a scripted agent must be able to
151
+ // tell that refusal apart from "the CLI broke". The paths ride in the payload so nothing has to
152
+ // re-run the merge to find out why.
153
+ if (code === 'conflict_at_submit') {
154
+ if (wantsJson) {
155
+ emitJson({
156
+ contract: CONTRACT,
157
+ ok: false,
158
+ error: code,
159
+ message,
160
+ paths: error.paths || [],
161
+ uncovered: error.uncovered || [],
162
+ covered: error.covered || [],
163
+ });
164
+ } else process.stderr.write(`land: error: ${code}: ${message}\n`);
165
+ return 3;
166
+ }
167
+ if (wantsJson) emitJson({ contract: CONTRACT, ok: false, error: { code, message } });
168
+ else process.stderr.write(`land: error: ${code}: ${message}\n`);
169
+ return 1;
170
+ }
171
+
172
+ // --- argv --------------------------------------------------------------------------------------
173
+
174
+ function parseArgs(argv, spec) {
175
+ const flags = new Set();
176
+ const values = {};
177
+ const positionals = [];
178
+ for (let index = 0; index < argv.length; index += 1) {
179
+ const arg = argv[index];
180
+ if (arg === '--') {
181
+ positionals.push(...argv.slice(index + 1));
182
+ break;
183
+ }
184
+ if (!arg.startsWith('--')) {
185
+ positionals.push(arg);
186
+ continue;
187
+ }
188
+ const equals = arg.indexOf('=');
189
+ const name = equals === -1 ? arg.slice(2) : arg.slice(2, equals);
190
+ const inline = equals === -1 ? null : arg.slice(equals + 1);
191
+ if (spec.bool.includes(name)) {
192
+ if (inline !== null) throw usageError(`--${name} takes no value`);
193
+ flags.add(name);
194
+ continue;
195
+ }
196
+ if (spec.value.includes(name)) {
197
+ let value = inline;
198
+ if (value === null) {
199
+ value = argv[index + 1];
200
+ index += 1;
201
+ }
202
+ if (value === undefined || value.startsWith('--')) throw usageError(`--${name} requires a value`);
203
+ values[name] = value;
204
+ continue;
205
+ }
206
+ throw usageError(`unknown option --${name}`);
207
+ }
208
+ return { flags, values, positionals };
209
+ }
210
+
211
+ // --- repository context ------------------------------------------------------------------------
212
+
213
+ function context(cwd) {
214
+ const top = toplevel(cwd);
215
+ const common = commonDir(cwd);
216
+ return { cwd, top, common, root: path.join(common, 'landing') };
217
+ }
218
+
219
+ function branchRef(name) {
220
+ return `refs/heads/${name}`;
221
+ }
222
+
223
+ function lines(stdout) {
224
+ return String(stdout || '').split('\n').map((line) => line.trim()).filter(Boolean);
225
+ }
226
+
227
+ function currentBranch(cwd) {
228
+ const res = runGitRaw(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
229
+ if (res.status !== 0) {
230
+ throw landingError('git_failed', `cannot resolve the current branch in ${cwd}`, { cwd, stderr: res.stderr });
231
+ }
232
+ const name = res.stdout.trim();
233
+ if (!name || name === 'HEAD') {
234
+ throw landingError('detached_head', `${cwd} is on a detached HEAD; check out a branch before submitting`, { cwd });
235
+ }
236
+ return name;
237
+ }
238
+
239
+ function unmergedPaths(cwd) {
240
+ return lines(runGitRaw(cwd, ['diff', '--name-only', '--diff-filter=U']).stdout);
241
+ }
242
+
243
+ function dirtyTrackedPaths(cwd) {
244
+ const unstaged = lines(runGitRaw(cwd, ['diff', '--name-only']).stdout);
245
+ const staged = lines(runGitRaw(cwd, ['diff', '--cached', '--name-only']).stdout);
246
+ return [...new Set([...unstaged, ...staged])];
247
+ }
248
+
249
+ function commitSubject(cwd, rev) {
250
+ const res = runGitRaw(cwd, ['log', '-1', '--format=%s', rev]);
251
+ const subject = res.status === 0 ? res.stdout.trim() : '';
252
+ return subject || rev.slice(0, 12);
253
+ }
254
+
255
+ function submitterWorkdir(ctx, job) {
256
+ const dir = job && job.submittedBy ? job.submittedBy.cwd : null;
257
+ if (dir && fs.existsSync(dir) && fs.existsSync(path.join(dir, '.git'))) return dir;
258
+ return ctx.cwd;
259
+ }
260
+
261
+ // --- D-4/D-5: conflict previews (§9 S-7/S-8) -----------------------------------------------------
262
+
263
+ // The conflict paths the queue itself owns — a `landing.regenerate[]` or `landing.refresh[]` entry's
264
+ // glob covers them, so the runner resolves them without a human. Everything else is hand-written and
265
+ // is what D-4 refuses at submit. `train.globMatch` is the very matcher the runner uses to decide
266
+ // `matchingRegenerateEntries`, so the precheck and the actual resolve can never disagree.
267
+ function pathCoverage(config, conflicts) {
268
+ const entries = []
269
+ .concat(config && Array.isArray(config.regenerate) ? config.regenerate : [])
270
+ .concat(config && Array.isArray(config.refresh) ? config.refresh : []);
271
+ const owns = (file) => entries.some((entry) => Array.isArray(entry.paths) && entry.paths.some((pattern) => train.globMatch(pattern, file)));
272
+ return { covered: conflicts.filter(owns), uncovered: conflicts.filter((file) => !owns(file)) };
273
+ }
274
+
275
+ // D-5 (§9 S-8): after every landing the target has moved, so every WAITING job's train is rebuilt
276
+ // against the new tip — `merge-tree` only, no worktree and no commit object — and its
277
+ // `conflictPreview` records what it would now conflict on. The sweep is advisory by construction: it
278
+ // never cancels, requeues or reorders anything, and a single uncomputable merge-tree skips that job
279
+ // (which keeps the preview it already had) instead of failing the landing that triggered the sweep.
280
+ function sweepConflictPreviews(ctx, workdir, config) {
281
+ const tip = revParseOrNull(workdir, branchRef(config.target));
282
+ if (!tip) return 0;
283
+ let written = 0;
284
+ for (const id of queue.readQueue(ctx.root).order) {
285
+ const job = queue.readJob(ctx.root, id);
286
+ if (!job || job.state !== 'queued') continue;
287
+ let merged;
288
+ try {
289
+ merged = mergeTree(workdir, tip, job.tip);
290
+ } catch (error) {
291
+ /* REPORT: the sweep is advisory — `runJob` re-decides the real conflict when the job is taken,
292
+ so one job whose merge-tree cannot be computed is skipped and keeps its previous preview */
293
+ progress(`job ${id}: conflict preview skipped (${error && error.message ? error.message : String(error)})`);
294
+ continue;
295
+ }
296
+ const { uncovered } = pathCoverage(config, merged.conflicts);
297
+ const next = uncovered.length ? { base: tip, paths: merged.conflicts, checkedAt: queue.nowIso() } : null;
298
+ const before = job.conflictPreview === undefined ? null : job.conflictPreview;
299
+ if (previewSignature(before) === previewSignature(next)) continue;
300
+ queue.updateJob(ctx.root, id, { conflictPreview: next });
301
+ written += 1;
302
+ progress(next ? `job ${id}: will conflict on ${next.paths.join(', ')}` : `job ${id}: conflict cleared`);
303
+ }
304
+ return written;
305
+ }
306
+
307
+ // `checkedAt` moves on every sweep, so the change test compares the two facts a reader acts on — the
308
+ // base the preview was measured against and the paths it names — never the timestamp.
309
+ function previewSignature(preview) {
310
+ if (!preview) return '-';
311
+ const paths = Array.isArray(preview.paths) ? preview.paths.join(',') : '';
312
+ return `${preview.base || ''}:${paths}`;
313
+ }
314
+
315
+ // The start-up half of D-5: a runner that has just claimed the lease previews the queue it is about
316
+ // to drain, so a session waiting behind a target that moved under a PREVIOUS runner learns about it
317
+ // before the next `takeNextJob`. A policy this process cannot read yet is not a runner failure — the
318
+ // per-job path re-reads and enforces its own policy.
319
+ function sweepOnRunnerStart(ctx) {
320
+ let config;
321
+ try {
322
+ config = loadTargetLandingConfig(ctx.cwd);
323
+ } catch (error) {
324
+ /* REPORT: the start-up sweep is advisory; `runJobInner` loads (and enforces) the target policy
325
+ for the job it actually takes, so an unreadable policy here only means "no previews yet" */
326
+ progress(`start-up conflict sweep skipped (${error && error.code ? error.code : 'unexpected'})`);
327
+ return 0;
328
+ }
329
+ return sweepConflictPreviews(ctx, ctx.cwd, config);
330
+ }
331
+
332
+ // --- the runner (D-2) --------------------------------------------------------------------------
333
+
334
+ // D-2: the runner's liveness record. The interval is what keeps `heartbeatAt` fresh, and it works
335
+ // only because gates run through an async `spawn` — the runner's event loop is free while a gate
336
+ // works, so a 40-minute gate is 40 minutes of heartbeats rather than one long silence. `unref()`
337
+ // because the lease, not the timer, decides when this process stops being the runner.
338
+ function startHeartbeat(root, token, current) {
339
+ return setInterval(() => {
340
+ try {
341
+ lease.heartbeat(root, token, { ...(current.jobId ? { jobId: current.jobId } : {}), jobIds: current.jobIds || null });
342
+ } catch (error) {
343
+ /* expected: the lease was reaped or replaced while this runner worked — the drain loop
344
+ re-checks the on-disk token before every job and stops rather than writing to a lease it
345
+ no longer holds */
346
+ }
347
+ // D-5: the same tick that keeps the lease fresh is the tick that reads the job's stop request.
348
+ // There is no new timer — the runner already polls every 10 s while a gate works — and the
349
+ // abort signal is what every `runShell` of this job (gates and regenerate entries) is watching.
350
+ if (!current.controller || current.controller.signal.aborted) return;
351
+ // D-2 (§9 S-1, WP-1): the predicate lives in lib/landing/stop.js. A `cancel` on ANY member voids
352
+ // the train's verdict (I-4); a preempt keeps the boundary rule, and a singleton satisfies "every".
353
+ const members = Array.isArray(current.jobIds) && current.jobIds.length
354
+ ? current.jobIds
355
+ : (current.jobId ? [current.jobId] : []);
356
+ if (!members.length) return;
357
+ try {
358
+ if (stop.shouldAbortTrain(members.map((id) => queue.readJob(root, id)))) current.controller.abort();
359
+ } catch (error) {
360
+ /* expected: the job file can be mid-rename for an instant, or the directory is being torn
361
+ down under a test — a read error must never kill the interval that keeps this runner's
362
+ lease alive, and the next tick reads again */
363
+ }
364
+ }, lease.LAND_HEARTBEAT_MS);
365
+ }
366
+
367
+ // D-7 (WP-2): the CURRENT target tip, the thing a `base_broken` job waits for. It is the target the
368
+ // committed policy names (the same loader `runJobInner` gates every job with), resolved fresh each
369
+ // drain iteration so the hold is decided on the tip as it is now, not as it was when the job was
370
+ // parked. A policy this process cannot read holds nothing (`null`) — `runJobInner` refuses such a job
371
+ // with the same error a moment later, and parking work forever on a comparison that failed open would
372
+ // be worse than running it.
373
+ function targetTipOf(ctx) {
374
+ try {
375
+ return revParseOrNull(ctx.cwd, branchRef(loadTargetLandingConfig(ctx.cwd).target));
376
+ } catch (error) {
377
+ /* REPORT: unreadable policy/target — no hold this iteration; the job itself surfaces the error */
378
+ return null;
379
+ }
380
+ }
381
+
382
+ // WP-1: the batched runner lives in lib/landing/batch-runner.js and runs on this file's helpers.
383
+ batchRunner.configure({ runJob, progress, submitterWorkdir, gateSelectionTelemetry, sweepConflictPreviews, branchRef, delay, afterTrainLanded: syncCli.afterTrainLanded });
384
+
385
+ async function drain(ctx, record, opts = {}) {
386
+ const token = record.token;
387
+ const current = { jobId: null, jobIds: null, controller: null };
388
+ const timer = startHeartbeat(ctx.root, token, current);
389
+ timer.unref();
390
+ let processed = 0;
391
+ let released = false;
392
+ try {
393
+ // D-5 (§9 S-8): preview the queue this runner inherited, before it takes the first job.
394
+ sweepOnRunnerStart(ctx);
395
+ for (;;) {
396
+ const held = lease.read(ctx.root);
397
+ if (!held || held.token !== token) {
398
+ progress('lease lost; stopping the runner');
399
+ break;
400
+ }
401
+ const targetTip = targetTipOf(ctx);
402
+ const window = await batchRunner.chooseWindow(ctx, targetTip);
403
+ const members = queue.takeNextBatch(ctx.root, {
404
+ targetTip,
405
+ window,
406
+ onEmpty: () => { released = lease.release(ctx.root, token); },
407
+ });
408
+ if (!members.length) break;
409
+ current.jobId = members[0].id;
410
+ current.jobIds = members.map((member) => member.id);
411
+ // D-5: one controller per train (stop.shouldAbortTrain); jobIds reach the lease NOW for `land cancel`.
412
+ try { lease.heartbeat(ctx.root, token, { jobId: current.jobId, jobIds: current.jobIds }); } catch (error) { /* expected: lease reaped/replaced — the drain loop re-checks the token before every job */ }
413
+ current.controller = new AbortController();
414
+ try {
415
+ await batchRunner.runTrain(ctx, members, token, current.controller.signal);
416
+ } finally {
417
+ current.controller = null;
418
+ current.jobId = null; current.jobIds = null;
419
+ try { lease.heartbeat(ctx.root, token, { jobId: null, jobIds: null }); } catch (error) { /* expected: lease reaped/replaced — nothing of this train's left to clear */ }
420
+ }
421
+ processed += members.length;
422
+ if (opts.once) break;
423
+ }
424
+ } finally {
425
+ clearInterval(timer);
426
+ if (!released) lease.release(ctx.root, token);
427
+ }
428
+ return processed;
429
+ }
430
+
431
+ // WP-0 (§5 D-8, §6.6, §8 WP-0): the shape `job.gate` and the terminal metrics row's `gate` share,
432
+ // built from the gate's own `--selection-out` record. A gate that wrote nothing (an older gate, or
433
+ // one that could not write) reads as `null` — "unknown", never a row of zeros. `fallbackReason` and
434
+ // `intersection` belong to WP-1/WP-3 and stay null until those exist.
435
+ function gateSelectionTelemetry(selection) {
436
+ if (!selection || typeof selection !== 'object') return null;
437
+ const finite = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
438
+ return {
439
+ mode: typeof selection.mode === 'string' ? selection.mode : null,
440
+ planBase: typeof selection.planBase === 'string' ? selection.planBase : null,
441
+ selected: finite(selection.selected),
442
+ executed: finite(selection.executed),
443
+ cached: finite(selection.cached),
444
+ // §6.6 calls the cache-miss map `missReasons`; bin/test-gate.sh writes it as `uncached`.
445
+ missReasons: selection.uncached && typeof selection.uncached === 'object' ? selection.uncached : null,
446
+ fallbackReason: null,
447
+ intersection: null,
448
+ };
449
+ }
450
+
451
+ // The gate's own timeout, the bound the base rerun inherits: the longest `timeoutSeconds` among the
452
+ // configured gates (there is one in practice), in milliseconds; undefined when none is set, which
453
+ // `runShell` treats as unbounded exactly like a gate with no timeout.
454
+ function gateTimeoutMs(config) {
455
+ const seconds = (Array.isArray(config.gates) ? config.gates : [])
456
+ .map((gate) => Number(gate && gate.timeoutSeconds))
457
+ .filter((value) => Number.isFinite(value) && value > 0);
458
+ return seconds.length ? Math.max(...seconds) * 1000 : undefined;
459
+ }
460
+
461
+ // D-6/D-7 (WP-2, design §5): the base-rerun diagnosis. A FAILING gate names the test files that
462
+ // failed; before that is reported as THIS branch's failure, exactly those files are rerun on the
463
+ // BASE commit — Chromium's with-patch/without-patch rule: a failure that reproduces WITHOUT the patch
464
+ // is not the patch's failure, it is a broken main. Returns `{ triage, gateSeconds, requeued }`:
465
+ // `triage` is what the job record carries (null when the pass did not run), `gateSeconds` has the
466
+ // rerun added to the gate's own, and `requeued: true` means the job was parked `base_broken` via
467
+ // `queue.requeueBaseBroken` and the caller must NOT also finish it as failed (the record already
468
+ // holds the outcome, and the runner will pick the job up again once the target tip moves).
469
+ //
470
+ // The pass is SKIPPED — reason untouched, `job.triage = null`, one `triage skipped (<why>)` line —
471
+ // when the gate reason is not a test failure (`gate_timeout`/`gate_dirtied_tree`/`gate_stopped` name
472
+ // no files), when the verdict came from the cache (nothing ran, so there is no fresh log), when the
473
+ // repo turned it off (`landing.triage: false` → `maxFiles` 0), when the log names no test file at all
474
+ // (a lint-only failure — lint is deterministic on the train tree and is never rerun on base), and
475
+ // when it names more than `triage.maxFiles` files (a red main wide enough that a rerun would cost as
476
+ // much as the gate it is diagnosing). A rerun that cannot RUN is not a verdict about base either: the
477
+ // failure is reported exactly as the gate found it.
478
+ async function triageFailedJob(ctx, { config, job, cached, reason, jobVerdict, built, logFile, workdir, signal, gateSeconds }) {
479
+ const skip = (why) => {
480
+ progress(`job ${job.id}: triage skipped (${why})`);
481
+ queue.updateJob(ctx.root, job.id, { triage: null });
482
+ return { triage: null, gateSeconds, requeued: false };
483
+ };
484
+ if (cached || (reason !== 'train_only' && reason !== 'gate_failed') || !(config.triage.maxFiles > 0)) {
485
+ return { triage: null, gateSeconds, requeued: false };
486
+ }
487
+ let parsed;
488
+ try {
489
+ parsed = train.failedFilesFromLog(fs.readFileSync(logFile, 'utf8'));
490
+ } catch (error) {
491
+ /* REPORT: a diagnosis that could not be made leaves the failure exactly as the gate reported it,
492
+ with `triage: null` saying so — never a hold on a base this run never measured */
493
+ return skip(`gate log unreadable: ${error.message}`);
494
+ }
495
+ if (!parsed.tests.length) return skip(parsed.lint ? 'lint failures are not rerun on base' : 'the gate log named no test file');
496
+ if (parsed.tests.length > config.triage.maxFiles) {
497
+ return skip(`${parsed.tests.length} failing files > triage.maxFiles ${config.triage.maxFiles}`);
498
+ }
499
+ let rerun;
500
+ try {
501
+ rerun = await train.rerunOnBase(workdir, {
502
+ base: built.base, files: parsed.tests, root: ctx.root, jobId: job.id, signal, cachePaths: config.cachePaths,
503
+ // D-7: the rerun is bounded by the same timeout as the gate it diagnoses (per launcher).
504
+ timeoutMs: gateTimeoutMs(config),
505
+ });
506
+ } catch (error) {
507
+ /* REPORT: rerunning is diagnostic, and a diagnostic that failed must not invent an outcome */
508
+ return skip(`rerun on base failed: ${error.message}`);
509
+ }
510
+ const triage = { base: built.base, files: parsed.tests.slice(), failedOnBase: rerun.failedOnBase, seconds: rerun.seconds };
511
+ const totalGateSeconds = Number(((Number(gateSeconds) || 0) + rerun.seconds).toFixed(3));
512
+ // A stopped rerun is not a verdict (the runner is being stopped): keep the failure, record what ran.
513
+ if (rerun.aborted) return { triage, gateSeconds: totalGateSeconds, requeued: false };
514
+ const everyOnBase = rerun.failedOnBase.length > 0 && parsed.tests.every((file) => rerun.failedOnBase.includes(file));
515
+ if (!everyOnBase) return { triage, gateSeconds: totalGateSeconds, requeued: false };
516
+ progress(`job ${job.id}: base_broken — ${rerun.failedOnBase.length} file(s) fail on base too; left queued until ${config.target} moves`);
517
+ queue.requeueBaseBroken(ctx.root, job.id, {
518
+ reason: 'base_broken',
519
+ verdict: jobVerdict,
520
+ gateLog: logFile,
521
+ triage,
522
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
523
+ }, { seconds: null, gateSeconds: totalGateSeconds });
524
+ return { triage, gateSeconds: totalGateSeconds, requeued: true };
525
+ }
526
+
527
+ // Anything unexpected still leaves the queue in a decidable state: the job is `failed` with the
528
+ // typed code recorded, never stuck in `in_progress` for a later runner to reap.
529
+ async function runJob(ctx, job, token, signal) {
530
+ try {
531
+ return await runJobInner(ctx, job, token, signal);
532
+ } catch (error) {
533
+ const code = typeof error.code === 'string' ? error.code : 'internal_error';
534
+ progress(`job ${job.id}: ${code}: ${error.message}`);
535
+ return queue.finishJob(ctx.root, job.id, {
536
+ state: 'failed',
537
+ reason: null,
538
+ error: { code, message: error.message },
539
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
540
+ });
541
+ }
542
+ }
543
+
544
+ async function runJobInner(ctx, job, token, signal) {
545
+ const workdir = submitterWorkdir(ctx, job);
546
+ // D-1/I-5: the policy this job is gated by is the `.ctm.json` COMMITTED AT THE TARGET, never the
547
+ // submitter's working tree. Reading the branch's own file would let any branch weaken the gate it
548
+ // is landed under (`gates: [{run: "echo ok"}]`) — a policy change has to land before it counts.
549
+ let config = loadTargetLandingConfig(workdir);
550
+ // WP-6 §13.4 R-R4/R-R6 (§13.7): a resolution of a CONFLICT is re-gated by the FAST gate instead of
551
+ // `landing.gates`; a resolution of a GATE FAILURE keeps the full gate and falls through. The copy
552
+ // replaces the whole `gates` array for the entire job, so every key derived from it —
553
+ // `gatesHash(config.gates)` for the verdict cache and the proof record — is the fast gate's, and
554
+ // never the commit gate's.
555
+ if (job.kind === 'resolve' && job.gateLane === 'fast' && config.publish && config.publish.resolveFastGate) {
556
+ config = { ...config, gates: [config.publish.resolveFastGate] };
557
+ }
558
+ const trainsRoot = train.trainsRoot(workdir, ctx.common).dir;
559
+ const trainDir = train.trainDirFor(trainsRoot, job.id);
560
+ // `landing/logs/` is not in state.js's SUBDIRS, so it is created here — before the first thing that
561
+ // can append to it (the D-8 regenerate log, which happens before the gate ever runs).
562
+ const logFile = path.join(queue.ensureLogsDir(ctx.root), `${job.id}.log`);
563
+ const targetWorktree = train.targetWorktreeFor(workdir, config.target);
564
+ const sourceDir = targetWorktree ? targetWorktree.path : workdir;
565
+ const foreignCommits = [];
566
+ let gateSeconds = null;
567
+ // WP-0 (design doc §5 D-8, §6.6, §8 WP-0, §9 S-13): the gate's self-description, spread onto the
568
+ // job record and onto this run's terminal metrics row. `null` means "no gate ran for this
569
+ // transition" — an `unchanged` job is `mode: 'none'`, a verdict-cache hit is `mode: 'cached'`.
570
+ // Reset at the top of every round so a later round can never inherit an earlier round's numbers.
571
+ let gateTelemetry = null;
572
+ let lastBuilt = null;
573
+ // Where the enforced policy came from — recorded so `land inspect` can say why a job was gated by
574
+ // rules its own branch does not contain.
575
+ queue.updateJob(ctx.root, job.id, { policySource: config.policySource });
576
+
577
+ // D-5: re-read this job's stop request off disk. Either kind is a stop: `kind:'cancel'` never
578
+ // lands, and `kind:'preempt'` (written by `urgent`, WP-3) sends the job back to the queue — unless
579
+ // the work is already done, which is the R-3 window handled at checkpoint 3 and 4 below.
580
+ const pendingStop = () => {
581
+ const fresh = queue.readJob(ctx.root, job.id);
582
+ return (fresh && fresh.stopRequest) || null;
583
+ };
584
+ // D-5: the cancel transition. `finishJob` re-reads the job under the mutex and spreads the patch,
585
+ // so the base/tree/train already recorded for this run survive.
586
+ const finishCanceled = (request) => {
587
+ progress(`job ${job.id}: stopped before the target moved`);
588
+ return queue.finishJob(ctx.root, job.id, {
589
+ state: 'canceled',
590
+ reason: 'stopped',
591
+ canceledBy: request.by,
592
+ stopRequest: null,
593
+ timestamps: { ...job.timestamps, canceled: queue.nowIso() },
594
+ }, { gateSeconds, gate: gateTelemetry });
595
+ };
596
+ // D-5 for a PREEMPT: the job is not terminal — it goes back to the queue (D-4/D-3) and the runner's
597
+ // next `takeNextJob` takes the urgent job, which sorts at the head. A cancel keeps winning over
598
+ // everything, at every checkpoint.
599
+ const stopBeforeLanding = (request) => {
600
+ if (request.kind === 'cancel') return finishCanceled(request);
601
+ progress(`job ${job.id}: preempted by ${request.forJob}; back to the queue`);
602
+ return queue.requeuePreempted(ctx.root, job.id);
603
+ };
604
+
605
+ for (let round = 1; round <= train.MAX_ROUNDS; round += 1) {
606
+ gateTelemetry = null;
607
+ const fresh = queue.readJob(ctx.root, job.id);
608
+ if (!fresh) return null;
609
+ // Checkpoint 1 (D-5): a job a dead runner left `canceled` directly is honoured as before ...
610
+ if (fresh.state === 'canceled') {
611
+ progress(`job ${job.id}: canceled before round ${round}`);
612
+ return fresh;
613
+ }
614
+ // ... and the round start is also where a stop request that arrived between jobs is seen.
615
+ const requestAtRoundStart = pendingStop();
616
+ if (requestAtRoundStart) return stopBeforeLanding(requestAtRoundStart);
617
+
618
+ let built = train.buildTrain(workdir, config.target, job);
619
+ lastBuilt = built;
620
+ progress(`job ${job.id} round ${round}/${train.MAX_ROUNDS}: ${built.kind} on ${built.base.slice(0, 12)}`);
621
+
622
+ // Checkpoint 2 (D-5), first half: the train has been built — a cancel that arrived while it was
623
+ // built stops the job before it can land (`unchanged`) or be acted on (`conflict`).
624
+ const requestAfterBuild = pendingStop();
625
+ if (requestAfterBuild) return stopBeforeLanding(requestAfterBuild);
626
+
627
+ if (built.kind === 'unchanged') {
628
+ // WP-0: no gate ran at all — the train is empty. `mode: 'none'` says exactly that, so a
629
+ // metrics reader never has to infer "no gate" from a missing field.
630
+ const noneGate = {
631
+ mode: 'none', planBase: null, selected: null, executed: null, cached: null,
632
+ missReasons: null, fallbackReason: null, intersection: null,
633
+ };
634
+ const unchanged = queue.finishJob(ctx.root, job.id, {
635
+ state: 'landed',
636
+ reason: 'unchanged',
637
+ base: built.base,
638
+ tree: built.tree,
639
+ train: built.train,
640
+ verdict: null,
641
+ gate: noneGate,
642
+ timestamps: { ...job.timestamps, landed: queue.nowIso() },
643
+ }, { gateSeconds, gate: noneGate });
644
+ // D-5: the target did not move, but a job finishing is still a change to the queue's state —
645
+ // and the sweep is idempotent (a job whose preview did not change is not written at all).
646
+ sweepConflictPreviews(ctx, workdir, config);
647
+ return unchanged;
648
+ }
649
+
650
+ if (built.kind === 'conflict') {
651
+ const entries = train.matchingRegenerateEntries(config, built.conflicts);
652
+ if (!entries) {
653
+ progress(`job ${job.id}: conflict in ${built.conflicts.join(', ')} — blocked`);
654
+ return queue.finishJob(ctx.root, job.id, {
655
+ state: 'blocked',
656
+ reason: 'conflict',
657
+ conflictPaths: built.conflicts,
658
+ base: built.base,
659
+ tree: built.tree,
660
+ train: null,
661
+ gate: null,
662
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
663
+ }, { gateSeconds, gate: gateTelemetry });
664
+ }
665
+ try {
666
+ built = await train.regenerateTrain({
667
+ cwd: workdir, dir: trainDir, job, base: built.base, conflicts: built.conflicts, entries, log: logFile, signal,
668
+ });
669
+ } catch (error) {
670
+ // Checkpoint 2 (D-5), second half: a `stopped` error from a regenerate entry is a STOP, not
671
+ // a failure — the entry was killed because this job's abort signal fired. Anything else (a
672
+ // real regenerate failure) is rethrown and becomes the job's failure.
673
+ if (error && error.code === 'stopped') {
674
+ const requestDuringRegenerate = pendingStop();
675
+ if (requestDuringRegenerate) return stopBeforeLanding(requestDuringRegenerate);
676
+ }
677
+ throw error;
678
+ }
679
+ lastBuilt = built;
680
+ progress(`job ${job.id}: regenerated ${built.regenerated.join(', ')}`);
681
+ }
682
+
683
+ // D-2/D-3 (WP-0): the refresh pass. Unlike regenerate this is not limited to a conflict — a
684
+ // tree-derived pin (the closure policy's `sourceBindings` digests) is stale the moment EITHER
685
+ // side touches a bound source, which is exactly the "one side changed server.js" case that never
686
+ // produces a textual conflict. The post-refresh tree is what the gate sees, what
687
+ // `readCachedVerdict` keys on, what `job.tree` records and what the CAS fast-forwards.
688
+ const refreshEntries = train.matchingRefreshEntries(config);
689
+ if (built.kind === 'train' && refreshEntries.length) {
690
+ try {
691
+ built = await train.refreshTrain({
692
+ cwd: workdir, dir: trainDir, job, built, entries: refreshEntries, log: logFile, signal,
693
+ });
694
+ } catch (error) {
695
+ // Same stop checkpoint as the regenerate block above: a `stopped` entry is a stop, never a
696
+ // failure; anything else (a real refresh failure) is rethrown and becomes the job's failure.
697
+ if (error && error.code === 'stopped') {
698
+ const requestDuringRefresh = pendingStop();
699
+ if (requestDuringRefresh) return stopBeforeLanding(requestDuringRefresh);
700
+ }
701
+ throw error;
702
+ }
703
+ lastBuilt = built;
704
+ if (built.refreshed.length) progress(`job ${job.id}: refreshed ${built.refreshed.join(', ')}`);
705
+ }
706
+
707
+ // WP-2 (§5 D-4, §9 S-7): before the legacy tree-keyed cache, ask the PROOF store. A fast-forward
708
+ // train (`built.train === job.tip`) is exactly what the author's own commit gate gated — the
709
+ // hook records `<tree>.<planBaseTree>` for it (§5 D-1, §9 S-5) — so a train whose whole chain is
710
+ // proven lands with no gate run at all. When the target moved, `built.train` is a merge commit
711
+ // the branch never proved, and the chain is not even consulted.
712
+ let proven = null;
713
+ if (built.train === job.tip) {
714
+ const chain = proof.proofChain(workdir, ctx.root, {
715
+ base: built.base, tip: job.tip, gatesHash: gatesHash(config.gates),
716
+ });
717
+ if (chain.ok) proven = chain;
718
+ }
719
+ // D-4: only a PASS is reusable (`readCachedVerdict` refuses to replay a FAIL, so a flake is
720
+ // retryable by resubmitting). A cached PASS skips the gate entirely. A PROVEN tree is stronger
721
+ // than a cached one, so the legacy store is not consulted for it.
722
+ const cached = proven ? null : gate.readCachedVerdict(ctx.root, built.tree, gatesHash(config.gates));
723
+ let verdict;
724
+ let reason = null;
725
+ if (proven) {
726
+ verdict = 'PASS';
727
+ reason = null;
728
+ gateSeconds = 0;
729
+ gateTelemetry = {
730
+ mode: 'proven', planBase: built.base, selected: null, executed: 0, cached: null,
731
+ missReasons: null, fallbackReason: null, intersection: null,
732
+ };
733
+ progress(`job ${job.id}: verdict PROVEN (proof chain of ${proven.checked} commits)`);
734
+ } else if (cached) {
735
+ verdict = cached.verdict;
736
+ reason = null;
737
+ gateSeconds = cached.seconds;
738
+ // WP-0: a verdict-cache hit ran no gate for this job, so it reports the mode that says so.
739
+ gateTelemetry = {
740
+ mode: 'cached', planBase: null, selected: null, executed: null, cached: null,
741
+ missReasons: null, fallbackReason: null, intersection: null,
742
+ };
743
+ progress(`job ${job.id}: verdict CACHED (${verdict})`);
744
+ // WP-0: a regenerate/refresh pass materialized the train worktree and only `gate.runGate`'s
745
+ // finally removes it; a cached PASS never runs the gate, so the worktree is dropped here.
746
+ if (fs.existsSync(trainDir)) progress(`job ${job.id}: train worktree ${JSON.stringify(train.removeTrainWorktree(workdir, trainDir))}`);
747
+ } else {
748
+ const result = await gate.runGate({
749
+ cwd: workdir, dir: trainDir, config, job, built, token, root: ctx.root, logFile, sourceDir, signal,
750
+ // WP-3a (§5 D-3, §6.5, §8 WP-3a): a MERGE train (`built.train !== job.tip`) is measured for S
751
+ // = sel(B→T) ∩ sel(P→T) while the gate still runs today's selection. A fast-forward train is
752
+ // already proved or gated as today, so it asks for no measurement (null, not a no-op closure).
753
+ onTrainReady: built.train === job.tip ? null : (dir) => mergeDelta.mergeDeltaSelection({
754
+ trainDir: dir, root: ctx.root, base: built.base, tip: job.tip, gatesHash: gatesHash(config.gates), env: process.env,
755
+ }),
756
+ });
757
+ verdict = result.verdict;
758
+ reason = result.reason;
759
+ gateSeconds = result.seconds;
760
+ gateTelemetry = gateSelectionTelemetry(result.selection);
761
+ // §6.6: the shadow record rides on `gate.intersection`, and the full S goes to
762
+ // `logs/<jobId>.merge-delta.json` (the job keeps its sha256 and the OQ-2 counters).
763
+ if (result.shadow) {
764
+ gateTelemetry = mergeDelta.recordIntersection(gateTelemetry, result.shadow, {
765
+ root: ctx.root, jobId: job.id, verdict, logFile: result.logPath,
766
+ });
767
+ }
768
+ progress(`job ${job.id}: gate ${verdict === null ? 'stopped' : verdict}${reason ? ` (${reason})` : ''} in ${result.seconds}s${result.shadow ? ` [merge-delta shadow ${result.shadow.mode}]` : ''}`);
769
+ // WP-2 (§5 D-1, §9 S-8): the train's own PASS is a proof too, so the NEXT train built on this
770
+ // tree (§5 D-3a's `proof(tree(B))`) — or a resubmit of it (§5 D-4) — needs no gate run. A
771
+ // refusal (a FLAKY rerun, a dependency change) or an unwritable store is reported and never
772
+ // changes this job's outcome: the gate already decided, and a proof is an optimization.
773
+ //
774
+ // The train's PASS goes through the SAME D-1 refusals the hook's PASS does — the runner is not
775
+ // a more trustworthy witness than the author's own commit gate, and a green run that contains a
776
+ // FLAKY rerun (or whose delta against the plan base touches a dependency manifest) is exactly
777
+ // what §5 D-1 says must never become a proof.
778
+ //
779
+ // ONE term is the hook's alone: `no_pass` asks whether the log carries the gate's own terminal
780
+ // line, and the runner does not need that witness — gate.js derives PASS from the gate's EXIT
781
+ // STATUS (`gate.js:234`, `const verdict = reason ? 'FAIL' : 'PASS'`), so a repository whose
782
+ // `gates[].run` is a plain script that prints no `[test-gate] VERDICT:` line is a legitimate
783
+ // configuration, and honouring `no_pass` here would make D-4 unreachable for every such repo
784
+ // (and for this repo's own queue fixtures, `tests-integration/fixtures/landing/gate-sentinel.sh`).
785
+ // The refusals the runner CAN witness from the same log — a FLAKY rerun, a dependency-changing
786
+ // delta — still apply.
787
+ //
788
+ // `result.logPath` is the job log, opened in APPEND mode and shared across the rounds of one
789
+ // job, so it can also carry an earlier round's lines; that can only make the refusal MORE
790
+ // conservative (a stale FLAKY line refuses a proof that would otherwise be recorded), never a
791
+ // false proof. An unreadable log is a skip, not an error — the job has already landed.
792
+ if (result.verdict === 'PASS') {
793
+ try {
794
+ const logText = fs.readFileSync(result.logPath, 'utf8');
795
+ const refused = proof.proofRefusal(workdir, {
796
+ planBase: built.base, tree: built.tree, logText,
797
+ });
798
+ const fatal = refused && refused !== 'no_pass' ? refused : null;
799
+ if (fatal) {
800
+ progress(`job ${job.id}: proof not recorded (${fatal})`);
801
+ } else {
802
+ proof.recordProof(ctx.root, {
803
+ tree: built.tree,
804
+ planBaseTree: revParse(workdir, `${built.base}^{tree}`),
805
+ gatesHash: gatesHash(config.gates),
806
+ verdict: 'PASS',
807
+ source: 'train',
808
+ commit: built.train,
809
+ seconds: result.seconds,
810
+ at: new Date().toISOString(),
811
+ logPath: result.logPath,
812
+ selection: result.selection,
813
+ });
814
+ }
815
+ } catch (error) {
816
+ progress(`job ${job.id}: proof not recorded (${error.code || 'error'}: ${error.message})`);
817
+ }
818
+ }
819
+ }
820
+ const jobVerdict = proven ? 'PROVEN' : (cached ? 'CACHED' : verdict);
821
+ const jobGateLog = cached ? (cached.logPath || null) : logFile;
822
+
823
+ queue.updateJob(ctx.root, job.id, {
824
+ base: built.base,
825
+ tree: built.tree,
826
+ train: built.train,
827
+ verdict: jobVerdict,
828
+ gateLog: jobGateLog,
829
+ gate: gateTelemetry,
830
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
831
+ });
832
+
833
+ // Checkpoint 3 (D-5): the gate has returned and its result is on the job record. A cancel that
834
+ // arrived while it ran stops the job here — the target is never moved — and D-6's stopped gate
835
+ // (verdict null, reason gate_stopped) is not a verdict to act on. A PREEMPT here requeues the
836
+ // job unless the gate PASSed: the work is done, so the request is moot (R-3) and is cleared.
837
+ const requestAfterGate = pendingStop();
838
+ if (requestAfterGate) {
839
+ if (requestAfterGate.kind === 'cancel' || verdict !== 'PASS') return stopBeforeLanding(requestAfterGate);
840
+ queue.updateJob(ctx.root, job.id, { stopRequest: null });
841
+ }
842
+
843
+ if (verdict !== 'PASS') {
844
+ // D-6: a stopped gate with no cancel request behind it must still be decidable — it is a
845
+ // failure of this run (the job never landed), never a PASS.
846
+ if (reason === 'gate_stopped') {
847
+ progress(`job ${job.id}: gate stopped with no cancel request`);
848
+ return queue.finishJob(ctx.root, job.id, {
849
+ state: 'failed',
850
+ reason: 'gate_stopped',
851
+ verdict: jobVerdict,
852
+ base: built.base,
853
+ tree: built.tree,
854
+ train: built.train,
855
+ gateLog: jobGateLog,
856
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
857
+ }, { gateSeconds, gate: gateTelemetry });
858
+ }
859
+ // D-9 Tier A, computed per job and never by running a second gate.
860
+ const overlap = train.overlapWithTarget(workdir, built.base, job.tip);
861
+ const trainOnly = built.train !== job.tip && overlap.length > 0;
862
+ const failures = ['gate_timeout', 'gate_dirtied_tree'];
863
+ const finalReason = failures.includes(reason) ? reason : (trainOnly ? 'train_only' : 'gate_failed');
864
+ // D-6/D-7 (WP-2): diagnose on base before reporting — a `base_broken` job is left queued (it is
865
+ // main that is red, not this branch) and only every other outcome is a failure of this run.
866
+ const diagnosis = await triageFailedJob(ctx, {
867
+ config, job, cached, reason: finalReason, jobVerdict, built, logFile: jobGateLog, workdir, signal, gateSeconds,
868
+ });
869
+ if (diagnosis.requeued) return queue.readJob(ctx.root, job.id);
870
+ progress(`job ${job.id}: failed (${finalReason})`);
871
+ return queue.finishJob(ctx.root, job.id, {
872
+ state: 'failed',
873
+ reason: finalReason,
874
+ verdict: jobVerdict,
875
+ base: built.base,
876
+ tree: built.tree,
877
+ train: built.train,
878
+ overlapWithTarget: overlap,
879
+ gateLog: jobGateLog,
880
+ triage: diagnosis.triage,
881
+ timestamps: { ...job.timestamps, gated: queue.nowIso() },
882
+ }, { gateSeconds: diagnosis.gateSeconds, gate: gateTelemetry });
883
+ }
884
+
885
+ // AC-3e/test hook: hold the runner here for LAND_TEST_PAUSE_BEFORE_LAND_MS so a stop request can
886
+ // be written inside the R-3 window (between checkpoint 3 and the land). Inert when unset.
887
+ const pauseBeforeLand = intFlag('LAND_TEST_PAUSE_BEFORE_LAND_MS', 0);
888
+ if (pauseBeforeLand > 0) await delay(pauseBeforeLand);
889
+
890
+ // Checkpoint 4 (D-5): the last read before the target moves. A cancel that arrived after
891
+ // checkpoint 3 stops the job here; the gate PASSed, but a cancel never lands. A preempt is moot
892
+ // for the same reason as at checkpoint 3 — the work is done — so it is cleared and the job lands.
893
+ const requestBeforeLand = pendingStop();
894
+ if (requestBeforeLand) {
895
+ if (requestBeforeLand.kind === 'cancel') return finishCanceled(requestBeforeLand);
896
+ queue.updateJob(ctx.root, job.id, { stopRequest: null });
897
+ }
898
+
899
+ // D-5/D-11: land the train the gate APPROVED, comparing against `built.base` — the commit this
900
+ // train was built on — never against a freshly re-read tip. A re-read is a check-then-act gap:
901
+ // a commit landing in between would be silently dropped when the swap moved the target to a
902
+ // train that no longer contains it. `cas_lost` is that race, and it is handled exactly like a
903
+ // target that moved before the gate started: record the foreign commits and retrain, which
904
+ // counts toward MAX_ROUNDS.
905
+ let landed;
906
+ try {
907
+ landed = train.landTrain(workdir, config.target, built.train, built.base, { env: { LAND_TOKEN: token } });
908
+ } catch (error) {
909
+ if (error && error.code === 'target_dirty') {
910
+ progress(`job ${job.id}: target worktree has local changes overlapping the train (${error.paths.join(', ')})`);
911
+ return queue.finishJob(ctx.root, job.id, {
912
+ state: 'blocked',
913
+ reason: 'target_dirty',
914
+ paths: error.paths,
915
+ verdict: jobVerdict,
916
+ base: built.base,
917
+ tree: built.tree,
918
+ train: built.train,
919
+ gateLog: jobGateLog,
920
+ gate: gateTelemetry,
921
+ }, { gateSeconds, gate: gateTelemetry });
922
+ }
923
+ if (error && error.code === 'cas_lost') {
924
+ const moved = error.current || revParseOrNull(workdir, branchRef(config.target));
925
+ foreignCommits.push(...train.commitsBetween(workdir, built.base, moved));
926
+ // Persisted, not just held in memory: a retrain that then SUCCEEDS has to keep saying which
927
+ // commits landed under it, or the record of the race disappears the moment the job lands.
928
+ queue.updateJob(ctx.root, job.id, { foreignCommits: [...foreignCommits] });
929
+ progress(`job ${job.id}: target moved to ${String(moved).slice(0, 12)} (cas_lost); retraining`);
930
+ continue;
931
+ }
932
+ throw error;
933
+ }
934
+
935
+ progress(`job ${job.id}: landed ${String(built.train).slice(0, 12)} via ${landed.via}`);
936
+ const landedJob = queue.finishJob(ctx.root, job.id, {
937
+ state: 'landed',
938
+ reason: null,
939
+ verdict: jobVerdict,
940
+ base: built.base,
941
+ tree: built.tree,
942
+ train: built.train,
943
+ landedVia: landed.via,
944
+ gateLog: jobGateLog,
945
+ gate: gateTelemetry,
946
+ timestamps: { ...job.timestamps, landed: queue.nowIso() },
947
+ }, { gateSeconds, gate: gateTelemetry });
948
+ // D-5 (§9 S-8): the target just moved, so every job still waiting is re-checked against the new
949
+ // tip BEFORE this runner takes the next one — the waiting session is told "merge main now" while
950
+ // it waits rather than discovering the conflict when its turn comes.
951
+ sweepConflictPreviews(ctx, workdir, config);
952
+ return landedJob;
953
+ }
954
+
955
+ progress(`job ${job.id}: the target moved on every round — blocked target_moving`);
956
+ return queue.finishJob(ctx.root, job.id, {
957
+ state: 'blocked',
958
+ reason: 'target_moving',
959
+ foreignCommits,
960
+ base: lastBuilt ? lastBuilt.base : null,
961
+ tree: lastBuilt ? lastBuilt.tree : null,
962
+ train: lastBuilt ? lastBuilt.train : null,
963
+ gate: gateTelemetry,
964
+ }, { gateSeconds, gate: gateTelemetry });
965
+ }
966
+
967
+ async function waitForTerminal(root, id, timeoutMs) {
968
+ const deadline = Date.now() + timeoutMs;
969
+ for (;;) {
970
+ const job = queue.readJob(root, id);
971
+ if (job && queue.TERMINAL_STATES.includes(job.state)) return job;
972
+ const remaining = deadline - Date.now();
973
+ if (remaining <= 0) return null;
974
+ await delay(Math.min(POLL_MS, remaining));
975
+ }
976
+ }
977
+
978
+ function exitCodeForState(jobState) {
979
+ if (jobState === 'landed') return 0;
980
+ if (jobState === 'failed' || jobState === 'blocked' || jobState === 'canceled') return 1;
981
+ return 0;
982
+ }
983
+
984
+ // --- verbs -------------------------------------------------------------------------------------
985
+
986
+ // D-1/D-7 (§6.2): the one place a priority level and its reason are validated, shared by
987
+ // `land submit` and `land priority`. The message names the FLAG, because the caller is a human or an
988
+ // agent reading a usage error (exit 2), and the level is the only thing the queue module refuses.
989
+ function parsePriorityLevel(rawPriority, rawReason) {
990
+ const level = rawPriority === undefined || rawPriority === null || rawPriority === '' ? 'normal' : String(rawPriority);
991
+ if (!queue.PRIORITIES.includes(level)) {
992
+ throw usageError('--priority must be one of normal, high, urgent');
993
+ }
994
+ const reason = rawReason === undefined || rawReason === null ? '' : String(rawReason);
995
+ // D-7: high and urgent are a request for someone else's attention, so they carry the reason for it.
996
+ if (level !== 'normal' && !reason.trim()) throw usageError('--reason is required for high and urgent');
997
+ if (reason.length > queue.PRIORITY_REASON_MAX) throw usageError(`--reason is at most ${queue.PRIORITY_REASON_MAX} characters`);
998
+ return { level, reason: reason || null };
999
+ }
1000
+
1001
+ // D-7: who asked. A loopback CLI has no identity to check (there is no owner check, matching
1002
+ // `land cancel`), so the audit trail records the session, the pid and the directory it ran from.
1003
+ function priorityBy(cwd) {
1004
+ return { session: strFlag('CTM_SESSION_ID') || null, pid: process.pid, cwd };
1005
+ }
1006
+
1007
+ // D-11/§6.2: `[high]` / `[urgent]` after the state, and nothing for `normal` (the common case stays
1008
+ // as narrow as it was).
1009
+ function priorityTag(level) {
1010
+ return level && level !== 'normal' ? ` [${level}]` : '';
1011
+ }
1012
+
1013
+ async function cmdSubmit(opts, cwd) {
1014
+ // Validated before anything touches the repository: a bad level or a missing reason is a usage
1015
+ // error whether or not the caller is even standing in a git worktree.
1016
+ const { level, reason } = parsePriorityLevel(opts.values.priority, opts.values.reason);
1017
+ const by = priorityBy(cwd);
1018
+ const ctx = context(cwd);
1019
+ // D-1/I-5: gates are resolved from the TARGET's committed policy, never the submitter's working
1020
+ // tree — a branch cannot weaken the gate that decides whether it lands.
1021
+ const config = loadTargetLandingConfig(cwd);
1022
+ const branch = opts.values.branch || currentBranch(cwd);
1023
+ const tip = revParse(cwd, branchRef(branch));
1024
+
1025
+ const unmerged = unmergedPaths(cwd);
1026
+ if (unmerged.length) {
1027
+ throw landingError('unmerged_paths', `${cwd} has unmerged paths: ${unmerged.join(', ')}`, { paths: unmerged });
1028
+ }
1029
+ const dirty = dirtyTrackedPaths(cwd);
1030
+ if (dirty.length) {
1031
+ progress(`warning: ${dirty.length} modified tracked file(s) in ${cwd}; the job lands the committed tip, not the working tree`);
1032
+ }
1033
+ queue.reapDeadRunner(ctx.root);
1034
+
1035
+ // D-11/I-5: reads the CTM session through `lib/env-flags.js` (the one flag reader) rather than
1036
+ // raw, so the flag-read ratchet needs no baseline entry for this file.
1037
+ const session = strFlag('CTM_SESSION_ID') || null;
1038
+ let job = queue.activeJobFor(ctx.root, branch, tip);
1039
+ // D-9: an already-active job for this branch@tip is re-prioritized rather than duplicated — the
1040
+ // same call an agent already makes is the bump verb. WITHOUT `--priority` the existing level is
1041
+ // kept, so a plain re-submit can never silently demote a bumped job.
1042
+ let preempt = null;
1043
+ if (job) {
1044
+ progress(`job ${job.id} already ${job.state} for ${branch}@${tip.slice(0, 12)}`);
1045
+ if (opts.values.priority !== undefined) {
1046
+ const updated = queue.setPriority(ctx.root, job.id, { priority: level, reason, by, via: 'submit' });
1047
+ job = updated.job;
1048
+ preempt = updated.preempt;
1049
+ }
1050
+ } else {
1051
+ if (!config.gates.length) {
1052
+ throw landingError(
1053
+ 'config_gates_missing',
1054
+ `no landing.gates configured in ${config.configDir} and no test command detected; a queue that gates nothing is not a queue`,
1055
+ { configDir: config.configDir },
1056
+ );
1057
+ }
1058
+ const duplicate = queue.duplicateSubmitter(ctx.root, session, process.pid, branch);
1059
+ if (duplicate) {
1060
+ // WP-0 D-8 (§9 S-13, AC-0a): a refusal is an EVENT. It is thrown before any job record exists,
1061
+ // so until now the queue could not see its own back-pressure at all (census §1).
1062
+ queue.recordRefusal(ctx.root, { reason: 'duplicate_submitter', session, branch });
1063
+ throw landingError(
1064
+ 'duplicate_submitter',
1065
+ `session ${session} already has job ${duplicate.id} (pid ${duplicate.submittedBy.pid}) in ${duplicate.state}`,
1066
+ { jobId: duplicate.id, pid: duplicate.submittedBy.pid },
1067
+ );
1068
+ }
1069
+ // D-4 (§9 S-7): decide the conflict HERE, not forty minutes later. `mergeTree` is the same oracle
1070
+ // the runner gates on and it writes no commit object — the tree it prints is garbage the moment
1071
+ // this process exits — so the precheck costs one `git merge-tree` and nothing else (§1.1: 24 jobs
1072
+ // waited 2,592 minutes to be blocked on a conflict that was already visible at submit).
1073
+ const targetTip = revParse(cwd, branchRef(config.target));
1074
+ const merged = mergeTree(cwd, targetTip, tip);
1075
+ let conflictPreview = null;
1076
+ if (merged.conflicts.length) {
1077
+ const { uncovered, covered } = pathCoverage(config, merged.conflicts);
1078
+ if (uncovered.length && !opts.flags.has('queue-anyway')) {
1079
+ // WP-0 D-8: the second refusal, recorded the same way (the file is only written when the
1080
+ // submit really is refused — `--queue-anyway` is an acceptance, not a refusal).
1081
+ queue.recordRefusal(ctx.root, { reason: 'conflict_at_submit', session, branch });
1082
+ throw landingError(
1083
+ 'conflict_at_submit',
1084
+ `${branch}@${tip.slice(0, 12)} conflicts with ${config.target} on ${uncovered.join(', ')}; merge ${config.target} into the branch first, or pass --queue-anyway`,
1085
+ { paths: merged.conflicts, uncovered, covered },
1086
+ );
1087
+ }
1088
+ // Either every path is owned by a regenerate/refresh entry (the runner resolves it) or the
1089
+ // submitter said `--queue-anyway` (it expects an earlier job to absorb the overlap): the job is
1090
+ // accepted, and only a conflict the QUEUE cannot resolve starts life with a preview.
1091
+ conflictPreview = uncovered.length
1092
+ ? { base: targetTip, paths: merged.conflicts, checkedAt: queue.nowIso() }
1093
+ : null;
1094
+ }
1095
+ job = {
1096
+ contract: CONTRACT,
1097
+ id: queue.newJobId(),
1098
+ repo: ctx.common,
1099
+ branch,
1100
+ tip,
1101
+ title: opts.values.title || commitSubject(cwd, tip),
1102
+ submittedBy: { session, pid: process.pid, cwd },
1103
+ state: 'queued',
1104
+ reason: null,
1105
+ // D-1/D-7: the level is the flag or `normal`; the audit trail opens with the `from: 'normal'`
1106
+ // entry only when the job was not submitted normal (a normal job has nothing to record).
1107
+ priority: level,
1108
+ priorityHistory: level === 'normal' ? [] : [{ from: 'normal', to: level, reason, via: 'submit', at: queue.nowIso(), by }],
1109
+ preemptions: 0,
1110
+ base: null,
1111
+ train: null,
1112
+ tree: null,
1113
+ verdict: null,
1114
+ conflictPaths: [],
1115
+ // D-4/D-5: the preview the job starts life with — null unless the submitter took a conflict the
1116
+ // queue cannot resolve with `--queue-anyway` (a covered conflict IS resolved by the runner, so
1117
+ // there is nothing for the submitter to merge main for).
1118
+ conflictPreview,
1119
+ overlapWithTarget: [],
1120
+ policySource: config.policySource,
1121
+ runnerPid: null,
1122
+ timestamps: { queued: queue.nowIso() },
1123
+ gateLog: null,
1124
+ };
1125
+ queue.enqueue(ctx.root, job);
1126
+ // D-4/§6.2: a FRESH urgent job asks for the running lower-ranked job to be stopped, the same
1127
+ // decision `land priority` makes — one implementation, called after the enqueue so the queue
1128
+ // already holds the new job when the request is written.
1129
+ if (level === 'urgent') preempt = queue.requestPreemptFor(ctx.root, job.id, { by, reason });
1130
+ }
1131
+
1132
+ // D-2: a submit must return in well under a second even when the gate takes 40 minutes, so it
1133
+ // never drains in the foreground. If a runner is already live this process is just a producer
1134
+ // (`runner: 'other'`); otherwise it starts a DETACHED `land run` child — on demand, never an idle
1135
+ // daemon — which claims the lease, drains the queue and exits when the queue is empty.
1136
+ const holder = lease.read(ctx.root);
1137
+ const live = holder && !lease.isDead(holder) ? holder : null;
1138
+ let runner;
1139
+ let runnerPid;
1140
+ if (live) {
1141
+ runner = 'other';
1142
+ runnerPid = live.pid;
1143
+ } else {
1144
+ runner = 'self';
1145
+ const started = spawnRunner(ctx);
1146
+ runnerPid = started ? started.pid : null;
1147
+ }
1148
+ if (runnerPid !== null) queue.updateJob(ctx.root, job.id, { runnerPid });
1149
+
1150
+ const position = queue.positionOf(ctx.root, job.id);
1151
+ const payload = {
1152
+ contract: CONTRACT,
1153
+ jobId: job.id,
1154
+ position,
1155
+ queued: queue.readQueue(ctx.root).order.length,
1156
+ runner,
1157
+ eta_s: queue.etaSeconds(ctx.root, position),
1158
+ // §6.2: the level this job now HOLDS — read back off the job, never the parsed flag, so a plain
1159
+ // re-submit (no `--priority`) of an already-bumped job reports `high`, not the `normal` default
1160
+ // (§6.1 `priorityOf`). `preempt` is null unless this submit asked for `urgent`, and then it is
1161
+ // the D-4 outcome — `'requested'` having really written the running job's stop request (WP-3).
1162
+ priority: queue.priorityOf(job),
1163
+ preempt,
1164
+ };
1165
+
1166
+ if (opts.flags.has('wait')) {
1167
+ // `--wait` still waits for the job's terminal state; the work itself happens in the runner
1168
+ // child started above (or in whoever already held the lease).
1169
+ const final = await waitForTerminal(ctx.root, job.id, WAIT_TIMEOUT_MS);
1170
+ if (!final) {
1171
+ emitJsonOrLine(opts, { ...payload, state: 'queued', reason: null, verdict: null }, `job ${job.id} did not reach a terminal state within ${WAIT_TIMEOUT_S}s`);
1172
+ return 75;
1173
+ }
1174
+ emitJsonOrLine(
1175
+ opts,
1176
+ { ...payload, position: null, state: final.state, reason: final.reason, verdict: final.verdict },
1177
+ `job ${job.id} ${final.state}${final.reason ? ` (${final.reason})` : ''}`,
1178
+ );
1179
+ return exitCodeForState(final.state);
1180
+ }
1181
+
1182
+ emitJsonOrLine(
1183
+ opts,
1184
+ payload,
1185
+ `job ${job.id} queued at position ${position} of ${payload.queued}${priorityTag(payload.priority)} (runner: ${runner})`
1186
+ + `${payload.eta_s === null ? '' : `, eta ~${payload.eta_s}s`}`
1187
+ + preemptSuffix(preempt, preemptTargetOf(ctx.root, job.id)),
1188
+ );
1189
+ return 0;
1190
+ }
1191
+
1192
+ // §6.2: the human line says which job an urgent bump is stopping, so a caller never has to read the
1193
+ // job files to find out. It names the job the request was written on — the runner's `in_progress`
1194
+ // job, which is the queue head — and nothing at all when `preempt` is not `'requested'`.
1195
+ function preemptSuffix(preempt, targetId) {
1196
+ return preempt === 'requested' && targetId ? `; stopping job ${targetId}` : '';
1197
+ }
1198
+
1199
+ // D-5 (§9 S-9): the D-4/D-5 preview as the `land status` row's suffix. A job with no preview (the
1200
+ // overwhelmingly common case) prints exactly the row it printed before WP-1.
1201
+ function conflictSuffix(preview) {
1202
+ const paths = preview && Array.isArray(preview.paths) ? preview.paths : [];
1203
+ return paths.length ? ` will conflict: ${paths.join(', ')}` : '';
1204
+ }
1205
+
1206
+ function preemptTargetOf(root, urgentJobId) {
1207
+ for (const queuedId of queue.readQueue(root).order) {
1208
+ if (queuedId === urgentJobId) continue;
1209
+ const job = queue.readJob(root, queuedId);
1210
+ if (job && job.state === 'in_progress') return job.id;
1211
+ }
1212
+ return null;
1213
+ }
1214
+
1215
+ // §6.2 `land priority <jobId> normal|high|urgent [--reason <text>] [--json]`: bump (or drop) any
1216
+ // ACTIVE job. It never touches the job's state — only its rank and its audit trail — so a job that
1217
+ // is gating keeps gating and a queued job keeps its place in line at its new level.
1218
+ function cmdPriority(opts, cwd) {
1219
+ const ctx = context(cwd);
1220
+ const id = opts.positionals[0];
1221
+ const rawLevel = opts.positionals[1];
1222
+ if (!id || !rawLevel) throw usageError('land priority <jobId> normal|high|urgent [--reason <text>] [--json]');
1223
+ const { level, reason } = parsePriorityLevel(rawLevel, opts.values.reason);
1224
+ const result = queue.setPriority(ctx.root, id, { priority: level, reason, by: priorityBy(cwd), via: 'priority' });
1225
+ const total = queue.readQueue(ctx.root).order.length;
1226
+ if (opts.flags.has('json')) {
1227
+ emitJson({
1228
+ contract: CONTRACT,
1229
+ jobId: result.job.id,
1230
+ priority: level,
1231
+ changed: result.changed,
1232
+ position: result.position,
1233
+ preempt: result.preempt,
1234
+ });
1235
+ return 0;
1236
+ }
1237
+ if (!result.changed) {
1238
+ emitLine(`job ${id} already ${level}`);
1239
+ return 0;
1240
+ }
1241
+ const history = Array.isArray(result.job.priorityHistory) ? result.job.priorityHistory : [];
1242
+ const previous = history.length ? history[history.length - 1].from : 'normal';
1243
+ emitLine(
1244
+ `job ${id} priority ${level} (was ${previous}), position ${result.position} of ${total}`
1245
+ + preemptSuffix(result.preempt, preemptTargetOf(ctx.root, id)),
1246
+ );
1247
+ return 0;
1248
+ }
1249
+
1250
+ // The detached runner: `node <this file> run`, in its own process group with its output in
1251
+ // `landing/logs/runner-<ts>.log`, unref'd so the submitter exits immediately. The child claims the
1252
+ // lease itself (a racing child simply finds one and exits), which is what keeps "exactly one runner"
1253
+ // true without the submitter holding a lock across a 40-minute gate.
1254
+ function spawnRunner(ctx) {
1255
+ const dir = queue.ensureLogsDir(ctx.root);
1256
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1257
+ const logPath = path.join(dir, `runner-${stamp}-${process.pid}.log`);
1258
+ const fd = fs.openSync(logPath, 'a');
1259
+ try {
1260
+ const child = spawn(process.execPath, [__filename, 'run'], {
1261
+ cwd: ctx.cwd,
1262
+ env: childEnv(),
1263
+ detached: true,
1264
+ stdio: ['ignore', fd, fd],
1265
+ });
1266
+ child.on('error', (error) => {
1267
+ // Report, never throw: the job is already queued and the next `land submit` (or `land run`)
1268
+ // starts a runner again, so a failed spawn must not turn into a stack trace here.
1269
+ progress(`runner failed to start: ${error.message}`);
1270
+ });
1271
+ child.unref();
1272
+ progress(`runner ${child.pid} started (log: ${logPath})`);
1273
+ return { pid: child.pid, logPath };
1274
+ } finally {
1275
+ fs.closeSync(fd);
1276
+ }
1277
+ }
1278
+
1279
+ function emitJsonOrLine(opts, payload, human) {
1280
+ if (opts.flags.has('json')) emitJson(payload);
1281
+ else emitLine(human);
1282
+ }
1283
+
1284
+ // Claim the lease, with one extra attempt when the first loss was a dead holder: `lease.claim`
1285
+ // reaps a dead holder internally, so a second call is the "the path is free now" case rather than
1286
+ // a spin.
1287
+ function claimRunner(ctx, jobId) {
1288
+ const first = lease.claim(ctx.root, { jobId });
1289
+ if (first) return first;
1290
+ const holder = lease.read(ctx.root);
1291
+ if (!holder || lease.isDead(holder)) return lease.claim(ctx.root, { jobId });
1292
+ return null;
1293
+ }
1294
+
1295
+ async function cmdRun(opts, cwd) {
1296
+ const ctx = context(cwd);
1297
+ queue.reapDeadRunner(ctx.root);
1298
+ const record = claimRunner(ctx, null);
1299
+ if (!record) {
1300
+ const holder = lease.read(ctx.root);
1301
+ progress(`another runner holds the lease${holder ? ` (pid ${holder.pid})` : ''}`);
1302
+ return 0;
1303
+ }
1304
+ const processed = await drain(ctx, record, { once: opts.flags.has('once') });
1305
+ progress(processed ? `drained ${processed} job(s)` : 'queue is empty');
1306
+ return 0;
1307
+ }
1308
+
1309
+ function cmdStatus(opts, cwd) {
1310
+ const dir = opts.values.repo ? path.resolve(opts.values.repo) : cwd;
1311
+ const ctx = context(dir);
1312
+ let config = null;
1313
+ let configError = null;
1314
+ try {
1315
+ config = loadLandingConfig(dir);
1316
+ } catch (error) {
1317
+ configError = { code: typeof error.code === 'string' ? error.code : 'unexpected', message: error.message };
1318
+ }
1319
+ const order = queue.readQueue(ctx.root).order;
1320
+ // OQ-3: one metrics read for the whole listing — the ETA is position × the median of the last 20
1321
+ // gate seconds, and stays `null` (never a guess) until 20 samples exist.
1322
+ const etaSamples = queue.recentGateSeconds(ctx.root, queue.ETA_SAMPLES);
1323
+ const etaFor = (position) => (position !== null && etaSamples.length >= queue.ETA_SAMPLES
1324
+ ? Math.round(position * queue.median(etaSamples))
1325
+ : null);
1326
+ const jobs = queue.listJobs(ctx.root).slice(-50).map((job) => {
1327
+ const index = order.indexOf(job.id);
1328
+ const position = index === -1 ? null : index + 1;
1329
+ return { ...queue.jobSummary(job), position, eta_s: etaFor(position) };
1330
+ });
1331
+ const radarSummary = statusRadar(dir, configError, opts.values['radar-for']);
1332
+ // WP-1 (§6.2): the origin half of the status — ahead/behind against the LOCAL tracking ref (no
1333
+ // fetch: `land status` stays a read verb) plus the last sync verdict from `landing/sync-state.json`.
1334
+ const origin = syncCli.statusOrigin(dir, ctx.common, config);
1335
+ const payload = {
1336
+ contract: CONTRACT,
1337
+ repo: ctx.common,
1338
+ target: config ? config.target : null,
1339
+ origin,
1340
+ queue: { depth: order.length, order },
1341
+ runner: queue.runnerSummary(ctx.root),
1342
+ jobs,
1343
+ // WP-0 D-8: the day's refusal count (the `land status` line, and a field for the CTM probe).
1344
+ refusedToday: queue.refusalsToday(ctx.root),
1345
+ config: configError
1346
+ ? { ok: false, error: configError }
1347
+ : { ok: true, gatesSource: config.gatesSource, gates: config.gates.map((entry) => entry.name) },
1348
+ // §8 WP-3: present only for `--radar-for` (the CTM probe). Plain `status` is the queue alone —
1349
+ // the repo-wide radar is `land conflicts`.
1350
+ radar: radarSummary ? radarSummary.payload : null,
1351
+ };
1352
+ if (opts.flags.has('json')) {
1353
+ emitJson(payload);
1354
+ return 0;
1355
+ }
1356
+ emitLine(`repo: ${payload.repo}`);
1357
+ emitLine(`target: ${payload.target === null ? '(unresolved)' : payload.target}`);
1358
+ emitLine(`queue: ${payload.queue.depth} job(s)`);
1359
+ for (const job of jobs) {
1360
+ // §6.2 / D-11: the level rides on the row for a bumped job; a normal row is byte-identical to
1361
+ // what it printed before this change.
1362
+ // WP-0 (§6.6/D-8): a job whose gate explained itself says so — `gate <mode> <executed>/<selected>`.
1363
+ // D-5 (§9 S-9): a non-empty preview is the queue saying "this job will not merge as it stands" —
1364
+ // the suffix is the whole action the reader has to take (merge the target into the branch).
1365
+ // D-7 (WP-2): a job parked on a broken base is `queued` on disk but is not waiting for its turn —
1366
+ // it is waiting for MAIN to move, and the state cell says so rather than leaving the reader to
1367
+ // wonder why a queued job never starts. Every other row is byte-identical to what it printed
1368
+ // before this change.
1369
+ const stateCell = job.retryAfterTargetMoves
1370
+ ? 'base_broken (waiting for main to move)'
1371
+ : `${job.state}${job.reason ? ` (${job.reason})` : ''}`;
1372
+ emitLine(` ${job.position === null ? '-' : job.position} ${job.id} ${stateCell}${priorityTag(job.priority)} ${job.branch} ${job.title}${gateTag(job.gate)}${batchTag(job.batch)}${conflictSuffix(job.conflictPreview)}`);
1373
+ }
1374
+ emitLine(payload.runner ? `runner: pid ${payload.runner.pid}${payload.runner.jobId ? ` job ${payload.runner.jobId}` : ''}` : 'runner: none');
1375
+ // WP-0 D-8 (§8 WP-0): the back-pressure the queue could never see before this change. Always
1376
+ // printed (0 when none), so the line is a metric and not a hint.
1377
+ emitLine(`refused today: ${payload.refusedToday}`);
1378
+ // Only printed when the repository opted in — an unconfigured repo's status stays byte-identical
1379
+ // to what it printed before this change.
1380
+ const originLine = syncCli.originStatusLine(config, origin);
1381
+ if (originLine) emitLine(originLine);
1382
+ if (radarSummary) emitLine(radarSummary.line);
1383
+ return 0;
1384
+ }
1385
+
1386
+ // WP-0 (§6.6): `land status` prints `gate <mode> <executed>/<selected>` for a job that carries the
1387
+ // gate's self-description, and nothing at all for one that does not (every job written before this
1388
+ // change, and every gate that never ran). Null numbers print as `?`, never as 0.
1389
+ // WP-1 §6 (WP-1 step 10, AC-1f): the batch suffix on a member's row — ` [batch b-… 3/5]`. A job with
1390
+ // no `batch` (every job written before this change, and every singleton under a window of one) prints
1391
+ // nothing at all, so its row stays byte-identical to today's.
1392
+ function batchTag(batch) {
1393
+ if (!batch || typeof batch !== 'object' || typeof batch.id !== 'string') return '';
1394
+ const total = Array.isArray(batch.members) ? batch.members.length : null;
1395
+ const index = Number.isInteger(batch.index) ? batch.index + 1 : null;
1396
+ if (total === null || index === null) return ` [batch ${batch.id}]`;
1397
+ return ` [batch ${batch.id} ${index}/${total}]`;
1398
+ }
1399
+
1400
+ function gateTag(gate) {
1401
+ if (!gate || typeof gate !== 'object' || !gate.mode) return '';
1402
+ const value = (number) => (typeof number === 'number' && Number.isFinite(number) ? String(number) : '?');
1403
+ return ` gate ${gate.mode} ${value(gate.executed)}/${value(gate.selected)}`;
1404
+ }
1405
+
1406
+ // `land status` without `--radar-for` has no radar (null): even a cache-only sweep has to list and
1407
+ // dirty-probe every worktree, which was ~51 s of a plain `land status` on a 413-worktree repo, to
1408
+ // print one line `land conflicts` already gives. A repository whose target is missing (or whose
1409
+ // policy is unreadable) has no radar either — the summary then says so instead of pretending L0.
1410
+ //
1411
+ // §8 WP-3: `--radar-for a,b` is the CTM probe's variant. It runs a FULL sweep
1412
+ // restricted to the listed branches, so each pair among them and each against the target is
1413
+ // actually computed, and the payload carries `pairs` (per-branch detail the view joins to sessions)
1414
+ // and `branches` (which worktrees were in scope) beside the usual summary. Un-listed worktrees are
1415
+ // filtered out before any dirty probe (radar.scopeBranches), so they are never snapshotted.
1416
+ function statusRadar(dir, configError, radarFor) {
1417
+ if (radarFor === undefined) return null;
1418
+ const onlyBranches = parseBranchList(radarFor);
1419
+ const unavailable = (code) => ({
1420
+ payload: { max: null, atLeastL2: null, unknown: null, pairs: [], branches: [] },
1421
+ line: `radar: unavailable (${code})`,
1422
+ });
1423
+ if (configError) return unavailable(configError.code);
1424
+ let config;
1425
+ try {
1426
+ config = loadTargetLandingConfig(dir);
1427
+ } catch (error) {
1428
+ return unavailable(typeof error.code === 'string' ? error.code : 'unexpected');
1429
+ }
1430
+ // The sweep is the only step here that can still refuse (a target branch that does not exist, a
1431
+ // worktree list git will not print). `land status` is the READ verb: the queue, runner and job
1432
+ // listing are all still true when the radar cannot be computed, so a radar refusal degrades the
1433
+ // radar field and never fails the verb. (It also keeps the CTM probe total: a session's chip must
1434
+ // not go dark because the repository's target branch has not been created yet.)
1435
+ let sweep;
1436
+ try {
1437
+ sweep = radar.sweep(dir, { config, onlyBranches });
1438
+ } catch (error) {
1439
+ return unavailable(typeof error.code === 'string' ? error.code : 'unexpected');
1440
+ }
1441
+ const summary = radar.summarize(sweep.pairs);
1442
+ return {
1443
+ payload: {
1444
+ ...summary,
1445
+ pairs: sweep.pairs,
1446
+ branches: sweep.branches.map((entry) => ({ branch: entry.branch, tip: entry.tip, dirty: entry.dirty })),
1447
+ },
1448
+ line: `radar: max ${summary.max} · ${summary.atLeastL2} pairs ≥ L2 · ${summary.unknown} unknown (scope: ${onlyBranches.join(', ') || '(none)'})`,
1449
+ };
1450
+ }
1451
+
1452
+ // `--radar-for a,b` / `--radar-for a`. An empty value is an empty restriction (see radar.sweep),
1453
+ // which is what a caller with no branches to name deserves: no pairs, no snapshots.
1454
+ function parseBranchList(raw) {
1455
+ const names = String(raw).split(',').map((name) => name.trim()).filter(Boolean);
1456
+ return [...new Set(names)];
1457
+ }
1458
+
1459
+ async function cmdWait(opts, cwd) {
1460
+ const ctx = context(cwd);
1461
+ const id = opts.positionals[0];
1462
+ if (!id) throw usageError('land wait <jobId> [--timeout-s <seconds>]');
1463
+ let timeoutMs = WAIT_TIMEOUT_MS;
1464
+ if (opts.values['timeout-s'] !== undefined) {
1465
+ const seconds = Number(opts.values['timeout-s']);
1466
+ if (!Number.isFinite(seconds) || seconds <= 0) throw usageError('--timeout-s must be a positive number of seconds');
1467
+ timeoutMs = Math.round(seconds * 1000);
1468
+ }
1469
+ const existing = queue.jobOrThrow(ctx.root, id);
1470
+ const final = queue.TERMINAL_STATES.includes(existing.state)
1471
+ ? existing
1472
+ : await waitForTerminal(ctx.root, id, timeoutMs);
1473
+ if (!final) {
1474
+ emitLine(`job ${id} still ${queue.readJob(ctx.root, id).state} after ${Math.round(timeoutMs / 1000)}s`);
1475
+ return 75;
1476
+ }
1477
+ emitLine(`job ${id} ${final.state}${final.reason ? ` (${final.reason})` : ''}`);
1478
+ return exitCodeForState(final.state);
1479
+ }
1480
+
1481
+ function cmdInspect(opts, cwd) {
1482
+ const ctx = context(cwd);
1483
+ const id = opts.positionals[0];
1484
+ if (!id) throw usageError('land inspect <jobId>');
1485
+ const job = queue.jobOrThrow(ctx.root, id);
1486
+ emitJson({
1487
+ contract: CONTRACT,
1488
+ job,
1489
+ // WP-0 (§6.6/D-8): the same object the job carries, hoisted so a reader does not have to know
1490
+ // where the runner keeps it.
1491
+ gate: job.gate === undefined ? null : job.gate,
1492
+ gateLog: job.gateLog || null,
1493
+ gateLogExists: Boolean(job.gateLog && fs.existsSync(job.gateLog)),
1494
+ verdictFile: job.tree ? gate.verdictFile(ctx.root, job.tree) : null,
1495
+ trainDirHint: job.id,
1496
+ });
1497
+ return 0;
1498
+ }
1499
+
1500
+ // §6.4: `land record-proof` — the one thing the pre-commit hook calls after a green gate (§9 S-5:
1501
+ // today the hook checks the PASS and throws it away). It resolves the policy exactly as the runner
1502
+ // does (`loadTargetLandingConfig`, the `.ctm.json` committed AT the target) and computes the gates
1503
+ // hash with the same call the gate itself was run under (`config.gatesHash(config.gates)`, the
1504
+ // identical expression `lib/landing/gate.js` `runGate` evaluates), so a proof the hook wrote is
1505
+ // readable by the runner verbatim — a mismatch on either half is a cache miss, never a wrong hit.
1506
+ //
1507
+ // Exit 3 and no write for every refusal: a log that is not a PASS, a log carrying a runner's FLAKY
1508
+ // evidence, a diff against the plan base that touches a dependency manifest, or a repository with no
1509
+ // landing config at the target. It never fails the commit — the hook ignores a non-zero exit and
1510
+ // prints one line (§6.4). Exit 0 prints the proof path.
1511
+ function cmdRecordProof(opts, cwd) {
1512
+ const refuse = (reason) => {
1513
+ if (opts.flags.has('json')) emitJson({ contract: CONTRACT, ok: false, error: { code: reason } });
1514
+ else process.stderr.write(`[land] record-proof: refused (${reason})\n`);
1515
+ return 3;
1516
+ };
1517
+ const { tree, log } = opts.values;
1518
+ const planBase = opts.values['plan-base'];
1519
+ if (!tree || !planBase || !log) {
1520
+ throw usageError('land record-proof --tree <oid> --plan-base <rev> --log <path> --seconds <n> [--selection <json-file>] [--commit <sha>] [--json]');
1521
+ }
1522
+ const seconds = opts.values.seconds === undefined ? 0 : Number(opts.values.seconds);
1523
+ if (!Number.isFinite(seconds)) throw usageError('--seconds must be a number');
1524
+ let config;
1525
+ try {
1526
+ config = loadTargetLandingConfig(cwd);
1527
+ } catch (error) {
1528
+ /* expected: no landing config at the target (or no repository at all) is a REFUSAL by design
1529
+ (§6.4) — the exit code and the one-line message ARE the disposition */
1530
+ return refuse(typeof error.code === 'string' ? error.code : 'config_unreadable');
1531
+ }
1532
+ let planBaseTree;
1533
+ try {
1534
+ planBaseTree = revParse(cwd, `${planBase}^{tree}`);
1535
+ } catch (error) {
1536
+ /* expected: an unresolvable --plan-base is a refusal, not a crash — nothing can be keyed
1537
+ without the parent's tree */
1538
+ return refuse('plan_base_unresolved');
1539
+ }
1540
+ let logText;
1541
+ try {
1542
+ logText = fs.readFileSync(log, 'utf8');
1543
+ } catch (error) {
1544
+ /* expected: an unreadable gate log cannot be shown to be a PASS, so it is refused rather than
1545
+ guessed at */
1546
+ return refuse('log_unreadable');
1547
+ }
1548
+ const refused = proof.proofRefusal(cwd, { planBase, tree, logText });
1549
+ if (refused) return refuse(refused);
1550
+ let selection = null;
1551
+ if (opts.values.selection !== undefined) {
1552
+ try {
1553
+ selection = JSON.parse(fs.readFileSync(opts.values.selection, 'utf8'));
1554
+ } catch (error) {
1555
+ /* expected: a gate that wrote no --selection-out file, or a torn one, reads as "unknown" —
1556
+ the proof still records the PASS rather than refusing over a missing extra */
1557
+ selection = null;
1558
+ }
1559
+ }
1560
+ const file = proof.recordProof(context(cwd).root, {
1561
+ tree,
1562
+ planBaseTree,
1563
+ gatesHash: gatesHash(config.gates),
1564
+ verdict: 'PASS',
1565
+ source: 'commit',
1566
+ commit: opts.values.commit === undefined ? null : opts.values.commit,
1567
+ seconds,
1568
+ at: new Date().toISOString(),
1569
+ logPath: path.resolve(cwd, log),
1570
+ selection,
1571
+ });
1572
+ if (opts.flags.has('json')) emitJson({ contract: CONTRACT, ok: true, proof: file });
1573
+ else emitLine(file);
1574
+ return 0;
1575
+ }
1576
+
1577
+ // §8 WP-3a/AC-3b: `land report merge-delta` is the OQ-2 instrument — it aggregates the shadow records
1578
+ // the runner already wrote onto every merge train's `gate.intersection` (n trains, n per mode, the
1579
+ // median |S|/|sel(B→T)| and the count of failed trains with a failing test outside S). It reads the
1580
+ // job records and nothing else, so it works on a repo whose runner is not running. The aggregation
1581
+ // itself lives in lib/landing/merge-delta.js, beside the measurement it reports on.
1582
+ function cmdReport(opts, cwd) {
1583
+ const topic = opts.positionals[0];
1584
+ if (topic !== 'merge-delta') throw usageError('land report merge-delta [--json]');
1585
+ const report = mergeDelta.mergeDeltaReport(context(cwd).root);
1586
+ if (opts.flags.has('json')) emitJson({ contract: CONTRACT, ...report });
1587
+ else for (const line of mergeDelta.reportLines(report)) emitLine(line);
1588
+ return 0;
1589
+ }
1590
+
1591
+ // §6.2. A job that is not running is finished directly (only the runner writes in-progress
1592
+ // transitions, and there is no runner). For an `in_progress` job the runner is the only writer, so
1593
+ // this verb ASKS it to stop (D-5) and then waits for the runner to settle the job.
1594
+ //
1595
+ // D-10: a runner from an older `land.js` has no `stop` capability, so it would keep going and, on a
1596
+ // PASS, overwrite the `canceled` state with `landed`. Writing a state that runner will clobber is
1597
+ // worse than refusing: the refusal names the pid and says what to do about it.
1598
+ async function cmdCancel(opts, cwd) {
1599
+ const ctx = context(cwd);
1600
+ const id = opts.positionals[0];
1601
+ if (!id) throw usageError('land cancel <jobId> [--force]');
1602
+ const job = queue.jobOrThrow(ctx.root, id);
1603
+ if (queue.TERMINAL_STATES.includes(job.state)) {
1604
+ throw landingError('job_terminal', `job ${id} is already ${job.state}`, { jobId: id, state: job.state });
1605
+ }
1606
+ // D-6 (§9 S-2, WP-1): a member of a running train is a WITHDRAW — no `--force`; the refusal below
1607
+ // protects a caller's own singleton gate, which no one else is gated by.
1608
+ if (job.state === 'in_progress' && !opts.flags.has('force') && !stop.isBatchMemberInFlight(job, lease.read(ctx.root))) {
1609
+ throw landingError('job_in_progress', `job ${id} is in_progress; rerun with --force to cancel a running job`, { jobId: id });
1610
+ }
1611
+ const record = job.state === 'in_progress' ? lease.read(ctx.root) : null;
1612
+ // D-10: no lease or a dead lease means no runner is working this job — finish it directly, which
1613
+ // is exactly today's path and is now correct.
1614
+ if (job.state !== 'in_progress' || !record || lease.isDead(record)) {
1615
+ const next = queue.finishJob(ctx.root, id, {
1616
+ state: 'canceled',
1617
+ reason: null,
1618
+ canceledBy: { pid: process.pid, cwd: process.cwd(), at: queue.nowIso() },
1619
+ timestamps: { ...job.timestamps, canceled: queue.nowIso() },
1620
+ });
1621
+ emitLine(`job ${id} ${next.state}`);
1622
+ return 0;
1623
+ }
1624
+ if (!lease.hasCap(record, 'stop')) {
1625
+ throw landingError(
1626
+ 'runner_cannot_stop',
1627
+ `runner pid ${record.pid} predates the stop primitive; it would land job ${id} anyway — wait for it, or stop that runner`,
1628
+ { jobId: id, pid: record.pid },
1629
+ );
1630
+ }
1631
+ return await requestStopAndWait(ctx, id, record);
1632
+ }
1633
+
1634
+ // D-5: write the stop request under the mutex, then poll the job until the runner settles it. The
1635
+ // runner's heartbeat (10 s) plus the SIGKILL grace (10 s) bound how long the request can take to
1636
+ // land; a runner that has not settled by `CANCEL_WAIT_MS` is reported (exit 75), never waited on.
1637
+ async function requestStopAndWait(ctx, id, record) {
1638
+ queue.requestStop(ctx.root, id, { kind: 'cancel', by: priorityBy(ctx.cwd), reason: null });
1639
+ const deadline = Date.now() + CANCEL_WAIT_MS;
1640
+ for (;;) {
1641
+ const job = queue.readJob(ctx.root, id);
1642
+ if (job && queue.TERMINAL_STATES.includes(job.state)) {
1643
+ if (job.state === 'canceled') {
1644
+ // D-6 (§9 S-3): a withdrawn member's line says what happened to the train's other members.
1645
+ const members = job.batch && Array.isArray(job.batch.members) ? job.batch.members : [];
1646
+ if (members.length >= 2) emitLine(`job ${id} withdrawn from batch ${job.batch.id}; ${members.length - 1} other member(s) requeued`);
1647
+ else emitLine(`job ${id} canceled`);
1648
+ return 0;
1649
+ }
1650
+ // The gate PASSed before the request was read: the job landed, and that is the truth.
1651
+ emitLine(`job ${id} ${job.state} (the cancel arrived after the gate)`);
1652
+ return 1;
1653
+ }
1654
+ const remaining = deadline - Date.now();
1655
+ if (remaining <= 0) break;
1656
+ await delay(Math.min(CANCEL_POLL_MS, remaining));
1657
+ }
1658
+ emitLine(`cancel requested; runner pid ${record.pid} will stop job ${id} within ~20 s`);
1659
+ return 75;
1660
+ }
1661
+
1662
+ // --- the radar verbs (WP-2, §5 D-7, §6.2) --------------------------------------------------------
1663
+
1664
+ // §6.2 `land conflicts`: the advisory radar. Exit 0 whatever the levels — a conflict here is
1665
+ // information, not a refusal; `land submit` is what decides, and it decides on the gate.
1666
+ function cmdConflicts(opts, cwd) {
1667
+ const config = loadTargetLandingConfig(cwd);
1668
+ const result = radar.sweep(cwd, { config, withBranch: opts.values.with });
1669
+ if (opts.flags.has('json')) {
1670
+ emitJson(result);
1671
+ return 0;
1672
+ }
1673
+ const shown = result.pairs.filter((pair) => radar.levelRank(pair.level) >= radar.levelRank('L1'));
1674
+ shown.sort(comparePairs);
1675
+ for (const pair of shown) emitLine(formatPair(pair));
1676
+ emitLine(`radar: ${shown.length} pairs, max ${radar.summarize(result.pairs).max}`);
1677
+ return 0;
1678
+ }
1679
+
1680
+ function comparePairs(left, right) {
1681
+ const byLevel = radar.levelRank(right.level) - radar.levelRank(left.level);
1682
+ if (byLevel !== 0) return byLevel;
1683
+ const byA = left.a.localeCompare(right.a);
1684
+ return byA !== 0 ? byA : left.b.localeCompare(right.b);
1685
+ }
1686
+
1687
+ // `L3 feat/a × main conflicts: x.js (ledger)` — the level, the pair, then what was found.
1688
+ function formatPair(pair) {
1689
+ const head = `${pair.level} ${pair.a} × ${pair.b}`;
1690
+ if (pair.conflicts.length) return `${head} conflicts: ${pair.conflicts.join(', ')}${pair.ledger ? ' (ledger)' : ''}`;
1691
+ const tags = [];
1692
+ if (pair.registry) tags.push('registry');
1693
+ if (pair.claims.length) tags.push(`claims: ${pair.claims.map((claim) => claim.session).join(', ')}`);
1694
+ return `${head} files: ${pair.files.join(', ')}${tags.length ? ` (${tags.join('; ')})` : ''}`;
1695
+ }
1696
+
1697
+ // The session is the claim's identity: a claim file is per session, so a session id that could name
1698
+ // a path is refused rather than sanitized (a rewritten id would be someone else's claim).
1699
+ function claimSession(opts) {
1700
+ const raw = opts.values.session || strFlag('CTM_SESSION_ID') || '';
1701
+ const session = String(raw).trim();
1702
+ if (!session) {
1703
+ throw landingError('session_required', 'no session: pass --session <id> or set CTM_SESSION_ID', {});
1704
+ }
1705
+ if (session === '.' || session === '..' || session.includes('/') || session.includes('\\')) {
1706
+ throw landingError('session_invalid', `session id ${JSON.stringify(session)} is not a usable claim name`, { session });
1707
+ }
1708
+ return session;
1709
+ }
1710
+
1711
+ function claimFile(ctx, session) {
1712
+ return path.join(ctx.root, 'claims', `${session}.json`);
1713
+ }
1714
+
1715
+ // Claim paths are recorded REPOSITORY-ROOT relative, because that is what the radar compares them
1716
+ // against (`git diff --name-only` is root-relative). A path typed from a subdirectory is prefixed with
1717
+ // that directory's `git rev-parse --show-prefix`, so `land claim lib/x.js` in `pkg/` stores
1718
+ // `pkg/lib/x.js` and matches. A path that climbs out of the repository (`..`) is refused rather than
1719
+ // stored as text the radar can never match. A leading `/` is treated as "from the repository root"
1720
+ // (the prefix is still applied), which is what a reader of the path expects.
1721
+ function claimPaths(cwd, rawPaths) {
1722
+ const res = runGitRaw(cwd, ['rev-parse', '--show-prefix']);
1723
+ if (res.status !== 0) {
1724
+ throw landingError('git_failed', `cannot resolve the repository prefix in ${cwd}`, { cwd, stderr: res.stderr });
1725
+ }
1726
+ const prefix = res.stdout.trim();
1727
+ return rawPaths.map((raw) => {
1728
+ // A leading `/` names a path from the repository root; anything else is relative to cwd.
1729
+ const text = String(raw);
1730
+ const rooted = text.startsWith('/');
1731
+ const normalized = path.posix.normalize(path.posix.join(rooted ? '' : prefix, text.replace(/^\/+/, '')));
1732
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
1733
+ throw landingError('claim_path_invalid', `claim path ${JSON.stringify(raw)} is outside the repository`, { path: raw });
1734
+ }
1735
+ return normalized;
1736
+ });
1737
+ }
1738
+
1739
+ // §6.2 `land claim <path>...`: a SOFT claim (D-7). It warns another session that its branch touches
1740
+ // the same paths; it never blocks, never locks, and expires on its own.
1741
+ function cmdClaim(opts, cwd) {
1742
+ const ctx = context(cwd);
1743
+ const session = claimSession(opts);
1744
+ const raw = opts.positionals.slice();
1745
+ if (!raw.length) throw usageError('land claim <path>... [--ttl-h 4] [--session <id>]');
1746
+ const paths = claimPaths(cwd, raw);
1747
+ const hours = opts.values['ttl-h'] === undefined ? radar.CLAIM_DEFAULT_TTL_HOURS : Number(opts.values['ttl-h']);
1748
+ if (!Number.isFinite(hours) || hours <= 0) throw usageError('--ttl-h must be a positive number of hours');
1749
+ const branch = currentBranch(cwd);
1750
+ const record = {
1751
+ contract: CONTRACT,
1752
+ session,
1753
+ branch,
1754
+ paths,
1755
+ until: new Date(Date.now() + hours * 3600 * 1000).toISOString(),
1756
+ };
1757
+ // One claim per session (the file is named for it): a second `land claim` replaces the first.
1758
+ writeJsonAtomic(claimFile(ctx, session), record);
1759
+ emitJsonOrLine(opts, record, `claim: ${session} on ${branch}: ${paths.join(', ')} until ${record.until}`);
1760
+ return 0;
1761
+ }
1762
+
1763
+ // §6.2 `land unclaim`: removes only the caller's own file. Nothing to remove is not an error — the
1764
+ // caller asked for the claim to be gone, and it is.
1765
+ function cmdUnclaim(opts, cwd) {
1766
+ const ctx = context(cwd);
1767
+ const session = claimSession(opts);
1768
+ const file = claimFile(ctx, session);
1769
+ // A claim written by another contract is still THIS session's file; it is removed either way, since
1770
+ // leaving it behind would keep warning others with text this build cannot read.
1771
+ const existed = fs.existsSync(file);
1772
+ if (existed) fs.rmSync(file, { force: true });
1773
+ emitJsonOrLine(opts, { contract: CONTRACT, session, removed: existed }, existed ? `unclaimed: ${session}` : `no claim for ${session}`);
1774
+ return 0;
1775
+ }
1776
+
1777
+ // --- WP-0/WP-1 (§6.1-§6.3, §7, §8 WP-0/WP-1): `land sync`, the sync-job half of the queue, and the
1778
+ // four origin doctor checks. The bodies live in lib/landing/sync-cli.js — moved verbatim to keep this
1779
+ // file under its frozen size ceiling. The module owns no process-level helper of its own: the ones
1780
+ // below are handed over once, here, right after this file's own definitions.
1781
+ syncCli.configure({
1782
+ progress, emitLine, emitJson, emitJsonOrLine, delay, context, branchRef, spawnRunner, readGuardState,
1783
+ CONTRACT, WAIT_TIMEOUT_MS, POLL_MS,
1784
+ });
1785
+
1786
+ // §13.5/§13.7 WP-6: `land resolve`, the resolver's validate-and-submit verb (lib/landing/resolve-cli.js).
1787
+ resolveCli.configure({ progress, emitJsonOrLine, context, branchRef, CONTRACT, WAIT_TIMEOUT_MS });
1788
+
1789
+ // (syncJobRecord / ensureRunner / finishSyncJob / afterTrainLanded moved to lib/landing/sync-cli.js.)
1790
+
1791
+ // --- WP-0 (§7, §8 WP-0, §9 S-3): `land doctor` and its four origin checks (hooks, guard, remote,
1792
+ // push-auth — the last a `--dry-run`, never a real push). Moved verbatim to lib/landing/sync-cli.js,
1793
+ // which is where the push-auth probe now lives; this file keeps the WP-6 target fence below.
1794
+
1795
+ // --- WP-6: the reference-transaction target fence (§7 E-1) ---------------------------------------
1796
+
1797
+ const GUARD_MARKER = '# landing-ref-guard (managed)';
1798
+ const GUARD_HOOK_NAME = 'reference-transaction';
1799
+ const GUARD_CHAINED_NAME = 'reference-transaction.chained';
1800
+ // The guard is a repo-root `bin/` script, resolved relative to this file — never a copy, and never
1801
+ // looked up through PATH (a repo whose PATH lacks the tools directory still installs the fence).
1802
+ const GUARD_SCRIPT = path.resolve(__dirname, '..', '..', 'bin', 'landing-ref-guard.sh');
1803
+ const GUARD_TARGET_LINE_RE = /^LANDING_TARGET_REF=.*$/m;
1804
+
1805
+ function guardHookPath(commonDirPath, name) {
1806
+ return path.join(commonDirPath, 'hooks', name);
1807
+ }
1808
+
1809
+ // The guard script with its `LANDING_TARGET_REF=` line rewritten to `refs/heads/<target>`. Read on
1810
+ // every install so the installed copy can be compared byte-for-byte with the source of truth.
1811
+ function renderGuardScript(target) {
1812
+ let source;
1813
+ try {
1814
+ source = fs.readFileSync(GUARD_SCRIPT, 'utf8');
1815
+ } catch (error) {
1816
+ throw landingError('guard_script_missing', `cannot read the landing guard script at ${GUARD_SCRIPT}: ${error.message}`, {
1817
+ file: GUARD_SCRIPT, cause: error,
1818
+ });
1819
+ }
1820
+ if (!source.includes(GUARD_MARKER)) {
1821
+ throw landingError('guard_script_invalid', `${GUARD_SCRIPT} is missing its ${GUARD_MARKER} marker`, { file: GUARD_SCRIPT });
1822
+ }
1823
+ if (!GUARD_TARGET_LINE_RE.test(source)) {
1824
+ throw landingError('guard_script_invalid', `${GUARD_SCRIPT} has no LANDING_TARGET_REF= line to rewrite`, { file: GUARD_SCRIPT });
1825
+ }
1826
+ const line = `LANDING_TARGET_REF=refs/heads/${target}`;
1827
+ return source.replace(GUARD_TARGET_LINE_RE, () => line);
1828
+ }
1829
+
1830
+ // Install (or refresh) the guard. Idempotent by construction: the installed bytes are a pure
1831
+ // function of the guard script and the target, so a second run writes nothing and — crucially —
1832
+ // does not move anything aside a second time. A foreign `reference-transaction` hook is moved to
1833
+ // `reference-transaction.chained` exactly once; if that name is already taken we refuse rather than
1834
+ // destroy either hook.
1835
+ function installGuard(commonDirPath, target) {
1836
+ const hooksDir = path.join(commonDirPath, 'hooks');
1837
+ const hook = guardHookPath(commonDirPath, GUARD_HOOK_NAME);
1838
+ const chained = guardHookPath(commonDirPath, GUARD_CHAINED_NAME);
1839
+ const content = renderGuardScript(target);
1840
+ fs.mkdirSync(hooksDir, { recursive: true });
1841
+
1842
+ let existing = null;
1843
+ try {
1844
+ existing = fs.readFileSync(hook, 'utf8');
1845
+ } catch (error) {
1846
+ /* expected: no reference-transaction hook in this repository yet — the common install case */
1847
+ existing = null;
1848
+ }
1849
+
1850
+ if (existing !== null && !existing.includes(GUARD_MARKER)) {
1851
+ if (fs.existsSync(chained)) {
1852
+ throw landingError('guard_hook_conflict', `${chained} already exists; refusing to move the foreign ${hook} aside on top of it`, {
1853
+ hook, chained,
1854
+ });
1855
+ }
1856
+ fs.renameSync(hook, chained);
1857
+ }
1858
+
1859
+ if (existing === content) {
1860
+ fs.chmodSync(hook, 0o755);
1861
+ return { hook, chained: fs.existsSync(chained) ? chained : null, action: 'unchanged' };
1862
+ }
1863
+
1864
+ // Temp file + rename inside the hooks directory: a reader (git, mid-transaction) never sees a
1865
+ // half-written hook.
1866
+ const staging = path.join(hooksDir, `.${GUARD_HOOK_NAME}.guard.${process.pid}.${Date.now()}`);
1867
+ try {
1868
+ fs.writeFileSync(staging, content, { mode: 0o755 });
1869
+ fs.chmodSync(staging, 0o755);
1870
+ fs.renameSync(staging, hook);
1871
+ } catch (error) {
1872
+ try {
1873
+ fs.rmSync(staging, { force: true });
1874
+ } catch (cleanupError) {
1875
+ /* expected: the staging file is private to this call and lives in the hooks directory; a
1876
+ failed cleanup leaves a stray dotfile behind, never a half-installed hook */
1877
+ }
1878
+ throw landingError('guard_install_failed', `cannot install ${hook}: ${error.message}`, { file: hook, cause: error });
1879
+ }
1880
+ return {
1881
+ hook,
1882
+ chained: fs.existsSync(chained) ? chained : null,
1883
+ action: existing === null ? 'installed' : 'updated',
1884
+ };
1885
+ }
1886
+
1887
+ // Doctor's three states. `present` requires BOTH our marker and the configured target, so a hook
1888
+ // left over from a repository whose target moved reads as `foreign` rather than as a fence that
1889
+ // silently guards the wrong branch.
1890
+ function readGuardState(commonDirPath, target) {
1891
+ const hook = guardHookPath(commonDirPath, GUARD_HOOK_NAME);
1892
+ let content;
1893
+ try {
1894
+ content = fs.readFileSync(hook, 'utf8');
1895
+ } catch (error) {
1896
+ /* expected: no reference-transaction hook in this repository — `guard: missing` */
1897
+ return 'missing';
1898
+ }
1899
+ if (!content.includes(GUARD_MARKER)) return 'foreign';
1900
+ const baked = GUARD_TARGET_LINE_RE.exec(content);
1901
+ if (!baked) return 'foreign';
1902
+ if (target !== null && baked[0] !== `LANDING_TARGET_REF=refs/heads/${target}`) return 'foreign';
1903
+ return 'present';
1904
+ }
1905
+
1906
+ // `land guard install`: the narrow installer bin/install-hooks.sh reuses. It reads the landing
1907
+ // config (the target and nothing else) and touches only the hook — never `.ctm.json`, never the
1908
+ // agent file, which is why install-hooks must not call `land init`.
1909
+ function cmdGuard(opts, cwd) {
1910
+ const verb = opts.positionals[0];
1911
+ if (verb !== 'install') throw usageError('usage: land guard install');
1912
+ const ctx = context(cwd);
1913
+ const config = loadLandingConfig(cwd);
1914
+ const result = installGuard(ctx.common, config.target);
1915
+ emitLine(`guard: ${result.action} ${result.hook} (target refs/heads/${config.target})`);
1916
+ if (result.chained) emitLine(`guard: chained ${result.chained}`);
1917
+ return 0;
1918
+ }
1919
+
1920
+ // --- init (D-10) --------------------------------------------------------------------------------
1921
+
1922
+ function renderAgentRule(target) {
1923
+ let raw;
1924
+ try {
1925
+ raw = fs.readFileSync(AGENT_RULE_FILE, 'utf8');
1926
+ } catch (error) {
1927
+ throw landingError('agent_rule_missing', `cannot read ${AGENT_RULE_FILE}: ${error.message}`, { file: AGENT_RULE_FILE });
1928
+ }
1929
+ return raw.replace(/\n+$/, '').replace(/<target>/g, target);
1930
+ }
1931
+
1932
+ function resolveAgentFile(top, requested) {
1933
+ if (requested !== undefined) {
1934
+ if (!AGENT_FILES.includes(requested)) {
1935
+ throw usageError(`--agent-file must be one of ${AGENT_FILES.join(', ')}`);
1936
+ }
1937
+ return path.join(top, requested);
1938
+ }
1939
+ for (const name of AGENT_FILES) {
1940
+ if (fs.existsSync(path.join(top, name))) return path.join(top, name);
1941
+ }
1942
+ return path.join(top, 'AGENTS.md');
1943
+ }
1944
+
1945
+ // The marked block: everything between the markers is exactly the rule text. Rewriting the block in
1946
+ // place is what makes a second `land init` byte-identical (AC-1h).
1947
+ function writeMarkedBlock(file, rule) {
1948
+ const block = `${MARK_BEGIN}\n${rule}\n${MARK_END}`;
1949
+ let existing = '';
1950
+ try {
1951
+ existing = fs.readFileSync(file, 'utf8');
1952
+ } catch (error) {
1953
+ /* expected: the agent file does not exist yet — it is created below with the block as its only
1954
+ content */
1955
+ existing = '';
1956
+ }
1957
+ let next;
1958
+ if (MARK_RE.test(existing)) next = existing.replace(MARK_RE, block);
1959
+ else if (existing === '') next = `${block}\n`;
1960
+ else next = `${existing.replace(/\n+$/, '')}\n\n${block}\n`;
1961
+ if (next === existing) return false;
1962
+ fs.writeFileSync(file, next);
1963
+ return true;
1964
+ }
1965
+
1966
+ // `.ctm.json#landing` is written only when it is absent: an existing `landing` object belongs to the
1967
+ // repository, not to this command. Everything else in the file is preserved (2-space JSON).
1968
+ function writeLandingConfig(file, landing) {
1969
+ let raw = null;
1970
+ try {
1971
+ raw = fs.readFileSync(file, 'utf8');
1972
+ } catch (error) {
1973
+ /* expected: no .ctm.json in this repository yet — one is created below */
1974
+ raw = null;
1975
+ }
1976
+ let data = {};
1977
+ if (raw !== null) {
1978
+ try {
1979
+ data = JSON.parse(raw);
1980
+ } catch (error) {
1981
+ throw landingError('config_invalid', `${file} is not valid JSON: ${error.message}`, { file });
1982
+ }
1983
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
1984
+ throw landingError('config_invalid', `${file} must contain a JSON object`, { file });
1985
+ }
1986
+ }
1987
+ if (data.landing !== undefined) return { file, written: false, created: false };
1988
+ const next = `${JSON.stringify({ ...data, landing }, null, 2)}\n`;
1989
+ fs.writeFileSync(file, next);
1990
+ return { file, written: true, created: raw === null };
1991
+ }
1992
+
1993
+ function cmdInit(opts, cwd) {
1994
+ const top = toplevel(cwd);
1995
+ const target = resolveMainBranch(top);
1996
+ const guard = opts.flags.has('guard');
1997
+ const config = writeLandingConfig(path.join(top, '.ctm.json'), { target, mode: 'serial', guard });
1998
+ const agentFile = resolveAgentFile(top, opts.values['agent-file']);
1999
+ const wroteBlock = writeMarkedBlock(agentFile, renderAgentRule(target));
2000
+ emitLine(`config: ${config.file}${config.written ? ' written' : ' already configured (left untouched)'}`);
2001
+ emitLine(`agent rule: ${agentFile}${wroteBlock ? ' written' : ' already up to date'}`);
2002
+ if (guard) {
2003
+ // WP-6 E-1: `--guard` installs the reference-transaction fence for the configured target. Same
2004
+ // installer as `land guard install`; the rest of init (config, agent file) is unchanged.
2005
+ const installed = installGuard(commonDir(top), target);
2006
+ emitLine(`guard: ${installed.action} ${installed.hook} (target refs/heads/${target})`);
2007
+ if (installed.chained) emitLine(`guard: chained ${installed.chained}`);
2008
+ } else {
2009
+ emitLine('guard: missing');
2010
+ }
2011
+ return 0;
2012
+ }
2013
+
2014
+ // --- dispatch ------------------------------------------------------------------------------------
2015
+
2016
+ async function dispatch(argv) {
2017
+ const [verb, ...rest] = argv;
2018
+ if (!verb || verb === 'help' || verb === '--help' || verb === '-h') {
2019
+ process.stdout.write(`${USAGE}\n`);
2020
+ return verb ? 0 : 2;
2021
+ }
2022
+ const spec = FLAG_SPECS[verb];
2023
+ if (!spec) throw usageError(`unknown command ${JSON.stringify(verb)}`);
2024
+ const opts = parseArgs(rest, spec);
2025
+ const cwd = process.cwd();
2026
+ switch (verb) {
2027
+ case 'init': return cmdInit(opts, cwd);
2028
+ case 'guard': return cmdGuard(opts, cwd);
2029
+ case 'submit': return await cmdSubmit(opts, cwd);
2030
+ case 'run': return await cmdRun(opts, cwd);
2031
+ case 'priority': return cmdPriority(opts, cwd);
2032
+ case 'status': return cmdStatus(opts, cwd);
2033
+ case 'wait': return await cmdWait(opts, cwd);
2034
+ case 'inspect': return cmdInspect(opts, cwd);
2035
+ case 'cancel': return await cmdCancel(opts, cwd);
2036
+ case 'conflicts': return cmdConflicts(opts, cwd);
2037
+ case 'claim': return cmdClaim(opts, cwd);
2038
+ case 'unclaim': return cmdUnclaim(opts, cwd);
2039
+ case 'record-proof': return cmdRecordProof(opts, cwd);
2040
+ case 'report': return cmdReport(opts, cwd);
2041
+ case 'sync': return await syncCli.cmdSync(opts, cwd);
2042
+ case 'resolve': return await resolveCli.cmdResolve(opts, cwd);
2043
+ case 'doctor': return syncCli.cmdDoctor(opts, cwd);
2044
+ default: throw usageError(`unknown command ${JSON.stringify(verb)}`);
2045
+ }
2046
+ }
2047
+
2048
+ async function main(argv) {
2049
+ const wantsJson = argv.includes('--json');
2050
+ try {
2051
+ return await dispatch(argv);
2052
+ } catch (error) {
2053
+ return reportFailure(error, wantsJson);
2054
+ }
2055
+ }
2056
+
2057
+ if (require.main === module) {
2058
+ // The CLI is asynchronous (gates are spawned, not blocked on), so the exit code is set once the
2059
+ // whole verb has settled; `process.exit` is never called early, or buffered stdout would be lost.
2060
+ main(process.argv.slice(2)).then(
2061
+ (code) => { process.exitCode = code; },
2062
+ (error) => {
2063
+ process.stderr.write(`land: unexpected: ${error && error.message ? error.message : String(error)}\n`);
2064
+ process.exitCode = 1;
2065
+ },
2066
+ );
2067
+ }
2068
+
2069
+ module.exports = { main, parseArgs, FLAG_SPECS, USAGE };