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
@@ -0,0 +1,125 @@
1
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { debug } from '../../debug/log.js';
5
+ import { loopAgentContractErrors } from '../contract/validate.js';
6
+
7
+ const contractSchema = JSON.parse(readFileSync(resolve(import.meta.dir, '../contract/loop-agent.schema.json'), 'utf8')) as { $id: string };
8
+ const allowedOverlayFields = new Set(['neighbors', 'cadence', 'grounding', 'budget']);
9
+ type RecordValue = Record<string, unknown>;
10
+
11
+ function record(value: unknown): value is RecordValue {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function schemaMajor(value: string): number | null {
16
+ const match = /^(?:https:\/\/elanous\.ai\/schemas\/loop-agent\/)?(\d+)\.\d+\.\d+$/.exec(value);
17
+ return match ? Number(match[1]) : null;
18
+ }
19
+
20
+ /** The package version is independent of the declared contract schema version. */
21
+ export function contractMajor(manifest: RecordValue): { ok: boolean; error?: string } {
22
+ const declared = manifest.schema ?? manifest.$schema ?? contractSchema.$id;
23
+ const expected = schemaMajor(contractSchema.$id);
24
+ const actual = typeof declared === 'string' ? schemaMajor(declared) : null;
25
+ const other = manifest.schema !== undefined && manifest.$schema !== undefined ? manifest.$schema : undefined;
26
+ if (actual !== null && expected !== null && actual === expected &&
27
+ (other === undefined || typeof other === 'string' && schemaMajor(other) === expected)) return { ok: true };
28
+ return { ok: false, error: `contract major mismatch: declared ${String(declared)}${other === undefined ? '' : ` / ${String(other)}`}, expected ${contractSchema.$id}` };
29
+ }
30
+
31
+ /** Replace only allowlisted top-level fields; refused fields never enter the merged manifest. */
32
+ export function mergeOverlay(manifest: RecordValue, overlay: unknown): { merged: RecordValue; refused: string[] } {
33
+ const merged = { ...manifest };
34
+ const refused: string[] = [];
35
+ if (!record(overlay)) return { merged, refused: ['overlay'] };
36
+ for (const [field, value] of Object.entries(overlay)) {
37
+ if (!allowedOverlayFields.has(field)) refused.push(field);
38
+ else merged[field] = value;
39
+ }
40
+ return { merged, refused };
41
+ }
42
+
43
+ function inside(root: string, path: string): boolean {
44
+ const part = relative(root, path);
45
+ return part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part);
46
+ }
47
+
48
+ function readReference(root: string, name: string, path: unknown, errors: string[]): void {
49
+ if (typeof path !== 'string' || !path || isAbsolute(path) || /^[a-zA-Z]:[\\/]/.test(path) || path.split(/[\\/]/).includes('..')) {
50
+ errors.push(`${name} path escape: ${String(path)}`);
51
+ return;
52
+ }
53
+ const candidate = resolve(root, path);
54
+ if (!inside(root, candidate)) {
55
+ errors.push(`${name} path escape: ${path}`);
56
+ return;
57
+ }
58
+ try {
59
+ const actual = realpathSync(candidate);
60
+ if (!inside(root, actual)) errors.push(`${name} path escape: ${path}`);
61
+ else if (!statSync(actual).isFile()) errors.push(`${name} file missing: ${path}`);
62
+ } catch {
63
+ errors.push(`${name} file missing: ${path}`);
64
+ }
65
+ }
66
+
67
+ export function readLoopPackage(dir: string): { manifest: RecordValue | null; overlay?: unknown; errors: string[] } {
68
+ const errors: string[] = [];
69
+ let manifest: RecordValue | null = null;
70
+ let overlay: unknown;
71
+ const root = realpathSync(dir);
72
+ if (!statSync(root).isDirectory()) throw new Error(`${dir} is not a directory`);
73
+ const readYaml = (name: string): unknown => {
74
+ const path = resolve(root, name);
75
+ const actual = realpathSync(path);
76
+ if (!inside(root, actual) || !statSync(actual).isFile()) throw new Error(`${name} path escape or not a file`);
77
+ return parseYaml(readFileSync(actual, 'utf8'));
78
+ };
79
+ try {
80
+ const parsed = readYaml('loop.yaml');
81
+ if (!record(parsed)) errors.push('loop.yaml must contain a manifest object');
82
+ else manifest = parsed;
83
+ } catch (error) { errors.push(`loop.yaml: ${error instanceof Error ? error.message : String(error)}`); }
84
+ try { overlay = readYaml('overlay.yaml'); }
85
+ catch (error) {
86
+ if (!(record(error) && error.code === 'ENOENT')) errors.push(`overlay.yaml: ${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ if (manifest) {
89
+ readReference(root, 'graph', manifest.graph, errors);
90
+ readReference(root, 'role', manifest.role, errors);
91
+ }
92
+ return { manifest, overlay, errors };
93
+ }
94
+
95
+ export function verifyLoopPackage(dir: string): {
96
+ ok: boolean; errors: string[]; refusedOverlayFields: string[]; merged: RecordValue | null;
97
+ } {
98
+ let manifest: RecordValue | null = null;
99
+ let overlay: unknown;
100
+ const errors: string[] = [];
101
+ try {
102
+ const read = readLoopPackage(dir);
103
+ manifest = read.manifest;
104
+ overlay = read.overlay;
105
+ errors.push(...read.errors);
106
+ } catch (error) { errors.push(`package directory: ${error instanceof Error ? error.message : String(error)}`); }
107
+ let merged: RecordValue | null = null;
108
+ let refusedOverlayFields: string[] = [];
109
+ if (manifest) {
110
+ const major = contractMajor(manifest);
111
+ if (major.error) errors.push(major.error);
112
+ const result = overlay === undefined ? { merged: { ...manifest }, refused: [] } : mergeOverlay(manifest, overlay);
113
+ refusedOverlayFields = result.refused;
114
+ merged = result.merged;
115
+ if (refusedOverlayFields.length) errors.push(`overlay refused: ${refusedOverlayFields.join(', ')}`);
116
+ // Schema declaration is package metadata, not a field in the loop-agent contract.
117
+ const { schema: _schema, $schema: _dollarSchema, ...contractInput } = merged;
118
+ errors.push(...loopAgentContractErrors(contractInput));
119
+ }
120
+ const ok = errors.length === 0;
121
+ debug.log('loop.package', ok ? 'verified' : 'refused', {
122
+ id: manifest?.id, version: manifest?.version, errors: errors.length, refusedOverlayFields,
123
+ });
124
+ return { ok, errors, refusedOverlayFields, merged };
125
+ }
@@ -0,0 +1,22 @@
1
+ export type LoopCronVerdict = 'alive' | 'late' | 'failed' | 'off';
2
+
3
+ /** Shared display inputs for a graph loop or a cron job. The caller supplies
4
+ * the cadence in milliseconds (or null when it is not known). */
5
+ export interface LoopCronVerdictInput {
6
+ enabled: boolean;
7
+ lastRunAt: string | null;
8
+ lastStatus: string | null;
9
+ intervalMs: number | null;
10
+ }
11
+
12
+ /** Pure health classification; a failed last execution takes precedence over age.
13
+ * With no valid timestamp or positive cadence, lateness cannot be established. */
14
+ export function loopCronVerdict(entry: LoopCronVerdictInput, nowMs: number): LoopCronVerdict {
15
+ if (!entry.enabled) return 'off';
16
+ if (entry.lastStatus === 'failed' || entry.lastStatus === 'error' || entry.lastStatus === 'failure') return 'failed';
17
+ const lastRunMs = entry.lastRunAt === null ? NaN : Date.parse(entry.lastRunAt);
18
+ if (Number.isFinite(nowMs) && Number.isFinite(lastRunMs) &&
19
+ entry.intervalMs !== null && Number.isFinite(entry.intervalMs) && entry.intervalMs > 0 &&
20
+ nowMs - lastRunMs > 2 * entry.intervalMs) return 'late';
21
+ return 'alive';
22
+ }
@@ -57,11 +57,11 @@ export const FEATURE_MATURITY = {
57
57
  pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta',
58
58
  where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
59
59
  pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
60
- research: 'beta', release: 'beta', seat: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
60
+ research: 'beta', release: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
61
61
  control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
62
62
  grounding: 'beta', graph: 'beta', loop: 'stable', card: 'stable', 'launch-head': 'beta',
63
63
  a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
64
- persona: 'beta', 'team-set': 'beta', market: 'beta', directive: 'stable', connector: 'beta', storyboard: 'beta',
64
+ persona: 'beta', team: 'beta', 'team-set': 'beta', market: 'beta', directive: 'stable', connector: 'beta', storyboard: 'beta',
65
65
  python: 'beta', 'self-update': 'stable', 'measure-fabric-arc-ab': 'beta', mcp: 'beta',
66
66
  relay: 'beta', 'telegram-test': 'beta', telegram: 'beta', 'discord-test': 'beta',
67
67
  sync: 'beta', status: 'beta', git: 'beta', gh: 'beta', repro: 'system',
@@ -71,7 +71,7 @@ export const FEATURE_MATURITY = {
71
71
  provider: 'stable', 'provider:set': 'beta', 'provider:restore': 'beta',
72
72
  'provider:rotate': 'beta', 'provider:use': 'beta', 'status-bar': 'beta',
73
73
  schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
74
- decisions: 'beta', directives: 'beta', fleet: 'beta', ops: 'stable', publish: 'beta',
74
+ decisions: 'beta', directives: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
75
75
  env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
76
76
  config: 'stable', tasks: 'stable', labels: 'beta', msg: 'beta', ask: 'stable',
77
77
  onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
@@ -89,6 +89,7 @@ export const FEATURE_MATURITY = {
89
89
  clear: 'stable',
90
90
  status: 'stable',
91
91
  now: 'beta', // CTX2 «지금 무엇이 돌고 있나» (#23379)
92
+ wish: 'beta',
92
93
  remaining: 'stable',
93
94
  setup: 'stable',
94
95
  quit: 'stable',
@@ -152,7 +153,7 @@ export const FEATURE_MATURITY = {
152
153
  ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
153
154
  decisions: 'stable', work: 'stable', cancel: 'stable',
154
155
  skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
155
- taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', project: 'beta',
156
+ taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', project: 'beta', wish: 'beta',
156
157
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
157
158
  harness: 'ops',
158
159
  // Existing commands outside the CXO four-grade menu list are owner-only.
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
+ }