elanous 0.2.11 → 0.2.13

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 (342) 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/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
  56. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +14 -14
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +14 -14
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +14 -14
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +14 -14
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +14 -14
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +14 -14
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +14 -14
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +14 -14
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +14 -14
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +14 -14
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +14 -14
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +14 -14
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +14 -14
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +14 -14
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +14 -14
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +14 -14
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +14 -14
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +14 -14
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +15 -15
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +14 -14
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +14 -14
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +14 -14
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +14 -14
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +14 -14
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +14 -14
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +14 -14
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +14 -14
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +14 -14
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +14 -14
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +14 -14
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +14 -14
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +14 -14
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +14 -14
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +14 -14
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +14 -14
  142. package/examples/teams/personas/backend.yaml +4 -0
  143. package/examples/teams/personas/pm.yaml +4 -0
  144. package/examples/teams/personas/qa.yaml +4 -0
  145. package/examples/teams/small-product.yaml +20 -0
  146. package/graphs/context/nightly-condense.yaml +21 -0
  147. package/graphs/context/recipes.yaml +12 -0
  148. package/graphs/demo/inside-seed.yaml +20 -0
  149. package/graphs/demo/recipes.yaml +12 -0
  150. package/graphs/lecture/lecture-note.yaml +28 -0
  151. package/graphs/lecture/recipes.yaml +17 -0
  152. package/package.json +2 -1
  153. package/plugins/card-followup/examples/sample-card.png +0 -0
  154. package/scripts/context-hooks/emit.ts +36 -14
  155. package/scripts/coord-channel-watch.sh +1 -0
  156. package/scripts/discord-seat-probe.ts +144 -0
  157. package/scripts/hq/hq-standby.sh +65 -0
  158. package/scripts/hq/standby-promote.ts +173 -0
  159. package/scripts/hq/standby-role.ts +59 -0
  160. package/scripts/hq/standby-snapshot.ts +226 -0
  161. package/scripts/hq/standby-verify.ts +219 -0
  162. package/scripts/lecture-note/node.ts +256 -0
  163. package/scripts/lib/telegram-inject-args.ts +31 -0
  164. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  165. package/scripts/release-loop/auto-approve-node.ts +1 -1
  166. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  167. package/scripts/release-loop/gate-node.ts +61 -17
  168. package/scripts/release-loop/ops-upgrade-node.ts +59 -6
  169. package/scripts/release-loop/publish-node.ts +4 -1
  170. package/scripts/release-loop/version-node.ts +13 -3
  171. package/scripts/telegram-inject.ts +14 -6
  172. package/scripts/test-deterministic.ts +53 -3
  173. package/scripts/test-diet/remeasure-list.ts +112 -0
  174. package/scripts/ux/demo-rehearsal.ts +17 -13
  175. package/scripts/ux/demo-seed.ts +108 -0
  176. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  177. package/skills/omni-crawl/SKILL.md +16 -16
  178. package/skills/omni-crawl/package-lock.json +1 -544
  179. package/skills/omni-crawl/package.json +3 -4
  180. package/skills/omni-crawl/scripts/main.ts +13 -13
  181. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  182. package/skills/omni-crawl/src/apify.ts +13 -2
  183. package/skills/omni-crawl/src/capture.ts +6 -0
  184. package/skills/omni-crawl/src/registry.ts +2 -2
  185. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  186. package/skills/omni-digest/SKILL.md +10 -11
  187. package/skills/omni-digest/package-lock.json +0 -545
  188. package/skills/omni-digest/package.json +2 -5
  189. package/skills/omni-digest/scripts/main.ts +3 -3
  190. package/skills/omni-digest/src/delegate.ts +5 -5
  191. package/skills/omni-digest/src/fetch.ts +2 -2
  192. package/skills/omni-digest/src/media.ts +25 -11
  193. package/skills/omni-digest/src/visual.ts +3 -2
  194. package/skills/omni-market/scripts/main.ts +1 -1
  195. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  196. package/src/agent/identity-env.ts +11 -0
  197. package/src/agent-mission/driver.ts +150 -23
  198. package/src/agent-mission/duo.ts +94 -0
  199. package/src/agent-mission/pty-decision.ts +2 -2
  200. package/src/autopilot/pty-control-loop.ts +4 -1
  201. package/src/autopilot/track-agent-shadow.ts +2 -1
  202. package/src/autopilot/track-agent.ts +4 -1
  203. package/src/boot/daemon-prompt-request.ts +3 -0
  204. package/src/boot/daemon-prompt-turn.ts +6 -2
  205. package/src/boot/daemon-runtime.ts +48 -16
  206. package/src/chat/index.ts +1 -0
  207. package/src/cli/decisions-cli.ts +51 -19
  208. package/src/cli/outputs-cli.ts +92 -0
  209. package/src/cli/persona-cli.ts +45 -13
  210. package/src/cli/pod-cli.ts +27 -3
  211. package/src/cli/pod-memory-advice.ts +35 -0
  212. package/src/cli/pr-cli.ts +15 -1
  213. package/src/cli/release-cli.ts +135 -15
  214. package/src/context-bus/condense.ts +194 -0
  215. package/src/context-bus/context-day.ts +82 -0
  216. package/src/context-bus/context-now-surfaces.ts +32 -2
  217. package/src/context-bus/context-now.ts +2 -2
  218. package/src/context-bus/coord-events.ts +2 -2
  219. package/src/context-bus/long-term-memory.ts +118 -22
  220. package/src/context-bus/nightly-condense.ts +153 -0
  221. package/src/context-bus/session-events.ts +35 -0
  222. package/src/dashboard/dashboard-status-lines.ts +2 -0
  223. package/src/dashboard/index.ts +8 -0
  224. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  225. package/src/decisions/decision-cards.ts +13 -7
  226. package/src/decisions/decision-ledger.ts +43 -17
  227. package/src/domains/outbound-alert.ts +99 -31
  228. package/src/domains/outbound-log-probe.ts +8 -0
  229. package/src/execution-loop/launch-gate.ts +17 -8
  230. package/src/field-note/telegram.ts +142 -0
  231. package/src/graph-kinds/registry.ts +2 -2
  232. package/src/graph-runner/graph-approval-card.ts +2 -2
  233. package/src/graph-runner/graph-grow-llm.ts +23 -13
  234. package/src/graph-runner/graph-grow.ts +69 -13
  235. package/src/graph-runner/graph-tick.ts +9 -2
  236. package/src/graph-runner/runner.ts +92 -24
  237. package/src/harness/harness-cli-command.ts +165 -17
  238. package/src/harness/harness-queue-child.ts +37 -0
  239. package/src/harness/harness-queue.ts +306 -0
  240. package/src/hooks/consult-intake.ts +153 -0
  241. package/src/hooks/github-shadow.ts +72 -2
  242. package/src/hooks/receiver.ts +82 -3
  243. package/src/hq/hq.ts +318 -0
  244. package/src/hq/lease.ts +210 -0
  245. package/src/hq/ledger-write-target.ts +41 -0
  246. package/src/index.ts +232 -16
  247. package/src/intake-plane/discord-seat-work.ts +36 -3
  248. package/src/intake-plane/linear-wish.ts +44 -0
  249. package/src/intake-plane/persona-answer.ts +5 -0
  250. package/src/intake-plane/telegram-seat-work.ts +13 -6
  251. package/src/intake-plane/wish-card.ts +33 -0
  252. package/src/llm/difficulty.ts +1 -1
  253. package/src/loops/checker.ts +178 -0
  254. package/src/loops/loop-cli.ts +37 -3
  255. package/src/loops/observe.ts +49 -0
  256. package/src/loops/orchestrator/overlap.ts +141 -0
  257. package/src/loops/orchestrator/traffic.ts +150 -0
  258. package/src/loops/package/verify.ts +125 -0
  259. package/src/loops/verdict.ts +22 -0
  260. package/src/maturity/feature-maturity.ts +5 -4
  261. package/src/memory.ts +37 -4
  262. package/src/nexus/api/card-followup-route.ts +118 -0
  263. package/src/nexus/api/decisions-route.ts +68 -0
  264. package/src/nexus/api/http-server.ts +78 -8
  265. package/src/nexus/api/outbound-report.ts +40 -7
  266. package/src/nexus/api/personas.ts +97 -54
  267. package/src/nexus/api/skill-exec-route.ts +97 -0
  268. package/src/nexus/api/task-cards-api.ts +19 -0
  269. package/src/nexus/api/workflow-history.ts +88 -0
  270. package/src/nexus/api/workflows.ts +52 -0
  271. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  272. package/src/nexus/outbound/router.ts +8 -3
  273. package/src/persona/loader.ts +18 -0
  274. package/src/persona/persona-todo.ts +14 -9
  275. package/src/persona/presets.ts +10 -2
  276. package/src/persona/team.ts +105 -0
  277. package/src/persona/types.ts +4 -0
  278. package/src/plugins/maker/market-bundle.ts +2 -1
  279. package/src/pod-lease/host-lease.ts +130 -20
  280. package/src/release-loop/checklist.ts +15 -3
  281. package/src/release-loop/feature-store.ts +24 -10
  282. package/src/release-loop/landed-but-yellow.ts +60 -0
  283. package/src/seat-dispatch/seat-ask.ts +57 -12
  284. package/src/seat-loop/seat-loop.ts +253 -18
  285. package/src/seat-loop/seat-pack.ts +127 -0
  286. package/src/self-dev/ask-launch-flow.ts +3 -0
  287. package/src/self-dev/draft-sweep.ts +44 -10
  288. package/src/self-dev/draft-triage-rules.ts +54 -2
  289. package/src/self-implement/budget-gate.ts +29 -12
  290. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  291. package/src/self-implement/gate-scope.ts +9 -8
  292. package/src/self-implement/headless-elanous-driver.ts +7 -2
  293. package/src/self-implement/l8-merge-queue.ts +157 -1
  294. package/src/self-implement/orchestrator.ts +15 -0
  295. package/src/self-implement/test-seams.ts +1 -0
  296. package/src/steward/launch.ts +5 -1
  297. package/src/steward/readiness.ts +125 -0
  298. package/src/steward/rescue.ts +75 -0
  299. package/src/steward/triage.ts +48 -9
  300. package/src/task-cards/card-cli.ts +78 -0
  301. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  302. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  303. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
  305. package/src/telegram-commands.ts +21 -0
  306. package/src/telegram-dispatch-forward.ts +1 -1
  307. package/src/telegram-fabric-plan.ts +90 -0
  308. package/src/telegram-project-command.ts +84 -4
  309. package/src/telegram.ts +37 -5
  310. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  311. package/src/user-config.ts +101 -13
  312. package/src/version/packed-revision.json +1 -1
  313. package/src/workflow-runtime/executor.ts +4 -0
  314. package/src/workflow-runtime/node-catalog.ts +13 -0
  315. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  316. package/src/workflow-runtime/schema.ts +17 -1
  317. package/src/workflow-runtime/types.ts +13 -1
  318. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  340. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  341. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  342. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -24,11 +24,12 @@ import { debug } from '../debug/log.js';
24
24
  import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
25
25
  import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
26
26
  import { PR_LABELS } from '../github/pr-labels.js';
27
- import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
27
+ import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, decideLaunchBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
28
28
  import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
29
29
  import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
30
30
  import { getUserConfig } from '../user-config.js';
31
31
  import { installHarnessCliSinkHook } from './harness-cli-sink.js';
32
+ import { addHarnessQueue, listHarnessQueue, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps } from './harness-queue.js';
32
33
  import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
33
34
  import { runHarnessPlanRfc } from './harness-plan-rfc.js';
34
35
  import { classifyGarbage, isGarbageProcessTarget, type GarbageProcess } from './process-garbage.js';
@@ -338,6 +339,8 @@ function resolveLaunchSubstrate(opts: HarnessSubstrateOpts): ResolvedHarnessSubs
338
339
  }
339
340
  type PodExit = { status: number | null; signal?: string | null };
340
341
  type PodExitReason = 'signal' | 'human-stop' | 'supervisor' | 'no-launch' | 'unknown';
342
+ /** GitHub lists at most this many files for one pull request; a list that reaches it is not provably complete. */
343
+ export const GITHUB_PR_FILES_LIST_CAP = 3000;
341
344
  const STOPPING_SUPERVISOR_VERDICTS = new Set(['UNCONVERGEABLE', 'CONTRACT-CONFLICT']);
342
345
  // Ledger events that only exist once the child actually ran — any of them rules out «never reached the Pod».
343
346
  const LAUNCH_EVIDENCE_EVENTS = new Set(['job-applied', 'pod-child-run', 'reviewed', 'rework-budget', 'pr-opened']);
@@ -578,8 +581,12 @@ const LAUNCH_BUDGET_PROVIDER: Readonly<Record<string, string>> = { 'codex-rotate
578
581
  export function harnessLaunchBudgetDecision(
579
582
  inputs: BudgetInputs,
580
583
  opts: { readonly childLlmProvider?: string; readonly childLlmModel?: string } = {},
581
- ): { readonly decision: BudgetDecision; readonly warning?: string } {
584
+ ): { readonly decision: BudgetDecision; readonly warning?: string; readonly unmeasuredProvider?: string } {
582
585
  const explicit = opts.childLlmProvider?.trim();
586
+ const judged = (selected: BudgetInputs) => {
587
+ const { decision, unmeasuredProvider } = decideLaunchBudget(selected);
588
+ return { decision, ...(unmeasuredProvider ? { unmeasuredProvider, warning: `budget: ${unmeasuredProvider} usage unmeasured — launching` } : {}) };
589
+ };
583
590
  if (explicit) {
584
591
  if (explicit !== CODEX_PROVIDER && explicit !== GROK_PROVIDER) {
585
592
  return {
@@ -588,13 +595,14 @@ export function harnessLaunchBudgetDecision(
588
595
  };
589
596
  }
590
597
  const chain = [{ provider: explicit, ...(opts.childLlmModel ? { model: opts.childLlmModel } : {}) }];
591
- return { decision: decideBudget({ ...inputs, preference: { ...inputs.preference, chain } }) };
598
+ return judged({ ...inputs, preference: { ...inputs.preference, chain } });
592
599
  }
593
- if (inputs.preference.chain.length > 0) return { decision: decideBudget(inputs) };
600
+ if (inputs.preference.chain.length > 0) return judged(inputs);
594
601
  const chain = DEFAULT_FALLBACK_CHAIN.map((step) => ({ provider: LAUNCH_BUDGET_PROVIDER[step] ?? step }));
602
+ const result = judged({ ...inputs, preference: { ...inputs.preference, chain } });
595
603
  return {
596
- decision: decideBudget({ ...inputs, preference: { ...inputs.preference, chain } }),
597
- warning: `budget: no chain in this universe's config — judged with the code default ${DEFAULT_FALLBACK_CHAIN.join(',')}`,
604
+ ...result,
605
+ warning: `budget: no chain in this universe's config — judged with the code default ${DEFAULT_FALLBACK_CHAIN.join(',')}${result.warning ? ` · ${result.warning}` : ''}`,
598
606
  };
599
607
  }
600
608
 
@@ -602,10 +610,13 @@ async function readHarnessLaunchBudget(
602
610
  opts: { readonly childLlmProvider?: string; readonly childLlmModel?: string } = {},
603
611
  ): Promise<BudgetDecision | 'unknown'> {
604
612
  try {
605
- const { decision, warning } = harnessLaunchBudgetDecision(await readBudgetInputsLive(), opts);
613
+ const { decision, warning, unmeasuredProvider } = harnessLaunchBudgetDecision(await readBudgetInputsLive(), opts);
606
614
  if (warning) {
607
615
  console.error(`⚠️ launch gate: ${warning}`);
608
- try { debug.log('execution-loop.launch-gate', 'budget-chain-defaulted', { warning, action: decision.action, provider: decision.provider }); } catch { /* observation is fail-soft */ }
616
+ try {
617
+ if (unmeasuredProvider) debug.log('execution-loop.launch-gate', 'budget-unmeasured', { provider: unmeasuredProvider });
618
+ if (!opts.childLlmProvider?.trim() && warning.includes('code default')) debug.log('execution-loop.launch-gate', 'budget-chain-defaulted', { warning, action: decision.action, provider: decision.provider });
619
+ } catch { /* observation is fail-soft */ }
609
620
  }
610
621
  return decision;
611
622
  }
@@ -644,8 +655,8 @@ async function dispatchHarnessAskSay(
644
655
  }
645
656
  goalId ??= launchRequestId(identity);
646
657
  // Under bun test with no injected gate deps, skip the live reads (≈5 s of codex rotation + run scans) —
647
- // CLI wiring tests time out on them. Gate tests inject deps and still run the gate (0.2.11 gate 10-03).
648
- if (process.env.NODE_ENV === 'test' && Object.keys(gate).length === 0) {
658
+ // CLI wiring tests time out on them. Gate tests inject deps or opt in with ELANOUS_LAUNCH_GATE_LIVE=1.
659
+ if (process.env.NODE_ENV === 'test' && Object.keys(gate).length === 0 && process.env.ELANOUS_LAUNCH_GATE_LIVE !== '1') {
649
660
  try { debug.log('execution-loop.launch-gate', 'skipped-test-env', { goalId }); } catch { /* observation is fail-soft */ }
650
661
  await dispatch(resolved);
651
662
  return;
@@ -1461,7 +1472,7 @@ export function readProcessOwnership(
1461
1472
  }
1462
1473
  }
1463
1474
 
1464
- function defaultListHarnessProcesses(): HarnessProcessListObservation {
1475
+ export function defaultListHarnessProcesses(): HarnessProcessListObservation {
1465
1476
  let out: string;
1466
1477
  try {
1467
1478
  out = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime=,command='], {
@@ -1577,10 +1588,13 @@ type GithubPull = {
1577
1588
  draft: boolean;
1578
1589
  state: 'open' | 'closed';
1579
1590
  head: { ref: string };
1591
+ base?: { ref: string };
1580
1592
  labels: Array<{ name: string }>;
1581
1593
  created_at: string;
1582
1594
  updated_at?: string;
1583
1595
  merged_at: string | null;
1596
+ body?: string | null;
1597
+ merge_commit_sha?: string | null;
1584
1598
  };
1585
1599
 
1586
1600
  type GhExecute = (args: string[]) => string;
@@ -1629,9 +1643,25 @@ export function draftSweepRunStatus(ledgerMatches: readonly RunLedgerMatch[], ru
1629
1643
  ['completed', 'failed', 'cancelled', 'abandoned'].includes(terminal) ? terminal : undefined);
1630
1644
  if (status && status !== 'unknown') statuses.add(status);
1631
1645
  }
1646
+ if (statuses.has('running')) return 'running';
1647
+ if (statuses.has('probable-running')) return 'probable-running';
1632
1648
  return statuses.size === 1 ? [...statuses][0] : undefined;
1633
1649
  }
1634
1650
 
1651
+ function currentDraftSweepRunId(matches: readonly RunLedgerMatch[], repository: string, number: number): string | undefined {
1652
+ const owners = matches.flatMap((match) => {
1653
+ const events = match.entries.filter((entry) => entry.event === 'pr-opened' && entry.data.number === number);
1654
+ if (events.length === 0 || events.some((entry) => ledgerPrRepository(entry.data, number) !== repository)) return [];
1655
+ const times = events.map((entry) => Date.parse(entry.timestamp ?? ''));
1656
+ return [{ runId: match.runId, time: times.every(Number.isFinite) ? Math.max(...times) : NaN }];
1657
+ });
1658
+ if (owners.length === 1) return owners[0]!.runId;
1659
+ if (owners.some((owner) => !Number.isFinite(owner.time))) return undefined;
1660
+ owners.sort((a, b) => b.time - a.time);
1661
+ if (owners.length > 1 && owners[0]!.time === owners[1]!.time) return undefined;
1662
+ return owners[0]?.runId;
1663
+ }
1664
+
1635
1665
  type DraftSweepGitExecute = (cwd: string, args: string[]) => Pick<ReturnType<typeof runGitCommand>, 'status' | 'stdout' | 'stderr'>;
1636
1666
 
1637
1667
  function githubRemoteRepository(url: string): string | undefined {
@@ -1669,24 +1699,117 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1669
1699
  if (!cache.has(repository)) cache.set(repository, inventory(repository, state));
1670
1700
  return cache.get(repository)!;
1671
1701
  };
1702
+ // GitHub stops listing a pull request's files at 3,000, and the cut looks like a normal last page.
1703
+ // A list that reaches the cap cannot prove it is complete, so it is reported as unknown (undefined):
1704
+ // a draft without a provable file list never qualifies for the all-files-landed close.
1705
+ const filesFor = (repository: string, number: number): string[] | undefined => {
1706
+ const files: string[] = [];
1707
+ for (let page = 1; page <= 100; page++) {
1708
+ const batch = ghJson<Array<{ filename: string }>>(['api', `repos/${repository}/pulls/${number}/files?per_page=100&page=${page}`], execute);
1709
+ if (!Array.isArray(batch) || batch.length > 100 || batch.some((file) => typeof file.filename !== 'string')) throw new Error('Incomplete PR file inventory');
1710
+ files.push(...batch.map((file) => file.filename));
1711
+ if (files.length >= GITHUB_PR_FILES_LIST_CAP) {
1712
+ debug.log('drafts.cleanup', 'file-inventory-capped', { number, listed: files.length });
1713
+ return undefined;
1714
+ }
1715
+ if (batch.length < 100) return files;
1716
+ }
1717
+ throw new Error('PR file inventory exceeded pagination limit');
1718
+ };
1719
+ const latestFileChangesFor = (draft: SweepDraft, repository: string): Record<string, string> | undefined => {
1720
+ if (!draft.changedFiles?.length) return undefined;
1721
+ const changes: Record<string, string> = {};
1722
+ for (let page = 1; page <= 100; page++) {
1723
+ const batch = ghJson<Array<{ sha: string; commit: { committer: { date: string } } }>>(
1724
+ ['api', `repos/${repository}/pulls/${draft.number}/commits?per_page=100&page=${page}`], execute);
1725
+ if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete draft commit inventory');
1726
+ for (const commit of batch) {
1727
+ const changedAt = commit.commit?.committer?.date;
1728
+ if (!commit.sha || !changedAt || !Number.isFinite(Date.parse(changedAt))) return undefined;
1729
+ const detail = ghJson<{ files?: Array<{ filename: string; previous_filename?: string }> }>(
1730
+ ['api', `repos/${repository}/commits/${commit.sha}?per_page=100`], execute);
1731
+ // GitHub truncates very large commit file lists; never infer coverage from a partial list.
1732
+ if (!Array.isArray(detail.files) || detail.files.length >= 100) return undefined;
1733
+ for (const file of detail.files) {
1734
+ for (const name of [file.filename, file.previous_filename]) {
1735
+ if (!name || !draft.changedFiles.includes(name)) continue;
1736
+ if (!changes[name] || Date.parse(changes[name]) < Date.parse(changedAt)) changes[name] = changedAt;
1737
+ }
1738
+ }
1739
+ }
1740
+ if (batch.length < 100) return draft.changedFiles.every((file) => changes[file]) ? changes : undefined;
1741
+ }
1742
+ return undefined;
1743
+ };
1744
+ const landingCommentsFor = (repository: string, number: number): string[] => {
1745
+ const comments: string[] = [];
1746
+ for (let page = 1; page <= 100; page++) {
1747
+ const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`], execute);
1748
+ if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete landing comment inventory');
1749
+ comments.push(...batch.filter((comment) => /\blanding-verified\b/i.test(comment.body ?? '')).map((comment) => comment.body!));
1750
+ if (batch.length < 100) return comments;
1751
+ }
1752
+ throw new Error('Landing comment inventory exceeded pagination limit');
1753
+ };
1672
1754
  let ledgerMatches: readonly RunLedgerMatch[] | undefined;
1673
1755
  let running: ReadonlyMap<string, string> | undefined;
1674
- const runStatusFor = (repository: string, number: number): string | undefined => {
1756
+ const runStatusFor = (draft: SweepDraft, repository: string): string | undefined => {
1675
1757
  ledgerMatches ??= listRunLedgers().matches;
1676
1758
  running ??= new Map(queryRunningRuns().entries.map((entry) => [entry.runId, entry.status]));
1677
- return draftSweepRunStatus(ledgerMatches, running, repository, number);
1759
+ if (draft.runId) {
1760
+ const match = ledgerMatches.find((entry) => entry.runId === draft.runId);
1761
+ return match ? draftSweepRunStatus([match], running, repository, draft.number) : undefined;
1762
+ }
1763
+ return draftSweepRunStatus(ledgerMatches, running, repository, draft.number);
1678
1764
  };
1679
1765
  return {
1680
1766
  listDrafts: async (page, perPage, repository): Promise<SweepDraft[]> =>
1681
1767
  (await listed(open, repository, 'open')).filter((pr) => pr.draft)
1682
1768
  .slice((page - 1) * perPage, page * perPage)
1683
1769
  .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref,
1684
- labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}) })),
1770
+ labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, body: pr.body ?? undefined,
1771
+ changedFiles: filesFor(repository, pr.number), ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
1772
+ runId: currentDraftSweepRunId(ledgerMatches ??= listRunLedgers().matches, repository, pr.number) })),
1685
1773
  listMerged: async (page, perPage, repository): Promise<SweepMergedPr[]> =>
1686
- (await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null)
1774
+ (await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null && pr.base?.ref === 'main')
1687
1775
  .slice((page - 1) * perPage, page * perPage)
1688
- .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref })),
1689
- getRunStatus: async (draft, repository) => runStatusFor(repository, draft.number),
1776
+ .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref, body: pr.body ?? undefined,
1777
+ mergedAt: pr.merged_at ?? undefined, changedFiles: filesFor(repository, pr.number),
1778
+ mergeCommitMessage: pr.merge_commit_sha ? ghJson<{ commit: { message: string } }>(
1779
+ ['api', `repos/${repository}/commits/${pr.merge_commit_sha}`], execute).commit.message : undefined,
1780
+ landingVerifiedComments: landingCommentsFor(repository, pr.number) })),
1781
+ getRunStatus: async (draft, repository) => runStatusFor(draft, repository),
1782
+ getLatestFileChanges: async (draft, repository) => latestFileChangesFor(draft, repository),
1783
+ hasFinalRunResult: async (draft, repository) => {
1784
+ ledgerMatches ??= listRunLedgers().matches;
1785
+ const ownedRuns = new Set(ledgerMatches.filter((match) => {
1786
+ const prEvents = match.entries.filter((entry) => entry.event === 'pr-opened' && entry.data.number === draft.number);
1787
+ return prEvents.length > 0 && prEvents.every((entry) => ledgerPrRepository(entry.data, draft.number) === repository);
1788
+ }).map((match) => match.runId));
1789
+ if (ownedRuns.size === 0) return false;
1790
+ // A previous owner's final does not establish finality for a later owner of this PR.
1791
+ if (draft.runId && !ownedRuns.has(draft.runId)) return false;
1792
+ if (!draft.runId && ownedRuns.size > 1) return false;
1793
+ const currentOwners = draft.runId ? new Set([draft.runId]) : ownedRuns;
1794
+ const path = logsDbPath();
1795
+ if (!existsSync(path)) return false;
1796
+ const store = LogStore.openReadOnly(path);
1797
+ try {
1798
+ let beforeId: number | undefined;
1799
+ for (;;) {
1800
+ const rows = store.query({ exactCategories: ['self-implement.result'], events: ['final'], grep: String(draft.number),
1801
+ limit: 1_000, ...(beforeId === undefined ? {} : { beforeId }) });
1802
+ for (const row of rows) {
1803
+ try {
1804
+ const data = JSON.parse(row.data ?? '{}') as { runId?: string; prNumber?: number };
1805
+ if (data.prNumber === draft.number && data.runId && currentOwners.has(data.runId)) return true;
1806
+ } catch { /* A malformed result cannot establish finality. */ }
1807
+ }
1808
+ if (rows.length < 1_000) return false;
1809
+ beforeId = rows.at(-1)!.id;
1810
+ }
1811
+ } finally { store.close(); }
1812
+ },
1690
1813
  listLiveBranches: async (repository) => {
1691
1814
  const cwd = process.cwd();
1692
1815
  const remote = git(cwd, ['config', '--get', 'remote.origin.url']);
@@ -1982,6 +2105,7 @@ export interface HarnessCliCommandDeps {
1982
2105
  deliverableVerify?: InstallDeliverableVerifyCliDeps;
1983
2106
  processObservation?: HarnessProcessObservationDeps;
1984
2107
  draftSweep?: HarnessDraftSweepDeps;
2108
+ queue?: HarnessQueueDeps;
1985
2109
  ask?: HarnessAskHandler;
1986
2110
  say?: HarnessSayHandler;
1987
2111
  /** Observe-only Pod host dispatch seam. */
@@ -2034,6 +2158,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2034
2158
  installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
2035
2159
  installHarnessBudgetCommand(harnessCmd);
2036
2160
  installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
2161
+ const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
2162
+ const queueDeps = deps.queue ?? {};
2163
+ const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
2164
+ queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
2165
+ .option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
2166
+ .option('--hold', '자동 병합 금지').option('--heavy', 'Pod high 메모리')
2167
+ .action((opts: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }) => queueAction(async () => {
2168
+ const row = await addHarnessQueue(opts, queueDeps);
2169
+ console.log(`${row.id} ${row.seat} ${row.kind} queued`);
2170
+ }));
2171
+ queue.command('list').description('대기·발사 원장 조회').action(() => queueAction(async () => {
2172
+ for (const row of listHarnessQueue(queueDeps)) console.log(`${row.id} ${row.seat} ${row.status} ${row.kind} ${row.input}`);
2173
+ }));
2174
+ queue.command('remove <id>').description('대기 중 또는 종료 확인된 항목 제거').action((id: string) => queueAction(async () => {
2175
+ if (!await removeHarnessQueue(id, queueDeps)) throw new Error(`queue item active or not found: ${id}`);
2176
+ console.log(`${id} removed`);
2177
+ }));
2178
+ queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
2179
+ console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
2180
+ }));
2181
+ queue.command('tick').description('맨 앞 한 건의 자리 몫과 Pod 풀 여유를 확인하고 발사').action(() => queueAction(async () => {
2182
+ const result = await tickHarnessQueue(queueDeps);
2183
+ console.log(`${result.outcome}${result.item ? ` ${result.item.id}` : ''}: ${result.reason}`);
2184
+ }));
2037
2185
 
2038
2186
  const ask = deps.ask;
2039
2187
  if (ask) {
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { renameSync, writeFileSync } from 'node:fs';
4
+
5
+ /** Only a confirmed terminal observation allows reconciliation to release an uncertain launch. */
6
+ export function writeHarnessQueueReceipt(receiptPath: string, state: 'started' | 'finished' | 'not-started'): void {
7
+ const temp = `${receiptPath}.${randomUUID()}.tmp`;
8
+ writeFileSync(temp, JSON.stringify({ state, at: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
9
+ renameSync(temp, receiptPath);
10
+ }
11
+
12
+ export async function runHarnessQueueChild(
13
+ receiptPath: string, command: string, args: string[],
14
+ start: typeof spawn = spawn,
15
+ ): Promise<number> {
16
+ const record = (state: 'started' | 'finished' | 'not-started') => writeHarnessQueueReceipt(receiptPath, state);
17
+ let child: ReturnType<typeof spawn>;
18
+ try { child = start(process.execPath, [command, ...args], { stdio: 'inherit', env: process.env }); }
19
+ catch { record('not-started'); return 1; }
20
+ return await new Promise<number>((done) => {
21
+ let started = false;
22
+ child.once('spawn', () => {
23
+ started = true;
24
+ record('started');
25
+ child.once('exit', (code) => { record('finished'); done(code ?? 1); });
26
+ });
27
+ child.once('error', () => {
28
+ if (!started) { record('not-started'); done(1); }
29
+ });
30
+ });
31
+ }
32
+
33
+ if (import.meta.main) {
34
+ const [receiptPath, command, ...args] = process.argv.slice(2);
35
+ if (!receiptPath || !command) throw new Error('queue child requires receipt path and command');
36
+ process.exitCode = await runHarnessQueueChild(receiptPath, command, args);
37
+ }
@@ -0,0 +1,306 @@
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';
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 { getUserConfig } from '../user-config.js';
9
+ import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
10
+ import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
11
+ import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
12
+ import { writeHarnessQueueReceipt } from './harness-queue-child.js';
13
+
14
+ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
15
+ export type QueueItem = {
16
+ 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;
18
+ };
19
+ export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
20
+ export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
21
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
22
+
23
+ export interface HarnessQueueDeps {
24
+ root?: string;
25
+ cap?: (seat: QueueSeat) => number;
26
+ pool?: () => QueuePool;
27
+ launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
28
+ alive?: (pid: number) => boolean;
29
+ processes?: () => readonly QueueProcess[];
30
+ receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
31
+ log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
32
+ }
33
+
34
+ export function harnessQueuePath(root = effectiveInstanceRoot()): string {
35
+ return join(root, 'harness', 'queue.json');
36
+ }
37
+
38
+ function observe(event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>, deps: HarnessQueueDeps): void {
39
+ try { (deps.log ?? ((name, payload) => debug.log('harness.queue', name, payload)))(event, data); }
40
+ catch { /* Logging cannot affect dispatch. */ }
41
+ }
42
+
43
+ function read(path: string): QueueItem[] {
44
+ if (!existsSync(path)) return [];
45
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
46
+ if (!Array.isArray(value) || !value.every((row) => row && typeof row === 'object'
47
+ && typeof row.id === 'string' && /^(OP|TC|MK|UX)$/.test(row.seat)
48
+ && (row.kind === 'say' || row.kind === 'ask') && typeof row.input === 'string'
49
+ && typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
50
+ && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
51
+ && (row.pid === undefined || Number.isSafeInteger(row.pid))
52
+ && (row.launchId === undefined || typeof row.launchId === 'string'))) {
53
+ throw new Error('harness queue: invalid queue file (no launch)');
54
+ }
55
+ return value as QueueItem[];
56
+ }
57
+
58
+ function save(path: string, items: QueueItem[]): void {
59
+ const temporary = `${path}.${randomUUID()}.tmp`;
60
+ try {
61
+ writeFileSync(temporary, `${JSON.stringify(items, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
62
+ renameSync(temporary, path);
63
+ } catch (error) {
64
+ try { unlinkSync(temporary); } catch { /* write did not finish */ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ // SQLite OS locks release on process death, including death while awaiting a launch.
70
+ async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
71
+ const path = harnessQueuePath(root);
72
+ mkdirSync(join(root, 'harness'), { recursive: true, mode: 0o700 });
73
+ const db = new Database(`${path}.mutex.sqlite`, { create: true, strict: true });
74
+ try {
75
+ const deadline = Date.now() + 30_000;
76
+ for (;;) {
77
+ try { db.exec('BEGIN IMMEDIATE'); break; }
78
+ catch (error) {
79
+ if ((error as { code?: string }).code !== 'SQLITE_BUSY' || Date.now() >= deadline) throw error;
80
+ await Bun.sleep(25);
81
+ }
82
+ }
83
+ try { return await action(path); }
84
+ finally { db.exec('ROLLBACK'); }
85
+ } finally { db.close(); }
86
+ }
87
+
88
+ function seat(value: string): QueueSeat {
89
+ if (!/^(OP|TC|MK|UX)$/.test(value)) throw new Error(`harness queue: unknown seat ${value}`);
90
+ return value as QueueSeat;
91
+ }
92
+
93
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
94
+ const assigned = seat(input.seat);
95
+ if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
96
+ const kind = input.ask === undefined ? 'say' : 'ask';
97
+ const text = kind === 'say' ? input.say! : input.ask!;
98
+ if (!text.trim()) throw new Error('harness queue add: 빈 입력');
99
+ const value = kind === 'ask' ? resolve(text) : text;
100
+ if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
101
+ const root = deps.root ?? effectiveInstanceRoot();
102
+ return locked(root, async (path) => {
103
+ 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]);
106
+ observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
107
+ return item;
108
+ });
109
+ }
110
+
111
+ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
112
+ return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
113
+ }
114
+
115
+ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
116
+ const root = deps.root ?? effectiveInstanceRoot();
117
+ return locked(root, async (path) => {
118
+ const rows = read(path);
119
+ const item = rows.find((row) => row.id === id);
120
+ if (!item) return false;
121
+ if (item.status !== 'queued' && item.status !== 'finished') {
122
+ if ((item.status !== 'launched' && item.status !== 'launching') || !item.launchId) return false;
123
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
124
+ if (state !== 'finished' && state !== 'not-started'
125
+ || (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
126
+ }
127
+ save(path, rows.filter((row) => row.id !== id));
128
+ observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
129
+ return true;
130
+ });
131
+ }
132
+
133
+ export function queueLaunchArgs(item: QueueItem): string[] {
134
+ return ['harness', item.kind, item.input, '--substrate', 'pod',
135
+ ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
136
+ }
137
+
138
+ export function readHarnessQueuePool(): QueuePool {
139
+ const config = getUserConfig();
140
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
141
+ const current = spec ? null : leaseKubectl(['config', 'current-context']);
142
+ const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
143
+ if (!context) throw new Error('pod lease status: no pool/context');
144
+ const members = parsePodPool(context);
145
+ const observed = measurePoolLease(members);
146
+ if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
147
+ const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
148
+ const records = podPoolHostLease(members).live();
149
+ return {
150
+ running: observed.members.reduce((sum, member) => sum + member.running!, 0),
151
+ pending: observed.members.reduce((sum, member) => sum + member.pending!, 0),
152
+ reserved: hostLeaseCounts(records).reserved + records.filter((row) => row.stage === 'job' && !leaseHasPendingPod(row, pendingJobs)).length,
153
+ limit: members.reduce((sum, member) => sum + member.capacity, 0),
154
+ };
155
+ }
156
+
157
+ function alive(pid: number): boolean {
158
+ try { process.kill(pid, 0); return true; } catch { return false; }
159
+ }
160
+
161
+ /** 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 });
164
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
165
+ const rows: QueueProcess[] = [];
166
+ 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]!;
170
+ if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
171
+ || !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
172
+ || !/(?:\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') {
178
+ 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);
182
+ } catch { /* Unknown attribution is charged to all seats. */ }
183
+ }
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 } : {}) });
187
+ }
188
+ return rows;
189
+ }
190
+
191
+ export function harnessQueueReceiptPath(root: string, launchId: string): string {
192
+ if (!/^hq-[0-9a-f-]{36}$/.test(launchId)) throw new Error('invalid queue launch id');
193
+ return join(root, 'harness', `${launchId}.receipt.json`);
194
+ }
195
+
196
+ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-started' | null {
197
+ try {
198
+ const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { state?: string };
199
+ return record.state === 'started' || record.state === 'finished' || record.state === 'not-started' ? record.state : null;
200
+ } catch (error) {
201
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
207
+ const repo = resolve(import.meta.dir, '../..');
208
+ const logPath = join(root, 'harness', `${item.id}.log`);
209
+ const fd = openSync(logPath, 'a', 0o600);
210
+ try {
211
+ const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
212
+ harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
213
+ ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
214
+ cwd: repo, detached: true, stdio: ['ignore', fd, fd],
215
+ env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
216
+ ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
217
+ });
218
+ const pid = await new Promise<number>((done, reject) => {
219
+ child.once('spawn', () => done(child.pid!));
220
+ child.once('error', (error) => {
221
+ try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
222
+ catch { /* A failed receipt write leaves the reservation safely indeterminate. */ }
223
+ reject(error);
224
+ });
225
+ });
226
+ child.unref();
227
+ return pid;
228
+ } finally { closeSync(fd); }
229
+ }
230
+
231
+ /** Only a wrapper receipt proving no active child can clear an uncertain reservation. */
232
+ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<'released' | 'running' | 'unknown'> {
233
+ const root = deps.root ?? effectiveInstanceRoot();
234
+ return locked(root, async (path) => {
235
+ const rows = read(path);
236
+ const item = rows.find((row) => row.id === id);
237
+ if (!item || !['launching', 'launched'].includes(item.status) || !item.launchId) return 'unknown';
238
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
239
+ const processes = (deps.processes ?? readHarnessQueueProcesses)();
240
+ if (processes.some((row) => row.launchId === item.launchId)
241
+ || (item.pid !== undefined && (deps.alive ?? alive)(item.pid) && state !== 'finished' && state !== 'not-started')) return 'running';
242
+ if (state !== 'finished' && state !== 'not-started') return 'unknown';
243
+ // A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
244
+ // Keep completed launches in the ledger, but never charge their seat again.
245
+ save(path, state === 'finished' && item.status === 'launched'
246
+ ? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
247
+ : rows.filter((row) => row.id !== id));
248
+ observe('skipped', { id, seat: item.seat, reason: `reconciled ${state}`, launchId: item.launchId }, deps);
249
+ return 'released';
250
+ });
251
+ }
252
+
253
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
254
+ const root = deps.root ?? effectiveInstanceRoot();
255
+ return locked(root, async (path) => {
256
+ const items = read(path);
257
+ const item = items.find((row) => row.status === 'queued');
258
+ 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);
260
+ if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
261
+ const wait = (reason: string, active: number): QueueTick => {
262
+ observe('waiting', { id: item.id, seat: item.seat, reason, active, cap }, deps);
263
+ return { outcome: 'waiting', item, reason };
264
+ };
265
+ let processes: readonly QueueProcess[];
266
+ try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
267
+ catch (error) { return wait(`harness process inventory unavailable: ${String(error)}`, 0); }
268
+ const current = items.flatMap((row) => {
269
+ if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
270
+ const state = (deps.receipt ?? receipt)(root, row.launchId);
271
+ if (state !== 'finished' && state !== 'not-started'
272
+ || processes.some((process) => process.launchId === row.launchId)) return [row];
273
+ return state === 'finished' && row.status === 'launched' ? [{ ...row, status: 'finished' as const }] : [];
274
+ });
275
+ if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
276
+ const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
277
+ const launchIds = new Set(charged.map((row) => row.launchId));
278
+ const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
279
+ (row.status === 'launching' || row.status === 'launched')
280
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
281
+ if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
282
+ let pool: QueuePool;
283
+ try { pool = (deps.pool ?? readHarnessQueuePool)(); }
284
+ catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
285
+ if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
286
+ return wait('pod lease status incomplete', active);
287
+ }
288
+ const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
289
+ if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
290
+ return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
291
+ }
292
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
293
+ save(path, current.map((row) => row.id === item.id ? launching : row));
294
+ try {
295
+ const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
296
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
297
+ const launched: QueueItem = { ...launching, pid, status: 'launched' };
298
+ save(path, current.map((row) => row.id === item.id ? launched : row));
299
+ observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
300
+ return { outcome: 'launched', item: launched, reason: 'spawned' };
301
+ } catch (error) {
302
+ observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
303
+ throw error;
304
+ }
305
+ });
306
+ }