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
package/src/memory.ts CHANGED
@@ -63,6 +63,16 @@ export function writeFileDurable(path: string, data: string, fsync: (fd: number)
63
63
 
64
64
  export type MemoryType = 'user' | 'feedback' | 'project' | 'reference';
65
65
 
66
+ /** Context-condense metadata travels with the existing curated memory file, not a parallel store. */
67
+ export interface ContextMemoryCard {
68
+ project: string;
69
+ seat: string;
70
+ topic: string;
71
+ source: string;
72
+ updatedAt: string;
73
+ status: 'active' | 'retired';
74
+ }
75
+
66
76
  export interface MemoryEntry {
67
77
  id: string; // uuid v4
68
78
  filename: string; // "<uuid>-<slug>.md"
@@ -89,6 +99,7 @@ export interface MemoryEntry {
89
99
  pinned: boolean;
90
100
  /** Optional pinned-memory freshness window; absent memories retain their original injection text. */
91
101
  staleAfterMinutes?: number;
102
+ contextCard?: ContextMemoryCard;
92
103
  }
93
104
 
94
105
  // ── Paths ────────────────────────────────────────────────────────────
@@ -192,6 +203,7 @@ export interface SaveMemoryOpts {
192
203
  staleAfterMinutes?: number;
193
204
  /** Override the generated id (tests). */
194
205
  id?: string;
206
+ contextCard?: ContextMemoryCard;
195
207
  }
196
208
 
197
209
  export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): MemoryEntry {
@@ -207,6 +219,7 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
207
219
  let existingPriority: number | undefined;
208
220
  let existingPinned: boolean | undefined;
209
221
  let existingStaleAfterMinutes: number | undefined;
222
+ let existingContextCard: ContextMemoryCard | undefined;
210
223
  if (existsSync(path)) {
211
224
  try {
212
225
  const parsed = parseFrontmatter(readFileSync(path, 'utf-8'));
@@ -214,6 +227,7 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
214
227
  if (parsed.fm.priority) existingPriority = parseInt(parsed.fm.priority, 10) || 0;
215
228
  if (parsed.fm.pinned) existingPinned = parsed.fm.pinned === 'true';
216
229
  existingStaleAfterMinutes = parseStaleAfterMinutes(parsed.fm['stale-after-minutes']);
230
+ existingContextCard = parseContextCard(parsed.fm['context-card']);
217
231
  } catch { /* keep new createdAt */ }
218
232
  }
219
233
  const priority = opts.priority ?? existingPriority ?? 0;
@@ -229,6 +243,8 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
229
243
  if (priority > 0) frontmatter.priority = String(Math.floor(priority));
230
244
  if (pinned) frontmatter.pinned = 'true';
231
245
  if (staleAfterMinutes !== undefined) frontmatter['stale-after-minutes'] = String(staleAfterMinutes);
246
+ const contextCard = opts.contextCard ?? existingContextCard;
247
+ if (contextCard) frontmatter['context-card'] = JSON.stringify(contextCard);
232
248
  const raw = renderFrontmatter(frontmatter, opts.body);
233
249
  writeFileDurable(path, raw);
234
250
 
@@ -248,9 +264,23 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
248
264
  priority,
249
265
  pinned,
250
266
  staleAfterMinutes,
267
+ ...(contextCard ? { contextCard } : {}),
251
268
  };
252
269
  }
253
270
 
271
+ function parseContextCard(raw: string | undefined): ContextMemoryCard | undefined {
272
+ if (!raw) return undefined;
273
+ try {
274
+ const card: unknown = JSON.parse(raw);
275
+ if (card && typeof card === 'object' && !Array.isArray(card)) {
276
+ const c = card as Record<string, unknown>;
277
+ if (['project', 'seat', 'topic', 'source', 'updatedAt'].every(key => typeof c[key] === 'string')
278
+ && (c.status === 'active' || c.status === 'retired')) return c as unknown as ContextMemoryCard;
279
+ }
280
+ } catch { /* malformed metadata is not a context memory */ }
281
+ return undefined;
282
+ }
283
+
254
284
  /** Parse one memory file. Returns null on read/parse error. */
255
285
  function parseMemoryFile(path: string): MemoryEntry | null {
256
286
  if (!existsSync(path)) return null;
@@ -261,6 +291,7 @@ function parseMemoryFile(path: string): MemoryEntry | null {
261
291
  const basename = path.split(/[\\/]/).pop() ?? '';
262
292
  const match = basename.match(/^([0-9a-f-]{36})-(.+)\.md$/i);
263
293
  const id = match ? match[1] : createHash('sha1').update(basename).digest('hex').slice(0, 36);
294
+ const contextCard = parseContextCard(fm['context-card']);
264
295
  return {
265
296
  id,
266
297
  filename: basename,
@@ -274,6 +305,7 @@ function parseMemoryFile(path: string): MemoryEntry | null {
274
305
  priority: fm.priority ? (parseInt(fm.priority, 10) || 0) : 0,
275
306
  pinned: fm.pinned === 'true',
276
307
  staleAfterMinutes: parseStaleAfterMinutes(fm['stale-after-minutes']),
308
+ ...(contextCard ? { contextCard } : {}),
277
309
  };
278
310
  }
279
311
 
@@ -364,6 +396,7 @@ export function searchMemories(
364
396
  const entries = listMemories({ type: opts.type }, root);
365
397
  const hits: SearchHit[] = [];
366
398
  for (const entry of entries) {
399
+ if (entry.contextCard?.status === 'retired') continue;
367
400
  // 정확-토큰 매칭(2026-07-19) — 이전 substring `includes(kw)` 는 "ref"가 "reference"·
368
401
  // "prefer" 안에까지 걸려 오차용(false-recall)을 냈다(openclaw tool-search.ts 이식:
369
402
  // tokenize→Set.has 전체토큰 동등). name/description 은 토큰집합 정확매칭, body 는
@@ -415,7 +448,7 @@ export function rebuildIndex(root: string = memoryRoot()): void {
415
448
  const byType: Record<MemoryType, MemoryEntry[]> = {
416
449
  user: [], feedback: [], project: [], reference: [],
417
450
  };
418
- for (const e of entries) byType[e.type].push(e);
451
+ for (const e of entries) if (e.contextCard?.status !== 'retired') byType[e.type].push(e);
419
452
 
420
453
  const lines: string[] = [];
421
454
  lines.push('# elanous memory index');
@@ -515,7 +548,7 @@ export function buildMemoryInjection(
515
548
 
516
549
  // 📌 pinned 기억 — 키워드 무관 항상 주입(relevance gate bypass · Letta pinned block /
517
550
  // claude-code MEMORY.md tier 동형). priority 높은 순. core/safety/ref-location 사실용.
518
- const pinnedMems = listMemories({}, root).filter(m => m.pinned).sort((a, b) => b.priority - a.priority);
551
+ const pinnedMems = listMemories({}, root).filter(m => m.pinned && m.contextCard?.status !== 'retired').sort((a, b) => b.priority - a.priority);
519
552
  const pinnedParts: string[] = [];
520
553
  for (const e of pinnedMems) {
521
554
  const chunk = pinnedChunk(e);
@@ -678,7 +711,7 @@ export async function buildMemoryInjectionLLM(
678
711
  }
679
712
 
680
713
  // pinned 항상주입(키워드/판정 무관).
681
- const pinnedMems = listMemories({}, root).filter((m) => m.pinned).sort((a, b) => b.priority - a.priority);
714
+ const pinnedMems = listMemories({}, root).filter((m) => m.pinned && m.contextCard?.status !== 'retired').sort((a, b) => b.priority - a.priority);
682
715
  const pinnedParts: string[] = [];
683
716
  for (const e of pinnedMems) {
684
717
  const chunk = pinnedChunk(e);
@@ -693,7 +726,7 @@ export async function buildMemoryInjectionLLM(
693
726
  let judged: MemoryEntry[] = [];
694
727
  if (!lowContext) {
695
728
  const candidates = listMemories({}, root)
696
- .filter((m) => !m.pinned && !injected.includes(m.id))
729
+ .filter((m) => !m.pinned && m.contextCard?.status !== 'retired' && !injected.includes(m.id))
697
730
  .slice(0, opts.maxCandidates ?? 60);
698
731
  if (candidates.length > 0) {
699
732
  try {
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute, join, relative, sep } from 'node:path';
4
+ import { defaultAttachmentBaseDir, resolveAttachmentPath } from '../../boot/attachment-store.js';
5
+ import { detectCard, NotACardError, runCardFollowup } from '../../card-followup/core.js';
6
+ import { debug } from '../../debug/log.js';
7
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
+ import { localOcrText } from '../../telegram-card-followup.js';
9
+ import { jsonResponse } from './json-response.js';
10
+
11
+ export const CARD_FOLLOWUP_PATH = '/v1/card-followup';
12
+
13
+ type CardJob = { id: string; status: 'judging' | 'running' | 'done' | 'not-card' | 'failed'; updatedAt: number; replies?: [string, string, string]; error?: string };
14
+ type CardDeps = {
15
+ rootDir?: string;
16
+ resolveAttachment?: typeof resolveAttachmentPath;
17
+ ocr?: typeof localOcrText;
18
+ detect?: typeof detectCard;
19
+ run?: typeof runCardFollowup;
20
+ now?: () => number;
21
+ };
22
+
23
+ function withinAttachmentStore(path: string): boolean {
24
+ try {
25
+ const relativePath = relative(realpathSync(defaultAttachmentBaseDir()), realpathSync(path));
26
+ return relativePath !== '' && relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
27
+ } catch { return false; }
28
+ }
29
+
30
+ /** The in-memory set identifies only work launched by this daemon; disk is the pollable ledger. */
31
+ export function createCardFollowupJobs({
32
+ rootDir = effectiveInstanceRoot(),
33
+ resolveAttachment = resolveAttachmentPath,
34
+ ocr = localOcrText,
35
+ detect = detectCard,
36
+ run = runCardFollowup,
37
+ now = Date.now,
38
+ }: CardDeps = {}) {
39
+ const base = join(rootDir, 'graph-runs', 'card-followup');
40
+ const active = new Set<string>();
41
+ const unavailable = new Set<string>();
42
+ const file = (id: string) => join(base, `${id}.pwa.json`);
43
+ const save = (job: CardJob) => {
44
+ mkdirSync(base, { recursive: true, mode: 0o700 });
45
+ chmodSync(base, 0o700);
46
+ writeFileSync(file(job.id), JSON.stringify(job), { mode: 0o600 });
47
+ chmodSync(file(job.id), 0o600);
48
+ };
49
+
50
+ return {
51
+ async post(req: Request): Promise<Response> {
52
+ let body: unknown;
53
+ try { body = await req.json(); } catch { body = null; }
54
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
55
+ const { attachmentId, context } = body as Record<string, unknown>;
56
+ if (context !== undefined && typeof context !== 'string') return jsonResponse({ error: 'bad_request' }, 400);
57
+ const imagePath = typeof attachmentId === 'string' ? resolveAttachment(attachmentId) : null;
58
+ if (!imagePath || (resolveAttachment === resolveAttachmentPath && !withinAttachmentStore(imagePath))) {
59
+ return jsonResponse({ error: 'attachment-not-found' }, 400);
60
+ }
61
+
62
+ const id = randomUUID();
63
+ const update = (status: CardJob['status'], fields: Partial<Pick<CardJob, 'replies' | 'error'>> = {}) =>
64
+ save({ id, status, updatedAt: now(), ...fields });
65
+ update('judging');
66
+ active.add(id);
67
+ debug.log('card-followup.pwa', 'started', { id });
68
+ // Start OCR on the next event-loop turn so the acceptance response precedes the work.
69
+ setTimeout(() => {
70
+ void (async () => {
71
+ try {
72
+ const ocrText = await ocr(imagePath);
73
+ if (detect({ ocrText }).decision !== 'card') {
74
+ update('not-card');
75
+ debug.log('card-followup.pwa', 'not-card', { id });
76
+ return;
77
+ }
78
+ update('running');
79
+ const result = await run({ imagePath, ocrText, rootDir, runId: id, ...(context === undefined ? {} : { context }) });
80
+ update('done', { replies: result.replies });
81
+ debug.log('card-followup.pwa', 'done', { id });
82
+ } catch (cause) {
83
+ const notCard = cause instanceof NotACardError;
84
+ // OCR·그래프 예외 문구엔 명함 원문이 섞일 수 있다 — 원장·응답엔 고정 코드만 싣는다(리뷰 must-fix).
85
+ try {
86
+ update(notCard ? 'not-card' : 'failed', notCard ? {} : { error: 'card-followup-failed' });
87
+ debug.log('card-followup.pwa', notCard ? 'not-card' : 'failed', { id });
88
+ } catch {
89
+ unavailable.add(id);
90
+ debug.log('card-followup.pwa', 'failed', { id });
91
+ }
92
+ } finally {
93
+ active.delete(id);
94
+ }
95
+ })().catch(() => {
96
+ unavailable.add(id);
97
+ active.delete(id);
98
+ });
99
+ });
100
+ return jsonResponse({ id }, 202);
101
+ },
102
+ get(id: string): Response {
103
+ 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);
104
+ if (unavailable.has(id)) return jsonResponse({ id, status: 'failed', error: 'card-followup-failed' });
105
+ let job: CardJob;
106
+ try { job = JSON.parse(readFileSync(file(id), 'utf8')) as CardJob; }
107
+ catch { return jsonResponse({ error: 'not-found' }, 404); }
108
+ if (job.id !== id) return jsonResponse({ error: 'not-found' }, 404);
109
+ if ((job.status === 'judging' || job.status === 'running') && !active.has(id)) {
110
+ return jsonResponse({ id, status: 'failed', error: 'daemon-restarted' });
111
+ }
112
+ const { status, replies, error } = job;
113
+ return jsonResponse({ id, status, ...(replies ? { replies } : {}), ...(error ? { error } : {}) });
114
+ },
115
+ };
116
+ }
117
+
118
+ export type CardFollowupJobs = ReturnType<typeof createCardFollowupJobs>;
@@ -0,0 +1,68 @@
1
+ import { debug } from '../../debug/log.js';
2
+ import { DecisionLedger, type DecisionEntry } from '../../decisions/decision-ledger.js';
3
+ import { jsonResponse } from './json-response.js';
4
+
5
+ export const DECISIONS_PATH = '/v1/decisions';
6
+
7
+ export interface DecisionsRouteDeps {
8
+ authorize?: (request: Request) => boolean;
9
+ ledger?: Pick<DecisionLedger, 'list' | 'decide'>;
10
+ }
11
+
12
+ export async function handleDecisions(req: Request, deps: DecisionsRouteDeps = {}): Promise<Response> {
13
+ const pathname = new URL(req.url).pathname;
14
+ if (pathname !== DECISIONS_PATH && !/^\/v1\/decisions\/[^/]+\/decide$/.test(pathname)) return jsonResponse({ error: 'not-found' }, 404);
15
+ if (!deps.authorize?.(req)) {
16
+ debug.log('decisions.api', 'rejected', { reason: 'unauthorized' });
17
+ return jsonResponse({ error: 'unauthorized' }, 401);
18
+ }
19
+ const ledger = deps.ledger ?? new DecisionLedger();
20
+ if (pathname === DECISIONS_PATH) {
21
+ if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
22
+ if (new URL(req.url).searchParams.get('status') !== 'open') {
23
+ debug.log('decisions.api', 'rejected', { reason: 'invalid-status' });
24
+ return jsonResponse({ error: 'bad_request' }, 400);
25
+ }
26
+ const decisions = ledger.list({ status: 'open' }).map(entry => ({
27
+ id: entry.id,
28
+ title: entry.title,
29
+ situation: entry.scqa.s.slice(0, 200),
30
+ options: entry.options.map(option => ({ id: option.key, label: option.label })),
31
+ recommendation: entry.recommendation,
32
+ raisedAt: entry.raisedAt,
33
+ dueAt: entry.dueAt,
34
+ }));
35
+ debug.log('decisions.api', 'listed', { count: decisions.length });
36
+ return jsonResponse({ decisions });
37
+ }
38
+ if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
39
+ let id: string;
40
+ try { id = decodeURIComponent(pathname.slice(DECISIONS_PATH.length + 1, -'/decide'.length)); }
41
+ catch {
42
+ debug.log('decisions.api', 'rejected', { reason: 'invalid-id' });
43
+ return jsonResponse({ error: 'not-found' }, 404);
44
+ }
45
+ let body: unknown;
46
+ try { body = await req.json(); } catch { body = null; }
47
+ if (!body || typeof body !== 'object' || Array.isArray(body)
48
+ || !('choice' in body) || typeof body.choice !== 'string' || !body.choice
49
+ || ('note' in body && body.note !== undefined && typeof body.note !== 'string')) {
50
+ debug.log('decisions.api', 'rejected', { id, reason: 'invalid-body' });
51
+ return jsonResponse({ error: 'bad_request' }, 400);
52
+ }
53
+ const { choice, note } = body as { choice: string; note?: string };
54
+ try {
55
+ const entry: DecisionEntry = ledger.decide(id, choice, { kind: 'human' }, note);
56
+ debug.log('decisions.api', 'decided', { id });
57
+ return jsonResponse(entry);
58
+ } catch (error) {
59
+ const message = error instanceof Error ? error.message : '';
60
+ const status = message.startsWith('decision not found:') ? 404
61
+ : message.startsWith('decision already closed:') ? 409
62
+ : message.startsWith('option not found:') || message.startsWith('note ') ? 400 : 0;
63
+ if (!status) throw error;
64
+ const reason = status === 404 ? 'not-found' : status === 409 ? 'already-decided' : 'bad_request';
65
+ debug.log('decisions.api', 'rejected', { id, reason });
66
+ return jsonResponse({ error: reason }, status);
67
+ }
68
+ }
@@ -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';
@@ -472,6 +475,9 @@ export interface NexusHttpServerOpts {
472
475
  /** Optional installer state root for isolated API consumers and tests. */
473
476
  pluginStateRoot?: string;
474
477
  outputs?: OutputsDeps;
478
+ decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
479
+ cardFollowup?: CardFollowupJobs;
480
+ skillExec?: SkillExecJobs;
475
481
  execRequests?: ExecRequestRunner;
476
482
  seatRequests?: SeatRequestsDeps;
477
483
  consultRequests?: ConsultRequestsDeps;
@@ -852,6 +858,16 @@ function execRequestRunner(): ExecRequestRunner {
852
858
  return execRequestRunnerInstance;
853
859
  }
854
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
+
855
871
  export async function routeRequest(
856
872
  req: Request,
857
873
  opts: NexusHttpServerOpts,
@@ -1338,6 +1354,23 @@ export async function routeRequest(
1338
1354
  return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
1339
1355
  }
1340
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
+
1341
1374
  if (method === 'GET' && pathname === '/v1/outputs') {
1342
1375
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1343
1376
  return handleOutputsGet(req, opts.outputs);
@@ -1371,6 +1404,20 @@ export async function routeRequest(
1371
1404
  // Templates POST handled here too (PR κ).
1372
1405
  // Config / secrets PUT/POST/DELETE here too (PR μ).
1373
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
+ }
1374
1421
  if (method === 'POST' && pathname === '/v1/plugins/install') {
1375
1422
  if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1376
1423
  return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
@@ -8,6 +8,7 @@ import {
8
8
  resolveStatePersonaDir,
9
9
  } from '../../persona/global-registry.js';
10
10
  import { clonePreset, editPersona, loadPresets } from '../../persona/presets.js';
11
+ import { resolvePersonaAddress } from '../../intake-plane/persona-answer.js';
11
12
  import type { PersonaRegistryEvent } from '../../persona/registry.js';
12
13
  import type { PersonaProfile } from '../../persona/types.js';
13
14
  import {
@@ -99,7 +100,7 @@ export async function handlePersonaGet(
99
100
  }
100
101
  await awaitGlobalPersonaLoad();
101
102
  const registry = getGlobalPersonaRegistry();
102
- const persona = registry.get(personaId);
103
+ const persona = resolvePersonaAddress(personaId, registry);
103
104
  if (!persona) return notFound(`persona ${personaId} not found`);
104
105
  return jsonResponse({ persona: toWire(persona) }, 200);
105
106
  }
@@ -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>;
@@ -138,6 +138,22 @@ export function validatePersonaShape(
138
138
  models = mr.models;
139
139
  }
140
140
 
141
+ let seat: string | undefined;
142
+ if (Object.hasOwn(obj, 'seat')) {
143
+ if (typeof obj['seat'] !== 'string' || !obj['seat'].trim()) {
144
+ return err(path, 'seat must be a non-empty string');
145
+ }
146
+ seat = obj['seat'];
147
+ }
148
+
149
+ let todo: string | undefined;
150
+ if (Object.hasOwn(obj, 'todo')) {
151
+ if (typeof obj['todo'] !== 'string' || !/^[a-z0-9_][a-z0-9_-]*\.todo\.jsonl$/i.test(obj['todo'])) {
152
+ return err(path, 'todo must be a per-persona .todo.jsonl filename');
153
+ }
154
+ todo = obj['todo'];
155
+ }
156
+
141
157
  let mentionPatterns: readonly string[] | undefined;
142
158
  if (obj['mentionPatterns'] !== undefined) {
143
159
  if (!Array.isArray(obj['mentionPatterns'])
@@ -149,6 +165,8 @@ export function validatePersonaShape(
149
165
 
150
166
  const profile: PersonaProfile = {
151
167
  personaId, displayName,
168
+ ...(seat !== undefined ? { seat } : {}),
169
+ ...(todo !== undefined ? { todo } : {}),
152
170
  ...(description.value !== undefined ? { description: description.value } : {}),
153
171
  ...(systemPrompt.value !== undefined ? { systemPrompt: systemPrompt.value } : {}),
154
172
  ...(brand !== undefined ? { brand } : {}),
@@ -6,6 +6,8 @@ export interface PersonaTodo {
6
6
  title: string;
7
7
  status: 'open' | 'done';
8
8
  createdAt: string;
9
+ priority?: number;
10
+ dueAt?: string;
9
11
  }
10
12
 
11
13
  export interface PersonaTodoReadResult {
@@ -28,7 +30,9 @@ function isPersonaTodo(value: unknown): value is PersonaTodo {
28
30
  && typeof item.title === 'string' && item.title.length > 0
29
31
  && (item.status === 'open' || item.status === 'done')
30
32
  && typeof item.createdAt === 'string'
31
- && Number.isFinite(Date.parse(item.createdAt));
33
+ && Number.isFinite(Date.parse(item.createdAt))
34
+ && (item.priority === undefined || (typeof item.priority === 'number' && Number.isInteger(item.priority) && item.priority >= 0))
35
+ && (item.dueAt === undefined || (typeof item.dueAt === 'string' && Number.isFinite(Date.parse(item.dueAt))));
32
36
  }
33
37
 
34
38
  /** Missing files are empty; malformed lines are skipped and returned for observation. */
@@ -67,13 +71,14 @@ export function writePersonaTodos(dir: string, personaId: string, todos: readonl
67
71
  writeFileSync(path, todos.map(todo => JSON.stringify(todo)).join('\n') + (todos.length ? '\n' : ''), 'utf8');
68
72
  }
69
73
 
70
- /** Earliest created open item; original order breaks equal-timestamp ties. */
74
+ /** Higher priority first, then earlier deadline, then earlier creation; file order breaks ties. */
75
+ export function orderedPersonaTodos(todos: readonly PersonaTodo[]): PersonaTodo[] {
76
+ return [...todos].filter((todo) => todo.status === 'open').sort((a, b) =>
77
+ (b.priority ?? 0) - (a.priority ?? 0)
78
+ || (a.dueAt && b.dueAt ? Date.parse(a.dueAt) - Date.parse(b.dueAt) : a.dueAt ? -1 : b.dueAt ? 1 : 0)
79
+ || Date.parse(a.createdAt) - Date.parse(b.createdAt));
80
+ }
81
+
71
82
  export function nextPersonaTodo(todos: readonly PersonaTodo[]): PersonaTodo | null {
72
- let oldest: PersonaTodo | null = null;
73
- for (const todo of todos) {
74
- if (todo.status === 'open' && (!oldest || Date.parse(todo.createdAt) < Date.parse(oldest.createdAt))) {
75
- oldest = todo;
76
- }
77
- }
78
- return oldest;
83
+ return orderedPersonaTodos(todos)[0] ?? null;
79
84
  }
@@ -88,6 +88,7 @@ export function presetToProfileYaml(p: PersonaPreset, common: PresetCommon, opts
88
88
  displayName: opts.name?.trim() ? `${opts.name.trim()} · ${p.role}` : p.displayName,
89
89
  description: p.oneLine,
90
90
  systemPrompt: presetSystemPrompt(p, common, name),
91
+ todo: `${opts.personaId}.todo.jsonl`,
91
92
  preset: { id: p.personaId, role: p.role, ...(title ? { title } : {}), tools: p.tools.map((t) => t.name), firstQuestions: p.firstQuestions },
92
93
  };
93
94
  return `# 프리셋 «${p.personaId}» 에서 만든 페르소나 — elanous persona add (원본: persona-presets/${p.personaId}.yaml)\n${stringify(profile, { lineWidth: 0 })}`;
@@ -99,7 +100,7 @@ export interface PersonaWriteResult { personaId: string; displayName: string; pa
99
100
  // Only fields in the runtime persona schema may be edited; provenance (`preset`) is retained, not editable.
100
101
  const PROFILE_KEYS = new Set([
101
102
  'personaId', 'displayName', 'description', 'systemPrompt', 'actionHosts', 'offsiteNavigation',
102
- 'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities',
103
+ 'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities', 'todo',
103
104
  ]);
104
105
  const NESTED_KEYS: Record<string, readonly string[]> = {
105
106
  models: ['primary', 'fallback', 'providers'], 'models.providers': ['ollama', 'openai'],