elanous 0.2.13 → 0.2.14

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 (445) 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/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.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-2df489622a52a33a.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/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f14987d5850a31e0.js → page-1abfe3aec3be6bdc.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-15374909d8f1aebd.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-66c27608f651b866.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-78b40a344ed22f24.js → page-e1e47d8d940aba3d.js} +1 -1
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/graphs/orchestrator/orchestrator.yaml +59 -0
  194. package/graphs/orchestrator/recipes.yaml +18 -0
  195. package/graphs/release/recipes.yaml +3 -0
  196. package/graphs/release/release-loop.yaml +4 -1
  197. package/graphs/rhythm/daily.yaml +38 -0
  198. package/graphs/rhythm/recipes.yaml +18 -0
  199. package/graphs/rhythm/weekly.yaml +39 -0
  200. package/graphs/video/recipes.yaml +3 -0
  201. package/graphs/video/youtube-summary.yaml +17 -0
  202. package/package.json +1 -1
  203. package/scripts/audit-test-state-writes.ts +72 -17
  204. package/scripts/brand/check.ts +186 -0
  205. package/scripts/coord-post.sh +11 -1
  206. package/scripts/docs-cli-check.ts +23 -6
  207. package/scripts/expose-rubric-check.ts +118 -0
  208. package/scripts/expose-rubric-suggest.ts +163 -0
  209. package/scripts/heal/run-role.ts +14 -0
  210. package/scripts/hq/standby-verify.ts +11 -7
  211. package/scripts/lessons/handbook-page.ts +85 -0
  212. package/scripts/release-loop/gate-node.ts +9 -0
  213. package/scripts/release-loop/publish-node.ts +14 -1
  214. package/scripts/release-loop/unattended-release.ts +23 -1
  215. package/scripts/release-loop/vault-note-node.ts +196 -0
  216. package/scripts/release-story/draft.ts +172 -0
  217. package/scripts/rhythm/daily.ts +383 -0
  218. package/scripts/rhythm/weekly.ts +294 -0
  219. package/scripts/skills/public-check.ts +169 -0
  220. package/scripts/test-deterministic.ts +4 -8
  221. package/scripts/test-diet/effectiveness.ts +195 -0
  222. package/scripts/test-diet/lib.ts +16 -8
  223. package/scripts/test-diet/node.ts +77 -9
  224. package/scripts/ux/demo-rehearsal.ts +2 -2
  225. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  226. package/scripts/video/youtube-summary.ts +133 -0
  227. package/skills/diagram-master/SKILL.md +42 -49
  228. package/skills/diagram-master/references/pyproject.toml +4 -1
  229. package/skills/diagram-master/references/routing-guide.md +4 -2
  230. package/skills/grill-me/SKILL.md +1 -0
  231. package/skills/youtube-master/SKILL.md +13 -13
  232. package/skills/youtube-master/package-lock.json +0 -545
  233. package/skills/youtube-master/package.json +2 -5
  234. package/skills/youtube-master/scripts/main.ts +2 -2
  235. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  236. package/skills/youtube-master/src/util.ts +30 -7
  237. package/src/agent-mission/driver.ts +3 -3
  238. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  239. package/src/autopilot/build/merge-intent.ts +53 -0
  240. package/src/chat/index.ts +2 -0
  241. package/src/claims/claims-ledger.ts +232 -0
  242. package/src/claims/claims-render.ts +157 -0
  243. package/src/cli/claims-cli.ts +94 -0
  244. package/src/cli/decisions-cli.ts +35 -3
  245. package/src/cli/docs-cli.ts +2 -0
  246. package/src/cli/flow-cli.ts +11 -0
  247. package/src/cli/freeze-cli.ts +45 -0
  248. package/src/cli/lesson-cli.ts +108 -0
  249. package/src/cli/logs-cli.ts +104 -2
  250. package/src/cli/logs-model-input.ts +131 -0
  251. package/src/cli/pr-cli.ts +29 -1
  252. package/src/cli/private-ledger-commands.ts +54 -4
  253. package/src/cli/pty-drive-verdict.ts +11 -2
  254. package/src/cli/release-cli.ts +144 -21
  255. package/src/cli/rfc-status.ts +91 -0
  256. package/src/connectors/linear.ts +5 -3
  257. package/src/context-bus/condense.ts +15 -20
  258. package/src/context-bus/context-now-public.ts +86 -0
  259. package/src/context-bus/context-now-surfaces.ts +55 -2
  260. package/src/context-bus/context-now.ts +4 -3
  261. package/src/context-bus/long-term-memory.ts +92 -11
  262. package/src/dashboard/index.ts +75 -22
  263. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  264. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  265. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  266. package/src/decisions/decision-ledger.ts +40 -1
  267. package/src/discord/sprint21-runtime.ts +16 -0
  268. package/src/discord-self-message.ts +8 -0
  269. package/src/discord-slash-wire.ts +31 -2
  270. package/src/domains/outbound-alert.ts +1 -1
  271. package/src/elanous-config-dir.ts +29 -4
  272. package/src/execution-loop/launch-gate.ts +29 -14
  273. package/src/flow/cards.ts +239 -0
  274. package/src/graph-runner/graph-tick.ts +4 -0
  275. package/src/graph-runner/runner.ts +30 -5
  276. package/src/harness/harness-cli-command.ts +152 -27
  277. package/src/harness/harness-incidents.ts +43 -12
  278. package/src/harness/harness-pod-dispatch.ts +2 -0
  279. package/src/harness/harness-queue-child.ts +3 -3
  280. package/src/harness/harness-queue.ts +216 -28
  281. package/src/harness/helper-scan.ts +139 -0
  282. package/src/harness/model-input-observation.ts +37 -0
  283. package/src/hq/hq.ts +1 -1
  284. package/src/hq/promote.ts +301 -0
  285. package/src/index.ts +83 -21
  286. package/src/intake-plane/linear-wish.ts +39 -11
  287. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  288. package/src/intake-plane/wish-card.ts +53 -2
  289. package/src/intake-plane/wish-folder.ts +5 -2
  290. package/src/intake-plane/wish-reply.ts +72 -0
  291. package/src/lessons/lesson-import.ts +106 -0
  292. package/src/lessons/lesson-ledger.ts +206 -0
  293. package/src/loops/activity.ts +203 -0
  294. package/src/loops/budget.ts +32 -0
  295. package/src/loops/checker.ts +5 -3
  296. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  297. package/src/loops/loop-cli.ts +36 -4
  298. package/src/loops/neighbors.ts +40 -0
  299. package/src/loops/observe.ts +10 -0
  300. package/src/loops/orchestrator/author-depth.ts +164 -0
  301. package/src/loops/orchestrator/author-input-check.ts +77 -0
  302. package/src/loops/orchestrator/author-ledger.ts +230 -0
  303. package/src/loops/orchestrator/finish-rate.ts +175 -0
  304. package/src/loops/orchestrator/overlap.ts +51 -12
  305. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  306. package/src/loops/orchestrator/tick.ts +302 -0
  307. package/src/loops/orchestrator/traffic.ts +18 -3
  308. package/src/loops/registry.ts +454 -9
  309. package/src/loops/status-rows.ts +118 -0
  310. package/src/loops/verdict.ts +17 -5
  311. package/src/maturity/bot-slash-catalog.ts +66 -0
  312. package/src/maturity/feature-maturity.ts +7 -4
  313. package/src/nexus/api/dashboard.ts +6 -1
  314. package/src/nexus/api/http-server.ts +18 -2
  315. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  316. package/src/nexus/api/schedules-read.ts +5 -1
  317. package/src/nexus/api/task-cards-api.ts +44 -5
  318. package/src/nexus/api/workflows.ts +2 -1
  319. package/src/release-loop/auto-start.ts +48 -0
  320. package/src/release-loop/checklist.ts +13 -4
  321. package/src/release-loop/feature-store.ts +28 -16
  322. package/src/release-loop/landing-freeze.ts +121 -0
  323. package/src/release-loop/placement.ts +242 -0
  324. package/src/release-loop/release-schedule.ts +3 -1
  325. package/src/seat-dispatch/ceo-commands.ts +2 -2
  326. package/src/seat-dispatch/seat-ask.ts +16 -3
  327. package/src/seat-dispatch/seat-questions.ts +58 -0
  328. package/src/seat-loop/seat-loop.ts +406 -31
  329. package/src/self-dev/dev-pipeline.ts +6 -2
  330. package/src/self-dev/draft-sweep.ts +76 -0
  331. package/src/self-dev/run-store.ts +73 -1
  332. package/src/self-dev/run-supervisor.ts +8 -0
  333. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  334. package/src/self-implement/budget-gate.ts +29 -0
  335. package/src/self-implement/expose-gate.ts +47 -0
  336. package/src/self-implement/frozen-merges.ts +294 -0
  337. package/src/self-implement/gate-cli.ts +10 -4
  338. package/src/self-implement/gate-scope.ts +46 -9
  339. package/src/self-implement/goal-archive.ts +222 -0
  340. package/src/self-implement/goal-lookup.ts +147 -0
  341. package/src/self-implement/heal-intake.ts +79 -0
  342. package/src/self-implement/host-regate.ts +25 -2
  343. package/src/self-implement/orchestrator.ts +114 -10
  344. package/src/self-implement/revert-guard.ts +62 -0
  345. package/src/self-implement/run-ledger.ts +1 -1
  346. package/src/self-implement/seams.ts +28 -8
  347. package/src/session/index.ts +2 -0
  348. package/src/task-cards/card-cli.ts +10 -2
  349. package/src/task-cards/card-store.ts +10 -6
  350. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  351. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  352. package/src/task-orchestrator/surfaces/self-implement-pod.ts +80 -16
  353. package/src/telegram-commands.ts +27 -3
  354. package/src/telegram-loops-command.ts +44 -0
  355. package/src/user-config.ts +120 -16
  356. package/src/version/packed-revision.json +1 -1
  357. package/src/workflow-runtime/storage.ts +31 -13
  358. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  369. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  373. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  444. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  445. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,294 @@
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 }
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))) throw new Error(`invalid frozen merge queue: ${path}`);
69
+ return data as FrozenMerge[];
70
+ }
71
+
72
+ function writeQueue(root: string, entries: FrozenMerge[]): void {
73
+ mkdirSync(root, { recursive: true });
74
+ const path = queuePath(root);
75
+ const temp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
76
+ try {
77
+ writeFileSync(temp, `${JSON.stringify(entries, null, 2)}\n`, { mode: 0o600 });
78
+ renameSync(temp, path);
79
+ } finally { rmSync(temp, { force: true }); }
80
+ }
81
+
82
+ /** Ordering of two heads of one PR in `repoRoot`: 'older' when `a` is a strict ancestor of `b`, 'newer' when `b` is one
83
+ * of `a`, otherwise unknown (unrelated, or git cannot see both objects). */
84
+ function headOrder(repoRoot: string, a: string, b: string): 'older' | 'newer' | 'unknown' {
85
+ const ancestor = (x: string, y: string) => {
86
+ try { return Bun.spawnSync(['git', '-C', repoRoot, 'merge-base', '--is-ancestor', x, y], { env: process.env }).exitCode === 0; }
87
+ catch { return false; }
88
+ };
89
+ if (a === b) return 'unknown';
90
+ if (ancestor(a, b)) return 'older';
91
+ if (ancestor(b, a)) return 'newer';
92
+ return 'unknown';
93
+ }
94
+
95
+ /** GitHub's current head of the PR, or null when it cannot be read. */
96
+ function githubHead(entry: FrozenMerge): string | null {
97
+ try {
98
+ const gh = Bun.which('gh', { PATH: process.env.PATH ?? '' }) ?? 'gh';
99
+ const view = Bun.spawnSync([gh, 'pr', 'view', String(entry.prNumber), '--json', 'headRefOid'], { cwd: entry.repoRoot, env: process.env, timeout: 30_000 });
100
+ if (view.exitCode !== 0) return null;
101
+ const head = (JSON.parse(view.stdout.toString()) as { headRefOid?: string }).headRefOid;
102
+ return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head) ? head : null;
103
+ } catch { return null; }
104
+ }
105
+
106
+ export function queueFrozenMerge(entry: FrozenMerge, root = effectiveInstanceRoot(), currentHead: (entry: FrozenMerge) => string | null = githubHead): void {
107
+ 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');
108
+ const repoRoot = repoIdentity(entry.repoRoot);
109
+ // Slow lookups (git, GitHub) happen outside the queue lock; inside it only the held head they were judged against
110
+ // is re-checked — if it changed meanwhile, the judgement is made again.
111
+ for (let attempt = 0; attempt < 3; attempt++) {
112
+ const seen = readQueue(root).find((item) => item.repoRoot === repoRoot && item.prNumber === entry.prNumber)?.headCommit;
113
+ let keepExisting = false;
114
+ let by = 'none';
115
+ if (seen && seen !== entry.headCommit) {
116
+ const order = headOrder(repoRoot, entry.headCommit, seen);
117
+ if (order !== 'unknown') { keepExisting = order === 'older'; by = 'ancestry'; }
118
+ else { keepExisting = currentHead({ ...entry, repoRoot }) !== entry.headCommit; by = 'github-or-unknown'; }
119
+ }
120
+ const done = lockedQueue(root, () => {
121
+ const current = readQueue(root);
122
+ const existing = current.find((item) => item.repoRoot === repoRoot && item.prNumber === entry.prNumber);
123
+ if (existing?.headCommit !== seen) return false; // changed while we looked: judge again
124
+ if (existing && keepExisting) {
125
+ debug.log('harness.merge', 'frozen-head-kept', { pr: entry.prNumber, kept: existing.headCommit, ignored: entry.headCommit, by });
126
+ return true;
127
+ }
128
+ writeQueue(root, [...current.filter((item) => item !== existing), { ...entry, repoRoot }]);
129
+ return true;
130
+ });
131
+ if (done) return;
132
+ }
133
+ throw new Error(`frozen merge queue kept changing for PR ${entry.prNumber}`);
134
+ }
135
+
136
+ export function pendingFrozenMerges(root = effectiveInstanceRoot()): number { return readQueue(root).length; }
137
+
138
+ /** The repository a run worktree belongs to, as a path that outlives the worktree: the main worktree when there is
139
+ * one (also with a separate git dir), else the bare repository directory; the worktree itself only if git cannot say. */
140
+ export function owningRepoRoot(worktree: string, spawn: typeof Bun.spawnSync = Bun.spawnSync): string | null {
141
+ let list: ReturnType<typeof Bun.spawnSync>;
142
+ try { list = spawn(['git', '-C', worktree, 'worktree', 'list', '--porcelain'], { env: process.env }); }
143
+ catch { return null; } // git could not even run: unknown owner — the held PR stays for a person
144
+ if (list.exitCode === 0) {
145
+ const first = (list.stdout?.toString() ?? "").split('\n\n')[0] ?? '';
146
+ const path = /^worktree (.+)$/m.exec(first)?.[1];
147
+ if (path) return path; // main worktree, or the bare repository directory (gh reads the remote from the git dir)
148
+ }
149
+ return null;
150
+ }
151
+
152
+ const sameEntry = (a: FrozenMerge, b: FrozenMerge) => a.prNumber === b.prNumber && a.repoRoot === b.repoRoot && a.headCommit === b.headCommit;
153
+
154
+ /** One runner per held PR: whoever creates the claim file (resume sweep or the original lander) merges it. */
155
+ /** One identity per repository path: symlinks and `..` resolve to the real path (absolute path when it does not exist). */
156
+ export function repoIdentity(path: string): string {
157
+ try { return realpathSync(path); } catch { return resolve(path); }
158
+ }
159
+
160
+ /** A claim names the repository as well as the PR number — the queue tells PRs apart the same way. */
161
+ export function claimPath(key: { prNumber: number; repoRoot: string }, root: string): string {
162
+ const repo = createHash('sha256').update(repoIdentity(key.repoRoot)).digest('hex').slice(0, 12);
163
+ return join(root, `landing-freeze-claim-${key.prNumber}-${repo}.lock`);
164
+ }
165
+
166
+ function claimFrozenMerge(entry: FrozenMerge, root: string): { release: () => void } | null {
167
+ mkdirSync(root, { recursive: true });
168
+ const release = tryLock(claimPath(entry, root));
169
+ return release ? { release } : null;
170
+ }
171
+
172
+ export type LandingAdmission =
173
+ | { kind: 'merge'; /** `true` once the merge is confirmed: drops this lander's own held entry. */ end: (merged?: boolean) => void }
174
+ | { kind: 'held'; freeze: LandingFreeze }
175
+ | { kind: 'taken' };
176
+
177
+ /** Gate one merge/publication on the freeze. While frozen the entry is queued; if the freeze is lifted while queueing,
178
+ * the entry is taken back and the caller merges now — unless a resume sweep already claimed it ('taken'). */
179
+ export function admitLandingMerge(
180
+ heldEntry: FrozenMerge | null | (() => FrozenMerge | null),
181
+ root = effectiveInstanceRoot(),
182
+ hooks: { afterQueue?: () => void } = {},
183
+ claimKey?: { prNumber: number; repoRoot: string; headCommit?: string },
184
+ ): LandingAdmission {
185
+ let entry: FrozenMerge | null | undefined;
186
+ // A held PR is merged by exactly one runner: the claim (repository + PR) is held from admission until the lander's
187
+ // merge has ended. Its queue entry stays until that merge is confirmed — `end(true)` removes this lander's own head;
188
+ // a failed or interrupted merge leaves it for the next resume sweep.
189
+ let claim: { release: () => void } | null = null;
190
+ let ownHead: { prNumber: number; repoRoot: string; headCommit?: string } | null = null;
191
+ const finish = (landing: { end: () => void }, held: { release: () => void } | null, own: typeof ownHead) => (merged?: boolean) => {
192
+ try {
193
+ // Only a confirmed merge of a known head removes that head; without a head nothing is dequeued (a sweep will
194
+ // find the PR merged on GitHub and drop it).
195
+ if (merged && own?.headCommit) {
196
+ const head = own.headCommit;
197
+ lockedQueue(root, () => writeQueue(root, readQueue(root).filter((item) =>
198
+ item.prNumber !== own.prNumber || item.repoRoot !== own.repoRoot || item.headCommit !== head)));
199
+ }
200
+ } finally { landing.end(); held?.release(); }
201
+ };
202
+ for (;;) {
203
+ const landing = beginLandingMerge(root);
204
+ if (!landing.frozen) {
205
+ if (!claim && claimKey) {
206
+ const key = { prNumber: claimKey.prNumber, repoRoot: repoIdentity(claimKey.repoRoot), headCommit: claimKey.headCommit };
207
+ const queued = () => readQueue(root).some((item) => item.prNumber === key.prNumber && item.repoRoot === key.repoRoot);
208
+ if (existsSync(claimPath(key, root)) || queued()) {
209
+ const probe = tryLock(claimPath(key, root));
210
+ if (!probe) { landing.end(); return { kind: 'taken' }; }
211
+ claim = { release: probe };
212
+ ownHead = key;
213
+ }
214
+ }
215
+ return { kind: 'merge', end: finish(landing, claim, ownHead) };
216
+ }
217
+ claim?.release();
218
+ claim = null;
219
+ ownHead = null;
220
+ if (entry === undefined) entry = typeof heldEntry === 'function' ? heldEntry() : heldEntry; // only built when frozen
221
+ if (!entry) return { kind: 'held', freeze: landing.frozen };
222
+ queueFrozenMerge(entry, root);
223
+ hooks.afterQueue?.();
224
+ if (readLandingFreeze(root)) return { kind: 'held', freeze: landing.frozen };
225
+ const mine = { ...entry, repoRoot: repoIdentity(entry.repoRoot) };
226
+ claim = claimFrozenMerge(mine, root);
227
+ if (!claim) return { kind: 'taken' };
228
+ // A resume sweep already merged and dequeued it between our queueing and our claim: nothing left to merge.
229
+ let stillHeld = false;
230
+ try { stillHeld = readQueue(root).some((item) => sameEntry(item, mine)); }
231
+ catch (error) { claim.release(); throw error; }
232
+ if (!stillHeld) { claim.release(); return { kind: 'taken' }; }
233
+ ownHead = mine;
234
+ debug.log('harness.merge', 'freeze-lifted-while-queueing', { pr: entry.prNumber });
235
+ }
236
+ }
237
+
238
+ /** Default «is this PR already merged at this head?» check — GitHub is the final word. */
239
+ function githubMergedAt(entry: FrozenMerge): boolean {
240
+ try {
241
+ // Resolve gh against the current PATH (Bun's spawn would otherwise use the PATH from process start).
242
+ const gh = Bun.which('gh', { PATH: process.env.PATH ?? '' }) ?? 'gh';
243
+ const view = Bun.spawnSync([gh, 'pr', 'view', String(entry.prNumber), '--json', 'state,headRefOid'], { cwd: entry.repoRoot, env: process.env, timeout: 30_000 });
244
+ if (view.exitCode !== 0) return false;
245
+ const pr = JSON.parse(view.stdout.toString()) as { state?: string; headRefOid?: string };
246
+ return pr.state === 'MERGED' && pr.headRefOid === entry.headCommit;
247
+ } catch { return false; } // unknown is «not known merged»: the head-pinned merge still refuses a second merge
248
+ }
249
+
250
+ /** Invoke at a supervision tick. A failed re-gate remains queued; only a confirmed merge is removed.
251
+ * «Merged once» does not rest on the claim lock alone: every merge is pinned to the held head
252
+ * (`gh pr merge --match-head-commit`, which refuses an already-merged PR), and before and after each attempt the
253
+ * sweep asks GitHub whether the PR is already merged at that head — a duplicate runner can at most repeat a re-gate. */
254
+ export async function sweepFrozenMerges(
255
+ merge: (entry: ResumedMerge) => Promise<{ passed: boolean; status?: string }> = async (entry) => {
256
+ const { runHostRegate } = await import('./host-regate.js');
257
+ return runHostRegate(entry);
258
+ },
259
+ root = effectiveInstanceRoot(),
260
+ alreadyMerged: (entry: FrozenMerge) => boolean | Promise<boolean> = githubMergedAt,
261
+ ): Promise<{ pending: number; merged: number }> {
262
+ const frozen = readLandingFreeze(root);
263
+ if (frozen) return { pending: readQueue(root).length, merged: 0 };
264
+ const pending = readQueue(root);
265
+ let merged = 0;
266
+ for (const entry of pending) {
267
+ if (readLandingFreeze(root)) break;
268
+ const claim = claimFrozenMerge(entry, root);
269
+ if (!claim) continue; // another sweep or the original lander is on it
270
+ try {
271
+ if (!readQueue(root).some((item) => sameEntry(item, entry))) continue; // taken back by its lander
272
+ if (entry.manual) { debug.log('harness.merge', 'resume-manual', { pr: entry.prNumber, repoRoot: entry.repoRoot }); continue; }
273
+ if (!(await alreadyMerged(entry))) {
274
+ const outcome = await merge({ ...entry, resumed: true });
275
+ if (outcome.status === 'frozen') continue;
276
+ // Whatever the merge call reported, only GitHub showing the PR merged at the held head dequeues it.
277
+ if (!(await alreadyMerged(entry))) {
278
+ if (outcome.passed) debug.log('harness.merge', 'resume-unconfirmed', { pr: entry.prNumber, head: entry.headCommit });
279
+ continue;
280
+ }
281
+ }
282
+ lockedQueue(root, () => {
283
+ const current = readQueue(root);
284
+ if (current.some((item) => sameEntry(item, entry))) {
285
+ writeQueue(root, current.filter((item) => !sameEntry(item, entry)));
286
+ merged++;
287
+ }
288
+ });
289
+ } catch (error) {
290
+ debug.log('harness.merge', 'resume-failed', { pr: entry.prNumber, error: String(error) });
291
+ } finally { claim.release(); }
292
+ }
293
+ return { pending: readQueue(root).length, merged };
294
+ }
@@ -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
  }
@@ -31,8 +31,32 @@
31
31
  import { importerTestsNotInRunSet, type ImporterTestIndex } from './importer-test-index.js';
32
32
  import type { RouteConsumerTestIndex } from './route-consumer-test-index.js';
33
33
  import { rankGateCallerCandidates } from './gate-candidate-ranking.js';
34
+ import { debug } from '../debug/log.js';
34
35
 
35
36
  export const GATE_CALLER_TEST_LIMIT = 30;
37
+
38
+ /** Command registries and their non-import-linked gate companions. Kept outside the caller cap. */
39
+ const PAIRED_REGISTRY_TESTS = {
40
+ registries: [
41
+ 'src/chat/index.ts',
42
+ 'src/telegram-commands.ts',
43
+ 'src/discord-slash-wire.ts',
44
+ 'src/index.ts',
45
+ ],
46
+ tests: [
47
+ 'src/maturity/feature-maturity.test.ts',
48
+ 'src/maturity/tui-slash-maturity.test.ts',
49
+ 'src/maturity/bot-command-maturity.test.ts',
50
+ 'src/maturity/cli-maturity.test.ts',
51
+ 'test/slash-filter.test.ts',
52
+ 'test/bot-slash-parity.test.ts',
53
+ ],
54
+ } as const;
55
+
56
+ /** 게이트 로그와 PR Gate 절에서 공통으로 쓰는 미검증 경고(파일 목록은 자르지 않는다). */
57
+ export function formatNoRelatedTestsWarning(files: readonly string[]): string {
58
+ return `⚠️ 동작 검증 0: ${files.join(', ')} — 사람 확인 필요`;
59
+ }
36
60
  export type GateCallerReason = 'import' | 'route';
37
61
 
38
62
  /** 테스트 파일 판정(기존 `seams.ts` 정규식과 동일 — 어휘 분기 금지). */
@@ -225,7 +249,7 @@ export function deriveRelatedTests(
225
249
  /** 왜 이 범위로 돌았나 — 로그만으로 답할 수 있게 하는 관측 필드(제1원칙).
226
250
  * ⚠️ 비-export 유지(리뷰 must-fix) — 소비처가 이 모듈 안뿐이다. 구조적 사용(호출부의 `scope.reason`)은
227
251
  * export 없이도 성립하므로 불필요한 공개 surface 를 만들지 않는다. */
228
- type GateScopeReason = 'changed-tests' | 'derived' | 'no-changes' | 'docs-only' | 'no-related-tests' | 'unmeasured';
252
+ type GateScopeReason = 'changed-tests' | 'derived' | 'paired-registry' | 'no-changes' | 'docs-only' | 'no-related-tests' | 'unmeasured';
229
253
 
230
254
  export type GateScopeMode = 'worktree' | 'postsync';
231
255
 
@@ -279,7 +303,7 @@ export function resolveGateScope(
279
303
  changed: readonly string[],
280
304
  exists: (path: string) => boolean,
281
305
  importerTestIndex?: ImporterTestIndex,
282
- opts?: { mode?: GateScopeMode; isDeleted?: (path: string) => boolean; routeConsumerTestIndex?: RouteConsumerTestIndex },
306
+ opts?: { mode?: GateScopeMode; isDeleted?: (path: string) => boolean; routeConsumerTestIndex?: RouteConsumerTestIndex; runChanged?: readonly string[]; round?: number },
283
307
  ): GateScopeDecision {
284
308
  // ⚠️ **실존 검증(리뷰 must-fix 4R)** — `gitChangedFiles` 는 `git diff --name-only HEAD` 라
285
309
  // **삭제·rename 前 경로도 포함**한다. 그걸 그대로 `testArgs` 로 넘기면 필터가 아무것도 매치하지
@@ -292,9 +316,9 @@ export function resolveGateScope(
292
316
  const missingTestFiles = changedTests.length - testFiles.length;
293
317
  const sourceFiles = userChanged.filter((f) => SOURCE_RE.test(f));
294
318
  const ignoredOutsideSrc = sourceFiles.filter((f) => !isTestFile(f) && !f.startsWith('src/')).length;
295
- /** ⭐ **문서가 아닌 사용자 변경** — 이게 비어야 docs-only 다(소스 부재가 아니라 **문서 전부**여야 한다). */
296
- const documentPaths = userChanged.filter(isDocPath);
297
- const nonDoc = userChanged.filter((f) => !isDocPath(f));
319
+ /** ⭐ **문서가 아닌 사용자 변경** — 시험 파일도 명시적으로 docs-only 대상에서 제외한다. */
320
+ const documentPaths = userChanged.filter((f) => isDocPath(f) && !isTestFile(f));
321
+ const nonDoc = userChanged.filter((f) => isTestFile(f) || !isDocPath(f));
298
322
  /**
299
323
  * ⭐ **이 변경 중 무엇이 검증되지 않나** — 분기와 **무관하게** 계산한다(리뷰 must-fix 2026-07-27).
300
324
  *
@@ -323,6 +347,8 @@ export function resolveGateScope(
323
347
  if (runSet.includes(f)) return true;
324
348
  if (opts?.isDeleted?.(f)) return true;
325
349
  if (importerTestIndex && (importerTestIndex.testsBySource.get(f) ?? []).some((t) => runSet.includes(t))) return true;
350
+ if (PAIRED_REGISTRY_TESTS.registries.some((registry) => registry === f)
351
+ && PAIRED_REGISTRY_TESTS.tests.some((test) => runSet.includes(test))) return true;
326
352
  const related = relatedFor(f);
327
353
  return related.length > 0 && related.every((t) => runSet.includes(t));
328
354
  };
@@ -354,7 +380,9 @@ export function resolveGateScope(
354
380
  importerTestIndex?.testsBySource ?? new Map());
355
381
  const callerTests = candidates.slice(0, GATE_CALLER_TEST_LIMIT);
356
382
  const callerTestsOverflow = candidates.slice(GATE_CALLER_TEST_LIMIT);
357
- const runWithCallers = (tests: readonly string[]) => [...new Set([...tests, ...callerTests.map(({ file }) => file)])];
383
+ const pairedRegistryTests = userChanged.some((file) => PAIRED_REGISTRY_TESTS.registries.some((registry) => registry === file))
384
+ ? PAIRED_REGISTRY_TESTS.tests.filter(exists) : [];
385
+ const runWithCallers = (tests: readonly string[]) => [...new Set([...tests, ...callerTests.map(({ file }) => file), ...pairedRegistryTests])];
358
386
  const base = { sourceFiles, documentPaths, ignoredOutsideSrc, missingTestFiles, elanousRuntimeArtifacts, callerTests, callerTestsOverflow,
359
387
  unverified: [] as readonly string[], derived: [] as readonly string[], pulledInRelatedTests: [] as readonly string[] };
360
388
  const withDocumentNonContribution = (runSet: readonly string[]) => ({
@@ -381,8 +409,17 @@ export function resolveGateScope(
381
409
  const derived = conventional;
382
410
  const runSet = runWithCallers(derived);
383
411
  if (runSet.length > 0) {
384
- return { ...base, ...withDocumentNonContribution(runSet), derived, testArgs: runSet, unverified: unverifiedFor(runSet), importerTestsNotRun: observationFor(runSet), skipTestStep: false, reason: 'derived' };
412
+ return { ...base, ...withDocumentNonContribution(runSet), derived, testArgs: runSet, unverified: unverifiedFor(runSet), importerTestsNotRun: observationFor(runSet), skipTestStep: false, reason: derived.length === 0 && callerTests.length === 0 ? 'paired-registry' : 'derived' };
413
+ }
414
+ // 마지막 라운드에 짝이 없어도 원 PR의 앞 라운드에서 편집한 소스·시험을 같은 규칙으로 한 번 더 잰다.
415
+ if (opts?.runChanged?.some((file) => !changed.includes(file))) {
416
+ const wholeRun = resolveGateScope([...new Set([...changed, ...opts.runChanged])], exists, importerTestIndex,
417
+ { ...opts, runChanged: undefined });
418
+ return wholeRun;
385
419
  }
386
- // 실행 집합이 비었으므로 비문서 전부가 미검증이다.
387
- return { ...base, ...withDocumentNonContribution([]), unverified: unverifiedFor([]), importerTestsNotRun: observationFor([]), skipTestStep: true, reason: 'no-related-tests' };
420
+ const unverified = unverifiedFor([]);
421
+ try {
422
+ debug.log('self-implement.gate-scope', 'no-related-tests', { files: unverified, round: opts?.round ?? null }, { level: 'warn' });
423
+ } catch { /* 관측 장애는 게이트 판정을 바꾸지 않는다. */ }
424
+ return { ...base, ...withDocumentNonContribution([]), unverified, importerTestsNotRun: observationFor([]), skipTestStep: true, reason: 'no-related-tests' };
388
425
  }