elanous 0.2.17 → 0.2.19

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 (491) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -0
  132. package/apps/pwa/out/decisions/index.txt +30 -0
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +2 -1
  207. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  208. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  209. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  210. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  211. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  212. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  213. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  214. package/scripts/ci-public-export-import-gate.ts +7 -2
  215. package/scripts/ci-typecheck-changed.ts +21 -6
  216. package/scripts/coord-channel-watch.sh +2 -2
  217. package/scripts/discord-seat-probe.ts +6 -3
  218. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  219. package/scripts/goals/authoring-metrics.ts +73 -0
  220. package/scripts/landing-heal/must-fix-match.ts +7 -7
  221. package/scripts/landing-heal/run-mission.ts +3 -2
  222. package/scripts/lib/pch-regress-map.ts +23 -0
  223. package/scripts/morning-report.ts +18 -4
  224. package/scripts/pch-regress.ts +88 -0
  225. package/scripts/public-export-test-run.ts +37 -2
  226. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  227. package/scripts/release-loop/cut-branch.ts +2 -1
  228. package/scripts/release-loop/cutoff.ts +4 -2
  229. package/scripts/release-loop/docs-land-node.ts +8 -1
  230. package/scripts/release-loop/docs-node.ts +1 -1
  231. package/scripts/release-loop/gate-diff.ts +18 -0
  232. package/scripts/release-loop/gate-node.ts +436 -59
  233. package/scripts/release-loop/gate-partial.ts +106 -0
  234. package/scripts/release-loop/gate-timing.ts +120 -0
  235. package/scripts/release-loop/node-verdict.ts +2 -1
  236. package/scripts/release-loop/npm-publish-node.ts +11 -7
  237. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  238. package/scripts/release-loop/preflight.ts +246 -0
  239. package/scripts/release-loop/publish-node.ts +8 -1
  240. package/scripts/release-loop/release-readiness.ts +11 -6
  241. package/scripts/release-loop/release-version.ts +48 -0
  242. package/scripts/release-loop/resume-release.ts +174 -0
  243. package/scripts/release-loop/shard-plan.ts +65 -0
  244. package/scripts/release-loop/unattended-release.ts +45 -7
  245. package/scripts/release-loop/version-node.ts +145 -40
  246. package/scripts/rhythm/daily.ts +25 -4
  247. package/scripts/skills/public-check.ts +62 -12
  248. package/scripts/test-deterministic.ts +51 -33
  249. package/scripts/test-diet/effectiveness.ts +62 -1
  250. package/scripts/test-diet/lib.ts +1 -1
  251. package/scripts/test-diet/node.ts +8 -1
  252. package/scripts/video/scene-table.ts +159 -0
  253. package/src/acp/core-turn-bridge.ts +39 -2
  254. package/src/agent-mission/review-loop.ts +4 -3
  255. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  256. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  257. package/src/away/presence.ts +40 -0
  258. package/src/away/release-watch.ts +255 -0
  259. package/src/briefing/brief-items.ts +86 -17
  260. package/src/chat/index.ts +2 -1
  261. package/src/chat/short-question.ts +3 -40
  262. package/src/cli/autopilot-cli.ts +30 -11
  263. package/src/cli/away-cli.ts +68 -0
  264. package/src/cli/brief-cli.ts +36 -2
  265. package/src/cli/freeze-cli.ts +3 -2
  266. package/src/cli/guardian-cli.ts +49 -0
  267. package/src/cli/intake-cli.ts +85 -7
  268. package/src/cli/log-event-names.ts +1 -0
  269. package/src/cli/notify-cli.ts +75 -0
  270. package/src/cli/pod-cli.ts +5 -1
  271. package/src/cli/provider-cli.ts +12 -2
  272. package/src/cli/pty-drive-cli.ts +14 -3
  273. package/src/cli/release-cli.ts +128 -19
  274. package/src/cli/setup-cli.ts +31 -1
  275. package/src/cli/tasks-cli.ts +403 -4
  276. package/src/context-bus/condense.ts +19 -3
  277. package/src/context-bus/context-day.ts +77 -24
  278. package/src/context-bus/context-first.ts +33 -0
  279. package/src/context-bus/context-now-public.ts +1 -1
  280. package/src/context-bus/context-now-surfaces.ts +16 -6
  281. package/src/context-bus/context-now.ts +97 -3
  282. package/src/core-turn/short-question.ts +100 -0
  283. package/src/dashboard/dashboard-status-lines.ts +24 -4
  284. package/src/dashboard/index.ts +50 -5
  285. package/src/dashboard/input/slash-executor.ts +6 -2
  286. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  287. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  288. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  289. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  290. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  291. package/src/discord-self-message.ts +20 -2
  292. package/src/discord-slash-wire.ts +1 -0
  293. package/src/domains/guardian-tick.ts +138 -0
  294. package/src/domains/outbound-alert.ts +244 -40
  295. package/src/domains/outbound-log-probe.ts +16 -1
  296. package/src/domains/release-tool.ts +2 -2
  297. package/src/domains/schedule-registry.ts +23 -0
  298. package/src/domains/telegram-kind-route.ts +39 -0
  299. package/src/domains/watch-default-scheduler.ts +95 -0
  300. package/src/esc-abort-gate.ts +44 -0
  301. package/src/execution-loop/gates.ts +11 -2
  302. package/src/flow-loop/split.ts +8 -4
  303. package/src/git-fs/worktree.ts +23 -3
  304. package/src/graph-runner/runner.ts +25 -5
  305. package/src/harness/harness-cli-command.ts +12 -2
  306. package/src/harness/harness-queue.ts +388 -33
  307. package/src/harness/harness-salvage-cli.ts +78 -0
  308. package/src/harness/harness-worktree-add.ts +68 -0
  309. package/src/harness/helper-scan.ts +10 -3
  310. package/src/harness/ledger-write-guard.ts +6 -16
  311. package/src/hq/hq.ts +6 -1
  312. package/src/hq/seats.ts +369 -0
  313. package/src/index.ts +185 -18
  314. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  315. package/src/instance/test-write-guard.ts +200 -0
  316. package/src/intake-plane/adopt-gate.ts +49 -0
  317. package/src/intake-plane/check.ts +3 -0
  318. package/src/intake-plane/collect-github.ts +57 -25
  319. package/src/intake-plane/digest.ts +39 -15
  320. package/src/intake-plane/discord-seat-work.ts +8 -2
  321. package/src/intake-plane/harness-target.ts +4 -3
  322. package/src/intake-plane/intake-sources.ts +1 -0
  323. package/src/intake-plane/items.ts +1 -0
  324. package/src/intake-plane/requirement-funnel.ts +316 -0
  325. package/src/knowledge/know-find.ts +10 -1
  326. package/src/loops/budget.ts +36 -0
  327. package/src/loops/orchestrator/degradation.ts +57 -0
  328. package/src/loops/orchestrator/finish-rate.ts +106 -4
  329. package/src/loops/orchestrator/grid-place.ts +11 -0
  330. package/src/loops/orchestrator/tick.ts +450 -54
  331. package/src/maturity/feature-maturity.ts +8 -5
  332. package/src/maturity/tui-slash-maturity.ts +1 -0
  333. package/src/mss/logging/log-store.ts +3 -0
  334. package/src/nexus/api/decisions-route.ts +11 -1
  335. package/src/nexus/api/http-server.ts +16 -5
  336. package/src/nexus/api/loop-edges.ts +352 -23
  337. package/src/nexus/api/ops-api.ts +96 -28
  338. package/src/nexus/api/ops-seats-sources.ts +2 -2
  339. package/src/nexus/api/workflow-templates.ts +3 -0
  340. package/src/nexus/index.ts +7 -0
  341. package/src/oauth/codex.ts +38 -0
  342. package/src/onboarding/first-chat-gate.ts +29 -0
  343. package/src/release-loop/checklist.ts +106 -6
  344. package/src/release-loop/feature-store.ts +64 -16
  345. package/src/release-loop/landing-freeze.ts +6 -4
  346. package/src/release-loop/light-rc.ts +173 -0
  347. package/src/release-loop/placement.ts +64 -20
  348. package/src/release-loop/rubric.ts +136 -0
  349. package/src/seat-dispatch/seat-ask.ts +79 -16
  350. package/src/seat-loop/seat-loop.ts +12 -6
  351. package/src/self-dev/ask-launch-flow.ts +24 -3
  352. package/src/self-dev/ask-path-candidates.ts +68 -0
  353. package/src/self-dev/dev-cli.ts +36 -0
  354. package/src/self-dev/dev-pipeline.ts +4 -0
  355. package/src/self-dev/draft-triage-rules.ts +1 -1
  356. package/src/self-dev/orchestrate-cli.ts +22 -0
  357. package/src/self-dev/orchestrate.ts +1 -0
  358. package/src/self-dev/pr-preview.ts +179 -0
  359. package/src/self-dev/round-pr-supersede.ts +244 -0
  360. package/src/self-dev/run-supervisor.ts +14 -0
  361. package/src/self-dev/shard-run.ts +23 -7
  362. package/src/self-implement/follow-up-goals.ts +2 -2
  363. package/src/self-implement/gate-cli.ts +29 -8
  364. package/src/self-implement/gate-remote.ts +427 -0
  365. package/src/self-implement/goal-author-clarification.ts +88 -13
  366. package/src/self-implement/goal-author.ts +8 -3
  367. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  368. package/src/self-implement/l8-merge-queue.ts +29 -5
  369. package/src/self-implement/orchestrator.ts +61 -13
  370. package/src/self-implement/prior-draft-findings.ts +4 -3
  371. package/src/self-implement/salvage-branches.ts +254 -0
  372. package/src/skills/skill-feedback.ts +76 -0
  373. package/src/task-agent/actions.ts +101 -7
  374. package/src/task-agent/board.ts +132 -0
  375. package/src/task-agent/card-evidence.ts +203 -0
  376. package/src/task-agent/completion-evidence.ts +126 -0
  377. package/src/task-agent/judge.ts +49 -5
  378. package/src/task-agent/live-moves.ts +176 -0
  379. package/src/task-agent/mission.ts +449 -0
  380. package/src/task-agent/shadow.ts +258 -0
  381. package/src/task-agent/task-hand.ts +337 -0
  382. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  383. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  384. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  385. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  386. package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
  387. package/src/telegram-away-command.ts +38 -0
  388. package/src/telegram-commands.ts +11 -0
  389. package/src/telegram-loops-command.ts +1 -1
  390. package/src/telegram-report-log-probe.ts +21 -0
  391. package/src/telegram-report.ts +27 -3
  392. package/src/telegram.ts +5 -20
  393. package/src/tui-client/dashboard-session.ts +1 -0
  394. package/src/user-config.ts +196 -4
  395. package/src/version/packed-revision.json +1 -1
  396. package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  401. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
  420. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  429. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
  489. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  490. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  491. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -26,10 +26,13 @@ import {
26
26
  import { listWorktrees } from '../git-fs/worktree.js';
27
27
  import { runShardedGateTests } from '../self-dev/shard-run.js';
28
28
 
29
- interface SelfGateCliOptions {
29
+ export interface SelfGateCliOptions {
30
30
  base?: string;
31
31
  pr?: string;
32
32
  shards?: number;
33
+ /** LIGHT-RC (0.2.19) — repository-relative test paths run on every call, whatever changed. Existing ones join the
34
+ * run (deduplicated, same introduced/preexisting baseline as the rest); missing ones go to `alwaysIncludeMissing`. */
35
+ alwaysInclude?: readonly string[];
33
36
  }
34
37
 
35
38
  interface ProcessResult {
@@ -102,7 +105,7 @@ interface GateOutput {
102
105
  error: (message: string) => void;
103
106
  }
104
107
 
105
- interface SelfGateCliResult {
108
+ export interface SelfGateCliResult {
106
109
  exitCode: number;
107
110
  lines: string[];
108
111
  changedFiles: string[];
@@ -110,6 +113,11 @@ interface SelfGateCliResult {
110
113
  unverified: readonly string[];
111
114
  documentPaths: readonly string[];
112
115
  documentsWithoutDerivedTests: readonly string[];
116
+ /** `alwaysInclude` paths that do not exist in the tree (empty when none were given). */
117
+ alwaysIncludeMissing: readonly string[];
118
+ /** New vs. already-present failures (unknown/preconditionUnmet = failures it could not classify). Absent = the test
119
+ * step did not produce a measurement (e.g. shards unmeasured). */
120
+ baseline?: { introduced: number; preexisting: number; unknown: number; preconditionUnmet: number };
113
121
  }
114
122
 
115
123
  /** 시험 단계와 독립인 정책 관문 넷. skipTestStep 이든 시험 통과든 같은 함수를 부른다. */
@@ -590,7 +598,12 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
590
598
  const expose = publicExposureFiles(selection.files).length > 0
591
599
  ? runExposeGate(cwd, selection.files, getUserConfig().harness?.exposeGate === 'strict' ? 'strict' : 'warn', deps.runCommand)
592
600
  : { passed: true, log: '' };
593
- const testFiles = [...(scope.testArgs ?? [])];
601
+ const alwaysInclude = [...new Set((options.alwaysInclude ?? []).map(normalizeGatePath).filter(Boolean))];
602
+ const alwaysIncludeMissing = alwaysInclude.filter((path) => !exists(path));
603
+ const scopedTests = scope.testArgs ?? [];
604
+ const alwaysIncludeExtra = alwaysInclude.filter((path) => exists(path) && !scopedTests.includes(path));
605
+ const testFiles = [...scopedTests, ...alwaysIncludeExtra];
606
+ const skipTestStep = scope.skipTestStep && alwaysIncludeExtra.length === 0;
594
607
  const unrunImporterTotal = lookupFailed ? null : (scope.importerTestsNotRun?.total ?? 0);
595
608
  const importerLookupFailed = Boolean(lookupFailed);
596
609
  const lines = [
@@ -619,6 +632,10 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
619
632
  ...(routeIndex?.lookupFailures.length ? [`⚠️ caller route base lookup failed — unmeasured: ${routeIndex.lookupFailures.map(({ source, reason }) => `${source} (${reason})`).join(', ')}`] : []),
620
633
  ...(scope.callerTestsOverflow.length ? [`⚠️ caller test cap exceeded — not run: ${scope.callerTestsOverflow.map(({ file, reasons }) => `${file} (${reasons.join('+')})`).join(', ')}`] : []),
621
634
  changedTestCountNote(cwd, selection.files, selection.baseRef, deps.runCommand, deps.readFile),
635
+ // Only printed when the caller asked for always-include paths — the plain `self gate` output is unchanged.
636
+ ...(options.alwaysInclude !== undefined
637
+ ? [`always include: ${alwaysInclude.length - alwaysIncludeMissing.length}/${alwaysInclude.length} (added ${alwaysIncludeExtra.length}${alwaysIncludeMissing.length ? ` · missing: ${alwaysIncludeMissing.join(', ')}` : ''})`]
638
+ : []),
622
639
  ];
623
640
  if (expose.log) lines.push(expose.log);
624
641
  if (options.pr) {
@@ -641,17 +658,18 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
641
658
  const iosPassed = runAdditionalGate('ios-gate', deps.runIosGate ?? runIosUnitTestGate, selection.files, cwd, lines);
642
659
  const pwaPassed = runAdditionalGate('pwa-gate', deps.runPwaGate ?? runPwaBuildGate, selection.files, cwd, lines);
643
660
 
644
- if (scope.skipTestStep) {
661
+ if (skipTestStep) {
645
662
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
646
663
  logGateCliBaseline(cwd, { ...ZERO_GATE_BASELINE, unrunImporterTotal, lookupFailed: importerLookupFailed });
647
- return { exitCode: androidPassed && iosPassed && pwaPassed && policyPassed && expose.passed ? 0 : 1, lines, changedFiles: selection.files, testFiles: [], unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
664
+ return { exitCode: androidPassed && iosPassed && pwaPassed && policyPassed && expose.passed ? 0 : 1, lines, changedFiles: selection.files, testFiles: [], unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests, alwaysIncludeMissing, baseline: { introduced: 0, preexisting: 0, unknown: 0, preconditionUnmet: 0 } };
648
665
  }
649
666
 
650
667
  if (options.shards !== undefined) {
651
668
  const sharded = (deps.runShards ?? runShardedGateTests)(cwd, testFiles, selection.baseRef, options.shards);
652
- lines.push(`shards: ${sharded.shards.length} (${sharded.shards.map((shard) => `${shard.id}=${shard.files.join(',')}`).join('; ')})`);
669
+ lines.push(`shards: ${sharded.shards.length} (${sharded.shards.map((shard) => `${shard.id}=${shard.files.join(',')}`).join('; ')})${sharded.measurementFailures ? ` · 측정 실패 ${sharded.measurementFailures}` : ''}`);
653
670
  for (const attempt of sharded.attempts) lines.push(`shard attempt: ${attempt.shardId} #${attempt.attempt}`);
654
671
  const { aggregate } = sharded;
672
+ let shardBaseline: SelfGateCliResult['baseline'];
655
673
  if (aggregate.status === 'unmeasured') lines.push(`shards: unmeasured (${aggregate.retryShardIds.join(', ') || sharded.reason || 'no complete JUnit'})`);
656
674
  else if (aggregate.report) {
657
675
  const report = aggregate.report;
@@ -662,15 +680,18 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
662
680
  baselineFiles: report.files, baselineStatus: report.baselineStatus, unrunImporterTotal, lookupFailed: importerLookupFailed,
663
681
  });
664
682
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, formatGateBaselineNote(report, unrunImporterTotal ?? 0)));
683
+ shardBaseline = { introduced: report.introduced, preexisting: report.preexisting, unknown: report.unknown, preconditionUnmet: report.preconditionUnmet };
665
684
  } else {
666
685
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, `tests: pass (${testFiles.length} files)`));
667
686
  logGateCliBaseline(cwd, { ...ZERO_GATE_BASELINE, unrunImporterTotal, lookupFailed: importerLookupFailed });
687
+ shardBaseline = { introduced: 0, preexisting: 0, unknown: 0, preconditionUnmet: 0 };
668
688
  }
669
689
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
670
690
  return {
671
691
  exitCode: aggregate.status !== 'passed' || !policyPassed || !androidPassed || !iosPassed || !pwaPassed || !expose.passed ? 1 : 0,
672
692
  lines, changedFiles: selection.files, testFiles, unverified: scope.unverified,
673
693
  documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests,
694
+ alwaysIncludeMissing, ...(shardBaseline ? { baseline: shardBaseline } : {}),
674
695
  };
675
696
  }
676
697
 
@@ -680,7 +701,7 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
680
701
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, `tests: pass (${testFiles.length} files)`));
681
702
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
682
703
  logGateCliBaseline(cwd, { ...ZERO_GATE_BASELINE, unrunImporterTotal, lookupFailed: importerLookupFailed });
683
- return { exitCode: policyPassed && androidPassed && iosPassed && pwaPassed && expose.passed ? 0 : 1, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
704
+ return { exitCode: policyPassed && androidPassed && iosPassed && pwaPassed && expose.passed ? 0 : 1, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests, alwaysIncludeMissing, baseline: { introduced: 0, preexisting: 0, unknown: 0, preconditionUnmet: 0 } };
684
705
  }
685
706
 
686
707
  if (worktreeLog.includes('deterministic environment setup failed')) lines.push(worktreeLog);
@@ -702,5 +723,5 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
702
723
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, formatGateBaselineNote(report, unrunImporterTotal ?? 0)));
703
724
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
704
725
  const exitCode = !androidPassed || !iosPassed || !pwaPassed || !policyPassed || !expose.passed || report.introduced > 0 || report.unknown > 0 || report.preconditionUnmet > 0 ? 1 : 0;
705
- return { exitCode, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
726
+ return { exitCode, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests, alwaysIncludeMissing, baseline: { introduced: report.introduced, preexisting: report.preexisting, unknown: report.unknown, preconditionUnmet: report.preconditionUnmet } };
706
727
  }
@@ -0,0 +1,427 @@
1
+ /** GATE-REMOTE (0.2.18 · 10-06) — run a heavy pre-landing check (self gate · changed-file tsc · public-export test run)
2
+ * on a remote host at the SAME commit and bring back only its result (exit code · stdout · stderr).
3
+ * Reach: the release gate's bare mirror (`~/mirror/elanous-agent.git`, refs/elanous/gate/<sha>) — copied minimally from
4
+ * scripts/release-loop/gate-node.ts (RELGUARD path, not edited); gate-node could later switch to this module. */
5
+ import { spawnSync } from 'node:child_process';
6
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
7
+ import { loadavg } from 'node:os';
8
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
9
+ import { debug } from '../debug/log.js';
10
+ import type { GateRemoteConfig } from '../user-config.js';
11
+
12
+ export type GateRemoteTool = 'self-gate' | 'ci-typecheck-changed' | 'public-export-test-run';
13
+
14
+ export interface GateRemoteSettings {
15
+ enabled: boolean;
16
+ host: string;
17
+ loadThreshold: number;
18
+ hostCap: number;
19
+ mirror: string;
20
+ slotWaitSeconds: number;
21
+ }
22
+
23
+ export const MAX_HOST_CAP = 2;
24
+
25
+ export const GATE_REMOTE_DEFAULTS: GateRemoteSettings = {
26
+ enabled: true, host: 'node-b', loadThreshold: 20, hostCap: 2, mirror: '~/mirror/elanous-agent.git', slotWaitSeconds: 600,
27
+ };
28
+
29
+ const hostPattern = /^(?:[\w.-]+@)?[\w.-]+$/;
30
+ export const isValidGateHost = (host: string): boolean => hostPattern.test(host) && !host.startsWith('-');
31
+ const mirrorPattern = /^(?:\/|~\/)[\w./-]+$/;
32
+ const shaPattern = /^[0-9a-f]{40}$/;
33
+
34
+ /** config > env > default (user-config hierarchy). Env: ELANOUS_GATE_REMOTE=off|on · ELANOUS_GATE_REMOTE_HOST · ELANOUS_GATE_REMOTE_LOAD. */
35
+ export function resolveGateRemoteSettings(config: GateRemoteConfig | undefined, env: NodeJS.ProcessEnv = process.env): GateRemoteSettings {
36
+ const envEnabled = env.ELANOUS_GATE_REMOTE === 'off' || env.ELANOUS_GATE_REMOTE === '0' ? false
37
+ : env.ELANOUS_GATE_REMOTE === 'on' || env.ELANOUS_GATE_REMOTE === '1' ? true : undefined;
38
+ const envLoadRaw = env.ELANOUS_GATE_REMOTE_LOAD && /^\d+(?:\.\d+)?$/.test(env.ELANOUS_GATE_REMOTE_LOAD) ? Number(env.ELANOUS_GATE_REMOTE_LOAD) : undefined;
39
+ const envLoad = envLoadRaw !== undefined && Number.isFinite(envLoadRaw) ? envLoadRaw : undefined;
40
+ const host = config?.host ?? env.ELANOUS_GATE_REMOTE_HOST?.trim() ?? GATE_REMOTE_DEFAULTS.host;
41
+ const mirror = config?.mirror ?? GATE_REMOTE_DEFAULTS.mirror;
42
+ return {
43
+ enabled: config?.enabled ?? envEnabled ?? GATE_REMOTE_DEFAULTS.enabled,
44
+ host: isValidGateHost(host) ? host : GATE_REMOTE_DEFAULTS.host,
45
+ loadThreshold: config?.loadThreshold ?? envLoad ?? GATE_REMOTE_DEFAULTS.loadThreshold,
46
+ // The cell's ceiling is 2 per host (GATE-REMOTE acceptance); config may only lower it.
47
+ hostCap: Math.min(MAX_HOST_CAP, Math.max(1, Math.floor(config?.hostCap ?? GATE_REMOTE_DEFAULTS.hostCap))),
48
+ mirror: mirrorPattern.test(mirror) && !mirror.split('/').includes('..') ? mirror : GATE_REMOTE_DEFAULTS.mirror,
49
+ slotWaitSeconds: config?.slotWaitSeconds ?? GATE_REMOTE_DEFAULTS.slotWaitSeconds,
50
+ };
51
+ }
52
+
53
+ /** The token after a bare `--remote` is its host when it is a valid ssh destination that is not an existing path and does
54
+ * not look like a test/list file — so `--remote node-b.lan a.test.ts` takes the host and `--remote a.test.ts` keeps the file. */
55
+ export function looksLikeHost(token: string, exists: (path: string) => boolean = existsSync): boolean {
56
+ if (!isValidGateHost(token) || exists(token)) return false;
57
+ return !/\.(?:test\.)?(?:[cm]?[jt]sx?|txt|json|md)$/i.test(token);
58
+ }
59
+
60
+ export interface GateRemoteFlags { remote?: string | true; local: boolean; rest: string[]; error?: string }
61
+
62
+ /** Pull `--remote [host]` / `--remote=<host>` / `--local` out of argv; everything else is left for the tool. */
63
+ export function extractGateRemoteFlags(argv: readonly string[]): GateRemoteFlags {
64
+ const out: GateRemoteFlags = { local: false, rest: [] };
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const arg = argv[i]!;
67
+ if (arg === '--local') out.local = true;
68
+ else if (arg.startsWith('--remote=')) out.remote = arg.slice('--remote='.length);
69
+ else if (arg === '--remote') {
70
+ const next = argv[i + 1];
71
+ if (next !== undefined && looksLikeHost(next)) { out.remote = next; i++; } else out.remote = true;
72
+ } else out.rest.push(arg);
73
+ }
74
+ if (typeof out.remote === 'string' && !isValidGateHost(out.remote)) out.error = `invalid --remote host: ${out.remote}`;
75
+ if (out.remote !== undefined && out.local) out.error = '--remote and --local cannot be combined';
76
+ return out;
77
+ }
78
+
79
+ export interface GateDispatchDecision { mode: 'local' | 'remote'; host?: string; reason: string; load1: number }
80
+
81
+ export function decideGateDispatch(input: { flags: Pick<GateRemoteFlags, 'remote' | 'local'>; settings: GateRemoteSettings; load1: number; env?: NodeJS.ProcessEnv }): GateDispatchDecision {
82
+ const { flags, settings, load1 } = input;
83
+ const env = input.env ?? process.env;
84
+ if (flags.local) return { mode: 'local', reason: 'flag-local', load1 };
85
+ if (env.ELANOUS_GATE_REMOTE_CHILD === '1') return { mode: 'local', reason: 'remote-child', load1 };
86
+ if (flags.remote !== undefined) return { mode: 'remote', host: flags.remote === true ? settings.host : flags.remote, reason: 'flag-remote', load1 };
87
+ if (!settings.enabled) return { mode: 'local', reason: 'config-off', load1 };
88
+ // Tests never reach a real host on their own (an explicit --remote with an injected runner still can).
89
+ if (env.NODE_ENV === 'test') return { mode: 'local', reason: 'test-env', load1 };
90
+ if (load1 > settings.loadThreshold) return { mode: 'remote', host: settings.host, reason: `load ${load1.toFixed(1)} > ${settings.loadThreshold}`, load1 };
91
+ return { mode: 'local', reason: `load ${load1.toFixed(1)} <= ${settings.loadThreshold}`, load1 };
92
+ }
93
+
94
+ // ── host lock (on the remote host, cap N) ───────────────────────
95
+ // The slots live on the host itself, so every machine that dispatches there shares one cap. Each slot is a kernel file
96
+ // lock (flock(1) / BSD lockf(1)) held for exactly the life of the run — a crashed or killed run releases it, so there is
97
+ // no staleness guess and no reclaim race.
98
+
99
+ export const SLOT_DIR = '/tmp/elanous-gate-remote-slots';
100
+ const BUSY = '__GATE_REMOTE_BUSY';
101
+
102
+ /** Shell lines that run "$W/run.sh" under the first free of `cap` slot locks, or print the busy marker and exit 98.
103
+ * run.sh must create "$W/started" first: that is how a lock refusal is told apart from the run's own exit code. */
104
+ export type SlotLockTool = 'flock' | 'lockf';
105
+ const SLOT_LOCK = { flock: 'flock -n', lockf: 'lockf -k -s -t 0' } as const;
106
+
107
+ /** `tools` is the preference order (default: Linux flock, then BSD/macOS lockf); a test can pin one path. */
108
+ export function hostSlotScript(cap: number, dir = SLOT_DIR, tools: readonly SlotLockTool[] = ['flock', 'lockf']): string[] {
109
+ const n = Math.min(MAX_HOST_CAP, Math.max(1, Math.floor(cap)));
110
+ const pick = tools.map((tool, i) => `${i ? 'elif' : 'if'} command -v ${tool} >/dev/null 2>&1; then GL="${SLOT_LOCK[tool]}"; `).join('');
111
+ return [
112
+ `GS=${quote(dir)}; mkdir -p "$GS" || infra slots`,
113
+ `${pick}else infra lock-tool; fi`,
114
+ `n=1; while [ $n -le ${n} ]; do $GL "$GS/slot$n" sh "$W/run.sh"; s=$?; [ -f "$W/started" ] && exit $s; n=$((n+1)); done`,
115
+ `echo "${BUSY}" >&2; exit 98`,
116
+ ];
117
+ }
118
+
119
+ // ── remote execution ───────────────────────────────────────────────
120
+
121
+ export interface ExecResult { rc: number | null; stdout: string; stderr: string; error?: string }
122
+ /** Raw bytes from the host — the tool's output is passed on byte for byte (strings are accepted from test runners). */
123
+ export interface SshResult { rc: number | null; stdout: Buffer | string; stderr: Buffer | string; error?: string }
124
+ export interface GateRemoteRunner {
125
+ /** Run a command locally (git rev-parse · status · push). */
126
+ local(cmd: string, args: string[], cwd: string, options?: { timeoutMs?: number; env?: NodeJS.ProcessEnv }): ExecResult;
127
+ /** Run a shell script on the host over ssh. */
128
+ ssh(host: string, script: string, timeoutMs: number): SshResult;
129
+ }
130
+
131
+ export const realGateRemoteRunner: GateRemoteRunner = {
132
+ local(cmd, args, cwd, options) {
133
+ const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
134
+ ...(options?.env ? { env: options.env } : {}), ...(options?.timeoutMs ? { timeout: options.timeoutMs, killSignal: 'SIGKILL' as const } : {}) });
135
+ return { rc: run.status, stdout: run.stdout ?? '', stderr: run.stderr ?? '', ...(run.error ? { error: String(run.error) } : {}) };
136
+ },
137
+ ssh(host, script, timeoutMs) {
138
+ const run = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host, 'sh', '-s'], {
139
+ input: script, cwd: '/tmp', maxBuffer: 256 * 1024 * 1024, timeout: timeoutMs, killSignal: 'SIGKILL',
140
+ });
141
+ return { rc: run.status, stdout: run.stdout ?? Buffer.alloc(0), stderr: run.stderr ?? Buffer.alloc(0), ...(run.error ? { error: String(run.error) } : {}) };
142
+ },
143
+ };
144
+
145
+ const quote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
146
+ const INFRA = '__GATE_REMOTE_INFRA=';
147
+ const RC = '__GATE_REMOTE_RC=';
148
+ export const PUSH_TIMEOUT_MS = 10 * 60_000;
149
+
150
+ export type RemoteRunOutcome =
151
+ | { kind: 'ran'; rc: number; stdout: Buffer; stderr: Buffer; commit: string; host: string; ms: number }
152
+ | { kind: 'busy' }
153
+ | { kind: 'infra'; reason: string };
154
+
155
+ export interface RemoteRunOptions {
156
+ repo: string;
157
+ host: string;
158
+ mirror: string;
159
+ /** argv run from the remote checkout root, e.g. ['bun', 'bin/elanous.mjs', 'self', 'gate', ...]; a function receives
160
+ * `sha(ref)` for each of `refs` so the remote command names the exact commit, not a ref the host may not have. */
161
+ argv: RemoteArgv;
162
+ hostCap?: number;
163
+ /** Host-side slot lock directory (default SLOT_DIR) — tests point it at a scratch dir. */
164
+ slotDir?: string;
165
+ /** Extra local refs whose commits must exist remotely (e.g. a --base ref). Resolved to shas. */
166
+ refs?: string[];
167
+ installPwa?: boolean;
168
+ timeoutMs?: number;
169
+ /** Files the remote run needs from this machine (e.g. the private export redaction table): sent over ssh stdin, written
170
+ * mode 600 inside the scratch directory, exposed to the tool via `env`, removed with it. Never on argv. */
171
+ payloadFiles?: PayloadFile[];
172
+ }
173
+
174
+ export interface PayloadFile { env: string; content: string }
175
+ export type RemoteArgv = string[] | ((sha: (ref: string) => string) => string[]);
176
+
177
+ /** Build the remote shell script (exported for tests). Every setup failure ends in an INFRA marker, the tool's rc in an RC marker. */
178
+ export function remoteScript(input: { mirror: string; commit: string; fetchShas: string[]; mainSha?: string; argv: string[]; installPwa: boolean; payloadFiles?: PayloadFile[]; hostCap?: number; slotDir?: string }): string {
179
+ const mirror = input.mirror.startsWith('~/') ? `"$HOME"/${quote(input.mirror.slice(2))}` : quote(input.mirror);
180
+ const fetches = input.fetchShas.map((sha) => `refs/elanous/gate/${sha}:refs/elanous/gate/${sha}`).join(' ');
181
+ const payloads = (input.payloadFiles ?? []).filter((file) => /^[A-Z_][A-Z0-9_]*$/.test(file.env));
182
+ const payloadLines = payloads.flatMap((file, i) => {
183
+ const delimiter = `__GATE_REMOTE_PAYLOAD_${i}__`;
184
+ if (file.content.split('\n').includes(delimiter)) throw new Error('payload contains its delimiter');
185
+ return [`cat > "$W/payload-${i}" <<'${delimiter}' || infra payload`, file.content.replace(/\n$/, ''), delimiter];
186
+ });
187
+ const payloadEnv = payloads.map((file, i) => `${file.env}="$W/payload-${i}" `).join('');
188
+ // One brace group: sh parses the whole script off stdin before running anything, so no child can swallow the rest of it.
189
+ // The work itself is written to "$W/run.sh" and runs under a host slot lock.
190
+ const RUN = '__GATE_REMOTE_RUN__';
191
+ const work = [
192
+ `infra() { echo "${INFRA}$1" >&2; exit 97; }`,
193
+ ': > "$W/started"',
194
+ `git clone -q --shared --no-checkout ${mirror} "$W/t" >"$W/setup.log" 2>&1 || infra clone`,
195
+ 'cd "$W/t" || infra cd',
196
+ `git fetch -q origin ${fetches} >>"$W/setup.log" 2>&1 || infra fetch`,
197
+ `git checkout -q --detach ${input.commit} >>"$W/setup.log" 2>&1 || infra checkout`,
198
+ ...(input.mainSha ? [`git update-ref refs/remotes/origin/main ${input.mainSha} || infra origin-main`] : []),
199
+ 'bun install >>"$W/setup.log" 2>&1 || infra bun-install',
200
+ ...(input.installPwa ? ['(cd apps/pwa && bun install) >>"$W/setup.log" 2>&1 || infra bun-install-pwa'] : []),
201
+ // A scratch universe: the remote run never touches the host's operational ~/.elanous state.
202
+ 'mkdir -p "$W/state" || infra scratch',
203
+ `${payloadEnv}ELANOUS_STATE_DIR="$W/state" ELANOUS_GATE_REMOTE_CHILD=1 ${input.argv.map(quote).join(' ')} </dev/null`,
204
+ // The marker always starts on its own line, whatever the tool's stderr ended with; the parser strips that newline.
205
+ `rc=$?; printf '\\n%s%s\\n' '${RC}' "$rc" >&2`,
206
+ 'exit 0',
207
+ ];
208
+ // Any argument line equal to the delimiter would end run.sh early and run the rest outside the slot lock.
209
+ if (work.join('\n').split('\n').includes(RUN)) throw new Error('run script contains its delimiter');
210
+ return [
211
+ '{',
212
+ 'umask 077',
213
+ 'PATH="$HOME/.bun/bin:/opt/homebrew/bin:$PATH"; export PATH',
214
+ `infra() { echo "${INFRA}$1" >&2; exit 97; }`,
215
+ 'W=$(mktemp -d /tmp/gate-remote-XXXXXXXX) || infra mktemp',
216
+ 'export W',
217
+ 'trap \'rm -rf "$W"\' EXIT',
218
+ ...payloadLines,
219
+ `cat > "$W/run.sh" <<'${RUN}' || infra run-script`,
220
+ ...work,
221
+ RUN,
222
+ ...hostSlotScript(input.hostCap ?? GATE_REMOTE_DEFAULTS.hostCap, input.slotDir),
223
+ '}',
224
+ '',
225
+ ].join('\n');
226
+ }
227
+
228
+ function revParse(runner: GateRemoteRunner, repo: string, ref: string): string | undefined {
229
+ const r = runner.local('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], repo);
230
+ const sha = r.stdout.trim();
231
+ return r.rc === 0 && shaPattern.test(sha) ? sha : undefined;
232
+ }
233
+
234
+ export function runOnRemote(opts: RemoteRunOptions, runner: GateRemoteRunner = realGateRemoteRunner): RemoteRunOutcome {
235
+ const started = Date.now();
236
+ const status = runner.local('git', ['status', '--porcelain'], opts.repo);
237
+ if (status.rc !== 0) return { kind: 'infra', reason: 'git-status-failed' };
238
+ // A remote checkout can only reproduce a commit — uncommitted or untracked files would silently not be measured.
239
+ if (status.stdout.trim()) return { kind: 'infra', reason: 'dirty-tree' };
240
+ const commit = revParse(runner, opts.repo, 'HEAD');
241
+ if (!commit) return { kind: 'infra', reason: 'head-unresolved' };
242
+ // Every check measures against merge-base(origin/main); without the local one the host would fall back to its mirror's
243
+ // (possibly stale) branch — not the same measurement.
244
+ const mainSha = revParse(runner, opts.repo, 'origin/main');
245
+ if (!mainSha) return { kind: 'infra', reason: 'origin-main-unresolved' };
246
+ const resolved = new Map<string, string>();
247
+ for (const ref of opts.refs ?? []) {
248
+ const sha = revParse(runner, opts.repo, ref);
249
+ if (!sha) return { kind: 'infra', reason: `ref-unresolved:${ref}` };
250
+ resolved.set(ref, sha);
251
+ }
252
+ const extra = [...resolved.values()];
253
+ let argv: string[];
254
+ try {
255
+ argv = typeof opts.argv === 'function' ? opts.argv((ref) => {
256
+ const sha = resolved.get(ref);
257
+ if (!sha) throw new Error(`ref not declared: ${ref}`);
258
+ return sha;
259
+ }) : opts.argv;
260
+ } catch (error) { return { kind: 'infra', reason: `argv: ${error instanceof Error ? error.message : String(error)}` }; }
261
+ const shas = [...new Set([commit, mainSha, ...extra])];
262
+ const pushTarget = `${opts.host}:${opts.mirror.startsWith('~/') ? opts.mirror.slice(2) : opts.mirror}`;
263
+ // Non-interactive and time-bounded: a stuck connection or auth prompt must end in a local fallback, not a hang.
264
+ const push = runner.local('git', ['push', '--quiet', pushTarget, ...shas.map((sha) => `${sha}:refs/elanous/gate/${sha}`)], opts.repo, {
265
+ timeoutMs: PUSH_TIMEOUT_MS,
266
+ env: { ...process.env, GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o ConnectTimeout=10', GIT_TERMINAL_PROMPT: '0' },
267
+ });
268
+ if (push.rc !== 0 || push.error) return { kind: 'infra', reason: `push-failed rc=${push.rc}: ${(push.stderr || push.error || '').trim().split('\n').slice(-2).join(' | ')}` };
269
+ const script = remoteScript({ mirror: opts.mirror, commit, fetchShas: shas, mainSha, argv, installPwa: opts.installPwa ?? false, payloadFiles: opts.payloadFiles, hostCap: opts.hostCap, slotDir: opts.slotDir });
270
+ const run = runner.ssh(opts.host, script, opts.timeoutMs ?? 120 * 60_000);
271
+ // The script prints "\n<marker><rc>\n": everything before that leading newline is the tool's own stderr, byte for byte.
272
+ const stdoutBytes = Buffer.from(run.stdout);
273
+ const stderrBytes = Buffer.from(run.stderr);
274
+ const stderrText = stderrBytes.toString('utf8');
275
+ const marker = stderrBytes.lastIndexOf(Buffer.from(`\n${RC}`));
276
+ // Busy (98) and setup failure (97) are the script's own exits, each with its marker as the last stderr line. A completed
277
+ // run exits 0 after the rc marker, and its stderr belongs to the tool — never scanned for markers.
278
+ if (!(run.rc === 0 && marker >= 0)) {
279
+ const last = stderrText.trimEnd().split('\n').at(-1) ?? '';
280
+ if (run.rc === 98 && last === BUSY) return { kind: 'busy' };
281
+ if (run.rc === 97 && last.startsWith(INFRA)) return { kind: 'infra', reason: `remote-${last.slice(INFRA.length).trim()}` };
282
+ }
283
+ // No marker, or ssh did not end cleanly after it (the script always exits 0 once the marker is written): output may be cut.
284
+ if (marker < 0) return { kind: 'infra', reason: `ssh rc=${run.rc}${run.error ? ` ${run.error}` : ''}: ${stderrText.trim().split('\n').slice(-2).join(' | ')}` };
285
+ if (run.rc !== 0 || run.error) return { kind: 'infra', reason: `ssh-incomplete rc=${run.rc}${run.error ? ` ${run.error}` : ''}` };
286
+ // The marker is the last thing the script writes: exactly "\n<marker><digits>\n". Anything else (empty, cut) is infra.
287
+ const tail = /^\n__GATE_REMOTE_RC=(\d{1,3})\n$/.exec(stderrBytes.subarray(marker).toString('utf8'));
288
+ const rc = tail ? Number(tail[1]) : NaN;
289
+ if (!(rc >= 0 && rc <= 255)) return { kind: 'infra', reason: 'rc-unparsed' };
290
+ // The tool's stderr is exactly what came before the marker line.
291
+ return { kind: 'ran', rc, stdout: stdoutBytes, stderr: stderrBytes.subarray(0, marker), commit, host: opts.host, ms: Date.now() - started };
292
+ }
293
+
294
+ /** An absolute argument outside the repo names a file the remote checkout does not have — such a run is not reproducible. */
295
+ export function outsideRepoArgs(args: readonly string[], repo: string): string[] {
296
+ // The tool resolves file arguments against the repository root; anything that lands outside it (absolute or ../) is not
297
+ // in the remote checkout.
298
+ return args.filter((arg) => {
299
+ if (arg.startsWith('-')) return false;
300
+ const rel = relative(repo, resolve(repo, arg));
301
+ return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
302
+ });
303
+ }
304
+
305
+ /** File arguments the remote checkout cannot reproduce: outside the repo (by string or after symlinks resolve), or not
306
+ * tracked at HEAD (ignored/untracked files are not in the pushed commit). `tracked` defaults to `git ls-files`. */
307
+ export function unreproducibleArgs(paths: readonly string[], repo: string, tracked: (path: string) => boolean = (path) =>
308
+ spawnSync('git', ['ls-files', '--error-unmatch', '--', path], { cwd: repo, stdio: 'ignore', maxBuffer: 1024 * 1024 }).status === 0): string[] {
309
+ let realRepo = repo;
310
+ try { realRepo = realpathSync(repo); } catch { /* keep as given */ }
311
+ return paths.filter((arg) => {
312
+ if (outsideRepoArgs([arg], repo).length) return true;
313
+ const abs = resolve(repo, arg);
314
+ if (!tracked(relative(repo, abs))) return true;
315
+ // Conservative: any symlink on the way (the file itself or a directory above it, inside the repo) means local —
316
+ // a link's target may be untracked, outside, looped or dangling, and none of that is worth reproducing remotely.
317
+ const parts = relative(repo, abs).split(sep);
318
+ let walk = realRepo;
319
+ for (const part of parts) {
320
+ walk = join(walk, part);
321
+ try { if (lstatSync(walk).isSymbolicLink()) return true; } catch { return false; } // missing locally: tracked() already answered
322
+ }
323
+ return false;
324
+ });
325
+ }
326
+
327
+ /** Rewrite absolute paths under the repo to repo-relative so they mean the same file in the remote checkout. */
328
+ export function relativizeArgs(args: readonly string[], repo: string): string[] {
329
+ return args.map((arg) => {
330
+ if (!isAbsolute(arg)) return arg;
331
+ const rel = relative(repo, arg);
332
+ return rel && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ? rel : arg;
333
+ });
334
+ }
335
+
336
+ /** For a JSON object stdout: add `host` and `remote: true` by splicing them in before the closing brace — every other byte
337
+ * of the tool's output (spacing, escapes, key order, trailing newline) is kept. Anything else passes through unchanged. */
338
+ export function annotateJsonStdout(stdout: string, host: string): string {
339
+ const trimmed = stdout.trim();
340
+ if (!trimmed.startsWith('{')) return stdout;
341
+ try {
342
+ const parsed = JSON.parse(trimmed) as unknown;
343
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return stdout;
344
+ } catch { return stdout; }
345
+ const close = stdout.lastIndexOf('}');
346
+ const empty = Object.keys(JSON.parse(trimmed) as object).length === 0;
347
+ return `${stdout.slice(0, close)}${empty ? '' : ','}"host":${JSON.stringify(host)},"remote":true${stdout.slice(close)}`;
348
+ }
349
+
350
+ export interface DispatchOptions {
351
+ tool: GateRemoteTool;
352
+ repo: string;
353
+ flags: Pick<GateRemoteFlags, 'remote' | 'local'>;
354
+ /** argv for the remote run (the tool re-invoked with `--local` so it never recurses). */
355
+ remoteArgv: RemoteArgv;
356
+ refs?: string[];
357
+ installPwa?: boolean;
358
+ payloadFiles?: PayloadFile[];
359
+ json?: boolean;
360
+ /** Set when this invocation cannot be reproduced remotely (e.g. self gate without --base): a remote decision becomes a
361
+ * logged fallback with this reason instead of a silent local run. */
362
+ localOnlyReason?: string;
363
+ runLocal: () => number | Promise<number>;
364
+ }
365
+
366
+ export interface DispatchDeps {
367
+ runner?: GateRemoteRunner;
368
+ settings?: GateRemoteSettings;
369
+ load1?: number;
370
+ env?: NodeJS.ProcessEnv;
371
+ sleep?: (ms: number) => Promise<void>;
372
+ now?: () => number;
373
+ write?: { out: (data: string | Uint8Array) => void; err: (data: string | Uint8Array) => void };
374
+ }
375
+
376
+ async function loadSettings(): Promise<GateRemoteSettings> {
377
+ try {
378
+ const { getUserConfig } = await import('../user-config.js');
379
+ return resolveGateRemoteSettings(getUserConfig().gateRemote);
380
+ } catch { return resolveGateRemoteSettings(undefined); }
381
+ }
382
+
383
+ /** Decide local/remote, run, and return the tool's exit code. Infra failure ⇒ local fallback (never a test result). */
384
+ export async function dispatchHeavyCheck(opts: DispatchOptions, deps: DispatchDeps = {}): Promise<number> {
385
+ const settings = deps.settings ?? await loadSettings();
386
+ const env = deps.env ?? process.env;
387
+ const write = deps.write ?? { out: (d: string | Uint8Array) => process.stdout.write(d), err: (d: string | Uint8Array) => process.stderr.write(d) };
388
+ const decision = decideGateDispatch({ flags: opts.flags, settings, load1: deps.load1 ?? loadavg()[0]!, env });
389
+ debug.log('gate.remote', 'decision', { tool: opts.tool, mode: decision.mode, host: decision.host, reason: decision.reason, load1: decision.load1 });
390
+ if (decision.mode === 'local') return opts.runLocal();
391
+ const host = decision.host!;
392
+ const fallback = (reason: string) => {
393
+ debug.log('gate.remote', 'fallback', { tool: opts.tool, host, reason });
394
+ write.err(`[gate.remote] fallback to local — ${reason}\n`);
395
+ return opts.runLocal();
396
+ };
397
+ if (opts.localOnlyReason) return fallback(opts.localOnlyReason);
398
+ const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
399
+ const now = deps.now ?? Date.now;
400
+ const deadline = now() + settings.slotWaitSeconds * 1000;
401
+ debug.log('gate.remote', 'dispatch', { tool: opts.tool, host, reason: decision.reason });
402
+ let outcome: RemoteRunOutcome;
403
+ for (;;) {
404
+ try {
405
+ outcome = runOnRemote({ repo: opts.repo, host, mirror: settings.mirror, argv: opts.remoteArgv, refs: opts.refs, installPwa: opts.installPwa, payloadFiles: opts.payloadFiles, hostCap: settings.hostCap }, deps.runner);
406
+ } catch (error) {
407
+ outcome = { kind: 'infra', reason: `exception: ${error instanceof Error ? error.message : String(error)}` };
408
+ }
409
+ if (outcome.kind !== 'busy') break;
410
+ debug.log('gate.remote', 'host-busy', { tool: opts.tool, host, cap: settings.hostCap });
411
+ // Never start an attempt past the deadline: an expired wait means local, even if a slot would free a moment later.
412
+ const left = deadline - now();
413
+ if (left <= 0) break;
414
+ await sleep(Math.min(15_000, left));
415
+ if (now() >= deadline) break;
416
+ }
417
+ if (outcome.kind === 'busy') return fallback(`host-busy (cap ${settings.hostCap} held for ${settings.slotWaitSeconds}s)`);
418
+ if (outcome.kind === 'infra') return fallback(outcome.reason);
419
+ // Parity: the tool's own stdout/stderr/rc pass through unchanged — only a JSON stdout gains host · remote:true.
420
+ debug.log('gate.remote', 'result', { tool: opts.tool, host: outcome.host, commit: outcome.commit, rc: outcome.rc, ms: outcome.ms });
421
+ // Bytes pass through untouched; only a JSON object stdout is re-serialised (to add host · remote:true).
422
+ const text = opts.json ? outcome.stdout.toString('utf8') : undefined;
423
+ const annotated = text !== undefined ? annotateJsonStdout(text, outcome.host) : undefined;
424
+ write.out(annotated !== undefined && annotated !== text ? annotated : outcome.stdout);
425
+ write.err(outcome.stderr);
426
+ return outcome.rc;
427
+ }