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
@@ -19,7 +19,7 @@ import { join, resolve } from 'node:path';
19
19
  import { debug } from '../../debug/log.js';
20
20
  import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
21
21
  import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
22
- import { HostPoolLease } from '../../pod-lease/host-lease.js';
22
+ import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
23
23
  import { measurePoolLease, recommendConcurrency, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
24
24
 
25
25
  export interface PodPoolMember {
@@ -143,6 +143,10 @@ export function genericConfigPodPool(config: { pod?: { pool?: string }; harness?
143
143
  }
144
144
 
145
145
  /** 노드 자리 배분 — 우선순위 순서로 첫 빈 자리. 순수(시각·대기는 호출자가). */
146
+ export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLease {
147
+ return new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
148
+ }
149
+
146
150
  export class PodPoolScheduler {
147
151
  private readonly inflight = new Map<string, number>();
148
152
  private readonly admission: PodLeaseAdmission;
@@ -157,7 +161,7 @@ export class PodPoolScheduler {
157
161
  constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
158
162
  this.pollMs = options.pollMs ?? 15_000;
159
163
  // One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
160
- this.hostLease = options.hostLease ?? new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
164
+ this.hostLease = options.hostLease ?? podPoolHostLease(members);
161
165
  const measureStatus = options.status ?? (() => {
162
166
  const measure = measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) });
163
167
  return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
@@ -166,9 +170,13 @@ export class PodPoolScheduler {
166
170
  this.admission = new PodLeaseAdmission({ status: async () => {
167
171
  const measured = await measureStatus();
168
172
  this.lastRaw = measured;
169
- // Launches admitted by other processes on this host but not yet visible as Jobs hold free slots too.
170
- const others = measured.recommended === null ? 0 : this.hostLease.othersPending();
171
- const decision = measured.recommended === null ? measured : { ...measured, recommended: Math.max(0, measured.recommended - others) };
173
+ const pendingJobs = measured.pendingJobs ?? [];
174
+ const ownStart = this.hostLease.selfStart();
175
+ const transferId = process.env.ELANOUS_POD_RESERVED_LEASE;
176
+ const transferIsOther = !!transferId && this.hostLease.reservationIsOther(transferId);
177
+ const others = measured.recommended === null ? 0 : this.hostLease.live().filter((r) =>
178
+ (r.pid !== process.pid || r.startedAt !== ownStart) && !leaseHasPendingPod(r, pendingJobs)).length;
179
+ const decision = measured.recommended === null ? measured : { ...measured, recommended: Math.max(0, measured.recommended - others + (transferIsOther ? 1 : 0)) };
172
180
  // An unknown reading cannot erase permits already granted: on recovery,
173
181
  // the first healthy recommendation must still account for their slots.
174
182
  if (decision.recommended === null) return decision;
@@ -181,19 +189,28 @@ export class PodPoolScheduler {
181
189
  }, ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged: options.dependencyMerged ?? ((after, signal) => predecessorState(after, {}, signal)) });
182
190
  }
183
191
  /** FIFO gate within this process, then a host lease so independent CLI processes cannot over-admit the same pool. */
184
- async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease & { applied: () => void }> {
192
+ async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease & { applied: (job: string, context: string, namespace: string) => void; observed: () => void }> {
185
193
  const local = await this.acquireLocalAdmission(signal, after);
186
194
  try {
187
195
  for (;;) {
188
196
  if (signal?.aborted) throw new Error('pod lease admission aborted');
189
197
  const free = this.lastRaw?.recommended ?? null;
190
- // Same check as the local gate, but atomic across processes: other unapplied leases must stay under the free slots.
191
- const lease = free !== null && free > 0 ? this.hostLease.tryReserve((others) => others < free) : null;
198
+ // Same check as the local gate, but atomic across processes: other authoring and pre-Running leases hold slots.
199
+ const reservedId = process.env.ELANOUS_POD_RESERVED_LEASE;
200
+ const transferred = reservedId ? this.hostLease.claim(reservedId) : null;
201
+ if (reservedId && !transferred) throw new Error('pod lease: authoring reservation could not be transferred');
202
+ if (transferred) delete process.env.ELANOUS_POD_RESERVED_LEASE;
203
+ const pendingJobs = this.lastRaw?.pendingJobs ?? [];
204
+ const lease = transferred ?? (free !== null && free > 0 ? this.hostLease.tryReserve((others) =>
205
+ others - this.hostLease.live().filter((r) => leaseHasPendingPod(r, pendingJobs) &&
206
+ (r.pid !== process.pid || r.startedAt !== this.hostLease.selfStart())).length < free) : null);
192
207
  if (lease) {
193
208
  let hostReleased = false;
194
209
  const releaseHost = () => { if (!hostReleased) { hostReleased = true; lease(); } };
195
- // Once the Job is applied the cluster measurement counts it — the host lease would double count.
196
- return Object.assign(() => { releaseHost(); local(); }, { applied: releaseHost });
210
+ return Object.assign(() => { releaseHost(); local(); }, {
211
+ applied: (job: string, context: string, namespace: string) => lease.applied(job, context, namespace),
212
+ observed: releaseHost,
213
+ });
197
214
  }
198
215
  await new Promise<void>((resolve, reject) => {
199
216
  const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, this.pollMs);
@@ -58,6 +58,9 @@ import { envLiteral } from '../../platform/env-literal.js';
58
58
  import { LLM_TIER_MAP_BY_PROVIDER, lookupLlmTierSpec, type LlmTierProvider } from '../../model-tier/llm-tier-map.js';
59
59
  import { parseSelfImplementJson, type SelfImplementJobDone, type SelfImplementJobSpawn } from './self-implement.js';
60
60
  import { codexQuotaPolicyFromConfig } from '../../oauth/codex-account-store.js';
61
+ import { goalTypeOf } from '../../../scripts/measure-pod-memory-by-goal.js';
62
+ import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
63
+ import { getUserConfig } from '../../user-config.js';
61
64
 
62
65
  export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
63
66
 
@@ -132,6 +135,10 @@ export interface PodSpawnOptions {
132
135
  env?: NodeJS.ProcessEnv;
133
136
  /** Detail switch file (test injection; defaults to the host's local/production selection). */
134
137
  liveDetailFile?: string;
138
+ /** Test seam for the opt-in user configuration; otherwise read the active user config. */
139
+ adviseDefaults?: boolean;
140
+ /** Test seam for the read-only measured advice. */
141
+ memoryAdvice?: () => PodMemoryAdvice;
135
142
  }
136
143
 
137
144
  export function defaultKubectl(args: readonly string[], input?: string): { status: number | null; stdout: string; stderr: string } {
@@ -411,7 +418,7 @@ export const POD_MEMORY_HIGH_DEFAULT = '32Gi';
411
418
  export const POD_MEMORY_LITE_DEFAULT = '2Gi';
412
419
  export const POD_MEMORY_TIERS = ['lite', 'standard', 'high'] as const;
413
420
  export type PodMemoryTier = (typeof POD_MEMORY_TIERS)[number];
414
- export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto';
421
+ export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto' | 'advise';
415
422
  const PWA_PATH = /(?:^|[\s`'"(,·])apps\/pwa\//m;
416
423
  const GOAL_LINE = /^[ \t]*(?:Pod 메모리|pod-memory)[ \t]*:[ \t]*(lite|standard|high)[ \t]*\r?$/im;
417
424
 
@@ -436,6 +443,12 @@ export function goalTouchesPwa(feature: string): boolean {
436
443
  return PWA_PATH.test(feature);
437
444
  }
438
445
 
446
+ /** A launch-time warning, not a memory-tier override or a test-command parser. */
447
+ export function broadPodTestWarning(goal: string): string | null {
448
+ const broad = /(?:전체\s*(?:시험|테스트)|(?:full|whole|entire|all)\s+(?:test|suite)|(?:전체|모든)\s*(?:test|suite)|\bbun\s+test\s+(?:[\w./-]+\/|[\w./-]+(?<!\.[cm]?[jt]sx?))(?:\s|$)|\bbun\s+test\s*(?:$|[;&|]))/imu.test(goal);
449
+ return broad ? '[pod] 넓은 시험 골 경고: 10-04 W10b 32Gi OOM — 넓은 bun test 한 프로세스가 16GB+ 사용; 바꾼 시험 파일만 지정해 실행하세요.' : null;
450
+ }
451
+
439
452
  export function parsePodMemoryTier(v: string | undefined): PodMemoryTier | null {
440
453
  const t = v?.trim().toLowerCase();
441
454
  return t && (POD_MEMORY_TIERS as readonly string[]).includes(t) ? (t as PodMemoryTier) : null;
@@ -447,14 +460,21 @@ function memoryGi(v: string): number | null {
447
460
  return m[2] === 'Gi' ? Number(m[1]) : Number(m[1]) / 1024;
448
461
  }
449
462
 
450
- export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>, parentGoal?: string): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
463
+ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>, parentGoal?: string, advice?: PodMemoryAdvice): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
451
464
  const option = parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER);
452
465
  const line = parsePodMemoryTier(GOAL_LINE.exec(feature)?.[1]);
453
- const parentLine = !option && !line && !goalTouchesPwa(feature) ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
466
+ const parentLine = !option && !line ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
467
+ const goalType = advice && !option && !line && !parentLine
468
+ ? (broadPodTestWarning(feature) ? 'test' : goalTypeOf(feature))
469
+ ?? (parentGoal ? (broadPodTestWarning(parentGoal) ? 'test' : goalTypeOf(parentGoal)) : null)
470
+ : null;
471
+ const recommendation = advice?.byGoalType.find((entry) => entry.goalType === goalType && entry.evidence.runs > 0)?.recommended;
454
472
  const [tier, source]: [PodMemoryTier, PodMemorySource] = option ? [option, 'option']
455
473
  : line ? [line, 'goal-line']
474
+ // 조각 자신의 PWA 경로가 부모 줄·실측 권고보다 먼저다 — 부모 «standard» 가 PWA 조각을 16Gi 로 내리면 OOM(POD7 · #23569 순서 회귀)
456
475
  : goalTouchesPwa(feature) ? ['high', 'pwa-auto']
457
476
  : parentLine ? [parentLine, 'parent-goal-line']
477
+ : recommendation ? [recommendation, 'advise']
458
478
  : parentGoal && goalTouchesPwa(parentGoal) ? ['high', 'parent-pwa-auto']
459
479
  : ['standard', 'default'];
460
480
  if (tier === 'lite') return { limit: env.ELANOUS_POD_MEMORY_LITE?.trim() || POD_MEMORY_LITE_DEFAULT, tier, source };
@@ -634,7 +654,6 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
634
654
  const hostSupervised = options.hostSupervised ?? true;
635
655
  return (input) => {
636
656
  const name = podJobName(input.spaceId);
637
- const featureMemory = podMemoryLimitFor(input.feature, env);
638
657
  let parentGoal: string | undefined;
639
658
  let parentGoalUnreadable = false;
640
659
  let inheritedFrom: string | undefined;
@@ -652,7 +671,14 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
652
671
  inheritedFrom = withinRoot;
653
672
  } catch { parentGoalUnreadable = true; }
654
673
  }
655
- const { limit: selectedMemoryLimit, tier: memoryTier, source: memorySource } = parentGoal === undefined ? featureMemory : podMemoryLimitFor(input.feature, env, parentGoal);
674
+ let advice: PodMemoryAdvice | undefined;
675
+ const explicitMemory = Boolean(parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER) || GOAL_LINE.test(input.feature) || GOAL_LINE.test(parentGoal ?? ''));
676
+ const useAdvice = options.adviseDefaults ?? getUserConfig().pod?.memory?.adviseDefaults === true;
677
+ if (useAdvice && !explicitMemory) {
678
+ try { advice = (options.memoryAdvice ?? readPodMemoryAdvice)(); }
679
+ catch (error) { debug.log('self-implement.pod', 'memory-advice-unavailable', { spaceId: input.spaceId, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
680
+ }
681
+ const { limit: selectedMemoryLimit, tier: memoryTier, source: memorySource } = podMemoryLimitFor(input.feature, env, parentGoal, advice);
656
682
  for (const [where, text] of [['feature', input.feature], ['parent-goal', parentGoal]] as const) {
657
683
  if (text && text.split(/\r?\n/u).some((line) => /Pod 메모리/u.test(line) && !GOAL_LINE.test(line))) {
658
684
  const reason = '한 줄 단독 `Pod 메모리: high|standard` 형식이 아님';
@@ -660,6 +686,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
660
686
  debug.log('self-implement.pod', 'memory-directive-ignored', { spaceId: input.spaceId, where, reason }, { level: 'warn' });
661
687
  }
662
688
  }
689
+ const broadTestWarning = broadPodTestWarning(`${input.feature}\n${parentGoal ?? ''}`);
690
+ if (broadTestWarning) console.warn(broadTestWarning);
663
691
  let memoryLimit = selectedMemoryLimit;
664
692
  const after = input.after ?? podPredecessorFor(input.feature, env, parentGoal);
665
693
  if (after !== undefined) debug.log('self-implement.pod', 'predecessor', { spaceId: input.spaceId, after, source: input.after !== undefined ? 'input' : 'goal-or-option' });
@@ -708,7 +736,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
708
736
  if (recoveryError) return { exitCode: 1, output: '', error: { code: 'pod-oom-recovery', message: recoveryError } };
709
737
  // Each Job attempt has its own host lease; releasing the first after OOM lets the retry
710
738
  // compete with other processes for the newly measured capacity before applying another Job.
711
- let releaseAdmission: ((() => void) & { applied?: () => void }) | undefined;
739
+ let releaseAdmission: ((() => void) & { applied?: (job: string, context: string, namespace: string) => void; observed?: () => void }) | undefined;
712
740
  try {
713
741
  // Every attempt, including an OOM retry, must acquire a pool slot before applying a Job.
714
742
  const acquireSlot = async (): Promise<PodPoolMember | null> => {
@@ -1059,6 +1087,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1059
1087
  const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1060
1088
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
1061
1089
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
1090
+ releaseAdmission?.applied?.(name, context, namespace);
1062
1091
  if (options.source?.kind === 'bundle') {
1063
1092
  const podName = await waitForRunningPod(kubectl, namespace, name, sleep);
1064
1093
  if (!podName) {
@@ -1066,6 +1095,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1066
1095
  cleanupSecret();
1067
1096
  return { exitCode: 1, output: '', error: { code: 'pod-source', message: `Pod for ${name} did not become Running` } };
1068
1097
  }
1098
+ releaseAdmission?.observed?.();
1099
+ if (releaseAdmission) delete releaseAdmission.observed;
1069
1100
  const copied = kubectl(['cp', options.source.bundlePath, `${podName}:/tmp/source.bundle`, '-c', 'child', '-n', namespace]);
1070
1101
  if (copied.status !== 0) {
1071
1102
  debug.log('self-implement.pod', 'source-mismatch', { job: name, kind: 'bundle', headCommit: options.source.headCommit });
@@ -1098,7 +1129,6 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1098
1129
  }
1099
1130
  writePodFragment({ spaceId: input.spaceId, context, namespace, job: name, inboxDir: POD_CONTROL_INBOX_DIR, ...(liveChildRunId === normalizeRunId(liveChildRunId) ? { runId: liveChildRunId } : {}), ...(parentRunId && normalizeRunId(parentRunId) === parentRunId ? { parentRunId } : {}) }, env);
1100
1131
  recorded = true;
1101
- releaseAdmission?.applied?.();
1102
1132
  debug.log('self-implement.pod', 'job-applied', { job: name, namespace, ...(member ? { context: member.context } : {}), image: jobImage, spaceId: input.spaceId, passEnv, extraArgs: options.extraArgs ?? [], ...(options.armEnv?.ELANOUS_ARM_ID ? { armId: options.armEnv.ELANOUS_ARM_ID } : {}) });
1103
1133
  let state: 'complete' | 'failed' | 'aborted' = 'failed';
1104
1134
  let failedReason = '';
@@ -1114,8 +1144,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1114
1144
  }
1115
1145
  const g = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[*].type}']);
1116
1146
  const types = g.stdout;
1117
- if (/Complete|SuccessCriteriaMet/.test(types)) { state = 'complete'; break; }
1147
+ if (/Complete|SuccessCriteriaMet/.test(types)) { releaseAdmission?.observed?.(); state = 'complete'; break; }
1118
1148
  if (/Failed|FailureTarget/.test(types)) {
1149
+ releaseAdmission?.observed?.();
1119
1150
  state = 'failed';
1120
1151
  const reason = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[?(@.type=="Failed")].reason}']);
1121
1152
  failedReason = reason.status === 0 ? reason.stdout.trim() : '';
@@ -1135,6 +1166,13 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1135
1166
  debug.log('self-implement.pod', 'container-terminated', { job: name, container: 'child', reason: containerReason, exitCode: containerExitCode, jobReason: failedReason || null, memoryLimit });
1136
1167
  break;
1137
1168
  }
1169
+ if (releaseAdmission?.observed) {
1170
+ const podPhase = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${name}`, '-o', 'jsonpath={.items[*].status.phase}']);
1171
+ if (podPhase.status === 0 && /\b(?:Running|Succeeded|Failed)\b/.test(podPhase.stdout)) {
1172
+ releaseAdmission.observed();
1173
+ delete releaseAdmission.observed;
1174
+ }
1175
+ }
1138
1176
  if (ghTokenExpiresAt !== null && ghTokenExpiresAt - now() <= 10 * 60_000) {
1139
1177
  try {
1140
1178
  const fresh = await issueVerifiedAppCredential();
@@ -46,6 +46,9 @@ import { classifyIntake } from './ad-pipeline/intake.js';
46
46
  import { createAdPipelineDeps, runAdPipeline } from './ad-pipeline/run.js';
47
47
  import { projectCommand } from './telegram-project-command.js';
48
48
  import { telegramNowSlash } from './context-bus/context-now-surfaces.js';
49
+ import { createWishCard } from './intake-plane/wish-card.js';
50
+ import { telegramDecisionOwner } from './decisions/telegram-decision-cards.js';
51
+ import { CardStore } from './task-cards/card-store.js';
49
52
  import type { ContextNowDeps } from './context-bus/context-now.js';
50
53
 
51
54
  /** How many prior user/assistant turns to pass into `executeSkill` as
@@ -155,6 +158,7 @@ export interface TgCommandContext {
155
158
  userConfig: UserConfig;
156
159
  /** All registered commands — used by `/help` to enumerate. */
157
160
  allCommands: TgSlashCommand[];
161
+ sendButtons?: (text: string, buttons: Array<Array<{ text: string; data: string }>>) => Promise<void>;
158
162
  /** When the caller posted a placeholder for a streaming command,
159
163
  * the streamer edits it in-place as the handler accumulates
160
164
  * output. Undefined for instant commands. Handlers must tolerate
@@ -590,6 +594,23 @@ export function defaultTelegramCommands(nowDeps?: ContextNowDeps): TgSlashComman
590
594
  },
591
595
  },
592
596
  projectCommand(),
597
+ {
598
+ name: 'wish',
599
+ description: '소원을 카드로 남기기',
600
+ handler: async (args, ctx, { userConfig }) => {
601
+ if (String(ctx.userId) !== telegramDecisionOwner(userConfig) || ctx.chatId !== ctx.userId || !ctx.isDm) {
602
+ return '대표만 쓸 수 있습니다';
603
+ }
604
+ if (args.length === 0) return '/wish <소원 한 줄>';
605
+ const store = new CardStore();
606
+ try {
607
+ const card = createWishCard({ text: args.join(' '), source: 'telegram', ref: `${ctx.chatId}:${ctx.messageId}` }, store);
608
+ return `소원 카드로 남겼습니다 — ${card.title} (카드 ${card.cardId.slice(0, 8)})`;
609
+ } finally {
610
+ store.close();
611
+ }
612
+ },
613
+ },
593
614
  {
594
615
  name: 'fork',
595
616
  description: 'Fork the current chat session (or /fork <prefix>) and continue HERE on the fork · time-travel: /fork before:N',
@@ -42,7 +42,7 @@ export function defaultBaseUrl(deps: { runtime?: () => ReturnType<typeof readNex
42
42
  return `http://${host}:${rt.httpPort}`;
43
43
  }
44
44
 
45
- function defaultToken(): string | null {
45
+ export function defaultToken(): string | null {
46
46
  const p = join(getElanousConfigDir(), 'acp-token');
47
47
  try { return existsSync(p) ? readFileSync(p, 'utf-8').trim() || null : null; } catch { return null; }
48
48
  }
@@ -0,0 +1,90 @@
1
+ import { defaultBaseUrl, defaultToken } from './telegram-dispatch-forward.js';
2
+ import type { FabricPlan } from './self-dev/fabric-plan-core.js';
3
+ import type { TelegramBot, TgIncoming } from './telegram.js';
4
+
5
+ export interface TelegramFabricPlanDeps {
6
+ fetchImpl?: typeof fetch;
7
+ baseUrl?: () => string | null;
8
+ token?: () => string | null;
9
+ }
10
+
11
+ /** Only a successful daemon draft is offered for approval; no execution endpoint is called here. */
12
+ export function attachTelegramFabricPlan(
13
+ bot: Pick<TelegramBot, 'isOwnerAllowed' | 'sendMessage' | 'sendInlineKeyboard' | 'onCallbackQuery' | 'answerCallbackQuery'>,
14
+ deps: TelegramFabricPlanDeps = {},
15
+ ): (ctx: TgIncoming) => Promise<boolean> {
16
+ const fetchImpl = deps.fetchImpl ?? fetch;
17
+ const pending = new Map<string, { chatId: number; userId: number; threadId?: number; messageId: number }>();
18
+ const endpoint = (path: string): { url: string; token: string | null } => {
19
+ const base = (deps.baseUrl ?? defaultBaseUrl)();
20
+ if (!base) throw new Error('데몬에 연결할 수 없습니다');
21
+ return { url: `${base}${path}`, token: (deps.token ?? defaultToken)() };
22
+ };
23
+ const post = async (path: string, body?: unknown): Promise<unknown> => {
24
+ const { url, token } = endpoint(path);
25
+ const response = await fetchImpl(url, {
26
+ method: 'POST',
27
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
28
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
29
+ });
30
+ if (!response.ok) throw new Error(`데몬 응답 ${response.status}`);
31
+ return response.json();
32
+ };
33
+
34
+ bot.onCallbackQuery(async (q) => {
35
+ const match = /^fabric:(approve|edit):([a-f0-9-]{36})$/i.exec(q.data);
36
+ if (!match) return;
37
+ const planId = match[2]!;
38
+ const original = pending.get(planId);
39
+ if (!original || !bot.isOwnerAllowed(q.userId) || q.userId !== original.userId
40
+ || q.chatId !== original.chatId || q.threadId !== original.threadId || q.messageId !== original.messageId) {
41
+ await bot.answerCallbackQuery(q.id, { text: '이 초안의 요청자만 선택할 수 있습니다' });
42
+ return;
43
+ }
44
+ if (match[1] === 'edit') {
45
+ pending.delete(planId);
46
+ await bot.answerCallbackQuery(q.id);
47
+ await bot.sendMessage(original.chatId, '고칠 내용을 반영해 «아크: <복합 문장>»으로 다시 보내세요. 이 초안은 승인되지 않았습니다.', { threadId: original.threadId });
48
+ return;
49
+ }
50
+ try {
51
+ await post(`/v1/fabric/plans/${planId}/approve`);
52
+ pending.delete(planId);
53
+ await bot.answerCallbackQuery(q.id, { text: '승인되었습니다' });
54
+ await bot.sendMessage(original.chatId, `아크 초안 ${planId} 승인됨 — 실행은 별도 단계입니다.`, { threadId: original.threadId });
55
+ } catch (error) {
56
+ await bot.answerCallbackQuery(q.id, { text: `승인 실패: ${error instanceof Error ? error.message : String(error)}` });
57
+ }
58
+ });
59
+
60
+ return async (ctx) => {
61
+ if (ctx.attachments.length || ctx.video || !ctx.text.startsWith('아크:')) return false;
62
+ const request = ctx.text.slice('아크:'.length).trim();
63
+ if (!request || request.includes('\n')) {
64
+ await bot.sendMessage(ctx.chatId, '«아크: <복합 문장>» 한 줄로 보내세요.', { replyTo: ctx.messageId, threadId: ctx.threadId });
65
+ return true;
66
+ }
67
+ try {
68
+ const body = await post('/v1/fabric/decompose', { request }) as { plan?: FabricPlan };
69
+ const plan = body.plan;
70
+ if (!plan || plan.status !== 'draft' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(plan.id)
71
+ || !Array.isArray(plan.nodes) || plan.nodes.length === 0) throw new Error('초안 형식 오류');
72
+ const steps = plan.nodes.map((node, i) => `${i + 1}. ${node.title}`).join('\n');
73
+ const preview = `아크 초안 · 실행 전 검토\n${steps}`;
74
+ const reply = { replyTo: ctx.messageId, threadId: ctx.threadId };
75
+ for (let offset = 0; offset + 4000 < preview.length; offset += 4000) {
76
+ const sent = await bot.sendMessage(ctx.chatId, preview.slice(offset, offset + 4000), reply);
77
+ if (sent?.messageId === undefined) throw new Error('단계 미리보기를 전부 전달하지 못했습니다 — 승인 불가');
78
+ }
79
+ const finalOffset = Math.floor((preview.length - 1) / 4000) * 4000;
80
+ const sent = await bot.sendInlineKeyboard(ctx.chatId, preview.slice(finalOffset), [[
81
+ { text: '승인', data: `fabric:approve:${plan.id}` },
82
+ { text: '고치기', data: `fabric:edit:${plan.id}` },
83
+ ]], reply);
84
+ if (sent?.messageId !== undefined) pending.set(plan.id, { chatId: ctx.chatId, userId: ctx.userId, threadId: ctx.threadId, messageId: sent.messageId });
85
+ } catch (error) {
86
+ await bot.sendMessage(ctx.chatId, `아크 초안 생성 실패: ${error instanceof Error ? error.message : String(error)}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
87
+ }
88
+ return true;
89
+ };
90
+ }
@@ -1,7 +1,10 @@
1
1
  import { debug } from './debug/log.js';
2
2
  import { getProject, listProjects, type Project } from './project/project-store.js';
3
3
  import { findSessionByTelegramChat, updateSessionMeta } from './session/index.js';
4
+ import { telegramDecisionOwner } from './decisions/telegram-decision-cards.js';
5
+ import type { TelegramBot } from './telegram.js';
4
6
  import type { TgSlashCommand } from './telegram-commands.js';
7
+ import type { UserConfig } from './user-config.js';
5
8
 
6
9
  export interface TelegramProjectCommandDeps {
7
10
  list: () => Project[];
@@ -19,7 +22,7 @@ export function projectCommand(deps: TelegramProjectCommandDeps = {
19
22
  return {
20
23
  name: 'project',
21
24
  description: '이 대화의 프로젝트 보기·옮기기',
22
- handler: async (args, ctx) => {
25
+ handler: async (args, ctx, ctxOptions) => {
23
26
  const session = deps.findSession(ctx.chatId, ctx.threadId, ctx.botId);
24
27
  if (!session) {
25
28
  debug.log('telegram.project', 'rejected', { projects: [], reason: 'no-session' });
@@ -28,13 +31,38 @@ export function projectCommand(deps: TelegramProjectCommandDeps = {
28
31
 
29
32
  if (args.length === 0) {
30
33
  const current = session.projectId ? deps.find(session.projectId) : null;
31
- const projects = deps.list().sort((a, b) => a.name.localeCompare(b.name)).slice(0, 20);
32
- debug.log('telegram.project', 'listed', { projects: projects.map(p => ({ id: p.id, name: p.name })) });
34
+ const projects = deps.list().sort((a, b) => a.name.localeCompare(b.name));
35
+ debug.log('telegram.project', 'listed', { projects: projects.slice(0, 20).map(p => ({ id: p.id, name: p.name })) });
36
+ if (ctx.isDm && ctx.chatId === ctx.userId
37
+ && String(ctx.userId) === telegramDecisionOwner(ctxOptions.userConfig) && ctxOptions.sendButtons) {
38
+ const eligible = projects.filter(p => {
39
+ const bytes = Buffer.byteLength(`prj:${p.id}`, 'utf8');
40
+ if (bytes <= 64) return true;
41
+ debug.log('telegram.project', 'button-rejected', { reason: 'callback-data-too-long', projectId: p.id, bytes });
42
+ return false;
43
+ });
44
+ const visible = eligible.slice(0, 7);
45
+ if (current && eligible.some(p => p.id === current.id) && !visible.some(p => p.id === current.id)) {
46
+ visible[visible.length - 1] = current;
47
+ }
48
+ const buttons = visible.map(p => ({ text: `${p.id === current?.id ? '✓ ' : ''}${p.name}`, data: `prj:${p.id}` }));
49
+ buttons.push({ text: `${session.projectId ? '' : '✓ '}받은 대화로`, data: 'prj:-' });
50
+ const rows: Array<Array<{ text: string; data: string }>> = [];
51
+ for (let i = 0; i < buttons.length; i += 2) rows.push(buttons.slice(i, i + 2));
52
+ try {
53
+ await ctxOptions.sendButtons('프로젝트를 선택하세요', rows);
54
+ return;
55
+ } catch (err) {
56
+ debug.log('telegram.project', 'button-rejected', {
57
+ reason: 'send-failed', error: err instanceof Error ? err.message : String(err),
58
+ });
59
+ }
60
+ }
33
61
  return [
34
62
  `지금 대화의 프로젝트: ${current?.name ?? '받은 대화(프로젝트 없음)'}`,
35
63
  '',
36
64
  '프로젝트 목록:',
37
- ...projects.map(p => `• ${p.name}`),
65
+ ...projects.slice(0, 20).map(p => `• ${p.name}`),
38
66
  '',
39
67
  '옮기려면 /project <이름>',
40
68
  ].join('\n');
@@ -69,3 +97,55 @@ export function projectCommand(deps: TelegramProjectCommandDeps = {
69
97
  },
70
98
  };
71
99
  }
100
+
101
+ export function attachTelegramProjectButtons(
102
+ bot: Pick<TelegramBot, 'botId' | 'onCallbackQuery' | 'sendMessage' | 'answerCallbackQuery'>,
103
+ cfg: UserConfig,
104
+ deps: TelegramProjectCommandDeps = {
105
+ list: listProjects,
106
+ find: getProject,
107
+ update: updateSessionMeta,
108
+ findSession: findSessionByTelegramChat,
109
+ },
110
+ ): () => void {
111
+ const owner = telegramDecisionOwner(cfg);
112
+ return bot.onCallbackQuery(async q => {
113
+ if (!q.data.startsWith('prj:')) return;
114
+ if (!owner || String(q.userId) !== owner) {
115
+ debug.log('telegram.project', 'button-rejected', { reason: 'not-owner', userId: q.userId, chatId: q.chatId });
116
+ await bot.answerCallbackQuery(q.id, { text: '권한이 없습니다' });
117
+ return;
118
+ }
119
+ if (q.chatId === undefined || q.chatId !== q.userId || q.threadId !== undefined) {
120
+ debug.log('telegram.project', 'button-rejected', { reason: 'not-private-chat', userId: q.userId, chatId: q.chatId });
121
+ await bot.answerCallbackQuery(q.id, { text: '개인 대화에서만 고를 수 있습니다' });
122
+ return;
123
+ }
124
+ const chatId = q.chatId;
125
+ const session = deps.findSession(chatId, q.threadId, bot.botId);
126
+ if (!session) {
127
+ debug.log('telegram.project', 'button-rejected', { reason: 'no-session', chatId });
128
+ await bot.answerCallbackQuery(q.id);
129
+ await bot.sendMessage(chatId, '아직 이 채팅에 대화가 없습니다 — 한 마디 보낸 뒤 다시');
130
+ return;
131
+ }
132
+ if (q.data === 'prj:-') {
133
+ deps.update(session.id, m => { delete m.projectId; });
134
+ debug.log('telegram.project', 'button-cleared', { chatId, sessionId: session.id });
135
+ await bot.answerCallbackQuery(q.id);
136
+ await bot.sendMessage(chatId, '받은 대화로 옮겼습니다');
137
+ return;
138
+ }
139
+ const project = deps.find(q.data.slice(4));
140
+ if (!project) {
141
+ debug.log('telegram.project', 'button-rejected', { reason: 'not-found', chatId, projectId: q.data.slice(4) });
142
+ await bot.answerCallbackQuery(q.id);
143
+ await bot.sendMessage(chatId, '그 이름의 프로젝트가 없습니다 — /project 로 목록 보기');
144
+ return;
145
+ }
146
+ deps.update(session.id, m => { m.projectId = project.id; });
147
+ debug.log('telegram.project', 'button-moved', { chatId, sessionId: session.id, projectId: project.id });
148
+ await bot.answerCallbackQuery(q.id);
149
+ await bot.sendMessage(chatId, `지금 프로젝트: ${project.name}`);
150
+ });
151
+ }