elanous 0.2.12 → 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 (289) 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-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → 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
  //
@@ -491,6 +491,8 @@ export interface AgentMissionSpec {
491
491
  memory?: boolean;
492
492
  /** PTY 닉네임(휴먼 리더블·goto 로 나중 접근). 기본 branch. */
493
493
  nickname?: string;
494
+ /** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
495
+ decisionMissionId?: string;
494
496
  /** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
495
497
  * (`createWorktree` 의 같은 이름 인자로 그대로 내려간다 · 판정은 거기 `gateWorktreeReuse` 가 한다).
496
498
  *
@@ -513,11 +515,14 @@ export interface AgentMissionResult {
513
515
  paths?: string[];
514
516
  rounds: number;
515
517
  evidencePath: string | null;
518
+ /** The evidence check outcome, independent of the final mission and DRIVE-OK verdicts. */
519
+ evidenceSatisfied?: boolean;
516
520
  committed: boolean;
517
521
  usedOmniCrawl: boolean;
518
522
  detail: string;
519
523
  ptyId?: string;
520
524
  webUrl?: string | null;
525
+ driveVerdict?: ReturnType<typeof verdictForFinalPtyScreen>['kind'];
521
526
  }
522
527
  /** @deprecated codex 특정 이름 — AgentMissionResult 을 쓰라. */
523
528
  export type CodexMissionResult = AgentMissionResult;
@@ -1609,7 +1614,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1609
1614
  const finalEv = verifyEvidence(wt.path, spec.evidence);
1610
1615
  if (!finalEv.ok) return {
1611
1616
  ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1612
- evidencePath: null, committed: false, usedOmniCrawl: false,
1617
+ evidencePath: null, evidenceSatisfied: false, committed: false, usedOmniCrawl: false,
1613
1618
  detail: `미완(증거 부족): ${finalEv.retry ?? '증거가 아직 부족하다.'}`,
1614
1619
  };
1615
1620
  if (checklist.length) {
@@ -1627,13 +1632,13 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1627
1632
  if (!c.ok) {
1628
1633
  debug.log('agent-mission', 'result', { ok: false, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed: false, usedOmni: false });
1629
1634
  return { ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1630
- evidencePath: finalEv.path, committed: false, usedOmniCrawl: false,
1635
+ evidencePath: finalEv.path, evidenceSatisfied: true, committed: false, usedOmniCrawl: false,
1631
1636
  detail: `미완(자동 커밋 실패): ${c.out.slice(0, 120)}` };
1632
1637
  }
1633
1638
  }
1634
1639
  debug.log('agent-mission', 'result', { ok: true, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed, usedOmni: false });
1635
1640
  return { ok: true, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
1636
- evidencePath: finalEv.path, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
1641
+ evidencePath: finalEv.path, evidenceSatisfied: true, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
1637
1642
  }
1638
1643
  env.TERM = 'xterm-256color';
1639
1644
  // ⭐run-identity — runId was resolved before either headless or PTY spawn.
@@ -1713,7 +1718,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
1713
1718
  process.stderr.write(`[agent-mission] pty=${h.id} watch=${webAddress.webUrl ?? `(web unavailable: ${webAddress.pwaUnavailableReason ?? 'not-resolved'})`}\n`);
1714
1719
  debug.log('agent-mission', 'pty-link', { id: h.id, webUrl: webAddress.webUrl, webUrlSource: webAddress.webUrlSource });
1715
1720
 
1716
- 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 };
1717
1722
  const decisionEvent = (event: Omit<DecisionInput, keyof typeof decisionIdentity>): void => {
1718
1723
  try { (deps.emitPtyDecision ?? emitPtyDecision)({ ...decisionIdentity, ...event } as DecisionInput); }
1719
1724
  catch { /* Observation must not change mission control or handoff. */ }
@@ -2142,7 +2147,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2142
2147
  try { h.kill(); } catch { /* noop */ }
2143
2148
  return { ok: true, worktree: wt.path, branch: wt.branch, rounds: control.steps, evidencePath: wt.path, committed: true, usedOmniCrawl: usedOmni,
2144
2149
  ...(handoffPrSkipped ? { pr: 'skipped' as const, reason: 'origin-not-github' as const } : {}),
2145
- detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
2150
+ evidenceSatisfied: true, detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
2146
2151
  }
2147
2152
  const done = control.termination.kind === 'success';
2148
2153
  const round = control.steps;
@@ -2216,8 +2221,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2216
2221
 
2217
2222
  return {
2218
2223
  ok: accepted, worktree: wt.path, branch: wt.branch, rounds: round,
2219
- evidencePath, committed, usedOmniCrawl: usedOmni,
2220
- ptyId: h.id, webUrl: webAddress.webUrl,
2224
+ evidencePath, evidenceSatisfied: finalEv.ok, committed, usedOmniCrawl: usedOmni,
2225
+ ptyId: h.id, webUrl: webAddress.webUrl, driveVerdict: driveVerdict.kind,
2221
2226
  detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
2222
2227
  };
2223
2228
  } finally { stopLive(); finishContext(false); }
@@ -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
+ }
@@ -44,7 +44,7 @@ function safe(value: string): string {
44
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;
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process';
2
2
  import { appendFileSync, mkdirSync, readFileSync, rmdirSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { stateDirSourceForChild } from '../agent/identity-env.js';
5
6
  import { effectiveInstanceRoot } from '../instance/resolve.js';
6
7
  import type { TrackAgentDecision, TrackAgentInput, TrackAgentResult } from './track-agent.js';
7
8
 
@@ -127,7 +128,7 @@ export async function launchTrackAgentShadow(input: TrackAgentInput, options: Sh
127
128
  const instanceRoot = effectiveInstanceRoot();
128
129
  const { stdout } = await exec('bun', ['bin/elanous.mjs', '--config-dir', instanceRoot, ...values], {
129
130
  cwd: process.env.ELANOUS_TOOL_CWD?.trim() || resolve(import.meta.dir, '../..'),
130
- env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot },
131
+ env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot, ELANOUS_STATE_DIR_SOURCE: stateDirSourceForChild(instanceRoot) },
131
132
  encoding: 'utf8', maxBuffer: 1024 * 1024,
132
133
  });
133
134
  return { stdout };
@@ -2,6 +2,7 @@ import { execFile, spawn } from 'node:child_process';
2
2
  import { readdirSync, readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { stateDirSourceForChild } from '../agent/identity-env.js';
5
6
  import { debug, redactSecretText } from '../debug/log.js';
6
7
  import { getOrCreatePersonaSession } from '../session/index.js';
7
8
  import { getUserConfig } from '../user-config.js';
@@ -38,7 +39,9 @@ const log = (event: string, data: Record<string, unknown>) => {
38
39
  };
39
40
  /** The daemon's own universe, resolved once and pinned on the child (config ⊕ state) — the harness isolates its own workers. */
40
41
  export function universeLaunch(args: readonly string[], root: string = effectiveInstanceRoot()): { argv: string[]; env: NodeJS.ProcessEnv } {
41
- return { argv: ['bin/elanous.mjs', '--config-dir', root, ...args], env: { ...process.env, ELANOUS_STATE_DIR: root } };
42
+ return { argv: ['bin/elanous.mjs', '--config-dir', root, ...args], env: {
43
+ ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_STATE_DIR_SOURCE: stateDirSourceForChild(root),
44
+ } };
42
45
  }
43
46
  const runCli = async (args: string[]): Promise<{ stdout: string }> => {
44
47
  const { argv, env } = universeLaunch(args);
@@ -38,6 +38,7 @@ export interface DaemonPromptBody {
38
38
  * (`none` · `readonly` · `chat` · `webterm`); invalid values reject
39
39
  * the request rather than silently falling back. */
40
40
  tools?: DaemonToolSurfaceKind;
41
+ personaId?: string;
41
42
  }
42
43
 
43
44
  export interface DaemonPromptRequest {
@@ -53,6 +54,7 @@ export interface DaemonPromptRequest {
53
54
  * not opt in; the caller-side (`handlePromptStreamPost`) interprets
54
55
  * null as "use the daemon's boot-time surface". */
55
56
  tools: DaemonToolSurfaceKind | null;
57
+ personaId?: string;
56
58
  }
57
59
 
58
60
  export type DaemonPromptRequestParseResult =
@@ -117,6 +119,7 @@ export function parseDaemonPromptBody(
117
119
  body.source ? writeInputSourceMeta(body.source) : undefined,
118
120
  ),
119
121
  tools: body.tools ?? null,
122
+ ...(typeof body.personaId === 'string' && body.personaId.trim() ? { personaId: body.personaId } : {}),
120
123
  },
121
124
  };
122
125
  }
@@ -10,7 +10,7 @@ import {
10
10
  appendUserPromptBlocksAndBuildMessages,
11
11
  } from './daemon-history-helper.js';
12
12
  import type { ContentBlock as AcpContentBlock } from '@agentclientprotocol/sdk';
13
- import type { DaemonSessionHistory } from './daemon-runtime.js';
13
+ import { personaPromptForChat, type DaemonSessionHistory } from './daemon-runtime.js';
14
14
  import type { DaemonPromptRequest } from './daemon-prompt-request.js';
15
15
  import type { DaemonToolSurface } from './daemon-tools/types.js';
16
16
  import type { ConfirmChannel } from '../hitl/confirm.js';
@@ -199,8 +199,9 @@ export async function runDaemonPromptTurn(opts: {
199
199
  // …" + active terminals) to the system prompt so the LLM has the
200
200
  // session id + terminal ids without an extra discovery call. Helper
201
201
  // returns the original prompt unchanged when there's nothing to add.
202
+ const personaPrompt = await personaPromptForChat(request.personaId);
202
203
  const augmentedSystemPrompt = appendWebtermContext(
203
- request.effectiveSystemPrompt,
204
+ personaPrompt ? [personaPrompt, request.effectiveSystemPrompt].filter(Boolean).join('\n\n') : request.effectiveSystemPrompt,
204
205
  request.sessionId,
205
206
  activeToolSurface,
206
207
  );
@@ -291,6 +292,9 @@ export async function runDaemonPromptTurn(opts: {
291
292
  if (!isFirstTurn && opts.projectInstructions) {
292
293
  messages.unshift({ role: 'system', content: opts.projectInstructions });
293
294
  }
295
+ if (!isFirstTurn && personaPrompt) {
296
+ messages.unshift({ role: 'system', content: personaPrompt });
297
+ }
294
298
  const result = await runCoreTurn({
295
299
  sessionId: request.sessionId,
296
300
  userText: request.userText,
@@ -55,6 +55,33 @@ import { elanousSelfAccessPrompt } from '../agent/self-ambient.js';
55
55
  import { createToolCwdResolver, type ToolCwdResolver } from './tool-cwd.js';
56
56
  import { resolveSessionProjectContext } from '../project/session-context.js';
57
57
  import { debug } from '../debug/log.js';
58
+ import { resolvePersonaAddress } from '../intake-plane/persona-answer.js';
59
+ import { awaitGlobalPersonaLoad, getGlobalPersonaRegistry } from '../persona/global-registry.js';
60
+
61
+ const turnPersonaPrompt = new AsyncLocalStorage<string | undefined>();
62
+
63
+ export function withChatPersonaPrompt<T>(prompt: string | undefined, run: () => T): T {
64
+ return turnPersonaPrompt.run(prompt, run);
65
+ }
66
+
67
+ export function prependChatPersonaOnLaterTurn(
68
+ messages: LLMMessage[], isFirstTurn: boolean,
69
+ ): LLMMessage[] {
70
+ const prompt = turnPersonaPrompt.getStore();
71
+ if (!isFirstTurn && prompt) messages.unshift({ role: 'system', content: prompt });
72
+ return messages;
73
+ }
74
+
75
+ export async function personaPromptForChat(
76
+ id: unknown,
77
+ deps: { load: () => Promise<unknown>; registry: () => Parameters<typeof resolvePersonaAddress>[1] } = {
78
+ load: awaitGlobalPersonaLoad, registry: getGlobalPersonaRegistry,
79
+ },
80
+ ): Promise<string | undefined> {
81
+ if (typeof id !== 'string' || !id.trim()) return undefined;
82
+ await deps.load();
83
+ return resolvePersonaAddress(id, deps.registry())?.systemPrompt;
84
+ }
58
85
 
59
86
  /** C4 (2026-07-12) — arm the non-detached-PTY kill ONCE per turn signal.
60
87
  * Multiple tool calls in one turn share the same AbortSignal instance;
@@ -135,7 +162,7 @@ export function composeDaemonSystemPrompt(
135
162
  promptMeta: Readonly<Record<string, unknown>> | undefined,
136
163
  sessionId?: string,
137
164
  ): string {
138
- return [basePrompt, buildDaemonInputSourceLine(promptMeta), elanousSelfAccessPrompt(sessionId)]
165
+ return [turnPersonaPrompt.getStore(), basePrompt, buildDaemonInputSourceLine(promptMeta), elanousSelfAccessPrompt(sessionId)]
139
166
  .filter((part): part is string => typeof part === 'string' && part.trim().length > 0)
140
167
  .join('\n\n');
141
168
  }
@@ -604,12 +631,11 @@ export function createDaemonRunTurn(
604
631
  const hasNonTextBlock = !!promptBlocks?.some(
605
632
  (b) => (b as { type?: string }).type !== 'text',
606
633
  );
607
- if (hasNonTextBlock) {
608
- return appendUserPromptBlocksAndBuildMessages(
609
- history, sessionId, promptBlocks!, systemPrompt,
610
- );
611
- }
612
- return appendUserAndBuildMessages(history, sessionId, userText, systemPrompt);
634
+ const isFirstTurn = history.get(sessionId).length === 0;
635
+ const messages = hasNonTextBlock
636
+ ? appendUserPromptBlocksAndBuildMessages(history, sessionId, promptBlocks!, systemPrompt)
637
+ : appendUserAndBuildMessages(history, sessionId, userText, systemPrompt);
638
+ return prependChatPersonaOnLaterTurn(messages, isFirstTurn);
613
639
  },
614
640
  getTools: () => surface.specs,
615
641
  dispatchTool: terminalCapableDispatch(surfaceHasPtyShell, async (name, args, ctx) => {
@@ -723,15 +749,21 @@ export function createDaemonRunTurn(
723
749
  });
724
750
  debugBridge.activate();
725
751
  }
726
- const projectCwd = resolveSessionProjectContext(turnCtx.sessionId)?.cwd;
727
- if (projectCwd || opts.acpSessionCwd) {
728
- // An ACP session's explicit cwd wins; the project folder is only the default when none was given (IA3 review).
729
- const explicitCwd = opts.acpSessionCwd && turnCtx.cwd ? turnCtx.cwd : undefined;
730
- const resolver = createToolCwdResolver({ tools: opts.tools ?? 'none', toolCwd: explicitCwd ?? projectCwd ?? turnCtx.cwd });
731
- await turnToolCwd.run(resolver, () => inner(turnCtx));
732
- } else {
733
- await inner(turnCtx);
734
- }
752
+ const elanousMeta = turnCtx.promptMeta?.elanous;
753
+ const personaId = elanousMeta && typeof elanousMeta === 'object' && !Array.isArray(elanousMeta)
754
+ && 'pwaPersonaId' in elanousMeta ? elanousMeta.pwaPersonaId : undefined;
755
+ const personaPrompt = await personaPromptForChat(personaId);
756
+ await withChatPersonaPrompt(personaPrompt, async () => {
757
+ const projectCwd = resolveSessionProjectContext(turnCtx.sessionId)?.cwd;
758
+ if (projectCwd || opts.acpSessionCwd) {
759
+ // An ACP session's explicit cwd wins; the project folder is only the default when none was given (IA3 review).
760
+ const explicitCwd = opts.acpSessionCwd && turnCtx.cwd ? turnCtx.cwd : undefined;
761
+ const resolver = createToolCwdResolver({ tools: opts.tools ?? 'none', toolCwd: explicitCwd ?? projectCwd ?? turnCtx.cwd });
762
+ await turnToolCwd.run(resolver, () => inner(turnCtx));
763
+ } else {
764
+ await inner(turnCtx);
765
+ }
766
+ });
735
767
  } finally {
736
768
  debugBridge?.dispose();
737
769
  }