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,294 @@
1
+ #!/usr/bin/env bun
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, appendFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { Database } from 'bun:sqlite';
6
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
7
+ import { getUserConfig } from '../../src/user-config.js';
8
+ import { loopEvent } from '../../src/loops/observe.js';
9
+ import { sendOutbound } from '../../src/domains/outbound-alert.js';
10
+ import { collectLandings, collectRelease, collectDecisions, type Landing, type Release, type Pending, type Section } from './daily.js';
11
+
12
+ type Issue = { id: string; title: string; owner: string; due: string; status: 'open' | 'done'; at: string; by: string };
13
+ type WeeklyParts = { landings: Section<Landing[]>; release: Section<Release>; issues: Section<Issue[]>;
14
+ followThrough: Section<{ issue: Issue; outcome: 'done' | '기한 뒤 완료' | '기한 넘김' | '진행'; doneAt?: string }[]>;
15
+ decisions: Section<Pending[]>; news: Section<{ title: string; url: string }[]> };
16
+ export type WeeklyDeps = { now?: () => Date; root?: string; vaultRoot?: string | null; sendEnabled?: boolean;
17
+ landings?: (now: Date, window: { from: Date; to: Date }) => Promise<Landing[]>; release?: () => Promise<Release>;
18
+ decisions?: () => Promise<Pending[]>; send?: (text: string, kind: string) => boolean;
19
+ receipt?: (key: string) => 'sent' | 'not-sent' | 'unknown';
20
+ log?: (category: string, event: string, data: Record<string, unknown>) => void; print?: (line: string) => void };
21
+ const reasonOf = (e: unknown) => e instanceof Error ? e.message : String(e);
22
+ const object = (v: unknown): Record<string, unknown> => v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : {};
23
+ const sortNames = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0;
24
+ const DAY = 86_400_000;
25
+ const KST = 9 * 3_600_000;
26
+ const kstDate = (date: Date) => new Date(date.getTime() + KST).toISOString().slice(0, 10);
27
+ function weekOf(now: Date) {
28
+ const local = new Date(now.getTime() + KST);
29
+ const day = local.getUTCDay();
30
+ const monday = Date.UTC(local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate() - ((day + 6) % 7));
31
+ const current = new Date(monday - KST);
32
+ const from = new Date(current.getTime() - 7 * DAY);
33
+ const to = current;
34
+ const thursday = new Date(monday + 3 * DAY);
35
+ const year = thursday.getUTCFullYear();
36
+ const firstThursday = Date.UTC(year, 0, 4);
37
+ const firstMonday = firstThursday - ((new Date(firstThursday).getUTCDay() + 6) % 7) * DAY;
38
+ const week = Math.floor((monday - firstMonday) / (7 * DAY)) + 1;
39
+ return { from, to, key: `${year}-W${String(week).padStart(2, '0')}` };
40
+ }
41
+ const issuePath = (root: string) => join(root, 'rhythm', 'weekly', 'issues.jsonl');
42
+ function issueHistory(root: string): Issue[] {
43
+ const file = issuePath(root);
44
+ if (!existsSync(file)) return [];
45
+ return readFileSync(file, 'utf8').split('\n').filter(Boolean).map((line, index) => {
46
+ let row: Record<string, unknown>;
47
+ try { row = object(JSON.parse(line)); } catch { throw new Error(`현안 원장 ${index + 1}줄 JSON 오류`); }
48
+ if (!['id', 'title', 'owner', 'due', 'at', 'by'].every(k => typeof row[k] === 'string' && row[k]) || !['open', 'done'].includes(String(row.status)) ||
49
+ !Number.isFinite(Date.parse(String(row.at))) || !Number.isFinite(Date.parse(String(row.due)))) throw new Error(`현안 원장 ${index + 1}줄 형식 오류`);
50
+ return row as Issue;
51
+ });
52
+ }
53
+ function latest(rows: Issue[], before = Infinity): Map<string, Issue> {
54
+ const map = new Map<string, Issue>();
55
+ for (const row of rows) if (Date.parse(row.at) < before) map.set(row.id, row);
56
+ return map;
57
+ }
58
+ function appendIssue(root: string, row: Issue) {
59
+ const file = issuePath(root);
60
+ mkdirSync(dirname(file), { recursive: true });
61
+ appendFileSync(file, `${JSON.stringify(row)}\n`);
62
+ }
63
+ function readDailyNews(root: string, from: Date, to: Date): Section<{ title: string; url: string }[]> {
64
+ const found = new Map<string, { title: string; url: string }>();
65
+ const failures: string[] = [];
66
+ let files = 0;
67
+ for (let at = from.getTime(); at < to.getTime(); at += DAY) {
68
+ const day = kstDate(new Date(at));
69
+ const file = join(root, 'rhythm', 'daily', `${day}.md`);
70
+ if (!existsSync(file)) { failures.push(`${day} 데일리 없음`); continue; }
71
+ files++;
72
+ try {
73
+ const text = readFileSync(file, 'utf8');
74
+ const block = text.split('## ⑦ 외부 동향\n')[1]?.split(/\n\n## /)[0]?.trim();
75
+ if (!block) throw new Error('외부 동향 절 없음');
76
+ if (block.startsWith('못 읽음 ·')) throw new Error(block.split('\n')[0]!);
77
+ for (const line of block.split('\n')) {
78
+ if (line === '수집된 기사 0건' || /^ 시사점: /.test(line)) continue;
79
+ const match = /^- (.+) — (https?:\/\/\S+)$/.exec(line);
80
+ if (!match) throw new Error(`외부 동향 기사 형식 오류: ${line}`);
81
+ if (!found.has(match[2]!)) found.set(match[2]!, { title: match[1]!, url: match[2]! });
82
+ }
83
+ } catch (e) { failures.push(`${day} ${reasonOf(e)}`); }
84
+ }
85
+ const news = [...found.values()].slice(0, 10);
86
+ if (failures.length) return { status: 'unreadable', reason: `${files ? failures.join('; ') : '데일리 없음'}${news.length ? `\n부분 수집 (완전한 주간 집계 아님):\n${news.map(n => `- ${n.title} — ${n.url}`).join('\n')}` : ''}` };
87
+ return { status: 'ok', value: news };
88
+ }
89
+ const labels: Record<keyof WeeklyParts, string> = {
90
+ landings: '① 지난주 성과', release: '② 이번 주 판 계획', issues: '③ 주간 현안',
91
+ followThrough: '④ 지난주 현안 이행 대조', decisions: '⑤ 결정 필요', news: '⑥ 주간 외부 동향',
92
+ };
93
+ function publishedVersions(root: string, from: Date, to: Date): string[] {
94
+ const directory = join(root, 'release');
95
+ if (!existsSync(directory)) return [];
96
+ return readdirSync(directory).filter(version => /^\d+\.\d+\.\d+$/.test(version)).filter(version => {
97
+ const file = join(directory, version, 'release.json');
98
+ if (!existsSync(file)) return false;
99
+ const record = object(JSON.parse(readFileSync(file, 'utf8')));
100
+ const publishedAt = record.publishedAt;
101
+ return record.version === version && typeof publishedAt === 'string' && Number.isFinite(Date.parse(publishedAt)) && Date.parse(publishedAt) >= from.getTime() && Date.parse(publishedAt) < to.getTime();
102
+ }).sort(sortNames);
103
+ }
104
+ function compose(parts: WeeklyParts, key: string, now: Date, versions: Section<string[]>) {
105
+ const open = parts.issues.status === 'ok' ? parts.issues.value : [];
106
+ const overdue = open.filter(i => Date.parse(i.due) < now.getTime());
107
+ const red = parts.release.status === 'ok' ? parts.release.value.red : [];
108
+ const risk = overdue[0] ? `${overdue[0].title} 기한 넘김 (${overdue[0].owner})` : red[0] ? `${red[0].name} red 칸` : '확인된 위험 없음(못 읽은 절은 별도 확인)';
109
+ const old = parts.decisions.status === 'ok' ? parts.decisions.value.filter(d => now.getTime() - Date.parse(d.openedAt) > 3 * DAY) : [];
110
+ const header = [
111
+ `S: 지난주 착지 ${parts.landings.status === 'ok' ? `${parts.landings.value.length}건` : `못 읽음 · ${parts.landings.reason}`} · 판 ${parts.release.status === 'ok' ? `${parts.release.value.green}/${parts.release.value.total}` : `못 읽음 · ${parts.release.reason}`}`,
112
+ `C: ${risk}`, `Q: 이번 주 ${old.length ? old[0]!.name : '어떤 현안을'} 정할 것인가?`,
113
+ `A: ${overdue[0] ? `${overdue[0].owner}가 ${overdue[0].title} 이행 기한을 확인` : red[0] ? `${red[0].name} 해소를 결정` : '열린 현안의 담당과 기한을 확인'}`,
114
+ ].join('\n');
115
+ const section = <T>(name: keyof WeeklyParts, value: Section<T>, format: (v: T) => string) =>
116
+ `## ${labels[name]}\n${value.status === 'ok' ? format(value.value) : `못 읽음 · ${value.reason}`}`;
117
+ const markdown = [
118
+ `# 주간 미팅 — ${key} (KST)`, header,
119
+ section('landings', parts.landings, list => {
120
+ const seats = [...new Set(list.map(i => i.seat))].sort(sortNames).map(s => `${s} ${list.filter(i => i.seat === s).length}`).join(' · ');
121
+ return `총 ${list.length}건 · 자리별 ${seats || '없음'}\n발행된 판: ${versions.status === 'ok' ? versions.value.join(', ') || '없음' : `못 읽음 · ${versions.reason}`}`;
122
+ }),
123
+ section('release', parts.release, r => `${r.version}: green ${r.green}/${r.total} · 다음 판 ${r.nextVersion ?? '미정'} ${r.nextGreen ?? '못 읽음'}/${r.nextTotal ?? '못 읽음'} · 다음 컷 ${r.cutAt ?? '미정'}\nred 칸: ${r.red.map(i => i.name).join(', ') || '없음'}`),
124
+ section('issues', parts.issues, list => list.map(i => `- ${i.id} ${i.title} · ${i.owner} · 기한 ${i.due}`).join('\n') || '열린 현안 없음'),
125
+ section('followThrough', parts.followThrough, list => list.map(({ issue, outcome, doneAt }) => `- ${issue.id} ${issue.title} · ${issue.owner} · 기한 ${issue.due} — ${outcome}${doneAt ? ` (완료 ${doneAt})` : ''}`).join('\n') || '지난주 열린 현안 없음'),
126
+ section('decisions', parts.decisions, list => list.filter(d => now.getTime() - Date.parse(d.openedAt) > 3 * DAY).map(d => `- ${d.name} (${d.openedAt})`).join('\n') || '3일 넘은 결정 없음'),
127
+ section('news', parts.news, list => list.map(n => `- ${n.title} — ${n.url}`).join('\n') || '수집된 기사 0건'),
128
+ ].join('\n\n') + '\n';
129
+ const sections = Object.fromEntries(Object.keys(labels).map(k => [k, parts[k as keyof WeeklyParts].status])) as Record<keyof WeeklyParts, 'ok' | 'unreadable'>;
130
+ return { markdown, header, sections, issuesOpen: open.length, issuesOverdue: overdue.length };
131
+ }
132
+ function vaultCopy(markdown: string, key: string, deps: WeeklyDeps) {
133
+ let root = deps.vaultRoot;
134
+ if (root === undefined) {
135
+ try {
136
+ root = String(object(getUserConfig().raw.obsidian).vaultRoot || '') || null;
137
+ if (root && effectiveInstanceRoot() !== prodInstanceRoot()) return { vaultError: '격리 인스턴스 — 운영 볼트 사본 건너뜀', vaultFatal: false };
138
+ } catch (e) { return { vaultError: reasonOf(e), vaultFatal: true }; }
139
+ }
140
+ if (!root) return { vaultError: null, vaultFatal: false };
141
+ try {
142
+ const file = join(root, '00. Inbox', 'Weekly Review', `${key}.md`);
143
+ mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, markdown);
144
+ return { vaultError: null, vaultFatal: false };
145
+ } catch (e) { return { vaultError: reasonOf(e), vaultFatal: true }; }
146
+ }
147
+ function sendReview(file: string, text: string, send: (text: string, kind: string) => boolean, receipt: (key: string) => 'sent' | 'not-sent' | 'unknown') {
148
+ const key = file.slice(file.lastIndexOf('/') + 1).replace(/\.md$/, '');
149
+ const db = new Database(join(dirname(file), 'delivery.sqlite'));
150
+ try {
151
+ db.run("CREATE TABLE IF NOT EXISTS deliveries (week TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('pending','unknown','sent')))");
152
+ db.run("INSERT OR IGNORE INTO deliveries(week, state) VALUES (?, 'pending')", [key]);
153
+ const state = (db.prepare('SELECT state FROM deliveries WHERE week = ?').get(key) as { state: string }).state;
154
+ if (state === 'sent') return { sent: false, sendError: null };
155
+ if (state === 'unknown') {
156
+ const verdict = receipt(key);
157
+ if (verdict === 'sent') { db.run("UPDATE deliveries SET state = 'sent' WHERE week = ?", [key]); return { sent: false, sendError: null }; }
158
+ if (verdict !== 'not-sent') return { sent: false, sendError: `발송 결과 불명 · RHYTHM-WEEKLY:${key} 영수증 확인 필요` };
159
+ db.run("UPDATE deliveries SET state = 'pending' WHERE week = ?", [key]);
160
+ }
161
+ if (!db.prepare("UPDATE deliveries SET state = 'unknown' WHERE week = ? AND state = 'pending'").run(key).changes)
162
+ return { sent: false, sendError: `발송 결과 불명 · RHYTHM-WEEKLY:${key} 영수증 확인 필요` };
163
+ try {
164
+ if (!send(`${text}\nRHYTHM-WEEKLY:${key}`, 'ops-report')) return { sent: false, sendError: '발송 실패 · 결과 불명 (영수증 확인 필요)' };
165
+ db.run("UPDATE deliveries SET state = 'sent' WHERE week = ?", [key]);
166
+ return { sent: true, sendError: null };
167
+ } catch (e) { return { sent: false, sendError: `발송 결과 불명 · ${reasonOf(e)}` }; }
168
+ } catch (e) { return { sent: false, sendError: `발송 상태 못 읽음 · ${reasonOf(e)}` }; }
169
+ finally { db.close(); }
170
+ }
171
+ function outboundReceipt(root: string, key: string): 'sent' | 'unknown' {
172
+ const file = join(root, ...(root === prodInstanceRoot() ? ['memory'] : []), 'surface_events.db');
173
+ if (!existsSync(file)) return 'unknown';
174
+ try {
175
+ const db = new Database(file, { readonly: true, create: false });
176
+ try { return db.prepare("SELECT 1 FROM events WHERE direction='outbound' AND kind='ops-report' AND instr(text, ?) > 0 LIMIT 1").get(`RHYTHM-WEEKLY:${key}`) ? 'sent' : 'unknown'; }
177
+ finally { db.close(); }
178
+ } catch { return 'unknown'; }
179
+ }
180
+ export async function runWeekly(options: { dryRun?: boolean; stage?: 'collect' | 'compose' | 'deliver' } = {}, deps: WeeklyDeps = {}) {
181
+ const now = (deps.now ?? (() => new Date()))();
182
+ const root = deps.root ?? effectiveInstanceRoot();
183
+ const { from, to, key } = weekOf(now);
184
+ const file = join(root, 'rhythm', 'weekly', `${key}.md`);
185
+ let markdown: string;
186
+ let header: string;
187
+ let sections: Record<keyof WeeklyParts, 'ok' | 'unreadable'>;
188
+ let issuesOpen: number;
189
+ let issuesOverdue: number;
190
+ if (options.stage === 'compose' || options.stage === 'deliver') {
191
+ if (!existsSync(file)) throw new Error(`수집 파일 없음: ${file}`);
192
+ markdown = readFileSync(file, 'utf8');
193
+ header = markdown.split('\n').slice(2, 6).join('\n');
194
+ if (!header.startsWith('S: ') || Object.values(labels).some(label => !markdown.includes(`## ${label}\n`))) throw new Error(`리뷰 파일 형식 오류: ${file}`);
195
+ sections = Object.fromEntries(Object.entries(labels).map(([k, label]) => [k, markdown.split(`## ${label}\n`)[1]?.startsWith('못 읽음 ·') ? 'unreadable' : 'ok'])) as typeof sections;
196
+ if (sections.issues === 'unreadable') { issuesOpen = 0; issuesOverdue = 0; }
197
+ else {
198
+ const open = [...latest(issueHistory(root)).values()].filter(i => i.status === 'open');
199
+ issuesOpen = open.length;
200
+ issuesOverdue = open.filter(i => Date.parse(i.due) < now.getTime()).length;
201
+ }
202
+ } else {
203
+ const safe = async <T>(fn: () => Promise<T>): Promise<Section<T>> => {
204
+ try { return { status: 'ok', value: await fn() }; } catch (e) { return { status: 'unreadable', reason: reasonOf(e) }; }
205
+ };
206
+ const issues = await safe(async () => issueHistory(root));
207
+ const parts: WeeklyParts = {
208
+ landings: await safe(() => (deps.landings ?? collectLandings)(now, { from, to })),
209
+ release: await safe(deps.release ?? collectRelease),
210
+ issues: issues.status === 'ok' ? { status: 'ok', value: [...latest(issues.value).values()].filter(i => i.status === 'open') } : issues,
211
+ followThrough: issues.status === 'ok' ? { status: 'ok', value: [...new Set([
212
+ ...[...latest(issues.value, from.getTime()).values()].filter(i => i.status === 'open').map(i => i.id),
213
+ ...issues.value.filter(i => Date.parse(i.at) >= from.getTime() && Date.parse(i.at) < to.getTime()).map(i => i.id),
214
+ ])].map(id => {
215
+ const issue = issues.value.filter(i => i.id === id && Date.parse(i.at) < to.getTime()).at(-1)!;
216
+ const current = latest(issues.value).get(id);
217
+ // A completion after the due date is a missed deadline, not a plain «done» (review round 3).
218
+ if (current?.status === 'done') return { issue, outcome: Date.parse(current.at) > Date.parse(issue.due) ? '기한 뒤 완료' as const : 'done' as const, doneAt: current.at };
219
+ return { issue, outcome: Date.parse(issue.due) < now.getTime() ? '기한 넘김' as const : '진행' as const };
220
+ }) } : issues,
221
+ decisions: await safe(deps.decisions ?? collectDecisions),
222
+ news: readDailyNews(root, from, to),
223
+ };
224
+ const versions = await safe(async () => publishedVersions(root, from, to));
225
+ if (versions.status === 'unreadable') {
226
+ parts.landings = { status: 'unreadable', reason: `${parts.landings.status === 'unreadable' ? `${parts.landings.reason}; ` : ''}발행된 판 목록 못 읽음 · ${versions.reason}` };
227
+ }
228
+ const review = compose(parts, key, now, versions);
229
+ ({ markdown, header, sections, issuesOpen, issuesOverdue } = review);
230
+ mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, markdown);
231
+ }
232
+ const vault = options.stage === 'deliver' ? { vaultError: null, vaultFatal: false } : vaultCopy(markdown, key, deps);
233
+ let sent = false;
234
+ let sendError: string | null = null;
235
+ let enabled = deps.sendEnabled === true;
236
+ if (deps.sendEnabled === undefined && !options.dryRun && options.stage !== 'collect') {
237
+ try { enabled = object(object(object(getUserConfig().raw.loops).rhythm).weekly).send === true; }
238
+ catch (e) { sendError = `발송 설정 못 읽음 · ${reasonOf(e)}`; }
239
+ }
240
+ if (!vault.vaultFatal && !options.dryRun && options.stage !== 'collect' && options.stage !== 'compose' && enabled) {
241
+ const delivery = sendReview(file, `${header}\n\n${file}`, deps.send ?? sendOutbound, deps.receipt ?? (key => outboundReceipt(root, key)));
242
+ sent = delivery.sent; sendError = delivery.sendError;
243
+ }
244
+ if (options.stage !== 'collect' && options.stage !== 'compose') {
245
+ const tick = { sections, issuesOpen, issuesOverdue, sent };
246
+ if (deps.log) deps.log('loop.rhythm-weekly', 'tick', tick); else loopEvent('rhythm-weekly', 'tick', tick);
247
+ }
248
+ return { file, markdown, header, sections, issuesOpen, issuesOverdue, sent, ...vault, sendError };
249
+ }
250
+ export async function main(args: string[] = process.argv.slice(2), deps: WeeklyDeps = {}) {
251
+ const print = deps.print ?? console.log;
252
+ const root = deps.root ?? effectiveInstanceRoot();
253
+ if (args[0] === '--help') {
254
+ print('bun scripts/rhythm/weekly.ts [--dry-run] [--json]\nbun scripts/rhythm/weekly.ts issue add --title <t> --owner <자리> --due <ISO> [--by <이름>]\nbun scripts/rhythm/weekly.ts issue done <id> [--by <이름>]\nbun scripts/rhythm/weekly.ts issue list [--json]');
255
+ return;
256
+ }
257
+ if (args[0] === 'issue') {
258
+ const [action, ...rest] = args.slice(1);
259
+ const flag = (name: string) => { const index = rest.indexOf(name); return index < 0 ? undefined : rest[index + 1]; };
260
+ const now = (deps.now ?? (() => new Date()))().toISOString();
261
+ if (action === 'list') {
262
+ if (rest.some(arg => arg !== '--json')) throw new Error('알 수 없는 옵션');
263
+ const rows = [...latest(issueHistory(root)).values()];
264
+ print(rest.includes('--json') ? JSON.stringify(rows) : rows.map(i => `${i.id} · ${i.title} · ${i.owner} · ${i.due} · ${i.status}`).join('\n') || '현안 없음');
265
+ return rows;
266
+ }
267
+ const by = rest.includes('--by') ? flag('--by') : process.env.AI_AGENT;
268
+ if (!by) throw new Error('--by 또는 AI_AGENT 필요');
269
+ if (action === 'add') {
270
+ const title = flag('--title'), owner = flag('--owner'), due = flag('--due');
271
+ if (!title || !owner || !due || !/^\d{4}-\d\d-\d\dT\d\d:\d\d/.test(due) || !Number.isFinite(Date.parse(due)) || rest.length % 2 ||
272
+ rest.some((arg, i) => i % 2 === 0 ? !['--title', '--owner', '--due', '--by'].includes(arg) : !arg || arg.startsWith('--'))) throw new Error('issue add: --title --owner --due <ISO> 필요');
273
+ const row: Issue = { id: randomUUID(), title, owner, due, status: 'open', at: now, by };
274
+ appendIssue(root, row); print(JSON.stringify(row)); return row;
275
+ }
276
+ if (action === 'done') {
277
+ const id = rest[0];
278
+ if (!id || (rest.length !== 1 && (rest.length !== 3 || rest[1] !== '--by' || !rest[2]))) throw new Error('issue done <id> [--by <이름>]');
279
+ const issue = latest(issueHistory(root)).get(id);
280
+ if (!issue) throw new Error(`현안 없음: ${id}`);
281
+ const row: Issue = { ...issue, status: 'done', at: now, by };
282
+ appendIssue(root, row); print(JSON.stringify(row)); return row;
283
+ }
284
+ throw new Error(`알 수 없는 issue 명령: ${action}`);
285
+ }
286
+ const stage = process.env.ELANOUS_GRAPH_DIR && ['collect', 'compose', 'deliver'].includes(args[0] ?? '') ? args.shift() as 'collect' | 'compose' | 'deliver' : undefined;
287
+ for (const arg of args) if (!['--dry-run', '--json'].includes(arg)) throw new Error(`알 수 없는 옵션: ${arg}`);
288
+ const result = await runWeekly({ dryRun: args.includes('--dry-run'), stage }, deps);
289
+ print(args.includes('--json') ? JSON.stringify(result) : `${result.markdown}\n파일: ${result.file} · sent=${result.sent}`);
290
+ if ((stage === 'compose' || !stage) && result.vaultFatal) throw new Error(`볼트 사본 실패 · ${result.vaultError}`);
291
+ if ((stage === 'deliver' || !stage) && result.sendError) throw new Error(`발송 실패 · ${result.sendError}`);
292
+ return result;
293
+ }
294
+ if (import.meta.main) main().catch(e => { console.error(`rhythm-weekly: ${reasonOf(e)}`); process.exitCode = 1; });
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bun
2
+ /** Static readiness inventory for the public (core) skills; never executes a skill. */
3
+ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
4
+ import { join, relative } from 'node:path';
5
+ import { computeSkillBoundaries, type SkillBoundaryReport } from '../skill-boundary.js';
6
+
7
+ export type PublicCheckKey = 'header' | 'entry' | 'free' | 'portable' | 'install';
8
+ export interface PublicSkillRow {
9
+ readonly skill: string;
10
+ readonly checks: Readonly<Record<PublicCheckKey, boolean | 'N/A'>>;
11
+ readonly violations: readonly string[];
12
+ }
13
+ export interface PublicCheckReport {
14
+ readonly skills: readonly PublicSkillRow[];
15
+ readonly errors: readonly string[];
16
+ readonly ok: boolean;
17
+ }
18
+
19
+ const PERSONAL_OR_INTERNAL = /\/Users\/|~\/source(?:\/|\b)|\b(?:INTERNAL_ONLY|ELANOUS_INTERNAL|PRIVATE_INTERNAL|internal-only)\b|@internal\b|내부\s*전용/iu;
20
+ const EXECUTABLE = /\.(?:ts|tsx|js|mjs|cjs|py|sh|bash)$/iu;
21
+ const ENTRY_DIR = /^(?:scripts|src|bin)\//u;
22
+ const NON_ENTRY = /(?:\.d\.ts|\.(?:test|spec)\.[^.]+)$/iu;
23
+ const RUN_COMMAND = /(?:^|[\s`$])(?:bun(?:\s+run)?|node|python3?|bash|sh|npx\s+tsx|uv\s+run\s+python3?)\s+((?:\.\/)?(?:scripts|src|bin)\/[\w./-]+\.(?:ts|tsx|js|mjs|cjs|py|sh|bash))\b/gimu;
24
+
25
+ function skillFiles(dir: string): string[] {
26
+ const files: string[] = [];
27
+ function visit(path: string): void {
28
+ for (const item of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
29
+ if (item.name === '.git' || item.name === 'node_modules' || item.name === '__pycache__') continue;
30
+ const file = join(path, item.name);
31
+ if (item.isDirectory()) visit(file);
32
+ else if (item.isFile()) files.push(file);
33
+ }
34
+ }
35
+ visit(dir);
36
+ return files;
37
+ }
38
+
39
+ function skillHeader(doc: string): string | undefined {
40
+ return /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(doc)?.[1];
41
+ }
42
+
43
+ function headerValid(doc: string, skill: string): boolean {
44
+ const header = skillHeader(doc);
45
+ if (!header) return false;
46
+ const name = /^name:\s*['"]?([^\s'"#]+)['"]?\s*$/mu.exec(header)?.[1];
47
+ const description = /^description:\s*(.*)$/mu.exec(header);
48
+ if (!description) return false;
49
+ const value = description[1]!.replace(/\s+#.*$/u, '').trim();
50
+ const following = header.slice(description.index + description[0].length).split(/\r?\n/u).slice(1);
51
+ const multiline = following.some((line) => /^\s+\S/u.test(line) && !/^\s*#/u.test(line));
52
+ const block = /^[>|][+-]?$/u.test(value);
53
+ // 리뷰 must-fix: `description: >-` 처럼 블록 표시만 있고 뒤 줄이 비면 설명이 없다.
54
+ return name === skill && (value !== '' && !block && !value.startsWith('#') || block && multiline);
55
+ }
56
+
57
+ function promptOnly(doc: string): boolean {
58
+ return /^prompt-only:\s*true\s*(?:#.*)?$/mu.test(skillHeader(doc) ?? '');
59
+ }
60
+
61
+ const ENTRY_NAME = /^(?:scripts|src)\/(?:main|cli|index|run)\.[^/]+$/u;
62
+
63
+ /** 리뷰 must-fix: 확장자·디렉터리만으로 «실행 진입점»이 아니다(`src/types.ts`).
64
+ * 인정 = 문서가 실제로 실행하는 파일 ∪ `bin/` 아래 ∪ scripts|src 바로 아래 main·cli·index·run. */
65
+ function executableEntries(files: readonly string[], dir: string, docs: string): string[] {
66
+ const commanded = new Set([...docs.matchAll(RUN_COMMAND)].map((command) => command[1]!.replace(/^\.\//u, '')));
67
+ return files.map((file) => relative(dir, file)).filter((file) => ENTRY_DIR.test(file) && EXECUTABLE.test(file) && !NON_ENTRY.test(file)
68
+ && (commanded.has(file) || file.startsWith('bin/') || ENTRY_NAME.test(file)));
69
+ }
70
+
71
+ function freeValid(doc: string, entries: ReadonlySet<string>): boolean {
72
+ const section = /(?:^|\n)#{1,4}\s*[^\n]*(?:무료|free)[^\n]*\n([\s\S]*?)(?=\n#{1,4}\s|$)/giu;
73
+ for (const match of doc.matchAll(section)) {
74
+ const body = match[1]!;
75
+ const keyless = /(?:키\s*없이|키\s*불필요|키\s*없는|API\s*키가\s*필요\s*없|no\s+(?:api\s+)?key|without\s+(?:an?\s+)?(?:api\s+)?key|keyless)/iu;
76
+ const envRequired = body.split(/\r?\n/u).some((line) => /\.env\b/iu.test(line) &&
77
+ !/(?:without|no|not\s+required|optional)\s+(?:an?\s+)?\.env\b|\.env\b[^\n]*(?:없이|불필요|선택|optional|not\s+required)/iu.test(line) &&
78
+ /(?:\b(?:require|need|configure|create|copy|source|set\s*up|must)\b[^\n]*\.env\b|\.env\b[^\n]*(?:필요|설정|생성|복사|로드|\b(?:required|needed|must)\b))/iu.test(line));
79
+ const runnable = [...body.matchAll(RUN_COMMAND)].some((command) => entries.has(command[1]!.replace(/^\.\//u, '')));
80
+ // 리뷰 must-fix: 제목이 «키 없이»여도 본문이 키를 요구하면 무료 경로가 아니다.
81
+ const keyRequired = /(?:\brequires?\s+(?:an?\s+)?(?:api\s+)?key\b|\bneeds?\s+(?:an?\s+)?(?:api\s+)?key\b|(?:API\s*)?키가?\s*필요(?!\s*없))/iu.test(body);
82
+ if (keyless.test(match[0]) && runnable && !envRequired && !keyRequired &&
83
+ !/(?:키\s*없는?\s+(?:자동|실행)\s*(?:경로|을)\s*(?:보장|지원)하지\s*않|without\s+(?:an?\s+)?(?:api\s+)?key\s+(?:is\s+)?not\s+supported)/iu.test(body)) return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /** 리뷰 must-fix: manifest 와 설치 명령의 «종류»가 맞아야 한다(requirements.txt 만 있는데 `bun install` 은 통과 아님). */
89
+ function installValid(doc: string, dir: string): boolean {
90
+ const has = (name: string) => existsSync(join(dir, name)) && lstatSync(join(dir, name)).isFile();
91
+ if (has('package.json') && /\b(?:bun|npm|pnpm|yarn)\s+(?:install|i)\b/imu.test(doc)) return true;
92
+ for (const req of ['requirements.txt', 'requirements-python.txt']) {
93
+ if (has(req) && new RegExp(`\\b(?:pip|pip3|uv\\s+pip)\\s+install\\s+-r\\s+\\S*${req.replace('.', '\\.')}\\b`, 'imu').test(doc)) return true;
94
+ }
95
+ if (has('pyproject.toml') && /\b(?:(?:pip|pip3|uv\s+pip)\s+install\s+(?:-e\s+)?\.(?:\s|$|\[)|uv\s+sync\b)/imu.test(doc)) return true;
96
+ return false;
97
+ }
98
+
99
+ /** `boundary` is injectable for pure fixture tests; production always derives it from the repository. */
100
+ export function checkPublicSkills(root: string, boundary: SkillBoundaryReport = computeSkillBoundaries(root)): PublicCheckReport {
101
+ const errors = [...boundary.errors];
102
+ const skills: PublicSkillRow[] = [];
103
+ const skillsDir = join(root, 'skills');
104
+ const addon = new Set(boundary.skills.filter((row) => row.verdict === 'addon').map((row) => row.skill));
105
+ const names = existsSync(skillsDir) ? readdirSync(skillsDir).filter((name) => {
106
+ const dir = join(skillsDir, name);
107
+ return lstatSync(dir).isDirectory() && !addon.has(name);
108
+ }).sort() : [];
109
+ if (!existsSync(skillsDir)) errors.push('skills/ 디렉터리가 없다');
110
+ for (const row of boundary.skills) {
111
+ if (row.verdict === 'core' && !names.includes(row.skill)) errors.push(`${row.skill}: skills/ 디렉터리가 없다`);
112
+ }
113
+ for (const skill of names) {
114
+ const dir = join(skillsDir, skill);
115
+ const files = skillFiles(dir);
116
+ const docPath = join(dir, 'SKILL.md');
117
+ const doc = files.includes(docPath) ? readFileSync(docPath, 'utf8') : '';
118
+ const docs = [doc, ...files.filter((f) => f !== docPath && /(?:^|\/)(?:README|INSTALL)\.md$/iu.test(f)).map((f) => readFileSync(f, 'utf8'))].join('\n');
119
+ const matches: string[] = [];
120
+ for (const file of files) {
121
+ const bytes = readFileSync(file);
122
+ if (bytes.includes(0)) continue;
123
+ try {
124
+ if (PERSONAL_OR_INTERNAL.test(new TextDecoder('utf-8', { fatal: true }).decode(bytes))) matches.push(relative(root, file));
125
+ } catch {
126
+ // Binary assets are not text; extensionless and dotfiles are checked when decodable.
127
+ }
128
+ }
129
+ const entries = new Set(executableEntries(files, dir, docs));
130
+ const checks: PublicSkillRow['checks'] = {
131
+ header: headerValid(doc, skill),
132
+ entry: entries.size > 0 ? true : promptOnly(doc) ? 'N/A' : false,
133
+ free: freeValid(docs, entries),
134
+ portable: matches.length === 0,
135
+ install: installValid(docs, dir),
136
+ };
137
+ const violations = [
138
+ ...(!checks.header ? ['SKILL.md 머리(name·description) 없음/불일치'] : []),
139
+ ...(checks.entry === false ? ['스킬 내부 실행 파일(scripts/src/bin)이 없음'] : []),
140
+ ...(!checks.free ? ['키/.env 없이 쓰는 무료 경로가 문서에 없음'] : []),
141
+ ...matches.map((file) => `${file}: 개인 경로/내부 표지 문자열`),
142
+ ...(!checks.install ? ['의존 manifest와 문서의 한 줄 설치 명령이 없음'] : []),
143
+ ];
144
+ skills.push({ skill, checks, violations });
145
+ }
146
+ return { skills, errors, ok: errors.length === 0 && skills.every((row) => row.violations.length === 0) };
147
+ }
148
+
149
+ export function formatPublicCheck(report: PublicCheckReport): string {
150
+ const columns: PublicCheckKey[] = ['header', 'entry', 'free', 'portable', 'install'];
151
+ return [
152
+ `skill ${columns.map((key) => key.padEnd(9)).join(' ')} result`,
153
+ ...report.skills.map((row) => `${row.skill.padEnd(34)} ${columns.map((key) => (row.checks[key] === 'N/A' ? 'N/A' : row.checks[key] ? 'OK' : 'FAIL').padEnd(9)).join(' ')} ${row.violations.length ? row.violations.join('; ') : 'OK'}`),
154
+ ...report.errors.map((error) => `ERROR ${error}`),
155
+ `PUBLIC-CHECK ${report.ok ? 'PASS' : 'FAIL'} (${report.skills.length} public skills)`,
156
+ ].join('\n');
157
+ }
158
+
159
+ if (import.meta.main) {
160
+ const args = process.argv.slice(2);
161
+ if (args.some((arg) => arg !== '--json')) {
162
+ console.error('Usage: bun scripts/skills/public-check.ts [--json]');
163
+ process.exitCode = 2;
164
+ } else {
165
+ const report = checkPublicSkills(join(import.meta.dir, '..', '..'));
166
+ console.log(args.includes('--json') ? JSON.stringify(report, null, 2) : formatPublicCheck(report));
167
+ if (!report.ok) process.exitCode = 1;
168
+ }
169
+ }
@@ -296,15 +296,11 @@ export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: s
296
296
  // resolves the old path through ELANOUS_TEST_HOME rather than the real home.
297
297
  delete env.XDG_CONFIG_HOME;
298
298
  env.ELANOUS_TEST_HOME = testRoot;
299
- // ⚠️ Redirect elanous's OWN roots explicitly — do not merely inherit them.
300
- // `ELANOUS_STATE_DIR` / `ELANOUS_CONFIG_DIR` are absolute paths that win over
301
- // HOME, so a developer who exports either one keeps pointing the "isolated"
302
- // run straight back at their real state while every other signal says the run
303
- // is contained. Deleting them is not enough either: absence resolves to the
304
- // default under HOME, which is fine here, but pinning them makes the
305
- // isolation legible in the child's own environment rather than implied.
299
+ // HOME and ELANOUS_STATE_DIR isolate both the state and default config roots.
300
+ // ELANOUS_CONFIG_DIR is not a config-dir override; discard an inherited value
301
+ // rather than passing an ignored variable that appears to provide isolation.
306
302
  env.ELANOUS_STATE_DIR = join(testRoot, 'state');
307
- env.ELANOUS_CONFIG_DIR = join(testRoot, 'config');
303
+ delete env.ELANOUS_CONFIG_DIR;
308
304
  // All ambient execution identities are removed by EXECUTION_ORIGIN_ENV_KEYS
309
305
  // above, before the child is spawned; logger API-shape tests need their absence.
310
306
  // ⚠️ No `ELANOUS_TEST_DETERMINISTIC` marker is exported. Nothing reads it yet,