create-walle 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -0,0 +1,885 @@
1
+ 'use strict';
2
+ // WP-1 (docs/local-landing-queue-multi-session-integration-2026-10-26.html §5 D-3/D-5/D-8, §11
3
+ // Risks, §8 WP-1): the train — how a job becomes exactly one commit to fast-forward the target to.
4
+ //
5
+ // D-3 builds the train WITHOUT touching any worktree (`merge-tree --write-tree` + `commit-tree`);
6
+ // the only worktree this module ever creates is the job's own gate worktree, and only D-8 (a
7
+ // conflict confined to `landing.regenerate` paths) needs it for the commit itself. Nothing here
8
+ // writes into a session worktree, and nothing here ever merges *into* the target checkout unguarded
9
+ // — D-5 goes through `git merge --ff-only`, which git itself refuses when it would clobber local
10
+ // state, or through the compare-and-swap `git update-ref <ref> <new> <old>`.
11
+
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const path = require('path');
15
+ const { spawn } = require('child_process');
16
+
17
+ const {
18
+ landingError,
19
+ childEnv,
20
+ commonDir,
21
+ runGitRaw,
22
+ git,
23
+ gitFailed,
24
+ revParse,
25
+ revParseOrNull,
26
+ isAncestor,
27
+ mergeTree,
28
+ commitTree,
29
+ casUpdateRef,
30
+ ffCheckedOut,
31
+ } = require('./git');
32
+
33
+ const MAX_ROUNDS = 3;
34
+ const GLOB_CACHE = new Map();
35
+ // A gate that ignores SIGTERM gets this long before its whole process group is SIGKILLed.
36
+ const GATE_KILL_GRACE_MS = 10000;
37
+
38
+ function targetRef(target) {
39
+ return `refs/heads/${target}`;
40
+ }
41
+
42
+ // Signal a whole process tree. `detached: true` made the shell a process-group leader, so the
43
+ // negative pid reaches every descendant the gate spawned — a bare `child.kill()` reaches only
44
+ // `/bin/sh` and leaves the test suite the gate started running forever (D-4's `gate_timeout` then
45
+ // reports a timeout while the machine keeps working).
46
+ function killGroup(child, signal) {
47
+ try {
48
+ process.kill(-child.pid, signal);
49
+ return true;
50
+ } catch (error) {
51
+ /* expected: the group is already gone, or the pid never became a group leader — fall back to
52
+ the direct pid, which is also how a non-detached child would be signalled */
53
+ try {
54
+ child.kill(signal);
55
+ return true;
56
+ } catch (inner) {
57
+ /* expected: the process exited between the signal and this call — nothing left to signal */
58
+ return false;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Every gate/regenerate command the queue runs goes through `/bin/sh -c` with an env built from
64
+ // `process.env` MINUS the seven §4 GIT_* names (`childEnv`), plus the LAND_* contract. `extra`
65
+ // cannot reintroduce a scrubbed name: `childEnv` applies it AFTER the scrub but the keys below are
66
+ // never GIT_*.
67
+ //
68
+ // D-4 keeps the gate a CHILD, never the runner itself, and it is asynchronous on purpose:
69
+ // `spawn` (not `spawnSync`) leaves the runner's event loop free, so the lease heartbeat really
70
+ // fires while a 40-minute gate works. Output streams straight into the job log file through
71
+ // `logFd` — a live `land inspect` tail — instead of accumulating in a 64 MiB in-memory buffer.
72
+ // WP-2 (§5 D-5): `signal` is an optional AbortSignal. The queue's stop primitive aborts it from the
73
+ // runner's heartbeat, and every command of the job — gates AND regenerate entries — runs through
74
+ // here, so this one option is the single place a running gate can be stopped. An already-aborted
75
+ // signal resolves WITHOUT spawning, and an abort mid-run signals the whole process group exactly the
76
+ // way the timeout path does (SIGTERM, then SIGKILL after `GATE_KILL_GRACE_MS`).
77
+ function runShell(command, { cwd, extra, timeoutMs, logFd, signal } = {}) {
78
+ return new Promise((resolve) => {
79
+ const started = Date.now();
80
+ if (signal && signal.aborted) {
81
+ resolve({ status: null, signal: null, timedOut: false, aborted: true, error: null, seconds: 0 });
82
+ return;
83
+ }
84
+ const stdio = logFd === undefined || logFd === null ? 'ignore' : logFd;
85
+ let child;
86
+ try {
87
+ child = spawn('/bin/sh', ['-c', command], {
88
+ cwd,
89
+ env: childEnv(extra),
90
+ detached: true,
91
+ stdio: ['ignore', stdio, stdio],
92
+ });
93
+ } catch (error) {
94
+ resolve({ status: null, signal: null, timedOut: false, aborted: false, error, seconds: 0 });
95
+ return;
96
+ }
97
+ let timedOut = false;
98
+ let aborted = false;
99
+ let killTimer = null;
100
+ const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
101
+ ? setTimeout(() => {
102
+ timedOut = true;
103
+ killGroup(child, 'SIGTERM');
104
+ killTimer = setTimeout(() => killGroup(child, 'SIGKILL'), GATE_KILL_GRACE_MS);
105
+ }, timeoutMs)
106
+ : null;
107
+ const onAbort = () => {
108
+ aborted = true;
109
+ killGroup(child, 'SIGTERM');
110
+ killTimer = setTimeout(() => killGroup(child, 'SIGKILL'), GATE_KILL_GRACE_MS);
111
+ };
112
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
113
+ const finish = (status, termSignal, error) => {
114
+ if (timer) clearTimeout(timer);
115
+ if (killTimer) clearTimeout(killTimer);
116
+ if (signal) signal.removeEventListener('abort', onAbort);
117
+ resolve({
118
+ status,
119
+ signal: termSignal,
120
+ timedOut,
121
+ aborted,
122
+ error: error || null,
123
+ seconds: Number(((Date.now() - started) / 1000).toFixed(3)),
124
+ });
125
+ };
126
+ child.on('error', (error) => finish(null, null, error));
127
+ child.on('close', (status, termSignal) => finish(status, termSignal, null));
128
+ });
129
+ }
130
+
131
+ // --- globs (§6.1 `regenerate[].paths`, `registries[]`) ----------------------------------------
132
+
133
+ // Git-style pathspec globbing: `**` crosses directory boundaries (including zero of them, so
134
+ // `docs/**` matches `docs/INDEX.html`), `*` and `?` do not.
135
+ function globToRegExp(pattern) {
136
+ const cached = GLOB_CACHE.get(pattern);
137
+ if (cached) return cached;
138
+ let out = '^';
139
+ for (let index = 0; index < pattern.length; index += 1) {
140
+ const ch = pattern[index];
141
+ if (ch === '*') {
142
+ if (pattern[index + 1] === '*') {
143
+ if (pattern[index + 2] === '/') {
144
+ out += '(?:[^/]*/)*';
145
+ index += 2;
146
+ } else {
147
+ out += '.*';
148
+ index += 1;
149
+ }
150
+ } else {
151
+ out += '[^/]*';
152
+ }
153
+ } else if (ch === '?') {
154
+ out += '[^/]';
155
+ } else if ('\\^$.|+()[]{}'.includes(ch)) {
156
+ out += `\\${ch}`;
157
+ } else {
158
+ out += ch;
159
+ }
160
+ }
161
+ const re = new RegExp(`${out}$`);
162
+ GLOB_CACHE.set(pattern, re);
163
+ return re;
164
+ }
165
+
166
+ function globMatch(pattern, file) {
167
+ if (globToRegExp(pattern).test(file)) return true;
168
+ // A directory pattern (`docs`, `docs/`) also owns everything underneath it.
169
+ return globToRegExp(`${pattern.replace(/\/+$/, '')}/**`).test(file);
170
+ }
171
+
172
+ // D-2 (§9 S-6): every `landing.refresh[]` entry runs on EVERY train. The entry's owned paths are
173
+ // tree-derived pins (the closure policy's `sourceBindings` digests), and the bound set is the
174
+ // policy's own key list — "did a bound source change" is not knowable cheaply from a path list, and
175
+ // the script is idempotent, so the simplest correct rule is to run all of them and let the
176
+ // `git diff --cached --quiet` check decide whether a new commit is needed at all.
177
+ function matchingRefreshEntries(config) {
178
+ return config && Array.isArray(config.refresh) ? config.refresh : [];
179
+ }
180
+
181
+ // D-8: every conflicted path must be owned by a `landing.regenerate[]` entry. Returns the distinct
182
+ // entries to run, or null when at least one path is hand-written (the job blocks).
183
+ function matchingRegenerateEntries(config, conflicts) {
184
+ const entries = config && Array.isArray(config.regenerate) ? config.regenerate : [];
185
+ const matched = [];
186
+ for (const file of conflicts) {
187
+ const entry = entries.find((candidate) => candidate.paths.some((pattern) => globMatch(pattern, file)));
188
+ if (!entry) return null;
189
+ if (!matched.includes(entry)) matched.push(entry);
190
+ }
191
+ return matched;
192
+ }
193
+
194
+ // MOVED here from `gate.js` verbatim (WP-2, §5 D-6) so the base rerun can build the same worktree:
195
+ // the cache directories a gate needs (`landing.cachePaths`, e.g. `node_modules`) are symlinked into
196
+ // the materialized worktree rather than copied. The source checkout only supplies them when it has
197
+ // them — a fresh clone with no `node_modules` simply has nothing to link in — and a path that is a
198
+ // REAL tracked directory in the tree wins over the link.
199
+ function linkCachePaths(dir, sourceDir, cachePaths) {
200
+ const linked = [];
201
+ for (const rel of cachePaths) {
202
+ const source = path.resolve(sourceDir, rel);
203
+ let stat = null;
204
+ try {
205
+ stat = fs.statSync(source);
206
+ } catch (error) {
207
+ /* expected: this cache directory has not been populated in the source checkout (a fresh
208
+ clone); there is simply nothing to link in */
209
+ continue;
210
+ }
211
+ if (!stat.isDirectory()) continue;
212
+ const link = path.join(dir, rel);
213
+ fs.mkdirSync(path.dirname(link), { recursive: true });
214
+ try {
215
+ fs.symlinkSync(source, link, 'dir');
216
+ linked.push(rel);
217
+ } catch (error) {
218
+ /* expected: the train tree already contains a real directory at this path (the cache path is
219
+ tracked in this repository after all) — the real directory wins */
220
+ }
221
+ }
222
+ return linked;
223
+ }
224
+
225
+ // --- D-3: the worktree-free train -------------------------------------------------------------
226
+
227
+ // `{ kind, base, tree, train, conflicts }` where kind is:
228
+ // 'unchanged' — the merge result is the target's own tree: the branch is already in the target
229
+ // 'train' — `train` is the commit to fast-forward the target to (the tip itself when the
230
+ // target is an ancestor of it, otherwise a two-parent merge commit)
231
+ // 'conflict' — `conflicts` holds the paths merge-tree reported
232
+ function buildTrain(cwd, target, job) {
233
+ const built = buildBatchTrain(cwd, target, [job]);
234
+ const skipped = built.skipped.length ? built.skipped[0] : null;
235
+ if (skipped) {
236
+ return { kind: 'conflict', base: built.base, tree: built.tree, train: null, conflicts: skipped.paths };
237
+ }
238
+ if (built.kind === 'unchanged') {
239
+ return { kind: 'unchanged', base: built.base, tree: built.tree, train: built.base, conflicts: [] };
240
+ }
241
+ return { kind: 'train', base: built.base, tree: built.tree, train: built.train, conflicts: [] };
242
+ }
243
+
244
+ // WP-1 D-1 (§5 D-1, §6, §8 WP-1, §9 S-2, AC-1a/AC-1c): the batched train. `c0 = base` (the target
245
+ // tip) and each member in queue order is merged ON TOP of the previous chain head:
246
+ // c_k = commitTree(mergeTree(c_{k-1}, tip_k), [c_{k-1}, tip_k])
247
+ // with the fast-forward shortcut `buildTrain` already had: when `tip_k` descends from `c_{k-1}` the
248
+ // train is `tip_k` itself, so a member that is simply ahead of the target keeps the commit the
249
+ // author's own gate proved (D-6's proof chain walks first parents and stays valid).
250
+ //
251
+ // A member whose mergeTree onto `c_{k-1}` CONFLICTS is skipped — `onSkip(member, paths)` is called,
252
+ // the chain does not advance, and the next member is merged onto the same head (D-1: the conflicting
253
+ // member never blocks the others; it goes back to the queue with `{ base: c_{k-1}, paths }` as its
254
+ // preview). A chain that never moves (every member skipped, or every merge a no-op) is `unchanged`.
255
+ //
256
+ // Returns `{ kind, base, members, skipped, chain, train, tree }`: `chain[k]` is the commit for
257
+ // `members[0..k]` minus the skipped ones, `train` the final head (== `base` when nothing moved).
258
+ function buildBatchTrain(cwd, target, members, opts = {}) {
259
+ const onSkip = typeof opts.onSkip === 'function' ? opts.onSkip : () => {};
260
+ const base = revParse(cwd, targetRef(target));
261
+ const baseTree = revParse(cwd, `${base}^{tree}`);
262
+ const list = Array.isArray(members) ? members : [];
263
+ const chain = [];
264
+ const skipped = [];
265
+ const kept = [];
266
+ let head = base;
267
+ for (const member of list) {
268
+ const merged = mergeTree(cwd, head, member.tip);
269
+ if (merged.conflicts.length) {
270
+ // `head` is the chain commit the conflict was measured against, which is what the member's
271
+ // `conflictPreview.base` must name (D-1); the design's signature is `onSkip(member, paths)` and
272
+ // the extra argument is additive.
273
+ onSkip(member, merged.conflicts, head);
274
+ skipped.push({ id: member.id, paths: merged.conflicts });
275
+ continue;
276
+ }
277
+ if (isAncestor(cwd, head, member.tip)) {
278
+ // D-1 ff case, exactly as `buildTrain` does for one job: the train IS the member's own commit.
279
+ head = member.tip;
280
+ chain.push(head);
281
+ kept.push(member);
282
+ continue;
283
+ }
284
+ head = commitTree(cwd, merged.tree, [head, member.tip], trainMessage(member));
285
+ chain.push(head);
286
+ kept.push(member);
287
+ }
288
+ const tree = revParse(cwd, `${head}^{tree}`);
289
+ const moved = chain.length > 0 && tree !== baseTree;
290
+ return {
291
+ kind: moved ? 'train' : 'unchanged',
292
+ base,
293
+ members: kept,
294
+ skipped,
295
+ chain,
296
+ train: moved ? head : base,
297
+ tree,
298
+ };
299
+ }
300
+
301
+ function trainMessage(job) {
302
+ return `land(${job.branch}): ${job.title}`;
303
+ }
304
+
305
+ // --- the gate worktree (§11 Risks) -------------------------------------------------------------
306
+
307
+ // §11: a gate that depends on its worktree path must see the repository root it expects, so the
308
+ // train worktree lives at `<repo-root>/.worktrees/land-<jobId>` whenever the common dir is exactly
309
+ // `<repo-root>/.git` AND `.worktrees/` is already git-ignored there (so it never shows up as
310
+ // untracked dirt in the repository). Otherwise it falls back to `<common-dir>/landing/trains/`.
311
+ function trainsRoot(cwd, commonDirPath) {
312
+ if (path.basename(commonDirPath) === '.git') {
313
+ const repoRoot = path.dirname(commonDirPath);
314
+ const probe = runGitRaw(repoRoot, ['check-ignore', '-q', '--', '.worktrees/land-probe']);
315
+ if (probe.status === 0) return { dir: path.join(repoRoot, '.worktrees'), source: 'repo-root' };
316
+ }
317
+ return { dir: path.join(commonDirPath, 'landing', 'trains'), source: 'common-dir' };
318
+ }
319
+
320
+ // WP-1 D-1 (§9 S-3): keyed by the TRAIN id — the batch id (`b-<ulid>`) for a batch, the job id for
321
+ // a singleton. One train, one worktree, one log, one selection record: every member of a batch points
322
+ // at the same artefacts, which is what makes the gate's output readable as a batch rather than as N
323
+ // copies of the first job's.
324
+ function trainDirFor(rootDir, trainId) {
325
+ return path.join(rootDir, `land-${trainId}`);
326
+ }
327
+
328
+ // A previous runner may have been killed between `worktree add` and its cleanup, leaving both the
329
+ // directory and its registration behind. Only paths that look like ours are touched.
330
+ function prepareTrainDir(cwd, dir) {
331
+ if (path.basename(dir).startsWith('land-') === false) {
332
+ throw landingError('train_dir_invalid', `refusing to prepare ${dir}: not a land-* train directory`, { dir });
333
+ }
334
+ runGitRaw(cwd, ['worktree', 'prune']);
335
+ let exists = true;
336
+ try {
337
+ fs.lstatSync(dir);
338
+ } catch (error) {
339
+ /* expected: the common case — nobody left a train worktree behind for this job */
340
+ exists = false;
341
+ }
342
+ if (!exists) return false;
343
+ const removed = runGitRaw(cwd, ['worktree', 'remove', '--force', '--force', dir]);
344
+ if (removed.status !== 0) {
345
+ // A directory that is no longer registered as a worktree (the registration was pruned) is
346
+ // still ours by name and location; remove the leftover so `worktree add` can succeed.
347
+ fs.rmSync(dir, { recursive: true, force: true });
348
+ }
349
+ return true;
350
+ }
351
+
352
+ function addTrainWorktree(cwd, dir, train, jobId) {
353
+ prepareTrainDir(cwd, dir);
354
+ const args = ['worktree', 'add', '--detach', '--lock', '--reason', `land job ${jobId}`, dir, train];
355
+ const res = runGitRaw(cwd, args);
356
+ if (res.status !== 0) throw gitFailed(cwd, args, res);
357
+ return dir;
358
+ }
359
+
360
+ // Only ever called for this job's own train directory (`<trains>/.worktrees|trains/land-<id>`),
361
+ // which the queue created itself and which no session can have work in. `--force --force` is
362
+ // required because the gate legitimately owns that directory and may have dirtied the tree inside
363
+ // it — AC-1d asserts the worktree is gone after `gate_dirtied_tree`.
364
+ function removeTrainWorktree(cwd, dir) {
365
+ if (!dir) return { removed: false, reason: 'no_dir' };
366
+ const res = runGitRaw(cwd, ['worktree', 'remove', '--force', '--force', dir]);
367
+ if (res.status === 0) return { removed: true };
368
+ let exists = true;
369
+ try {
370
+ fs.lstatSync(dir);
371
+ } catch (error) {
372
+ /* expected: git already removed the directory but reported a non-zero status (for example a
373
+ concurrent `worktree prune`) — the directory is gone, which is what the caller asked for */
374
+ exists = false;
375
+ }
376
+ if (!exists) return { removed: true, note: 'already absent' };
377
+ return { removed: false, reason: String(res.stderr || res.stdout || '').trim() || `git worktree remove exited ${res.status}` };
378
+ }
379
+
380
+ // `git worktree list --porcelain`, parsed into one entry per worktree.
381
+ function worktreeList(cwd) {
382
+ const args = ['worktree', 'list', '--porcelain'];
383
+ const res = runGitRaw(cwd, args);
384
+ if (res.status !== 0) throw gitFailed(cwd, args, res);
385
+ const entries = [];
386
+ let current = null;
387
+ for (const line of res.stdout.split('\n')) {
388
+ if (line === '') {
389
+ if (current) entries.push(current);
390
+ current = null;
391
+ continue;
392
+ }
393
+ const space = line.indexOf(' ');
394
+ const key = space === -1 ? line : line.slice(0, space);
395
+ const value = space === -1 ? '' : line.slice(space + 1);
396
+ if (key === 'worktree') {
397
+ if (current) entries.push(current);
398
+ current = { path: value, branch: null, detached: false, locked: false, bare: false, prunable: false };
399
+ } else if (current) {
400
+ if (key === 'branch') current.branch = value;
401
+ else if (key === 'detached') current.detached = true;
402
+ else if (key === 'locked') current.locked = true;
403
+ else if (key === 'bare') current.bare = true;
404
+ else if (key === 'prunable') current.prunable = true;
405
+ }
406
+ }
407
+ if (current) entries.push(current);
408
+ return entries;
409
+ }
410
+
411
+ // D-5: the worktree (if any) that has the target branch checked out. Detached worktrees — the
412
+ // queue's own train worktrees, and any user checkout — are never the target.
413
+ function targetWorktreeFor(cwd, target) {
414
+ const ref = targetRef(target);
415
+ return worktreeList(cwd).find((entry) => entry.branch === ref && !entry.bare && !entry.prunable) || null;
416
+ }
417
+
418
+ // --- D-5: landing ------------------------------------------------------------------------------
419
+
420
+ function commitsBetween(cwd, from, to) {
421
+ const res = runGitRaw(cwd, ['log', '--format=%H', `${from}..${to}`]);
422
+ if (res.status !== 0) return [];
423
+ return res.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
424
+ }
425
+
426
+ function mergeBase(cwd, a, b) {
427
+ const res = runGitRaw(cwd, ['merge-base', a, b]);
428
+ if (res.status !== 0) return null;
429
+ const value = res.stdout.trim();
430
+ return value || null;
431
+ }
432
+
433
+ // D-5: fast-forward the target to exactly `train`. A target checked out somewhere goes through
434
+ // `git merge --ff-only` in that worktree (git refuses without modifying anything when local changes
435
+ // would be clobbered -> `target_dirty`); otherwise the branch is moved with a compare-and-swap.
436
+ //
437
+ // `expectedBase` is the commit this train was BUILT on, and it is what the swap compares against —
438
+ // never a re-read tip. Re-reading would move the target to a train that does not contain a commit
439
+ // that landed in the meantime, silently dropping it; comparing against the build base turns that
440
+ // race into `cas_lost`, which the caller treats exactly like a moved target (record the foreign
441
+ // commits, rebuild, retrain). A target checked out in a worktree cannot be rewound by
442
+ // `merge --ff-only` at all, but it can refuse; the HEAD comparison reports that as `cas_lost` too.
443
+ //
444
+ // WP-6: `opts.env` is handed to BOTH ref-moving git children, so the runner can present the
445
+ // `LAND_TOKEN` the E-1 reference-transaction guard compares against its own lease record. It is the
446
+ // env EXTRA, not a whole environment: `git.js#childEnv` starts from `process.env` minus the §4
447
+ // GIT_* list and only then applies `extra`, which is exactly how `runShell` passes `LAND_TOKEN` to
448
+ // gates (gate.js:151). Spreading `process.env` here instead would re-introduce the scrubbed GIT_*
449
+ // names (I-8). Existing callers pass no `opts` and are byte-for-byte unaffected.
450
+ function landTrain(cwd, target, train, expectedBase, opts = {}) {
451
+ const gitOpts = { env: opts.env };
452
+ const worktree = targetWorktreeFor(cwd, target);
453
+ if (worktree) {
454
+ const head = revParseOrNull(worktree.path, 'HEAD');
455
+ if (expectedBase !== undefined && head !== expectedBase) {
456
+ throw landingError('cas_lost', `${targetRef(target)} is checked out at ${head}, expected ${expectedBase}`, {
457
+ ref: targetRef(target), expected: expectedBase, current: head, cwd: worktree.path, worktree: worktree.path,
458
+ });
459
+ }
460
+ const result = ffCheckedOut(worktree.path, train, gitOpts);
461
+ return { via: 'ff', worktree: worktree.path, sha: result.sha };
462
+ }
463
+ const expected = expectedBase === undefined ? revParseOrNull(cwd, targetRef(target)) : expectedBase;
464
+ casUpdateRef(cwd, targetRef(target), train, expected, gitOpts);
465
+ return { via: 'cas', worktree: null, sha: revParseOrNull(cwd, targetRef(target)) };
466
+ }
467
+
468
+ // --- D-8: conflicts confined to regenerate paths ------------------------------------------------
469
+
470
+ function changedFilesBetween(cwd, from, to) {
471
+ const res = runGitRaw(cwd, ['diff', '--name-only', from, to]);
472
+ if (res.status !== 0) return [];
473
+ return res.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
474
+ }
475
+
476
+ function hunkHeaders(cwd, from, to, file) {
477
+ const res = runGitRaw(cwd, ['diff', '--no-color', '-U0', from, to, '--', file]);
478
+ if (res.status !== 0) return [];
479
+ return res.stdout.split('\n').filter((line) => line.startsWith('@@')).map((line) => line.trim());
480
+ }
481
+
482
+ // D-9 Tier A, run per job on a FAIL: the files the target changed since the merge base intersect
483
+ // the files the branch changed. `sameHunkContext` marks a file where both sides moved the SAME
484
+ // -U0 hunk header (git's own function context), which is the strongest available signal short of a
485
+ // second gate — and a second gate is explicitly out of scope.
486
+ function overlapWithTarget(cwd, targetTip, tip) {
487
+ const base = mergeBase(cwd, targetTip, tip);
488
+ if (!base) return [];
489
+ const branchFiles = new Set(changedFilesBetween(cwd, base, tip));
490
+ const overlap = [];
491
+ for (const file of changedFilesBetween(cwd, base, targetTip)) {
492
+ if (!branchFiles.has(file)) continue;
493
+ const branchHunks = new Set(hunkHeaders(cwd, base, tip, file));
494
+ const shared = hunkHeaders(cwd, base, targetTip, file).some((header) => branchHunks.has(header));
495
+ overlap.push({ file, sameHunkContext: shared });
496
+ }
497
+ return overlap;
498
+ }
499
+
500
+ // The body shared by `regenerateTrain` (D-8) and `refreshTrain` (D-2): run every entry in the
501
+ // ALREADY-MATERIALIZED train worktree `dir` with the §4 LAND_* contract, honour the stop signal,
502
+ // then stage EXACTLY the paths the entries own and refuse to go on while anything is left unmerged.
503
+ // Returns the owned paths that actually changed, which is what tells `refreshTrain` whether the train
504
+ // commit has to be remade at all.
505
+ //
506
+ // Entries run in order and the next one may depend on the previous one's output, so the loop is
507
+ // deliberately sequential. A stopped entry is a STOP, not a failure — `land.js` turns the `stopped`
508
+ // code into `canceled` at its stop checkpoint.
509
+ async function runEntriesInWorktree({ dir, job, base, entries, log, signal, label, conflicts }) {
510
+ const logFd = log ? fs.openSync(log, 'a') : null;
511
+ const write = (text) => { if (logFd !== null) fs.writeSync(logFd, `${text}\n`); };
512
+ try {
513
+ for (const entry of entries) {
514
+ write(`$ ${entry.run}`);
515
+ // eslint-disable-next-line no-await-in-loop
516
+ const res = await runShell(entry.run, {
517
+ cwd: dir,
518
+ extra: { LAND_TRAIN: dir, LAND_BASE: base, LAND_TIP: job.tip, LAND_JOB: job.id },
519
+ logFd,
520
+ signal,
521
+ });
522
+ if (res.aborted) {
523
+ throw landingError('stopped', `${label} entry ${JSON.stringify(entry.run)} stopped`, {
524
+ jobId: job.id, run: entry.run,
525
+ });
526
+ }
527
+ if (res.timedOut) {
528
+ throw landingError(`${label}_timeout`, `${label} entry ${JSON.stringify(entry.run)} timed out`, {
529
+ jobId: job.id, run: entry.run, conflicts,
530
+ });
531
+ }
532
+ if (res.status !== 0) {
533
+ throw landingError(`${label}_failed`, `${label} entry ${JSON.stringify(entry.run)} exited ${res.status}`, {
534
+ jobId: job.id, run: entry.run, status: res.status, signal: res.signal,
535
+ });
536
+ }
537
+ write(`[${label}] ${entry.run}: ok`);
538
+ }
539
+ } finally {
540
+ if (logFd !== null) fs.closeSync(logFd);
541
+ }
542
+
543
+ // Stage EXACTLY the paths the entries own: expand each pattern against what actually changed.
544
+ const statusRes = runGitRaw(dir, ['status', '--porcelain']);
545
+ const changed = statusRes.stdout
546
+ .split('\n')
547
+ .map((line) => line.trimEnd())
548
+ .filter((line) => line.length > 3)
549
+ .map((line) => {
550
+ const body = line.slice(3);
551
+ const renamed = body.includes(' -> ') ? body.split(' -> ').pop() : body;
552
+ return renamed.startsWith('"') && renamed.endsWith('"') ? renamed.slice(1, -1) : renamed;
553
+ })
554
+ .filter((file) => entries.some((entry) => entry.paths.some((pattern) => globMatch(pattern, file))));
555
+ if (changed.length) {
556
+ const addArgs = ['add', '-A', '--', ...changed];
557
+ const added = runGitRaw(dir, addArgs);
558
+ if (added.status !== 0) throw gitFailed(dir, addArgs, added);
559
+ }
560
+
561
+ const unmerged = runGitRaw(dir, ['diff', '--name-only', '--diff-filter=U']);
562
+ const stillUnmerged = unmerged.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
563
+ if (stillUnmerged.length) {
564
+ throw landingError('conflict_unresolved', `${label} left unmerged paths: ${stillUnmerged.join(', ')}`, {
565
+ jobId: job.id, paths: stillUnmerged, conflicts,
566
+ });
567
+ }
568
+ return changed;
569
+ }
570
+
571
+ // D-8: materialize the train worktree at the target tip, merge the branch WITHOUT committing, run
572
+ // each matching `regenerate` entry, stage exactly the paths the entries own, verify nothing is left
573
+ // unmerged, and commit. This is one of two cases where the train commit is made in a worktree — and
574
+ // it is made with PLUMBING (`write-tree` + `commit-tree`), never `git commit`: a real commit fires
575
+ // the repository's own `pre-commit` hook (a ten-minute gate here) and demands a configured
576
+ // `user.name`, neither of which a queue that is merely rebuilding a conflicted file has any
577
+ // business triggering.
578
+ async function regenerateTrain({ cwd, dir, job, base, conflicts, entries, log, signal }) {
579
+ addTrainWorktree(cwd, dir, base, job.id);
580
+ const mergeArgs = ['merge', '--no-commit', '--no-ff', job.tip];
581
+ const merged = runGitRaw(dir, mergeArgs);
582
+ // exit 1 is the expected "conflict" outcome we are here to resolve; anything else is a real
583
+ // failure of the merge machinery.
584
+ if (merged.status !== 0 && merged.status !== 1) throw gitFailed(dir, mergeArgs, merged);
585
+
586
+ await runEntriesInWorktree({ dir, job, base, entries, log, signal, label: 'regenerate', conflicts });
587
+
588
+ // The merge already staged the result in the train worktree's own index, so the tree is simply
589
+ // what that index says; the commit object is built with `commit-tree` (no hooks, no identity).
590
+ const tree = git(dir, ['write-tree']).trim();
591
+ const train = commitTree(dir, tree, [base, job.tip], trainMessage(job));
592
+ if (log) fs.appendFileSync(log, `[regenerate] committed ${train} (tree ${tree})\n`);
593
+ return { kind: 'train', base, tree, train, conflicts: [], regenerated: conflicts };
594
+ }
595
+
596
+ // D-2: the refresh pass. Unlike `regenerate`, a refresh entry runs on EVERY non-unchanged train, not
597
+ // only on a conflict: a tree-derived pin (the closure policy's `sourceBindings` digests) is stale the
598
+ // moment either side touches a bound source, and there is no textual conflict to notice it.
599
+ //
600
+ // The worktree is materialized at the ALREADY-BUILT train commit (`built.train`) — no merge, no
601
+ // `--no-ff` — because the train already contains both sides. The train commit is remade with plumbing
602
+ // only when an owned path actually changed, so a refresh that changes nothing leaves the merge-tree
603
+ // train byte-for-byte as it was and D-3's cache key, `job.tree` and the CAS all still describe the
604
+ // tree the gate approved.
605
+ async function refreshTrain({ cwd, dir, job, built, entries, log, signal }) {
606
+ addTrainWorktree(cwd, dir, built.train, job.id);
607
+ const changed = await runEntriesInWorktree({ dir, job, base: built.base, entries, log, signal, label: 'refresh', conflicts: [] });
608
+
609
+ const staged = runGitRaw(dir, ['diff', '--cached', '--quiet']);
610
+ // 0 = the index still matches `built.train`'s tree: the entries were a no-op, so there is nothing
611
+ // to commit and the merge-tree train stands as it was. 1 = something is staged, which is the
612
+ // normal "a digest was stale" case.
613
+ if (staged.status !== 0 && staged.status !== 1) throw gitFailed(dir, ['diff', '--cached', '--quiet'], staged);
614
+ if (staged.status === 0) {
615
+ if (log) fs.appendFileSync(log, `[refresh] no owned path changed (${entries.length} entry/entries); train ${built.train} unchanged\n`);
616
+ return { ...built, refreshed: [] };
617
+ }
618
+
619
+ const tree = git(dir, ['write-tree']).trim();
620
+ const train = commitTree(dir, tree, [built.base, job.tip], trainMessage(job));
621
+ if (log) fs.appendFileSync(log, `[refresh] committed ${train} (tree ${tree})\n`);
622
+ return { kind: 'train', base: built.base, tree, train, conflicts: [], refreshed: changed };
623
+ }
624
+
625
+ // --- D-6/D-7: the base rerun (§9 S-11, WP-2) ----------------------------------------------------
626
+
627
+ // The exact shapes `bash bin/test-gate.sh` prints on failure, read from the real landing logs
628
+ // (<repo>/.git/landing/logs/ — job 01m60h92c4ntd29g line 257072, job
629
+ // 01m60zmdj2e1tczj lines 3155 and 5970):
630
+ //
631
+ // [test-suite] FAILED (1 file(s)):
632
+ // - tests-integration/host-socket-protocol.test.js: terminal attempt failed (EXIT-HANG …)
633
+ // # [run-hermetic] FAILED (2 file(s)):
634
+ // # - tests/x.test.js: terminal attempt failed
635
+ // [lint-parallel] FAIL node ../bin/test-timing-lint.js --project-root . --baseline …
636
+ //
637
+ // The block body is `- <path>: <reason>` rows (bin/test-runner-adapter.js:921-944) and the `# `
638
+ // prefix appears when the gate relays another runner's stdout; both are accepted. A path is kept
639
+ // only when it really ends in `.test.js`, so a `[test-gate] FAIL ctm-changed (exit 1, …)` phase
640
+ // line or a `[test-suite] FAIL (introduced 1, pre-existing 0)` attribution line is never mistaken
641
+ // for a file.
642
+ //
643
+ // A row's path is relative to the RUNNER's cwd, not the repository root: bin/test-gate.sh:1238/1242
644
+ // invokes each project's runner as `cd <project> && node <runner> <files>`, so the real rows above
645
+ // read `tests-integration/host-socket-protocol.test.js`, not `coding-task-manager/...`. The block's
646
+ // label is what says which project that was (bin/test-runner-adapter.js:850 — `test-suite` is ctm,
647
+ // `run-hermetic` is wall-e), so it is what turns a row back into a repo-root-relative path; a path
648
+ // that already carries a project directory is never prefixed twice, and a label with no project
649
+ // (a root phase, which already runs from the repository root) is left alone. Anything the parser
650
+ // cannot place is still returned — `rerunOnBase` resolves it against the base tree and skips what
651
+ // it cannot find. Order is log order with duplicates dropped. `lint` is true when a
652
+ // `[lint-parallel] FAIL` line is present (bin/lint-parallel.js:561) — lint failures are deterministic
653
+ // on the train tree and are never rerun on base (§5 D-6).
654
+ const FAILED_BLOCK = /^\s*#?\s*\[([\w-]+)\]\s+FAILED\s+\(\d+\s+file\(s\)\):\s*$/;
655
+ const FAILED_ROW = /^\s*#?\s*[-*]\s*(\S+\.test\.js)\s*(?::|$)/;
656
+ const FAIL_LINE = /^\s*#?\s*\[([\w-]+)\]\s+FAIL\s+(\S+\.test\.js)\s*$/;
657
+ const LINT_FAIL = /^\s*#?\s*\[lint-parallel\]\s+FAIL\b/;
658
+ // The runner labels whose paths are relative to a project directory, and that directory.
659
+ const LABEL_PROJECTS = Object.freeze({ 'test-suite': 'coding-task-manager', 'run-hermetic': 'wall-e' });
660
+ const PROJECT_DIRS = Object.freeze(['coding-task-manager', 'wall-e']);
661
+
662
+ function normalizeTestPath(raw, label) {
663
+ let file = String(raw).trim().replace(/^["']+|["']+$/g, '').replace(/\\/g, '/');
664
+ while (file.startsWith('./')) file = file.slice(2);
665
+ const project = LABEL_PROJECTS[label];
666
+ if (project && !PROJECT_DIRS.some((dir) => file === dir || file.startsWith(`${dir}/`))) {
667
+ file = `${project}/${file}`;
668
+ }
669
+ return file;
670
+ }
671
+
672
+ // Returns `{ tests, lint, raw, sawBlock }`: `tests` are repo-root-relative (see above), `raw` the
673
+ // same files exactly as the log printed them (what the gate-once merge-delta shadow compares against
674
+ // its selection S — `merge-delta.failingFiles` is a wrapper over this parser so the runner's log
675
+ // format is read in ONE place), and `sawBlock` whether any `FAILED (N file(s)):` block was present
676
+ // at all (no block = "cannot say", never "nothing failed").
677
+ function failedFilesFromLog(logText) {
678
+ const tests = [];
679
+ const raw = [];
680
+ const seen = new Set();
681
+ const seenRaw = new Set();
682
+ let lint = false;
683
+ let sawBlock = false;
684
+ let blockLabel = null;
685
+ const collect = (rawFile, label) => {
686
+ const printed = String(rawFile).trim();
687
+ if (printed && !seenRaw.has(printed)) { seenRaw.add(printed); raw.push(printed); }
688
+ const file = normalizeTestPath(rawFile, label);
689
+ if (!file || seen.has(file)) return;
690
+ seen.add(file);
691
+ tests.push(file);
692
+ };
693
+ for (const line of String(logText === undefined || logText === null ? '' : logText).split('\n')) {
694
+ if (!lint && LINT_FAIL.test(line)) lint = true;
695
+ if (blockLabel !== null) {
696
+ const row = FAILED_ROW.exec(line);
697
+ if (row) {
698
+ collect(row[1], blockLabel);
699
+ continue;
700
+ }
701
+ // A blank line or anything that is not another `- path` row ends the block.
702
+ blockLabel = null;
703
+ }
704
+ const block = FAILED_BLOCK.exec(line);
705
+ if (block) {
706
+ blockLabel = block[1];
707
+ sawBlock = true;
708
+ continue;
709
+ }
710
+ const failed = FAIL_LINE.exec(line);
711
+ if (failed) collect(failed[2], failed[1]);
712
+ }
713
+ return { tests, lint, raw, sawBlock };
714
+ }
715
+
716
+ // The launchers a failed file can belong to, in a fixed order so a rerun is deterministic. A file is
717
+ // routed by its repo-root-relative prefix: `coding-task-manager/` and `wall-e/` are the two projects
718
+ // with their own runner (bin/test-gate.sh:1238/1242 invokes each as
719
+ // `cd <project> && node <runner> <project-relative files>`), and everything else is a root test run
720
+ // through the slot broker. `dir` is where the launcher runs, relative to the base worktree; `argv`
721
+ // builds its command line from project-relative files.
722
+ const RERUN_LAUNCHERS = Object.freeze([
723
+ { id: 'coding-task-manager', dir: 'coding-task-manager', argv: (files) => ['bin/run-test-suite.js', ...files] },
724
+ { id: 'wall-e', dir: 'wall-e', argv: (files) => ['tests/run-hermetic.js', ...files] },
725
+ { id: 'root', dir: null, argv: (files) => ['bin/with-test-slots.js', '--', 'node', '--test', ...files] },
726
+ ]);
727
+
728
+ function launcherFor(file) {
729
+ for (const launcher of RERUN_LAUNCHERS) {
730
+ if (launcher.dir && file.startsWith(`${launcher.dir}/`)) {
731
+ return { launcher, file: file.slice(launcher.dir.length + 1) };
732
+ }
733
+ }
734
+ return { launcher: RERUN_LAUNCHERS[RERUN_LAUNCHERS.length - 1], file };
735
+ }
736
+
737
+ // A single-quoted POSIX word: the launcher command goes through `/bin/sh -c` (`runShell`), so a path
738
+ // with a space or a quote in it has to survive the shell.
739
+ function shQuote(value) {
740
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
741
+ }
742
+
743
+ // The base worktree lives beside the gate's own train worktree, in the same trains root, so both are
744
+ // created and reaped by the same helpers and neither can be mistaken for somebody's checkout.
745
+ function baseWorktreeDir(cwd, jobId) {
746
+ return path.join(trainsRoot(cwd, commonDir(cwd)).dir, `land-${jobId}-base`);
747
+ }
748
+
749
+ // D-6/D-7: rerun the files a FAILING train named on the BASE commit — Chromium's with-patch /
750
+ // without-patch rule: a failure that reproduces WITHOUT the branch is not the branch's failure.
751
+ //
752
+ // The rerun happens in a second worktree at `base` (same trains root and same cache symlinks as the
753
+ // gate's own), removed in the `finally` exactly like the gate's. Each project's launcher runs ONCE
754
+ // with only its own files, and a file counts as `failedOnBase` only when its launcher exits non-zero
755
+ // AND names that file through the same FAILED-block shape the gate prints — an exit code alone is
756
+ // not evidence about a specific file. A file that does not EXIST on base (a test the branch added)
757
+ // cannot be base-broken and is skipped, as is one whose launcher cannot run at all.
758
+ //
759
+ // The rerun output is appended to the job's gate log under a `[triage] rerun on base <sha12>` header,
760
+ // so one file holds both runs and `land inspect <job>` shows the whole diagnosis.
761
+ //
762
+ // `signal` is the job's AbortSignal: an abort stops the loop and returns what ran so far with
763
+ // `aborted: true` — the runner is being stopped, and a stopped diagnosis is never a verdict.
764
+ async function rerunOnBase(cwd, { base, files, root, jobId, signal, env, cachePaths = [], timeoutMs } = {}) {
765
+ const started = Date.now();
766
+ const ranFiles = [];
767
+ const named = new Set();
768
+ const dir = baseWorktreeDir(cwd, jobId);
769
+ const logFile = path.join(root, 'logs', `${jobId}.log`);
770
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'land-triage-'));
771
+ let aborted = false;
772
+ try {
773
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
774
+ fs.appendFileSync(logFile, `\n[triage] rerun on base ${String(base).slice(0, 12)}\n`);
775
+ addTrainWorktree(cwd, dir, base, jobId);
776
+ linkCachePaths(dir, cwd, cachePaths);
777
+
778
+ // Group by launcher in RERUN_LAUNCHERS order so the log reads the same way every time.
779
+ const groups = [];
780
+ for (const file of files) {
781
+ if (!fs.existsSync(path.join(dir, file))) continue; // added by the branch: base cannot be broken by it
782
+ const routed = launcherFor(file);
783
+ let group = groups.find((candidate) => candidate.id === routed.launcher.id);
784
+ if (!group) {
785
+ group = { id: routed.launcher.id, launcher: routed.launcher, files: [] };
786
+ groups.push(group);
787
+ }
788
+ group.files.push(routed.file);
789
+ ranFiles.push(file);
790
+ }
791
+ const rank = (group) => RERUN_LAUNCHERS.findIndex((launcher) => launcher.id === group.id);
792
+ groups.sort((a, b) => rank(a) - rank(b));
793
+
794
+ for (const group of groups) {
795
+ if (signal && signal.aborted) {
796
+ aborted = true;
797
+ break;
798
+ }
799
+ const groupCwd = group.launcher.dir ? path.join(dir, group.launcher.dir) : dir;
800
+ const command = ['node', ...group.launcher.argv(group.files)].map(shQuote).join(' ');
801
+ fs.appendFileSync(logFile, `$ (cd ${group.launcher.dir || '.'} && ${command})\n`);
802
+ const outFile = path.join(scratch, `${group.id}.out`);
803
+ const outFd = fs.openSync(outFile, 'a');
804
+ let res;
805
+ try {
806
+ // eslint-disable-next-line no-await-in-loop -- launchers share the machine and run one at a time
807
+ res = await runShell(command, {
808
+ cwd: groupCwd,
809
+ extra: { ...(env || {}), LAND_JOB: jobId, LAND_BASE: base, LAND_TRIAGE: 'base' },
810
+ logFd: outFd,
811
+ // Bounded like the gate itself (the caller passes the gate's own timeout): a hanging test on
812
+ // base must not hold the runner lease past what the gate it is diagnosing was allowed.
813
+ timeoutMs,
814
+ signal,
815
+ });
816
+ } finally {
817
+ fs.closeSync(outFd);
818
+ }
819
+ const output = fs.readFileSync(outFile, 'utf8');
820
+ fs.appendFileSync(logFile, output);
821
+ if (res.aborted) {
822
+ aborted = true;
823
+ break;
824
+ }
825
+ if (res.status === 0 || res.error) continue; // a launcher that cannot run proves nothing about base
826
+ const parsed = failedFilesFromLog(output);
827
+ for (const entry of parsed.tests) {
828
+ const file = group.launcher.dir && entry.startsWith(`${group.launcher.dir}/`)
829
+ ? entry.slice(group.launcher.dir.length + 1)
830
+ : entry;
831
+ if (group.files.includes(file)) named.add(file);
832
+ }
833
+ }
834
+ } finally {
835
+ removeTrainWorktree(cwd, dir);
836
+ fs.rmSync(scratch, { recursive: true, force: true });
837
+ }
838
+ const failedOnBase = files.filter((file) => {
839
+ const routed = launcherFor(file);
840
+ return named.has(routed.file) && ranFiles.includes(file);
841
+ });
842
+ return {
843
+ ranFiles,
844
+ failedOnBase,
845
+ aborted,
846
+ seconds: Number(((Date.now() - started) / 1000).toFixed(3)),
847
+ logPath: logFile,
848
+ };
849
+ }
850
+
851
+ module.exports = {
852
+ MAX_ROUNDS,
853
+ GATE_KILL_GRACE_MS,
854
+ killGroup,
855
+ runShell,
856
+ targetRef,
857
+ globToRegExp,
858
+ globMatch,
859
+ matchingRegenerateEntries,
860
+ matchingRefreshEntries,
861
+ trainMessage,
862
+ buildTrain,
863
+ buildBatchTrain,
864
+ trainsRoot,
865
+ trainDirFor,
866
+ prepareTrainDir,
867
+ addTrainWorktree,
868
+ removeTrainWorktree,
869
+ worktreeList,
870
+ targetWorktreeFor,
871
+ commitsBetween,
872
+ mergeBase,
873
+ landTrain,
874
+ changedFilesBetween,
875
+ hunkHeaders,
876
+ overlapWithTarget,
877
+ regenerateTrain,
878
+ refreshTrain,
879
+ linkCachePaths,
880
+ failedFilesFromLog,
881
+ launcherFor,
882
+ shQuote,
883
+ baseWorktreeDir,
884
+ rerunOnBase,
885
+ };