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
@@ -30,6 +30,7 @@ nodes:
30
30
  - { node_id: npm-publish, kind: git, recipe: 'cmd:npm-publish', max_visits: 1 }
31
31
  - { node_id: docs-land, kind: git, recipe: 'cmd:docs-land', max_visits: 1 }
32
32
  - { node_id: verify, kind: gate, recipe: 'cmd:verify', max_visits: 1 }
33
+ - { node_id: vault-note, kind: gate, recipe: 'cmd:vault-note', max_visits: 1 }
33
34
  - { node_id: ops-upgrade, kind: agent, recipe: 'cmd:ops-upgrade', max_visits: 1 }
34
35
  - { node_id: version-dev-bump, kind: git, recipe: 'cmd:version-dev-bump', max_visits: 1 }
35
36
  - { node_id: done, kind: gate, max_visits: 1 }
@@ -54,6 +55,8 @@ edges:
54
55
  - { from: publish, on: outcome, map: { ok: npm-publish, fail: failed, error: failed } }
55
56
  - { from: npm-publish, on: outcome, map: { ok: docs-land, fail: docs-land, error: docs-land } }
56
57
  - { from: docs-land, on: outcome, map: { ok: verify, fail: failed, error: failed } }
57
- - { from: verify, on: outcome, map: { ok: ops-upgrade, fail: failed, error: failed } }
58
+ - { from: verify, on: outcome, map: { ok: vault-note, fail: failed, error: failed } }
59
+ # Warning-only: vault note failures after publication never block operational upgrades.
60
+ - { from: vault-note, on: outcome, map: { ok: ops-upgrade, fail: ops-upgrade, error: ops-upgrade } }
58
61
  - { from: ops-upgrade, on: outcome, map: { ok: version-dev-bump, fail: version-dev-bump, error: version-dev-bump } }
59
62
  - { from: version-dev-bump, on: outcome, map: { ok: done, fail: failed, error: failed } }
@@ -0,0 +1,38 @@
1
+ graph_id: rhythm-daily
2
+ loop:
3
+ title: 데일리 리뷰
4
+ description: 어제 착지·판·루프·그리드·결정·위험·외부 동향을 한 장으로 읽는다
5
+ trigger:
6
+ cron: '30 8 * * *'
7
+ version: 1
8
+ entry_node: collect
9
+ terminal_nodes: [done, failed]
10
+ nodes:
11
+ - node_id: collect
12
+ kind: agent
13
+ recipe: cmd:collect
14
+ max_visits: 1
15
+ - node_id: compose
16
+ kind: agent
17
+ recipe: cmd:compose
18
+ max_visits: 1
19
+ - node_id: deliver
20
+ kind: agent
21
+ recipe: cmd:deliver
22
+ max_visits: 1
23
+ - node_id: done
24
+ kind: gate
25
+ max_visits: 1
26
+ - node_id: failed
27
+ kind: gate
28
+ max_visits: 1
29
+ edges:
30
+ - from: collect
31
+ on: outcome
32
+ map: { ok: compose, fail: compose }
33
+ - from: compose
34
+ on: outcome
35
+ map: { ok: deliver, fail: failed }
36
+ - from: deliver
37
+ on: outcome
38
+ map: { ok: done, fail: failed }
@@ -0,0 +1,18 @@
1
+ collect:
2
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/daily.ts" collect --json'
3
+ timeout_ms: 400000
4
+ compose:
5
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/daily.ts" compose --json'
6
+ timeout_ms: 30000
7
+ deliver:
8
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/daily.ts" deliver --json'
9
+ timeout_ms: 30000
10
+ weekly-collect:
11
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/weekly.ts" collect --json'
12
+ timeout_ms: 400000
13
+ weekly-compose:
14
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/weekly.ts" compose --json'
15
+ timeout_ms: 30000
16
+ weekly-deliver:
17
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/rhythm/weekly.ts" deliver --json'
18
+ timeout_ms: 30000
@@ -0,0 +1,39 @@
1
+ graph_id: rhythm-weekly
2
+ loop:
3
+ title: 주간 미팅 자료
4
+ description: 지난주 성과·이번 주 판·현안·이행 대조·결정·외부 동향을 한 장으로 읽는다
5
+ trigger:
6
+ cron: '0 9 * * 1'
7
+ version: 1
8
+ entry_node: collect
9
+ terminal_nodes: [done, failed]
10
+ nodes:
11
+ - node_id: collect
12
+ kind: agent
13
+ recipe: cmd:weekly-collect
14
+ max_visits: 1
15
+ - node_id: compose
16
+ kind: agent
17
+ recipe: cmd:weekly-compose
18
+ max_visits: 1
19
+ - node_id: deliver
20
+ kind: agent
21
+ recipe: cmd:weekly-deliver
22
+ max_visits: 1
23
+ - node_id: done
24
+ kind: gate
25
+ max_visits: 1
26
+ - node_id: failed
27
+ kind: gate
28
+ max_visits: 1
29
+ edges:
30
+ # A failed collect must not let compose deliver an older file of the same week (review round 3).
31
+ - from: collect
32
+ on: outcome
33
+ map: { ok: compose, fail: failed }
34
+ - from: compose
35
+ on: outcome
36
+ map: { ok: deliver, fail: failed }
37
+ - from: deliver
38
+ on: outcome
39
+ map: { ok: done, fail: failed }
@@ -0,0 +1,3 @@
1
+ summary:
2
+ command: 'bun "$ELANOUS_GRAPH_DIR/../../scripts/video/youtube-summary.ts"'
3
+ timeout_ms: 120000
@@ -0,0 +1,17 @@
1
+ graph_id: youtube-summary
2
+ version: 1
3
+ loop:
4
+ title: Built-in YouTube summary
5
+ description: Turn video captions (or STT) into a local summary or study note; no publishing or external skill.
6
+ exec_request: true
7
+ inputs: [url, format]
8
+ trigger:
9
+ events: [manual]
10
+ entry_node: summary
11
+ terminal_nodes: [done, failed]
12
+ nodes:
13
+ - { node_id: summary, kind: agent, recipe: 'cmd:summary', max_visits: 1 }
14
+ - { node_id: done, kind: gate, max_visits: 1 }
15
+ - { node_id: failed, kind: gate, max_visits: 1 }
16
+ edges:
17
+ - { from: summary, on: outcome, map: { ok: done, no-transcript: done, invalid-video-id: failed, fail: failed } }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elanous",
3
3
  "mcpName": "io.github.ElanvitalAI/elanous",
4
- "version": "0.2.13",
4
+ "version": "0.2.14",
5
5
  "description": "Self-healing coding harness where eyes, hands, and memory all turn on one sentence — it authors its own goals, implements them in isolated worktrees, and reviews its own work.",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Elanvital AI",
@@ -34,18 +34,13 @@ const statePattern = /(?:\b(?:homedir|HOME|ELANOUS_(?:STATE|CONFIG|NEXUS)_DIR|EL
34
34
  export type StaticIsolationSignal = 'ELANOUS_STATE_DIR' | '--config-dir' | 'mkdtemp';
35
35
  export type StaticSafetyClassification = 'isolated' | 'manual-review';
36
36
 
37
- const inspectedManualReviews: Record<string, string> = {
38
- 'src/autopilot/build/config-isolation.test.ts': 'setElanousConfigDir redirects the tested config into an isolated worktree path; SHA assertions prove the real config is unchanged.',
39
- 'test/artifact-store.test.ts': 'the apparent writes target an in-memory ArtifactFs fake, not the host filesystem.',
40
- 'test/pty-drive-workdir-decision.test.ts': 'setElanousConfigDir receives only /tmp paths and the afterEach reset restores the override.',
41
- 'test/working-dir.test.ts': 'the fixed test root is derived from tmpdir() and removed by afterAll.',
42
- };
43
-
44
37
  export type WriterCall = {
45
38
  line: number;
46
39
  pattern: string;
47
40
  staticIsolationSignals: StaticIsolationSignal[];
48
41
  staticSafety: StaticSafetyClassification;
42
+ /** The expanded target names the real home (homedir(), $HOME, ~/) — the only writer calls that fail the audit. */
43
+ realHomeTarget: boolean;
49
44
  };
50
45
 
51
46
  type AuditFinding = {
@@ -123,8 +118,10 @@ export function classifyStaticIsolation(text: string): { signals: StaticIsolatio
123
118
  return { signals, safety: signals.length ? 'isolated' : 'manual-review' };
124
119
  }
125
120
 
126
- function writerIsolation(source: ts.SourceFile, writer: ts.CallExpression | ts.NewExpression, name: string): { signals: StaticIsolationSignal[]; safety: StaticSafetyClassification } {
127
- if (name === 'mkdtemp' || name === 'mkdtempSync') return { signals: ['mkdtemp'], safety: 'isolated' };
121
+ const REAL_HOME_TARGET = /\bhomedir\s*\(|process\.env\.HOME\b|process\.env\[\s*['"]HOME['"]\s*\]|['"`]~\//;
122
+
123
+ function writerIsolation(source: ts.SourceFile, writer: ts.CallExpression | ts.NewExpression, name: string): { signals: StaticIsolationSignal[]; safety: StaticSafetyClassification; realHomeTarget: boolean } {
124
+ if (name === 'mkdtemp' || name === 'mkdtempSync') return { signals: ['mkdtemp'], safety: 'isolated', realHomeTarget: false };
128
125
  const variables = new Map<string, string>();
129
126
  const visit = (node: ts.Node): void => {
130
127
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) variables.set(node.name.text, node.initializer.getText(source));
@@ -138,7 +135,40 @@ function writerIsolation(source: ts.SourceFile, writer: ts.CallExpression | ts.N
138
135
  return `(${expand(variables.get(name)!)})`;
139
136
  });
140
137
  const argumentsText = writer.arguments?.map((argument) => argument.getText(source)).join(' ') ?? '';
141
- return classifyStaticIsolation(expand(argumentsText));
138
+ return { ...classifyStaticIsolation(expand(argumentsText)), realHomeTarget: realHomeTarget(source, writer) };
139
+ }
140
+
141
+ /** Lexically nearest declaration of `name` visible before `at` (same file-wide name reused across tests must not leak). */
142
+ function scopedInitializer(source: ts.SourceFile, at: ts.Node, name: string): ts.Expression | undefined {
143
+ for (let scope: ts.Node | undefined = at.parent; scope; scope = scope.parent) {
144
+ if (!ts.isBlock(scope) && !ts.isSourceFile(scope) && !ts.isModuleBlock(scope)) continue;
145
+ let found: ts.Expression | undefined;
146
+ for (const statement of scope.statements) {
147
+ if (statement.pos >= at.pos) break;
148
+ if (!ts.isVariableStatement(statement)) continue;
149
+ for (const declaration of statement.declarationList.declarations) {
150
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer) found = declaration.initializer;
151
+ }
152
+ }
153
+ if (found) return found;
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ /** Only the target path (first argument) counts — written content may mention homedir() as fixture text. */
159
+ function realHomeTarget(source: ts.SourceFile, writer: ts.CallExpression | ts.NewExpression): boolean {
160
+ const target = writer.arguments?.[0];
161
+ if (!target) return false;
162
+ const seen = new Set<string>();
163
+ const expand = (node: ts.Node, text: string): string => text.replace(/\b[A-Za-z_$][\w$]*\b/g, (name) => {
164
+ if (seen.has(name)) return name;
165
+ const initializer = scopedInitializer(source, node, name);
166
+ if (!initializer) return name;
167
+ seen.add(name);
168
+ return `(${expand(initializer, initializer.getText(source))})`;
169
+ });
170
+ const expanded = expand(target, target.getText(source));
171
+ return REAL_HOME_TARGET.test(expanded) && classifyStaticIsolation(expanded).safety !== 'isolated';
142
172
  }
143
173
 
144
174
  export type CandidateClassification = {
@@ -158,18 +188,30 @@ export function addedCandidates(report: ReturnType<typeof auditTestStateWrites>,
158
188
  return new Set(report.findings.map(({ file }) => file).filter((file) => !previousInventory.has(file)));
159
189
  }
160
190
 
191
+ /**
192
+ * Fails only writer calls whose target reaches the real home without isolation evidence. A static scan cannot
193
+ * follow a temp directory through function returns or parameters, so "no evidence" alone stays visible as
194
+ * manual review in the rendered inventory instead of failing hundreds of mkdtemp-backed writes.
195
+ */
196
+ export function unclassifiedCandidates(report: ReturnType<typeof auditTestStateWrites>, previousInventory: ReadonlySet<string>): string[] {
197
+ const { isolated, manualReview } = classifyCandidates(report, addedCandidates(report, previousInventory));
198
+ // The file-wide isolation label can be borrowed from a same-named temp variable in another test, so the
199
+ // scoped real-HOME judgement decides on every call, including ones labelled isolated.
200
+ return [...isolated, ...manualReview].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
201
+ .filter(({ realHomeTarget }) => realHomeTarget)
202
+ .map(({ file, line, pattern }) => `${file}:${line} ${pattern} — writes a real HOME path without isolation evidence`);
203
+ }
204
+
161
205
  function classificationSummary(classification: CandidateClassification): string {
162
206
  const { findings, isolated, manualReview } = classification;
163
- const isolatedFiles = findings.filter(({ staticSafety }) => staticSafety === 'isolated');
164
207
  const manualReviewFiles = findings.filter(({ staticSafety }) => staticSafety === 'manual-review');
165
208
  return [
166
209
  `- August 8 candidate window: **${findings.length} files** (current inventory minus the preserved August 8 inventory).`,
167
- `- Candidate classification (files): **${isolatedFiles.length} ㉠ isolated** / **${manualReviewFiles.length} ㉡ manual review**.`,
168
- `- ㉡ candidate files: ${manualReviewFiles.length ? manualReviewFiles.map(({ file }) => `\`${file}\``).join(', ') : '0건'}.`,
210
+ `- Candidate classification (files): **${findings.length - manualReviewFiles.length} ㉠ isolated** / **${manualReviewFiles.length} ㉡ manual review**.`,
169
211
  `- Writer-call evidence in the August 8 window: **${isolated.length} isolated** / **${manualReview.length} manual review**; a file is ㉡ whenever any writer call lacks a direct approved signal.`,
170
212
  `- ㉡ writer calls: ${manualReview.length ? manualReview.map(({ file, line, pattern }) => `\`${file}:${line} ${pattern}\``).join(', ') : '0건'}.`,
171
- `- Manual inspection opened: **${manualReviewFiles.length}**; genuine HOME/state writers: **0건**.`,
172
- ...manualReviewFiles.map(({ file }) => `- Manual inspection — \`${file}\`: ${inspectedManualReviews[file] ?? 'opened; no genuine HOME/state write established.'}`),
213
+ `- Manual review required: **${manualReviewFiles.length}** candidate files. This static audit does not establish whether an unclassified write targets a real HOME/state path.`,
214
+ ...manualReviewFiles.map(({ file }) => `- Manual review pending — \`${file}\`: isolation is unverified.`),
173
215
  ].join('\n');
174
216
  }
175
217
 
@@ -198,7 +240,7 @@ export function auditTestStateWrites(root = repo): { total: number; findings: Au
198
240
  visit(source);
199
241
  const writerCalls = writerNodes.map(({ name, node }) => {
200
242
  const classification = writerIsolation(source, node, name);
201
- return { line: lineOf(source, node), pattern: formatWriterPattern(name, node, source), staticIsolationSignals: classification.signals, staticSafety: classification.safety };
243
+ return { line: lineOf(source, node), pattern: formatWriterPattern(name, node, source), staticIsolationSignals: classification.signals, staticSafety: classification.safety, realHomeTarget: classification.realHomeTarget };
202
244
  });
203
245
  const stateLines = text.split('\n').flatMap((line, index) => statePattern.test(line) ? [index + 1] : []);
204
246
  if (writerCalls.length && stateLines.length) {
@@ -221,8 +263,9 @@ export function auditTestStateWrites(root = repo): { total: number; findings: Au
221
263
  return { total: files.length, findings };
222
264
  }
223
265
 
224
- export function renderAudit(report: ReturnType<typeof auditTestStateWrites>, previous = august8Inventory(repo)): string {
266
+ export function renderAudit(report: ReturnType<typeof auditTestStateWrites>, previous = august8Inventory(repo), reviewed = new Set(readFileSync(baselinePath, 'utf8').split('\n').filter((line) => line && !line.startsWith('#')))): string {
225
267
  const candidateWindow = classifyCandidates(report, addedCandidates(report, previous));
268
+ const automatic = classifyCandidates(report, addedCandidates(report, reviewed));
226
269
  const rows = report.findings.map((finding) =>
227
270
  `| \`${finding.file}\` | ${finding.writerLines.join(', ')} | ${finding.writerTargets.map((target) => `\`${target}\``).join('<br>')} | ${finding.stateLines.join(', ')} | ${finding.risk} | ${finding.isolation} | ${finding.staticSafety} (${finding.staticIsolationSignals.join(', ') || 'none'}) |`,
228
271
  );
@@ -230,6 +273,7 @@ export function renderAudit(report: ReturnType<typeof auditTestStateWrites>, pre
230
273
  '# Test Home and State Write Audit',
231
274
  '',
232
275
  'This inventory is generated by `scripts/audit-test-state-writes.ts` using TypeScript AST call detection for filesystem/state writer calls and text detection for home/state-root evidence. It covers every `*.test.*` and `*.spec.*` file under `test/` and `src/`.',
276
+ `Rows for files outside the reviewed baseline are a snapshot from generation time; refresh with \`${INVENTORY_REFRESH_COMMAND}\`.`,
233
277
  '',
234
278
  `- Scanned test files: **${report.total}**`,
235
279
  `- Potential home/state writers: **${report.findings.length}**`,
@@ -238,6 +282,17 @@ export function renderAudit(report: ReturnType<typeof auditTestStateWrites>, pre
238
282
  '- Static safety classification is approval-gating: only `ELANOUS_STATE_DIR`, `--config-dir`, or `mkdtemp` counts as isolated; all other candidates require manual inspection.',
239
283
  classificationSummary(candidateWindow),
240
284
  '',
285
+ '## 분류기 통과(자동) — 사람 검토 기준선 밖',
286
+ '',
287
+ `Reviewed baseline: \`test/test-home-state-write-audit-baseline.txt\`. Regenerate with \`${INVENTORY_REFRESH_COMMAND}\`. Only candidates with every writer call statically isolated appear below; manual-review calls fail the audits rather than expanding the baseline.`,
288
+ '',
289
+ '| File | Writer lines | Approved isolation signals |',
290
+ '| --- | --- | --- |',
291
+ ...automatic.findings.filter(({ staticSafety }) => staticSafety === 'isolated').map(({ file, writerLines, staticIsolationSignals }) =>
292
+ `| \`${file}\` | ${writerLines.join(', ')} | ${staticIsolationSignals.join(', ')} |`),
293
+ '',
294
+ '## 전체 스캔 (사람 검토 + 자동 분류)',
295
+ '',
241
296
  '| File | Writer call lines (AST) | Write target or store argument | Home/state evidence lines (text) | Risk | Current isolation evidence | Static safety |',
242
297
  '| --- | --- | --- | --- | --- | --- | --- |',
243
298
  ...rows,
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { extname, join, resolve } from 'node:path';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { debug } from '../../src/debug/log.js';
6
+
7
+ type Scope = 'public-docs' | 'release-notes' | 'site' | 'deck' | 'skill' | 'any';
8
+ type Kind = 'forbid' | 'require-form' | 'korean-in-english' | 'number-needs-source';
9
+ type Rule = { id: string; kind: Kind; pattern: string; flags?: string; scope: Scope[]; why: string; source: string; allowInCode?: boolean; allowPattern?: string; sourcePattern?: string };
10
+ type Finding = { file: string; line: number; id: string; match: string };
11
+ type GlossaryEntry = { internal: string; public: string; publicForbidden: boolean };
12
+ const scopes: Scope[] = ['public-docs', 'release-notes', 'site', 'deck', 'skill', 'any'];
13
+ const kinds: Kind[] = ['forbid', 'require-form', 'korean-in-english', 'number-needs-source'];
14
+ const defaultRules = join(import.meta.dir, '../../docs/brand/brand-rules.yaml');
15
+
16
+ function loadRules(path: string): Rule[] {
17
+ const document = parseYaml(readFileSync(path, 'utf8')) as unknown;
18
+ if (!document || typeof document !== 'object' || !('rules' in document) || !Array.isArray(document.rules)) throw new Error(`invalid brand rules: ${path}`);
19
+ const glossary = 'glossary' in document ? document.glossary : [];
20
+ if (!Array.isArray(glossary) || !glossary.every((entry: unknown) => entry && typeof entry === 'object'
21
+ && typeof (entry as GlossaryEntry).internal === 'string' && typeof (entry as GlossaryEntry).public === 'string'
22
+ && typeof (entry as GlossaryEntry).publicForbidden === 'boolean')) throw new Error(`invalid brand glossary: ${path}`);
23
+ const ids = new Set<string>();
24
+ const rules: Rule[] = document.rules.map((candidate: unknown) => {
25
+ if (!candidate || typeof candidate !== 'object') throw new Error(`invalid brand rule: ${path}`);
26
+ const rule = candidate as Partial<Rule>;
27
+ if (typeof rule.id !== 'string' || !rule.id || ids.has(rule.id) || !kinds.includes(rule.kind as Kind)
28
+ || typeof rule.pattern !== 'string' || !rule.pattern || typeof rule.why !== 'string' || !rule.why
29
+ || typeof rule.source !== 'string' || !rule.source || !Array.isArray(rule.scope) || !rule.scope.length
30
+ || !rule.scope.every((scope) => scopes.includes(scope)) || (rule.flags !== undefined && typeof rule.flags !== 'string')
31
+ || (rule.allowInCode !== undefined && typeof rule.allowInCode !== 'boolean')
32
+ || (rule.allowPattern !== undefined && typeof rule.allowPattern !== 'string')
33
+ || (rule.kind === 'number-needs-source' && (typeof rule.sourcePattern !== 'string' || !rule.sourcePattern))) throw new Error(`invalid brand rule: ${path}`);
34
+ new RegExp(rule.pattern, rule.flags);
35
+ if (rule.sourcePattern) new RegExp(rule.sourcePattern, rule.flags);
36
+ if (rule.allowPattern) new RegExp(rule.allowPattern, rule.flags);
37
+ ids.add(rule.id);
38
+ return rule as Rule;
39
+ });
40
+ for (const entry of glossary as GlossaryEntry[]) {
41
+ if (!entry.publicForbidden) continue;
42
+ const id = `GLOSSARY-${entry.internal}`;
43
+ if (!entry.internal || ids.has(id)) throw new Error(`invalid brand glossary: ${path}`);
44
+ ids.add(id);
45
+ const literal = entry.internal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+ const pattern = `(?<![가-힣])${literal}(?![가-힣])`;
47
+ rules.push({ id, kind: 'forbid', pattern, scope: ['any'], why: `Use ${entry.public} instead of the internal term.`, source: `${path}#glossary` });
48
+ }
49
+ return rules;
50
+ }
51
+
52
+ function filesAt(paths: string[]): string[] {
53
+ const files = new Set<string>();
54
+ const visit = (path: string): void => {
55
+ const stat = statSync(path);
56
+ if (stat.isDirectory()) {
57
+ for (const entry of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
58
+ if (entry.isDirectory() || entry.isFile()) visit(join(path, entry.name));
59
+ }
60
+ } else if (stat.isFile()) files.add(path);
61
+ };
62
+ for (const path of paths) visit(resolve(path));
63
+ return [...files].sort();
64
+ }
65
+
66
+ function maskUiStrings(line: string): string {
67
+ const ui = /^\s*(?:>\s*`?\s*)?(?:(?:const|let|var)\s+)?(?:label|title|button|placeholder|ariaLabel|aria-label|tooltip|caption|uiText|uiMessage)\s*[:=]\s*(["'`])([^"'`\n]*[가-힣][^"'`\n]*)\1/;
68
+ const chars = line.split('');
69
+ const match = ui.exec(line);
70
+ if (match) {
71
+ const value = match[2]!;
72
+ const start = match.index + match[0].indexOf(value);
73
+ for (let i = start; i < start + value.length; i++) chars[i] = ' ';
74
+ }
75
+ return chars.join('');
76
+ }
77
+
78
+ function htmlVisible(content: string): string {
79
+ const blank = (text: string) => text.replace(/[^\r\n]/g, ' ');
80
+ return content.replace(/<!--[\s\S]*?-->|<(?:script|style)\b[^>]*>[\s\S]*?<\/\s*(?:script|style)\s*>/gi, blank)
81
+ .replace(/<(?:[^>"']|"[^"]*"|'[^']*')*>/g, (tag) => {
82
+ if (!/^<img\b/i.test(tag)) return blank(tag);
83
+ const alt = /\balt\s*=\s*(["'])(.*?)\1/is.exec(tag);
84
+ if (!alt) return blank(tag);
85
+ const offset = alt.index + alt[0].indexOf(alt[2]!);
86
+ return blank(tag.slice(0, offset)) + alt[2] + blank(tag.slice(offset + alt[2].length));
87
+ });
88
+ }
89
+
90
+ function isEnglishDocument(content: string, file: string, scope: Scope): boolean {
91
+ const html = ['.html', '.htm'].includes(extname(file).toLowerCase());
92
+ const frontmatter = /^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
93
+ const language = html
94
+ ? /<html\b[^>]*\blang\s*=\s*["']?([a-z]{2,}(?:-[a-z]+)*)/i.exec(content)?.[1]
95
+ ?? /<meta\b[^>]*\b(?:http-equiv\s*=\s*["']?content-language|name\s*=\s*["']?language)[^>]*\bcontent\s*=\s*["']?([a-z]{2,})/i.exec(content)?.[1]
96
+ : frontmatter ? /^lang(?:uage)?\s*:\s*["']?([a-z]{2,}(?:-[a-z]+)*)/im.exec(frontmatter)?.[1] : undefined;
97
+ if (language) return /^en(?:-|$)/i.test(language);
98
+ // Language-suffixed filenames are an explicit declaration, even without frontmatter.
99
+ if (/(?:^|[._-])(?:en|ko)(?:-[a-z]+)?\.[^.]+$/i.test(file)) return /(?:^|[._-])en(?:-[a-z]+)?\.[^.]+$/i.test(file);
100
+ if (scope !== 'site') return false;
101
+ const body = (html ? htmlVisible(content) : content.replace(/^---\s*\r?\n[\s\S]*?\r?\n---/, ''))
102
+ .replace(/\bElanous\b/gi, '');
103
+ return /\b[A-Za-z]{2,}(?:\s+[A-Za-z]{2,}){2,}\b/.test(body);
104
+ }
105
+
106
+ function findInFile(file: string, rules: Rule[], scope: Scope): Finding[] {
107
+ const findings: Finding[] = [];
108
+ const content = readFileSync(file, 'utf8');
109
+ const html = ['.html', '.htm'].includes(extname(file).toLowerCase());
110
+ const englishDocument = isEnglishDocument(content, file, scope);
111
+ const markdown = ['.md', '.mdx'].includes(extname(file).toLowerCase());
112
+ const bodyLines = html ? htmlVisible(content).split(/\r?\n/) : [];
113
+ let fence: { marker: string; length: number } | undefined;
114
+ const allLines = content.split(/\r?\n/);
115
+ for (const [index, line] of allLines.entries()) {
116
+ const lineFindings: Finding[] = [];
117
+ const marker = markdown ? /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1] : undefined;
118
+ if (marker) {
119
+ if (!fence) fence = { marker: marker[0]!, length: marker.length };
120
+ else if (marker[0] === fence.marker && marker.length >= fence.length) fence = undefined;
121
+ }
122
+ const quoted = markdown && /^\s*>/.test(line);
123
+ const inCode = Boolean(fence || marker || quoted);
124
+ for (const rule of rules) {
125
+ if (rule.kind === 'korean-in-english' && !englishDocument) continue;
126
+ const text = rule.kind === 'korean-in-english' && html ? bodyLines[index]! : line;
127
+ const visible = rule.allowInCode && inCode ? maskUiStrings(text) : text;
128
+ const allowed = rule.allowPattern ? [...line.matchAll(new RegExp(rule.allowPattern, [...new Set(`${rule.flags ?? ''}g`)].join('')))] : [];
129
+ const matches = visible.matchAll(new RegExp(rule.pattern, [...new Set(`${rule.flags ?? ''}g`)].join('')));
130
+ // B4: a measured number must carry its source marker on the same line or a neighbouring one.
131
+ const sourced = rule.kind === 'number-needs-source'
132
+ && [index - 1, index, index + 1].some((i) => i >= 0 && i < allLines.length && new RegExp(rule.sourcePattern!, rule.flags).test(allLines[i]!));
133
+ for (const match of matches) {
134
+ if (sourced) break;
135
+ if (allowed.some((exception) => exception.index! <= match.index! && match.index! < exception.index! + exception[0].length)) continue;
136
+ lineFindings.push({ file, line: index + 1, id: rule.id, match: match[0] });
137
+ if (rule.kind === 'korean-in-english') break;
138
+ }
139
+ }
140
+ findings.push(...lineFindings.filter((finding) => finding.id !== 'B11'
141
+ || !lineFindings.some((other) => other.id === 'BRAND-AUTONOMY' && other.match.includes(finding.match))));
142
+ }
143
+ return findings;
144
+ }
145
+
146
+ export function checkBrand(scope: Scope, paths: string[], rulesPath = defaultRules): { files: number; findings: Finding[]; rules: number; missing: boolean } {
147
+ if (!existsSync(rulesPath)) {
148
+ debug.log('brand.check', 'run', { scope, files: 0, findings: 0, rules: 0 });
149
+ return { files: 0, findings: [], rules: 0, missing: true };
150
+ }
151
+ const applicable = loadRules(rulesPath).filter((rule) => scope === 'any' || rule.scope.includes('any') || rule.scope.includes(scope));
152
+ const files = filesAt(paths);
153
+ const findings = files.flatMap((file) => findInFile(file, applicable, scope));
154
+ debug.log('brand.check', 'run', { scope, files: files.length, findings: findings.length, rules: applicable.length });
155
+ return { files: files.length, findings, rules: applicable.length, missing: false };
156
+ }
157
+
158
+ if (import.meta.main) {
159
+ try {
160
+ const args = process.argv.slice(2);
161
+ let scope: Scope | undefined;
162
+ let rulesPath = defaultRules;
163
+ let json = false;
164
+ const paths: string[] = [];
165
+ for (let i = 0; i < args.length; i++) {
166
+ const arg = args[i]!;
167
+ if (arg === '--scope') scope = args[++i] as Scope;
168
+ else if (arg === '--rules') rulesPath = args[++i] ?? '';
169
+ else if (arg === '--json') json = true;
170
+ else if (arg.startsWith('--')) throw new Error(`unknown option: ${arg}`);
171
+ else paths.push(arg);
172
+ }
173
+ if (!scope || !scopes.includes(scope) || !paths.length || !rulesPath) throw new Error('usage: bun scripts/brand/check.ts --scope <scope> <file|folder...> [--rules <path>] [--json]');
174
+ const result = checkBrand(scope, paths, rulesPath);
175
+ if (result.missing) console.log(json ? JSON.stringify({ ...result, message: '규칙 없음' }) : '규칙 없음');
176
+ else if (json) console.log(JSON.stringify({ scope, ...result }));
177
+ else {
178
+ for (const finding of result.findings) console.log(`${finding.file}:${finding.line}:${finding.id}:${finding.match}`);
179
+ console.log(`brand-check ${scope} files=${result.files} findings=${result.findings.length} rules=${result.rules}`);
180
+ }
181
+ if (result.findings.length) process.exitCode = 1;
182
+ } catch (error) {
183
+ console.error(error instanceof Error ? error.message : String(error));
184
+ process.exitCode = 2;
185
+ }
186
+ }
@@ -220,7 +220,17 @@ fi
220
220
  echo "[coord-post] 발신 성공$VIA · 채널 #$PR · 신원 [$ID] · 시각 $TS" >&2
221
221
  # Only a successfully sent post is recorded; never pass the body or later lines to the ledger.
222
222
  POST_URL=$(printf '%s\n' "$POST_OUTPUT" | grep -oE 'https://github\.com/[^[:space:]]+/issues/[0-9]+#issuecomment-[0-9]+|https://github\.com/[^[:space:]]+/pull/[0-9]+#issuecomment-[0-9]+' | head -1 || true)
223
- if ! bun bin/elanous.mjs coord event record --seat "$ID" --header "$(head -1 "$TMP")" --url "$POST_URL" >/dev/null 2>&1; then
223
+ # The channel is a production surface, so its record belongs in the production ledger, not this tree's
224
+ # test universe (OP 10-04 18:00). An explicit ELANOUS_STATE_DIR is the caller's chosen universe and wins;
225
+ # otherwise prefer the installed CLI (resolves prod from any tree), else pin the source CLI to prod.
226
+ if [ -n "${ELANOUS_STATE_DIR:-}" ]; then
227
+ RECORD=(bun bin/elanous.mjs coord event record)
228
+ elif command -v eln >/dev/null 2>&1; then
229
+ RECORD=(eln coord event record)
230
+ else
231
+ RECORD=(env ELANOUS_STATE_DIR="$HOME/.elanous" bun bin/elanous.mjs --config-dir "$HOME/.elanous" coord event record)
232
+ fi
233
+ if ! "${RECORD[@]}" --seat "$ID" --header "$(head -1 "$TMP")" --url "$POST_URL" >/dev/null 2>&1; then
224
234
  echo '⚠️ 맥락 원장 기록 실패(발신은 됐다)' >&2
225
235
  fi
226
236
  exit 0
@@ -2,7 +2,7 @@
2
2
  // 공개 문서에 적힌 `elanous …` 명령·하위 명령·플래그가 «실제 CLI» 에 있나 — 문서가 실제와 어긋나는 1순위 원인을 잡는다.
3
3
  // bun scripts/docs-cli-check.ts [--json] [files…] (기본 = release/public/내부 문서 `*` ⊕ README.md)
4
4
  // 🩸 계기(2026-09-25 🅢 RFC 피드백 ③): README·install.md 가 거짓이 된 원인이 전부 «설치기·doctor 가 바뀌었는데 문서가 모름»이었다.
5
- // 자 = `bun bin/elanous.mjs <cmd> [<sub>] --help` 산출(원천) — 문서의 코드 블록과 인라인 코드에서 `elanous ` 로 시작하는 것만 본다.
5
+ // 자 = `bun bin/elanous.mjs <cmd> [<sub>] [<subsub>] --help` 산출(원천) — 문서의 코드 블록과 인라인 코드에서 `elanous ` 로 시작하는 것만 본다.
6
6
  // ⛔ 못 본 것은 «없다»로 적지 않는다: `--help` 가 실패하면 그 명령은 `unmeasured` 로 따로 센다.
7
7
  import { readdirSync, readFileSync, statSync } from 'node:fs';
8
8
  import { join, relative, resolve } from 'node:path';
@@ -10,7 +10,7 @@ import { spawnSync } from 'node:child_process';
10
10
 
11
11
  const REPO = resolve(import.meta.dir, '..');
12
12
 
13
- export interface DocCommand { file: string; line: number; text: string; cmd: string; sub?: string; flags: string[] }
13
+ export interface DocCommand { file: string; line: number; text: string; cmd: string; sub?: string; subsub?: string; flags: string[] }
14
14
  export interface Finding { kind: 'unknown-command' | 'unknown-subcommand' | 'unknown-flag' | 'unmeasured'; ref: DocCommand; detail: string }
15
15
 
16
16
  /** 코드 블록(``` … ```) 줄과 인라인 코드(`elanous …`)에서 elanous 호출을 뽑는다. 자리표(`<x>`)·셸 변수는 인자로 보고 넘긴다. */
@@ -24,15 +24,17 @@ export function extractElanousCommands(file: string, text: string): DocCommand[]
24
24
  for (const m of raw.matchAll(/`(elanous [^`]+)`/g)) candidates.push(m[1]!);
25
25
  for (const c of candidates) {
26
26
  for (const seg of c.split(/&&|\|\||;|\|/)) {
27
- const m = /(?:^|\s)elanous\s+(.+)$/.exec(seg.trim());
27
+ const m = /^(?:(?:\$\s+|>\s+|sudo\s+|[A-Za-z_][A-Za-z0-9_]*=\S+\s+))*elanous\s+(.+)$/.exec(seg.trim());
28
28
  if (!m) continue;
29
29
  const tokens = m[1]!.trim().split(/\s+/).filter(Boolean);
30
30
  const cmd = tokens[0];
31
31
  if (!cmd || !/^[a-z][a-z0-9:-]*$/.test(cmd)) continue;
32
32
  const second = tokens[1];
33
33
  const sub = second && /^[a-z][a-z0-9-]*$/.test(second) ? second : undefined;
34
+ const third = tokens[2];
35
+ const subsub = sub && third && /^[a-z][a-z0-9-]*$/.test(third) ? third : undefined;
34
36
  const flags = tokens.filter((t) => /^--[a-z]/.test(t)).map((t) => t.replace(/[=,.)].*$/, ''));
35
- out.push({ file, line: i + 1, text: seg.trim(), cmd, ...(sub ? { sub } : {}), flags });
37
+ out.push({ file, line: i + 1, text: seg.trim(), cmd, ...(sub ? { sub } : {}), ...(subsub ? { subsub } : {}), flags });
36
38
  }
37
39
  }
38
40
  });
@@ -63,13 +65,28 @@ export function checkCommands(refs: readonly DocCommand[], help: HelpRunner = de
63
65
  const cmdHelp = get([ref.cmd]);
64
66
  if (!cmdHelp.ok) { findings.push({ kind: 'unmeasured', ref, detail: `elanous ${ref.cmd} --help 실패` }); continue; }
65
67
  let flagsHelp = cmdHelp.out;
68
+ let flagsPath = `elanous ${ref.cmd}`;
66
69
  if (ref.sub && /\nCommands:\n/.test(cmdHelp.out)) {
67
70
  if (!listed(cmdHelp.out, ref.sub)) { findings.push({ kind: 'unknown-subcommand', ref, detail: `elanous ${ref.cmd} ${ref.sub}` }); continue; }
71
+ flagsPath = `elanous ${ref.cmd} ${ref.sub}`;
68
72
  const subHelp = get([ref.cmd, ref.sub]);
69
- if (subHelp.ok) flagsHelp = subHelp.out;
73
+ if (!subHelp.ok) {
74
+ findings.push({ kind: 'unmeasured', ref, detail: `${flagsPath} --help 실패` });
75
+ continue;
76
+ }
77
+ flagsHelp = subHelp.out;
78
+ if (ref.subsub && listed(subHelp.out, ref.subsub)) {
79
+ flagsPath += ` ${ref.subsub}`;
80
+ const subsubHelp = get([ref.cmd, ref.sub, ref.subsub]);
81
+ if (!subsubHelp.ok) {
82
+ findings.push({ kind: 'unmeasured', ref, detail: `${flagsPath} --help 실패` });
83
+ continue;
84
+ }
85
+ flagsHelp = subsubHelp.out;
86
+ }
70
87
  }
71
88
  for (const f of ref.flags) {
72
- if (!new RegExp(`(^|[\\s,])${f}(\\b|[\\s,=<\\[])`, 'm').test(flagsHelp)) findings.push({ kind: 'unknown-flag', ref, detail: `${f} (elanous ${ref.cmd}${ref.sub ? ` ${ref.sub}` : ''})` });
89
+ if (!new RegExp(`(^|[\\s,])${f}(\\b|[\\s,=<\\[])`, 'm').test(flagsHelp)) findings.push({ kind: 'unknown-flag', ref, detail: `${f} (${flagsPath})` });
73
90
  }
74
91
  }
75
92
  return findings;