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
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { homedir } from 'node:os';
7
+ import { debug } from '../../src/debug/log.js';
8
+ import { streamLLM } from '../../src/llm.js';
9
+ import { unified } from 'unified';
10
+ import remarkParse from 'remark-parse';
11
+ import remarkGfm from 'remark-gfm';
12
+ import remarkRehype from 'remark-rehype';
13
+ import rehypeSanitize from 'rehype-sanitize';
14
+ import rehypeStringify from 'rehype-stringify';
15
+
16
+ export type Stage = 'collect' | 'draft' | 'render' | 'html' | 'pdf';
17
+ export interface Input { name: string; kind: 'pdf' | 'image' | 'markdown'; text: string }
18
+ export interface Counts { inputs: number; diagrams: number; rendered: number; html: 'yes' | 'no'; pdf: 'yes' | 'skipped' }
19
+ export interface StageResult extends Counts { outcome: 'ok' | 'skipped' }
20
+ export interface StageOptions {
21
+ llm?: (prompt: string) => Promise<string>;
22
+ toolsPath?: string;
23
+ referenceRoot?: string;
24
+ }
25
+
26
+ const ownRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../skills');
27
+ const kinds: Record<string, Input['kind']> = {
28
+ '.pdf': 'pdf', '.md': 'markdown', '.png': 'image', '.jpg': 'image', '.jpeg': 'image', '.webp': 'image',
29
+ };
30
+ const mermaid = /^```mermaid\s*\n([\s\S]*?)^```\s*$/gm;
31
+ const escapeHtml = (text: string) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
32
+
33
+ function pathStatus(path: string): ReturnType<typeof lstatSync> | undefined {
34
+ try { return lstatSync(path); }
35
+ catch (error) {
36
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ function outputDir(folder: string): string {
42
+ if (!lstatSync(folder).isDirectory()) throw new Error(`강의 폴더가 아니다: ${folder}`);
43
+ const dir = join(resolve(folder), 'lecture-note');
44
+ const status = pathStatus(dir);
45
+ if (status && (status.isSymbolicLink() || !status.isDirectory())) throw new Error('lecture-note 출력 폴더가 안전하지 않다');
46
+ mkdirSync(dir, { recursive: true });
47
+ return dir;
48
+ }
49
+
50
+ function artifact(dir: string, name: string): string {
51
+ const path = join(dir, name);
52
+ const status = pathStatus(path);
53
+ if (status && (status.isSymbolicLink() || !status.isFile())) throw new Error(`출력 파일이 안전하지 않다: ${path}`);
54
+ return path;
55
+ }
56
+
57
+ function binary(name: string, path = process.env.PATH ?? ''): string | undefined {
58
+ for (const part of path.split(':').filter(Boolean)) {
59
+ const at = join(part, name);
60
+ if (existsSync(at) && statSync(at).isFile()) return at;
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ function run(bin: string, args: string[], dir: string, timeout: number): ReturnType<typeof spawnSync> {
66
+ return spawnSync(bin, args, { cwd: dir, encoding: 'utf8', timeout, maxBuffer: 20 * 1024 * 1024,
67
+ env: { ...process.env, HOME: dir, XDG_CACHE_HOME: dir, XDG_CONFIG_HOME: dir, UV_CACHE_DIR: dir, PYTHONDONTWRITEBYTECODE: '1' } });
68
+ }
69
+
70
+ function readInputs(dir: string): Input[] {
71
+ const value: unknown = JSON.parse(readFileSync(join(dir, 'inputs.json'), 'utf8'));
72
+ if (!Array.isArray(value) || !value.every((item) => item && typeof item.name === 'string' && typeof item.kind === 'string' && typeof item.text === 'string')) {
73
+ throw new Error('inputs.json 형식 오류');
74
+ }
75
+ return value as Input[];
76
+ }
77
+
78
+ function counts(dir: string): Counts {
79
+ const inputs = existsSync(join(dir, 'inputs.json')) ? readInputs(dir).length : 0;
80
+ const notes = existsSync(join(dir, 'notes.md')) ? readFileSync(join(dir, 'notes.md'), 'utf8') : '';
81
+ const diagrams = [...notes.matchAll(mermaid)].length;
82
+ const rendered = [...Array(diagrams).keys()].filter(i => existsSync(join(dir, `diagram-${i + 1}.png`))).length;
83
+ return { inputs, diagrams, rendered, html: existsSync(join(dir, 'notes.html')) ? 'yes' : 'no', pdf: existsSync(join(dir, 'notes.pdf')) ? 'yes' : 'skipped' };
84
+ }
85
+
86
+ export function collect(folder: string, options: StageOptions = {}): StageResult {
87
+ const dir = outputDir(folder);
88
+ const files: Input[] = [];
89
+ const pdfText = binary('pdftotext', options.toolsPath);
90
+ const sources = resolve(folder, 'sources/common');
91
+ const scan = [{ base: folder, prefix: '' }, ...(pathStatus(sources)?.isDirectory() && !pathStatus(sources)?.isSymbolicLink()
92
+ ? [{ base: sources, prefix: 'sources/common/' }] : [])];
93
+ for (const { base, prefix } of scan) for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
94
+ const kind = kinds[extname(entry.name).toLowerCase()];
95
+ if (!entry.isFile() || !kind) continue;
96
+ const at = join(base, entry.name);
97
+ const text = kind === 'markdown' ? readFileSync(at, 'utf8') : kind === 'image' ? '이미지 — 시각 자료 (텍스트 추출 없음)' :
98
+ pdfText ? (() => {
99
+ const result = run(pdfText, ['-layout', at, '-'], dir, 60000);
100
+ return result.status === 0 ? String(result.stdout) : 'PDF 텍스트 없음 — pdftotext 추출 실패';
101
+ })() : 'PDF 텍스트 없음 — pdftotext 없음';
102
+ files.push({ name: prefix + entry.name, kind, text });
103
+ }
104
+ writeFileSync(artifact(dir, 'inputs.json'), JSON.stringify(files, null, 2) + '\n');
105
+ return { ...counts(dir), outcome: 'ok' };
106
+ }
107
+
108
+ const instructions = `강의 자료로 종합 디지털 강의 노트 Markdown만 작성하라.
109
+ 강의 진행 순서를 유지하고 손필기/슬라이드의 시각 정보와 요약의 텍스트를 통합하라. 읽지 못한 이미지는 추측하지 말고 출처를 표시하라.
110
+ 다이어그램은 Mermaid 우선으로 해당 설명 위치에 \`\`\`mermaid 코드블록으로 내장하라 (곡선·자유형은 코드로 억지 변환하지 않는다).
111
+ 비교·분류는 표로, 실제 사례는 별도 사례 박스로, 핵심 원칙은 인용문으로 표시하라.
112
+ 수식은 KaTeX 인라인 \\( ... \\) 형식, 강조 수식은 > \\( ... \\) 형식으로 쓰고 $$ 블록 수식이나 볼드/코드 수식은 쓰지 마라.
113
+ 강의 개요 바로 아래에 해당 주차 신규 용어집 표(| 약어 | 영문 | 한국어 | 정의 |)를 배치하라.
114
+ 목차와 각 헤딩 위의 영문 앵커를 만들고, 맨 끝에 다이어그램 목록 부록을 둬라. 근거 없는 내용은 만들어내지 마라.`;
115
+
116
+ export async function draft(folder: string, options: StageOptions = {}): Promise<StageResult> {
117
+ const dir = outputDir(folder);
118
+ const inputs = readInputs(dir);
119
+ if (!inputs.length) throw new Error('강의 자료가 없다');
120
+ const prompt = `${instructions}\n\n자료:\n${inputs.map(i => `### ${i.name} (${i.kind})\n${i.text}`).join('\n\n')}`;
121
+ const ask = options.llm ?? ((request: string) => streamLLM([{ role: 'user', content: request }], () => {}));
122
+ const notes = (await ask(prompt)).trim();
123
+ if (!notes) throw new Error('LLM 초안이 비었다');
124
+ writeFileSync(artifact(dir, 'notes.md'), notes + '\n');
125
+ return { ...counts(dir), outcome: 'ok' };
126
+ }
127
+
128
+ export function render(folder: string, options: StageOptions = {}): StageResult {
129
+ const dir = outputDir(folder);
130
+ const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
131
+ const renderer = join(options.referenceRoot ?? ownRoot, 'diagram-master/references/engines/mermaid/render_mermaid.py');
132
+ const uv = binary('uv', options.toolsPath);
133
+ let unavailable = 0;
134
+ for (const [i, match] of [...notes.matchAll(mermaid)].entries()) {
135
+ const png = artifact(dir, `diagram-${i + 1}.png`);
136
+ if (existsSync(png)) rmSync(png);
137
+ if (!uv || !existsSync(renderer)) { unavailable++; continue; }
138
+ const mmd = artifact(dir, `diagram-${i + 1}.mmd`);
139
+ writeFileSync(mmd, match[1]!);
140
+ const result = run(uv, ['run', 'python', renderer, mmd, '--output', png], dir, 120000);
141
+ if (result.status !== 0 || !existsSync(png)) {
142
+ if (existsSync(png)) rmSync(png);
143
+ throw new Error(`Mermaid 렌더 실패 (diagram-${i + 1}, exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PNG 없음'}`);
144
+ }
145
+ }
146
+ if (unavailable) console.log(`렌더러 없음: ${unavailable}`);
147
+ return { ...counts(dir), outcome: 'ok' };
148
+ }
149
+
150
+ function markdownHtml(text: string): string {
151
+ let suffix = 0;
152
+ while (text.includes(`LECTUREMATHOPEN${suffix}`) || text.includes(`LECTUREMATHCLOSE${suffix}`)) suffix++;
153
+ const open = `LECTUREMATHOPEN${suffix}`;
154
+ const close = `LECTUREMATHCLOSE${suffix}`;
155
+ const protectedText = text.replace(/\\\(/g, open).replace(/\\\)/g, close);
156
+ return String(unified().use(remarkParse).use(remarkGfm)
157
+ .use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).processSync(protectedText))
158
+ .replaceAll(open, '\\(').replaceAll(close, '\\)');
159
+ }
160
+
161
+ export function html(folder: string, options: StageOptions = {}): StageResult {
162
+ const dir = outputDir(folder);
163
+ const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
164
+ const template = readFileSync(join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/html-template.md'), 'utf8');
165
+ const css = template.match(/```css\n([\s\S]*?)\n```/)?.[1];
166
+ if (!css) throw new Error('HTML 템플릿 CSS 없음');
167
+ let n = 0;
168
+ const withoutAppendix = notes.replace(/\n(?:---\n\n)?(?:<a id="appendix"><\/a>\n)?#+ 부록: 다이어그램 목록[\s\S]*$/u, '');
169
+ const anchored = withoutAppendix.replace(/^<a id="([a-zA-Z0-9-]+)"><\/a>$/gm, (_match, id: string) => `\n\nLECTURE_ANCHOR_${id}_END\n\n`);
170
+ const body = markdownHtml(anchored.replace(mermaid, (_block, source: string) => {
171
+ const png = `diagram-${++n}.png`;
172
+ return existsSync(join(dir, png)) ? `\n\n![diagram-${n}](${png})\n\n` : `\n\n\`\`\`mermaid\n${source}\`\`\`\n\n`;
173
+ })).replace(/<p><img src="(diagram-\d+\.png)" alt="(diagram-\d+)"><\/p>/g,
174
+ '<div class="diagram-container"><img src="$1" alt="$2"></div>')
175
+ .replace(/<p>LECTURE_ANCHOR_([a-zA-Z0-9-]+)_END<\/p>/g, '<a id="$1"></a>');
176
+ const title = notes.match(/^# (.+)$/m)?.[1] ?? '강의 노트';
177
+ const sections: string[] = [];
178
+ let heading = 0;
179
+ const numbered = body.replace(/<(h[12])>(.*?)<\/\1>/g, (_m, tag: string, label: string) => {
180
+ const id = `sec-${++heading}`;
181
+ sections.push(`<li><a href="#${id}">${label}</a></li>`);
182
+ return `<${tag} id="${id}">${label}</${tag}>`;
183
+ });
184
+ const page = `<!DOCTYPE html>\n<html lang="ko"><head><meta charset="UTF-8"><title>${escapeHtml(title)}</title>` +
185
+ '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">' +
186
+ '<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>' +
187
+ '<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:\'\\\\(\',right:\'\\\\)\',display:false}]});"></script>' +
188
+ `<style>${css}</style></head><body><div class="cover"><h1>${escapeHtml(title)}</h1></div>` +
189
+ `<div class="toc"><h2>목차</h2><ol>${sections.join('\n')}</ol></div><div class="section">${numbered}</div></body></html>\n`;
190
+ writeFileSync(artifact(dir, 'notes.html'), page);
191
+ return { ...counts(dir), outcome: 'ok' };
192
+ }
193
+
194
+ export function pdf(folder: string, options: StageOptions = {}): StageResult {
195
+ const dir = outputDir(folder);
196
+ const source = artifact(dir, 'notes.html');
197
+ if (!existsSync(source)) throw new Error('notes.html 없음');
198
+ const converter = join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/convert_to_pdf.py');
199
+ const python = binary('python3', options.toolsPath);
200
+ const target = artifact(dir, 'notes.pdf');
201
+ if (existsSync(target)) throw new Error('기존 notes.pdf 를 덮어쓰지 않는다');
202
+ const result = python && existsSync(converter) ? run(python, [converter, source], dir, 120000) : undefined;
203
+ const engineMissing = !result || (result.status !== 0 && !result.error &&
204
+ (/^ModuleNotFoundError: No module named ['"]playwright['"]$/m.test(String(result.stderr)) ||
205
+ /BrowserType\.launch: Executable doesn't exist at/.test(String(result.stderr))));
206
+ if (engineMissing) {
207
+ if (existsSync(target)) rmSync(target);
208
+ console.log('PDF 엔진 없음 — HTML 이 산출물');
209
+ return { ...counts(dir), pdf: 'skipped', outcome: 'skipped' };
210
+ }
211
+ if (result && (result.status !== 0 || !existsSync(target))) {
212
+ if (existsSync(target)) rmSync(target);
213
+ throw new Error(`PDF 변환 실패 (exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PDF 없음'}`);
214
+ }
215
+ return { ...counts(dir), outcome: 'ok' };
216
+ }
217
+
218
+ export async function runStage(stage: Stage, folder: string, options: StageOptions = {}): Promise<StageResult> {
219
+ const result = stage === 'collect' ? collect(folder, options) : stage === 'draft' ? await draft(folder, options) :
220
+ stage === 'render' ? render(folder, options) : stage === 'html' ? html(folder, options) : pdf(folder, options);
221
+ debug.log('lecture.note', stage, { inputs: result.inputs, diagrams: result.diagrams, rendered: result.rendered, pdf: result.pdf });
222
+ return result;
223
+ }
224
+
225
+ function folderFromContext(): string {
226
+ const arg = process.argv.indexOf('--folder');
227
+ if (arg >= 0) {
228
+ const folder = process.argv[arg + 1];
229
+ if (!folder) throw new Error('--folder 값 없음');
230
+ return resolve(folder.replace(/^~(?=\/)/, homedir()));
231
+ }
232
+ const context = process.env.ELANOUS_GRAPH_CONTEXT;
233
+ if (!context) throw new Error('input.folder 필요');
234
+ const data: unknown = JSON.parse(context.trim().startsWith('{') ? context : readFileSync(context, 'utf8'));
235
+ const folder = (data as { input?: { folder?: unknown } })?.input?.folder;
236
+ if (typeof folder !== 'string' || !folder.trim()) throw new Error('input.folder 필요');
237
+ const expanded = folder.replace(/^~(?=\/)/, homedir());
238
+ if (!isAbsolute(expanded)) throw new Error('input.folder 절대 경로 필요');
239
+ return resolve(expanded);
240
+ }
241
+
242
+ if (import.meta.main) {
243
+ const stage = process.argv[2];
244
+ if (!['collect', 'draft', 'render', 'html', 'pdf'].includes(stage ?? '')) throw new Error('stage: collect|draft|render|html|pdf');
245
+ let folder = '';
246
+ try {
247
+ folder = folderFromContext();
248
+ const result = await runStage(stage as Stage, folder);
249
+ console.log(`lecture-note ${folder} inputs=${result.inputs} diagrams=${result.diagrams} rendered=${result.rendered} html=${result.html} pdf=${result.pdf}`);
250
+ console.log(JSON.stringify({ outcome: result.outcome }));
251
+ } catch (error) {
252
+ console.error(error);
253
+ console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
254
+ process.exitCode = 1;
255
+ }
256
+ }
@@ -0,0 +1,31 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ export function parseInjectArgs(
4
+ argv: string[],
5
+ env: { TELEGRAM_TEST_BOT?: string | undefined },
6
+ ): { to: string; text?: string; photos: string[]; caption?: string } {
7
+ const value = (flag: string): string | undefined => {
8
+ const index = argv.indexOf(flag);
9
+ if (index < 0) return undefined;
10
+ const next = argv[index + 1];
11
+ if (!next || next.startsWith('--')) throw new Error(`옵션 값 없음: ${flag}`);
12
+ return next;
13
+ };
14
+ const to = value('--to') ?? env.TELEGRAM_TEST_BOT;
15
+ const text = value('--text');
16
+ const photos: string[] = [];
17
+ for (let i = 0; i < argv.length; i++) {
18
+ if (argv[i] !== '--photo') continue;
19
+ const photo = argv[++i];
20
+ if (!photo || photo.startsWith('--') || !existsSync(photo)) {
21
+ throw new Error(`사진 파일 경로 없음: ${photo ?? '(미지정)'}`);
22
+ }
23
+ photos.push(photo);
24
+ }
25
+ if (!to || (!text && photos.length === 0)) {
26
+ throw new Error('사용: telegram-inject.ts --to @monad_test_bot --text "<objective>"');
27
+ }
28
+ return photos.length > 0
29
+ ? { to, photos, ...(text !== undefined ? { caption: text } : {}) }
30
+ : { to, text, photos };
31
+ }
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bun
2
+ /** Seven-day read-only measurement of self-implement Pod memory by goal kind. */
3
+ import { existsSync } from 'node:fs';
4
+ import { LogStore, logsDbPath, type LogStoreRow } from '../src/mss/logging/log-store.js';
5
+
6
+ export const GOAL_TYPES = ['test', 'code', 'pwa-build', 'docs'] as const;
7
+ export type GoalType = typeof GOAL_TYPES[number];
8
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const MIB = 1024 * 1024;
10
+
11
+ type Observation = Pick<LogStoreRow, 'ts_ms' | 'event' | 'data' | 'category'>;
12
+ type Run = { type: GoalType | null; limit: string | null; peakMiB: number | null; samples: number; terminated: boolean; oomKilled: boolean };
13
+ type MemorySample = { cgroupBytes?: unknown };
14
+ export type MemoryMeasurement = {
15
+ window: { since: string; until: string };
16
+ sources: { logs: string; observations: number; samples: number; unclassifiedRuns: number; classification: 'goal-fields-or-space-id'; peakCoverage: 'recorded-samples-only' };
17
+ byGoalType: Array<{ goalType: GoalType; runs: number; measured: number; missingPeak: number; samples: number; memoryLimits: Record<string, number>; terminations: number; oomKilled: number; peakMiB: { p50: number | null; p95: number | null; max: number | null } }>;
18
+ };
19
+
20
+ function payload(raw: string | null): Record<string, unknown> {
21
+ try {
22
+ const parsed: unknown = JSON.parse(raw ?? 'null');
23
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
24
+ } catch { return {}; }
25
+ }
26
+
27
+ /** Infer only unambiguous kinds from free text; never turn an unknown title into code. */
28
+ export function goalTypeOf(description: string): GoalType | null {
29
+ const kinds: GoalType[] = [];
30
+ if (/\b(?:pwa|next\.js|frontend|front-end|web app)[-\s]+(?:build|compile|typecheck|bundle)\b/i.test(description)) kinds.push('pwa-build');
31
+ if (/\b(?:test|tests|testing|spec|coverage|gate)\b/i.test(description) || /시험|테스트|검증/.test(description)) kinds.push('test');
32
+ if (/\b(?:docs?|documentation|readme|manual|report)\b/i.test(description) || /문서|보고서/.test(description)) kinds.push('docs');
33
+ if (/\b(?:implement|implementation|coding|code|cli)\b/i.test(description) || /구현|코드/.test(description)) kinds.push('code');
34
+ return kinds.length === 1 ? kinds[0]! : null;
35
+ }
36
+
37
+ function recordedGoalType(data: Record<string, unknown>, spaceId: string): GoalType | null {
38
+ if ('goalType' in data) return GOAL_TYPES.find((type) => type === data.goalType) ?? null;
39
+ if ('goalKind' in data) return GOAL_TYPES.find((type) => type === data.goalKind) ?? null;
40
+ const descriptions = [data.goalTitle, data.goalDescription, data.feature].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
41
+ return descriptions.length ? goalTypeOf(descriptions.join(' ')) : goalTypeOf(spaceId);
42
+ }
43
+
44
+ function percentile(sorted: readonly number[], fraction: number): number | null {
45
+ if (!sorted.length) return null;
46
+ const index = (sorted.length - 1) * fraction;
47
+ const low = Math.floor(index);
48
+ return Math.round((sorted[low]! + (sorted[Math.ceil(index)]! - sorted[low]!) * (index - low)) * 100) / 100;
49
+ }
50
+
51
+ /** All rows are read before aggregation: log query's default 100-row limit is not a seven-day census. */
52
+ export function measurePodMemoryByGoal(rows: readonly Observation[], nowMs: number = Date.now(), logs = logsDbPath()): MemoryMeasurement {
53
+ const sinceMs = nowMs - WEEK_MS;
54
+ const windowRows = rows.filter((row) => row.ts_ms >= sinceMs && row.ts_ms <= nowMs && row.category === 'self-implement.pod');
55
+ const runs = new Map<string, Run>();
56
+ const jobToSpace = new Map<string, string>();
57
+ let samples = 0;
58
+ const ordered = [...windowRows].sort((a, b) => a.ts_ms - b.ts_ms);
59
+ for (const row of ordered) {
60
+ if (row.event !== 'memory-limit') continue;
61
+ const data = payload(row.data);
62
+ const id = typeof data.spaceId === 'string' ? data.spaceId : '';
63
+ if (!id) continue;
64
+ runs.set(id, { type: recordedGoalType(data, id), limit: typeof data.memoryLimit === 'string' ? data.memoryLimit : null,
65
+ peakMiB: null, samples: 0, terminated: false, oomKilled: false });
66
+ }
67
+ for (const row of ordered) {
68
+ const data = payload(row.data);
69
+ const job = typeof data.job === 'string' ? data.job : '';
70
+ const directId = typeof data.spaceId === 'string' ? data.spaceId : '';
71
+ if (row.event === 'job-applied' && job && runs.has(directId)) jobToSpace.set(job, directId);
72
+ const run = runs.get(directId || jobToSpace.get(job) || '');
73
+ if (!run) continue;
74
+ if (row.event === 'job-finished') {
75
+ run.terminated = true;
76
+ run.oomKilled ||= data.containerReason === 'OOMKilled';
77
+ }
78
+ if (row.event === 'oom-evidence') { run.terminated = true; run.oomKilled = true; }
79
+ if (data.reason === 'OOMKilled') { run.terminated = true; run.oomKilled = true; }
80
+ if (row.event !== 'oom-evidence' && row.event !== 'memory-last') continue;
81
+ const rawSamples: unknown[] = row.event === 'oom-evidence' && Array.isArray(data.samples) ? data.samples : [data.sample];
82
+ for (const raw of rawSamples) {
83
+ const record = raw && typeof raw === 'object' ? raw as MemorySample : {};
84
+ const bytes = record.cgroupBytes;
85
+ if (typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 0) continue;
86
+ const reading = bytes / MIB;
87
+ run.peakMiB = Math.max(run.peakMiB ?? 0, reading);
88
+ run.samples++;
89
+ samples++;
90
+ }
91
+ }
92
+ return {
93
+ window: { since: new Date(sinceMs).toISOString(), until: new Date(nowMs).toISOString() },
94
+ sources: { logs, observations: windowRows.length, samples, unclassifiedRuns: [...runs.values()].filter((run) => run.type === null).length,
95
+ classification: 'goal-fields-or-space-id', peakCoverage: 'recorded-samples-only' },
96
+ byGoalType: GOAL_TYPES.map((goalType) => {
97
+ const group = [...runs.values()].filter((run) => run.type === goalType);
98
+ const peaks = group.flatMap((run) => run.peakMiB === null ? [] : [run.peakMiB]).sort((a, b) => a - b);
99
+ const memoryLimits: Record<string, number> = {};
100
+ for (const run of group) if (run.limit) memoryLimits[run.limit] = (memoryLimits[run.limit] ?? 0) + 1;
101
+ return { goalType, runs: group.length, measured: peaks.length, missingPeak: group.length - peaks.length,
102
+ samples: group.reduce((count, run) => count + run.samples, 0), memoryLimits,
103
+ terminations: group.filter((run) => run.terminated).length, oomKilled: group.filter((run) => run.oomKilled).length,
104
+ peakMiB: { p50: percentile(peaks, 0.5), p95: percentile(peaks, 0.95), max: peaks.at(-1) ?? null } };
105
+ }),
106
+ };
107
+ }
108
+
109
+ export function readPodMemoryMeasurement(path: string = logsDbPath(), nowMs: number = Date.now()): MemoryMeasurement {
110
+ if (!existsSync(path)) throw new Error(`log store not found: ${path}`);
111
+ const store = LogStore.openReadOnly(path);
112
+ try {
113
+ return measurePodMemoryByGoal(store.queryAll({ exactCategories: ['self-implement.pod'], sinceMs: nowMs - WEEK_MS, untilMs: nowMs }), nowMs, path);
114
+ } finally { store.close(); }
115
+ }
116
+
117
+ if (import.meta.main) {
118
+ const args = process.argv.slice(2);
119
+ const usage = 'Usage: bun scripts/measure-pod-memory-by-goal.ts [--json] [--logs-db <existing-logs.db>] [--help]';
120
+ const dbArg = args.indexOf('--logs-db');
121
+ const db = dbArg === -1 ? logsDbPath() : args[dbArg + 1];
122
+ if (!db || (dbArg !== -1 && (db.startsWith('--') || args.lastIndexOf('--logs-db') !== dbArg))
123
+ || args.some((arg, index) => !['--json', '--help', '--logs-db'].includes(arg) && index !== dbArg + 1)) throw new Error(usage);
124
+ if (args.includes('--help')) console.log(usage);
125
+ else {
126
+ const result = readPodMemoryMeasurement(db);
127
+ if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
128
+ else for (const group of result.byGoalType) console.log(`${group.goalType}: runs=${group.runs} measured=${group.measured} peak p50=${group.peakMiB.p50 ?? 'n/a'} MiB p95=${group.peakMiB.p95 ?? 'n/a'} MiB OOMKilled=${group.oomKilled}`);
129
+ }
130
+ }
@@ -15,7 +15,7 @@ const outcomeMetric = (name: string, node: Record<string, unknown> | undefined):
15
15
  });
16
16
 
17
17
  /** Rows shown in auto-approval.md but never counted toward the 8 blocking metrics. */
18
- const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
18
+ export const WARNING_ONLY = new Set(['tui-regress', 'mac-smoke']);
19
19
 
20
20
  /** `ok` · `warn` (a failure) · `unmeasured` (error or no output) — the macOS smoke never blocks publishing. */
21
21
  function macSmokeLevel(node: Record<string, unknown> | undefined): 'ok' | 'warn' | 'unmeasured' {
@@ -11,10 +11,11 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
11
11
  if (gate.ok && gate.moved.length) {
12
12
  const [major, minor, patch] = version.split('.').map(Number);
13
13
  const next = `${major}.${minor}.${patch! + 1}`;
14
+ // 이월은 «같은 칸»을 다음 판으로 옮기는 것 — 다른 판 id 충돌 검사(CKID #23407)의 대상이 아니다.
14
15
  for (const id of gate.moved) {
15
16
  const item = data.items.find((candidate) => candidate.id === id)!;
16
17
  if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
17
- try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
18
+ try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }, { allowDuplicateId: true }); }
18
19
  catch (error) {
19
20
  if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
20
21
  }
@@ -18,7 +18,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
18
18
  import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
19
19
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
20
20
 
21
- interface CommandResult { rc: number; output: string; passedIds?: string[] }
21
+ interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }> }
22
22
  export interface GateRunner {
23
23
  command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
24
24
  localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
@@ -50,6 +50,7 @@ export interface GateResult {
50
50
  fixed: number;
51
51
  knownEnv: number;
52
52
  knownEnvCleared: string[];
53
+ stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
53
54
  baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
54
55
  durationMs: number;
55
56
  error?: string;
@@ -124,7 +125,8 @@ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-20
124
125
  /** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
125
126
  export const POD_SHARD_FILE_CAP = 27;
126
127
 
127
- export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
128
+ // review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
129
+ export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
128
130
  /** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
129
131
  export const GATE_NIGHTLY_AUDITS: readonly string[] = [
130
132
  // Replaces F12's real-repo bucket B apps/pwa scope check and its whole-repo export partition check.
@@ -267,7 +269,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
267
269
  }, 0);
268
270
  const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
269
271
  const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
270
- const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
272
+ const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
271
273
  const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
272
274
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
273
275
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
@@ -321,13 +323,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
321
323
  const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
322
324
  const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
323
325
  const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
326
+ const errors = clean === undefined ? NaN : summaryCount(clean, 'error');
324
327
  let reason: ShardReason | undefined;
325
328
  let namedFailures: number | undefined;
326
329
  let unattributedDetail: string | undefined;
327
330
  if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
328
331
  else if (!clean || rc === undefined) reason = 'no-output';
329
332
  else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
330
- || Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
333
+ || Number(ran[2]) === 0 || Number(ran[2]) > paths.length
334
+ || (Number(ran[1]) === 0 && !(errors > 0))) reason = 'incomplete';
331
335
  else {
332
336
  try {
333
337
  const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
@@ -354,6 +358,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
354
358
  }
355
359
  }
356
360
  }
361
+ if (reason === 'no-output' && !retriedNoOutput) {
362
+ const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
363
+ debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
364
+ return retry;
365
+ }
357
366
  if (reason) {
358
367
  // 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
359
368
  // 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
@@ -394,7 +403,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
394
403
  return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
395
404
  }
396
405
  return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
397
- errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
406
+ errors: Number.isNaN(errors) ? 0 : errors,
398
407
  ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
399
408
  };
400
409
  // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
@@ -407,11 +416,12 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
407
416
  const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
408
417
  errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
409
418
  { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
410
- if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
411
- if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
419
+ if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1)) throw new StalledPodShards(stalledShards, total);
420
+ if (total.ran === 0 && total.errors === 0 && !stalledShards.length) throw new Error('sweep incomplete: no tests ran');
412
421
  const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
413
422
  return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`,
414
- passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
423
+ passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort(),
424
+ stalledEnv: stalledShards.flatMap((item) => item.files.map((file) => ({ file, reason: 'no-output' as const }))) };
415
425
  };
416
426
  return {
417
427
  command,
@@ -544,7 +554,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
544
554
  return baselineCommit(root, version);
545
555
  };
546
556
  const repo = resolve(opts.repo ?? process.cwd());
547
- const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
557
+ const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: 0 };
548
558
  let work: string | undefined;
549
559
  let baseSnapshot = false;
550
560
  let cutFailures: SweepFailures | undefined;
@@ -595,7 +605,38 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
595
605
  for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
596
606
  const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
597
607
  opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
598
- const cut = failuresOf(cutRun, 'cut sweep');
608
+ const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
609
+ const stalled = run.stalledEnv ?? [];
610
+ const noCompletedShard = stalled.length && run.rc === 0
611
+ && /(?:^|\n)0 pass\n0 fail\n0 errors\nRan 0 tests across 0 files\./.test(run.output);
612
+ const measured: SweepFailures = noCompletedShard ? { failures: [], errors: [] } : failuresOf(run, `${label} sweep`);
613
+ if (!stalled.length) return measured;
614
+ let hostTree = tree;
615
+ const local: GateResult['stalledEnv'] = [];
616
+ try {
617
+ if (opts.remote) {
618
+ hostTree = join(mkdtempSync(join(tmpdir(), 'release-gate-local-')), 'cut');
619
+ check(await runner.localCommand('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, hostTree], repo), 'host cut clone');
620
+ check(await runner.localCommand('git', ['checkout', '--quiet', '--detach', commit], hostTree), 'host cut checkout');
621
+ for (const dir of [hostTree, join(hostTree, 'apps/pwa')]) check(await runner.localCommand('bun', ['install'], dir), `host bun install ${dir}`);
622
+ }
623
+ for (const { file, reason } of stalled) {
624
+ if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
625
+ const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree);
626
+ const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
627
+ if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
628
+ measured.failures.push(...parsed.failures);
629
+ measured.errors.push(...parsed.errors);
630
+ local.push({ file, reason, local: parsed.failures.length || parsed.errors.length ? 'failed' : 'passed' });
631
+ }
632
+ } finally {
633
+ if (opts.remote && hostTree !== tree) rmSync(dirname(hostTree), { recursive: true, force: true });
634
+ }
635
+ result.stalledEnv.push(...local);
636
+ debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
637
+ return measured;
638
+ };
639
+ const cut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
599
640
  cutFailures = cut;
600
641
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
601
642
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
@@ -624,22 +665,24 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
624
665
  // hour of local baseline after a 23-minute Pod cut).
625
666
  const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
626
667
  opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
627
- baseline = failuresOf(baseRun, 'baseline sweep');
668
+ baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
628
669
  }
629
670
  for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
630
671
  const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
631
672
  const cutIds = [...cut.failures, ...cut.errors];
673
+ const cutHostFailed = new Set(cutRun.stalledEnv?.map(({ file }) => file) ?? []);
632
674
  const baselineIds = [...baseline.failures, ...baseline.errors];
633
675
  const countedCut = splitKnownEnv(cutIds, known);
634
676
  const countedBaseline = splitKnownEnv(baselineIds, known);
635
677
  result.knownEnv = countedCut.knownEnv.length;
636
- result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
678
+ result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
637
679
  const diff = diffFailures(countedCut.counted, countedBaseline.counted);
638
680
  result.fixed = diff.fixed.length;
639
681
  result.preexisting = diff.common.length;
640
682
  for (const file of new Set(diff.newFailures.map(fileOf))) {
641
- const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
642
- const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
683
+ const isolatedCut = cutHostFailed.has(file)
684
+ ? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
685
+ : failuresOf(await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree), `cut isolated ${file}`);
643
686
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
644
687
  const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
645
688
  if (candidates.length === 0) continue;
@@ -684,7 +727,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
684
727
  if (opts.remote) runner.remoteMirror = undefined;
685
728
  result.durationMs = Date.now() - start;
686
729
  }
687
- if (cutFailures && !result.stalledShards?.length) {
730
+ if (cutFailures && !result.stalledShards?.length && !result.stalledEnv.length) {
688
731
  try {
689
732
  for (const location of roots) {
690
733
  const path = failureFile(location, opts.version);
@@ -708,7 +751,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
708
751
  export function graphGateResult(result: GateResult, graph: boolean) {
709
752
  return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
710
753
  verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
711
- summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
754
+ summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
755
+ + (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
712
756
  }
713
757
 
714
758
  export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
@@ -766,7 +810,7 @@ if (import.meta.main) {
766
810
  version = opts.version;
767
811
  result = await judgeGate(opts);
768
812
  } catch (error) {
769
- result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
813
+ result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: Date.now() - start, error: String(error) };
770
814
  }
771
815
  if (result.error) console.error(result.error);
772
816
  debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });