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
@@ -1,10 +1,12 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { randomUUID } from 'node:crypto';
3
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { Database } from 'bun:sqlite';
6
6
  import { debug } from '../debug/log.js';
7
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';
8
10
  import { getUserConfig } from '../user-config.js';
9
11
  import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
10
12
  import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
@@ -14,14 +16,35 @@ import { writeHarnessQueueReceipt } from './harness-queue-child.js';
14
16
  export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
15
17
  export type QueueItem = {
16
18
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
17
- at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string;
19
+ at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string;
18
20
  };
19
21
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
20
22
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
21
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
+ }
22
44
 
23
45
  export interface HarnessQueueDeps {
24
46
  root?: string;
47
+ configPath?: string;
25
48
  cap?: (seat: QueueSeat) => number;
26
49
  pool?: () => QueuePool;
27
50
  launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
@@ -49,7 +72,8 @@ function read(path: string): QueueItem[] {
49
72
  && typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
50
73
  && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
51
74
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
52
- && (row.launchId === undefined || typeof row.launchId === 'string'))) {
75
+ && (row.launchId === undefined || typeof row.launchId === 'string')
76
+ && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string'))) {
53
77
  throw new Error('harness queue: invalid queue file (no launch)');
54
78
  }
55
79
  return value as QueueItem[];
@@ -90,7 +114,7 @@ function seat(value: string): QueueSeat {
90
114
  return value as QueueSeat;
91
115
  }
92
116
 
93
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
117
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
94
118
  const assigned = seat(input.seat);
95
119
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
96
120
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -100,9 +124,21 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
100
124
  if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
101
125
  const root = deps.root ?? effectiveInstanceRoot();
102
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
+ }
103
138
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
104
- hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued' };
105
- save(path, [...read(path), item]);
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]);
106
142
  observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
107
143
  return item;
108
144
  });
@@ -112,6 +148,60 @@ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
112
148
  return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
113
149
  }
114
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
+
115
205
  export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
116
206
  const root = deps.root ?? effectiveInstanceRoot();
117
207
  return locked(root, async (path) => {
@@ -124,6 +214,10 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
124
214
  if (state !== 'finished' && state !== 'not-started'
125
215
  || (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
126
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
+ }
127
221
  save(path, rows.filter((row) => row.id !== id));
128
222
  observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
129
223
  return true;
@@ -159,31 +253,106 @@ function alive(pid: number): boolean {
159
253
  }
160
254
 
161
255
  /** Host inventory includes harness authoring before it has a run ledger. Unattributed processes count for every seat. */
162
- export function readHarnessQueueProcesses(): readonly QueueProcess[] {
163
- const result = spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
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 });
164
260
  if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
165
- const rows: QueueProcess[] = [];
261
+ const candidates: { pid: number; ppid: number; command: string }[] = [];
166
262
  for (const line of result.stdout.split('\n')) {
167
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
168
- if (!match) continue;
169
- const command = match[2]!;
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]!;
170
269
  if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
171
270
  || !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
172
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;
173
- const pid = Number(match[1]);
174
- if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('invalid harness pid');
175
- let envSeat: string | undefined;
176
- let envLaunch: string | undefined;
177
- if (process.platform === 'linux') {
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;
178
328
  try {
179
- const env = readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
180
- envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
181
- envLaunch = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
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 });
182
340
  } catch { /* Unknown attribution is charged to all seats. */ }
183
341
  }
184
- const assigned = envSeat ?? /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1];
185
- rows.push({ pid, ...(assigned && /^(OP|TC|MK|UX)$/.test(assigned) ? { seat: assigned as QueueSeat } : {}),
186
- ...(envLaunch ? { launchId: envLaunch } : {}) });
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 } : {}) });
187
356
  }
188
357
  return rows;
189
358
  }
@@ -242,6 +411,8 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
242
411
  if (state !== 'finished' && state !== 'not-started') return 'unknown';
243
412
  // A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
244
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);
245
416
  save(path, state === 'finished' && item.status === 'launched'
246
417
  ? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
247
418
  : rows.filter((row) => row.id !== id));
@@ -256,21 +427,36 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
256
427
  const items = read(path);
257
428
  const item = items.find((row) => row.status === 'queued');
258
429
  if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
259
- const cap = (deps.cap ?? ((s: QueueSeat) => getUserConfig().harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
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);
260
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;
261
437
  const wait = (reason: string, active: number): QueueTick => {
262
- observe('waiting', { id: item.id, seat: item.seat, reason, active, cap }, deps);
438
+ observe('waiting', { id: item.id, seat: item.seat, reason, active, cap, attributed, unattributed }, deps);
263
439
  return { outcome: 'waiting', item, reason };
264
440
  };
265
441
  let processes: readonly QueueProcess[];
266
442
  try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
267
- catch (error) { return wait(`harness process inventory unavailable: ${String(error)}`, 0); }
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
+ }
268
451
  const current = items.flatMap((row) => {
269
452
  if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
270
453
  const state = (deps.receipt ?? receipt)(root, row.launchId);
271
454
  if (state !== 'finished' && state !== 'not-started'
272
455
  || processes.some((process) => process.launchId === row.launchId)) return [row];
273
- return state === 'finished' && row.status === 'launched' ? [{ ...row, status: 'finished' as const }] : [];
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 [];
274
460
  });
275
461
  if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
276
462
  const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
@@ -289,6 +475,8 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
289
475
  if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
290
476
  return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
291
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);
292
480
  const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
293
481
  save(path, current.map((row) => row.id === item.id ? launching : row));
294
482
  try {
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ import { debug } from '../debug/log.js';
2
+
3
+ /** One observation per harness node execution or loop tick, not per LLM request. */
4
+ export type ModelInputObservationTarget = {
5
+ scope: 'harness-node' | 'loop-tick';
6
+ nodeKind: string;
7
+ graphId?: string;
8
+ runId?: string;
9
+ nodeId?: string;
10
+ tickId?: string;
11
+ };
12
+
13
+ export type ModelInputObservation = ModelInputObservationTarget & (
14
+ | { status: 'measured'; inputTokens: number }
15
+ | { status: 'unmeasured'; inputTokens: null; reason: 'usage-unavailable' | 'invalid-usage' }
16
+ );
17
+
18
+ /**
19
+ * Publish a single structured record at the execution boundary. Only provider-reported,
20
+ * nonnegative integer counts are measurements; missing/invalid usage is never a zero.
21
+ * The caller supplies the total across model calls for this execution, if available.
22
+ */
23
+ export function observeModelInputTokens(
24
+ target: ModelInputObservationTarget,
25
+ inputTokens?: number | null,
26
+ ): ModelInputObservation {
27
+ const observation: ModelInputObservation = inputTokens === null || inputTokens === undefined
28
+ ? { ...target, status: 'unmeasured', inputTokens: null, reason: 'usage-unavailable' }
29
+ : Number.isSafeInteger(inputTokens) && inputTokens >= 0
30
+ ? { ...target, status: 'measured', inputTokens }
31
+ : { ...target, status: 'unmeasured', inputTokens: null, reason: 'invalid-usage' };
32
+
33
+ try {
34
+ debug.log('harness.model-input', 'recorded', observation);
35
+ } catch { /* Logging must not interrupt the node or tick. */ }
36
+ return observation;
37
+ }
package/src/hq/hq.ts CHANGED
@@ -302,7 +302,7 @@ export function hqCliWriteAllowed(command: string, override = false, deps: HqDep
302
302
  }
303
303
  if (allowed) return true;
304
304
  observe(r, 'hq.fence', 'cli-refused', { command, reason: decision.reason, holder: decision.holder, generation: decision.generation });
305
- console.error(`본부는 ${decision.holder ?? 'unknown'} gen ${decision.generation ?? 'unknown'} — 거기서 쓰거나 --hq-override(관측)`);
305
+ console.error(`본부는 ${decision.holder ?? 'unknown'} gen ${decision.generation ?? 'unknown'} — 원장 쓰기는 지금 본부로 보내라 (수동 우회: --hq-override, 관측)`);
306
306
  process.exitCode = 4;
307
307
  return false;
308
308
  }