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
@@ -1,20 +1,32 @@
1
1
  export type LoopCronVerdict = 'alive' | 'late' | 'failed' | 'off';
2
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). */
3
+ /** Shared display inputs for a graph loop or cron job. Fixed intervals use
4
+ * milliseconds; calendar cron schedules supply the second actual due instant. */
5
5
  export interface LoopCronVerdictInput {
6
6
  enabled: boolean;
7
7
  lastRunAt: string | null;
8
8
  lastStatus: string | null;
9
9
  intervalMs: number | null;
10
+ /** Second scheduled execution after the last run, for calendar-based cron schedules. */
11
+ secondDueAt?: string | null;
12
+ /** Cron-aware daemon state (src/domains/schedule-state.ts). Callers pass it for cron schedules only —
13
+ * a fixed interval keeps its own «2× interval» rule (review must-fix: stale must not override an interval). */
14
+ scheduleState?: 'live' | 'firing' | 'stale' | 'off';
10
15
  }
11
16
 
12
17
  /** Pure health classification; a failed last execution takes precedence over age.
13
- * With no valid timestamp or positive cadence, lateness cannot be established. */
18
+ * Without a valid last run and a cadence/due instant, lateness needs the daemon stale signal. */
14
19
  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';
20
+ if (!entry.enabled || entry.scheduleState === 'off') return 'off';
21
+ if (entry.lastStatus === 'failed' || entry.lastStatus === 'error' || entry.lastStatus === 'failure' ||
22
+ entry.lastStatus === 'abandoned' || entry.lastStatus === 'expired') return 'failed';
23
+ if (entry.scheduleState === 'stale') return 'late';
17
24
  const lastRunMs = entry.lastRunAt === null ? NaN : Date.parse(entry.lastRunAt);
25
+ if (entry.secondDueAt !== undefined) {
26
+ const secondDueMs = entry.secondDueAt === null ? NaN : Date.parse(entry.secondDueAt);
27
+ return Number.isFinite(lastRunMs) && Number.isFinite(nowMs) &&
28
+ Number.isFinite(secondDueMs) && secondDueMs > lastRunMs && nowMs > secondDueMs ? 'late' : 'alive';
29
+ }
18
30
  if (Number.isFinite(nowMs) && Number.isFinite(lastRunMs) &&
19
31
  entry.intervalMs !== null && Number.isFinite(entry.intervalMs) && entry.intervalMs > 0 &&
20
32
  nowMs - lastRunMs > 2 * entry.intervalMs) return 'late';
@@ -0,0 +1,66 @@
1
+ import type { SlashCommand } from '../chat/index.js';
2
+ import type { Maturity } from './feature-maturity.js';
3
+ import type { BotSurface } from './bot-command-maturity.js';
4
+
5
+ export interface BotSlashCatalogEntry {
6
+ name: string;
7
+ description: string;
8
+ supported: boolean;
9
+ }
10
+
11
+ export interface BotSlashCatalog {
12
+ commands: BotSlashCatalogEntry[];
13
+ /** Null means this name is already handled, or is not in the catalog. */
14
+ unsupportedReply(name: string): string | null;
15
+ }
16
+
17
+ /** The menu is a projection of registered core commands, not a second list of invented names.
18
+ * A bot's actual handlers take precedence over maturity metadata: a grade describes
19
+ * visibility, never whether an existing command can be dispatched. */
20
+ export function buildBotSlashCatalog(options: {
21
+ surface: BotSurface;
22
+ coreCommands: readonly Pick<SlashCommand, 'name' | 'description'>[];
23
+ maturity: {
24
+ tuiSlash: Readonly<Record<string, Maturity>>;
25
+ telegramCommand: Readonly<Record<string, Maturity>>;
26
+ discordCommand: Readonly<Record<string, Maturity>>;
27
+ };
28
+ handledCommands: readonly { name: string; description: string }[];
29
+ }): BotSlashCatalog {
30
+ const { surface, coreCommands, maturity, handledCommands } = options;
31
+ const grades = surface === 'telegram' ? maturity.telegramCommand : maturity.discordCommand;
32
+ const validName = surface === 'telegram' ? /^[a-z0-9_]{1,32}$/ : /^[a-z0-9_-]{1,32}$/;
33
+ const commands: BotSlashCatalogEntry[] = [];
34
+ const seen = new Set<string>();
35
+ for (const command of handledCommands) {
36
+ if (seen.has(command.name)) continue;
37
+ commands.push({ name: command.name, description: command.description, supported: true });
38
+ seen.add(command.name);
39
+ }
40
+ const limit = surface === 'telegram' ? 100 : 25;
41
+ const candidateNames = new Set<string>();
42
+ const candidates = coreCommands.filter((command) => {
43
+ const name = command.name;
44
+ const grade = Object.hasOwn(grades, name) ? grades[name] : undefined;
45
+ if (seen.has(name) || candidateNames.has(name) || !validName.test(name) || grade === undefined || grade === 'broken' || grade === 'system') return false;
46
+ candidateNames.add(name);
47
+ return true;
48
+ });
49
+ if (commands.length + candidates.length > limit) {
50
+ throw new Error(`${surface} command catalog exceeds ${limit} slots`);
51
+ }
52
+ for (const command of candidates) {
53
+ const name = command.name;
54
+ const description = command.description.replace(/\s+/g, ' ').trim();
55
+ const suffix = ` (${surface === 'telegram' ? '텔레그램' : '디스코드'} 미지원)`;
56
+ commands.push({ name, description: `${description.slice(0, 100 - suffix.length)}${suffix}`, supported: false });
57
+ seen.add(name);
58
+ }
59
+ const unsupported = new Set(commands.filter((command) => !command.supported).map((command) => command.name));
60
+ return {
61
+ commands,
62
+ unsupportedReply(name) {
63
+ return unsupported.has(name) ? `/${name}은(는) ${surface === 'telegram' ? '텔레그램' : '디스코드'}에서 아직 지원되지 않습니다. TUI에서 /${name}을(를) 사용하세요.` : null;
64
+ },
65
+ };
66
+ }
@@ -41,6 +41,7 @@ export const FEATURE_MATURITY = {
41
41
  '/design-check': { pwa: 'tool' },
42
42
  '/control': { pwa: 'tool' },
43
43
  '/scheduler': { pwa: 'ops' },
44
+ '/loops': { pwa: 'beta' },
44
45
  '/ops/release': { pwa: 'ops' },
45
46
  '/ops/seats': { pwa: 'ops' },
46
47
  '/ops/checklist': { pwa: 'ops' },
@@ -57,7 +58,7 @@ export const FEATURE_MATURITY = {
57
58
  pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta',
58
59
  where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
59
60
  pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
60
- research: 'beta', release: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
61
+ research: 'beta', release: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
61
62
  control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
62
63
  grounding: 'beta', graph: 'beta', loop: 'stable', card: 'stable', 'launch-head': 'beta',
63
64
  a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
@@ -71,7 +72,7 @@ export const FEATURE_MATURITY = {
71
72
  provider: 'stable', 'provider:set': 'beta', 'provider:restore': 'beta',
72
73
  'provider:rotate': 'beta', 'provider:use': 'beta', 'status-bar': 'beta',
73
74
  schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
74
- decisions: 'beta', directives: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
75
+ decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
75
76
  env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
76
77
  config: 'stable', tasks: 'stable', labels: 'beta', msg: 'beta', ask: 'stable',
77
78
  onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
@@ -110,6 +111,8 @@ export const FEATURE_MATURITY = {
110
111
  plugin: 'tool',
111
112
  widget: 'tool',
112
113
  log: 'tool',
114
+ loops: 'beta',
115
+ persona: 'beta', // G3 b — 대화별 페르소나(GOAL-BENCH B7 #23836)
113
116
  memory: 'beta',
114
117
  export: 'stable',
115
118
  delta: 'tool',
@@ -153,7 +156,7 @@ export const FEATURE_MATURITY = {
153
156
  ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
154
157
  decisions: 'stable', work: 'stable', cancel: 'stable',
155
158
  skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
156
- taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', project: 'beta', wish: 'beta',
159
+ taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', project: 'beta', wish: 'beta',
157
160
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
158
161
  harness: 'ops',
159
162
  // Existing commands outside the CXO four-grade menu list are owner-only.
@@ -162,7 +165,7 @@ export const FEATURE_MATURITY = {
162
165
  },
163
166
  discordCommand: {
164
167
  status: 'stable', sessions: 'stable', new: 'stable', fork: 'stable',
165
- persona: 'beta', poll: 'beta', attach: 'beta',
168
+ persona: 'beta', poll: 'beta', attach: 'beta', now: 'beta',
166
169
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool',
167
170
  relay: 'ops', showroom: 'ops', bots: 'ops', bot: 'ops', screen: 'ops', chart: 'ops',
168
171
  routines: 'ops', botsay: 'ops', 'voice-join': 'ops', 'voice-leave': 'ops', 'voice-status': 'ops',
@@ -4,6 +4,7 @@
4
4
  // GET /v1/dashboard/timeline — 신호 타임라인 (?hours=48&floor=6)
5
5
  // GET /v1/dashboard/heatmap — 자산×국가×섹터 매력도 + US 섹터 + 뉴스 분포
6
6
  // GET /v1/dashboard/digs — 디깅 리포트 피드 (?limit=20)
7
+ // GET /v1/dashboard/loop-activity — 루프 활동 그래프 (?since=24h)
7
8
  //
8
9
  // 전부 read-only · 캐시/로컬 SQLite 만 (dashboard-data.ts) — 라이브 fetch 없음.
9
10
  // PWA 폴링(60s) 대상. reflection.ts 패턴(CORS·checkAuth seam) 상속.
@@ -16,12 +17,15 @@ import {
16
17
  } from '../../domains/dashboard-data.js';
17
18
  import { readLiveSnapshot } from '../../domains/market-live.js';
18
19
  import { marketSessions } from '../../domains/finance.js';
20
+ import { loopActivity } from '../../loops/activity.js';
19
21
 
20
22
  export interface DashboardRouteOpts {
21
23
  /** Auth check seam. */
22
24
  checkAuth?: (req: Request) => boolean;
23
25
  /** Test seam — 실제 detached 수집 spawn 대체. */
24
26
  spawnCollector?: () => void;
27
+ /** Read-only activity provider for isolated route tests. */
28
+ loopActivity?: typeof loopActivity;
25
29
  }
26
30
 
27
31
  const CORS_HEADERS: Record<string, string> = {
@@ -43,7 +47,7 @@ export function parseDashboardPath(pathname: string): string | null {
43
47
  const m = /^\/v1\/dashboard\/([^/]+)$/.exec(pathname);
44
48
  if (!m) return null;
45
49
  const seg = decodeURIComponent(m[1]!);
46
- return ['summary', 'timeline', 'heatmap', 'digs', 'schedules', 'ontology', 'backtest', 'loops', 'ops'].includes(seg) ? seg : null;
50
+ return ['summary', 'timeline', 'heatmap', 'digs', 'schedules', 'ontology', 'backtest', 'loops', 'loop-activity', 'ops'].includes(seg) ? seg : null;
47
51
  }
48
52
 
49
53
  /** POST /v1/dashboard/refresh-live — 온디맨드 라이브 재수집 트리거 (refresh 버튼 설계 ·
@@ -99,6 +103,7 @@ export function handleDashboard(req: Request, section: string, opts: DashboardRo
99
103
  case 'ontology': return jsonResponse({ ok: true, ontology: dashboardOntology() }, 200);
100
104
  case 'backtest': return jsonResponse({ ok: true, backtest: dashboardBacktest() }, 200);
101
105
  case 'loops': return jsonResponse({ ok: true, loops: dashboardLoops() }, 200);
106
+ case 'loop-activity': return jsonResponse({ ok: true, activity: (opts.loopActivity ?? loopActivity)({ since: q.get('since') ?? '24h' }) }, 200);
102
107
  case 'ops': {
103
108
  const mid = q.get('mission');
104
109
  if (mid) return jsonResponse({ ok: true, missionDetail: dashboardOpsMission(mid) }, 200);
@@ -184,7 +184,9 @@ import { handleChannelBotsGet, handleChannelBotSet } from './setup-channel-bot.j
184
184
  import { handleLlmRoutePredict } from './llm-route-predict.js';
185
185
  import type { MissionRouter } from '../../llm/mission-router.js';
186
186
  import { handleContextFetchUrl } from './context-url.js';
187
- import { contextNow } from '../../context-bus/context-now.js';
187
+ import { contextNow, type ContextNowDeps } from '../../context-bus/context-now.js';
188
+ import type { ContextNowAudience } from '../../context-bus/context-now-surfaces.js';
189
+ import { renderVoiceNow } from '../../context-bus/context-now-surfaces.js';
188
190
  import type { IntentPredictionService } from '../../intent-prediction/index.js';
189
191
  import type { NotificationActionLoopback } from '../../web-push/notification-action-loopback.js';
190
192
  import { handlePlatforms } from './platforms.js';
@@ -486,6 +488,8 @@ export interface NexusHttpServerOpts {
486
488
  eventBus: NexusEventBus;
487
489
  /** Test seam for the read-only usage collector; production uses the cached handler. */
488
490
  usageHandler?: () => Promise<Response>;
491
+ /** Test seam for the context-now ledgers; production reads the live ledgers. */
492
+ contextNowDeps?: ContextNowDeps;
489
493
  /** Supervisor handle — required for the write API (PR ι). When omitted
490
494
  * (skipSupervisor mode), mutation routes return 503. */
491
495
  supervisor?: Supervisor;
@@ -2879,7 +2883,19 @@ export async function routeRequest(
2879
2883
  }
2880
2884
  if (pathname === '/v1/context/now') {
2881
2885
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2882
- return jsonResponse(contextNow({ ...(url.searchParams.has('topic') ? { topic: url.searchParams.get('topic') ?? '' } : {}) }));
2886
+ const audiences = url.searchParams.getAll('audience');
2887
+ if (audiences.length > 1) return jsonResponse({ error: 'invalid_audience' }, 400);
2888
+ const requested = audiences[0];
2889
+ if (requested !== undefined && requested !== 'operator' && requested !== 'user' && requested !== 'public-demo') {
2890
+ return jsonResponse({ error: 'invalid_audience' }, 400);
2891
+ }
2892
+ const forced = getUserConfig().nexus?.demoMode === true;
2893
+ const audience: ContextNowAudience = forced ? 'public-demo' : requested ?? 'operator';
2894
+ const answer = contextNow({ audience, ...(url.searchParams.has('topic') ? { topic: url.searchParams.get('topic') ?? '' } : {}) }, opts.contextNowDeps);
2895
+ debug.log('context.now', 'served', { audience, hidden: answer.hiddenCount ?? 0, forced });
2896
+ return jsonResponse(url.searchParams.get('format') === 'voice'
2897
+ ? { text: renderVoiceNow(answer, audience), audience, hiddenCount: answer.hiddenCount ?? 0 }
2898
+ : { ...answer, audience, hiddenCount: answer.hiddenCount ?? 0 });
2883
2899
  }
2884
2900
  // §6.3 (2026-05-09) — context URL fetch (showroom).
2885
2901
  // POST /v1/context/fetch-url
@@ -53,7 +53,7 @@ export function startApprovalGate(input: ApprovalGateInput, deps: ApprovalGateDe
53
53
  void Promise.resolve().then(() => (deps.runHostRegate ?? runHostRegate)({
54
54
  prNumber: input.prNumber, headCommit: input.headSha, repoRoot: input.repoRoot, verifyOnly: true,
55
55
  })).then(async (result) => {
56
- let status = result.status ?? (result.passed ? 'passed' : 'failed');
56
+ let status: ApprovalGateStatus = result.status === 'frozen' ? 'unmeasured' : result.status ?? (result.passed ? 'passed' : 'failed');
57
57
  let failures = result.failures.map(({ step, detail }) => `${step} — ${detail.replace(/\s+/g, ' ')}`.slice(0, 800));
58
58
  if (status === 'passed') {
59
59
  const current = await (deps.readPr ?? readPr)(input);
@@ -193,9 +193,13 @@ function inventory(deps: SchedulesReadDeps): { cards: ScheduleCard[]; names: Map
193
193
  return { cards, names };
194
194
  }
195
195
 
196
+ export function readSchedulesInventory(deps: SchedulesReadDeps = {}): ScheduleCard[] {
197
+ return inventory(deps).cards;
198
+ }
199
+
196
200
  export function handleSchedulesList(req: Request, opts: MetaApiOpts, deps: SchedulesReadDeps = {}): Response {
197
201
  if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
198
- const { cards } = inventory(deps);
202
+ const cards = readSchedulesInventory(deps);
199
203
  const schedules = new URL(req.url).searchParams.get('includeOff') === '1' ? cards : cards.filter(c => c.state !== 'off');
200
204
  return jsonResponse({ schedules, count: schedules.length }, 200);
201
205
  }
@@ -1,4 +1,8 @@
1
- import { CardStore } from '../../task-cards/card-store.js';
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { CardStore, type TaskCard } from '../../task-cards/card-store.js';
5
+ import { releaseLedgerRoot } from '../../instance/resolve.js';
2
6
  import { createWishCard } from '../../intake-plane/wish-card.js';
3
7
  import { jsonResponse } from './json-response.js';
4
8
 
@@ -7,19 +11,53 @@ export async function handleTaskCardsWishPost(req: Request, root?: string): Prom
7
11
  try { body = await req.json(); }
8
12
  catch { return jsonResponse({ error: 'bad_request' }, 400); }
9
13
  if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
10
- const { text, ref } = body as Record<string, unknown>;
11
- if (typeof text !== 'string' || !text.trim() || typeof ref !== 'string' || !ref.trim()) {
14
+ const { text, ref, sessionId } = body as Record<string, unknown>;
15
+ if (typeof text !== 'string' || !text.trim() || typeof ref !== 'string' || !ref.trim()
16
+ || typeof sessionId !== 'string' || !sessionId.trim()) {
12
17
  return jsonResponse({ error: 'bad_request' }, 400);
13
18
  }
14
19
  const store = new CardStore(root);
15
20
  try {
16
- const card = createWishCard({ text, source: 'pwa', ref }, store);
21
+ const card = createWishCard({ text, source: 'pwa', ref, replyTo: { surface: 'pwa', sessionId } }, store);
17
22
  return jsonResponse(card, card.created ? 201 : 200);
18
23
  } finally {
19
24
  store.close();
20
25
  }
21
26
  }
22
27
 
28
+ export interface WishPlacement { cellId: string; cellTitle: string; version: string; status: 'green' | 'yellow' | 'red' | 'done' }
29
+
30
+ export function isSplitCell(cell: unknown): cell is { id: string } {
31
+ return cell !== null && typeof cell === 'object' && 'id' in cell
32
+ && typeof cell.id === 'string' && cell.id.trim().length > 0;
33
+ }
34
+
35
+ /** Join the live checklist to the wish card by the split cell ID, never by title or card status. */
36
+ export function wishPlacements(card: TaskCard, root?: string): WishPlacement[] {
37
+ if (!card.goalId.startsWith('wish:')) return [];
38
+ const split = [...card.sections].reverse().find(section => section.key === 'orch:split');
39
+ if (!split) return [];
40
+ let cells: unknown;
41
+ try { cells = JSON.parse(split.content); } catch { return []; }
42
+ if (!Array.isArray(cells)) return [];
43
+ const ids = [...new Set(cells.filter(isSplitCell).map(cell => cell.id.trim()))];
44
+ if (!ids.length) return [];
45
+ const ledgerRoot = root ?? releaseLedgerRoot();
46
+ const path = join(ledgerRoot, 'release', 'features.sqlite');
47
+ if (!existsSync(path)) return [];
48
+ const db = new Database(path, { readonly: true, strict: true });
49
+ try {
50
+ const placements: WishPlacement[] = [];
51
+ for (const cellId of ids) {
52
+ const rows = db.query('SELECT a.version, a.status, COALESCE(a.title_override, f.title) AS title FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.feature_id = ? ORDER BY a.version').all(cellId) as Array<{ version: string; status: string; title: string }>;
53
+ for (const row of rows) if (['green', 'yellow', 'red', 'done'].includes(row.status)) {
54
+ placements.push({ cellId, cellTitle: row.title, version: row.version, status: row.status as WishPlacement['status'] });
55
+ }
56
+ }
57
+ return placements;
58
+ } finally { db.close(); }
59
+ }
60
+
23
61
  /** Read-only card API; route registration and authorization belong to the HTTP server. */
24
62
  export function handleTaskCardsGet(pathname: string, root?: string): Response {
25
63
  if (pathname !== '/v1/task-cards' && !/^\/v1\/task-cards\/[^/]+$/.test(pathname)) {
@@ -46,7 +84,8 @@ export function handleTaskCardsGet(pathname: string, root?: string): Response {
46
84
  const store = new CardStore(root);
47
85
  try {
48
86
  const card = store.getCard(id);
49
- return card ? jsonResponse({ card }) : jsonResponse({ error: 'not_found' }, 404);
87
+ return card ? jsonResponse({ card, ...(card.goalId.startsWith('wish:') ? { placements: wishPlacements(card, root) } : {}) })
88
+ : jsonResponse({ error: 'not_found' }, 404);
50
89
  } finally {
51
90
  store.close();
52
91
  }
@@ -302,6 +302,7 @@ export async function handleWorkflowPut(
302
302
  }
303
303
  const r = saveWorkflow(name, yamlText, { scope });
304
304
  if (!r.ok) {
305
+ if (r.readonly) return jsonResponse({ error: 'readonly_workflow', reason: r.error }, 409);
305
306
  return jsonResponse(
306
307
  {
307
308
  error: 'invalid_workflow',
@@ -323,7 +324,7 @@ export function handleWorkflowDelete(
323
324
  const url = new URL(req.url);
324
325
  const scope = url.searchParams.get('scope') === 'global' ? 'global' : 'project';
325
326
  const r = deleteWorkflow(name, { scope });
326
- if (!r.ok) return jsonResponse({ error: 'delete_failed', reason: r.error }, 404);
327
+ if (!r.ok) return jsonResponse({ error: r.readonly ? 'readonly_workflow' : 'delete_failed', reason: r.error }, r.readonly ? 409 : 404);
327
328
  return jsonResponse({ ok: true, path: r.path, scope }, 200);
328
329
  }
329
330
 
@@ -0,0 +1,48 @@
1
+ import { releaseLedgerRoot } from '../instance/resolve.js';
2
+ import { releaseReadiness, type ReleaseReadiness } from '../../scripts/release-loop/release-readiness.js';
3
+ import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
4
+
5
+ /** Cuts become eligible at cutAt and remain eligible through the end of the window. */
6
+ export function selectScheduledCut(schedules: readonly ReleaseSchedule[], now: Date, windowMinutes: number): ReleaseSchedule | null {
7
+ if (!Number.isFinite(windowMinutes) || windowMinutes <= 0) throw new Error('windowMinutes must be positive and finite');
8
+ const current = now.getTime();
9
+ if (!Number.isFinite(current)) throw new Error('now must be a valid date');
10
+ return schedules
11
+ .filter((schedule) => {
12
+ const cut = Date.parse(schedule.cutAt);
13
+ return Number.isFinite(cut) && cut <= current && current - cut <= windowMinutes * 60_000;
14
+ })
15
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt) || a.version.localeCompare(b.version))[0] ?? null;
16
+ }
17
+
18
+ export type AutoStartResult =
19
+ | { status: 'no-cut' }
20
+ | { status: 'planned'; version: string; cutAt: string }
21
+ | { status: 'skipped'; version: string; reason: Exclude<ReleaseReadiness, { ready: true }>['reason'] }
22
+ | { status: 'started'; version: string; cutAt: string };
23
+
24
+ export interface AutoStartOptions {
25
+ windowMinutes: number;
26
+ apply?: boolean;
27
+ now?: Date;
28
+ ledgerRoot?: string;
29
+ schedules?: (root: string) => ReleaseSchedule[];
30
+ readiness?: (version: string, ledgerRoot: string, now: Date) => ReleaseReadiness;
31
+ launch?: (version: string) => void | Promise<void>;
32
+ }
33
+
34
+ /** Select from the release schedule; the existing `release run --if-ready` owns the launch-time lock and guards. */
35
+ export async function autoStartScheduledRelease(options: AutoStartOptions): Promise<AutoStartResult> {
36
+ const root = options.ledgerRoot ?? releaseLedgerRoot();
37
+ const now = options.now ?? new Date();
38
+ // Validate the window even when the ledger has no rows.
39
+ const schedule = selectScheduledCut((options.schedules ?? listSchedules)(root), now, options.windowMinutes);
40
+ if (!schedule) return { status: 'no-cut' };
41
+ const { version, cutAt } = schedule;
42
+ if (!options.apply) return { status: 'planned', version, cutAt };
43
+ const readiness = (options.readiness ?? ((v, ledgerRoot, at) => releaseReadiness(v, { ledgerRoot, now: () => at })))(version, root, now);
44
+ if (!readiness.ready) return { status: 'skipped', version, reason: readiness.reason };
45
+ if (!options.launch) throw new Error('auto-start apply requires a launch callback');
46
+ await options.launch(version);
47
+ return { status: 'started', version, cutAt };
48
+ }
@@ -13,6 +13,9 @@ export interface ChecklistItem {
13
13
  title: string;
14
14
  status: ChecklistStatus;
15
15
  owner?: string;
16
+ priority?: 'P0' | 'P1' | 'P2';
17
+ predecessors?: string[];
18
+ deadlineVersion?: string;
16
19
  evidence?: string;
17
20
  disposition?: ChecklistDisposition;
18
21
  kind?: ChecklistKind;
@@ -93,11 +96,14 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
93
96
  debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
94
97
  }
95
98
 
96
- export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind }, options: { allowDuplicateId?: boolean } = {}): Checklist {
99
+ export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
97
100
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
98
101
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
99
102
  if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
100
103
  if (input.owner !== undefined) parseOwner(input.owner);
104
+ if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
105
+ if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
106
+ if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
101
107
  return mutate(v, (data, otherItems) => {
102
108
  if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
103
109
  const collisions = otherItems(input.id);
@@ -108,27 +114,30 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
108
114
  }
109
115
  const by = process.env.ELANOUS_TRACK || 'cli';
110
116
  const at = new Date().toISOString();
111
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), updatedAt: at, updatedBy: by };
117
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), updatedAt: at, updatedBy: by };
112
118
  data.items.push(item);
113
119
  change(data, item.id, 'add', null, item, by, at);
114
120
  return true;
115
121
  });
116
122
  }
117
123
 
118
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind }, by: string): Checklist {
124
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, by: string): Checklist {
119
125
  return mutate(v, (data) => {
120
126
  const item = data.items.find((i) => i.id === id);
121
127
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
122
128
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
123
129
  if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
124
130
  if (patch.kind !== undefined && patch.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen');
131
+ if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
132
+ if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
133
+ if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
125
134
  if (patch.owner !== undefined) {
126
135
  parseOwner(patch.owner);
127
136
  if (item.owner && patch.owner !== item.owner) {
128
137
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
129
138
  }
130
139
  }
131
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
140
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
132
141
  if (fields.length === 0) return false;
133
142
  const at = new Date().toISOString();
134
143
  for (const field of fields) {
@@ -58,14 +58,20 @@ function open(root = releaseLedgerRoot()): Database {
58
58
  if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
59
59
  if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
60
60
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
61
+ const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
62
+ for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT']] as const) {
63
+ if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
64
+ }
65
+ const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
66
+ for (const name of ['freeze_from', 'freeze_until']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
61
67
  return db;
62
68
  } catch (error) { db.close(); throw error; }
63
69
  }
64
70
 
65
- export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; updatedAt: string; updatedBy: string }
66
- type StoredSchedule = { version: string; cut_at: string; land_by: string | null; updated_at: string; updated_by: string };
71
+ export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
72
+ type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; updated_at: string; updated_by: string };
67
73
  function scheduleRow(row: StoredSchedule): ScheduleRow {
68
- return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, updatedAt: row.updated_at, updatedBy: row.updated_by };
74
+ return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
69
75
  }
70
76
 
71
77
  export function readSchedule(version: string, root?: string): ScheduleRow | null {
@@ -84,7 +90,7 @@ export function readSchedules(root?: string): ScheduleRow[] {
84
90
  finally { db.close(); }
85
91
  }
86
92
 
87
- export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string }, by: string, root?: string): ScheduleRow {
93
+ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ScheduleRow {
88
94
  validateVersion(version);
89
95
  const db = open(root);
90
96
  try {
@@ -93,13 +99,17 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
93
99
  const cutAt = patch.cutAt ?? previous?.cut_at;
94
100
  if (!cutAt) throw new CliUserError('새 판에는 --cut-at 이 필요하다');
95
101
  const landBy = patch.landBy ?? previous?.land_by ?? null;
96
- if (previous && previous.cut_at === cutAt && previous.land_by === landBy) return scheduleRow(previous);
102
+ const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
103
+ const freezeUntil = patch.freezeUntil ?? previous?.freeze_until ?? null;
104
+ if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
105
+ if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil) return scheduleRow(previous);
97
106
  const at = new Date().toISOString();
98
- db.query(`INSERT INTO release_schedules (version, cut_at, land_by, updated_at, updated_by) VALUES (?, ?, ?, ?, ?)
99
- ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
100
- .run(version, cutAt, landBy, at, by);
107
+ db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
108
+ ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
109
+ .run(version, cutAt, landBy, freezeFrom, freezeUntil, at, by);
101
110
  for (const [field, from, to] of [
102
111
  ['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
112
+ ['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
103
113
  ] as const) {
104
114
  if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
105
115
  }
@@ -134,12 +144,14 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
134
144
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
135
145
  }
136
146
  function items(db: Database, version: string): ChecklistItem[] {
137
- const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
147
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
138
148
  FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | null>>;
139
149
  return rows.map((r) => {
140
150
  const evidence = [r.evidence, ...evidenceRefs(db, version, r.id!)].filter((value) => value !== null).join('\n');
141
151
  return { id: r.id!, title: r.title!, status: r.status as ChecklistItem['status'],
142
152
  ...(r.owner !== null ? { owner: r.owner! } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
153
+ ...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors!) as string[] } : {}),
154
+ ...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version! } : {}),
143
155
  ...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
144
156
  updatedAt: r.updated_at!, updatedBy: r.updated_by! };
145
157
  });
@@ -153,9 +165,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
153
165
  if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
154
166
  // features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
155
167
  else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
156
- db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
157
- ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
158
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.updatedAt, item.updatedBy);
168
+ db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
+ ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
170
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.updatedAt, item.updatedBy);
159
171
  }
160
172
  function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
161
173
 
@@ -196,8 +208,8 @@ function importOnDb(db: Database, version: string): boolean {
196
208
  // Keep a legacy per-version title until an explicit retitle unifies the feature identity.
197
209
  if (!existing) putItem(db, version, item);
198
210
  else {
199
- db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
200
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.updatedAt, item.updatedBy);
211
+ db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
212
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.updatedAt, item.updatedBy);
201
213
  }
202
214
  }
203
215
  for (const entry of data.history) record(db, version, entry);
@@ -224,12 +236,12 @@ function importOnDb(db: Database, version: string): boolean {
224
236
  record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
225
237
  continue;
226
238
  }
227
- const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind'] as const;
239
+ const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const;
228
240
  const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
229
241
  const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
230
242
  const differs = fields.some((field) => field === 'evidence'
231
243
  ? (!preserveRefs && current.evidence !== item.evidence)
232
- : current[field] !== item[field]);
244
+ : JSON.stringify(current[field]) !== JSON.stringify(item[field]));
233
245
  if (!differs) continue;
234
246
  if (!(Date.parse(item.updatedAt) > Date.parse(current.updatedAt))) {
235
247
  ledgerWonIds.push(item.id);