elanous 0.2.13 → 0.2.15

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 (524) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  10. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  100. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  106. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  107. package/apps/pwa/out/approvals/index.html +1 -1
  108. package/apps/pwa/out/approvals/index.txt +23 -23
  109. package/apps/pwa/out/autopilot/index.html +1 -1
  110. package/apps/pwa/out/autopilot/index.txt +23 -23
  111. package/apps/pwa/out/board/index.html +1 -1
  112. package/apps/pwa/out/board/index.txt +23 -23
  113. package/apps/pwa/out/botlab/index.html +1 -1
  114. package/apps/pwa/out/botlab/index.txt +22 -22
  115. package/apps/pwa/out/bots/index.html +1 -1
  116. package/apps/pwa/out/bots/index.txt +22 -22
  117. package/apps/pwa/out/ceo/index.html +1 -0
  118. package/apps/pwa/out/ceo/index.txt +33 -0
  119. package/apps/pwa/out/chat/index.html +1 -1
  120. package/apps/pwa/out/chat/index.txt +22 -22
  121. package/apps/pwa/out/consult/index.html +1 -1
  122. package/apps/pwa/out/consult/index.txt +23 -23
  123. package/apps/pwa/out/control/index.html +1 -1
  124. package/apps/pwa/out/control/index.txt +23 -23
  125. package/apps/pwa/out/design-check/index.html +1 -1
  126. package/apps/pwa/out/design-check/index.txt +23 -23
  127. package/apps/pwa/out/editor/index.html +1 -1
  128. package/apps/pwa/out/editor/index.txt +23 -23
  129. package/apps/pwa/out/exec/index.html +1 -1
  130. package/apps/pwa/out/exec/index.txt +23 -23
  131. package/apps/pwa/out/field/index.html +1 -1
  132. package/apps/pwa/out/field/index.txt +23 -23
  133. package/apps/pwa/out/healthz +1 -1
  134. package/apps/pwa/out/index.html +1 -1
  135. package/apps/pwa/out/index.txt +22 -22
  136. package/apps/pwa/out/inside/index.html +1 -1
  137. package/apps/pwa/out/inside/index.txt +22 -22
  138. package/apps/pwa/out/intake/index.html +1 -1
  139. package/apps/pwa/out/intake/index.txt +23 -23
  140. package/apps/pwa/out/live/index.html +1 -1
  141. package/apps/pwa/out/live/index.txt +23 -23
  142. package/apps/pwa/out/loops/index.html +1 -0
  143. package/apps/pwa/out/loops/index.txt +30 -0
  144. package/apps/pwa/out/market/index.html +1 -1
  145. package/apps/pwa/out/market/index.txt +22 -22
  146. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  147. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  148. package/apps/pwa/out/morning/index.html +1 -1
  149. package/apps/pwa/out/morning/index.txt +23 -23
  150. package/apps/pwa/out/observatory/index.html +1 -1
  151. package/apps/pwa/out/observatory/index.txt +23 -23
  152. package/apps/pwa/out/ops/checklist/index.html +1 -1
  153. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  154. package/apps/pwa/out/ops/release/index.html +1 -1
  155. package/apps/pwa/out/ops/release/index.txt +22 -22
  156. package/apps/pwa/out/ops/seats/index.html +1 -1
  157. package/apps/pwa/out/ops/seats/index.txt +22 -22
  158. package/apps/pwa/out/outputs/index.html +1 -1
  159. package/apps/pwa/out/outputs/index.txt +22 -22
  160. package/apps/pwa/out/reflection/index.html +1 -1
  161. package/apps/pwa/out/reflection/index.txt +23 -23
  162. package/apps/pwa/out/scheduler/index.html +1 -1
  163. package/apps/pwa/out/scheduler/index.txt +23 -23
  164. package/apps/pwa/out/sessions/index.html +1 -1
  165. package/apps/pwa/out/sessions/index.txt +22 -22
  166. package/apps/pwa/out/settings/devices/index.html +1 -1
  167. package/apps/pwa/out/settings/devices/index.txt +23 -23
  168. package/apps/pwa/out/settings/index.html +1 -1
  169. package/apps/pwa/out/settings/index.txt +23 -23
  170. package/apps/pwa/out/setup/done/index.html +1 -1
  171. package/apps/pwa/out/setup/done/index.txt +23 -23
  172. package/apps/pwa/out/setup/index.html +1 -1
  173. package/apps/pwa/out/setup/index.txt +23 -23
  174. package/apps/pwa/out/share/index.html +1 -1
  175. package/apps/pwa/out/share/index.txt +23 -23
  176. package/apps/pwa/out/showroom/index.html +1 -1
  177. package/apps/pwa/out/showroom/index.txt +22 -22
  178. package/apps/pwa/out/tasks/index.html +1 -1
  179. package/apps/pwa/out/tasks/index.txt +23 -23
  180. package/apps/pwa/out/term/index.html +1 -1
  181. package/apps/pwa/out/term/index.txt +23 -23
  182. package/apps/pwa/out/today/index.html +1 -1
  183. package/apps/pwa/out/today/index.txt +23 -23
  184. package/apps/pwa/out/trace/index.html +1 -1
  185. package/apps/pwa/out/trace/index.txt +23 -23
  186. package/apps/pwa/out/vault/index.html +1 -1
  187. package/apps/pwa/out/vault/index.txt +23 -23
  188. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  189. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  190. package/apps/pwa/out/workflows/index.html +1 -1
  191. package/apps/pwa/out/workflows/index.txt +23 -23
  192. package/apps/pwa/out/workspace/index.html +1 -1
  193. package/apps/pwa/out/workspace/index.txt +23 -23
  194. package/apps/pwa/out/worktrees/index.html +1 -1
  195. package/apps/pwa/out/worktrees/index.txt +23 -23
  196. package/graphs/heal/heal-loop.yaml +1 -1
  197. package/graphs/implement-loop.yaml +1 -1
  198. package/graphs/orchestrator/orchestrator.yaml +59 -0
  199. package/graphs/orchestrator/recipes.yaml +18 -0
  200. package/graphs/release/recipes.yaml +6 -0
  201. package/graphs/release/release-loop.yaml +6 -1
  202. package/graphs/rhythm/daily.yaml +38 -0
  203. package/graphs/rhythm/recipes.yaml +18 -0
  204. package/graphs/rhythm/weekly.yaml +39 -0
  205. package/graphs/video/recipes.yaml +3 -0
  206. package/graphs/video/youtube-summary.yaml +17 -0
  207. package/package.json +1 -1
  208. package/scripts/audit-test-state-writes.ts +72 -17
  209. package/scripts/brand/check.ts +188 -0
  210. package/scripts/ci-typecheck-changed.ts +50 -18
  211. package/scripts/coord-post.sh +11 -1
  212. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  213. package/scripts/docs-cli-check.ts +23 -6
  214. package/scripts/expose-rubric-check.ts +118 -0
  215. package/scripts/expose-rubric-suggest.ts +169 -0
  216. package/scripts/heal/run-role.ts +14 -0
  217. package/scripts/hq/standby-verify.ts +11 -7
  218. package/scripts/lecture-note/node.ts +31 -11
  219. package/scripts/lessons/handbook-page.ts +112 -0
  220. package/scripts/marketing/generate-ax-brief.ts +140 -0
  221. package/scripts/marketing/generate-feature-map.ts +118 -0
  222. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  223. package/scripts/release-loop/gate-node.ts +89 -14
  224. package/scripts/release-loop/publish-node.ts +14 -1
  225. package/scripts/release-loop/unattended-release.ts +23 -1
  226. package/scripts/release-loop/vault-note-node.ts +428 -0
  227. package/scripts/release-story/draft.ts +220 -0
  228. package/scripts/rhythm/daily.ts +570 -0
  229. package/scripts/rhythm/weekly.ts +315 -0
  230. package/scripts/skills/public-check.ts +169 -0
  231. package/scripts/smart-merge/resync-shadow.ts +147 -0
  232. package/scripts/test-deterministic.ts +4 -8
  233. package/scripts/test-diet/effectiveness.ts +195 -0
  234. package/scripts/test-diet/lib.ts +16 -8
  235. package/scripts/test-diet/node.ts +77 -9
  236. package/scripts/ux/demo-rehearsal.ts +2 -2
  237. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  238. package/scripts/video/youtube-summary.ts +133 -0
  239. package/skills/diagram-master/SKILL.md +42 -49
  240. package/skills/diagram-master/references/pyproject.toml +4 -1
  241. package/skills/diagram-master/references/routing-guide.md +4 -2
  242. package/skills/grill-me/SKILL.md +1 -0
  243. package/skills/youtube-master/SKILL.md +13 -13
  244. package/skills/youtube-master/package-lock.json +0 -545
  245. package/skills/youtube-master/package.json +2 -5
  246. package/skills/youtube-master/scripts/main.ts +2 -2
  247. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  248. package/skills/youtube-master/src/util.ts +30 -7
  249. package/src/agent-env/profile.ts +161 -0
  250. package/src/agent-mission/driver.ts +3 -3
  251. package/src/agent-substrate/pr-reviewer.ts +2 -0
  252. package/src/ask-user-question/pending-questions.ts +5 -0
  253. package/src/auth/github-app-token.ts +112 -13
  254. package/src/autopilot/build/llm-conflict-merge.ts +73 -10
  255. package/src/autopilot/build/merge-intent.ts +53 -0
  256. package/src/briefing/brief-items.ts +121 -0
  257. package/src/chat/index.ts +2 -0
  258. package/src/claims/claims-ledger.ts +232 -0
  259. package/src/claims/claims-render.ts +166 -0
  260. package/src/cli/agent-env-cli.ts +21 -0
  261. package/src/cli/claims-cli.ts +94 -0
  262. package/src/cli/decisions-cli.ts +44 -3
  263. package/src/cli/docs-cli.ts +2 -0
  264. package/src/cli/doctor-cli.ts +23 -1
  265. package/src/cli/doctor-node-modules.ts +147 -0
  266. package/src/cli/flow-cli.ts +11 -0
  267. package/src/cli/freeze-cli.ts +45 -0
  268. package/src/cli/intake-cli.ts +46 -17
  269. package/src/cli/lesson-cli.ts +108 -0
  270. package/src/cli/logs-cli.ts +104 -2
  271. package/src/cli/logs-model-input.ts +131 -0
  272. package/src/cli/pod-cli.ts +2 -0
  273. package/src/cli/pr-cli.ts +29 -1
  274. package/src/cli/private-ledger-commands.ts +54 -4
  275. package/src/cli/pty-drive-verdict.ts +11 -2
  276. package/src/cli/release-cli.ts +190 -33
  277. package/src/cli/rfc-status.ts +91 -0
  278. package/src/cli/seat-requests-cli.ts +52 -0
  279. package/src/connectors/linear.ts +5 -3
  280. package/src/context-bus/condense.ts +15 -20
  281. package/src/context-bus/context-now-public.ts +96 -0
  282. package/src/context-bus/context-now-surfaces.ts +55 -2
  283. package/src/context-bus/context-now.ts +6 -3
  284. package/src/context-bus/coord-events.ts +6 -4
  285. package/src/context-bus/long-term-memory.ts +92 -11
  286. package/src/dashboard/index.ts +75 -22
  287. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  288. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  289. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  290. package/src/decisions/decision-cards.ts +33 -7
  291. package/src/decisions/decision-ledger.ts +40 -1
  292. package/src/decisions/proact-meter.ts +286 -0
  293. package/src/decisions/proact1-lite.ts +86 -0
  294. package/src/decisions/web-push-decision-cards.ts +1 -1
  295. package/src/discord/sprint21-runtime.ts +16 -0
  296. package/src/discord-self-message.ts +8 -0
  297. package/src/discord-slash-wire.ts +31 -2
  298. package/src/domains/alert-audit.ts +271 -0
  299. package/src/domains/core-tools.ts +4 -3
  300. package/src/domains/ops-facts-tool.ts +31 -1
  301. package/src/domains/outbound-alert.ts +1 -1
  302. package/src/elanous-config-dir.ts +29 -4
  303. package/src/execution-loop/launch-gate.ts +29 -14
  304. package/src/flow/cards.ts +239 -0
  305. package/src/graph-runner/graph-tick.ts +4 -0
  306. package/src/graph-runner/runner.ts +30 -5
  307. package/src/harness/harness-cli-command.ts +259 -38
  308. package/src/harness/harness-incidents.ts +45 -12
  309. package/src/harness/harness-pod-dispatch.ts +102 -11
  310. package/src/harness/harness-queue-child.ts +3 -3
  311. package/src/harness/harness-queue.ts +583 -65
  312. package/src/harness/helper-repair.ts +87 -0
  313. package/src/harness/helper-scan.ts +151 -0
  314. package/src/harness/launch-stamp.ts +236 -0
  315. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  316. package/src/harness/ledger-write-guard.ts +35 -0
  317. package/src/harness/model-input-observation.ts +37 -0
  318. package/src/harness/pod-memory-policy.ts +15 -0
  319. package/src/harness/repository-name.ts +55 -0
  320. package/src/hq/fence-audit.ts +90 -0
  321. package/src/hq/hq.ts +63 -5
  322. package/src/hq/lease.ts +4 -4
  323. package/src/hq/move-plan.ts +126 -0
  324. package/src/hq/promote.ts +332 -0
  325. package/src/index.ts +159 -27
  326. package/src/intake-plane/check.ts +63 -0
  327. package/src/intake-plane/digest.ts +37 -15
  328. package/src/intake-plane/front-classifier.ts +6 -0
  329. package/src/intake-plane/items.ts +3 -2
  330. package/src/intake-plane/lens.ts +177 -0
  331. package/src/intake-plane/linear-wish.ts +39 -11
  332. package/src/intake-plane/route.ts +145 -1
  333. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  334. package/src/intake-plane/wish-card.ts +53 -2
  335. package/src/intake-plane/wish-folder.ts +5 -2
  336. package/src/intake-plane/wish-reply.ts +72 -0
  337. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  338. package/src/launch-head/seat-goal-queue.ts +198 -0
  339. package/src/lessons/lesson-import.ts +106 -0
  340. package/src/lessons/lesson-ledger.ts +206 -0
  341. package/src/loops/activity.ts +203 -0
  342. package/src/loops/budget.ts +63 -0
  343. package/src/loops/checker.ts +5 -3
  344. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  345. package/src/loops/loop-cli.ts +36 -4
  346. package/src/loops/neighbors.ts +40 -0
  347. package/src/loops/observe.ts +10 -0
  348. package/src/loops/orchestrator/author-depth.ts +220 -0
  349. package/src/loops/orchestrator/author-input-check.ts +82 -0
  350. package/src/loops/orchestrator/author-ledger.ts +232 -0
  351. package/src/loops/orchestrator/finish-rate.ts +175 -0
  352. package/src/loops/orchestrator/overlap.ts +51 -12
  353. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  354. package/src/loops/orchestrator/seat-share.ts +56 -0
  355. package/src/loops/orchestrator/tick.ts +309 -0
  356. package/src/loops/orchestrator/traffic.ts +134 -13
  357. package/src/loops/registry.ts +535 -9
  358. package/src/loops/status-rows.ts +118 -0
  359. package/src/loops/verdict.ts +17 -5
  360. package/src/maturity/bot-slash-catalog.ts +66 -0
  361. package/src/maturity/feature-maturity.ts +9 -5
  362. package/src/maturity/tui-slash-availability.ts +31 -0
  363. package/src/mss/logging/log-store.ts +35 -4
  364. package/src/nexus/api/dashboard.ts +6 -1
  365. package/src/nexus/api/http-server.ts +33 -3
  366. package/src/nexus/api/loop-edges.ts +173 -0
  367. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  368. package/src/nexus/api/schedules-read.ts +18 -1
  369. package/src/nexus/api/seat-requests.ts +79 -56
  370. package/src/nexus/api/task-cards-api.ts +44 -5
  371. package/src/nexus/api/workflows.ts +2 -1
  372. package/src/prompt-enhance/enhance.ts +6 -1
  373. package/src/release-loop/auto-start.ts +51 -0
  374. package/src/release-loop/checklist.ts +22 -4
  375. package/src/release-loop/feature-store.ts +71 -32
  376. package/src/release-loop/landing-freeze.ts +121 -0
  377. package/src/release-loop/merged-pr-checklist.ts +125 -0
  378. package/src/release-loop/placement.ts +302 -0
  379. package/src/release-loop/release-schedule.ts +3 -1
  380. package/src/seat-dispatch/ceo-commands.ts +2 -2
  381. package/src/seat-dispatch/seat-ask.ts +16 -3
  382. package/src/seat-dispatch/seat-questions.ts +58 -0
  383. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  384. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  385. package/src/seat-loop/seat-loop.ts +531 -40
  386. package/src/self-dev/dev-pipeline.ts +14 -6
  387. package/src/self-dev/draft-sweep.ts +162 -6
  388. package/src/self-dev/draft-triage-rules.ts +25 -8
  389. package/src/self-dev/orchestrate.ts +15 -1
  390. package/src/self-dev/review-provider-fallback.ts +6 -1
  391. package/src/self-dev/run-store.ts +73 -1
  392. package/src/self-dev/run-supervisor.ts +57 -2
  393. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  394. package/src/self-implement/budget-gate.ts +29 -0
  395. package/src/self-implement/expose-gate.ts +47 -0
  396. package/src/self-implement/frozen-merges.ts +295 -0
  397. package/src/self-implement/gate-cli.ts +10 -4
  398. package/src/self-implement/gate-scope.ts +46 -9
  399. package/src/self-implement/goal-archive.ts +222 -0
  400. package/src/self-implement/goal-author.ts +43 -5
  401. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  402. package/src/self-implement/goal-lookup.ts +147 -0
  403. package/src/self-implement/heal-intake.ts +197 -0
  404. package/src/self-implement/host-regate.ts +32 -2
  405. package/src/self-implement/l8-merge-queue.ts +132 -0
  406. package/src/self-implement/orchestrator.ts +299 -48
  407. package/src/self-implement/pipeline-shape.ts +1 -1
  408. package/src/self-implement/revert-guard.ts +97 -0
  409. package/src/self-implement/run-ledger.ts +3 -1
  410. package/src/self-implement/run-status-mapping.ts +2 -0
  411. package/src/self-implement/seams.ts +29 -9
  412. package/src/self-implement/self-implement-cli.ts +1 -0
  413. package/src/self-implement/speed-claims.ts +338 -0
  414. package/src/session/index.ts +2 -0
  415. package/src/steward/triage.ts +74 -3
  416. package/src/steward/working-backwards.ts +4 -1
  417. package/src/task-cards/card-cli.ts +10 -2
  418. package/src/task-cards/card-store.ts +10 -6
  419. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  420. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  421. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  422. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  424. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
  426. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  427. package/src/telegram-commands.ts +27 -3
  428. package/src/telegram-loops-command.ts +44 -0
  429. package/src/telegram.ts +25 -4
  430. package/src/user-config.ts +146 -18
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/workflow-runtime/nodes/bash.ts +9 -1
  433. package/src/workflow-runtime/storage.ts +31 -13
  434. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  445. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  449. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  523. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  524. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -5,10 +5,19 @@ import type { DeployVerifyFinding } from '../harness/browser-verify.js';
5
5
  import type { SelfDevJobResult } from './orchestrate.js';
6
6
  import { recordSelfDevRunSupervisorStop, selfDevRunsDir } from './run-store.js';
7
7
  import { debug } from '../debug/log.js';
8
+ import { sweepFrozenMerges } from '../self-implement/frozen-merges.js';
8
9
 
9
10
  export interface SupervisorRound {
10
11
  round: number;
11
12
  landed: number;
13
+ /**
14
+ * Pieces that finished (`ok=true`) with at least one new commit against
15
+ * this run's own `--base` — not a main merge. Omitted on rounds that
16
+ * never measured it, so older history stays comparable.
17
+ */
18
+ baseCompleted?: number;
19
+ /** The run's own `--base` those completions were counted against. */
20
+ base?: string;
12
21
  actionable: number;
13
22
  reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'];
14
23
  }
@@ -21,6 +30,7 @@ export type SupervisorStopReason =
21
30
  | 'parent-signals-red'
22
31
  | 'needs-human'
23
32
  | 'no-actionable-work'
33
+ | 'harvestable-awaiting-human'
24
34
  | 'handed-off-to-salvage'
25
35
  | 'max-rounds'
26
36
  | 'no-progress'
@@ -38,6 +48,7 @@ export const SUPERVISOR_STOP_REASONS = [
38
48
  'parent-signals-red',
39
49
  'needs-human',
40
50
  'no-actionable-work',
51
+ 'harvestable-awaiting-human',
41
52
  'handed-off-to-salvage',
42
53
  'max-rounds',
43
54
  'no-progress',
@@ -104,9 +115,25 @@ export function countLanded(results: readonly SelfDevJobResult[]): number {
104
115
  return results.filter(hasDelivered).length;
105
116
  }
106
117
 
118
+ /** One base label for the round. Mixed or missing bases stay unnamed. */
119
+ export function roundBase(results: readonly SelfDevJobResult[]): string | undefined {
120
+ const bases = [...new Set(results.flatMap((result) => result.base === undefined ? [] : [result.base]))];
121
+ return bases.length === 1 ? bases[0] : undefined;
122
+ }
123
+
124
+ /**
125
+ * Finished pieces (`ok=true`) that added at least one commit against this
126
+ * run's own `--base`. A main merge is not required, and a commit without
127
+ * `ok=true` is not completion.
128
+ */
129
+ export function countBaseCompleted(results: readonly SelfDevJobResult[]): number {
130
+ return results.filter((result) => result.ok === true && (result.commitsAheadOfBase ?? 0) >= 1).length;
131
+ }
132
+
107
133
  export function madeProgress(prev: SupervisorRound | undefined, next: SupervisorRound): boolean {
108
134
  if (!prev) return true;
109
135
  if (next.landed > 0) return true;
136
+ if ((next.baseCompleted ?? 0) > (prev.baseCompleted ?? 0)) return true;
110
137
  if (next.actionable < prev.actionable) return true;
111
138
  return next.reviewMustFixTrend === 'improving';
112
139
  }
@@ -190,8 +217,16 @@ export function decideNextRun(input: {
190
217
  const t = triageRun(activeResults, deployFindings, observation === 'failed');
191
218
  const round = history.length;
192
219
  const landed = countLanded(results);
220
+ const baseCompleted = countBaseCompleted(results);
221
+ const completionBase = roundBase(results);
193
222
  const actionable = countActionable(t);
194
- const thisRound: SupervisorRound = { round, landed, actionable, reviewMustFixTrend };
223
+ const thisRound: SupervisorRound = {
224
+ round,
225
+ landed,
226
+ ...(baseCompleted > 0 || completionBase !== undefined ? { baseCompleted, ...(completionBase === undefined ? {} : { base: completionBase }) } : {}),
227
+ actionable,
228
+ reviewMustFixTrend,
229
+ };
195
230
  const goalPlanRevisionObservation = formatGoalPlanRevisionObservation(results);
196
231
  const withGoalPlanRevisionObservation = (why: string): string => goalPlanRevisionObservation
197
232
  && goalPlanRevisionObservation !== GOAL_PLAN_REVISION_OBSERVATION_UNAVAILABLE
@@ -225,6 +260,16 @@ export function decideNextRun(input: {
225
260
  };
226
261
  }
227
262
 
263
+ const harvestable = results.filter((result) => result.harvestable === true && !!result.branch);
264
+ if (harvestable.length > 0) {
265
+ return {
266
+ ...base,
267
+ action: 'stop',
268
+ stopReason: 'harvestable-awaiting-human',
269
+ why: withGoalPlanRevisionObservation(`구현 완료 · 수확 대기 — ${harvestable.map((result) => result.branch).join(' · ')}`),
270
+ };
271
+ }
272
+
228
273
  if (deliverableMergeCompleteness === 'complete') {
229
274
  return {
230
275
  ...base,
@@ -363,7 +408,7 @@ export function decideNextRun(input: {
363
408
  ...base,
364
409
  action: 'stop',
365
410
  stopReason: 'no-progress',
366
- why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 — 착지도 안 늘고 남은 조각도 안 줄었다(${actionable}). 골·불변식을 의심할 자리`),
411
+ why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 — base=${completionBase ?? 'main'} 기준 완료 ${baseCompleted} · 착지 ${landed} · 남은 조각도 안 줄었다(${actionable}). 골·불변식을 의심할 자리`),
367
412
  };
368
413
  }
369
414
  }
@@ -389,9 +434,12 @@ export function appendRound(
389
434
  reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'],
390
435
  ): SupervisorRound[] {
391
436
  const t = triageRun(results.filter((result) => result.salvage !== 'launched'));
437
+ const baseCompleted = countBaseCompleted(results);
438
+ const base = roundBase(results);
392
439
  return [...history, {
393
440
  round: history.length,
394
441
  landed: countLanded(results),
442
+ ...(baseCompleted > 0 || base !== undefined ? { baseCompleted, ...(base === undefined ? {} : { base }) } : {}),
395
443
  actionable: countActionable(t),
396
444
  ...(reviewMustFixTrend === undefined ? {} : { reviewMustFixTrend }),
397
445
  }];
@@ -410,6 +458,7 @@ export interface SuperviseRunOptions {
410
458
  onRound?: (results: readonly SupervisorJobResult[]) => void;
411
459
  runStore?: { runId?: string; operatorDir?: string; dir?: string; isolatedDir?: string };
412
460
  observe?: (event: string, data: Record<string, unknown>) => void;
461
+ sweepPendingMerges?: typeof sweepFrozenMerges;
413
462
  observeDeliverables?: () => Promise<{
414
463
  readonly deployFindings: ReadonlyMap<string, { target: string; findings?: readonly DeployVerifyFinding[] | readonly unknown[] }>;
415
464
  readonly unmeasured: readonly unknown[];
@@ -432,6 +481,12 @@ export async function superviseRun(opts: SuperviseRunOptions): Promise<Superviso
432
481
  let history: SupervisorRound[] = [];
433
482
 
434
483
  for (;;) {
484
+ try {
485
+ const resumed = await (opts.sweepPendingMerges ?? sweepFrozenMerges)();
486
+ if (resumed.pending || resumed.merged) opts.observe?.('frozen-merges', resumed);
487
+ } catch (error) {
488
+ try { debug.log('harness.merge', 'resume-failed', { error: String(error) }); } catch { /* observation must not interrupt supervision */ }
489
+ }
435
490
  if (opts.enrich) results = [...opts.enrich(results)];
436
491
 
437
492
  let observed: {
@@ -13,7 +13,7 @@ import { posix as posixPath } from 'node:path';
13
13
  import type { LLMToolSpec } from '../llm.js';
14
14
  import type { ToolRuntime, ToolRuntimeContext } from '../tool-runtime/types.js';
15
15
  import { orchestrateSelfDev, type SelfDevJobResult, type OrchestrateSelfDevOptions, type SelfDevGoal } from './orchestrate.js';
16
- import type { SelfDevRunParticipant, SelfDevRunState } from './run-store.js';
16
+ import { processBirthId, type SelfDevRunParticipant, type SelfDevRunState } from './run-store.js';
17
17
  import { HARNESS_RUN_ID_ENV, resolveRunIdentity as defaultResolveRunIdentity } from '../harness/harness-space.js';
18
18
  import { decomposeSelfDevGoal, type SelfDevDecomposition, type SelfDevDecomposeOptions } from './decompose.js';
19
19
  import { decomposeFabricRequest, type FabricDecomposeRequestOptions, type FabricDecomposeRequestResult } from './fabric-decompose-adapter.js';
@@ -302,6 +302,7 @@ interface OrchestrateRunLedgerInput {
302
302
  readonly prior: Pick<SelfDevRunState, 'dependencies' | 'results'> | null;
303
303
  readonly goals: readonly SelfDevGoal[];
304
304
  readonly pid: number;
305
+ readonly seat?: SelfDevRunState['seat'];
305
306
  readonly runIdSource: SelfDevRunParticipant['runIdSource'];
306
307
  readonly now: () => number;
307
308
  readonly onPersistenceFailure?: (stage: 'checkpoint' | 'participant', error: unknown) => void;
@@ -421,6 +422,7 @@ export function bindOrchestrateRunLedger(
421
422
  input: OrchestrateRunLedgerInput,
422
423
  ): OrchestrateRunLedgerBinding {
423
424
  const dependencies = deps.checkpointDependencies(input.prior, input.goals);
425
+ const pidStart = processBirthId(input.pid);
424
426
  const checkpoint = (results: SelfDevRunState['results']): void => {
425
427
  try {
426
428
  deps.saveRun({
@@ -431,6 +433,8 @@ export function bindOrchestrateRunLedger(
431
433
  dependencies,
432
434
  goals: [...input.goals],
433
435
  pid: input.pid,
436
+ ...(pidStart ? { pidStart } : {}),
437
+ ...(input.seat ? { seat: input.seat } : {}),
434
438
  });
435
439
  } catch (error) {
436
440
  if (input.onPersistenceFailure) input.onPersistenceFailure('checkpoint', error);
@@ -3,8 +3,10 @@
3
3
 
4
4
  import type { RotationCandidate } from '../oauth/codex-account-rotation.js';
5
5
  import { inspectCodexRotation } from '../oauth/codex-account-store.js';
6
+ import { codexPolicyAllowsCredits } from '../oauth/codex-quota-policy.js';
6
7
  import { getUsageStore } from '../budget/usage-store.js';
7
8
  import type { UsageSnapshot } from '../budget/types.js';
9
+ import { debug } from '../debug/log.js';
8
10
  import {
9
11
  DEFAULT_BUDGET_GATE_MAX_USED_PERCENT,
10
12
  type BudgetGateMaxUsedPercent,
@@ -25,6 +27,9 @@ export interface BudgetCodexCandidate {
25
27
  readonly name: string;
26
28
  readonly reached?: boolean;
27
29
  readonly usedPercent?: number;
30
+ /** 선불 크레딧 잔액(계정에 귀속된 신호만). 모르면 비운다. */
31
+ readonly creditBalance?: number;
32
+ readonly hasCredits?: boolean;
28
33
  }
29
34
 
30
35
  export interface DecideBudgetInput {
@@ -36,6 +41,8 @@ export interface DecideBudgetInput {
36
41
  /** 주간 사용률. `undefined` 는 «모름» — 쓸 수 있다고 보지 않는다. */
37
42
  readonly grokUsedPercent: number | undefined;
38
43
  readonly maxUsedPercent: BudgetGateMaxUsedPercent;
44
+ /** codex 한도 정책이 credits 인가(`codexPolicyAllowsCredits` · 회전·Pod 배분과 같은 해석). 없으면 false. */
45
+ readonly codexCreditsAllowed?: boolean;
39
46
  }
40
47
 
41
48
  export interface BudgetDecision {
@@ -50,6 +57,7 @@ export interface BudgetInputs {
50
57
  readonly codexCandidates: readonly BudgetCodexCandidate[];
51
58
  readonly grokUsedPercent: number | undefined;
52
59
  readonly maxUsedPercent: BudgetGateMaxUsedPercent;
60
+ readonly codexCreditsAllowed?: boolean;
53
61
  }
54
62
 
55
63
  const WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
@@ -98,6 +106,16 @@ function codexCandidateUsable(candidate: BudgetCodexCandidate, cap: number | und
98
106
  return candidate.usedPercent < cap;
99
107
  }
100
108
 
109
+ /** BUDGET-GATE(10-05) — 정책이 credits 면 구독 % 가 차도 크레딧 잔액이 «확인된» 계정으로 계속 간다(Pod 배분과 같은 결론).
110
+ * 잔액을 모르면 쓰지 않는다 — 크레딧은 돈이라 fail-closed. 잔액이 가장 큰 계정 하나를 돌려준다. */
111
+ function codexCreditCandidate(input: DecideBudgetInput): BudgetCodexCandidate | undefined {
112
+ if (input.codexCreditsAllowed !== true) return undefined;
113
+ return input.codexCandidates
114
+ .filter((candidate) => candidate.hasCredits !== false
115
+ && typeof candidate.creditBalance === 'number' && Number.isFinite(candidate.creditBalance) && candidate.creditBalance > 0)
116
+ .sort((a, b) => (b.creditBalance! - a.creditBalance!) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))[0];
117
+ }
118
+
101
119
  function slotUsable(
102
120
  provider: string,
103
121
  input: DecideBudgetInput,
@@ -106,6 +124,13 @@ function slotUsable(
106
124
  if (provider === CODEX_PROVIDER) {
107
125
  const cap = capFor(input.maxUsedPercent, CODEX_PROVIDER);
108
126
  const measuredUsable = input.codexCandidates.some((candidate) => codexCandidateUsable(candidate, cap));
127
+ const credit = measuredUsable ? undefined : codexCreditCandidate(input);
128
+ if (credit) {
129
+ try {
130
+ debug.log('budget.gate', 'credits-allowed', { used: credit.usedPercent ?? null, balance: credit.creditBalance, policy: 'credits' });
131
+ } catch { /* observation is fail-soft */ }
132
+ return { usable: true, reason: `${formatCodexReason(input.codexCandidates, cap)} · credits allowed (balance ${credit.creditBalance})` };
133
+ }
109
134
  const unmeasured = allowUnmeasured && !measuredUsable && (input.codexCandidates.length === 0
110
135
  || input.codexCandidates.some((candidate) => candidate.reached !== true
111
136
  && (typeof candidate.usedPercent !== 'number' || !Number.isFinite(candidate.usedPercent))));
@@ -188,6 +213,8 @@ export function codexCandidatesFromRotation(
188
213
  name: candidate.name,
189
214
  ...(candidate.reached !== undefined ? { reached: candidate.reached } : {}),
190
215
  ...(candidate.usedPercent !== undefined ? { usedPercent: candidate.usedPercent } : {}),
216
+ ...(candidate.creditBalance !== undefined ? { creditBalance: candidate.creditBalance } : {}),
217
+ ...(candidate.hasCredits !== undefined ? { hasCredits: candidate.hasCredits } : {}),
191
218
  }));
192
219
  }
193
220
 
@@ -212,6 +239,8 @@ export function readBudgetInputs(deps: ReadBudgetInputsDeps = {}): BudgetInputs
212
239
  codexCandidates: codexCandidatesFromRotation(rotation.candidates),
213
240
  grokUsedPercent: grokWeeklyUsedPercent(snapshot),
214
241
  maxUsedPercent,
242
+ // 회전·Pod 배분과 같은 해석(`resolveCodexQuotaPolicy` → `codexPolicyAllowsCredits`)을 읽는다 — 다시 짓지 않는다.
243
+ ...(rotation.policy && codexPolicyAllowsCredits(rotation.policy.policy) ? { codexCreditsAllowed: true } : {}),
215
244
  };
216
245
  }
217
246
 
@@ -0,0 +1,47 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { debug } from '../debug/log.js';
3
+
4
+ type ExposeMode = 'warn' | 'strict';
5
+ type ProcessResult = { status: number | null; stdout?: string; stderr?: string; error?: Error };
6
+ type ExposeResult = { passed: boolean; log: string };
7
+
8
+ /** The rubric only accepts root-level public Markdown documents. */
9
+ export function publicExposureFiles(changed: readonly string[]): string[] {
10
+ return [...new Set(changed.filter((file) => /^release\/public\/docs\/[^/]+\.md$/.test(file)))];
11
+ }
12
+
13
+ export function runExposeGate(
14
+ cwd: string,
15
+ changed: readonly string[],
16
+ mode: ExposeMode,
17
+ run: (command: string, args: string[], cwd: string) => ProcessResult = (command, args, dir) => {
18
+ const result = spawnSync(command, args, { cwd: dir, encoding: 'utf8', timeout: 300_000 });
19
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '', error: result.error };
20
+ },
21
+ ): ExposeResult {
22
+ const files = publicExposureFiles(changed);
23
+ if (files.length === 0) return { passed: true, log: '' };
24
+ let unreviewed: number | null = null;
25
+ let failed: number | null = null;
26
+ let docs: number | null = null;
27
+ let error = '';
28
+ try {
29
+ const result = run('bun', ['scripts/expose-rubric-check.ts', '--json', '--files', ...files], cwd);
30
+ if (result.status !== 0 || result.error) throw new Error(result.stderr?.trim() || result.error?.message || `exit ${result.status}`);
31
+ const parsed = JSON.parse(result.stdout ?? '') as Record<string, unknown>;
32
+ const lists = ['missing', 'orphan', 'failing', 'internal', 'unreviewed', 'unjudged'] as const;
33
+ if (!Number.isSafeInteger(parsed.docs) || (parsed.docs as number) < 0 || !lists.every((key) => Array.isArray(parsed[key]))) {
34
+ throw new Error('invalid exposure rubric JSON');
35
+ }
36
+ docs = parsed.docs as number;
37
+ unreviewed = new Set([...(parsed.missing as string[]), ...(parsed.unjudged as string[])]).size;
38
+ failed = new Set([...(parsed.orphan as string[]), ...(parsed.failing as Array<{ path: string }>).map((item) => item.path), ...(parsed.internal as string[])]).size;
39
+ } catch (cause) {
40
+ error = cause instanceof Error ? cause.message : String(cause);
41
+ }
42
+ try { debug.log('self-implement.gate', 'expose', { files, unreviewed, failed, mode }); } catch { /* observation must not change judgment */ }
43
+ const log = docs === null
44
+ ? `[expose] 못 쟀다 — ${error}`
45
+ : `[expose] ${files.length}개 · 미판정 ${unreviewed} · fail ${failed}`;
46
+ return { passed: mode === 'warn' || (docs !== null && unreviewed === 0 && failed === 0), log };
47
+ }
@@ -0,0 +1,295 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dlopen, FFIType } from 'bun:ffi';
3
+ import { createHash } from 'node:crypto';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import { debug } from '../debug/log.js';
7
+ import { beginLandingMerge, readLandingFreeze, type LandingFreeze } from '../release-loop/landing-freeze.js';
8
+
9
+ /** `manual`: the owning repository could not be determined — a sweep never resumes it; a person does. */
10
+ export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true; /** Host goal document, so a resumed merge can still apply its «이 칸 완료» declaration (CL-AUTO). */ goalFile?: string }
11
+ /** What a resume sweep hands its merge function: the held entry, marked as already claimed by this sweep. */
12
+ export type ResumedMerge = FrozenMerge & { resumed: true };
13
+
14
+ function queuePath(root: string): string { return join(root, 'landing-freeze-pending.json'); }
15
+
16
+ /** flock(2) through bun:ffi — the kernel drops the lock when its holder exits, so there is no stale lock to reclaim
17
+ * and no reclaim race. The lock file itself is never deleted (unlinking a flock file would split later lockers). */
18
+ const LOCK_EX = 2, LOCK_NB = 4;
19
+ let flockCall: ((fd: number, op: number) => number) | undefined;
20
+ function flock(fd: number, op: number): number {
21
+ if (!flockCall) {
22
+ const names = process.platform === 'darwin' ? ['libc.dylib'] : ['libc.so.6', `libc.musl-${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}.so.1`];
23
+ let lastError: unknown;
24
+ for (const name of names) {
25
+ try {
26
+ const lib = dlopen(name, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
27
+ flockCall = (descriptor, operation) => lib.symbols.flock(descriptor, operation) as number;
28
+ break;
29
+ } catch (error) { lastError = error; }
30
+ }
31
+ if (!flockCall) throw new Error(`flock unavailable (${names.join(', ')}): ${String(lastError)}`);
32
+ }
33
+ return flockCall(fd, op);
34
+ }
35
+
36
+ export function tryLandingLock(path: string): (() => void) | null {
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ const fd = openSync(path, 'a', 0o600);
39
+ if (flock(fd, LOCK_EX | LOCK_NB) !== 0) { closeSync(fd); return null; }
40
+ let released = false;
41
+ return () => { if (!released) { released = true; closeSync(fd); } };
42
+ }
43
+
44
+ function tryLock(path: string): (() => void) | null { return tryLandingLock(path); }
45
+
46
+ function lockedQueue<T>(root: string, action: () => T): T {
47
+ mkdirSync(root, { recursive: true });
48
+ const path = join(root, 'landing-freeze-pending.lock');
49
+ const deadline = Date.now() + 5_000;
50
+ let release = tryLock(path);
51
+ while (!release) {
52
+ if (Date.now() >= deadline) throw new Error(`frozen merge queue lock busy: ${path}`);
53
+ Bun.sleepSync(25);
54
+ release = tryLock(path);
55
+ }
56
+ try { return action(); }
57
+ finally { release(); }
58
+ }
59
+
60
+ function readQueue(root: string): FrozenMerge[] {
61
+ const path = queuePath(root);
62
+ if (!existsSync(path)) return [];
63
+ const data: unknown = JSON.parse(readFileSync(path, 'utf8'));
64
+ if (!Array.isArray(data) || !data.every((item) => item && typeof item === 'object' &&
65
+ Number.isSafeInteger(item.prNumber) && item.prNumber > 0 &&
66
+ typeof item.headCommit === 'string' && /^[a-f0-9]{40}$/i.test(item.headCommit) &&
67
+ typeof item.repoRoot === 'string' && item.repoRoot.length > 0 &&
68
+ (item.manual === undefined || item.manual === true) &&
69
+ (item.goalFile === undefined || (typeof item.goalFile === 'string' && item.goalFile.length > 0)))) throw new Error(`invalid frozen merge queue: ${path}`);
70
+ return data as FrozenMerge[];
71
+ }
72
+
73
+ function writeQueue(root: string, entries: FrozenMerge[]): void {
74
+ mkdirSync(root, { recursive: true });
75
+ const path = queuePath(root);
76
+ const temp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
77
+ try {
78
+ writeFileSync(temp, `${JSON.stringify(entries, null, 2)}\n`, { mode: 0o600 });
79
+ renameSync(temp, path);
80
+ } finally { rmSync(temp, { force: true }); }
81
+ }
82
+
83
+ /** Ordering of two heads of one PR in `repoRoot`: 'older' when `a` is a strict ancestor of `b`, 'newer' when `b` is one
84
+ * of `a`, otherwise unknown (unrelated, or git cannot see both objects). */
85
+ function headOrder(repoRoot: string, a: string, b: string): 'older' | 'newer' | 'unknown' {
86
+ const ancestor = (x: string, y: string) => {
87
+ try { return Bun.spawnSync(['git', '-C', repoRoot, 'merge-base', '--is-ancestor', x, y], { env: process.env }).exitCode === 0; }
88
+ catch { return false; }
89
+ };
90
+ if (a === b) return 'unknown';
91
+ if (ancestor(a, b)) return 'older';
92
+ if (ancestor(b, a)) return 'newer';
93
+ return 'unknown';
94
+ }
95
+
96
+ /** GitHub's current head of the PR, or null when it cannot be read. */
97
+ function githubHead(entry: FrozenMerge): string | null {
98
+ try {
99
+ const gh = Bun.which('gh', { PATH: process.env.PATH ?? '' }) ?? 'gh';
100
+ const view = Bun.spawnSync([gh, 'pr', 'view', String(entry.prNumber), '--json', 'headRefOid'], { cwd: entry.repoRoot, env: process.env, timeout: 30_000 });
101
+ if (view.exitCode !== 0) return null;
102
+ const head = (JSON.parse(view.stdout.toString()) as { headRefOid?: string }).headRefOid;
103
+ return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head) ? head : null;
104
+ } catch { return null; }
105
+ }
106
+
107
+ export function queueFrozenMerge(entry: FrozenMerge, root = effectiveInstanceRoot(), currentHead: (entry: FrozenMerge) => string | null = githubHead): void {
108
+ if (!Number.isSafeInteger(entry.prNumber) || entry.prNumber <= 0 || !/^[0-9a-f]{40}$/i.test(entry.headCommit) || !entry.repoRoot) throw new Error('invalid frozen merge entry');
109
+ const repoRoot = repoIdentity(entry.repoRoot);
110
+ // Slow lookups (git, GitHub) happen outside the queue lock; inside it only the held head they were judged against
111
+ // is re-checked — if it changed meanwhile, the judgement is made again.
112
+ for (let attempt = 0; attempt < 3; attempt++) {
113
+ const seen = readQueue(root).find((item) => item.repoRoot === repoRoot && item.prNumber === entry.prNumber)?.headCommit;
114
+ let keepExisting = false;
115
+ let by = 'none';
116
+ if (seen && seen !== entry.headCommit) {
117
+ const order = headOrder(repoRoot, entry.headCommit, seen);
118
+ if (order !== 'unknown') { keepExisting = order === 'older'; by = 'ancestry'; }
119
+ else { keepExisting = currentHead({ ...entry, repoRoot }) !== entry.headCommit; by = 'github-or-unknown'; }
120
+ }
121
+ const done = lockedQueue(root, () => {
122
+ const current = readQueue(root);
123
+ const existing = current.find((item) => item.repoRoot === repoRoot && item.prNumber === entry.prNumber);
124
+ if (existing?.headCommit !== seen) return false; // changed while we looked: judge again
125
+ if (existing && keepExisting) {
126
+ debug.log('harness.merge', 'frozen-head-kept', { pr: entry.prNumber, kept: existing.headCommit, ignored: entry.headCommit, by });
127
+ return true;
128
+ }
129
+ writeQueue(root, [...current.filter((item) => item !== existing), { ...entry, repoRoot }]);
130
+ return true;
131
+ });
132
+ if (done) return;
133
+ }
134
+ throw new Error(`frozen merge queue kept changing for PR ${entry.prNumber}`);
135
+ }
136
+
137
+ export function pendingFrozenMerges(root = effectiveInstanceRoot()): number { return readQueue(root).length; }
138
+
139
+ /** The repository a run worktree belongs to, as a path that outlives the worktree: the main worktree when there is
140
+ * one (also with a separate git dir), else the bare repository directory; the worktree itself only if git cannot say. */
141
+ export function owningRepoRoot(worktree: string, spawn: typeof Bun.spawnSync = Bun.spawnSync): string | null {
142
+ let list: ReturnType<typeof Bun.spawnSync>;
143
+ try { list = spawn(['git', '-C', worktree, 'worktree', 'list', '--porcelain'], { env: process.env }); }
144
+ catch { return null; } // git could not even run: unknown owner — the held PR stays for a person
145
+ if (list.exitCode === 0) {
146
+ const first = (list.stdout?.toString() ?? "").split('\n\n')[0] ?? '';
147
+ const path = /^worktree (.+)$/m.exec(first)?.[1];
148
+ if (path) return path; // main worktree, or the bare repository directory (gh reads the remote from the git dir)
149
+ }
150
+ return null;
151
+ }
152
+
153
+ const sameEntry = (a: FrozenMerge, b: FrozenMerge) => a.prNumber === b.prNumber && a.repoRoot === b.repoRoot && a.headCommit === b.headCommit;
154
+
155
+ /** One runner per held PR: whoever creates the claim file (resume sweep or the original lander) merges it. */
156
+ /** One identity per repository path: symlinks and `..` resolve to the real path (absolute path when it does not exist). */
157
+ export function repoIdentity(path: string): string {
158
+ try { return realpathSync(path); } catch { return resolve(path); }
159
+ }
160
+
161
+ /** A claim names the repository as well as the PR number — the queue tells PRs apart the same way. */
162
+ export function claimPath(key: { prNumber: number; repoRoot: string }, root: string): string {
163
+ const repo = createHash('sha256').update(repoIdentity(key.repoRoot)).digest('hex').slice(0, 12);
164
+ return join(root, `landing-freeze-claim-${key.prNumber}-${repo}.lock`);
165
+ }
166
+
167
+ function claimFrozenMerge(entry: FrozenMerge, root: string): { release: () => void } | null {
168
+ mkdirSync(root, { recursive: true });
169
+ const release = tryLock(claimPath(entry, root));
170
+ return release ? { release } : null;
171
+ }
172
+
173
+ export type LandingAdmission =
174
+ | { kind: 'merge'; /** `true` once the merge is confirmed: drops this lander's own held entry. */ end: (merged?: boolean) => void }
175
+ | { kind: 'held'; freeze: LandingFreeze }
176
+ | { kind: 'taken' };
177
+
178
+ /** Gate one merge/publication on the freeze. While frozen the entry is queued; if the freeze is lifted while queueing,
179
+ * the entry is taken back and the caller merges now — unless a resume sweep already claimed it ('taken'). */
180
+ export function admitLandingMerge(
181
+ heldEntry: FrozenMerge | null | (() => FrozenMerge | null),
182
+ root = effectiveInstanceRoot(),
183
+ hooks: { afterQueue?: () => void } = {},
184
+ claimKey?: { prNumber: number; repoRoot: string; headCommit?: string },
185
+ ): LandingAdmission {
186
+ let entry: FrozenMerge | null | undefined;
187
+ // A held PR is merged by exactly one runner: the claim (repository + PR) is held from admission until the lander's
188
+ // merge has ended. Its queue entry stays until that merge is confirmed — `end(true)` removes this lander's own head;
189
+ // a failed or interrupted merge leaves it for the next resume sweep.
190
+ let claim: { release: () => void } | null = null;
191
+ let ownHead: { prNumber: number; repoRoot: string; headCommit?: string } | null = null;
192
+ const finish = (landing: { end: () => void }, held: { release: () => void } | null, own: typeof ownHead) => (merged?: boolean) => {
193
+ try {
194
+ // Only a confirmed merge of a known head removes that head; without a head nothing is dequeued (a sweep will
195
+ // find the PR merged on GitHub and drop it).
196
+ if (merged && own?.headCommit) {
197
+ const head = own.headCommit;
198
+ lockedQueue(root, () => writeQueue(root, readQueue(root).filter((item) =>
199
+ item.prNumber !== own.prNumber || item.repoRoot !== own.repoRoot || item.headCommit !== head)));
200
+ }
201
+ } finally { landing.end(); held?.release(); }
202
+ };
203
+ for (;;) {
204
+ const landing = beginLandingMerge(root);
205
+ if (!landing.frozen) {
206
+ if (!claim && claimKey) {
207
+ const key = { prNumber: claimKey.prNumber, repoRoot: repoIdentity(claimKey.repoRoot), headCommit: claimKey.headCommit };
208
+ const queued = () => readQueue(root).some((item) => item.prNumber === key.prNumber && item.repoRoot === key.repoRoot);
209
+ if (existsSync(claimPath(key, root)) || queued()) {
210
+ const probe = tryLock(claimPath(key, root));
211
+ if (!probe) { landing.end(); return { kind: 'taken' }; }
212
+ claim = { release: probe };
213
+ ownHead = key;
214
+ }
215
+ }
216
+ return { kind: 'merge', end: finish(landing, claim, ownHead) };
217
+ }
218
+ claim?.release();
219
+ claim = null;
220
+ ownHead = null;
221
+ if (entry === undefined) entry = typeof heldEntry === 'function' ? heldEntry() : heldEntry; // only built when frozen
222
+ if (!entry) return { kind: 'held', freeze: landing.frozen };
223
+ queueFrozenMerge(entry, root);
224
+ hooks.afterQueue?.();
225
+ if (readLandingFreeze(root)) return { kind: 'held', freeze: landing.frozen };
226
+ const mine = { ...entry, repoRoot: repoIdentity(entry.repoRoot) };
227
+ claim = claimFrozenMerge(mine, root);
228
+ if (!claim) return { kind: 'taken' };
229
+ // A resume sweep already merged and dequeued it between our queueing and our claim: nothing left to merge.
230
+ let stillHeld = false;
231
+ try { stillHeld = readQueue(root).some((item) => sameEntry(item, mine)); }
232
+ catch (error) { claim.release(); throw error; }
233
+ if (!stillHeld) { claim.release(); return { kind: 'taken' }; }
234
+ ownHead = mine;
235
+ debug.log('harness.merge', 'freeze-lifted-while-queueing', { pr: entry.prNumber });
236
+ }
237
+ }
238
+
239
+ /** Default «is this PR already merged at this head?» check — GitHub is the final word. */
240
+ function githubMergedAt(entry: FrozenMerge): boolean {
241
+ try {
242
+ // Resolve gh against the current PATH (Bun's spawn would otherwise use the PATH from process start).
243
+ const gh = Bun.which('gh', { PATH: process.env.PATH ?? '' }) ?? 'gh';
244
+ const view = Bun.spawnSync([gh, 'pr', 'view', String(entry.prNumber), '--json', 'state,headRefOid'], { cwd: entry.repoRoot, env: process.env, timeout: 30_000 });
245
+ if (view.exitCode !== 0) return false;
246
+ const pr = JSON.parse(view.stdout.toString()) as { state?: string; headRefOid?: string };
247
+ return pr.state === 'MERGED' && pr.headRefOid === entry.headCommit;
248
+ } catch { return false; } // unknown is «not known merged»: the head-pinned merge still refuses a second merge
249
+ }
250
+
251
+ /** Invoke at a supervision tick. A failed re-gate remains queued; only a confirmed merge is removed.
252
+ * «Merged once» does not rest on the claim lock alone: every merge is pinned to the held head
253
+ * (`gh pr merge --match-head-commit`, which refuses an already-merged PR), and before and after each attempt the
254
+ * sweep asks GitHub whether the PR is already merged at that head — a duplicate runner can at most repeat a re-gate. */
255
+ export async function sweepFrozenMerges(
256
+ merge: (entry: ResumedMerge) => Promise<{ passed: boolean; status?: string }> = async (entry) => {
257
+ const { runHostRegate } = await import('./host-regate.js');
258
+ return runHostRegate(entry);
259
+ },
260
+ root = effectiveInstanceRoot(),
261
+ alreadyMerged: (entry: FrozenMerge) => boolean | Promise<boolean> = githubMergedAt,
262
+ ): Promise<{ pending: number; merged: number }> {
263
+ const frozen = readLandingFreeze(root);
264
+ if (frozen) return { pending: readQueue(root).length, merged: 0 };
265
+ const pending = readQueue(root);
266
+ let merged = 0;
267
+ for (const entry of pending) {
268
+ if (readLandingFreeze(root)) break;
269
+ const claim = claimFrozenMerge(entry, root);
270
+ if (!claim) continue; // another sweep or the original lander is on it
271
+ try {
272
+ if (!readQueue(root).some((item) => sameEntry(item, entry))) continue; // taken back by its lander
273
+ if (entry.manual) { debug.log('harness.merge', 'resume-manual', { pr: entry.prNumber, repoRoot: entry.repoRoot }); continue; }
274
+ if (!(await alreadyMerged(entry))) {
275
+ const outcome = await merge({ ...entry, resumed: true });
276
+ if (outcome.status === 'frozen') continue;
277
+ // Whatever the merge call reported, only GitHub showing the PR merged at the held head dequeues it.
278
+ if (!(await alreadyMerged(entry))) {
279
+ if (outcome.passed) debug.log('harness.merge', 'resume-unconfirmed', { pr: entry.prNumber, head: entry.headCommit });
280
+ continue;
281
+ }
282
+ }
283
+ lockedQueue(root, () => {
284
+ const current = readQueue(root);
285
+ if (current.some((item) => sameEntry(item, entry))) {
286
+ writeQueue(root, current.filter((item) => !sameEntry(item, entry)));
287
+ merged++;
288
+ }
289
+ });
290
+ } catch (error) {
291
+ debug.log('harness.merge', 'resume-failed', { pr: entry.prNumber, error: String(error) });
292
+ } finally { claim.release(); }
293
+ }
294
+ return { pending: readQueue(root).length, merged };
295
+ }
@@ -12,6 +12,8 @@ import { runMockModuleRestoreGate } from '../../scripts/ci-mock-module-restore-g
12
12
  import { parseReviewDepthFromFilesJson } from '../agent-mission/review-depth.js';
13
13
  import { buildGateBaselineReport, calculateGateTestCountChange, formatGateBaselineNote, formatGateTestCountNote, isGateTestFile, runGateBaseline, type BaselineProcessResult, type BaselineProcessStatus, type GateTestFailure } from './gate-baseline.js';
14
14
  import { resolveGateScope } from './gate-scope.js';
15
+ import { publicExposureFiles, runExposeGate } from './expose-gate.js';
16
+ import { getUserConfig } from '../user-config.js';
15
17
  import { debug } from '../debug/log.js';
16
18
  import { buildDocumentGuardianIndex, formatDocumentGuardians } from './document-guardian-index.js';
17
19
  import { buildImporterTestIndex, isTestPath, type ImporterTestIndex } from './importer-test-index.js';
@@ -585,6 +587,9 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
585
587
  ? buildRouteConsumerTestIndex(cwd, availableIndex.testPaths, selection.files, selection.baseRef) : null;
586
588
  const scope = resolveGateScope(selection.files, exists, availableIndex,
587
589
  { routeConsumerTestIndex: routeIndex ?? undefined });
590
+ const expose = publicExposureFiles(selection.files).length > 0
591
+ ? runExposeGate(cwd, selection.files, getUserConfig().harness?.exposeGate === 'strict' ? 'strict' : 'warn', deps.runCommand)
592
+ : { passed: true, log: '' };
588
593
  const testFiles = [...(scope.testArgs ?? [])];
589
594
  const unrunImporterTotal = lookupFailed ? null : (scope.importerTestsNotRun?.total ?? 0);
590
595
  const importerLookupFailed = Boolean(lookupFailed);
@@ -615,6 +620,7 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
615
620
  ...(scope.callerTestsOverflow.length ? [`⚠️ caller test cap exceeded — not run: ${scope.callerTestsOverflow.map(({ file, reasons }) => `${file} (${reasons.join('+')})`).join(', ')}`] : []),
616
621
  changedTestCountNote(cwd, selection.files, selection.baseRef, deps.runCommand, deps.readFile),
617
622
  ];
623
+ if (expose.log) lines.push(expose.log);
618
624
  if (options.pr) {
619
625
  const worktreeObservation = observePrWorktreeDirtiness(cwd, selection.prHeadRefName, {
620
626
  runCommand: deps.runCommand,
@@ -638,7 +644,7 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
638
644
  if (scope.skipTestStep) {
639
645
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
640
646
  logGateCliBaseline(cwd, { ...ZERO_GATE_BASELINE, unrunImporterTotal, lookupFailed: importerLookupFailed });
641
- return { exitCode: androidPassed && iosPassed && pwaPassed && policyPassed ? 0 : 1, lines, changedFiles: selection.files, testFiles: [], unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
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 };
642
648
  }
643
649
 
644
650
  if (options.shards !== undefined) {
@@ -662,7 +668,7 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
662
668
  }
663
669
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
664
670
  return {
665
- exitCode: aggregate.status !== 'passed' || !policyPassed || !androidPassed || !iosPassed || !pwaPassed ? 1 : 0,
671
+ exitCode: aggregate.status !== 'passed' || !policyPassed || !androidPassed || !iosPassed || !pwaPassed || !expose.passed ? 1 : 0,
666
672
  lines, changedFiles: selection.files, testFiles, unverified: scope.unverified,
667
673
  documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests,
668
674
  };
@@ -674,7 +680,7 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
674
680
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, `tests: pass (${testFiles.length} files)`));
675
681
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
676
682
  logGateCliBaseline(cwd, { ...ZERO_GATE_BASELINE, unrunImporterTotal, lookupFailed: importerLookupFailed });
677
- return { exitCode: policyPassed && androidPassed && iosPassed && pwaPassed ? 0 : 1, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
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 };
678
684
  }
679
685
 
680
686
  if (worktreeLog.includes('deterministic environment setup failed')) lines.push(worktreeLog);
@@ -695,6 +701,6 @@ export function runSelfGateCli(cwd: string, options: SelfGateCliOptions = {}, de
695
701
  });
696
702
  lines.push(formatPartialObservationNote(unrunImporterTotal ?? 0, formatGateBaselineNote(report, unrunImporterTotal ?? 0)));
697
703
  const policyPassed = runPolicyGates(selection.files, cwd, lines, deps);
698
- const exitCode = !androidPassed || !iosPassed || !pwaPassed || !policyPassed || report.introduced > 0 || report.unknown > 0 || report.preconditionUnmet > 0 ? 1 : 0;
704
+ const exitCode = !androidPassed || !iosPassed || !pwaPassed || !policyPassed || !expose.passed || report.introduced > 0 || report.unknown > 0 || report.preconditionUnmet > 0 ? 1 : 0;
699
705
  return { exitCode, lines, changedFiles: selection.files, testFiles, unverified: scope.unverified, documentPaths: scope.documentPaths, documentsWithoutDerivedTests: scope.documentsWithoutDerivedTests };
700
706
  }