elanous 0.2.12 → 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 (534) 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/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  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-1abfe3aec3be6bdc.js +1 -0
  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-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  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-793fa536b3a6ef37.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-e1e47d8d940aba3d.js +1 -0
  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-e297866051595d81.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/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,129 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { constants, closeSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, writeSync } from 'node:fs';
3
+ import { basename, dirname, join, parse, resolve } from 'node:path';
4
+ import { dlopen, FFIType } from 'bun:ffi';
5
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
6
+
7
+ /** Bench samples live in the instance's private state, never in a repository or card response. */
8
+ export function wishBenchmarkLedgerPath(root: string = elanousStateRoot()): string {
9
+ // OP D 길 원장(채널 10-04 22:48)과 같은 파일·같은 줄 꼴 — OP 가 손으로 더하는 줄과 섞여 한 표본이 된다.
10
+ return join(root, 'bench', 'goal-bench-requirements.jsonl');
11
+ }
12
+
13
+ function privateDirectory(path: string, privateMode: boolean): void {
14
+ try { mkdirSync(path, { mode: 0o700 }); }
15
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; }
16
+ const state = lstatSync(path);
17
+ if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(`Unsafe wish benchmark directory: ${path}`);
18
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
19
+ try {
20
+ if (privateMode) fchmodSync(fd, 0o700);
21
+ const opened = fstatSync(fd);
22
+ if (opened.dev !== state.dev || opened.ino !== state.ino) {
23
+ throw new Error(`Wish benchmark directory changed while opening: ${path}`);
24
+ }
25
+ } finally { closeSync(fd); }
26
+ }
27
+
28
+ function realExistingPrefix(path: string): string {
29
+ let head = path; const tail: string[] = [];
30
+ for (;;) {
31
+ try { return join(realpathSync(head), ...tail.reverse()); }
32
+ catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
34
+ const parent = dirname(head); if (parent === head) return path;
35
+ tail.push(basename(head)); head = parent;
36
+ }
37
+ }
38
+ }
39
+
40
+ function preparePrivateDirectory(root: string): string {
41
+ // 맥에서 `/var` 는 `/private/var` 링크다 — 이미 있는 상위 경로는 실제 경로로 풀고, 새로 만들 아래 칸만 링크를 막는다.
42
+ const absolute = realExistingPrefix(resolve(root));
43
+ let current = parse(absolute).root;
44
+ const parts = absolute.slice(current.length).split(/[\\/]/).filter(Boolean);
45
+ for (const part of parts) {
46
+ current = join(current, part);
47
+ // A sample cannot be written inside a source checkout, even if the state root was overridden.
48
+ try {
49
+ lstatSync(join(current, '.git'));
50
+ throw new Error(`Wish benchmark state root is inside a repository: ${current}`);
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
53
+ }
54
+ privateDirectory(current, false);
55
+ }
56
+ for (const part of ['bench']) {
57
+ current = join(current, part);
58
+ privateDirectory(current, true);
59
+ }
60
+ return current;
61
+ }
62
+
63
+ // Advisory locks belong to the open descriptor, not the pathname: SIGKILL closes it automatically.
64
+ // Keep the lock file itself in place so no competing writer can lock a different inode.
65
+ const libc = dlopen(process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : 'libc.so.6', {
66
+ flock: { args: [FFIType.int, FFIType.int], returns: FFIType.int },
67
+ });
68
+
69
+ function withLedgerLock(dir: string, action: () => void): void {
70
+ const lock = join(dir, '.wish-cards.lock');
71
+ const fd = openSync(lock, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
72
+ try {
73
+ const opened = fstatSync(fd);
74
+ if (!opened.isFile() || opened.nlink !== 1) throw new Error('Unsafe wish benchmark lock');
75
+ fchmodSync(fd, 0o600);
76
+ // LOCK_EX blocks rather than stealing a live writer's lock. Kernel releases it on crash.
77
+ if (libc.symbols.flock(fd, 2) !== 0) throw new Error('Wish benchmark ledger lock failed');
78
+ const current = lstatSync(lock);
79
+ if (opened.dev !== current.dev || opened.ino !== current.ino) throw new Error('Wish benchmark lock changed while waiting');
80
+ action();
81
+ } finally {
82
+ closeSync(fd);
83
+ }
84
+ }
85
+
86
+ export function recordWishBenchmarkSample(
87
+ sample: { cardId: string; at: string; surface: string; text: string },
88
+ ): void {
89
+ const stateRoot = elanousStateRoot();
90
+ const dir = preparePrivateDirectory(stateRoot);
91
+ const expected = lstatSync(dir);
92
+ const path = wishBenchmarkLedgerPath(stateRoot);
93
+ withLedgerLock(dir, () => {
94
+ const current = lstatSync(dir);
95
+ if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
96
+ throw new Error('Wish benchmark directory changed while waiting for lock');
97
+ }
98
+ // Re-check under the lock: another process may have recorded this card while we waited.
99
+ const fd = openSync(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
100
+ try {
101
+ const state = fstatSync(fd);
102
+ if (!state.isFile() || state.nlink !== 1) throw new Error('Wish benchmark ledger is not a private regular file');
103
+ fchmodSync(fd, 0o600);
104
+ if (readFileSync(fd, 'utf8').split('\n').some(line => {
105
+ if (!line) return false;
106
+ const row = JSON.parse(line) as { id?: string; cardId?: string };
107
+ return (row.id ?? row.cardId) === sample.cardId;
108
+ })) return;
109
+ // 줄 꼴 = OP 원장과 같다: id · at · surface · text(원문 그대로) · chars · op_cells(참조 칸 — OP 가 채운다) · note.
110
+ const line = JSON.stringify({ id: sample.cardId, at: sample.at, surface: sample.surface, text: sample.text,
111
+ chars: Array.from(sample.text).length, op_cells: [], note: 'wish-card' }) + '\n';
112
+ writeSync(fd, line);
113
+ } finally {
114
+ closeSync(fd);
115
+ }
116
+ });
117
+ }
118
+
119
+ /** 카드 생성 경로용: 벤치 원장 쓰기가 실패해도(저장소 안 상태 루트 · 권한 · 링크) 소원 카드는 만들어져야 한다.
120
+ * 실패는 관측 한 줄로만 남긴다(원문은 싣지 않는다). */
121
+ export function recordWishBenchmarkSampleSafely(sample: Parameters<typeof recordWishBenchmarkSample>[0]): boolean {
122
+ try {
123
+ recordWishBenchmarkSample(sample);
124
+ return true;
125
+ } catch (error) {
126
+ debug.log('intake.wish-bench', 'skipped', { cardId: sample.cardId, surface: sample.surface, reason: String((error as Error)?.message ?? error).split(':')[0] });
127
+ return false;
128
+ }
129
+ }
@@ -1,13 +1,54 @@
1
1
  import { debug } from '../debug/log.js';
2
2
  import { CardStore } from '../task-cards/card-store.js';
3
+ import { recordWishBenchmarkSampleSafely } from './wish-benchmark-ledger.js';
4
+
5
+ export type WishReplyTarget =
6
+ | { surface: 'telegram'; chatId: string; threadId?: string; botId?: string }
7
+ | { surface: 'pwa'; sessionId: string }
8
+ | { surface: 'linear'; issueId: string }
9
+ | { surface: 'tui' };
10
+
11
+ /** `intake:reply:0` 섹션 꼴 — surface ⊕ 표면 안 주소(텔레그램 대화 · PWA 세션 · Linear 이슈 · TUI 없음). */
12
+ export function replySection(target: WishReplyTarget): { surface: WishReplyTarget['surface']; address: string | null } {
13
+ switch (target.surface) {
14
+ case 'telegram': return { surface: 'telegram', address: target.threadId ? `${target.chatId}:${target.threadId}` : target.chatId };
15
+ case 'pwa': return { surface: 'pwa', address: target.sessionId };
16
+ case 'linear': return { surface: 'linear', address: target.issueId };
17
+ case 'tui': return { surface: 'tui', address: null };
18
+ }
19
+ }
20
+
21
+ export type WishInput = {
22
+ text: string;
23
+ source: WishReplyTarget['surface'];
24
+ ref: string;
25
+ replyTo?: WishReplyTarget;
26
+ };
27
+
28
+ function replyTarget(input: WishInput): WishReplyTarget {
29
+ if (input.replyTo) {
30
+ if (input.replyTo.surface !== input.source) throw new Error('소원 출처와 회신 대상이 다릅니다');
31
+ return input.replyTo;
32
+ }
33
+ switch (input.source) {
34
+ case 'telegram': return { surface: 'telegram', chatId: input.ref.split(':', 1)[0]! };
35
+ case 'pwa':
36
+ case 'linear': throw new Error('소원 회신 대상을 명시하세요');
37
+ case 'tui': return { surface: 'tui' };
38
+ }
39
+ }
3
40
 
4
41
  export function createWishCard(
5
- { text, source, ref }: { text: string; source: 'telegram' | 'pwa' | 'tui'; ref: string },
42
+ { text, source, ref, replyTo }: WishInput,
6
43
  store: CardStore = new CardStore(),
7
44
  ): { cardId: string; title: string; created: boolean } {
8
45
  const title = text.trim().split(/\r?\n/, 1)[0]!.trim().slice(0, 80);
9
46
  if (!title) throw new Error('소원 글을 입력하세요');
10
47
  if (!ref?.trim()) throw new Error('소원 참조가 필요합니다');
48
+ const target = replyTarget({ text, source, ref, replyTo });
49
+ if (target.surface === 'telegram' && !/^-?\d+$/.test(target.chatId)
50
+ || target.surface === 'pwa' && !target.sessionId.trim()
51
+ || target.surface === 'linear' && !target.issueId.trim()) throw new Error('소원 회신 대상이 필요합니다');
11
52
  const goalId = `wish:${source}:${ref}`;
12
53
  const old = store.listCards().find(card => card.goalId === goalId);
13
54
  const card = old ?? store.createCard({ goalId, title });
@@ -22,12 +63,22 @@ export function createWishCard(
22
63
  key: 'intake:wish:0',
23
64
  owner: 'steward',
24
65
  content: JSON.stringify({
25
- source, ref, title: card.title, text: sameRequest ? text : null, at: new Date().toISOString(),
66
+ source, ref, replyTo: target, title: card.title, text: sameRequest ? text : null, at: new Date().toISOString(),
26
67
  ...(old ? { recovered: true } : {}), ...(sameRequest ? {} : { mismatch: true }),
27
68
  }),
28
69
  });
29
70
  if (!sameRequest) debug.log('intake.wish', 'recovered-mismatch', { source, cardId: card.id });
30
71
  }
72
+ // FLOW1 원장 약속(TC 21:15): 회신 주소를 `intake:reply:0 {surface, address}` 로도(원장 섹션 허용 목록상 `intake` 아래) 남긴다 — FLOW1a 는 이 꼴을 읽고
73
+ // 보내기는 `sendCardReply(cardId, text)` 만 부른다(wish-reply.ts). 원천은 위 intake 섹션의 replyTo 그대로.
74
+ if (!card.sections.some(section => section.key === 'intake:reply:0')) {
75
+ store.appendSection(card.id, { key: 'intake:reply:0', owner: 'steward', content: JSON.stringify(replySection(target)) });
76
+ }
77
+ const originalIntake = store.getCard(card.id)?.sections.find(section => section.key === 'intake:wish:0');
78
+ if (originalIntake) {
79
+ const first = JSON.parse(originalIntake.content) as { source: WishInput['source']; text: string | null; at: string };
80
+ if (first.text !== null) recordWishBenchmarkSampleSafely({ cardId: card.id, at: first.at, surface: first.source, text: first.text });
81
+ }
31
82
  debug.log('intake.wish', created ? 'created' : 'duplicate', { source, cardId: card.id });
32
83
  return { cardId: card.id, title: card.title, created };
33
84
  }
@@ -1,6 +1,7 @@
1
1
  import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
2
2
  import { basename, join, relative } from 'node:path';
3
3
  import { CardStore } from '../task-cards/card-store.js';
4
+ import { recordWishBenchmarkSampleSafely } from './wish-benchmark-ledger.js';
4
5
 
5
6
  export interface WishScanResult {
6
7
  added: number;
@@ -52,9 +53,11 @@ export function scanWishFolder({ dir, store }: { dir: string; store: CardStore }
52
53
  result.skipped++;
53
54
  continue;
54
55
  }
55
- const title = /^# (.+)$/m.exec(readFileSync(path, 'utf8'))?.[1]?.trim() || basename(path, '.md');
56
+ const text = readFileSync(path, 'utf8');
57
+ const title = /^# (.+)$/m.exec(text)?.[1]?.trim() || basename(path, '.md');
56
58
  const card = old ?? store.createCard({ goalId, title });
57
- const intake = { source: 'wish', path: relPath, mtime, title };
59
+ if (intakeSections.length === 0) recordWishBenchmarkSampleSafely({ cardId: card.id, at: card.createdAt, surface: 'wish', text });
60
+ const intake = { source: 'wish', path: relPath, mtime, title, text };
58
61
  store.appendSection(card.id, {
59
62
  key: `intake:wish:${intakeSections.length}`,
60
63
  owner: 'steward',
@@ -0,0 +1,72 @@
1
+ import { commentLinearIssue } from '../connectors/linear.js';
2
+ import { getSecretAsync } from '../nexus/config/secrets/index.js';
3
+ import { appendMessage, adoptSession } from '../session/index.js';
4
+ import { CardStore } from '../task-cards/card-store.js';
5
+ import { resolveChannelBotToken } from '../channel-bot-token.js';
6
+ import { getUserConfig } from '../user-config.js';
7
+ import type { WishReplyTarget } from './wish-card.js';
8
+
9
+ export interface WishReplySinks {
10
+ telegram: (target: Extract<WishReplyTarget, { surface: 'telegram' }>, text: string) => Promise<void>;
11
+ pwa: (sessionId: string, text: string) => Promise<void>;
12
+ linear: (issueId: string, text: string) => Promise<void>;
13
+ }
14
+
15
+ const defaultSinks: WishReplySinks = {
16
+ telegram: async (target, text) => {
17
+ const cfg = getUserConfig();
18
+ const primary = resolveChannelBotToken('telegram', cfg).token;
19
+ const token = !target.botId || primary.split(':')[0] === target.botId
20
+ ? primary
21
+ : cfg.telegram.channels?.find(channel => channel.botToken?.split(':')[0] === target.botId)?.botToken;
22
+ if (!token) throw new Error('Telegram 회신 봇 토큰이 없습니다');
23
+ let response: Response;
24
+ try {
25
+ response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
26
+ method: 'POST', headers: { 'content-type': 'application/json' },
27
+ body: JSON.stringify({ chat_id: target.chatId, text, ...(target.threadId ? { message_thread_id: Number(target.threadId) } : {}) }),
28
+ });
29
+ } catch {
30
+ throw new Error('Telegram 회신 요청에 실패했습니다');
31
+ }
32
+ if (!response.ok || (await response.json() as { ok?: boolean }).ok !== true) throw new Error('Telegram 회신에 실패했습니다');
33
+ },
34
+ pwa: async (sessionId, text) => {
35
+ adoptSession(sessionId, { source: 'pwa', origin: 'pwa' });
36
+ appendMessage(sessionId, { role: 'assistant', content: text, ts: new Date().toISOString() });
37
+ },
38
+ linear: async (issueId, text) => {
39
+ const apiKey = await getSecretAsync('connector.linear.apiKey');
40
+ if (!apiKey) throw new Error('Linear 키가 없습니다');
41
+ await commentLinearIssue({ apiKey, issueId, body: text });
42
+ },
43
+ };
44
+
45
+ /** A later workflow needs only the card id and one line; the original intake section owns the destination. */
46
+ export async function replyToWishCard(cardId: string, line: string, store?: CardStore, sinks: WishReplySinks = defaultSinks): Promise<void> {
47
+ if (!line.trim() || line.includes('\n') || line.includes('\r')) throw new Error('회신은 한 줄이어야 합니다');
48
+ const owned = !store;
49
+ const cards = store ?? new CardStore();
50
+ try {
51
+ const card = cards.getCard(cardId);
52
+ if (!card) throw new Error(`소원 카드를 찾을 수 없습니다: ${cardId}`);
53
+ const section = card.sections.find(item => item.key === 'intake:wish:0');
54
+ if (!section) throw new Error(`소원 회신 대상이 없습니다: ${cardId}`);
55
+ const { replyTo } = JSON.parse(section.content) as { replyTo?: WishReplyTarget };
56
+ if (!replyTo || typeof replyTo !== 'object' || !('surface' in replyTo)) throw new Error(`소원 회신 대상이 없습니다: ${cardId}`);
57
+ switch (replyTo.surface) {
58
+ case 'telegram': await sinks.telegram(replyTo, line); return;
59
+ case 'pwa': await sinks.pwa(replyTo.sessionId, line); return;
60
+ case 'linear': await sinks.linear(replyTo.issueId, line); return;
61
+ case 'tui': return;
62
+ default: throw new Error(`알 수 없는 소원 회신 대상: ${cardId}`);
63
+ }
64
+ } finally {
65
+ if (owned) cards.close();
66
+ }
67
+ }
68
+
69
+ /** FLOW1 원장 약속 이름(TC 21:15) — FLOW1a 가 동적 import 로 부른다. 동작은 replyToWishCard 와 같다. */
70
+ export async function sendCardReply(cardId: string, text: string): Promise<void> {
71
+ return replyToWishCard(cardId, text);
72
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename, join } from 'node:path';
4
+ import { debug } from '../debug/log.js';
5
+ import { LessonLedger } from './lesson-ledger.js';
6
+
7
+ export interface LessonImportItem { id: string; incident: string; cause: string; remedy: string; source: string }
8
+ export interface LessonImportSkipped { source: string; reason: string }
9
+ export interface LessonScan { files: number; items: LessonImportItem[]; skipped: LessonImportSkipped[] }
10
+
11
+ function firstParagraph(lines: string[], heading: RegExp): string {
12
+ const start = lines.findIndex(line => {
13
+ const match = line.match(/^##\s+(.+)$/);
14
+ if (!match) return false;
15
+ const title = match[1]!.replace(/^(?:(?:\d+[.)]|§\s*\d+\.?|[a-z][.)]|[①-⑳])\s*|[\p{Extended_Pictographic}\uFE0F\u200D]+\s*|\s)+/giu, '');
16
+ return heading.test(title);
17
+ });
18
+ if (start === -1) return '';
19
+ const paragraph: string[] = [];
20
+ for (const line of lines.slice(start + 1)) {
21
+ if (/^#{1,2}\s+/.test(line)) break;
22
+ if (/^#{3,6}\s+/.test(line)) {
23
+ if (paragraph.length) break;
24
+ continue;
25
+ }
26
+ if (!line.trim()) {
27
+ if (paragraph.length) break;
28
+ continue;
29
+ }
30
+ if (/^```|^~~~/.test(line.trim())) break;
31
+ paragraph.push(line.trim());
32
+ }
33
+ return paragraph.join(' ');
34
+ }
35
+
36
+ export function scanLessonDocs(root: string): LessonScan {
37
+ const sources: string[] = [];
38
+ const visit = (directory: string): void => {
39
+ for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
40
+ if (entry.isSymbolicLink()) continue;
41
+ const source = `${directory}/${entry.name}`;
42
+ if (entry.isDirectory()) {
43
+ if (source !== 'docs/goals' && source !== 'docs/archive') visit(source);
44
+ } else if (entry.isFile() && /^(?:INCIDENT|FINDING)-.*\.md$/.test(entry.name)) {
45
+ sources.push(source);
46
+ }
47
+ }
48
+ };
49
+ if (existsSync(join(root, 'docs')) && lstatSync(join(root, 'docs')).isDirectory()) visit('docs');
50
+ sources.sort();
51
+ const items: LessonImportItem[] = [];
52
+ const skipped: LessonImportSkipped[] = [];
53
+ for (const source of sources) {
54
+ const lines = readFileSync(join(root, source), 'utf8').split(/\r?\n/);
55
+ const title = lines.find(line => /^#\s+/.test(line));
56
+ const incident = title?.replace(/^#\s+/, '').replace(/^(?:INCIDENT|FINDING)\s*[—–-]\s*/i, '').trim() ?? '';
57
+ const cause = firstParagraph(lines, /^(?:원인|근본|기전|Root cause|Why|Complication|C$)/i);
58
+ const remedy = firstParagraph(lines, /^(?:교훈|재발 방지|처방|대응|수리|Fix|Answer|A$)/i);
59
+ if (!incident || (!cause && !remedy)) {
60
+ skipped.push({ source, reason: !incident ? 'missing incident title' : 'missing cause and remedy' });
61
+ continue;
62
+ }
63
+ const filename = basename(source, '.md');
64
+ const date = filename.match(/\d{4}-\d{2}-\d{2}/)?.[0];
65
+ const slug = filename.replace(/^(?:INCIDENT|FINDING)-/, '').replace(/(?:^|-)\d{4}-\d{2}-\d{2}(?=-|$)/, '')
66
+ .replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/g, '');
67
+ const base = slug || 'lesson';
68
+ // Include the source fingerprint even before a collision is observed: a later document cannot rename an earlier ID.
69
+ const fingerprint = createHash('sha256').update(source).digest('hex');
70
+ const id = `${base}-${date ?? 'undated'}-${fingerprint}`;
71
+ items.push({ id, incident, cause, remedy, source });
72
+ }
73
+ debug.log('lessons.import', 'scanned', { files: sources.length, items: items.length, skipped: skipped.length, written: 0 });
74
+ return { files: sources.length, items, skipped };
75
+ }
76
+
77
+ export function importLessons(ledger: LessonLedger, items: LessonImportItem[], options: { apply: boolean; by: string }): {
78
+ items: LessonImportItem[]; skipped: LessonImportSkipped[]; written: number;
79
+ } {
80
+ const pending: LessonImportItem[] = [];
81
+ const skipped: LessonImportSkipped[] = [];
82
+ const seenIds = new Set<string>();
83
+ const seenSources = new Set<string>(existsSync(ledger.path) ? ledger.importedSources() : []);
84
+ for (const item of items) {
85
+ if (seenIds.has(item.id) || seenSources.has(item.source)) {
86
+ skipped.push({ source: item.source, reason: 'already imported or duplicate in scan' });
87
+ continue;
88
+ }
89
+ let exists = false;
90
+ if (existsSync(ledger.path)) {
91
+ try { ledger.get(item.id); exists = true; }
92
+ catch (error) {
93
+ if (!(error instanceof Error) || !error.message.startsWith('lesson not found:')) throw error;
94
+ }
95
+ }
96
+ if (exists) {
97
+ skipped.push({ source: item.source, reason: 'already imported' });
98
+ continue;
99
+ }
100
+ seenIds.add(item.id);
101
+ seenSources.add(item.source);
102
+ pending.push(item);
103
+ if (options.apply) ledger.importDocument({ ...item, owner: options.by, by: options.by });
104
+ }
105
+ return { items: pending, skipped, written: options.apply ? pending.length : 0 };
106
+ }
@@ -0,0 +1,206 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { chmodSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
+ import { debug } from '../debug/log.js';
6
+
7
+ export type LessonStatus = 'open' | 'enforced' | 'candidate' | 'promoted';
8
+ export interface LessonRow {
9
+ id: string;
10
+ incident: string;
11
+ cause: string;
12
+ remedy: string;
13
+ enforced_by: string;
14
+ disproof: string | null;
15
+ owner: string;
16
+ status: LessonStatus;
17
+ created_at: string;
18
+ updated_at: string;
19
+ occurrence_count: number;
20
+ }
21
+ export interface LessonOccurrence { lesson_id: string; at: string; source: string; note: string }
22
+ export interface LessonHistory { lesson_id: string; at: string; by: string; event: 'add' | 'recur' | 'enforce' | 'promote'; detail: string }
23
+ export interface LessonDetail extends LessonRow { occurrences: LessonOccurrence[]; history: LessonHistory[] }
24
+ export interface LessonLedgerOptions { stateDir?: string; now?: () => Date }
25
+ export interface LessonAddInput { id: string; incident: string; cause: string; remedy: string; owner: string; source: string; enforcedBy?: string; disproof?: string; by?: string }
26
+
27
+ export class LessonInputError extends Error {
28
+ override name = 'LessonInputError';
29
+ }
30
+
31
+ function required(value: string, field: string): string {
32
+ if (typeof value !== 'string' || !value.trim()) throw new LessonInputError(`${field} is required`);
33
+ return value.trim();
34
+ }
35
+
36
+ function enforcement(value: string): string {
37
+ if (typeof value !== 'string') throw new LessonInputError('enforcedBy is required');
38
+ return value.split(',').map(part => required(part, 'enforcedBy')).join(',');
39
+ }
40
+
41
+ export class LessonLedger {
42
+ readonly path: string;
43
+ private readonly now: () => Date;
44
+
45
+ constructor(options: LessonLedgerOptions = {}) {
46
+ this.path = join(options.stateDir ?? elanousStateRoot(), 'lessons', 'lessons.sqlite');
47
+ this.now = options.now ?? (() => new Date());
48
+ }
49
+
50
+ private open(): Database {
51
+ mkdirSync(dirname(this.path), { recursive: true });
52
+ const db = new Database(this.path, { create: true, strict: true });
53
+ try {
54
+ chmodSync(this.path, 0o600);
55
+ db.exec('PRAGMA busy_timeout = 10000');
56
+ db.exec('PRAGMA journal_mode = WAL');
57
+ db.exec('PRAGMA foreign_keys = ON');
58
+ db.exec(`CREATE TABLE IF NOT EXISTS lessons (
59
+ id TEXT PRIMARY KEY, incident TEXT NOT NULL, cause TEXT NOT NULL, remedy TEXT NOT NULL,
60
+ enforced_by TEXT NOT NULL DEFAULT '', disproof TEXT, owner TEXT NOT NULL,
61
+ status TEXT NOT NULL CHECK(status IN ('open','enforced','candidate','promoted')),
62
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
63
+ CREATE TABLE IF NOT EXISTS occurrences (
64
+ lesson_id TEXT NOT NULL REFERENCES lessons(id), at TEXT NOT NULL, source TEXT NOT NULL, note TEXT NOT NULL);
65
+ CREATE TABLE IF NOT EXISTS history (
66
+ lesson_id TEXT NOT NULL REFERENCES lessons(id), at TEXT NOT NULL, by TEXT NOT NULL,
67
+ event TEXT NOT NULL, detail TEXT NOT NULL);`);
68
+ return db;
69
+ } catch (error) { db.close(); throw error; }
70
+ }
71
+
72
+ private using<T>(work: (db: Database) => T): T {
73
+ const db = this.open();
74
+ try { return work(db); } finally { db.close(); }
75
+ }
76
+
77
+ private transaction<T>(db: Database, work: () => T): T {
78
+ db.exec('BEGIN IMMEDIATE');
79
+ try { const result = work(); db.exec('COMMIT'); return result; }
80
+ catch (error) { db.exec('ROLLBACK'); throw error; }
81
+ }
82
+
83
+ private row(db: Database, id: string): LessonRow {
84
+ const row = db.query(`SELECT l.*, (SELECT count(*) FROM occurrences WHERE lesson_id = l.id) AS occurrence_count
85
+ FROM lessons l WHERE l.id = ?`).get(id) as LessonRow | null;
86
+ if (!row) throw new LessonInputError(`lesson not found: ${id}`);
87
+ return row;
88
+ }
89
+
90
+ private record(db: Database, id: string, at: string, by: string, event: LessonHistory['event'], detail: string): void {
91
+ db.query('INSERT INTO history (lesson_id, at, by, event, detail) VALUES (?, ?, ?, ?, ?)').run(id, at, by, event, detail);
92
+ }
93
+
94
+ private log(event: LessonHistory['event'], row: LessonRow): void {
95
+ debug.log('lessons.ledger', event, { id: row.id, status: row.status, occurrences: row.occurrence_count });
96
+ }
97
+
98
+ add(input: LessonAddInput): LessonRow {
99
+ return this.addValidated(input, false);
100
+ }
101
+
102
+ importDocument(input: LessonAddInput): LessonRow {
103
+ return this.addValidated(input, true);
104
+ }
105
+
106
+ importedSources(): string[] {
107
+ return this.using(db => (db.query("SELECT detail AS source FROM history WHERE event = 'add' AND detail LIKE 'docs/%'").all() as { source: string }[]).map(row => row.source));
108
+ }
109
+
110
+ private addValidated(input: LessonAddInput, allowPartial: boolean): LessonRow {
111
+ const id = required(input.id, 'id');
112
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) throw new LessonInputError('invalid lesson id');
113
+ const incident = required(input.incident, 'incident');
114
+ const cause = allowPartial ? input.cause.trim() : required(input.cause, 'cause');
115
+ const remedy = allowPartial ? input.remedy.trim() : required(input.remedy, 'remedy');
116
+ if (!cause && !remedy) throw new LessonInputError('cause or remedy is required');
117
+ const owner = required(input.owner, 'owner');
118
+ const source = required(input.source, 'source');
119
+ const enforcedBy = input.enforcedBy === undefined || !input.enforcedBy.trim() ? '' : enforcement(input.enforcedBy);
120
+ const disproof = input.disproof === undefined || !input.disproof.trim() ? null : required(input.disproof, 'disproof');
121
+ return this.using(db => this.transaction(db, () => {
122
+ const at = this.now().toISOString();
123
+ db.query(`INSERT INTO lessons (id, incident, cause, remedy, enforced_by, disproof, owner, status, created_at, updated_at)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, incident, cause, remedy, enforcedBy, disproof, owner, enforcedBy ? 'enforced' : 'open', at, at);
125
+ db.query('INSERT INTO occurrences (lesson_id, at, source, note) VALUES (?, ?, ?, ?)').run(id, at, source, '');
126
+ this.record(db, id, at, input.by === undefined ? owner : required(input.by, 'by'), 'add', source);
127
+ const row = this.row(db, id);
128
+ this.log('add', row);
129
+ return row;
130
+ }));
131
+ }
132
+
133
+ recur(id: string, input: { source: string; note?: string; by: string }): LessonRow {
134
+ const source = required(input.source, 'source');
135
+ const by = required(input.by, 'by');
136
+ const note = input.note ?? '';
137
+ return this.using(db => this.transaction(db, () => {
138
+ const previous = this.row(db, id);
139
+ const at = this.now().toISOString();
140
+ db.query('INSERT INTO occurrences (lesson_id, at, source, note) VALUES (?, ?, ?, ?)').run(id, at, source, note);
141
+ const status = previous.status === 'promoted' ? 'promoted' : 'candidate';
142
+ db.query('UPDATE lessons SET status = ?, updated_at = ? WHERE id = ?').run(status, at, id);
143
+ this.record(db, id, at, by, 'recur', `${source}${note ? ` · ${note}` : ''}`);
144
+ const row = this.row(db, id);
145
+ this.log('recur', row);
146
+ return row;
147
+ }));
148
+ }
149
+
150
+ enforce(id: string, input: { enforcedBy: string; by: string }): LessonRow {
151
+ const enforcedBy = enforcement(input.enforcedBy);
152
+ const by = required(input.by, 'by');
153
+ return this.using(db => this.transaction(db, () => {
154
+ const previous = this.row(db, id);
155
+ const at = this.now().toISOString();
156
+ const paths = [...new Set([...previous.enforced_by.split(',').filter(Boolean), ...enforcedBy.split(',')])].join(',');
157
+ const status = previous.status === 'promoted' ? 'promoted' : 'enforced';
158
+ db.query('UPDATE lessons SET enforced_by = ?, status = ?, updated_at = ? WHERE id = ?').run(paths, status, at, id);
159
+ this.record(db, id, at, by, 'enforce', enforcedBy);
160
+ const row = this.row(db, id);
161
+ this.log('enforce', row);
162
+ return row;
163
+ }));
164
+ }
165
+
166
+ promote(id: string, input: { rulePath: string; by: string }): LessonRow {
167
+ const rulePath = required(input.rulePath, 'rulePath');
168
+ const by = required(input.by, 'by');
169
+ return this.using(db => this.transaction(db, () => {
170
+ const previous = this.row(db, id);
171
+ if (!previous.disproof?.trim()) throw new LessonInputError('disproof is required to promote a lesson');
172
+ const at = this.now().toISOString();
173
+ const paths = [...new Set([...previous.enforced_by.split(',').filter(Boolean), rulePath])].join(',');
174
+ db.query("UPDATE lessons SET enforced_by = ?, status = 'promoted', updated_at = ? WHERE id = ?").run(paths, at, id);
175
+ this.record(db, id, at, by, 'promote', rulePath);
176
+ const row = this.row(db, id);
177
+ this.log('promote', row);
178
+ return row;
179
+ }));
180
+ }
181
+
182
+ find(query: string): LessonRow[] {
183
+ const term = required(query, 'query').toLowerCase();
184
+ return this.using(db => (db.query(`SELECT l.*, (SELECT count(*) FROM occurrences WHERE lesson_id = l.id) AS occurrence_count
185
+ FROM lessons l WHERE instr(lower(incident), ?) > 0 OR instr(lower(cause), ?) > 0 OR instr(lower(remedy), ?) > 0
186
+ ORDER BY occurrence_count DESC, l.created_at, l.id`).all(term, term, term) as LessonRow[]));
187
+ }
188
+
189
+ list(filters: { status?: LessonStatus } = {}): LessonRow[] {
190
+ return this.using(db => (db.query(`SELECT l.*, (SELECT count(*) FROM occurrences WHERE lesson_id = l.id) AS occurrence_count
191
+ FROM lessons l WHERE (? IS NULL OR l.status = ?)
192
+ ORDER BY occurrence_count DESC, l.updated_at DESC, l.id`).all(filters.status ?? null, filters.status ?? null) as LessonRow[]));
193
+ }
194
+
195
+ candidates(): LessonRow[] {
196
+ return this.using(db => db.query(`SELECT l.*, (SELECT count(*) FROM occurrences WHERE lesson_id = l.id) AS occurrence_count
197
+ FROM lessons l WHERE l.status = 'candidate' ORDER BY occurrence_count DESC, l.created_at, l.id`).all() as LessonRow[]);
198
+ }
199
+
200
+ get(id: string): LessonDetail {
201
+ return this.using(db => ({ ...this.row(db, id),
202
+ occurrences: db.query('SELECT lesson_id, at, source, note FROM occurrences WHERE lesson_id = ? ORDER BY rowid').all(id) as LessonOccurrence[],
203
+ history: db.query('SELECT lesson_id, at, by, event, detail FROM history WHERE lesson_id = ? ORDER BY rowid').all(id) as LessonHistory[],
204
+ }));
205
+ }
206
+ }