elanous 0.2.18 → 0.2.20

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 (350) 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/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -7,9 +7,12 @@
7
7
  * - 기본은 SHADOW: 카드와 명령만 남기고 아무것도 띄우지 않는다. `--live` 일 때만 주입된 launcher 로 띄운다.
8
8
  * - 관측: `debug.log('task-agent', 'handed', {taskId, seat, checklistId, mode})`.
9
9
  */
10
+ import { execFileSync } from 'node:child_process';
10
11
  import { randomBytes } from 'node:crypto';
11
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
- import { dirname, join } from 'node:path';
12
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { resolveDaemonHarnessTarget } from '../intake-plane/harness-target.js';
15
+ import { HARNESS_RUN_ID_ENV, mintRunId, normalizeRunId } from '../harness/harness-space.js';
13
16
  import { debug } from '../debug/log.js';
14
17
  import { effectiveInstanceRoot } from '../instance/resolve.js';
15
18
  import { withFileLockSync } from '../storage/file-lock.js';
@@ -18,17 +21,35 @@ export const TASK_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
18
21
  export type TaskSeat = typeof TASK_SEATS[number];
19
22
  /** 과제 카드 id 접두 — `task show <id>` 가 이 접두로 TOX 태스크와 갈린다. */
20
23
  export const TASK_CARD_PREFIX = 'ta-';
24
+ /**
25
+ * 과제 종결 종류(RFC-loop-agent-map §A4b③) — 비면 code-pr(PR 병합)로 읽는다.
26
+ * code-pr 밖의 종류는 «확인 증거»(`judge.ts` TaskJudgeInput.evidence)로 종결한다 — 증거를 만드는 일은 DEFAULT-HANDS 몫.
27
+ */
28
+ export const COMPLETION_KINDS = ['code-pr', 'artifact', 'research-report', 'ops-action', 'content', 'watch-brief', 'decision-support'] as const;
29
+ export type CompletionKind = typeof COMPLETION_KINDS[number];
21
30
 
22
- export interface TaskCardEvent { at: string; event: string; detail?: string }
31
+ /** `run-bound`·`goal-bound`·`pr-bound` 은 runId(⊕ pr)를 싣는다 — 옛 카드의 칸은 at·event·detail 뿐이다. */
32
+ export interface TaskCardEvent { at: string; event: string; detail?: string; runId?: string; pr?: number }
23
33
  export interface TaskCardMove { kind: 'launch' | 'wait'; command?: string[]; reason: string }
24
34
  export interface TaskCard {
25
35
  id: string;
26
36
  text: string;
27
37
  seat?: TaskSeat;
28
38
  checklistId?: string;
39
+ /** 종결 종류 — 비면 code-pr. */
40
+ completion?: CompletionKind;
41
+ /** 과제 대상 프로젝트(§A4b②) — target 은 절대 경로(git 이 아닐 수 있다). id 는 `task hand --project` 가 싣는다(옛 카드엔 없을 수 있다). */
42
+ project?: { id?: string; target: string };
43
+ /** 보드(§A4b① `board.ts`) 소속 — 목표 id · 이정표 id(`task hand --goal/--milestone`). 조각 카드의 `goalId`(골 sha 16자)와 다른 칸이다. */
44
+ goal?: string;
45
+ milestone?: string;
29
46
  createdAt: string;
30
- /** launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사. */
31
- status: 'handed' | 'launched' | 'launch-failed';
47
+ /**
48
+ * launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사.
49
+ * failed = 발사된 런이 PR·수확 가지 없이 죽었거나 끝내 안 떴다(TA-JUDGE-DEAD-RUN · `card-evidence.ts`) — 사유
50
+ * (`failed/needs-owner|needs-relaunch — …`)는 history 마지막 칸 · 자동 재발사하지 않는다(사람/판단부 몫).
51
+ */
52
+ status: 'handed' | 'launched' | 'launch-failed' | 'failed';
32
53
  history: TaskCardEvent[];
33
54
  /** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
34
55
  pieces?: string[];
@@ -36,17 +57,48 @@ export interface TaskCard {
36
57
  /** 조각 카드만 — 소속 미션 카드 id · 먼저 착지해야 하는 조각 id(선후). */
37
58
  mission?: string;
38
59
  after?: string[];
39
- /** 조각 카드만 — 착지 근거(② `tasks advance --pr|--goal`): 이 조각 런이 연 PR 번호 · 골 id(16자 · 런 원장 `pr-opened` 로 PR 을 찾는다). */
40
- pr?: number;
60
+ /**
61
+ * 이 카드 런이 연 PR · 골 id(16자). 두 출처가 한 칸을 쓴다(UX LOOP-INTERACT 계약 — card.runId → goalId):
62
+ * - 수동(② `tasks advance --pr|--goal` · 미션 조각만) — PR 은 번호(number) · `refSource: 'manual'`.
63
+ * - 런 원장 묶기(TA-CARD-RUN-LINK `card-evidence.ts`) — PR 은 `{number, url}` · 비어 있을 때만 «한 번» 채운다 · 수동 근거를 덮지 않는다.
64
+ * 번호는 `cardPrNumber` 로 읽는다(옛 카드의 number 도 그대로 읽힌다).
65
+ */
66
+ pr?: number | TaskCardPr;
41
67
  goalId?: string;
68
+ /** `manual` = pr/goalId 를 사람이 적었다(`recordPieceRef`) — 런 묶기가 건드리지 않는다. */
69
+ refSource?: 'manual';
42
70
  /** 조각 카드만 — ② 가 선행 착지 뒤 넘겼다(한 번만 · 같은 잠금 안에서 먼저 적는다). */
43
71
  handed?: { at: string; mode: 'shadow' | 'live'; claim?: string; error?: string };
72
+ /**
73
+ * TA-CARD-RUN-LINK — `--live` 발사가 발사기에 넘긴 런 id(`ELANOUS_RUN_ID` 상속 → 하니스 런이 그 id 로 돈다) ⊕ 발사 토큰.
74
+ * 발사기가 그 id 를 «받았다»고 돌려줄 때만 적는다(한 번만). 원장이 아직 없으면 그 런은 시작 전이거나 대기열로 갔다.
75
+ */
76
+ runId?: string;
77
+ launchId?: string;
78
+ /** Pod 런이면 PR 을 연 자식 런 id(런 묶기가 PR 과 함께 적는다). */
79
+ runChildId?: string;
44
80
  /** 미션 카드만 — 모든 조각 착지 → 칸 green «제안»(체크리스트는 손대지 않는다 · OP/주인이 뒤집는다). */
45
81
  greenProposal?: { at: string; checklistId: string | null; evidence: Record<string, string> };
82
+ /** TA-JUDGE-LIVE-SAFE — live `review` 수가 리뷰를 요청한 PR 머리들(머리마다 한 번 · 띄우기 실패면 error · 이력은 지우지 않는다). */
83
+ reviewRequests?: Array<{ pr: number; head: string; at: string; error?: string }>;
84
+ }
85
+
86
+ /** 런 원장에서 묶은 PR — url 은 원장(pr-opened · Pod job-finished)에 있을 때만(지어내지 않는다). */
87
+ export interface TaskCardPr { number: number; url?: string }
88
+
89
+ /** 카드의 PR 번호 — 수동(number)·묶음(`{number,url}`) 둘 다. */
90
+ export function cardPrNumber(card: Pick<TaskCard, 'pr'>): number | undefined {
91
+ return typeof card.pr === 'number' ? card.pr : card.pr?.number;
46
92
  }
47
93
 
48
94
  interface TaskAgentStateFile { tasks?: Record<string, TaskCard>; [key: string]: unknown }
49
95
 
96
+ function isGitWorkTree(path: string): boolean {
97
+ try {
98
+ return execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: path, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() === 'true';
99
+ } catch { return false; }
100
+ }
101
+
50
102
  export function taskAgentStatePath(): string {
51
103
  return join(effectiveInstanceRoot(), 'task-agent-actions.json');
52
104
  }
@@ -130,6 +182,7 @@ export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskC
130
182
  if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
131
183
  if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
132
184
  if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
185
+ if (card.status === 'failed') return { kind: 'wait', reason: card.history.at(-1)?.detail ?? '런 실패 — PR·수확 가지 없음' };
133
186
  return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
134
187
  }
135
188
 
@@ -137,12 +190,22 @@ export function shellQuote(arg: string): string {
137
190
  return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
138
191
  }
139
192
 
140
- export type TaskLauncher = (args: string[]) => void | Promise<void>;
193
+ /** 발사기에 넘기는 연결 — env(`ELANOUS_RUN_ID`)를 자식 환경에 얹으면 하니스 런이 `runId` 로 돈다(`resolveRunIdentity` 상속). launchId 는 카드에만 남는 발사 토큰이다. */
194
+ export interface TaskLaunchContext { runId: string; launchId: string; env: Record<string, string> }
195
+ /** 발사기가 «이 런 id 로 띄웠다»고 돌려주는 영수증 — 없으면 카드에 런 id 를 적지 않는다(추측하지 않는다). */
196
+ export interface TaskLaunchReceipt { runId?: string }
197
+ export type TaskLauncher = (args: string[], cwd?: string, context?: TaskLaunchContext) => void | TaskLaunchReceipt | Promise<void | TaskLaunchReceipt>;
141
198
 
142
199
  export interface HandTaskOptions {
143
200
  text: string;
144
201
  seat?: TaskSeat;
145
202
  checklistId?: string;
203
+ completion?: TaskCard['completion'];
204
+ /** `--project <id> --target <dir>` — target 은 존재 확인 뒤 절대 경로로 바꿔 싣는다. */
205
+ project?: { id: string; target: string };
206
+ /** 보드 소속 목표·이정표 id — 카드에 그대로 싣는다. */
207
+ goal?: string;
208
+ milestone?: string;
146
209
  live?: boolean;
147
210
  statePath?: string;
148
211
  /** `--live` 일 때만 불린다. */
@@ -179,12 +242,31 @@ export class TaskLaunchError extends Error {
179
242
  }
180
243
  }
181
244
 
182
- export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean }
245
+ export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean; cwd?: string }
183
246
 
184
247
  export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
185
248
  const text = opts.text.trim();
186
249
  if (!text) throw new Error('과제 한 줄이 비어 있다');
187
250
  if (opts.seat !== undefined && !TASK_SEATS.includes(opts.seat)) throw new Error(`자리는 ${TASK_SEATS.join('|')} 중 하나다: ${opts.seat}`);
251
+ if (opts.completion !== undefined && !COMPLETION_KINDS.includes(opts.completion)) throw new Error(`종결 종류는 ${COMPLETION_KINDS.join('|')} 중 하나다: ${opts.completion}`);
252
+ for (const [flag, value] of [['--goal', opts.goal], ['--milestone', opts.milestone]] as const) {
253
+ if (value !== undefined && (!value.trim() || /\p{Cc}/u.test(value))) throw new Error(`${flag} id 가 비었거나 제어 문자를 담았다: ${JSON.stringify(value)}`);
254
+ }
255
+ let project: TaskCard['project'];
256
+ let targetIsGit: boolean | null = null;
257
+ if (opts.project) {
258
+ if (!opts.project.id.trim()) throw new Error('--project id 가 비어 있다');
259
+ const target = resolve(opts.project.target);
260
+ let isDir = false;
261
+ try { isDir = statSync(target).isDirectory(); } catch { /* 없음 */ }
262
+ if (!isDir) throw new Error(`대상 디렉터리가 없습니다: ${target}`);
263
+ targetIsGit = isGitWorkTree(target);
264
+ // 코드 종결(code-pr · 비면 code-pr)은 git 대상만 — 그 밖의 종결 종류는 git 이 아닌 폴더도 받는다(같은 판정을 harness-target 과 공유).
265
+ const codeCompletion = (opts.completion ?? 'code-pr') === 'code-pr';
266
+ const selection = resolveDaemonHarnessTarget({ configured: target, cwd: target, isGitRepo: () => targetIsGit === true, allowNonGit: !codeCompletion });
267
+ if (!selection.ok) throw new Error(`코드 종결은 git 대상이 필요 — --completion 을 고르거나 git init: ${target}`);
268
+ project = { id: opts.project.id, target: selection.repo };
269
+ }
188
270
  const now = (opts.now ?? (() => new Date()))();
189
271
  const id = opts.id ?? `${TASK_CARD_PREFIX}${now.toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
190
272
  const mode = opts.live ? 'live' : 'shadow';
@@ -193,6 +275,10 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
193
275
  id, text,
194
276
  ...(opts.seat ? { seat: opts.seat } : {}),
195
277
  ...(opts.checklistId ? { checklistId: opts.checklistId } : {}),
278
+ ...(opts.completion ? { completion: opts.completion } : {}),
279
+ ...(project ? { project } : {}),
280
+ ...(opts.goal ? { goal: opts.goal } : {}),
281
+ ...(opts.milestone ? { milestone: opts.milestone } : {}),
196
282
  createdAt: now.toISOString(),
197
283
  status: 'handed',
198
284
  history: [],
@@ -210,17 +296,21 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
210
296
  if (!guard(current)) return undefined;
211
297
  owned.ok = true;
212
298
  // next 의 옛 근거는 버리고 디스크의 근거로 «교체»한다 — 정정된 근거 옆에 옛 근거가 되살아나지 않게.
213
- const { pr: _stalePr, goalId: _staleGoal, ...base } = next;
214
- return { ...base, ...(current?.pr !== undefined ? { pr: current.pr } : {}), ...(current?.goalId !== undefined ? { goalId: current.goalId } : {}) };
299
+ const { pr: _stalePr, goalId: _staleGoal, refSource: _staleSource, ...base } = next;
300
+ return { ...base, ...(current?.pr !== undefined ? { pr: current.pr } : {}), ...(current?.goalId !== undefined ? { goalId: current.goalId } : {}), ...(current?.refSource ? { refSource: current.refSource } : {}) };
215
301
  });
216
302
  if (!owned.ok) throw new TaskCardSupersededError(next.id);
217
303
  };
218
304
  writeCard(path, card);
219
- try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode }); } catch { /* fail-soft */ }
220
- if (mode === 'shadow') return { card, move, mode, launched: false };
305
+ try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode, projectId: project?.id ?? null, targetIsGit }); } catch { /* fail-soft */ }
306
+ if (mode === 'shadow') return { card, move, mode, launched: false, ...(project ? { cwd: project.target } : {}) };
221
307
  if (!opts.launcher) throw new Error('--live 인데 launcher 가 없다');
308
+ const runId = mintRunId();
309
+ const launchId = `tl-${randomBytes(6).toString('hex')}`;
310
+ const context: TaskLaunchContext = { runId, launchId, env: { [HARNESS_RUN_ID_ENV]: runId } };
311
+ let receipt: void | TaskLaunchReceipt;
222
312
  try {
223
- await opts.launcher(move.command!);
313
+ receipt = await opts.launcher(move.command!, project?.target, context);
224
314
  } catch (error) {
225
315
  const reason = error instanceof Error ? error.message : String(error);
226
316
  const failed: TaskCard = { ...card, status: 'launch-failed', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch-failed', detail: reason }] };
@@ -228,8 +318,25 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
228
318
  try { debug.log('task-agent', 'launch-failed', { taskId: id, seat: card.seat ?? null, reason }); } catch { /* fail-soft */ }
229
319
  throw new TaskLaunchError(failed, reason);
230
320
  }
231
- const launched: TaskCard = { ...card, status: 'launched', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch', detail: move.command!.join(' ') }] };
321
+ const launchedAt = (opts.now ?? (() => new Date()))().toISOString();
322
+ const receivedRunId = receipt && typeof receipt.runId === 'string' ? receipt.runId.trim() : '';
323
+ // 넘긴 런 id 와 «같은» 영수증만 — 다른 id 면 env 로 넘긴 런과 카드가 어긋난다(묶지 않는다).
324
+ const boundRunId = receivedRunId && receivedRunId === runId && normalizeRunId(receivedRunId) === receivedRunId ? receivedRunId : undefined;
325
+ if (receivedRunId && !boundRunId) {
326
+ try { debug.log('task-agent', 'card-run-unbound', { card: id, launchId, expected: runId, received: receivedRunId }); } catch { /* fail-soft */ }
327
+ }
328
+ const launched: TaskCard = {
329
+ ...card, status: 'launched',
330
+ ...(boundRunId ? { runId: boundRunId, launchId } : {}),
331
+ history: [
332
+ { at: launchedAt, event: 'launch', detail: move.command!.join(' ') },
333
+ ...(boundRunId ? [{ at: launchedAt, event: 'run-bound', detail: boundRunId, runId: boundRunId }] : []),
334
+ ],
335
+ };
232
336
  writeCard(path, launched);
233
- try { debug.log('task-agent', 'launched', { taskId: id, seat: card.seat ?? null }); } catch { /* fail-soft */ }
234
- return { card: launched, move, mode, launched: true };
337
+ try { debug.log('task-agent', 'launched', { taskId: id, seat: card.seat ?? null, runId: boundRunId ?? null }); } catch { /* fail-soft */ }
338
+ if (boundRunId) {
339
+ try { debug.log('task-agent', 'card-run-bound', { card: id, runId: boundRunId, launchId }); } catch { /* fail-soft */ }
340
+ }
341
+ return { card: launched, move, mode, launched: true, ...(project ? { cwd: project.target } : {}) };
235
342
  }
@@ -14,6 +14,7 @@ import { debug } from '../../debug/log.js';
14
14
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
15
15
  import { collectPodArtifacts } from './pod-artifact-return.js';
16
16
  import { podBunCacheVolume } from './pod-bun-cache.js';
17
+ import { podHostDirsInit, podInstallSlotsVolume } from './pod-install-slots.js';
17
18
  import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
18
19
  import { podSkillsDigest, readSkillEnvFiles, resolvePodSkills } from './pod-skills.js';
19
20
  import { podSourceScript, type PodSource } from './pod-source-receive.js';
@@ -37,6 +38,54 @@ export const POD_LITE_IMAGE = 'elanous-harness-lite:local';
37
38
  export const POD_LITE_MEMORY_LIMIT = '2Gi';
38
39
 
39
40
  /** 요청은 한도를 넘을 수 없다(쿠버네티스가 Job 을 거부한다) — lite 2Gi 처럼 한도가 기본 요청보다 작으면 한도로 맞춘다. */
41
+ /** The command Job container's CPU limit when the caller names none (GATE-SPEED A3①: unchanged default). */
42
+ export const POD_COMMAND_CPU_LIMIT_DEFAULT = '4';
43
+
44
+ /** Container CPU request/limit override. Omitted fields keep the defaults (request `POD_CHILD_REQUESTS.cpu` · limit 4). */
45
+ export interface PodCpu { request?: string; limit?: string }
46
+
47
+ /** Kubernetes CPU quantity → millicores (`1` · `0.5` · `500m`); null when it is not a positive quantity we accept. */
48
+ export function podCpuMillis(value: string): number | null {
49
+ const m = /^(?:(\d+(?:\.\d{1,3})?)|(\d+)m)$/.exec(value.trim());
50
+ if (!m) return null;
51
+ const millis = m[1] !== undefined ? Math.round(Number(m[1]) * 1000) : Number(m[2]);
52
+ return Number.isSafeInteger(millis) && millis > 0 ? millis : null;
53
+ }
54
+
55
+ /** Resolve a CPU override against the defaults; throws when a value is malformed or the request exceeds the limit (k8s would refuse the Job). */
56
+ export function resolvePodCpu(cpu: PodCpu | undefined): { request: string; limit: string } {
57
+ const request = cpu?.request?.trim() || POD_CHILD_REQUESTS.cpu;
58
+ const limit = cpu?.limit?.trim() || POD_COMMAND_CPU_LIMIT_DEFAULT;
59
+ const r = podCpuMillis(request), l = podCpuMillis(limit);
60
+ if (r === null) throw new Error(`pod cpu request is not a positive CPU quantity: ${request}`);
61
+ if (l === null) throw new Error(`pod cpu limit is not a positive CPU quantity: ${limit}`);
62
+ if (r > l) throw new Error(`pod cpu request ${request} exceeds limit ${limit}`);
63
+ return { request, limit };
64
+ }
65
+
66
+ /** Config value → CPU override (release gate `release.loop.gatePodCpu`). A scalar (`2` · `"1500m"`) sets request = limit;
67
+ * `{ request, limit }` sets either side. Absent/empty = undefined (request 1 / limit 4). Malformed values throw. */
68
+ export function parsePodCpu(value: unknown, label = 'cpu'): PodCpu | undefined {
69
+ const text = (v: unknown): string | undefined => {
70
+ if (v === undefined || v === null || v === '') return undefined;
71
+ if (typeof v !== 'number' && typeof v !== 'string') throw new Error(`${label} must be a CPU quantity or { request, limit }`);
72
+ return String(v).trim() || undefined;
73
+ };
74
+ let cpu: PodCpu;
75
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
76
+ const { request, limit, ...rest } = value as Record<string, unknown>;
77
+ if (Object.keys(rest).length) throw new Error(`${label} has unknown keys: ${Object.keys(rest).join(', ')}`);
78
+ const r = text(request), l = text(limit);
79
+ cpu = { ...(r ? { request: r } : {}), ...(l ? { limit: l } : {}) };
80
+ } else {
81
+ const both = text(value);
82
+ cpu = both ? { request: both, limit: both } : {};
83
+ }
84
+ if (!cpu.request && !cpu.limit) return undefined;
85
+ resolvePodCpu(cpu);
86
+ return cpu;
87
+ }
88
+
40
89
  export function memoryRequestWithin(limit: string | undefined): string {
41
90
  const gi = (value: string | undefined) => { const m = /^(\d+)Gi$/.exec(value ?? ''); return m ? Number(m[1]) : null; };
42
91
  const request = gi(POD_CHILD_REQUESTS.memory);
@@ -128,8 +177,12 @@ export interface PodCommandJobInput {
128
177
  clone?: boolean;
129
178
  hostMirror?: string;
130
179
  bunCache?: string;
180
+ /** GATE-INSTALL-CACHE — hostPath of the node-wide install tokens (release gate shards only · `pod-install-slots.ts`). */
181
+ installSlots?: string;
131
182
  /** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
132
183
  memoryLimit?: string;
184
+ /** GATE-SPEED A3①: CPU request/limit override (release gate shards only). Omitted = request 1 / limit 4. */
185
+ cpu?: PodCpu;
133
186
  source?: PodSource;
134
187
  deadlineSeconds: number;
135
188
  runId?: string;
@@ -147,6 +200,10 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
147
200
  }
148
201
  const grok = o.llm === 'grok';
149
202
  const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
203
+ const installSlots = o.installSlots ? podInstallSlotsVolume(o.installSlots) : undefined;
204
+ // hostPath directories the uid-1000 child writes: a root init container hands them over (DirectoryOrCreate is root 0755).
205
+ const hostDirMounts = [...(bunCache ? [bunCache.volumeMount] : []), ...(installSlots ? [installSlots.volumeMount] : [])];
206
+ const cpu = resolvePodCpu(o.cpu);
150
207
  const quotedArgs = o.command.map(bashSingleQuote).join(' ');
151
208
  const credLines = [
152
209
  ...(grok ? ['mkdir -p ~/.grok && install -m 600 /creds/grok-auth.json ~/.grok/auth.json'] : []),
@@ -182,26 +239,30 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
182
239
  spec: {
183
240
  restartPolicy: 'Never',
184
241
  securityContext: { runAsUser: 1000, fsGroup: 1000 },
185
- initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
242
+ initContainers: [
243
+ { name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] },
244
+ ...(hostDirMounts.length ? [podHostDirsInit(o.image, o.imagePullPolicy ?? 'Never', hostDirMounts)] : []),
245
+ ],
186
246
  containers: [{
187
247
  name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
188
- resources: { requests: { ...POD_CHILD_REQUESTS, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
248
+ resources: { requests: { ...POD_CHILD_REQUESTS, cpu: cpu.request, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: cpu.limit } },
189
249
  command: ['bash', '-c'], args: [script],
190
250
  env: [
191
251
  ...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
192
252
  { name: 'ELANOUS_SUBSTRATE', value: 'pod' },
193
253
  { name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
194
254
  ],
195
- ...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
255
+ ...(secretKeys || o.hostMirror || hostDirMounts.length ? { volumeMounts: [
196
256
  ...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
197
257
  ...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
198
- ...(bunCache ? [bunCache.volumeMount] : []),
258
+ ...hostDirMounts,
199
259
  ] } : {}),
200
260
  }],
201
- ...(secretKeys || o.hostMirror || bunCache ? { volumes: [
261
+ ...(secretKeys || o.hostMirror || hostDirMounts.length ? { volumes: [
202
262
  ...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
203
263
  ...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
204
264
  ...(bunCache ? [bunCache.volume] : []),
265
+ ...(installSlots ? [installSlots.volume] : []),
205
266
  ] } : {}),
206
267
  },
207
268
  },
@@ -270,8 +331,12 @@ export interface RunPodCommandOptions {
270
331
  clone?: boolean;
271
332
  hostMirror?: string;
272
333
  bunCache?: string;
334
+ /** GATE-INSTALL-CACHE — hostPath of the node-wide install tokens (absolute). */
335
+ installSlots?: string;
273
336
  /** 컨테이너 메모리 한도(기본 16Gi · lite 면 2Gi). */
274
337
  memoryLimit?: string;
338
+ /** GATE-SPEED A3①: CPU request/limit override — only the release gate passes it. Omitted = request 1 / limit 4. */
339
+ cpu?: PodCpu;
275
340
  /** POD7: 호출부가 «네트워크 스킬만 쓰는 명령»이라고 말할 때만 lite 이미지 ⊕ 2Gi. */
276
341
  lite?: boolean;
277
342
  source?: PodSource;
@@ -284,6 +349,7 @@ export interface RunPodCommandOptions {
284
349
  name?: string;
285
350
  pollMs?: number;
286
351
  kubectl?: Kubectl;
352
+ /** A caller-owned scheduler is self-capped: acquires count only its own Jobs, never cluster occupancy (POOLGLOBAL-REG). */
287
353
  poolScheduler?: PodPoolScheduler;
288
354
  checkPool?: typeof checkPodPool;
289
355
  syncImages?: typeof syncPoolImages;
@@ -356,6 +422,10 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
356
422
  if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
357
423
  const bunCache = options.bunCache?.trim() || undefined;
358
424
  if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
425
+ const installSlots = options.installSlots?.trim() || undefined;
426
+ if (installSlots && !isAbsolute(installSlots)) throw new Error('pod installSlots must be an absolute directory path');
427
+ // Fail before any Secret/Job is applied — a bad quantity must not reach the cluster.
428
+ if (options.cpu) resolvePodCpu(options.cpu);
359
429
 
360
430
  let member: PodPoolMember | null = null;
361
431
  let pool = options.poolScheduler;
@@ -366,12 +436,16 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
366
436
  const members = parsePodPool(spec);
367
437
  const checked = (options.checkPool ?? checkPodPool)(members, baseKubectl as PoolKubectl);
368
438
  if (!checked.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
369
- pool = new PodPoolScheduler(checked.ready);
439
+ // Occupancy reads go through the same kubectl as the readiness check (an injected one included).
440
+ pool = new PodPoolScheduler(checked.ready, { kubectl: baseKubectl as PoolKubectl });
370
441
  }
371
442
  }
372
443
  if (pool) {
444
+ // POOLGLOBAL-REG: a caller that hands in its own scheduler (the release gate fans its shards over one) holds its own
445
+ // concurrency cap — its Jobs count against the spec caps, not the cluster's goal Pods (#24253 → 0.2.15 gate ran 1 shard).
446
+ const selfCapped = options.poolScheduler !== undefined;
373
447
  for (;;) {
374
- member = await pool.tryAcquire();
448
+ member = await pool.tryAcquire(undefined, undefined, { selfCapped });
375
449
  if (member) break;
376
450
  await sleep(options.pollMs ?? 15_000);
377
451
  }
@@ -426,7 +500,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
426
500
  name, namespace, launch, image: imageRef ?? image,
427
501
  ...(imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
428
502
  repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
429
- ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(memoryLimit ? { memoryLimit } : {}), deadlineSeconds,
503
+ ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(installSlots ? { installSlots } : {}), ...(memoryLimit ? { memoryLimit } : {}), ...(options.cpu ? { cpu: options.cpu } : {}), deadlineSeconds,
430
504
  ...(options.runId ? { runId: options.runId } : {}),
431
505
  });
432
506
  kubectl(['-n', namespace, 'delete', 'job', '-l', own, '--ignore-not-found']);
@@ -1,3 +1,7 @@
1
+ /** STOP-RECORD stop_class values used by the card terminal classifier. */
2
+ export const STOP_CLASS_POD_FAILURE = 'pod-failure' as const;
3
+ export const STOP_CLASS_NO_LAUNCH = 'no-launch' as const;
4
+
1
5
  /** Executor kinds of the dev child's `--json` result line (`src/index.ts` writeStdoutJson · DevExecutor.kind). */
2
6
  const CHILD_RESULT_KINDS = new Set(['self', 'external']);
3
7
 
@@ -0,0 +1,89 @@
1
+ // GATE-INSTALL-CACHE (0.2.20) — a node-wide throttle for the release-gate shards' `bun install`.
2
+ //
3
+ // Cause, measured on the 0.2.19 gate (`elanous logs --category release-loop.gate --event pod-shard --all --include-test`):
4
+ // · no cache is «not» the cause — the harness image carries a warm Bun cache, and a Pod that installed alone
5
+ // (10-07 10:48–10:55 · 12:13 · 12:16) took 2.4–5.0 s with `pod-bun-cache source=none`;
6
+ // · the same install took 150–340 s when 24 shard Pods (or ~20 isolation Pods) started together on the one 32-CPU
7
+ // node-b node (09:3x round 1: 221–337 s · 11:4x round 3: 150–240 s · 10:34 isolation wave: 124–188 s).
8
+ // ⇒ concurrent installs thrash the node (24 × copying node_modules out of the image cache + CPU), so a shared
9
+ // hostPath Bun cache would not help (the bytes are already local) — fewer installs «at once» does.
10
+ //
11
+ // Concurrency safety: the hostPath holds only throttle tokens (`slot-<n>` directories), never packages. Each Pod still
12
+ // installs into its own container (its own node_modules, its own image-local Bun cache), so correctness never depends on
13
+ // the token: a lost token (Pod killed mid-install) only lowers throughput until it goes stale (`staleSeconds`), a doubled
14
+ // token (two waiters reclaiming one stale slot in the same instant) only lets one extra install run, and a waiter that
15
+ // cannot get a token within `waitSeconds` — or finds the directory unwritable — installs anyway (fail-open).
16
+ // `mkdir` is atomic on one filesystem, and every Pod on a node sees the same hostPath directory.
17
+
18
+ /** Default hostPath of the install tokens on each gate node. */
19
+ export const POD_INSTALL_SLOTS_HOST_PATH = '/var/lib/elanous/gate-install-slots';
20
+ /** Default concurrent installs per node (`release.loop.gatePodInstallSlots` · 0 = off). Alone ≈ 3–5 s ⇒ 24 shards ≈ 6 waves. */
21
+ export const POD_INSTALL_SLOTS_DEFAULT = 4;
22
+ export const POD_INSTALL_SLOTS_MOUNT = '/gate-install-slots';
23
+
24
+ /** hostPath volume ⊕ mount for the tokens. `DirectoryOrCreate` makes a root-owned 0755 directory, so the Pod also needs
25
+ * {@link podHostDirsInit} (the child runs as uid 1000). */
26
+ export function podInstallSlotsVolume(hostPath: string = POD_INSTALL_SLOTS_HOST_PATH) {
27
+ return {
28
+ volume: { name: 'install-slots', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
29
+ volumeMount: { name: 'install-slots', mountPath: POD_INSTALL_SLOTS_MOUNT, readOnly: false },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * A root init container that hands the mounted hostPath directories to the child's uid. Without it a `DirectoryOrCreate`
35
+ * hostPath stays root:root 0755 and the child's `[ -w … ]` guard silently skips it (the Bun cache option was a no-op).
36
+ */
37
+ export function podHostDirsInit(image: string, imagePullPolicy: string, mounts: ReadonlyArray<{ name: string; mountPath: string; readOnly: boolean }>) {
38
+ const paths = mounts.map((mount) => `'${mount.mountPath.replace(/'/g, `'\\''`)}'`).join(' ');
39
+ return {
40
+ name: 'host-dirs', image, imagePullPolicy,
41
+ securityContext: { runAsUser: 0 },
42
+ command: ['sh', '-c'],
43
+ // Never fails the Pod: an unchanged directory only means the child falls back (no cache · no throttle).
44
+ args: [`for d in ${paths}; do mkdir -p "$d" && chown 1000:1000 "$d" && chmod 0775 "$d" || echo "[host-dirs] could not prepare $d"; done; exit 0`],
45
+ volumeMounts: mounts.map((mount) => ({ ...mount })),
46
+ };
47
+ }
48
+
49
+ const int = (value: number, label: string) => {
50
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error(`invalid ${label}: ${value}`);
51
+ return value;
52
+ };
53
+
54
+ /**
55
+ * Bash that defines `install_slot_acquire` / `install_slot_release`. Acquire waits for one of `slots` tokens under `dir`
56
+ * (polling every `pollSeconds`, default 2 s), reclaims a token older than `staleSeconds` (minute granularity), and gives up after `waitSeconds` (install runs anyway).
57
+ * It prints `[gate] install-slot <n> waited <s>s` or `[gate] install-slot none …` for the shard log.
58
+ */
59
+ export function installSlotScript(o: { dir?: string; slots: number; waitSeconds?: number; staleSeconds?: number; pollSeconds?: number }): string {
60
+ const dir = `'${(o.dir ?? POD_INSTALL_SLOTS_MOUNT).replace(/'/g, `'\\''`)}'`;
61
+ const slots = int(o.slots, 'install slots');
62
+ const wait = int(o.waitSeconds ?? 600, 'install slot wait');
63
+ const poll = o.pollSeconds ?? 2;
64
+ if (!Number.isFinite(poll) || poll <= 0 || poll > 60) throw new Error(`invalid install slot poll: ${poll}`);
65
+ const staleMinutes = Math.max(1, Math.ceil(int(o.staleSeconds ?? 900, 'install slot stale age') / 60));
66
+ return [
67
+ 'install_slot=',
68
+ 'install_slot_acquire() {',
69
+ ` local d=${dir} t0 now n`,
70
+ ' t0=$(date +%s)',
71
+ ' if [ ! -d "$d" ] || [ ! -w "$d" ]; then echo "[gate] install-slot none (directory not writable)"; return 0; fi',
72
+ ' while :; do',
73
+ ` for n in $(seq 0 ${slots - 1}); do`,
74
+ ' if mkdir "$d/slot-$n" 2>/dev/null; then',
75
+ ' install_slot="$d/slot-$n"',
76
+ ' echo "[gate] install-slot $n waited $(( $(date +%s) - t0 ))s"; return 0',
77
+ ' fi',
78
+ // A token whose holder died (directory mtime older than the stale age · `find -mmin` is the same on GNU and BSD) is
79
+ // renamed away first (rename is atomic: one reclaimer wins) and the slot is tried again on the next pass.
80
+ ` if [ -n "$(find "$d/slot-$n" -maxdepth 0 -mmin +${staleMinutes} 2>/dev/null)" ]; then mv "$d/slot-$n" "$d/stale-$n-$$" 2>/dev/null && rm -rf "$d/stale-$n-$$"; fi`,
81
+ ' done',
82
+ ' now=$(date +%s)',
83
+ ` if [ $(( now - t0 )) -ge ${wait} ]; then echo "[gate] install-slot none (waited $(( now - t0 ))s)"; return 0; fi`,
84
+ ` sleep ${poll}`,
85
+ ' done',
86
+ '}',
87
+ 'install_slot_release() { if [ -n "$install_slot" ]; then rm -rf "$install_slot"; install_slot=; fi; }',
88
+ ].join('\n');
89
+ }