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
@@ -22,6 +22,9 @@ import { spawnSync } from 'node:child_process';
22
22
  import { isAbsolute } from 'node:path';
23
23
  import { createProject, listProjects } from '../../project/project-store.js';
24
24
  import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
25
+ import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
26
+ import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
27
+ import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
25
28
  import { compareTokenConstTime } from '../../acp/transport/auth.js';
26
29
  import { ensureAuthToken } from '../../auth/acp-token.js';
27
30
  import type { NexusState } from '../state/state.js';
@@ -335,7 +338,7 @@ import {
335
338
  handleTasksList,
336
339
  handleTaskDetail,
337
340
  } from './tasks-scheduler.js';
338
- import { handleTaskCardsGet } from './task-cards-api.js';
341
+ import { handleTaskCardsGet, handleTaskCardsWishPost } from './task-cards-api.js';
339
342
  import { handleTaskCreatePost, handleTaskApprovePost } from './tasks-create.js';
340
343
  import { matchIngestAuthorization } from './ingest-token.js';
341
344
  import {
@@ -344,6 +347,8 @@ import {
344
347
  handleWorkflowApprovalsPending,
345
348
  handleWorkflowDelete,
346
349
  handleWorkflowGet,
350
+ handleWorkflowHistoryList,
351
+ handleWorkflowHistoryVersionGet,
347
352
  handleWorkflowPut,
348
353
  handleWorkflowRunGet,
349
354
  handleWorkflowRunsList,
@@ -470,6 +475,9 @@ export interface NexusHttpServerOpts {
470
475
  /** Optional installer state root for isolated API consumers and tests. */
471
476
  pluginStateRoot?: string;
472
477
  outputs?: OutputsDeps;
478
+ decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
479
+ cardFollowup?: CardFollowupJobs;
480
+ skillExec?: SkillExecJobs;
473
481
  execRequests?: ExecRequestRunner;
474
482
  seatRequests?: SeatRequestsDeps;
475
483
  consultRequests?: ConsultRequestsDeps;
@@ -850,6 +858,16 @@ function execRequestRunner(): ExecRequestRunner {
850
858
  return execRequestRunnerInstance;
851
859
  }
852
860
 
861
+ let cardFollowupJobs: CardFollowupJobs | undefined;
862
+ function defaultCardFollowupJobs(): CardFollowupJobs {
863
+ return cardFollowupJobs ??= createCardFollowupJobs();
864
+ }
865
+
866
+ let skillExecJobs: SkillExecJobs | undefined;
867
+ function defaultSkillExecJobs(): SkillExecJobs {
868
+ return skillExecJobs ??= createSkillExecJobs();
869
+ }
870
+
853
871
  export async function routeRequest(
854
872
  req: Request,
855
873
  opts: NexusHttpServerOpts,
@@ -1336,6 +1354,23 @@ export async function routeRequest(
1336
1354
  return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
1337
1355
  }
1338
1356
 
1357
+ // Polling must remain outside the non-GET dispatch below.
1358
+ if (method === 'GET' && pathname.startsWith(`${SKILL_EXEC_PATH}/`) && !pathname.slice(SKILL_EXEC_PATH.length + 1).includes('/')) {
1359
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1360
+ return (opts.skillExec ?? defaultSkillExecJobs()).get(pathname.slice(SKILL_EXEC_PATH.length + 1));
1361
+ }
1362
+ if (method === 'GET' && pathname.startsWith(`${CARD_FOLLOWUP_PATH}/`) && !pathname.slice(CARD_FOLLOWUP_PATH.length + 1).includes('/')) {
1363
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1364
+ return (opts.cardFollowup ?? defaultCardFollowupJobs()).get(pathname.slice(CARD_FOLLOWUP_PATH.length + 1));
1365
+ }
1366
+
1367
+ if (method === 'GET' && pathname === DECISIONS_PATH) {
1368
+ return handleDecisions(req, {
1369
+ authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
1370
+ ...opts.decisions,
1371
+ });
1372
+ }
1373
+
1339
1374
  if (method === 'GET' && pathname === '/v1/outputs') {
1340
1375
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1341
1376
  return handleOutputsGet(req, opts.outputs);
@@ -1360,10 +1395,29 @@ export async function routeRequest(
1360
1395
  return handleSkillProblems(req, opts.metaApi);
1361
1396
  }
1362
1397
 
1398
+ if (method === 'GET' && pathname === '/v1/persona-presets') {
1399
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1400
+ return dispatchPersonaRoute(req, pathname) as Promise<Response>;
1401
+ }
1402
+
1363
1403
  // Mutation routes (PR ι) — POST/PATCH/DELETE on /v1/nexus/tabs[/:id[/action]].
1364
1404
  // Templates POST handled here too (PR κ).
1365
1405
  // Config / secrets PUT/POST/DELETE here too (PR μ).
1366
1406
  if (method !== 'GET') {
1407
+ if (method === 'POST' && pathname === SKILL_EXEC_PATH) {
1408
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1409
+ return (opts.skillExec ?? defaultSkillExecJobs()).post(req);
1410
+ }
1411
+ if (method === 'POST' && pathname === CARD_FOLLOWUP_PATH) {
1412
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1413
+ return (opts.cardFollowup ?? defaultCardFollowupJobs()).post(req);
1414
+ }
1415
+ if (method === 'POST' && /^\/v1\/decisions\/[^/]+\/decide$/.test(pathname)) {
1416
+ return handleDecisions(req, {
1417
+ authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
1418
+ ...opts.decisions,
1419
+ });
1420
+ }
1367
1421
  if (method === 'POST' && pathname === '/v1/plugins/install') {
1368
1422
  if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1369
1423
  return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
@@ -1389,6 +1443,10 @@ export async function routeRequest(
1389
1443
  const mutated = await handleGraphsMutation(pathname, req);
1390
1444
  if (mutated) return mutated;
1391
1445
  }
1446
+ if (method === 'POST' && pathname === '/v1/task-cards/wish') {
1447
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1448
+ return handleTaskCardsWishPost(req);
1449
+ }
1392
1450
  if (method === 'POST' && pathname === '/v1/tasks') {
1393
1451
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
1394
1452
  return handleTaskCreatePost(req, opts.metaApi);
@@ -1645,10 +1703,10 @@ export async function routeRequest(
1645
1703
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1646
1704
  return pathname === '/v1/setup/child-llm' ? handleChildLlmSet(req) : handleAnswerPrioritySet(req);
1647
1705
  }
1648
- // PWA `/settings` Phase 3 (2026-05-19) — persona description PATCH.
1649
- // dispatchPersonaRoute 가 PATCH 분기를 자체 처리. GET sibling 은
1650
- // 아래 GET-only 블록의 기존 /v1/personas 핸들러.
1651
- if (pathname.startsWith('/v1/personas/') && method === 'PATCH') {
1706
+ // Persona creation and editing require owner auth before parsing a write body.
1707
+ if ((method === 'POST' && pathname === '/v1/personas')
1708
+ || (method === 'PATCH' && pathname.startsWith('/v1/personas/'))) {
1709
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1652
1710
  const result = await dispatchPersonaRoute(req, pathname);
1653
1711
  if (result) return result;
1654
1712
  }
@@ -2645,7 +2703,7 @@ export async function routeRequest(
2645
2703
  catch { return jsonResponse({ error: 'bad_request' }, 400); }
2646
2704
  return handleScheduleDetail(req, id, opts.metaApi);
2647
2705
  }
2648
- if (pathname === '/v1/task-cards' || /^\/v1\/task-cards\/[^/]+$/.test(pathname)) {
2706
+ if (method === 'GET' && (pathname === '/v1/task-cards' || /^\/v1\/task-cards\/[^/]+$/.test(pathname))) {
2649
2707
  return handleTaskCardsGet(pathname);
2650
2708
  }
2651
2709
  if (pathname === '/v1/tasks') {
@@ -2738,11 +2796,23 @@ export async function routeRequest(
2738
2796
  }
2739
2797
  if (pathname.startsWith('/v1/workflows/')) {
2740
2798
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
2741
- const wfName = decodeURIComponent(pathname.slice('/v1/workflows/'.length));
2799
+ const segments = pathname.slice('/v1/workflows/'.length).split('/');
2800
+ let wfName: string;
2801
+ try { wfName = decodeURIComponent(segments[0] ?? ''); }
2802
+ catch { return jsonResponse({ error: 'bad_request', reason: 'invalid workflow name' }, 400); }
2742
2803
  if (!wfName || wfName.includes('/') || wfName.includes('\\') || wfName.includes('..')) {
2743
2804
  return jsonResponse({ error: 'bad_request', reason: 'invalid workflow name' }, 400);
2744
2805
  }
2745
- return handleWorkflowGet(req, wfName, opts.metaApi);
2806
+ if (segments.length === 2 && segments[1] === 'history') {
2807
+ return handleWorkflowHistoryList(req, wfName, opts.metaApi);
2808
+ }
2809
+ if (segments.length === 3 && segments[1] === 'history') {
2810
+ let id: string;
2811
+ try { id = decodeURIComponent(segments[2] ?? ''); }
2812
+ catch { return jsonResponse({ error: 'bad_request', reason: 'invalid workflow history version id' }, 400); }
2813
+ return handleWorkflowHistoryVersionGet(req, wfName, id, opts.metaApi);
2814
+ }
2815
+ if (segments.length === 1) return handleWorkflowGet(req, wfName, opts.metaApi);
2746
2816
  }
2747
2817
  if (pathname.startsWith('/v1/nexus/templates/')) {
2748
2818
  const name = pathname.slice('/v1/nexus/templates/'.length);
@@ -19,6 +19,9 @@ import { checkAuth, type MetaApiOpts } from './meta-api.js';
19
19
  import { routeOutbound } from '../outbound/router.js';
20
20
  import { getUserConfig } from '../../user-config.js';
21
21
  import { openSurfaceEventsDb, recordEvent } from '../../domains/surface-events.js';
22
+ import { debug } from '../../debug/log.js';
23
+ import { resolveReportTarget } from '../../telegram-report.js';
24
+ import type { RouteResult, RouterDeps } from '../outbound/router.js';
22
25
 
23
26
  /** 크로스서피스 기억(P0.5) — /v1/outbound = ALL outbound 단일 수렴점이므로 여기서
24
27
  * 원장 기록하면 sendOutbound 클라 + 직접 POST(morning-report·external) 전부 포착.
@@ -31,6 +34,21 @@ function recordOutboundEvent(text: string, kind: string): void {
31
34
  } catch { /* 발송 원장 기록 실패는 무시 */ }
32
35
  }
33
36
 
37
+ function observeDaemonSend(cfg: ReturnType<typeof getUserConfig>, text: string, kind: string, result: RouteResult, source: string): void {
38
+ if (result.suppressed) return; // 라우터가 suppressedBy 만 싣는 억제 관측을 남긴다.
39
+ try {
40
+ const target = result.channels.some(c => c.type === 'telegram') ? resolveReportTarget(cfg, kind) : null;
41
+ const id = target?.botToken.split(':', 1)[0];
42
+ const bot = id && /^\d+$/.test(id) ? `telegram:${id}` : target ? 'telegram:configured' : 'none';
43
+ debug.log('outbound.send', result.delivered ? 'sent' : 'failed', {
44
+ kind: /^[a-zA-Z0-9_-]{1,80}$/.test(kind) ? kind : 'unknown',
45
+ source: /^[a-zA-Z0-9_./-]{1,160}$/.test(source) ? source : 'unknown',
46
+ chars: text.length, bot, path: 'daemon',
47
+ channels: result.channels.map(c => ({ type: c.type, ok: c.ok })),
48
+ });
49
+ } catch { /* observation must not change the send result */ }
50
+ }
51
+
34
52
  interface OutboundBody {
35
53
  text?: string;
36
54
  markdown?: boolean;
@@ -45,14 +63,16 @@ function json(body: unknown, status: number): Response {
45
63
  }
46
64
 
47
65
  /** The daemon's own sendOutbound path (OB8) — what POST /v1/outbound does, without the HTTP self-call. */
48
- export async function routeOutboundInProcess(text: string, kind: string): Promise<boolean> {
49
- const result = await routeOutbound(getUserConfig(), { text, markdown: false, kind });
50
- if (result.delivered) recordOutboundEvent(text, kind);
66
+ export async function routeOutboundInProcess(text: string, kind: string, source = 'daemon-in-process', deferFailureObservation = false): Promise<boolean> {
67
+ const cfg = getUserConfig();
68
+ const result = await routeOutbound(cfg, { text, markdown: false, kind });
69
+ if (result.delivered || !deferFailureObservation) observeDaemonSend(cfg, text, kind, result, source);
70
+ if (result.delivered && !result.suppressed) recordOutboundEvent(text, kind);
51
71
  return result.delivered;
52
72
  }
53
73
 
54
74
  /** POST /v1/outbound — fan one message out to the routed channels. */
55
- export async function handleOutboundReport(req: Request, metaApi: MetaApiOpts): Promise<Response> {
75
+ export async function handleOutboundReport(req: Request, metaApi: MetaApiOpts, routerDeps: RouterDeps = {}): Promise<Response> {
56
76
  if (!checkAuth(req, metaApi)) return json({ error: 'unauthorized' }, 401);
57
77
 
58
78
  let body: OutboundBody;
@@ -68,16 +88,29 @@ export async function handleOutboundReport(req: Request, metaApi: MetaApiOpts):
68
88
  // Router is per-channel fail-soft; `delivered` = any channel ok.
69
89
  // Producers (outbound-alert.ts deliver()) gate their direct-telegram
70
90
  // fallback on this top-level boolean — keep it stable.
71
- const result = await routeOutbound(cfg, { text, markdown: body.markdown ?? true, kind });
72
- if (result.delivered) recordOutboundEvent(text, kind); // 크로스서피스 기억 원장(P0.5)
91
+ const result = await routeOutbound(cfg, { text, markdown: body.markdown ?? true, kind }, routerDeps);
92
+ const clientFallback = req.headers.get('X-Elanous-Client-Fallback') === 'direct';
93
+ const source = req.headers.get('X-Elanous-Outbound-Source') ?? 'daemon-http';
94
+ if (result.delivered || !clientFallback) observeDaemonSend(cfg, text, kind, result, source);
95
+ if (result.delivered && !result.suppressed) recordOutboundEvent(text, kind); // 크로스서피스 기억 원장(P0.5)
73
96
  return json({
74
97
  delivered: result.delivered,
75
98
  // Legacy single-channel field (pre-R6 consumers) + full breakdown.
76
- channel: result.delivered ? (result.channels.find(c => c.ok)?.type ?? 'none') : 'none',
99
+ channel: result.suppressed ? 'suppressed' : (result.channels.find(c => c.ok)?.type ?? 'none'),
77
100
  channels: result.channels,
101
+ ...(result.suppressed ? { suppressed: true, suppressedBy: result.suppressedBy } : {}),
102
+ ...(!result.suppressed && result.channels.length === 0 ? { reason: 'no-channel' } : {}),
78
103
  kind,
79
104
  }, result.delivered ? 200 : 503);
80
105
  } catch (e) {
106
+ if (req.headers.get('X-Elanous-Client-Fallback') !== 'direct') {
107
+ try {
108
+ debug.log('outbound.send', 'failed', {
109
+ kind: /^[a-zA-Z0-9_-]{1,80}$/.test(kind) ? kind : 'unknown',
110
+ source: 'daemon-http', chars: text.length, bot: 'unknown', path: 'daemon',
111
+ });
112
+ } catch { /* observation must not change the response */ }
113
+ }
81
114
  return json({ error: e instanceof Error ? e.message : String(e) }, 502);
82
115
  }
83
116
  }
@@ -1,27 +1,14 @@
1
- // NEXUS · personas REST handlers (§6.4 · 2026-05-09).
2
- //
3
- // Endpoints
4
- // GET /v1/personas — list all loaded personas
5
- // GET /v1/personas/:personaId — single persona profile
6
- //
7
- // Architecture: read-only thin wrapper over the global PersonaRegistry
8
- // (`src/persona/global-registry.ts`). Disk yaml is the source of truth;
9
- // editing requires text editor + `elanous persona reload` (or fs.watch
10
- // auto-reload). PWA Showroom consumes this for the per-panel persona
11
- // picker (§6.4 Q2 = REST read-only).
12
- //
13
- // PLAN: 내부 문서 `PLAN-cv-3-showroom-mvp-2026-05-07` §6.4
14
- // Cross-ref: src/persona/global-registry.ts (singleton accessor)
15
- // src/persona/types.ts (PersonaProfile shape)
16
-
17
- import { homedir } from 'node:os';
18
- import { join } from 'node:path';
1
+ // NEXUS · personas REST handlers. Presets clone into the state persona store;
2
+ // writes reload the global registry so subsequent reads see the saved profile.
19
3
 
20
4
  import {
21
5
  awaitGlobalPersonaLoad,
22
6
  getGlobalPersonaRegistry,
23
7
  reloadGlobalPersonaRegistry,
8
+ resolveStatePersonaDir,
24
9
  } from '../../persona/global-registry.js';
10
+ import { clonePreset, editPersona, loadPresets } from '../../persona/presets.js';
11
+ import { resolvePersonaAddress } from '../../intake-plane/persona-answer.js';
25
12
  import type { PersonaRegistryEvent } from '../../persona/registry.js';
26
13
  import type { PersonaProfile } from '../../persona/types.js';
27
14
  import {
@@ -113,7 +100,7 @@ export async function handlePersonaGet(
113
100
  }
114
101
  await awaitGlobalPersonaLoad();
115
102
  const registry = getGlobalPersonaRegistry();
116
- const persona = registry.get(personaId);
103
+ const persona = resolvePersonaAddress(personaId, registry);
117
104
  if (!persona) return notFound(`persona ${personaId} not found`);
118
105
  return jsonResponse({ persona: toWire(persona) }, 200);
119
106
  }
@@ -193,22 +180,41 @@ export function handlePersonasEvents(
193
180
  });
194
181
  }
195
182
 
196
- /** Resolve personas dir same way `global-registry.ts` does, so the PATCH
197
- * endpoint targets the file the registry will reload. */
198
- function resolvePersonasDir(): string {
199
- const env = process.env.ELANOUS_PERSONAS_DIR;
200
- if (env && env.length > 0) return env;
201
- return join(homedir(), '.elanous', 'personas');
183
+ /** GET /v1/persona-presets — shipped preset catalog in index order. */
184
+ export function handlePersonaPresets(req: Request, opts: PersonaRouteOpts = {}): Response {
185
+ if (opts.checkAuth && !opts.checkAuth(req)) return jsonResponse({ error: 'unauthorized' }, 401);
186
+ return jsonResponse({ presets: loadPresets() }, 200);
202
187
  }
203
188
 
204
- /** PATCH /v1/personas/:personaId — update description.
205
- *
206
- * Body: `{ description: "<string, ≤ 280 chars>" }` — empty string clears.
207
- * Other fields ignored (yaml surgical edit pattern).
208
- *
209
- * Returns 200 with the updated `PersonaWire` after reload, 400 / 404 /
210
- * 500 with structured `{ error, reason? }`.
211
- */
189
+ /** POST /v1/personas — clone a shipped preset into the user's persona store. */
190
+ export async function handlePersonaCreate(req: Request, opts: PersonaRouteOpts = {}): Promise<Response> {
191
+ if (opts.checkAuth && !opts.checkAuth(req)) return jsonResponse({ error: 'unauthorized' }, 401);
192
+ let body: unknown;
193
+ try { body = await req.json(); }
194
+ catch { return jsonResponse({ error: 'invalid-json' }, 400); }
195
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'invalid-body' }, 400);
196
+ const { preset, name } = body as Record<string, unknown>;
197
+ if (typeof preset !== 'string' || !preset.trim() || typeof name !== 'string' || !name.trim()) {
198
+ return jsonResponse({ error: 'preset-and-name-required' }, 400);
199
+ }
200
+ await awaitGlobalPersonaLoad();
201
+ if (!loadPresets().some((p) => p.personaId === preset)) return notFound(`preset ${preset} not found`);
202
+ try {
203
+ const created = clonePreset(preset, name.trim(), { dir: resolveStatePersonaDir() });
204
+ await reloadGlobalPersonaRegistry();
205
+ const persona = getGlobalPersonaRegistry().get(created.personaId);
206
+ if (!persona) return notFound(`persona ${created.personaId} not found after reload`);
207
+ return jsonResponse({ persona: toWire(persona) }, 201);
208
+ } catch (error) {
209
+ if (error instanceof Error && error.message.startsWith('persona name already exists:')) {
210
+ return jsonResponse({ error: 'persona-name-conflict' }, 409);
211
+ }
212
+ return jsonResponse({ error: 'create-failed' }, 500);
213
+ }
214
+ }
215
+
216
+ /** PATCH /v1/personas/:personaId — description-only edits retain the
217
+ * existing surgical YAML behavior; multi-field edits use the persona store. */
212
218
  export async function handlePersonaPatch(
213
219
  req: Request,
214
220
  personaId: string,
@@ -217,17 +223,23 @@ export async function handlePersonaPatch(
217
223
  if (opts.checkAuth && !opts.checkAuth(req)) {
218
224
  return jsonResponse({ error: 'unauthorized' }, 401);
219
225
  }
220
- let body: { description?: unknown };
221
- try {
222
- body = (await req.json()) as { description?: unknown };
223
- } catch {
224
- return jsonResponse({ error: 'invalid-json' }, 400);
226
+ let parsed: unknown;
227
+ try { parsed = await req.json(); }
228
+ catch { return jsonResponse({ error: 'invalid-json' }, 400); }
229
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
230
+ return jsonResponse({ error: 'invalid-body' }, 400);
225
231
  }
226
- if (typeof body.description !== 'string') {
227
- return jsonResponse({ error: 'description-required' }, 400);
232
+ const body = parsed as Record<string, unknown>;
233
+ const fields = Object.keys(body);
234
+ if (!fields.length) return jsonResponse({ error: 'description-required' }, 400);
235
+ if (fields.some((field) => !['displayName', 'description', 'systemPrompt'].includes(field))) {
236
+ return jsonResponse({ error: 'invalid-persona-field' }, 400);
228
237
  }
229
- if (body.description.length > MAX_PERSONA_DESCRIPTION_LENGTH * 4) {
230
- // Guard against pathological inputs before the helper trims.
238
+ if (fields.some((field) => typeof body[field] !== 'string')
239
+ || (body.displayName !== undefined && !(body.displayName as string).trim())) {
240
+ return jsonResponse({ error: 'invalid-persona-field' }, 400);
241
+ }
242
+ if (typeof body.description === 'string' && body.description.length > MAX_PERSONA_DESCRIPTION_LENGTH * 4) {
231
243
  return jsonResponse({ error: 'description-too-long' }, 400);
232
244
  }
233
245
 
@@ -237,14 +249,37 @@ export async function handlePersonaPatch(
237
249
  return notFound(`persona ${personaId} not found`);
238
250
  }
239
251
 
240
- const dir = resolvePersonasDir();
241
- const result = updatePersonaDescription(dir, personaId, body.description);
242
- if (!result.ok) {
243
- const status = result.reason === 'file-not-found' ? 404 : 400;
244
- return jsonResponse({ error: 'patch-failed', reason: result.reason }, status);
252
+ const dir = resolveStatePersonaDir();
253
+ if (fields.length === 1 && typeof body.description === 'string') {
254
+ const result = updatePersonaDescription(dir, personaId, body.description);
255
+ if (!result.ok) {
256
+ const status = result.reason === 'file-not-found' ? 404 : 400;
257
+ return jsonResponse({ error: 'patch-failed', reason: result.reason }, status);
258
+ }
259
+ } else {
260
+ const edits: Record<string, unknown> = {};
261
+ for (const field of fields) edits[field] = body[field];
262
+ if (typeof edits.description === 'string') {
263
+ const description = edits.description.trim();
264
+ edits.description = description;
265
+ if (description.length > MAX_PERSONA_DESCRIPTION_LENGTH) {
266
+ return jsonResponse({ error: 'description-too-long' }, 400);
267
+ }
268
+ }
269
+ try {
270
+ editPersona(personaId, edits, { dir });
271
+ } catch (error) {
272
+ if (error instanceof Error && error.message.startsWith('persona not found:')) {
273
+ return notFound(`persona ${personaId} not found in state store`);
274
+ }
275
+ if (error instanceof Error && error.message.startsWith('persona name already exists:')) {
276
+ return jsonResponse({ error: 'persona-name-conflict' }, 409);
277
+ }
278
+ return jsonResponse({ error: 'patch-failed', reason: error instanceof Error ? error.message : 'invalid-persona' }, 400);
279
+ }
245
280
  }
246
281
 
247
- // Reload so the in-memory registry reflects the new description on the
282
+ // Reload so the in-memory registry reflects the saved fields on the
248
283
  // next GET. fs.watch would auto-reload but is async + race-y vs the
249
284
  // immediate echo below.
250
285
  await reloadGlobalPersonaRegistry();
@@ -255,7 +290,7 @@ export async function handlePersonaPatch(
255
290
  return jsonResponse({ persona: toWire(reloaded) }, 200);
256
291
  }
257
292
 
258
- /** Combined dispatcher — `/v1/personas[/:id]` + `/v1/personas/events`.
293
+ /** Combined dispatcher — `/v1/persona-presets` + `/v1/personas[/:id]` + `/v1/personas/events`.
259
294
  * Returns null when the pathname doesn't match so the caller chains. */
260
295
  export async function dispatchPersonaRoute(
261
296
  req: Request,
@@ -263,11 +298,14 @@ export async function dispatchPersonaRoute(
263
298
  opts: PersonaRouteOpts = {},
264
299
  ): Promise<Response | null> {
265
300
  const method = req.method.toUpperCase();
301
+ if (pathname === '/v1/persona-presets') {
302
+ if (method !== 'GET') return jsonResponse({ error: 'method not allowed' }, 405);
303
+ return handlePersonaPresets(req, opts);
304
+ }
266
305
  if (pathname === '/v1/personas') {
267
- if (method !== 'GET') {
268
- return jsonResponse({ error: 'method not allowed' }, 405);
269
- }
270
- return handlePersonasList(req, opts);
306
+ if (method === 'GET') return handlePersonasList(req, opts);
307
+ if (method === 'POST') return handlePersonaCreate(req, opts);
308
+ return jsonResponse({ error: 'method not allowed' }, 405);
271
309
  }
272
310
  // SSE — must come BEFORE the single-persona regex so that the
273
311
  // literal "events" segment isn't misread as a personaId.
@@ -279,8 +317,13 @@ export async function dispatchPersonaRoute(
279
317
  }
280
318
  const match = pathname.match(/^\/v1\/personas\/([^/]+)$/);
281
319
  if (!match) return null;
282
- const personaId = decodeURIComponent(match[1]!);
320
+ let personaId: string;
321
+ try { personaId = decodeURIComponent(match[1]!); }
322
+ catch { return jsonResponse({ error: 'invalid-persona-id' }, 400); }
283
323
  if (method === 'PATCH') {
324
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(personaId)) {
325
+ return jsonResponse({ error: 'invalid-persona-id' }, 400);
326
+ }
284
327
  return handlePersonaPatch(req, personaId, opts);
285
328
  }
286
329
  if (method !== 'GET') {
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { debug } from '../../debug/log.js';
5
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
+ import { dispatchSkillExec } from '../../tool-runtime/skill-exec-runtime.js';
7
+ import { jsonResponse } from './json-response.js';
8
+
9
+ export const SKILL_EXEC_PATH = '/v1/skills/exec';
10
+
11
+ type SkillJob = {
12
+ id: string;
13
+ skill: string;
14
+ status: 'running' | 'done' | 'rejected' | 'failed';
15
+ updatedAt: number;
16
+ ok?: boolean;
17
+ output?: string;
18
+ reason?: 'not-allowlisted';
19
+ error?: string;
20
+ };
21
+
22
+ type SkillExecDeps = {
23
+ rootDir?: string;
24
+ exec?: typeof dispatchSkillExec;
25
+ now?: () => number;
26
+ };
27
+
28
+ export function createSkillExecJobs({ rootDir = effectiveInstanceRoot(), exec = dispatchSkillExec, now = Date.now }: SkillExecDeps = {}) {
29
+ const base = join(rootDir, 'skill-exec');
30
+ const active = new Set<string>();
31
+ const unavailable = new Set<string>();
32
+ const file = (id: string) => join(base, `${id}.json`);
33
+ const save = (job: SkillJob) => {
34
+ mkdirSync(base, { recursive: true, mode: 0o700 });
35
+ chmodSync(base, 0o700);
36
+ writeFileSync(file(job.id), JSON.stringify(job), { mode: 0o600 });
37
+ chmodSync(file(job.id), 0o600);
38
+ };
39
+
40
+ return {
41
+ async post(req: Request): Promise<Response> {
42
+ let body: unknown;
43
+ try { body = await req.json(); } catch { body = null; }
44
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
45
+ const { skill, task } = body as Record<string, unknown>;
46
+ if (typeof skill !== 'string' || !skill.trim() || typeof task !== 'string' || !task.trim() || task.length > 4000) {
47
+ return jsonResponse({ error: 'bad_request' }, 400);
48
+ }
49
+ const id = randomUUID();
50
+ const update = (status: SkillJob['status'], fields: Partial<Pick<SkillJob, 'ok' | 'output' | 'reason' | 'error'>> = {}) =>
51
+ save({ id, skill, status, updatedAt: now(), ...fields });
52
+ update('running');
53
+ active.add(id);
54
+ debug.log('skill-exec.pwa', 'started', { id, skill });
55
+ setTimeout(() => {
56
+ void (async () => {
57
+ try {
58
+ const result = await exec({ skill, task });
59
+ if (!result.ok && result.output.startsWith(`자동 실행 대상 아님: ${skill}.`)) {
60
+ update('rejected', { reason: 'not-allowlisted' });
61
+ debug.log('skill-exec.pwa', 'rejected', { id, skill });
62
+ } else {
63
+ update('done', { ok: result.ok, output: result.output.slice(0, 20_000) });
64
+ debug.log('skill-exec.pwa', 'done', { id, skill });
65
+ }
66
+ } catch {
67
+ try {
68
+ update('failed', { error: 'skill-exec-failed' });
69
+ } catch {
70
+ unavailable.add(id);
71
+ }
72
+ debug.log('skill-exec.pwa', 'failed', { id, skill });
73
+ } finally {
74
+ active.delete(id);
75
+ }
76
+ })().catch(() => {
77
+ unavailable.add(id);
78
+ active.delete(id);
79
+ });
80
+ });
81
+ return jsonResponse({ id }, 202);
82
+ },
83
+ get(id: string): Response {
84
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) return jsonResponse({ error: 'not-found' }, 404);
85
+ if (unavailable.has(id)) return jsonResponse({ id, status: 'failed', error: 'skill-exec-failed' });
86
+ let job: SkillJob;
87
+ try { job = JSON.parse(readFileSync(file(id), 'utf8')) as SkillJob; }
88
+ catch { return jsonResponse({ error: 'not-found' }, 404); }
89
+ if (job.id !== id) return jsonResponse({ error: 'not-found' }, 404);
90
+ if (job.status === 'running' && !active.has(id)) return jsonResponse({ id, status: 'failed', error: 'daemon-restarted' });
91
+ const { status, ok, output, reason, error } = job;
92
+ return jsonResponse({ id, status, ...(ok !== undefined ? { ok } : {}), ...(output !== undefined ? { output } : {}), ...(reason ? { reason } : {}), ...(error ? { error } : {}) });
93
+ },
94
+ };
95
+ }
96
+
97
+ export type SkillExecJobs = ReturnType<typeof createSkillExecJobs>;
@@ -1,6 +1,25 @@
1
1
  import { CardStore } from '../../task-cards/card-store.js';
2
+ import { createWishCard } from '../../intake-plane/wish-card.js';
2
3
  import { jsonResponse } from './json-response.js';
3
4
 
5
+ export async function handleTaskCardsWishPost(req: Request, root?: string): Promise<Response> {
6
+ let body: unknown;
7
+ try { body = await req.json(); }
8
+ catch { return jsonResponse({ error: 'bad_request' }, 400); }
9
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
10
+ const { text, ref } = body as Record<string, unknown>;
11
+ if (typeof text !== 'string' || !text.trim() || typeof ref !== 'string' || !ref.trim()) {
12
+ return jsonResponse({ error: 'bad_request' }, 400);
13
+ }
14
+ const store = new CardStore(root);
15
+ try {
16
+ const card = createWishCard({ text, source: 'pwa', ref }, store);
17
+ return jsonResponse(card, card.created ? 201 : 200);
18
+ } finally {
19
+ store.close();
20
+ }
21
+ }
22
+
4
23
  /** Read-only card API; route registration and authorization belong to the HTTP server. */
5
24
  export function handleTaskCardsGet(pathname: string, root?: string): Response {
6
25
  if (pathname !== '/v1/task-cards' && !/^\/v1\/task-cards\/[^/]+$/.test(pathname)) {