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
@@ -8,11 +8,11 @@
8
8
  * X 는 media.variants 로 MP4 직링크를 주므로 yt-dlp 없이 curl 로 바로 받는다.
9
9
  */
10
10
 
11
- import { execFile as execFileCb, execFileSync } from 'node:child_process';
11
+ import { execFile as execFileCb } from 'node:child_process';
12
12
  import { mkdir, readdir, readFile, rm } from 'node:fs/promises';
13
- import { existsSync } from 'node:fs';
13
+ import { accessSync, constants, existsSync } from 'node:fs';
14
14
  import { tmpdir } from 'node:os';
15
- import { join } from 'node:path';
15
+ import { join, delimiter } from 'node:path';
16
16
  import { promisify } from 'node:util';
17
17
  import { env } from './env.js';
18
18
  import type { TweetMedia } from './types.js';
@@ -25,10 +25,22 @@ const MAX_DURATION_MS = Number(env('OMNI_DIGEST_MEDIA_MAX_MS', String(20 * 60 *
25
25
  const CHUNK_SECONDS = 600;
26
26
 
27
27
  function has(bin: string): boolean {
28
- try {
29
- execFileSync('which', [bin], { stdio: 'ignore' });
30
- return true;
31
- } catch { return false; }
28
+ for (const dir of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
29
+ try { accessSync(join(dir, bin), constants.X_OK); return true; } catch { /* try next directory */ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ const missingEngines = new Set<string>();
35
+
36
+ export function mediaEngineAvailable(engine: 'ffmpeg' | 'whisper'): boolean {
37
+ if (has(engine)) return true;
38
+ if (!missingEngines.has(engine)) {
39
+ missingEngines.add(engine);
40
+ if (engine === 'ffmpeg') console.error('ffmpeg 없음 — 영상 오디오 추출·전사 빠짐 · 설치하면 영상 전사 켜짐');
41
+ else console.error('whisper 없음 — 로컬 음성 전사 빠짐 · 설치하면 오프라인 전사 켜짐 (youtube-master 폴백은 계속)');
42
+ }
43
+ return false;
32
44
  }
33
45
 
34
46
  async function run(bin: string, args: string[]): Promise<string> {
@@ -47,7 +59,7 @@ export async function downloadVideo(url: string, outdir: string): Promise<string
47
59
 
48
60
  /** 영상에서 16kHz 모노 mp3 를 추출한다 (STT 엔진 공통 입력 규격). */
49
61
  export async function extractAudio(videoPath: string, outdir: string): Promise<string> {
50
- if (!has('ffmpeg')) throw new Error('ffmpeg 가 필요합니다 (brew install ffmpeg)');
62
+ if (!mediaEngineAvailable('ffmpeg')) throw new Error('ffmpeg 가 필요합니다 (brew install ffmpeg)');
51
63
  const out = join(outdir, 'audio.mp3');
52
64
  await run('ffmpeg', [
53
65
  '-hide_banner', '-loglevel', 'error', '-y',
@@ -61,7 +73,7 @@ export async function extractAudio(videoPath: string, outdir: string): Promise<s
61
73
 
62
74
  /** 로컬 whisper CLI 전사. 미설치면 null 을 반환해 폴백을 유도한다. */
63
75
  async function transcribeLocalWhisper(audioPath: string, outdir: string, lang: string): Promise<string | null> {
64
- if (!has('whisper')) return null;
76
+ if (!mediaEngineAvailable('whisper')) return null;
65
77
  const model = env('OMNI_DIGEST_WHISPER_MODEL', 'small');
66
78
  try {
67
79
  await run('whisper', [
@@ -81,11 +93,12 @@ async function transcribeLocalWhisper(audioPath: string, outdir: string, lang: s
81
93
  }
82
94
 
83
95
  /** youtube-master 의 STT 체인(ElevenLabs → OpenAI → Gemini)으로 폴백. */
84
- async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Promise<string | null> {
96
+ export async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Promise<string | null> {
85
97
  const skillRoot = env('YOUTUBE_MASTER_ROOT', join(process.env.HOME || '', '.claude/skills/youtube-master'));
86
98
  const script = join(skillRoot, 'scripts/transcribe-local.ts');
87
99
  if (!existsSync(script)) return null;
88
100
 
101
+ if (!mediaEngineAvailable('ffmpeg')) return null;
89
102
  // transcribe-local.ts 는 chunk_%03d.mp3 가 든 디렉터리를 입력으로 받는다
90
103
  const chunksDir = join(outdir, 'chunks');
91
104
  await mkdir(chunksDir, { recursive: true });
@@ -98,7 +111,7 @@ async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Pr
98
111
 
99
112
  const outFile = join(outdir, 'transcript.txt');
100
113
  try {
101
- await run('npx', ['tsx', script, chunksDir, outFile]);
114
+ await run(process.execPath, [script, chunksDir, outFile]);
102
115
  const text = (await readFile(outFile, 'utf-8')).trim();
103
116
  return text || null;
104
117
  } catch {
@@ -137,6 +150,7 @@ export async function transcribeTweetMedia(
137
150
  console.log(` 영상 ${Math.round(video.durationMs / 1000)}s — 상한(${Math.round(MAX_DURATION_MS / 1000)}s) 초과로 전사 생략`);
138
151
  return null;
139
152
  }
153
+ if (!mediaEngineAvailable('ffmpeg')) return null;
140
154
 
141
155
  const workdir = join(tmpdir(), `omni-digest-media-${video.mediaKey}`);
142
156
  try {
@@ -13,7 +13,7 @@ import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'n
13
13
  import { tmpdir } from 'node:os';
14
14
  import { join } from 'node:path';
15
15
  import { env, requireEnv } from './env.js';
16
- import { downloadVideo } from './media.js';
16
+ import { downloadVideo, mediaEngineAvailable } from './media.js';
17
17
  import type { TweetMedia } from './types.js';
18
18
 
19
19
  const MAX_FRAMES = 8;
@@ -101,7 +101,8 @@ export async function absorbTweetVisuals(media: TweetMedia[], opts: { tweetId: s
101
101
  } catch (e) { log(` [시각] 이미지 ${i + 1} 실패: ${String(e).slice(0, 120)}`); }
102
102
  }
103
103
  const videos = media.filter((m) => (m.type === 'video' || m.type === 'animated_gif') && (m.bestMp4Url || m.sttMp4Url));
104
- for (const [i, v] of videos.entries()) {
104
+ const canExtractFrames = !videos.length || mediaEngineAvailable('ffmpeg');
105
+ for (const [i, v] of (canExtractFrames ? videos : []).entries()) {
105
106
  if (v.durationMs && v.durationMs > MAX_VIDEO_MS_FOR_FRAMES) { log(` [시각] 영상 ${i + 1} 길어서 프레임 생략`); continue; }
106
107
  try {
107
108
  const dir = join(work, `v${i + 1}`);
@@ -5,7 +5,7 @@ import { resolveDateRange } from '../src/date.js';
5
5
  import { route, listProviders } from '../src/router.js';
6
6
  import { render } from '../src/render.js';
7
7
  import type { Command } from '../src/types.js';
8
- import { writeStdoutJson } from '../../../src/cli/stdout-json.ts';
8
+ import { writeStdoutJson } from './stdout-json.js';
9
9
 
10
10
  initEnv();
11
11
 
@@ -0,0 +1,4 @@
1
+ /** Write JSON output completely before the process can exit. (Kept inside the skill so the skill runs outside this repository — public pack and lite Pod.) */
2
+ export async function writeStdoutJson(text: string): Promise<void> {
3
+ await new Promise<void>((resolve, reject) => process.stdout.write(text, error => error ? reject(error) : resolve()));
4
+ }
@@ -21,6 +21,17 @@
21
21
  import { debug } from '../debug/log.js';
22
22
  import { getCapturedEnv, capturedEnvAvailable } from '../shell-env-bootstrap.js';
23
23
  import { resolveCurrentInstance } from '../instance/current.js';
24
+ import { treeDerivedRootFor } from '../instance/resolve.js';
25
+
26
+ /** Pin a child state root together with its origin; only reuse a parent's tag for the same root. */
27
+ export function stateDirSourceForChild(root: string, env: NodeJS.ProcessEnv = process.env): string {
28
+ if (env.ELANOUS_STATE_DIR === root && env.ELANOUS_STATE_DIR_SOURCE?.trim()) {
29
+ return env.ELANOUS_STATE_DIR_SOURCE;
30
+ }
31
+ const current = resolveCurrentInstance();
32
+ return root === treeDerivedRootFor(process.cwd()) || (current.root === root && current.layer === 'default')
33
+ ? 'derived' : 'explicit';
34
+ }
24
35
 
25
36
  // ── 전파 allowlist ─────────────────────────────────────────────────────────
26
37
  //
@@ -22,8 +22,10 @@ import { streamLLM, type LLMMessage } from '../llm.js';
22
22
  import { debug } from '../debug/log.js';
23
23
  import { emitPtyDecision, type DecisionInput } from './pty-decision.js';
24
24
  import { observeExternalEvent } from '../context-bus/external-events.js';
25
+ import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
25
26
  import { emitDecision } from '../live/detail-switch.js';
26
27
  import { resolvePtyWebAddress } from '../cli/pty-web-address.js';
28
+ import { verdictForFinalPtyScreen } from '../cli/pty-drive-verdict.js';
27
29
  import { reemitPtyUsage } from '../budget/pty-usage-reemit.js';
28
30
  import { classifyAuthError } from '../oauth/codex.js';
29
31
  import type { FallbackStep } from '../oauth/fallback-chain.js';
@@ -451,6 +453,8 @@ export interface AgentMissionSpec {
451
453
  evidence: EvidenceMode;
452
454
  /** RFC 최대 라운드(기본 16). */
453
455
  maxRounds?: number;
456
+ /** 화면 무변화 회복 문턱(ms, 기본 45초). */
457
+ recoverAfterMs?: number;
454
458
  /** 완료 시 자동 commit(기본 true). */
455
459
  commit?: boolean;
456
460
  /** 스크린 캡처 저장 디렉토리. */
@@ -487,6 +491,8 @@ export interface AgentMissionSpec {
487
491
  memory?: boolean;
488
492
  /** PTY 닉네임(휴먼 리더블·goto 로 나중 접근). 기본 branch. */
489
493
  nickname?: string;
494
+ /** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
495
+ decisionMissionId?: string;
490
496
  /** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
491
497
  * (`createWorktree` 의 같은 이름 인자로 그대로 내려간다 · 판정은 거기 `gateWorktreeReuse` 가 한다).
492
498
  *
@@ -509,11 +515,14 @@ export interface AgentMissionResult {
509
515
  paths?: string[];
510
516
  rounds: number;
511
517
  evidencePath: string | null;
518
+ /** The evidence check outcome, independent of the final mission and DRIVE-OK verdicts. */
519
+ evidenceSatisfied?: boolean;
512
520
  committed: boolean;
513
521
  usedOmniCrawl: boolean;
514
522
  detail: string;
515
523
  ptyId?: string;
516
524
  webUrl?: string | null;
525
+ driveVerdict?: ReturnType<typeof verdictForFinalPtyScreen>['kind'];
517
526
  }
518
527
  /** @deprecated codex 특정 이름 — AgentMissionResult 을 쓰라. */
519
528
  export type CodexMissionResult = AgentMissionResult;
@@ -538,6 +547,12 @@ export interface AgentMissionDeps {
538
547
  checkEvidence?: typeof checkEvidence;
539
548
  commitWorktree?: typeof commitWorktree;
540
549
  dispatchOpenPullRequest?: typeof dispatchOpenPullRequest;
550
+ /** Optional context-journal sink for isolated missions. */
551
+ emitContextEvent?: (input: SessionEventInput) => void;
552
+ /** Internal continuation retains the original mission's context-event owner. */
553
+ contextEventRun?: boolean;
554
+ /** Carries a PR number back to the outer mission's context event. */
555
+ onMissionPrNumber?: (number: number) => void;
541
556
  terminateProcessTree?: typeof terminateMissionProcessTree;
542
557
  repoStatus?: (repo: string) => string | Promise<string>;
543
558
  emitDecision?: typeof emitDecision;
@@ -835,18 +850,36 @@ export interface MissionControlBrainOpts {
835
850
  /** Screen-grounded brain summary for the next backend, never the raw PTY screen. */
836
851
  readonly onSummary?: (summary: string) => void;
837
852
  readonly onDecision?: (text: string, reply?: { question: string; answer: string }) => void;
853
+ readonly recoverAfterMs?: number;
854
+ readonly initialCommand?: string;
855
+ readonly onRecover?: (blocked: string, action: string) => void;
838
856
  }
839
857
 
840
858
  /** 기존 5-action mission 판단을 canonical PTY control-loop 결정으로 축소한다. search는 이 경계에서만
841
859
  * 부작용(context 기록)을 끝내고, child에는 context를 읽고 계속하라는 단일 input만 전달한다. */
842
- export function createMissionControlBrain(opts: MissionControlBrainOpts): RunSupervisor {
860
+ export type MissionControlBrain = RunSupervisor & {
861
+ /** Report whether the last returned input was accepted by the PTY; recovery stages and events commit only on acceptance. */
862
+ readonly onInputResult: (text: string, accepted: boolean) => void;
863
+ };
864
+
865
+ export function createMissionControlBrain(opts: MissionControlBrainOpts): MissionControlBrain {
843
866
  const history: string[] = [];
844
867
  const backendName = (opts.backend ?? codexBackend).name;
845
868
  // ★ P4 dedup(제한적 재시도·리뷰) — (layer:spec)별 시도횟수. 성공 시 영구 차단, 실패는 최대 2회까지
846
869
  // 허용(일시 네트워크/프로세스 오류는 재시도, 지속 실패는 루프 차단).
847
870
  const provisionAttempts = new Map<string, number>();
848
871
  const PROVISION_MAX_ATTEMPTS = 2;
849
- return createLlmControlBrain({
872
+ const recoverAfterMs = opts.recoverAfterMs ?? 45_000;
873
+ if (!Number.isFinite(recoverAfterMs) || recoverAfterMs <= 0) throw new Error('recoverAfterMs must be positive');
874
+ let lastScreen: string | undefined;
875
+ let repeatedQuestion = 0;
876
+ let lastQuestion: string | undefined;
877
+ let recoveryStage = 0;
878
+ let pendingReplayBlocked: string | undefined;
879
+ // The recovery input returned by the last decide(); committed (stage advance + event) only when the PTY accepts it.
880
+ let pendingRecovery: { blocked: string; action: string; text: string; replay?: boolean } | undefined;
881
+ let lastCommand = opts.initialCommand ?? opts.mission;
882
+ const llmBrain = createLlmControlBrain({
850
883
  goal: opts.mission,
851
884
  stream: opts.stream,
852
885
  maxTokens: 500,
@@ -887,7 +920,10 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
887
920
  if (decision.reason && decision.action !== 'ask-human') opts.onSummary?.(decision.reason.slice(0, 2000));
888
921
  if (decision.action === 'handoff' && decision.to && decision.mission) return { action: 'handoff', to: decision.to, mission: decision.mission, ...(decision.carry ? { carry: decision.carry } : {}) };
889
922
  if (decision.action === 'ask-human') return { action: 'ask-human', reason: decision.reason, ...(decision.url ? { url: decision.url } : {}), ...(decision.code ? { code: decision.code } : {}) };
890
- if (decision.action === 'send') return { action: 'input', text: `${decision.text || 'continue'}\r` };
923
+ if (decision.action === 'send') {
924
+ lastCommand = decision.text || 'continue';
925
+ return { action: 'input', text: `${lastCommand}\r` };
926
+ }
891
927
  if (decision.action === 'verify' || decision.action === 'done') return { action: 'done', reason: decision.reason || 'agent completed' };
892
928
  if (decision.action === 'search') {
893
929
  await opts.search(decision.query || opts.mission.slice(0, 120), obs.step);
@@ -922,6 +958,57 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
922
958
  return { action: 'wait' };
923
959
  },
924
960
  });
961
+ return {
962
+ onInputResult: (text, accepted) => {
963
+ const recovery = pendingRecovery;
964
+ // Only the input this brain returned settles its recovery step (a trust answer in between does not).
965
+ if (!recovery || recovery.text !== text) return;
966
+ pendingRecovery = undefined;
967
+ if (!accepted) return;
968
+ if (recovery.replay) { pendingReplayBlocked = undefined; opts.onRecover?.(recovery.blocked, recovery.action); return; }
969
+ recoveryStage += 1;
970
+ if (recovery.action === 'Ctrl-C and replay previous command') pendingReplayBlocked = recovery.blocked;
971
+ else opts.onRecover?.(recovery.blocked, recovery.action);
972
+ },
973
+ decide: async (obs, signal) => {
974
+ pendingRecovery = undefined;
975
+ if (signal?.aborted) return { action: 'wait' };
976
+ const lines = classifierFrameLines(obs.screen);
977
+ // Only the active bottom line can be a repeated prompt; a question left in
978
+ // scrollback while new work appears is not a request for input.
979
+ const tail = lines.at(-1);
980
+ const question = tail && /\?(?:\s*\[[^\]\r\n]+\])?\s*$/.test(tail) ? tail : undefined;
981
+ const screenChanged = lastScreen !== undefined && obs.screen !== lastScreen;
982
+ if (screenChanged) recoveryStage = 0;
983
+ repeatedQuestion = question ? (!screenChanged && question === lastQuestion ? repeatedQuestion + 1 : 1) : 0;
984
+ lastQuestion = question;
985
+ lastScreen = obs.screen;
986
+ if (pendingReplayBlocked !== undefined) {
987
+ // Ctrl-C consumes its own step; replay on the next observation even when the
988
+ // interrupted child keeps redrawing a non-prompt screen. A rejected replay is retried.
989
+ const text = `${lastCommand}\r`;
990
+ pendingRecovery = { blocked: pendingReplayBlocked, action: 'Ctrl-C and replay previous command', text, replay: true };
991
+ return { action: 'input', text };
992
+ }
993
+ const blocked = repeatedQuestion >= 3 ? `same question repeated three times: ${question}`
994
+ : (obs.sameScreenMs ?? 0) >= recoverAfterMs ? `screen unchanged for ${obs.sameScreenMs}ms` : undefined;
995
+ if (blocked && opts.evidenceReady()) return { action: 'done', reason: 'Evidence ready on stalled screen' };
996
+ if (blocked && !signal?.aborted) {
997
+ const action = ['Enter', 'Esc', 'Ctrl-C and replay previous command', 'Human needed'] as const;
998
+ const next = action[Math.min(recoveryStage, 3)]!;
999
+ if (next !== 'Human needed') {
1000
+ const text = next === 'Enter' ? '\r' : next === 'Esc' ? '\x1b' : '\x03';
1001
+ pendingRecovery = { blocked, action: next, text };
1002
+ return { action: 'input', text };
1003
+ }
1004
+ recoveryStage += 1;
1005
+ opts.onRecover?.(blocked, next);
1006
+ const summary = lines.slice(-3).join(' | ').slice(0, 240) || '(empty screen)';
1007
+ return { action: 'no-progress', terminal: true, reason: `사람 필요 · 마지막 화면 요약: ${summary}` };
1008
+ }
1009
+ return llmBrain.decide(obs, signal);
1010
+ },
1011
+ };
925
1012
  }
926
1013
 
927
1014
  /**
@@ -1273,6 +1360,21 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1273
1360
  } finally { signal?.removeEventListener('abort', stopWaiting); }
1274
1361
  }
1275
1362
  const startedAt = Date.now();
1363
+ const contextOwner = !deps.contextEventRun;
1364
+ const contextRunId = contextOwner ? ensureRunId() : undefined;
1365
+ let contextPrNumber: number | undefined;
1366
+ const emitContext = (kind: 'task-claimed' | 'task-done', detail: string): void => {
1367
+ if (!contextOwner) return;
1368
+ try {
1369
+ (deps.emitContextEvent ?? emitSessionEvent)({ kind, seat: 'TC', text: `Agent mission ${detail}: ${spec.mission.split(/\r?\n/, 1)[0]}`,
1370
+ ref: `runId=${contextRunId}${contextPrNumber === undefined ? '' : ` PR=#${contextPrNumber}`}` });
1371
+ } catch (error) {
1372
+ try { debug.log('agent-mission', 'context-event-failed', { kind, runId: contextRunId, error: String(error) }, { level: 'warn' }); } catch { /* fail-soft */ }
1373
+ }
1374
+ };
1375
+ emitContext('task-claimed', 'claimed');
1376
+ let result: AgentMissionResult | undefined;
1377
+ try {
1276
1378
  const initialFiles = workdir && spec.evidence.kind === 'doc' ? initialEvidenceFiles(workdir, spec.evidence) : undefined;
1277
1379
  let handle: PtyHandle | undefined;
1278
1380
  let childPid: number | undefined;
@@ -1306,10 +1408,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1306
1408
  });
1307
1409
  signal?.addEventListener('abort', abort, { once: true });
1308
1410
  if (signal?.aborted) abort();
1309
- let result: AgentMissionResult;
1310
1411
  try {
1311
1412
  if (cancelled) await aborted;
1312
- const body = runAgentMissionBody(spec, { ...deps, onControlActive: (pending) => {
1413
+ const body = runAgentMissionBody(spec, { ...deps, contextEventRun: true, onMissionPrNumber: (number) => {
1414
+ contextPrNumber = number;
1415
+ try { deps.onMissionPrNumber?.(number); } catch { /* context propagation never changes a mission result */ }
1416
+ }, onControlActive: (pending) => {
1313
1417
  activeSettled = false;
1314
1418
  activeControl = pending.then(() => { activeSettled = true; }, () => { activeSettled = true; });
1315
1419
  }, onControlPending: (pending) => {
@@ -1377,10 +1481,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
1377
1481
  if (paths.length) {
1378
1482
  debug.log('agent-mission', 'outside-write', { workdir, pids: [], reason: paths.join(', ') });
1379
1483
  (deps.emitDecision ?? emitDecision)({ kind: 'ESCALATE', what: '작업 폴더 밖 쓰기', reason: paths.join(', '), purpose: '사람이 확인한다', target: 'human' });
1380
- return { ...result, ok: false, reason: 'outside-write', paths, committed: false };
1484
+ result = { ...result, ok: false, reason: 'outside-write', paths, committed: false };
1485
+ return result;
1381
1486
  }
1382
1487
  }
1383
1488
  return result;
1489
+ } finally { emitContext('task-done', result?.ok ? 'completed' : 'ended'); }
1384
1490
  }
1385
1491
 
1386
1492
  async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDeps, startedAt: number, initialFiles?: ReadonlyMap<string, string>): Promise<AgentMissionResult> {
@@ -1508,7 +1614,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1508
1614
  const finalEv = verifyEvidence(wt.path, spec.evidence);
1509
1615
  if (!finalEv.ok) return {
1510
1616
  ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1511
- evidencePath: null, committed: false, usedOmniCrawl: false,
1617
+ evidencePath: null, evidenceSatisfied: false, committed: false, usedOmniCrawl: false,
1512
1618
  detail: `미완(증거 부족): ${finalEv.retry ?? '증거가 아직 부족하다.'}`,
1513
1619
  };
1514
1620
  if (checklist.length) {
@@ -1526,13 +1632,13 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1526
1632
  if (!c.ok) {
1527
1633
  debug.log('agent-mission', 'result', { ok: false, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed: false, usedOmni: false });
1528
1634
  return { ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1529
- evidencePath: finalEv.path, committed: false, usedOmniCrawl: false,
1635
+ evidencePath: finalEv.path, evidenceSatisfied: true, committed: false, usedOmniCrawl: false,
1530
1636
  detail: `미완(자동 커밋 실패): ${c.out.slice(0, 120)}` };
1531
1637
  }
1532
1638
  }
1533
1639
  debug.log('agent-mission', 'result', { ok: true, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed, usedOmni: false });
1534
1640
  return { ok: true, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1535
- evidencePath: finalEv.path, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
1641
+ evidencePath: finalEv.path, evidenceSatisfied: true, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
1536
1642
  }
1537
1643
  env.TERM = 'xterm-256color';
1538
1644
  // ⭐run-identity — runId was resolved before either headless or PTY spawn.
@@ -1612,7 +1718,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1612
1718
  process.stderr.write(`[agent-mission] pty=${h.id} watch=${webAddress.webUrl ?? `(web unavailable: ${webAddress.pwaUnavailableReason ?? 'not-resolved'})`}\n`);
1613
1719
  debug.log('agent-mission', 'pty-link', { id: h.id, webUrl: webAddress.webUrl, webUrlSource: webAddress.webUrlSource });
1614
1720
 
1615
- const decisionIdentity = { missionId: runId, sessionId: runId, terminalId: h.id, agent: backend.name };
1721
+ const decisionIdentity = { missionId: spec.decisionMissionId ?? runId, sessionId: runId, terminalId: h.id, agent: backend.name };
1616
1722
  const decisionEvent = (event: Omit<DecisionInput, keyof typeof decisionIdentity>): void => {
1617
1723
  try { (deps.emitPtyDecision ?? emitPtyDecision)({ ...decisionIdentity, ...event } as DecisionInput); }
1618
1724
  catch { /* Observation must not change mission control or handoff. */ }
@@ -1779,6 +1885,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1779
1885
  if (deps.isMissionAborted?.()) throw new Error('MISSION_ABORTED');
1780
1886
  // 미션 전송 — 인핸싱/기억으로 가공됐으면(멀티라인) 파일로 떨궈 read 지시(TUI 멀티라인 위험·verbatim 보존).
1781
1887
  // 원문 그대로면(단문) 타이핑.
1888
+ let submittedCommand = spec.mission;
1889
+ const submittedInputs: string[] = [];
1782
1890
  if (missionText !== spec.mission) {
1783
1891
  const pf = join(wt.path, '.mission-prompt.md');
1784
1892
  try { writeFileSync(pf, missionText); } catch { /* noop */ }
@@ -1786,9 +1894,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1786
1894
  ? '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라. [elanous 인핸싱]의 커버리지 체크리스트 모든 항목을 산출물에 빠짐없이 반영하고(요약·일반화 금지), 완료하면 MISSION-COMPLETE 라고 답하라.'
1787
1895
  : '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라(원문 그대로·[elanous 기억]은 참조 컨텍스트). 완료하면 MISSION-COMPLETE 라고 답하라.';
1788
1896
  debug.log('agent-mission', 'mission-send', { via: 'file', file: '.mission-prompt.md', chars: missionText.length, enhanced: enhanceActive });
1897
+ submittedCommand = note;
1898
+ submittedInputs.push(note);
1789
1899
  drive(note); await sleep(800); drive('\r');
1790
1900
  } else {
1791
1901
  debug.log('agent-mission', 'mission-send', { via: 'type', chars: spec.mission.length });
1902
+ submittedInputs.push(spec.mission);
1792
1903
  drive(spec.mission); await sleep(800); drive('\r');
1793
1904
  }
1794
1905
 
@@ -1803,6 +1914,9 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1803
1914
  backend,
1804
1915
  stream: deps.controlStream,
1805
1916
  handoffEnabled: true,
1917
+ recoverAfterMs: spec.recoverAfterMs,
1918
+ initialCommand: submittedCommand,
1919
+ onRecover: (blocked, action) => { decisionEvent({ step: 'recover', text: action, detail: { blocked, action } }); },
1806
1920
  onSummary: (summary) => { controllerSummary = summary; },
1807
1921
  onDecision: (text, reply) => {
1808
1922
  pendingAnswer = reply ? { text: `${reply.answer || 'continue'}\r`, ...reply } : undefined;
@@ -1824,6 +1938,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1824
1938
  // before asking the LLM — otherwise the mission text is typed into the prompt and the run stalls.
1825
1939
  const trustAwareBrain: RunSupervisor = {
1826
1940
  decide: (obs, signal) => {
1941
+ decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` });
1827
1942
  pendingAnswer = undefined;
1828
1943
  let answer: string | undefined;
1829
1944
  if (backend.handleTrust?.(obs.screen, (bytes) => { answer = bytes; }) && answer !== undefined) {
@@ -1889,7 +2004,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1889
2004
  handoffPrSkipped = true;
1890
2005
  debug.log('agent-mission', 'handoff-pr-skipped', { reason: 'origin-not-github' });
1891
2006
  } else {
1892
- (deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
2007
+ const opened = (deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
2008
+ if (opened.number !== null) deps.onMissionPrNumber?.(opened.number);
1893
2009
  }
1894
2010
  gatedHandoff = true;
1895
2011
  },
@@ -1916,7 +2032,6 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1916
2032
  renderPng: () => h.renderScreenPng(),
1917
2033
  bus,
1918
2034
  ident: { ptyId: h.id, instance: resolveInstanceName(), runId },
1919
- onPublishedFrame: (obs) => decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` }),
1920
2035
  });
1921
2036
  debug.log('agent-mission', 'takeover-wait', { id: h.id, maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS });
1922
2037
  const controlRun = runWithControlObserve(
@@ -1932,7 +2047,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1932
2047
  pendingAnswer = undefined;
1933
2048
  const accepted = inject(text);
1934
2049
  if (accepted) {
2050
+ submittedInputs.push(text);
1935
2051
  decisionEvent({ step: 'input', text: `Sent ${text.startsWith('\x1b') ? 'key' : 'text'}: ${text.startsWith('\x1b') ? 'navigation' : text}` });
2052
+ }
2053
+ // Recovery stages and recover events are committed only after the PTY accepted the input.
2054
+ brain.onInputResult(text, accepted);
2055
+ if (accepted) {
1936
2056
  if (reply) decisionEvent({ step: 'answer', text: 'Answered agent question', detail: { question: reply.question, answer: reply.answer } });
1937
2057
  }
1938
2058
  return accepted;
@@ -1948,6 +2068,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1948
2068
  } : {}),
1949
2069
  awaitOwnership: { maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS },
1950
2070
  settle: async () => {
2071
+ // A frozen screen goes quiet after 6 s, so stall recovery is still observed; work that keeps
2072
+ // drawing output keeps the original 360 s settle and round budget.
1951
2073
  const quiet = waitForQuiet(h, 6000, 360000);
1952
2074
  await (deps.awaitMission ? deps.awaitMission(quiet) : quiet);
1953
2075
  },
@@ -1986,7 +2108,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1986
2108
  throw error;
1987
2109
  }
1988
2110
  },
1989
- }, { maxSteps: maxRounds });
2111
+ }, { maxSteps: maxRounds, stuckLimit: maxRounds + 1 });
1990
2112
  deps.onControlActive?.(pending);
1991
2113
  return pending;
1992
2114
  },
@@ -2025,7 +2147,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2025
2147
  try { h.kill(); } catch { /* noop */ }
2026
2148
  return { ok: true, worktree: wt.path, branch: wt.branch, rounds: control.steps, evidencePath: wt.path, committed: true, usedOmniCrawl: usedOmni,
2027
2149
  ...(handoffPrSkipped ? { pr: 'skipped' as const, reason: 'origin-not-github' as const } : {}),
2028
- detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
2150
+ evidenceSatisfied: true, detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
2029
2151
  }
2030
2152
  const done = control.termination.kind === 'success';
2031
2153
  const round = control.steps;
@@ -2079,24 +2201,29 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2079
2201
  if (deps.isMissionAborted?.()) return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
2080
2202
  rounds: round, evidencePath: null, committed: false, usedOmniCrawl: usedOmni, detail: 'aborted' };
2081
2203
  const finalEv = verifyEvidence(wt.path, spec.evidence);
2082
- const evidencePath = finalEv.ok ? finalEv.path : null;
2204
+ const finalScreen = await h.renderScreen();
2205
+ const driveVerdict = verdictForFinalPtyScreen({ screen: finalScreen, inputHistory: submittedInputs, exitCode: h.exitCode ?? null });
2206
+ debug.log('agent-mission', 'drive-verdict', { kind: driveVerdict.kind, reason: driveVerdict.reason.slice(0, 200) });
2207
+ const recoverExhausted = control.termination.kind === 'stuck' && control.termination.reason.startsWith('사람 필요');
2208
+ const accepted = finalEv.ok && !recoverExhausted && driveVerdict.kind !== 'done-but-failed';
2209
+ const evidencePath = accepted ? finalEv.path : null;
2083
2210
  let committed = false;
2084
- if (!deps.isMissionAborted?.() && !spec.workdir && finalEv.ok && (spec.commit ?? true)) {
2211
+ if (!deps.isMissionAborted?.() && !spec.workdir && accepted && (spec.commit ?? true)) {
2085
2212
  const c = (deps.commitWorktree ?? commitWorktree)(wt.path, `chore(agent-mission): ${spec.branch} — ${backend.name}-in-elanous PTY RFC 산출`);
2086
2213
  committed = c.ok;
2087
2214
  debug.log('agent-mission', 'commit', { ok: c.ok, out: c.out.slice(0, 120) });
2088
2215
  }
2089
- debug.log('agent-mission', 'result', { ok: finalEv.ok, rounds: round, evidencePath, committed, usedOmni });
2090
- decisionEvent({ step: 'done', text: finalEv.ok ? 'Mission evidence accepted' : 'Mission ended without sufficient evidence', detail: { result: { kind: evidencePath ? 'file' : 'text', ref: evidencePath ?? (control.termination.kind === 'error' ? control.termination.message : 'evidence unavailable') } } });
2091
- stopLive(finalEv.ok);
2216
+ debug.log('agent-mission', 'result', { ok: accepted, rounds: round, evidencePath, committed, usedOmni });
2217
+ decisionEvent({ step: 'done', text: accepted ? 'Mission evidence accepted' : 'Mission ended without sufficient evidence', detail: { result: { kind: evidencePath ? 'file' : 'text', ref: evidencePath ?? (control.termination.kind === 'error' ? control.termination.message : recoverExhausted ? control.termination.reason : 'evidence unavailable') } } });
2218
+ stopLive(accepted);
2092
2219
  if (h.pid) await terminateMissionProcessTree(h.pid);
2093
2220
  try { h.kill(); } catch { /* noop */ }
2094
2221
 
2095
2222
  return {
2096
- ok: finalEv.ok, worktree: wt.path, branch: wt.branch, rounds: round,
2097
- evidencePath, committed, usedOmniCrawl: usedOmni,
2098
- ptyId: h.id, webUrl: webAddress.webUrl,
2099
- detail: done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2223
+ ok: accepted, worktree: wt.path, branch: wt.branch, rounds: round,
2224
+ evidencePath, evidenceSatisfied: finalEv.ok, committed, usedOmniCrawl: usedOmni,
2225
+ ptyId: h.id, webUrl: webAddress.webUrl, driveVerdict: driveVerdict.kind,
2226
+ detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2100
2227
  };
2101
2228
  } finally { stopLive(); finishContext(false); }
2102
2229
  }
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { runAgentMission, resolveBackend, type AgentBackend, type AgentMissionResult, type AgentMissionSpec, type EvidenceMode } from './driver.js';
4
+
5
+ export interface DuoOptions {
6
+ left: string;
7
+ right: string;
8
+ branch: string;
9
+ base?: string;
10
+ missionFile?: string;
11
+ evidence?: string;
12
+ docDir?: string;
13
+ docGlob?: string;
14
+ testPath?: string;
15
+ file?: string;
16
+ maxRounds?: string;
17
+ commit?: boolean;
18
+ }
19
+
20
+ export interface DuoSide {
21
+ side: 'left' | 'right';
22
+ backend: string;
23
+ branch: string;
24
+ finished: number;
25
+ result?: AgentMissionResult;
26
+ error?: string;
27
+ }
28
+
29
+ export interface DuoResult {
30
+ missionId: string;
31
+ sides: [DuoSide, DuoSide];
32
+ lines: [string, string];
33
+ exitCode: number;
34
+ }
35
+
36
+ export interface DuoDeps {
37
+ resolveBackend?: typeof resolveBackend;
38
+ runMission?: typeof runAgentMission;
39
+ readFile?: (path: string) => string;
40
+ id?: () => string;
41
+ }
42
+
43
+ function evidenceFor(opts: DuoOptions): EvidenceMode {
44
+ if (opts.evidence === 'doc') return { kind: 'doc', dirRel: opts.docDir ?? 'docs/plans', glob: new RegExp(opts.docGlob ?? '^PLAN-.*[.]md$', 'i') };
45
+ if (opts.evidence === 'test') {
46
+ if (!opts.testPath) throw new Error('test 모드엔 --test-path 필요');
47
+ return { kind: 'test', testPath: opts.testPath, ...(opts.file ? { fileRel: opts.file } : {}) };
48
+ }
49
+ if (opts.evidence === undefined || opts.evidence === 'tsc') return { kind: 'tsc' };
50
+ throw new Error('--evidence 는 doc|tsc|test 여야 합니다');
51
+ }
52
+
53
+ function render(side: DuoSide, first: DuoSide | undefined): string {
54
+ const evidence = side.result?.evidenceSatisfied === true
55
+ ? side.result.evidencePath ? `충족(${side.result.evidencePath})` : '충족'
56
+ : side.result?.evidenceSatisfied === false ? '미충족' : '미측정';
57
+ const drive = side.result?.driveVerdict ?? '미측정';
58
+ const detail = side.error ?? (!side.result?.ok ? side.result?.detail : undefined);
59
+ return `${side.side} ${side.backend} · branch=${side.branch} · terminalId=${side.result?.ptyId ?? '없음'} · ${first === side ? '먼저' : '나중'} · 증거 ${evidence} · DRIVE-OK ${drive} · ${side.result?.ok ? '완료' : '실패'}${detail ? ` (${detail})` : ''}`;
60
+ }
61
+
62
+ /** Run two independent existing agent missions; neither branch waits for or cancels its peer. */
63
+ export async function runAgentMissionDuo(textParts: string[], opts: DuoOptions, deps: DuoDeps = {}): Promise<DuoResult> {
64
+ if (!opts.branch?.trim()) throw new Error('--branch 필요');
65
+ if (opts.left !== 'codex' && opts.left !== 'claude') throw new Error('--left 는 codex|claude 여야 합니다');
66
+ if (opts.right !== 'codex' && opts.right !== 'claude') throw new Error('--right 는 codex|claude 여야 합니다');
67
+ if (opts.left === opts.right) throw new Error('duo 에는 서로 다른 backend 둘이 필요합니다');
68
+ const mission = opts.missionFile ? (deps.readFile ?? ((path) => readFileSync(path, 'utf8')))(opts.missionFile) : textParts.join(' ');
69
+ if (!mission.trim()) throw new Error('미션 텍스트가 비었다 — <text...> 또는 --mission-file 필요');
70
+ const evidence = evidenceFor(opts);
71
+ const rounds = opts.maxRounds === undefined ? 16 : Number(opts.maxRounds);
72
+ if (!Number.isSafeInteger(rounds) || rounds <= 0) throw new Error('--max-rounds 는 양의 정수여야 합니다');
73
+ const resolve = deps.resolveBackend ?? resolveBackend;
74
+ const backends: [AgentBackend, AgentBackend] = [resolve(opts.left), resolve(opts.right)];
75
+ if (backends[0].name !== opts.left || backends[1].name !== opts.right) throw new Error('backend 선택이 요청과 다릅니다');
76
+ const id = `duo-${(deps.id ?? randomUUID)()}`;
77
+ const run = deps.runMission ?? runAgentMission;
78
+ let completed = 0;
79
+ const branches = [`${opts.branch}-${opts.left}`, `${opts.branch}-${opts.right}`] as const;
80
+ const tasks = backends.map(async (backend, index): Promise<DuoSide> => {
81
+ const side = index === 0 ? 'left' : 'right';
82
+ const branch = branches[index]!;
83
+ const spec: AgentMissionSpec = {
84
+ mission, branch, agent: backend, decisionMissionId: id, evidence, maxRounds: rounds,
85
+ ...(opts.base ? { base: opts.base } : {}), commit: opts.commit !== false,
86
+ enhance: false, memory: false, resources: 'off', entry: 'external-verbatim',
87
+ };
88
+ try { return { side, backend: backend.name, branch, result: await run(spec), finished: ++completed }; }
89
+ catch (error) { return { side, backend: backend.name, branch, error: error instanceof Error ? error.message : String(error), finished: ++completed }; }
90
+ });
91
+ const sides = await Promise.all(tasks) as [DuoSide, DuoSide];
92
+ const first = sides[0].finished <= sides[1].finished ? sides[0] : sides[1];
93
+ return { missionId: id, sides, lines: [render(sides[0], first), render(sides[1], first)], exitCode: sides.every((s) => s.result?.ok) ? 0 : 2 };
94
+ }
@@ -41,10 +41,10 @@ function safe(value: string): string {
41
41
  }
42
42
 
43
43
  /** The only emission point for the inside-PTY decision stream. Never log unsanitized input. */
44
- export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = debug.log): PtyDecision {
44
+ export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = (category, event, data) => debug.log(category, event, data)): PtyDecision {
45
45
  let stream = missionStreams.get(input.missionId);
46
46
  if (!stream) {
47
- stream = { alias: `mission-${missionStreams.size + 1}`, seq: -1 };
47
+ stream = { alias: `${input.missionId.startsWith('duo-') ? 'duo' : 'mission'}-${missionStreams.size + 1}`, seq: -1 };
48
48
  missionStreams.set(input.missionId, stream);
49
49
  }
50
50
  stream.seq += 1;