elanous 0.2.13 → 0.2.14

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 (445) 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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f14987d5850a31e0.js → page-1abfe3aec3be6bdc.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-15374909d8f1aebd.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-66c27608f651b866.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-78b40a344ed22f24.js → page-e1e47d8d940aba3d.js} +1 -1
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/graphs/orchestrator/orchestrator.yaml +59 -0
  194. package/graphs/orchestrator/recipes.yaml +18 -0
  195. package/graphs/release/recipes.yaml +3 -0
  196. package/graphs/release/release-loop.yaml +4 -1
  197. package/graphs/rhythm/daily.yaml +38 -0
  198. package/graphs/rhythm/recipes.yaml +18 -0
  199. package/graphs/rhythm/weekly.yaml +39 -0
  200. package/graphs/video/recipes.yaml +3 -0
  201. package/graphs/video/youtube-summary.yaml +17 -0
  202. package/package.json +1 -1
  203. package/scripts/audit-test-state-writes.ts +72 -17
  204. package/scripts/brand/check.ts +186 -0
  205. package/scripts/coord-post.sh +11 -1
  206. package/scripts/docs-cli-check.ts +23 -6
  207. package/scripts/expose-rubric-check.ts +118 -0
  208. package/scripts/expose-rubric-suggest.ts +163 -0
  209. package/scripts/heal/run-role.ts +14 -0
  210. package/scripts/hq/standby-verify.ts +11 -7
  211. package/scripts/lessons/handbook-page.ts +85 -0
  212. package/scripts/release-loop/gate-node.ts +9 -0
  213. package/scripts/release-loop/publish-node.ts +14 -1
  214. package/scripts/release-loop/unattended-release.ts +23 -1
  215. package/scripts/release-loop/vault-note-node.ts +196 -0
  216. package/scripts/release-story/draft.ts +172 -0
  217. package/scripts/rhythm/daily.ts +383 -0
  218. package/scripts/rhythm/weekly.ts +294 -0
  219. package/scripts/skills/public-check.ts +169 -0
  220. package/scripts/test-deterministic.ts +4 -8
  221. package/scripts/test-diet/effectiveness.ts +195 -0
  222. package/scripts/test-diet/lib.ts +16 -8
  223. package/scripts/test-diet/node.ts +77 -9
  224. package/scripts/ux/demo-rehearsal.ts +2 -2
  225. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  226. package/scripts/video/youtube-summary.ts +133 -0
  227. package/skills/diagram-master/SKILL.md +42 -49
  228. package/skills/diagram-master/references/pyproject.toml +4 -1
  229. package/skills/diagram-master/references/routing-guide.md +4 -2
  230. package/skills/grill-me/SKILL.md +1 -0
  231. package/skills/youtube-master/SKILL.md +13 -13
  232. package/skills/youtube-master/package-lock.json +0 -545
  233. package/skills/youtube-master/package.json +2 -5
  234. package/skills/youtube-master/scripts/main.ts +2 -2
  235. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  236. package/skills/youtube-master/src/util.ts +30 -7
  237. package/src/agent-mission/driver.ts +3 -3
  238. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  239. package/src/autopilot/build/merge-intent.ts +53 -0
  240. package/src/chat/index.ts +2 -0
  241. package/src/claims/claims-ledger.ts +232 -0
  242. package/src/claims/claims-render.ts +157 -0
  243. package/src/cli/claims-cli.ts +94 -0
  244. package/src/cli/decisions-cli.ts +35 -3
  245. package/src/cli/docs-cli.ts +2 -0
  246. package/src/cli/flow-cli.ts +11 -0
  247. package/src/cli/freeze-cli.ts +45 -0
  248. package/src/cli/lesson-cli.ts +108 -0
  249. package/src/cli/logs-cli.ts +104 -2
  250. package/src/cli/logs-model-input.ts +131 -0
  251. package/src/cli/pr-cli.ts +29 -1
  252. package/src/cli/private-ledger-commands.ts +54 -4
  253. package/src/cli/pty-drive-verdict.ts +11 -2
  254. package/src/cli/release-cli.ts +144 -21
  255. package/src/cli/rfc-status.ts +91 -0
  256. package/src/connectors/linear.ts +5 -3
  257. package/src/context-bus/condense.ts +15 -20
  258. package/src/context-bus/context-now-public.ts +86 -0
  259. package/src/context-bus/context-now-surfaces.ts +55 -2
  260. package/src/context-bus/context-now.ts +4 -3
  261. package/src/context-bus/long-term-memory.ts +92 -11
  262. package/src/dashboard/index.ts +75 -22
  263. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  264. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  265. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  266. package/src/decisions/decision-ledger.ts +40 -1
  267. package/src/discord/sprint21-runtime.ts +16 -0
  268. package/src/discord-self-message.ts +8 -0
  269. package/src/discord-slash-wire.ts +31 -2
  270. package/src/domains/outbound-alert.ts +1 -1
  271. package/src/elanous-config-dir.ts +29 -4
  272. package/src/execution-loop/launch-gate.ts +29 -14
  273. package/src/flow/cards.ts +239 -0
  274. package/src/graph-runner/graph-tick.ts +4 -0
  275. package/src/graph-runner/runner.ts +30 -5
  276. package/src/harness/harness-cli-command.ts +152 -27
  277. package/src/harness/harness-incidents.ts +43 -12
  278. package/src/harness/harness-pod-dispatch.ts +2 -0
  279. package/src/harness/harness-queue-child.ts +3 -3
  280. package/src/harness/harness-queue.ts +216 -28
  281. package/src/harness/helper-scan.ts +139 -0
  282. package/src/harness/model-input-observation.ts +37 -0
  283. package/src/hq/hq.ts +1 -1
  284. package/src/hq/promote.ts +301 -0
  285. package/src/index.ts +83 -21
  286. package/src/intake-plane/linear-wish.ts +39 -11
  287. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  288. package/src/intake-plane/wish-card.ts +53 -2
  289. package/src/intake-plane/wish-folder.ts +5 -2
  290. package/src/intake-plane/wish-reply.ts +72 -0
  291. package/src/lessons/lesson-import.ts +106 -0
  292. package/src/lessons/lesson-ledger.ts +206 -0
  293. package/src/loops/activity.ts +203 -0
  294. package/src/loops/budget.ts +32 -0
  295. package/src/loops/checker.ts +5 -3
  296. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  297. package/src/loops/loop-cli.ts +36 -4
  298. package/src/loops/neighbors.ts +40 -0
  299. package/src/loops/observe.ts +10 -0
  300. package/src/loops/orchestrator/author-depth.ts +164 -0
  301. package/src/loops/orchestrator/author-input-check.ts +77 -0
  302. package/src/loops/orchestrator/author-ledger.ts +230 -0
  303. package/src/loops/orchestrator/finish-rate.ts +175 -0
  304. package/src/loops/orchestrator/overlap.ts +51 -12
  305. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  306. package/src/loops/orchestrator/tick.ts +302 -0
  307. package/src/loops/orchestrator/traffic.ts +18 -3
  308. package/src/loops/registry.ts +454 -9
  309. package/src/loops/status-rows.ts +118 -0
  310. package/src/loops/verdict.ts +17 -5
  311. package/src/maturity/bot-slash-catalog.ts +66 -0
  312. package/src/maturity/feature-maturity.ts +7 -4
  313. package/src/nexus/api/dashboard.ts +6 -1
  314. package/src/nexus/api/http-server.ts +18 -2
  315. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  316. package/src/nexus/api/schedules-read.ts +5 -1
  317. package/src/nexus/api/task-cards-api.ts +44 -5
  318. package/src/nexus/api/workflows.ts +2 -1
  319. package/src/release-loop/auto-start.ts +48 -0
  320. package/src/release-loop/checklist.ts +13 -4
  321. package/src/release-loop/feature-store.ts +28 -16
  322. package/src/release-loop/landing-freeze.ts +121 -0
  323. package/src/release-loop/placement.ts +242 -0
  324. package/src/release-loop/release-schedule.ts +3 -1
  325. package/src/seat-dispatch/ceo-commands.ts +2 -2
  326. package/src/seat-dispatch/seat-ask.ts +16 -3
  327. package/src/seat-dispatch/seat-questions.ts +58 -0
  328. package/src/seat-loop/seat-loop.ts +406 -31
  329. package/src/self-dev/dev-pipeline.ts +6 -2
  330. package/src/self-dev/draft-sweep.ts +76 -0
  331. package/src/self-dev/run-store.ts +73 -1
  332. package/src/self-dev/run-supervisor.ts +8 -0
  333. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  334. package/src/self-implement/budget-gate.ts +29 -0
  335. package/src/self-implement/expose-gate.ts +47 -0
  336. package/src/self-implement/frozen-merges.ts +294 -0
  337. package/src/self-implement/gate-cli.ts +10 -4
  338. package/src/self-implement/gate-scope.ts +46 -9
  339. package/src/self-implement/goal-archive.ts +222 -0
  340. package/src/self-implement/goal-lookup.ts +147 -0
  341. package/src/self-implement/heal-intake.ts +79 -0
  342. package/src/self-implement/host-regate.ts +25 -2
  343. package/src/self-implement/orchestrator.ts +114 -10
  344. package/src/self-implement/revert-guard.ts +62 -0
  345. package/src/self-implement/run-ledger.ts +1 -1
  346. package/src/self-implement/seams.ts +28 -8
  347. package/src/session/index.ts +2 -0
  348. package/src/task-cards/card-cli.ts +10 -2
  349. package/src/task-cards/card-store.ts +10 -6
  350. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  351. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  352. package/src/task-orchestrator/surfaces/self-implement-pod.ts +80 -16
  353. package/src/telegram-commands.ts +27 -3
  354. package/src/telegram-loops-command.ts +44 -0
  355. package/src/user-config.ts +120 -16
  356. package/src/version/packed-revision.json +1 -1
  357. package/src/workflow-runtime/storage.ts +31 -13
  358. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  369. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  373. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  444. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  445. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,66 @@
1
+ import { awaitGlobalPersonaLoad, getGlobalPersonaRegistry } from '../../persona/global-registry.js';
2
+ import { resolvePersonaAddress } from '../../intake-plane/persona-answer.js';
3
+ import { loadSession, updateSessionMeta } from '../../session/index.js';
4
+ import { assemblePersonaPrompt } from '../../persona/prompt-assembler.js';
5
+ import type { LLMMessage } from '../../llm.js';
6
+
7
+ /** Unattached conversations use a dashboard-local id; attached sessions use persisted metadata. */
8
+ export class TuiChatPersonaSelection {
9
+ private readonly selected = new Map<string, string>();
10
+
11
+ get(sessionId: string): string | undefined {
12
+ return loadSession(sessionId)?.meta.chatPersonaId ?? this.selected.get(sessionId);
13
+ }
14
+
15
+ set(sessionId: string, personaId: string | undefined): void {
16
+ const meta = updateSessionMeta(sessionId, (m) => {
17
+ if (personaId) m.chatPersonaId = personaId;
18
+ else delete m.chatPersonaId;
19
+ });
20
+ if (!meta) {
21
+ if (personaId) this.selected.set(sessionId, personaId);
22
+ else this.selected.delete(sessionId);
23
+ } else {
24
+ this.selected.delete(sessionId);
25
+ }
26
+ }
27
+ }
28
+
29
+ export async function handleTuiChatPersona(
30
+ args: readonly string[], sessionId: string,
31
+ selection: TuiChatPersonaSelection,
32
+ ): Promise<string> {
33
+ if (args[0] === '-') {
34
+ if (args.length !== 1) return '쓰는 법: /persona [이름|-]';
35
+ selection.set(sessionId, undefined);
36
+ return '이 대화의 페르소나를 뺐습니다';
37
+ }
38
+ try {
39
+ await awaitGlobalPersonaLoad();
40
+ const registry = getGlobalPersonaRegistry();
41
+ if (!args.length) {
42
+ const selected = selection.get(sessionId);
43
+ const personas = registry.list().sort((a, b) => a.displayName.localeCompare(b.displayName));
44
+ const current = personas.find(p => p.personaId === selected);
45
+ return `이 대화의 페르소나: ${current ? `${current.displayName} (${current.personaId})` : '없음'}\n고를 수 있는 페르소나: ${personas.map(p => `${p.displayName} (${p.personaId})${p.personaId === selected ? ' (지금)' : ''}`).join(' · ') || '없음'}\n고르기: /persona <이름> · 빼기: /persona -`;
46
+ }
47
+ const name = args.join(' ');
48
+ const persona = resolvePersonaAddress(name, registry);
49
+ if (!persona) return `그런 페르소나가 없습니다: ${name}`;
50
+ selection.set(sessionId, persona.personaId);
51
+ return `${persona.displayName} 페르소나를 이 대화에 골랐습니다`;
52
+ } catch {
53
+ return '페르소나 목록을 읽지 못했습니다';
54
+ }
55
+ }
56
+
57
+ export function tuiChatPersonaMessages(
58
+ sessionId: string, selection: TuiChatPersonaSelection,
59
+ ): LLMMessage[] {
60
+ const id = selection.get(sessionId);
61
+ if (!id) return [];
62
+ const persona = getGlobalPersonaRegistry().get(id);
63
+ if (!persona) return [];
64
+ const prompt = assemblePersonaPrompt(persona, '').systemPrompt;
65
+ return prompt ? [{ role: 'system', content: prompt }] : [];
66
+ }
@@ -273,6 +273,7 @@
273
273
  // of circular dependency on dashboard/index.ts.
274
274
 
275
275
  import { tuiNowSlash } from '../../context-bus/context-now-surfaces.js';
276
+ import { handleTuiChatPersona, type TuiChatPersonaSelection } from './chat-persona.js';
276
277
  import { createWishCard } from '../../intake-plane/wish-card.js';
277
278
  import { CardStore } from '../../task-cards/card-store.js';
278
279
  import type { ContextNowDeps } from '../../context-bus/context-now.js';
@@ -390,6 +391,8 @@ import { resolveTerminalMoveDestination } from '../../terminal-matrix/mobility.j
390
391
  import { loadPersistedSessions } from '../../terminal/session-persistence.js';
391
392
  import { dispatchTerminalModalObserve } from '../../skills/tools/terminal-modal.js';
392
393
  import { SlashCommandRegistry } from './registry.js';
394
+ import { cronSecondDueAt, filterLoopRows, formatLoopsTable, localLoopRows, type LoopsTableRow } from './loops-table.js';
395
+ import { loopCronVerdict } from '../../loops/verdict.js';
393
396
  import { executeImmediateDashboardSlash } from '../input/slash-executor.js';
394
397
  import {
395
398
  buildContextSlashCommand,
@@ -1029,6 +1032,7 @@ export interface DashboardSlashContext {
1029
1032
  /** Currently-attached session id (TUI's pointer to a JSONL on disk). */
1030
1033
  getAttachedSessionId(): string | null;
1031
1034
  setAttachedSessionId(id: string | null): void;
1035
+ chatPersona?: { sessionId(): string; selection: TuiChatPersonaSelection; newConversation?(): void };
1032
1036
  /** Telegram chat id paired with the attached session, when handed off. */
1033
1037
  getAttachedChatId(): number | null;
1034
1038
  setAttachedChatId(id: number | null): void;
@@ -1038,6 +1042,9 @@ export interface DashboardSlashContext {
1038
1042
  send: (cfg: ReturnType<typeof getUserConfig>, text: string) => Promise<boolean>;
1039
1043
  };
1040
1044
 
1045
+ /** Test seam for the local read-only loop and cron registry. */
1046
+ loopsLocalRows?: (now: Date) => LoopsTableRow[];
1047
+
1041
1048
  sessionSlash: {
1042
1049
  /** opts.remote — when the dashboard is attached to a remote daemon. */
1043
1050
  remoteDaemon(): { url: string; token: string | undefined } | null;
@@ -1364,6 +1371,28 @@ function emitReplyToChat(ctx: Pick<DashboardSlashContext, 'pushChatLine'>, line:
1364
1371
  ctx.pushChatLine(line);
1365
1372
  }
1366
1373
 
1374
+ async function fetchLoopsDaemonJson<T>(url: string, headers: Record<string, string>): Promise<T> {
1375
+ const controller = new AbortController();
1376
+ let timeout: ReturnType<typeof setTimeout> | undefined;
1377
+ try {
1378
+ return await Promise.race([
1379
+ (async () => {
1380
+ const response = await fetch(url, { headers, signal: controller.signal });
1381
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1382
+ return await response.json() as T;
1383
+ })(),
1384
+ new Promise<never>((_, reject) => {
1385
+ timeout = setTimeout(() => {
1386
+ controller.abort();
1387
+ reject(new Error('request timed out'));
1388
+ }, 1_000);
1389
+ }),
1390
+ ]);
1391
+ } finally {
1392
+ if (timeout) clearTimeout(timeout);
1393
+ }
1394
+ }
1395
+
1367
1396
  export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashCommandRegistry<DashboardSlashContext, DashboardSlashReturn> {
1368
1397
  const registry = new SlashCommandRegistry<DashboardSlashContext, DashboardSlashReturn>();
1369
1398
 
@@ -1371,6 +1400,61 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
1371
1400
  for (const line of tuiNowSlash(args, nowDeps)) ctx.pushChatLine(line);
1372
1401
  });
1373
1402
 
1403
+ registry.register('loops', async (args, ctx) => {
1404
+ if (args.length > 1) {
1405
+ ctx.pushChatLine(ctx.warning(' usage: /loops [owner]'));
1406
+ return;
1407
+ }
1408
+ const now = new Date(Date.now());
1409
+ let rows: LoopsTableRow[] = [];
1410
+ const remote = ctx.sessionSlash.remoteDaemon();
1411
+ if (remote) {
1412
+ const base = deriveDaemonHttpBase(remote.url);
1413
+ if (base) {
1414
+ try {
1415
+ const headers: Record<string, string> = remote.token ? { authorization: `Bearer ${remote.token}` } : {};
1416
+ const body = await fetchLoopsDaemonJson<{ schedules?: Array<{ name: string; domain?: string | null; runVia?: string | null;
1417
+ source: string; cron?: string | null; state: string; lastRun?: { at: string; status: string | null; exit: number | null } | null;
1418
+ intervalMs?: number | null }> }>(`${base}/v1/schedules?includeOff=1`, headers);
1419
+ rows = (body.schedules ?? []).map(schedule => ({
1420
+ layer: 'daemon cron', name: schedule.name, owner: schedule.domain ?? '—', mode: schedule.runVia ?? schedule.source,
1421
+ lastRun: schedule.lastRun?.at ?? null,
1422
+ verdict: loopCronVerdict({ enabled: schedule.state !== 'off', lastRunAt: schedule.lastRun?.at ?? null,
1423
+ lastStatus: schedule.lastRun?.exit != null && schedule.lastRun.exit !== 0 ? 'failed' : schedule.lastRun?.status ?? null,
1424
+ intervalMs: schedule.intervalMs ?? null,
1425
+ secondDueAt: cronSecondDueAt(schedule.cron ?? null, schedule.lastRun?.at ?? null) }, now.getTime()),
1426
+ }));
1427
+ } catch (err) {
1428
+ ctx.pushChatLine(ctx.warning(` daemon cron unavailable: ${err instanceof Error ? err.message : String(err)}`));
1429
+ }
1430
+ try {
1431
+ const headers: Record<string, string> = remote.token ? { authorization: `Bearer ${remote.token}` } : {};
1432
+ const body = await fetchLoopsDaemonJson<{ loops?: { loops?: Array<{
1433
+ name: string; label: string; armed: boolean | null; category?: string;
1434
+ last: { at: string; status: string } | null;
1435
+ }> } | null }>(`${base}/v1/dashboard/loops`, headers);
1436
+ // dashboardLoops returns LoopStatus (no owner field). Its category is
1437
+ // exec/reflect, not an owner; keep ownership unknown rather than
1438
+ // attributing every daemon loop to a guessed domain.
1439
+ rows.push(...(body.loops?.loops ?? []).map(loop => ({
1440
+ layer: 'daemon loop', name: loop.label || loop.name, owner: '—', mode: loop.category ?? 'loop',
1441
+ lastRun: loop.last?.at ?? null,
1442
+ verdict: loopCronVerdict({ enabled: loop.armed !== false, lastRunAt: loop.last?.at ?? null,
1443
+ lastStatus: loop.last?.status ?? null, intervalMs: null }, now.getTime()),
1444
+ })));
1445
+ } catch (err) {
1446
+ ctx.pushChatLine(ctx.warning(` daemon loops unavailable: ${err instanceof Error ? err.message : String(err)}`));
1447
+ }
1448
+ }
1449
+ }
1450
+ try { rows.push(...(ctx.loopsLocalRows ?? localLoopRows)(now)); }
1451
+ catch (err) { ctx.pushChatLine(ctx.warning(` local loops unavailable: ${err instanceof Error ? err.message : String(err)}`)); }
1452
+ const filtered = filterLoopRows(rows, args[0]);
1453
+ for (const line of formatLoopsTable(filtered, text => ctx.error(text))) ctx.pushChatLine(` ${line}`);
1454
+ if (!filtered.length) ctx.pushChatLine(ctx.muted(' No loops or cron jobs found.'));
1455
+ ctx.setChatScrollOffset(-1);
1456
+ });
1457
+
1374
1458
  registry.register('wish', (args, ctx) => {
1375
1459
  const text = args.join(' ').trim();
1376
1460
  if (!text) {
@@ -1427,6 +1511,16 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
1427
1511
  }
1428
1512
  });
1429
1513
 
1514
+ registry.register('persona', async (args, ctx) => {
1515
+ if (!ctx.chatPersona) {
1516
+ ctx.pushChatLine(ctx.warning(' 페르소나를 사용할 수 없습니다'));
1517
+ } else {
1518
+ const text = await handleTuiChatPersona(args, ctx.chatPersona.sessionId(), ctx.chatPersona.selection);
1519
+ for (const line of text.split('\n')) ctx.pushChatLine(ctx.muted(` ${line}`));
1520
+ }
1521
+ ctx.setChatScrollOffset(-1);
1522
+ });
1523
+
1430
1524
  registry.register('directive', async (args, ctx) => {
1431
1525
  try {
1432
1526
  const { addDirective } = await import('../../steward/directive.js');
@@ -4042,6 +4136,7 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
4042
4136
  const prev = attached;
4043
4137
  ctx.setAttachedSessionId(null);
4044
4138
  ctx.setAttachedChatId(null);
4139
+ ctx.chatPersona?.newConversation?.();
4045
4140
  const chatHistory = ctx.compactSlash.chatHistory;
4046
4141
  const sysFromDash = chatHistory.find(m => m.role === 'system');
4047
4142
  chatHistory.length = 0;
@@ -4151,6 +4246,10 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
4151
4246
  sessionAppendMessage(meta.id, { role: m.role, content, ts: nowTs });
4152
4247
  }
4153
4248
  sessionAttachTelegram(meta.id, chatTarget);
4249
+ if (ctx.chatPersona) {
4250
+ const selected = ctx.chatPersona.selection.get(ctx.chatPersona.sessionId());
4251
+ if (selected) ctx.chatPersona.selection.set(meta.id, selected);
4252
+ }
4154
4253
  ctx.setAttachedSessionId(meta.id);
4155
4254
  ctx.setAttachedChatId(chatTarget);
4156
4255
  ctx.chatLines.push(ctx.success(` ✓ attached session ${meta.id.slice(0, 8)} → chat ${chatTarget} (${chatHistory.length} turns mirrored)`));
@@ -0,0 +1,77 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { listLoops, type LoopEntry } from '../../loops/registry.js';
4
+ import { loopCronVerdict, type LoopCronVerdict } from '../../loops/verdict.js';
5
+ import { listSchedules, schedulesDbPath, type ScheduleRow } from '../../domains/schedule-registry.js';
6
+ import { nextRuns } from '../../domains/schedule-state.js';
7
+
8
+ export interface LoopsTableRow {
9
+ layer: string;
10
+ name: string;
11
+ owner: string;
12
+ mode: string;
13
+ lastRun: string | null;
14
+ verdict: LoopCronVerdict;
15
+ }
16
+
17
+ /** Read the already-inventoried local registry without opening/migrating a writable store. */
18
+ export function localLoopRows(now: Date): LoopsTableRow[] {
19
+ let schedules: ScheduleRow[] = [];
20
+ if (existsSync(schedulesDbPath())) {
21
+ const db = new Database(schedulesDbPath(), { readonly: true });
22
+ try { schedules = listSchedules(db); } finally { db.close(); }
23
+ }
24
+ const loops = listLoops({ schedules, now });
25
+ const rows: LoopsTableRow[] = loops.map(loop => graphLoopRow(loop, schedules, now));
26
+ const graphJobIds = new Set(loops.flatMap(loop => loop.jobs.map(job => job.id)));
27
+ for (const schedule of schedules) {
28
+ if (graphJobIds.has(schedule.id)) continue;
29
+ rows.push(scheduleRow(schedule, 'local cron', now));
30
+ }
31
+ return rows;
32
+ }
33
+
34
+ export function cronSecondDueAt(cron: string | null, lastRun: string | null): string | null {
35
+ if (!cron || !lastRun) return null;
36
+ const last = new Date(lastRun);
37
+ if (!Number.isFinite(last.getTime())) return null;
38
+ return nextRuns(cron, last, 2)[1] ?? null;
39
+ }
40
+
41
+ export function graphLoopRow(loop: LoopEntry, schedules: ScheduleRow[], now: Date): LoopsTableRow {
42
+ const active = loop.jobs.map(job => schedules.find(row => row.id === job.id)).filter((r): r is ScheduleRow => !!r);
43
+ const cron = loop.trigger.cron ?? active[0]?.cron ?? null;
44
+ const interval = active.find(r => r.interval_ms && r.interval_ms > 0)?.interval_ms ?? null;
45
+ const lastRun = loop.lastRun?.at ?? active.map(r => r.last_run).filter((at): at is string => !!at).sort().at(-1) ?? null;
46
+ const status = loop.lastRun?.status ?? active.find(r => r.last_run === lastRun)?.last_status ?? null;
47
+ return {
48
+ layer: 'local loop', name: loop.title, owner: loop.owner ?? '—', mode: loop.mode ?? (loop.trigger.cron ? 'cron' : 'event'), lastRun,
49
+ verdict: loopCronVerdict({ enabled: loop.enabled, lastRunAt: lastRun, lastStatus: status, intervalMs: interval,
50
+ secondDueAt: cronSecondDueAt(cron, lastRun) }, now.getTime()),
51
+ };
52
+ }
53
+
54
+ export function scheduleRow(row: ScheduleRow, layer: string, now: Date): LoopsTableRow {
55
+ return {
56
+ layer, name: row.name, owner: row.domain ?? '—', mode: row.run_via || row.source,
57
+ lastRun: row.last_run,
58
+ verdict: loopCronVerdict({ enabled: !!row.enabled && row.disabled_reason !== 'vanished', lastRunAt: row.last_run,
59
+ lastStatus: row.last_exit !== null && row.last_exit !== undefined && row.last_exit !== 0 ? 'failed' : row.last_status ?? null,
60
+ intervalMs: row.interval_ms, secondDueAt: cronSecondDueAt(row.cron, row.last_run) }, now.getTime()),
61
+ };
62
+ }
63
+
64
+ export function filterLoopRows(rows: LoopsTableRow[], owner?: string): LoopsTableRow[] {
65
+ return owner ? rows.filter(row => row.owner !== '—' && row.owner.toLowerCase() === owner.toLowerCase()) : rows;
66
+ }
67
+
68
+ export function formatLoopsTable(rows: LoopsTableRow[], verdictColor: (text: string) => string): string[] {
69
+ const columns = ['LAYER', 'NAME', 'OWNER', 'MODE', 'LAST RUN', 'VERDICT'];
70
+ const data = rows.map(r => [r.layer, r.name, r.owner, r.mode, r.lastRun ?? '—', r.verdict]);
71
+ const widths = columns.map((col, i) => Math.max(col.length, ...data.map(cells => cells[i]!.length)));
72
+ const format = (cells: string[]) => cells.map((cell, i) => cell.padEnd(widths[i]!)).join(' ');
73
+ return [format(columns), ...rows.map((row, i) => {
74
+ const cells = data[i]!;
75
+ return `${format(cells.slice(0, 5))} ${row.verdict === 'late' || row.verdict === 'failed' ? verdictColor(cells[5]!) : cells[5]}`;
76
+ })];
77
+ }
@@ -31,7 +31,15 @@ export interface DecisionEntry {
31
31
  history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
32
32
  }
33
33
  export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume' | 'pendingQuestion' | 'crossCheck' | 'crossCheckSkipped' | 'alternative' | 'dissent'> & { raisedAt?: string };
34
- type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
34
+ export type Seat = 'OP' | 'MK' | 'TC' | 'UX';
35
+ export interface SeatDecisionRecord {
36
+ id: string; title: string; decision: string; seat: Seat; delegation: string;
37
+ reporting: 'posthoc'; recordedAt: string;
38
+ /** Present only when the actual decision time is known. */
39
+ decidedAt?: string; versionAtDecision?: Versions; refs?: string[];
40
+ }
41
+ export type SeatDecisionInput = Pick<SeatDecisionRecord, 'title' | 'decision' | 'seat' | 'delegation' | 'refs'> & { decidedAt?: string };
42
+ type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'seat-recorded'; entry: SeatDecisionRecord } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
35
43
  export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions; writeAnswer?: typeof writePendingQuestionAnswer }
36
44
 
37
45
  /** null = the decision resumes no run; otherwise whether the answer file was written (or already matched). */
@@ -196,6 +204,7 @@ export class DecisionLedger {
196
204
  if (filters.category && !CATEGORIES.includes(filters.category)) throw new Error('invalid category');
197
205
  const map = new Map<string, DecisionEntry>();
198
206
  for (const event of this.events()) {
207
+ if (event.type === 'seat-recorded') continue;
199
208
  if (event.type === 'raised') {
200
209
  if (map.has(event.entry.id)) throw new Error(`duplicate decision: ${event.entry.id}`);
201
210
  map.set(event.entry.id, event.entry);
@@ -221,6 +230,36 @@ export class DecisionLedger {
221
230
  && (!filters.decidedBy || e.decidedBy?.kind === filters.decidedBy)
222
231
  && (!filters.category || e.category === filters.category)).sort((a, b) => (b.raisedAt ?? b.importedAt ?? '').localeCompare(a.raisedAt ?? a.importedAt ?? '') || a.id.localeCompare(b.id));
223
232
  }
233
+ /** Delegated decisions are already made: they share the append-only ledger, never the owner's open-card state machine. */
234
+ recordSeatDecision(input: SeatDecisionInput): SeatDecisionRecord {
235
+ const seat = input.seat;
236
+ if (!['OP', 'MK', 'TC', 'UX'].includes(seat)) throw new Error('invalid seat');
237
+ const title = safe(single(input.title, 'title'));
238
+ const decision = safe(single(input.decision, 'decision'));
239
+ const delegation = safe(single(input.delegation, 'delegation'));
240
+ const refs = input.refs?.map(ref => safe(single(ref, 'ref')));
241
+ const decidedAt = input.decidedAt === undefined ? undefined : utc(input.decidedAt);
242
+ return this.locked(() => {
243
+ const existing = this.seatReport();
244
+ if (refs?.some(ref => existing.some(entry => entry.seat === seat && entry.title === title && entry.decision === decision && entry.refs?.includes(ref)))) throw new Error('seat decision source already recorded');
245
+ const recordedAt = utc(this.now().toISOString());
246
+ const prefix = `SD-${recordedAt.slice(0, 10).replaceAll('-', '')}-`;
247
+ const next = existing.reduce((n, entry) => entry.id.startsWith(prefix) ? Math.max(n, Number(entry.id.slice(prefix.length)) + 1) : n, 1);
248
+ const entry: SeatDecisionRecord = { id: `${prefix}${String(next).padStart(2, '0')}`, title, decision, seat, delegation,
249
+ reporting: 'posthoc', recordedAt, ...(decidedAt ? { decidedAt, versionAtDecision: this.version(decidedAt) } : {}), ...(refs?.length ? { refs } : {}) };
250
+ this.append({ type: 'seat-recorded', entry });
251
+ debug.log('decisions', 'seat-recorded', { id: entry.id, seat });
252
+ return entry;
253
+ });
254
+ }
255
+ seatReport(filters: { since?: string; seat?: Seat } = {}): SeatDecisionRecord[] {
256
+ const since = filters.since === undefined ? undefined : utc(filters.since);
257
+ if (filters.seat && !['OP', 'MK', 'TC', 'UX'].includes(filters.seat)) throw new Error('invalid seat');
258
+ return this.events().filter((event): event is Extract<Event, { type: 'seat-recorded' }> => event.type === 'seat-recorded')
259
+ .map(event => event.entry)
260
+ .filter(entry => (!since || (entry.recordedAt ?? entry.decidedAt ?? '') >= since) && (!filters.seat || entry.seat === filters.seat))
261
+ .sort((a, b) => (b.recordedAt ?? b.decidedAt ?? '').localeCompare(a.recordedAt ?? a.decidedAt ?? '') || a.id.localeCompare(b.id));
262
+ }
224
263
  show(id: string): DecisionEntry { const entry = this.list({ status: 'all' }).find(e => e.id === id); if (!entry) throw new Error(`decision not found: ${id}`); return entry; }
225
264
  raise(input: RaiseInput): DecisionEntry {
226
265
  const clean = validate(input);
@@ -22,6 +22,9 @@
22
22
 
23
23
  import { debug } from '../debug/log.js';
24
24
  import { botAudienceFor, gateDiscordSchemas } from '../maturity/bot-command-maturity.js';
25
+ import { buildBotSlashCatalog } from '../maturity/bot-slash-catalog.js';
26
+ import { FEATURE_MATURITY } from '../maturity/feature-maturity.js';
27
+ import { SLASH_COMMANDS } from '../chat/index.js';
25
28
  import type { DiscordBot } from '../discord.js';
26
29
  import {
27
30
  loadLayeredPersonaDirs,
@@ -195,6 +198,19 @@ export async function wireSprint21Runtime(opts: WireSprint21Opts): Promise<Sprin
195
198
  router.bind(statusCommand);
196
199
  router.bind(pollCommand);
197
200
  for (const command of botCommands) router.bind(command);
201
+ const catalog = buildBotSlashCatalog({
202
+ surface: 'discord', coreCommands: SLASH_COMMANDS, maturity: FEATURE_MATURITY,
203
+ handledCommands: router.schemas(),
204
+ });
205
+ for (const entry of catalog.commands) {
206
+ if (entry.supported) continue;
207
+ const { name, description } = entry;
208
+ router.bind({ schema: { name, description }, handler: async () => ({
209
+ type: RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
210
+ content: catalog.unsupportedReply(entry.name)!,
211
+ ephemeral: true,
212
+ }) });
213
+ }
198
214
 
199
215
  // 5. Wire bot callbacks — INTERACTION_CREATE + reactions
200
216
  // (Bot must be created with onInteraction/onReaction in opts;
@@ -64,6 +64,8 @@ import { getUserConfig } from './user-config.js';
64
64
  import { detectCard, runCardFollowup, NotACardError } from './card-followup/core.js';
65
65
  import { localOcrText } from './telegram-card-followup.js';
66
66
  import type { runGraph } from './graph-runner/runner.js';
67
+ import { readSlashContextNow, renderTelegramNow } from './context-bus/context-now-surfaces.js';
68
+ import type { ContextNowDeps } from './context-bus/context-now.js';
67
69
 
68
70
  const SLASH_FOCUS_TURNS_DEFAULT = 8;
69
71
 
@@ -139,6 +141,7 @@ export interface DiscordSelfMessageDeps {
139
141
  questionChannelFor?: (channelId: string) => import('./hitl/question.js').QuestionChannel;
140
142
  log?: (msg: string) => void;
141
143
  cardFollowupDeps?: { ocrText?: (path: string) => Promise<string | null>; runGraph?: typeof runGraph; rootDir?: () => string };
144
+ nowDeps?: ContextNowDeps;
142
145
  }
143
146
 
144
147
  /** Compose the full discord self+interweave onMessage handler. */
@@ -356,6 +359,11 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
356
359
  // Voice text commands pass the guild gate for the voice adapter's
357
360
  // sake — never route them into the LLM turn (legacy behavior).
358
361
  if (/^\/voice-(join|leave|status)\b/.test(ctx.text.trim())) return undefined;
362
+ const nowCommand = /^[/!]now(?:\s+([\s\S]*))?$/.exec(ctx.text.trim());
363
+ if (nowCommand) {
364
+ const args = nowCommand[1]?.trim().split(/\s+/).filter(Boolean) ?? [];
365
+ return renderTelegramNow(readSlashContextNow(args, deps.nowDeps));
366
+ }
359
367
  if (!ctx.text.trim() && ctx.attachments.length === 1 && ctx.attachments[0]!.contentType?.startsWith('image/')) {
360
368
  const photo = ctx.attachments[0]!;
361
369
  if (detectCard({ width: photo.width, height: photo.height, ocrText: null }).decision !== 'skip') {
@@ -16,12 +16,15 @@
16
16
  // 우회해 도착하므로 여기서 allowlist를 직접 강제한다.
17
17
 
18
18
  import type { UserConfig } from './user-config.js';
19
+ import { SLASH_COMMANDS } from './chat/index.js';
19
20
  import type { DiscordBot, DcIncoming, DcMessageStreamer } from './discord.js';
20
21
  import { makeCommandRest } from './discord/slash-registry.js';
21
22
  import { normalizeInteractionPayload } from './discord/slash-router.js';
22
23
  import type { SlashCommandSchema } from './discord/slash-types.js';
23
24
  import { debug } from './debug/log.js';
24
25
  import { gateDiscordSchemas, readBotAudience } from './maturity/bot-command-maturity.js';
26
+ import { buildBotSlashCatalog } from './maturity/bot-slash-catalog.js';
27
+ import { FEATURE_MATURITY } from './maturity/feature-maturity.js';
25
28
  import { resolveChannelBotToken } from './channel-bot-token.js';
26
29
 
27
30
  const REST_BASE = 'https://discord.com/api/v10';
@@ -42,6 +45,9 @@ export const ELANOUS_SLASH_COMMANDS: readonly SlashCommandSchema[] = [
42
45
  { name: 'file', description: '첨부 (이미지/문서 — 백엔드가 봄)', type: 11, required: false },
43
46
  ] },
44
47
  { name: 'brain', description: '브레인 모드로 복귀 (위임 해제)' },
48
+ { name: 'now', description: '지금 판·칸·결정·자리·최근 맥락 보기', options: [
49
+ { name: 'topic', description: '좁혀 볼 주제 (선택)', type: 3, required: false },
50
+ ] },
45
51
  { name: 'sessions', description: '바인딩된 세션 목록' },
46
52
  { name: 'new', description: '새 세션으로 시작 (이전 대화 보존)' },
47
53
  { name: 'attach', description: '이 채널을 다른 세션에 연결', options: [
@@ -58,6 +64,15 @@ export const ELANOUS_SLASH_COMMANDS: readonly SlashCommandSchema[] = [
58
64
  { name: 'voice-status', description: '보이스 연결 상태' },
59
65
  ] as const;
60
66
 
67
+ /** The native wire keeps its original schemas (including options); only new catalog
68
+ * entries use the core registry's description and the unsupported response. */
69
+ export function discordWireCatalog() {
70
+ return buildBotSlashCatalog({
71
+ surface: 'discord', coreCommands: SLASH_COMMANDS, maturity: FEATURE_MATURITY,
72
+ handledCommands: ELANOUS_SLASH_COMMANDS,
73
+ });
74
+ }
75
+
61
76
  /** SlashInteraction → 기존 텍스트 명령 문장 합성. */
62
77
  export function synthesizeCommandText(
63
78
  commandName: string,
@@ -79,6 +94,10 @@ export function synthesizeCommandText(
79
94
  case 'voice-leave': return '!voice-leave';
80
95
  case 'voice-status': return '!voice-status';
81
96
  case 'sessions': return '!sessions';
97
+ case 'now': {
98
+ const topic = options.get('topic');
99
+ return typeof topic === 'string' && topic.trim() ? `!now ${topic.trim()}` : '!now';
100
+ }
82
101
  case 'new': return '!new';
83
102
  case 'attach': return `!attach ${String(options.get('prefix') ?? '').trim()}`;
84
103
  case 'brain': return '!brain';
@@ -123,8 +142,16 @@ export function buildDiscordSlashWire(deps: DiscordSlashWireDeps): DiscordSlashW
123
142
  const guilds = await (await fetchImpl(`${REST_BASE}/users/@me/guilds`, auth)).json() as Array<{ id: string }>;
124
143
  const rest = makeCommandRest({ token, ...(deps.__fetchImpl ? { fetchImpl: deps.__fetchImpl } : {}) });
125
144
  const audience = readBotAudience((deps.userConfig.raw?.discord as { commandAudience?: unknown } | undefined)?.commandAudience);
126
- const { schemas, hidden } = gateDiscordSchemas(ELANOUS_SLASH_COMMANDS, audience);
127
- debug.log('discord.command', 'menu-filtered', { total: ELANOUS_SLASH_COMMANDS.length, shown: schemas.length - hidden, ...audience });
145
+ const catalog = discordWireCatalog();
146
+ const handled = new Map(ELANOUS_SLASH_COMMANDS.map((schema) => [schema.name, schema]));
147
+ const allSchemas: SlashCommandSchema[] = catalog.commands.map((entry) => {
148
+ const original = handled.get(entry.name);
149
+ if (original) return original;
150
+ const { name, description } = entry;
151
+ return { name, description };
152
+ });
153
+ const { schemas, hidden } = gateDiscordSchemas(allSchemas, audience);
154
+ debug.log('discord.command', 'menu-filtered', { total: allSchemas.length, shown: schemas.length - hidden, ...audience });
128
155
  for (const g of Array.isArray(guilds) ? guilds : []) {
129
156
  const registered = await rest.bulkOverwriteGuild(app.id, g.id, schemas);
130
157
  log(`[slash] registered ${registered.length} commands in guild ${g.id}`);
@@ -180,6 +207,8 @@ export function buildDiscordSlashWire(deps: DiscordSlashWireDeps): DiscordSlashW
180
207
  await ack(it.id, it.token, `이 봇은 <#${deps.channelScope}> 채널 전용입니다.`);
181
208
  return;
182
209
  }
210
+ const unsupported = discordWireCatalog().unsupportedReply(it.commandName);
211
+ if (unsupported) { await ack(it.id, it.token, unsupported); return; }
183
212
  const text = synthesizeCommandText(it.commandName, it.options);
184
213
  const attachments = resolvedAttachments(raw);
185
214
  await ack(it.id, it.token, `▶ \`${text}\`${attachments.length ? ` (+첨부 ${attachments.length})` : ''}`);
@@ -448,7 +448,7 @@ function reportUndeliverable(kind: string, daemonPath: DaemonPathClass | 'not-fo
448
448
  try { debug.log('outbound.send', 'undeliverable', { kind: safeObservationLabel(kind), daemonPath, universe, root }); } catch { /* fail-soft */ }
449
449
  const hint = universe === 'prod'
450
450
  ? '운영 데몬이 떠 있는지 확인: elanous nexus show'
451
- : '운영으로 보내려면 설치본 elanous 로 실행하거나 ELANOUS_STATE_DIR=~/.elanous ELANOUS_CONFIG_DIR=~/.elanous 를 준다';
451
+ : '운영으로 보내려면 설치본 elanous 로 실행하거나 ELANOUS_STATE_DIR=~/.elanous 와 --config-dir ~/.elanous 를 준다';
452
452
  try { console.error(`[outbound] ⛔ 못 보냄(${kind}) — 데몬 ${daemonPath} · 직접 발송도 실패(토큰 없음 또는 전송 실패) · 이 프로세스의 우주 ${universe} (${root}) · ${hint}`); } catch { /* fail-soft */ }
453
453
  }
454
454
 
@@ -15,18 +15,41 @@
15
15
  // Resolution order:
16
16
  // 1. Programmatic override set via `setElanousConfigDir(dir)` (incl.
17
17
  // from the `--config-dir` CLI flag).
18
- // 2. `~/.elanous` — default daily-driver root.
18
+ // 2. State-dir via `effectiveInstanceRoot()` (including isolated source trees).
19
+ // 3. `~/.elanous` for the installed daily-driver root.
19
20
 
20
21
  import { homedir } from 'node:os';
21
- import { join } from 'node:path';
22
+ import { resolve } from 'node:path';
22
23
 
23
24
  let override: string | undefined;
25
+ let checkedIgnoredEnv = false;
26
+
27
+ function normalizedDir(dir: string): string {
28
+ const trimmed = dir.trim();
29
+ const expanded = trimmed === '~' ? homedir() : trimmed.startsWith('~/') ? `${homedir()}${trimmed.slice(1)}` : trimmed;
30
+ return resolve(expanded);
31
+ }
32
+
33
+ function warnIfIgnoredEnv(resolved: string, source: 'flag' | 'state-dir' | 'default'): void {
34
+ if (checkedIgnoredEnv) return;
35
+ checkedIgnoredEnv = true;
36
+ const env = process.env.ELANOUS_CONFIG_DIR;
37
+ if (env === undefined || normalizedDir(env) === normalizedDir(resolved)) return;
38
+ console.error(`ELANOUS_CONFIG_DIR 는 읽지 않습니다 — 설정 폴더 = ${resolved} · 그 폴더를 쓰려면 --config-dir ${normalizedDir(env)}`);
39
+ try {
40
+ const { debug } = require('./debug/log.js') as typeof import('./debug/log.js');
41
+ debug.log('config.dir', 'env-ignored', { env, resolved, source });
42
+ } catch { /* logging must not change config resolution */ }
43
+ }
24
44
 
25
45
  /** Resolve the current elanous config / daemon root directory. Caller
26
46
  * decides what subpath to append (`workflows/`, `config.json`,
27
47
  * `acp-token`, ...). */
28
48
  export function getElanousConfigDir(): string {
29
- if (override !== undefined) return override;
49
+ if (override !== undefined) {
50
+ warnIfIgnoredEnv(override, 'flag');
51
+ return override;
52
+ }
30
53
  // ★ §4d (P3 · 2026-07-26) — **config-dir 은 state-dir 을 따라간다.**
31
54
  //
32
55
  // `ELANOUS_STATE_DIR` 은 env 라 전 자손에 자동 전파되는데 config-dir 은 argv 재부착 **7곳**
@@ -41,7 +64,9 @@ export function getElanousConfigDir(): string {
41
64
  // (`configDirFollowingStateDir` 은 §4d 계약을 문서화·테스트하는 순수 함수로 남는다 —
42
65
  // 실제 해석 경로는 이 리졸버 하나다.)
43
66
  const { effectiveInstanceRoot } = require('./instance/resolve.js') as typeof import('./instance/resolve.js');
44
- return effectiveInstanceRoot();
67
+ const resolved = effectiveInstanceRoot();
68
+ warnIfIgnoredEnv(resolved, process.env.ELANOUS_STATE_DIR?.trim() ? 'state-dir' : 'default');
69
+ return resolved;
45
70
  }
46
71
 
47
72
  /** Programmatic override. Trims, rejects empty/blank. Child processes
@@ -30,19 +30,32 @@ export function activeRunsForGoal(goalId: string, deps: ActiveRunsForGoalDeps =
30
30
  if (observation.completeness !== 'complete') unknownReasons.add(`ledger observation ${observation.completeness}`);
31
31
  for (const root of observation.pty.unreadable) unknownReasons.add(`unreadable PTY: ${root}`);
32
32
  const active = new Set<string>();
33
+ let skippedTerminal = 0;
33
34
  for (const run of observation.entries) {
34
- if (run.status === 'ended-unclosed') continue;
35
- if (run.ledgerDirectories.length === 0) { unknown = true; unknownReasons.add(`${run.runId}: ledger directory absent`); continue; }
35
+ if (run.status === 'ended-unclosed') { skippedTerminal += 1; continue; }
36
+ const hasObservedPty = run.ptyRefs.length > 0;
37
+ const live = run.status === 'running' && hasObservedPty;
38
+ if (run.ledgerDirectories.length === 0) {
39
+ if (hasObservedPty) { unknown = true; unknownReasons.add(`${run.runId}: ledger directory absent`); }
40
+ else skippedTerminal += 1;
41
+ continue;
42
+ }
36
43
  let identified = false;
37
44
  for (const directory of run.ledgerDirectories) {
38
45
  try {
39
46
  const ledger = loadLedger(run.runId, directory);
40
- if (ledger === null) { unknown = true; unknownReasons.add(`${run.runId}: ledger unavailable`); continue; }
47
+ if (ledger === null) {
48
+ if (hasObservedPty) { unknown = true; unknownReasons.add(`${run.runId}: ledger unavailable`); }
49
+ continue;
50
+ }
41
51
  const ids = new Set(ledger.flatMap((entry) => entry.goalId ? [entry.goalId] : []));
42
- if (ids.size !== 1) { unknown = true; unknownReasons.add(`${run.runId}: expected one goalId, found ${ids.size}`); continue; }
52
+ if (ids.size !== 1) {
53
+ if (hasObservedPty) { unknown = true; unknownReasons.add(`${run.runId}: expected one goalId, found ${ids.size}`); }
54
+ continue;
55
+ }
43
56
  identified = true;
44
57
  let matches = ids.has(goalId);
45
- if (!matches && goalId.startsWith('request-') && run.status === 'running' && run.ptyRefs.length > 0) {
58
+ if (!matches && goalId.startsWith('request-') && live) {
46
59
  const document = ledger.find((entry) => entry.event === 'start')?.data.goalFile;
47
60
  if (typeof document === 'string') {
48
61
  try {
@@ -52,20 +65,22 @@ export function activeRunsForGoal(goalId: string, deps: ActiveRunsForGoalDeps =
52
65
  }
53
66
  }
54
67
  if (matches) {
55
- if (run.status === 'running' && run.ptyRefs.length > 0) active.add(run.runId);
68
+ if (live) active.add(run.runId);
56
69
  else { unknown = true; unknownReasons.add(`${run.runId}: matching ledger without live PTY`); }
57
70
  }
58
- } catch { unknown = true; unknownReasons.add(`${run.runId}: ledger read failed`); }
71
+ } catch {
72
+ if (hasObservedPty) { unknown = true; unknownReasons.add(`${run.runId}: ledger read failed`); }
73
+ }
59
74
  }
60
- if (!identified) unknown = true;
75
+ if (!identified && !hasObservedPty) skippedTerminal += 1;
61
76
  }
62
77
  // Positive evidence wins even if a different process or store is unreadable.
63
- if (active.size > 0) return [...active].sort();
64
- if (unknown) {
65
- try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [...unknownReasons] }); } catch { /* observation is fail-soft */ }
66
- return 'unknown';
67
- }
68
- return [];
78
+ const result: ActiveRunsForGoal = active.size > 0 ? [...active].sort() : unknown ? 'unknown' : [];
79
+ try {
80
+ debug.log('execution-loop.launch-gate', 'active-runs-result', { goalId, result, skippedTerminal });
81
+ if (result === 'unknown') debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [...unknownReasons], skippedTerminal });
82
+ } catch { /* observation is fail-soft */ }
83
+ return result;
69
84
  } catch (error) {
70
85
  try { debug.log('execution-loop.launch-gate', 'active-runs-unknown', { goalId, reasons: [error instanceof Error ? error.message : String(error)] }); } catch { /* observation is fail-soft */ }
71
86
  return 'unknown';