elanous 0.2.12 → 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 (534) 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/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  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-1abfe3aec3be6bdc.js +1 -0
  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-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  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-793fa536b3a6ef37.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-e1e47d8d940aba3d.js +1 -0
  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-e297866051595d81.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/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,494 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { Database } from 'bun:sqlite';
6
+ import { debug } from '../debug/log.js';
7
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
8
+ import { decideSpawn } from '../loops/budget.js';
9
+ import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
10
+ import { getUserConfig } from '../user-config.js';
11
+ import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
12
+ import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
13
+ import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
14
+ import { writeHarnessQueueReceipt } from './harness-queue-child.js';
15
+
16
+ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
17
+ export type QueueItem = {
18
+ id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
19
+ at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string;
20
+ };
21
+ export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
22
+ export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
23
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
24
+ type ProcessProbe = {
25
+ run?: typeof spawnSync; platform?: NodeJS.Platform;
26
+ cwd?: (pid: number) => string;
27
+ seatTrees?: Partial<Record<QueueSeat, string[]>>;
28
+ runsDir?: string;
29
+ environ?: (pid: number) => string;
30
+ birthId?: (pid: number) => string | undefined;
31
+ };
32
+ const queueSeatNames = ['OP', 'TC', 'MK', 'UX'] as const;
33
+
34
+ export function queueSeatForCwd(cwd: string, trees: Partial<Record<QueueSeat, string[]>> = {}): QueueSeat | undefined {
35
+ if (!cwd.startsWith('/')) return undefined;
36
+ const current = resolve(cwd);
37
+ const matches = queueSeatNames.filter((s) => trees[s]?.some((tree) => {
38
+ if (!tree.startsWith('/')) return false;
39
+ const prefix = resolve(tree);
40
+ return current === prefix || current.startsWith(`${prefix}/`);
41
+ }));
42
+ return matches.length === 1 ? matches[0] : undefined;
43
+ }
44
+
45
+ export interface HarnessQueueDeps {
46
+ root?: string;
47
+ configPath?: string;
48
+ cap?: (seat: QueueSeat) => number;
49
+ pool?: () => QueuePool;
50
+ launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
51
+ alive?: (pid: number) => boolean;
52
+ processes?: () => readonly QueueProcess[];
53
+ receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
54
+ log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
55
+ }
56
+
57
+ export function harnessQueuePath(root = effectiveInstanceRoot()): string {
58
+ return join(root, 'harness', 'queue.json');
59
+ }
60
+
61
+ function observe(event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>, deps: HarnessQueueDeps): void {
62
+ try { (deps.log ?? ((name, payload) => debug.log('harness.queue', name, payload)))(event, data); }
63
+ catch { /* Logging cannot affect dispatch. */ }
64
+ }
65
+
66
+ function read(path: string): QueueItem[] {
67
+ if (!existsSync(path)) return [];
68
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
69
+ if (!Array.isArray(value) || !value.every((row) => row && typeof row === 'object'
70
+ && typeof row.id === 'string' && /^(OP|TC|MK|UX)$/.test(row.seat)
71
+ && (row.kind === 'say' || row.kind === 'ask') && typeof row.input === 'string'
72
+ && typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
73
+ && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
74
+ && (row.pid === undefined || Number.isSafeInteger(row.pid))
75
+ && (row.launchId === undefined || typeof row.launchId === 'string')
76
+ && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string'))) {
77
+ throw new Error('harness queue: invalid queue file (no launch)');
78
+ }
79
+ return value as QueueItem[];
80
+ }
81
+
82
+ function save(path: string, items: QueueItem[]): void {
83
+ const temporary = `${path}.${randomUUID()}.tmp`;
84
+ try {
85
+ writeFileSync(temporary, `${JSON.stringify(items, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
86
+ renameSync(temporary, path);
87
+ } catch (error) {
88
+ try { unlinkSync(temporary); } catch { /* write did not finish */ }
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ // SQLite OS locks release on process death, including death while awaiting a launch.
94
+ async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
95
+ const path = harnessQueuePath(root);
96
+ mkdirSync(join(root, 'harness'), { recursive: true, mode: 0o700 });
97
+ const db = new Database(`${path}.mutex.sqlite`, { create: true, strict: true });
98
+ try {
99
+ const deadline = Date.now() + 30_000;
100
+ for (;;) {
101
+ try { db.exec('BEGIN IMMEDIATE'); break; }
102
+ catch (error) {
103
+ if ((error as { code?: string }).code !== 'SQLITE_BUSY' || Date.now() >= deadline) throw error;
104
+ await Bun.sleep(25);
105
+ }
106
+ }
107
+ try { return await action(path); }
108
+ finally { db.exec('ROLLBACK'); }
109
+ } finally { db.close(); }
110
+ }
111
+
112
+ function seat(value: string): QueueSeat {
113
+ if (!/^(OP|TC|MK|UX)$/.test(value)) throw new Error(`harness queue: unknown seat ${value}`);
114
+ return value as QueueSeat;
115
+ }
116
+
117
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
118
+ const assigned = seat(input.seat);
119
+ if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
120
+ const kind = input.ask === undefined ? 'say' : 'ask';
121
+ const text = kind === 'say' ? input.say! : input.ask!;
122
+ if (!text.trim()) throw new Error('harness queue add: 빈 입력');
123
+ const value = kind === 'ask' ? resolve(text) : text;
124
+ if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
125
+ const root = deps.root ?? effectiveInstanceRoot();
126
+ return locked(root, async (path) => {
127
+ const rows = read(path);
128
+ if (input.idempotencyKey !== undefined) {
129
+ if (!input.idempotencyKey.trim()) throw new Error('harness queue add: empty idempotency key');
130
+ const existing = rows.find((row) => row.idempotencyKey === input.idempotencyKey
131
+ && (row.status === 'queued' || row.status === 'launching' || row.status === 'launched')
132
+ && queueRowOutcome(row, root) !== 'retryable');
133
+ if (existing) {
134
+ if (existing.seat !== assigned || existing.kind !== kind || existing.input !== value) throw new Error('harness queue: idempotency key collision');
135
+ return existing;
136
+ }
137
+ }
138
+ const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
139
+ hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
140
+ ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
141
+ save(path, [...rows, item]);
142
+ observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
143
+ return item;
144
+ });
145
+ }
146
+
147
+ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
148
+ return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
149
+ }
150
+
151
+ /** A missing row is not proof of failure unless the queue left a cancellation marker. */
152
+ export function harnessQueueOutcome(id: string, deps: HarnessQueueDeps = {}): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
153
+ const root = deps.root ?? effectiveInstanceRoot();
154
+ const row = listHarnessQueue(deps).find((item) => item.id === id);
155
+ if (!row) {
156
+ const marker = join(root, 'harness', `${id}.outcome`);
157
+ if (!existsSync(marker)) return 'unknown';
158
+ const value = readFileSync(marker, 'utf8');
159
+ return value === 'succeeded' || value === 'retryable' ? value : 'unknown';
160
+ }
161
+ return queueRowOutcome(row, root);
162
+ }
163
+
164
+ const keyMarkerPath = (root: string, key: string) => join(root, 'harness', 'by-key', `${createHash('sha256').update(key).digest('hex')}.id`);
165
+
166
+ function recordQueueOutcome(root: string, id: string, outcome: 'succeeded' | 'retryable', key?: string): void {
167
+ const path = join(root, 'harness', `${id}.outcome`);
168
+ if (!existsSync(path)) writeFileSync(path, outcome, { flag: 'wx', mode: 0o600 });
169
+ // The row is about to leave the queue; keep «which id last held this key» so a caller that crashed before
170
+ // recording the id can still find the outcome (LOOP-LIVE1 · seat loop recovery).
171
+ if (key) {
172
+ mkdirSync(join(root, 'harness', 'by-key'), { recursive: true });
173
+ writeFileSync(keyMarkerPath(root, key), id, { mode: 0o600 });
174
+ }
175
+ }
176
+
177
+ /** Last queue id that settled under this idempotency key, or undefined when none was recorded. */
178
+ export function harnessQueueIdForKey(key: string, deps: HarnessQueueDeps = {}): string | undefined {
179
+ try {
180
+ const id = readFileSync(keyMarkerPath(deps.root ?? effectiveInstanceRoot(), key), 'utf8').trim();
181
+ return /^hq-[0-9a-f-]{36}$/i.test(id) ? id : undefined;
182
+ } catch { return undefined; }
183
+ }
184
+
185
+ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
186
+ if (row.status === 'queued') return 'pending';
187
+ if (row.status === 'finished') {
188
+ const marker = join(root, 'harness', `${row.id}.outcome`);
189
+ if (existsSync(marker)) {
190
+ const value = readFileSync(marker, 'utf8');
191
+ if (value === 'succeeded' || value === 'retryable') return value;
192
+ }
193
+ }
194
+ if (!row.launchId) return 'unknown';
195
+ const receiptPath = harnessQueueReceiptPath(root, row.launchId);
196
+ if (!existsSync(receiptPath)) return 'pending';
197
+ const record: unknown = JSON.parse(readFileSync(receiptPath, 'utf8'));
198
+ if (!record || typeof record !== 'object') return 'unknown';
199
+ const { state, exitCode } = record as { state?: unknown; exitCode?: unknown };
200
+ if (state === 'not-started') return 'retryable';
201
+ if (state === 'finished') return exitCode === 0 ? 'succeeded' : typeof exitCode === 'number' ? 'retryable' : 'unknown';
202
+ return state === 'started' ? 'pending' : 'unknown';
203
+ }
204
+
205
+ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
206
+ const root = deps.root ?? effectiveInstanceRoot();
207
+ return locked(root, async (path) => {
208
+ const rows = read(path);
209
+ const item = rows.find((row) => row.id === id);
210
+ if (!item) return false;
211
+ if (item.status !== 'queued' && item.status !== 'finished') {
212
+ if ((item.status !== 'launched' && item.status !== 'launching') || !item.launchId) return false;
213
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
214
+ if (state !== 'finished' && state !== 'not-started'
215
+ || (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
216
+ }
217
+ const outcome = item.status === 'queued' ? 'retryable' : queueRowOutcome(item, root);
218
+ if (item.idempotencyKey && (outcome === 'retryable' || outcome === 'succeeded')) {
219
+ recordQueueOutcome(root, id, outcome, item.idempotencyKey);
220
+ }
221
+ save(path, rows.filter((row) => row.id !== id));
222
+ observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
223
+ return true;
224
+ });
225
+ }
226
+
227
+ export function queueLaunchArgs(item: QueueItem): string[] {
228
+ return ['harness', item.kind, item.input, '--substrate', 'pod',
229
+ ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
230
+ }
231
+
232
+ export function readHarnessQueuePool(): QueuePool {
233
+ const config = getUserConfig();
234
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
235
+ const current = spec ? null : leaseKubectl(['config', 'current-context']);
236
+ const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
237
+ if (!context) throw new Error('pod lease status: no pool/context');
238
+ const members = parsePodPool(context);
239
+ const observed = measurePoolLease(members);
240
+ if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
241
+ const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
242
+ const records = podPoolHostLease(members).live();
243
+ return {
244
+ running: observed.members.reduce((sum, member) => sum + member.running!, 0),
245
+ pending: observed.members.reduce((sum, member) => sum + member.pending!, 0),
246
+ reserved: hostLeaseCounts(records).reserved + records.filter((row) => row.stage === 'job' && !leaseHasPendingPod(row, pendingJobs)).length,
247
+ limit: members.reduce((sum, member) => sum + member.capacity, 0),
248
+ };
249
+ }
250
+
251
+ function alive(pid: number): boolean {
252
+ try { process.kill(pid, 0); return true; } catch { return false; }
253
+ }
254
+
255
+ /** Host inventory includes harness authoring before it has a run ledger. Unattributed processes count for every seat. */
256
+ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly QueueProcess[] {
257
+ const run = probe.run ?? spawnSync;
258
+ const platform = probe.platform ?? process.platform;
259
+ const result = run('ps', ['-eo', 'pid=,ppid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
260
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
261
+ const candidates: { pid: number; ppid: number; command: string }[] = [];
262
+ for (const line of result.stdout.split('\n')) {
263
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
264
+ if (!match) {
265
+ if (/(?:elanous\.mjs|harness-queue-child\.ts)/.test(line)) throw new Error('invalid harness process inventory row');
266
+ continue;
267
+ }
268
+ const command = match[3]!;
269
+ if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
270
+ || !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
271
+ || !/(?:\bharness\s+(?:say|ask)\b|\bdev\s+.*(?:--say|--ask|--file|--implement)\b|\bself\s+(?:implement|orchestrate)\b|harness-queue-child\.ts)/.test(command)) continue;
272
+ const pid = Number(match[1]), ppid = Number(match[2]);
273
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(ppid) || ppid < 0) throw new Error('invalid harness pid');
274
+ candidates.push({ pid, ppid, command });
275
+ }
276
+ const byPid = new Map(candidates.map((row) => [row.pid, row]));
277
+ const children = new Map<number, typeof candidates>();
278
+ for (const candidate of candidates) {
279
+ const siblings = children.get(candidate.ppid) ?? [];
280
+ siblings.push(candidate);
281
+ children.set(candidate.ppid, siblings);
282
+ }
283
+ const trees = probe.seatTrees ?? getUserConfig().loops?.orchestrator?.seatTrees;
284
+ const ledgerSeats = new Map<number, QueueSeat>();
285
+ const ambiguousPids = new Set<number>();
286
+ const candidatePids = new Set(candidates.map(({ pid }) => pid));
287
+ const births = new Map<number, string | undefined>();
288
+ for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
289
+ const pid = record.pid;
290
+ if (!pid || !candidatePids.has(pid) || !record.pidStart || !queueSeatNames.some((name) => name === record.seat)) continue;
291
+ if (!births.has(pid)) {
292
+ try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
293
+ catch { births.set(pid, undefined); }
294
+ }
295
+ if (births.get(pid) !== record.pidStart || ambiguousPids.has(pid)) continue;
296
+ const prior = ledgerSeats.get(pid);
297
+ if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
298
+ else ledgerSeats.set(pid, record.seat!);
299
+ }
300
+ const rows: QueueProcess[] = [];
301
+ // Attribute a run after grouping: the orchestrator may switch working trees while its launcher stays in HQ.
302
+ for (const root of candidates.filter((row) => !byPid.has(row.ppid))) {
303
+ const members: { process: typeof root; depth: number }[] = [];
304
+ const visit = (process: typeof root, depth: number): void => {
305
+ members.push({ process, depth });
306
+ for (const child of children.get(process.pid) ?? []) visit(child, depth + 1);
307
+ };
308
+ visit(root, 0);
309
+ const attributed: { seat: QueueSeat; depth: number; source: 'env' | 'flag' | 'ledger' | 'cwd'; priority: number }[] = [];
310
+ const worktrees: { seat: QueueSeat; depth: number }[] = [];
311
+ let launchId: string | undefined;
312
+ for (const { process: { pid, command }, depth } of members) {
313
+ let envSeat: string | undefined;
314
+ if (platform === 'linux') {
315
+ try {
316
+ const env = (probe.environ ?? ((id: number) => readFileSync(`/proc/${id}/environ`, 'utf8')))(pid).split('\0');
317
+ envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
318
+ launchId ??= env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
319
+ } catch { /* Unknown attribution is charged to all seats. */ }
320
+ }
321
+ const flag = /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1] as QueueSeat | undefined;
322
+ const envAssigned = envSeat && /^(OP|TC|MK|UX)$/.test(envSeat) ? envSeat as QueueSeat : undefined;
323
+ if (envAssigned) attributed.push({ seat: envAssigned, depth, source: 'env', priority: 0 });
324
+ if (flag) attributed.push({ seat: flag, depth, source: 'flag', priority: 1 });
325
+ const fromLedger = ledgerSeats.get(pid);
326
+ if (fromLedger) attributed.push({ seat: fromLedger, depth, source: 'ledger', priority: 2 });
327
+ if (!trees || !queueSeatNames.some((s) => trees[s]?.length)) continue;
328
+ try {
329
+ let cwd: string;
330
+ if (probe.cwd) cwd = probe.cwd(pid);
331
+ else if (platform === 'linux') cwd = readlinkSync(`/proc/${pid}/cwd`);
332
+ else {
333
+ const lsof = run('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 5_000 });
334
+ if (lsof.error || lsof.status !== 0 || typeof lsof.stdout !== 'string') throw new Error('cwd unavailable');
335
+ cwd = lsof.stdout.split('\n').find((line) => line.startsWith('n/'))?.slice(1) ?? '';
336
+ }
337
+ if (!cwd.startsWith('/')) continue;
338
+ const found = queueSeatForCwd(cwd, trees);
339
+ if (found) worktrees.push({ seat: found, depth });
340
+ } catch { /* Unknown attribution is charged to all seats. */ }
341
+ }
342
+ const deepest = Math.max(...members.map((entry) => entry.depth));
343
+ const inferred = new Set(worktrees.filter((entry) => entry.depth === deepest).map((entry) => entry.seat));
344
+ const complete = worktrees.filter((entry) => entry.depth === deepest).length
345
+ === members.filter((entry) => entry.depth === deepest).length;
346
+ if (complete && inferred.size === 1) attributed.push({ seat: [...inferred][0]!, depth: deepest, source: 'cwd', priority: 3 });
347
+ const priority = Math.min(...attributed.map((entry) => entry.priority));
348
+ const eligible = attributed.filter((entry) => entry.priority === priority);
349
+ const depth = Math.max(...eligible.map((entry) => entry.depth));
350
+ const top = eligible.filter((entry) => entry.depth === depth);
351
+ const seats = new Set(top.map((entry) => entry.seat));
352
+ const assigned = seats.size === 1 ? top[0] : undefined;
353
+ try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
354
+ catch { /* Observation does not block inventory. */ }
355
+ rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}) });
356
+ }
357
+ return rows;
358
+ }
359
+
360
+ export function harnessQueueReceiptPath(root: string, launchId: string): string {
361
+ if (!/^hq-[0-9a-f-]{36}$/.test(launchId)) throw new Error('invalid queue launch id');
362
+ return join(root, 'harness', `${launchId}.receipt.json`);
363
+ }
364
+
365
+ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-started' | null {
366
+ try {
367
+ const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { state?: string };
368
+ return record.state === 'started' || record.state === 'finished' || record.state === 'not-started' ? record.state : null;
369
+ } catch (error) {
370
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
376
+ const repo = resolve(import.meta.dir, '../..');
377
+ const logPath = join(root, 'harness', `${item.id}.log`);
378
+ const fd = openSync(logPath, 'a', 0o600);
379
+ try {
380
+ const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
381
+ harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
382
+ ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
383
+ cwd: repo, detached: true, stdio: ['ignore', fd, fd],
384
+ env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
385
+ ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
386
+ });
387
+ const pid = await new Promise<number>((done, reject) => {
388
+ child.once('spawn', () => done(child.pid!));
389
+ child.once('error', (error) => {
390
+ try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
391
+ catch { /* A failed receipt write leaves the reservation safely indeterminate. */ }
392
+ reject(error);
393
+ });
394
+ });
395
+ child.unref();
396
+ return pid;
397
+ } finally { closeSync(fd); }
398
+ }
399
+
400
+ /** Only a wrapper receipt proving no active child can clear an uncertain reservation. */
401
+ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<'released' | 'running' | 'unknown'> {
402
+ const root = deps.root ?? effectiveInstanceRoot();
403
+ return locked(root, async (path) => {
404
+ const rows = read(path);
405
+ const item = rows.find((row) => row.id === id);
406
+ if (!item || !['launching', 'launched'].includes(item.status) || !item.launchId) return 'unknown';
407
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
408
+ const processes = (deps.processes ?? readHarnessQueueProcesses)();
409
+ if (processes.some((row) => row.launchId === item.launchId)
410
+ || (item.pid !== undefined && (deps.alive ?? alive)(item.pid) && state !== 'finished' && state !== 'not-started')) return 'running';
411
+ if (state !== 'finished' && state !== 'not-started') return 'unknown';
412
+ // A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
413
+ // Keep completed launches in the ledger, but never charge their seat again.
414
+ const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(item, root);
415
+ if (item.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, id, outcome, item.idempotencyKey);
416
+ save(path, state === 'finished' && item.status === 'launched'
417
+ ? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
418
+ : rows.filter((row) => row.id !== id));
419
+ observe('skipped', { id, seat: item.seat, reason: `reconciled ${state}`, launchId: item.launchId }, deps);
420
+ return 'released';
421
+ });
422
+ }
423
+
424
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
425
+ const root = deps.root ?? effectiveInstanceRoot();
426
+ return locked(root, async (path) => {
427
+ const items = read(path);
428
+ const item = items.find((row) => row.status === 'queued');
429
+ if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
430
+ const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
431
+ const cap = (deps.cap ?? ((s: QueueSeat) => config.harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
432
+ if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
433
+ const caps = config.loops?.orchestrator?.seatCaps;
434
+ const gate = config.loops?.orchestrator?.releaseGate;
435
+ const attributed: Record<QueueSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
436
+ let unattributed = 0;
437
+ const wait = (reason: string, active: number): QueueTick => {
438
+ observe('waiting', { id: item.id, seat: item.seat, reason, active, cap, attributed, unattributed }, deps);
439
+ return { outcome: 'waiting', item, reason };
440
+ };
441
+ let processes: readonly QueueProcess[];
442
+ try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
443
+ catch (error) {
444
+ const budget = decideSpawn({ seat: item.seat, running: null, caps, gate });
445
+ return wait(`${budget.reason}: harness process inventory unavailable: ${String(error)}`, 0);
446
+ }
447
+ for (const row of processes) {
448
+ if (row.seat) attributed[row.seat]++;
449
+ else unattributed++;
450
+ }
451
+ const current = items.flatMap((row) => {
452
+ if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
453
+ const state = (deps.receipt ?? receipt)(root, row.launchId);
454
+ if (state !== 'finished' && state !== 'not-started'
455
+ || processes.some((process) => process.launchId === row.launchId)) return [row];
456
+ if (state === 'finished' && row.status === 'launched') return [{ ...row, status: 'finished' as const }];
457
+ const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(row, root);
458
+ if (row.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, row.id, outcome, row.idempotencyKey);
459
+ return [];
460
+ });
461
+ if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
462
+ const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
463
+ const launchIds = new Set(charged.map((row) => row.launchId));
464
+ const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
465
+ (row.status === 'launching' || row.status === 'launched')
466
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
467
+ if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
468
+ let pool: QueuePool;
469
+ try { pool = (deps.pool ?? readHarnessQueuePool)(); }
470
+ catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
471
+ if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
472
+ return wait('pod lease status incomplete', active);
473
+ }
474
+ const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
475
+ if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
476
+ return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
477
+ }
478
+ const budget = decideSpawn({ seat: item.seat, running: active, caps, gate });
479
+ if (!budget.allow) return wait(`${budget.reason}: seat ${item.seat}: ${active}/${budget.cap}`, active);
480
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
481
+ save(path, current.map((row) => row.id === item.id ? launching : row));
482
+ try {
483
+ const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
484
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
485
+ const launched: QueueItem = { ...launching, pid, status: 'launched' };
486
+ save(path, current.map((row) => row.id === item.id ? launched : row));
487
+ observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
488
+ return { outcome: 'launched', item: launched, reason: 'spawned' };
489
+ } catch (error) {
490
+ observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
491
+ throw error;
492
+ }
493
+ });
494
+ }
@@ -0,0 +1,139 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { debug } from '../debug/log.js';
3
+
4
+ export type StoppedPrCategory = 'review-budget' | 'no-progress' | 'environment-gap' | 'memo-not-reflected' | 'review-oscillation' | 'goal-revision' | 'report-deficit' | 'unknown';
5
+ export type StoppedPrClassification = {
6
+ category: StoppedPrCategory;
7
+ signals: string[];
8
+ firstAction: string;
9
+ blockers: string[];
10
+ blockerCount: number;
11
+ hasPublicExportLeak: boolean;
12
+ };
13
+
14
+ function section(body: string, title: string): string {
15
+ const heading = `## ${title}`;
16
+ const lines = body.split(/\r?\n/);
17
+ const start = lines.findIndex((line) => line.trim() === heading);
18
+ if (start < 0) return '';
19
+ const end = lines.findIndex((line, i) => i > start && /^##\s+/.test(line));
20
+ return lines.slice(start + 1, end < 0 ? undefined : end).join('\n');
21
+ }
22
+
23
+ function field(text: string, key: string): string {
24
+ const line = text.split(/\r?\n/).find((entry) => new RegExp(`^\\s*(?:[-*]\\s*)?(?:\\*\\*)?${key}(?:\\*\\*)?\\s*[::]\\s*`, 'i').test(entry));
25
+ return line?.replace(new RegExp(`^\\s*(?:[-*]\\s*)?(?:\\*\\*)?${key}(?:\\*\\*)?\\s*[::]\\s*`, 'i'), '').trim().replace(/^`|`$/g, '') ?? '';
26
+ }
27
+
28
+ export function classifyStoppedPr({ body, comments }: { body: string; comments: readonly (string | { body: string })[] }): StoppedPrClassification {
29
+ const stop = section(body, '중단 사유');
30
+ const classification = section(body, '중단 원인 분류');
31
+ const gate = section(body, 'Gate');
32
+ const reason = field(stop, 'reason');
33
+ const verdict = field(stop, 'verdict');
34
+ const label = field(classification, 'classification');
35
+ const basis = field(classification, 'classificationBasis');
36
+ const commentText = comments.map((comment) => typeof comment === 'string' ? comment : comment.body);
37
+ const signals = [reason, verdict, label, basis].filter(Boolean);
38
+ const allBlockers = gate.split(/\r?\n/).filter((line) => /^\s*(?:✗\s+|\[test\]\s+FAIL\b)/i.test(line));
39
+ const blockers = allBlockers.slice(0, 5);
40
+ const publicExportLeak = allBlockers.find((line) => /^\s*✗\s+public-export-leak\b/i.test(line));
41
+ if (publicExportLeak && !blockers.includes(publicExportLeak)) signals.push(publicExportLeak);
42
+ const failedTests = allBlockers.filter((line) => /^\s*\[test\]\s+FAIL\b.*[—–-](?:\s*\d+\s+(?:pass|fail|skip|errors?)\b\s*[,|])*\s*[1-9]\d*\s+errors?\b/i.test(line));
43
+ const failedPwaTest = failedTests.find((line) => /\bapps\/pwa\/[^\s]*\.test\.tsx?\b/i.test(line));
44
+ if (failedPwaTest) signals.push(failedPwaTest);
45
+ const hostRetest = /(?:^|;\s*)host=[^;\n]+;\s*cause=(?:missing (?:node_modules|dependency|environment variable)|호스트 의존성 없음)[^;\n]*;\s*retest=PASS bun test (\S+\.test\.tsx?)(?:\s*(?:;|$))/i.exec(basis);
46
+ // A code error in the Gate outranks any written basis: a basis string alone never turns a code failure into an environment gap.
47
+ const codeErrorInGate = /\b(?:TypeError|ReferenceError|SyntaxError|RangeError)\b|expect\(received\)/.test(gate);
48
+ const hostFailureConfirmed = !codeErrorInGate && Boolean(hostRetest && failedTests.some((line) => {
49
+ const failedPath = /^\s*\[test\]\s+FAIL\s+bun test (\S+\.test\.tsx?)\s+[—–-]/i.exec(line)?.[1];
50
+ return failedPath === hostRetest?.[1];
51
+ }));
52
+ const reviews = commentText.flatMap((text) => {
53
+ const round = Number(/<!--\s*elanous-pr-comment v1 role=reviewer round=(\d+)\b/.exec(text)?.[1] ?? /\bRound\s+(\d+):\s*reviewer requested\b/i.exec(text)?.[1] ?? -1);
54
+ const mustFix = /\bRound\s+\d+:\s*reviewer requested\s+(\d+)\s+must-fix change\(s\)/i.exec(text);
55
+ return mustFix ? [{ round, count: Number(mustFix[1]), evidence: mustFix[0] }] : [];
56
+ }).sort((a, b) => b.round - a.round);
57
+ const latest = reviews[0];
58
+ if (latest) signals.push(latest.evidence);
59
+
60
+ const stopEvidence = `${reason}\n${verdict}\n${label}\n${basis}\n${body.match(/정지\((?:needs-human|no-progress)\)/g)?.join('\n') ?? ''}`;
61
+ let category: StoppedPrCategory = 'unknown';
62
+ let action = '하지 않음 — 정지 원인 근거 부족';
63
+ if (hostFailureConfirmed && failedTests.length > 0 && (/(?:자식\s*무관|environment(?:-gap|al)?|환경\s*결손)/i.test(`${label}\n${basis}`) || failedPwaTest)) {
64
+ category = 'environment-gap';
65
+ action = '그래프 인스턴스 성형 — 호스트에서 실패 시험만 재측; 원 Gate 보존';
66
+ } else if (/(?:메모\s*미반영|memo[- ]not[- ]reflected)/i.test(`${label}\n${basis}\n${reason}`)) {
67
+ category = 'memo-not-reflected';
68
+ action = '안내 메모 — 동일 칸의 살아 있는 런과 다음 산출 대조; 대상 불명이면 하지 않음';
69
+ } else if (/(?:리뷰\s*진동|설계\s*역행|review[- ]oscillation)/i.test(`${label}\n${basis}\n${reason}`)) {
70
+ category = 'review-oscillation';
71
+ action = latest?.count ? '헬퍼 자식 spawn — 기존 시험 설계 보존 후 한 라운드' : '하지 않음 — 구체적인 must-fix 없음';
72
+ } else if (label === 'goal-unconvergeable-candidate' || basis === 'supervisor-unconvergeable-goal-candidate') {
73
+ category = 'goal-revision';
74
+ signals.push(label === 'goal-unconvergeable-candidate'
75
+ ? `classification=${label} → goal-revision`
76
+ : `classificationBasis=${basis} → goal-revision`);
77
+ action = '골 개정 — 골 문면(판정 신호·경계)을 고쳐 재발사; 구현 자식 붙이지 않음';
78
+ } else if (label === 'report-deficit') {
79
+ category = 'report-deficit';
80
+ signals.push(`classification=${label} → report-deficit`);
81
+ action = '증거 재측 — 인용된 판정 명령을 호스트에서 다시 돌려 결과를 PR 에 남김; 코드 수정 아님';
82
+ } else if (/review-budget/i.test(stopEvidence) || basis === 'must-fix-reported') {
83
+ // Real supervisor bodies (UNCONVERGEABLE · implementation-deficit) carry `classificationBasis: must-fix-reported` instead of the RFC word.
84
+ if (basis === 'must-fix-reported') signals.push('classificationBasis=must-fix-reported → review-budget');
85
+ category = 'review-budget';
86
+ action = latest?.count ? '헬퍼 자식 spawn — must-fix 원문과 기존 시험 설계로 한 라운드' : '하지 않음 — must-fix 0 또는 미관측; 수리 자식 대상 아님';
87
+ } else if (/no-progress/i.test(stopEvidence) || basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') {
88
+ if (basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') signals.push(`classificationBasis=${basis} → no-progress`);
89
+ category = 'no-progress';
90
+ const green = /\[test\]\s+PASS\b/.test(gate) && !/\[test\]\s+FAIL\b/.test(gate);
91
+ const documentGoal = /(?:문서\s*(?:골|목표)|docs?\/|RFC)/i.test(`${reason}\n${basis}\n${body.split(/\r?\n/)[0] ?? ''}`);
92
+ action = green && latest?.count === 0 && documentGoal
93
+ ? '중간 착지 ⊕ 연결 골 — must-fix 0 · 시험 초록 · 문서 골이면 잔여 확인; 잔여 0이면 연결 골 하지 않음'
94
+ : '중간 착지 ⊕ 연결 골 — 녹색 조각만 확인; 불가분 골·실패 Gate 착지 하지 않음';
95
+ }
96
+ return {
97
+ category, signals,
98
+ firstAction: allBlockers.length
99
+ ? `Gate 차단 먼저${publicExportLeak ? ' (✗ public-export-leak 확인)' : category === 'environment-gap' ? ' (그래프 인스턴스 성형으로 호스트 실패 시험 재측)' : ''}`
100
+ : action,
101
+ blockers, blockerCount: allBlockers.length, hasPublicExportLeak: Boolean(publicExportLeak),
102
+ };
103
+ }
104
+
105
+ /** Stopped means the supervisor wrote a stop verdict or reason into the `## 중단 사유` section — a quoted heading or stop wording elsewhere in the body (goal text, templates) does not count. */
106
+ export function isStoppedPrBody(body: string): boolean {
107
+ const stop = section(body, '중단 사유');
108
+ return Boolean(field(stop, 'verdict') || field(stop, 'reason'));
109
+ }
110
+
111
+ export type ScannedStoppedPr = StoppedPrClassification & { pr: number; headRefName: string; isDraft: boolean };
112
+ export type RunGh = (args: readonly string[]) => string | Promise<string>;
113
+
114
+ export async function scanStoppedPrs({ runGh }: { runGh: RunGh }): Promise<ScannedStoppedPr[]> {
115
+ const prs = JSON.parse(await runGh(['pr', 'list', '--state', 'open', '--limit', '100000', '--json', 'number,headRefName,isDraft,body'])) as Array<{ number: number; headRefName: string; isDraft: boolean; body: string }>;
116
+ const results: ScannedStoppedPr[] = [];
117
+ for (const pr of prs) {
118
+ if (!pr.headRefName.startsWith('self-impl/') || !isStoppedPrBody(pr.body)) continue;
119
+ const data = JSON.parse(await runGh(['pr', 'view', String(pr.number), '--json', 'comments'])) as { comments: Array<{ body: string }> };
120
+ const result = classifyStoppedPr({ body: pr.body, comments: data.comments });
121
+ debug.log('harness.helper', 'classified', { pr: pr.number, category: result.category, firstAction: result.firstAction, blockers: result.blockers, blockerCount: result.blockerCount, hasPublicExportLeak: result.hasPublicExportLeak });
122
+ results.push({ pr: pr.number, headRefName: pr.headRefName, isDraft: pr.isDraft, ...result });
123
+ }
124
+ return results;
125
+ }
126
+
127
+ if (import.meta.main) {
128
+ if (process.argv.slice(2).some((arg) => arg !== '--json')) {
129
+ console.error('사용법: bun src/harness/helper-scan.ts [--json]');
130
+ process.exitCode = 1;
131
+ } else {
132
+ const rows = await scanStoppedPrs({ runGh: (args) => execFileSync('gh', [...args], { encoding: 'utf8' }) });
133
+ if (process.argv.includes('--json')) console.log(JSON.stringify(rows, null, 2));
134
+ else for (const row of rows) {
135
+ const gate = row.blockerCount ? ` · Gate 차단 ${row.blockerCount}(${row.hasPublicExportLeak ? row.blockers.find((line) => /public-export-leak/i.test(line))?.trim() ?? '✗ public-export-leak (6번째 이후)' : row.blockers[0]!.trim()})` : '';
136
+ console.log(`#${row.pr} ${row.category}${gate} → ${row.firstAction}`);
137
+ }
138
+ }
139
+ }