elanous 0.2.13 → 0.2.15

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 (524) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  10. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.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-c952712c87a1f757.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/899-46946fe8363e1262.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  100. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  106. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  107. package/apps/pwa/out/approvals/index.html +1 -1
  108. package/apps/pwa/out/approvals/index.txt +23 -23
  109. package/apps/pwa/out/autopilot/index.html +1 -1
  110. package/apps/pwa/out/autopilot/index.txt +23 -23
  111. package/apps/pwa/out/board/index.html +1 -1
  112. package/apps/pwa/out/board/index.txt +23 -23
  113. package/apps/pwa/out/botlab/index.html +1 -1
  114. package/apps/pwa/out/botlab/index.txt +22 -22
  115. package/apps/pwa/out/bots/index.html +1 -1
  116. package/apps/pwa/out/bots/index.txt +22 -22
  117. package/apps/pwa/out/ceo/index.html +1 -0
  118. package/apps/pwa/out/ceo/index.txt +33 -0
  119. package/apps/pwa/out/chat/index.html +1 -1
  120. package/apps/pwa/out/chat/index.txt +22 -22
  121. package/apps/pwa/out/consult/index.html +1 -1
  122. package/apps/pwa/out/consult/index.txt +23 -23
  123. package/apps/pwa/out/control/index.html +1 -1
  124. package/apps/pwa/out/control/index.txt +23 -23
  125. package/apps/pwa/out/design-check/index.html +1 -1
  126. package/apps/pwa/out/design-check/index.txt +23 -23
  127. package/apps/pwa/out/editor/index.html +1 -1
  128. package/apps/pwa/out/editor/index.txt +23 -23
  129. package/apps/pwa/out/exec/index.html +1 -1
  130. package/apps/pwa/out/exec/index.txt +23 -23
  131. package/apps/pwa/out/field/index.html +1 -1
  132. package/apps/pwa/out/field/index.txt +23 -23
  133. package/apps/pwa/out/healthz +1 -1
  134. package/apps/pwa/out/index.html +1 -1
  135. package/apps/pwa/out/index.txt +22 -22
  136. package/apps/pwa/out/inside/index.html +1 -1
  137. package/apps/pwa/out/inside/index.txt +22 -22
  138. package/apps/pwa/out/intake/index.html +1 -1
  139. package/apps/pwa/out/intake/index.txt +23 -23
  140. package/apps/pwa/out/live/index.html +1 -1
  141. package/apps/pwa/out/live/index.txt +23 -23
  142. package/apps/pwa/out/loops/index.html +1 -0
  143. package/apps/pwa/out/loops/index.txt +30 -0
  144. package/apps/pwa/out/market/index.html +1 -1
  145. package/apps/pwa/out/market/index.txt +22 -22
  146. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  147. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  148. package/apps/pwa/out/morning/index.html +1 -1
  149. package/apps/pwa/out/morning/index.txt +23 -23
  150. package/apps/pwa/out/observatory/index.html +1 -1
  151. package/apps/pwa/out/observatory/index.txt +23 -23
  152. package/apps/pwa/out/ops/checklist/index.html +1 -1
  153. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  154. package/apps/pwa/out/ops/release/index.html +1 -1
  155. package/apps/pwa/out/ops/release/index.txt +22 -22
  156. package/apps/pwa/out/ops/seats/index.html +1 -1
  157. package/apps/pwa/out/ops/seats/index.txt +22 -22
  158. package/apps/pwa/out/outputs/index.html +1 -1
  159. package/apps/pwa/out/outputs/index.txt +22 -22
  160. package/apps/pwa/out/reflection/index.html +1 -1
  161. package/apps/pwa/out/reflection/index.txt +23 -23
  162. package/apps/pwa/out/scheduler/index.html +1 -1
  163. package/apps/pwa/out/scheduler/index.txt +23 -23
  164. package/apps/pwa/out/sessions/index.html +1 -1
  165. package/apps/pwa/out/sessions/index.txt +22 -22
  166. package/apps/pwa/out/settings/devices/index.html +1 -1
  167. package/apps/pwa/out/settings/devices/index.txt +23 -23
  168. package/apps/pwa/out/settings/index.html +1 -1
  169. package/apps/pwa/out/settings/index.txt +23 -23
  170. package/apps/pwa/out/setup/done/index.html +1 -1
  171. package/apps/pwa/out/setup/done/index.txt +23 -23
  172. package/apps/pwa/out/setup/index.html +1 -1
  173. package/apps/pwa/out/setup/index.txt +23 -23
  174. package/apps/pwa/out/share/index.html +1 -1
  175. package/apps/pwa/out/share/index.txt +23 -23
  176. package/apps/pwa/out/showroom/index.html +1 -1
  177. package/apps/pwa/out/showroom/index.txt +22 -22
  178. package/apps/pwa/out/tasks/index.html +1 -1
  179. package/apps/pwa/out/tasks/index.txt +23 -23
  180. package/apps/pwa/out/term/index.html +1 -1
  181. package/apps/pwa/out/term/index.txt +23 -23
  182. package/apps/pwa/out/today/index.html +1 -1
  183. package/apps/pwa/out/today/index.txt +23 -23
  184. package/apps/pwa/out/trace/index.html +1 -1
  185. package/apps/pwa/out/trace/index.txt +23 -23
  186. package/apps/pwa/out/vault/index.html +1 -1
  187. package/apps/pwa/out/vault/index.txt +23 -23
  188. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  189. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  190. package/apps/pwa/out/workflows/index.html +1 -1
  191. package/apps/pwa/out/workflows/index.txt +23 -23
  192. package/apps/pwa/out/workspace/index.html +1 -1
  193. package/apps/pwa/out/workspace/index.txt +23 -23
  194. package/apps/pwa/out/worktrees/index.html +1 -1
  195. package/apps/pwa/out/worktrees/index.txt +23 -23
  196. package/graphs/heal/heal-loop.yaml +1 -1
  197. package/graphs/implement-loop.yaml +1 -1
  198. package/graphs/orchestrator/orchestrator.yaml +59 -0
  199. package/graphs/orchestrator/recipes.yaml +18 -0
  200. package/graphs/release/recipes.yaml +6 -0
  201. package/graphs/release/release-loop.yaml +6 -1
  202. package/graphs/rhythm/daily.yaml +38 -0
  203. package/graphs/rhythm/recipes.yaml +18 -0
  204. package/graphs/rhythm/weekly.yaml +39 -0
  205. package/graphs/video/recipes.yaml +3 -0
  206. package/graphs/video/youtube-summary.yaml +17 -0
  207. package/package.json +1 -1
  208. package/scripts/audit-test-state-writes.ts +72 -17
  209. package/scripts/brand/check.ts +188 -0
  210. package/scripts/ci-typecheck-changed.ts +50 -18
  211. package/scripts/coord-post.sh +11 -1
  212. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  213. package/scripts/docs-cli-check.ts +23 -6
  214. package/scripts/expose-rubric-check.ts +118 -0
  215. package/scripts/expose-rubric-suggest.ts +169 -0
  216. package/scripts/heal/run-role.ts +14 -0
  217. package/scripts/hq/standby-verify.ts +11 -7
  218. package/scripts/lecture-note/node.ts +31 -11
  219. package/scripts/lessons/handbook-page.ts +112 -0
  220. package/scripts/marketing/generate-ax-brief.ts +140 -0
  221. package/scripts/marketing/generate-feature-map.ts +118 -0
  222. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  223. package/scripts/release-loop/gate-node.ts +89 -14
  224. package/scripts/release-loop/publish-node.ts +14 -1
  225. package/scripts/release-loop/unattended-release.ts +23 -1
  226. package/scripts/release-loop/vault-note-node.ts +428 -0
  227. package/scripts/release-story/draft.ts +220 -0
  228. package/scripts/rhythm/daily.ts +570 -0
  229. package/scripts/rhythm/weekly.ts +315 -0
  230. package/scripts/skills/public-check.ts +169 -0
  231. package/scripts/smart-merge/resync-shadow.ts +147 -0
  232. package/scripts/test-deterministic.ts +4 -8
  233. package/scripts/test-diet/effectiveness.ts +195 -0
  234. package/scripts/test-diet/lib.ts +16 -8
  235. package/scripts/test-diet/node.ts +77 -9
  236. package/scripts/ux/demo-rehearsal.ts +2 -2
  237. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  238. package/scripts/video/youtube-summary.ts +133 -0
  239. package/skills/diagram-master/SKILL.md +42 -49
  240. package/skills/diagram-master/references/pyproject.toml +4 -1
  241. package/skills/diagram-master/references/routing-guide.md +4 -2
  242. package/skills/grill-me/SKILL.md +1 -0
  243. package/skills/youtube-master/SKILL.md +13 -13
  244. package/skills/youtube-master/package-lock.json +0 -545
  245. package/skills/youtube-master/package.json +2 -5
  246. package/skills/youtube-master/scripts/main.ts +2 -2
  247. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  248. package/skills/youtube-master/src/util.ts +30 -7
  249. package/src/agent-env/profile.ts +161 -0
  250. package/src/agent-mission/driver.ts +3 -3
  251. package/src/agent-substrate/pr-reviewer.ts +2 -0
  252. package/src/ask-user-question/pending-questions.ts +5 -0
  253. package/src/auth/github-app-token.ts +112 -13
  254. package/src/autopilot/build/llm-conflict-merge.ts +73 -10
  255. package/src/autopilot/build/merge-intent.ts +53 -0
  256. package/src/briefing/brief-items.ts +121 -0
  257. package/src/chat/index.ts +2 -0
  258. package/src/claims/claims-ledger.ts +232 -0
  259. package/src/claims/claims-render.ts +166 -0
  260. package/src/cli/agent-env-cli.ts +21 -0
  261. package/src/cli/claims-cli.ts +94 -0
  262. package/src/cli/decisions-cli.ts +44 -3
  263. package/src/cli/docs-cli.ts +2 -0
  264. package/src/cli/doctor-cli.ts +23 -1
  265. package/src/cli/doctor-node-modules.ts +147 -0
  266. package/src/cli/flow-cli.ts +11 -0
  267. package/src/cli/freeze-cli.ts +45 -0
  268. package/src/cli/intake-cli.ts +46 -17
  269. package/src/cli/lesson-cli.ts +108 -0
  270. package/src/cli/logs-cli.ts +104 -2
  271. package/src/cli/logs-model-input.ts +131 -0
  272. package/src/cli/pod-cli.ts +2 -0
  273. package/src/cli/pr-cli.ts +29 -1
  274. package/src/cli/private-ledger-commands.ts +54 -4
  275. package/src/cli/pty-drive-verdict.ts +11 -2
  276. package/src/cli/release-cli.ts +190 -33
  277. package/src/cli/rfc-status.ts +91 -0
  278. package/src/cli/seat-requests-cli.ts +52 -0
  279. package/src/connectors/linear.ts +5 -3
  280. package/src/context-bus/condense.ts +15 -20
  281. package/src/context-bus/context-now-public.ts +96 -0
  282. package/src/context-bus/context-now-surfaces.ts +55 -2
  283. package/src/context-bus/context-now.ts +6 -3
  284. package/src/context-bus/coord-events.ts +6 -4
  285. package/src/context-bus/long-term-memory.ts +92 -11
  286. package/src/dashboard/index.ts +75 -22
  287. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  288. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  289. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  290. package/src/decisions/decision-cards.ts +33 -7
  291. package/src/decisions/decision-ledger.ts +40 -1
  292. package/src/decisions/proact-meter.ts +286 -0
  293. package/src/decisions/proact1-lite.ts +86 -0
  294. package/src/decisions/web-push-decision-cards.ts +1 -1
  295. package/src/discord/sprint21-runtime.ts +16 -0
  296. package/src/discord-self-message.ts +8 -0
  297. package/src/discord-slash-wire.ts +31 -2
  298. package/src/domains/alert-audit.ts +271 -0
  299. package/src/domains/core-tools.ts +4 -3
  300. package/src/domains/ops-facts-tool.ts +31 -1
  301. package/src/domains/outbound-alert.ts +1 -1
  302. package/src/elanous-config-dir.ts +29 -4
  303. package/src/execution-loop/launch-gate.ts +29 -14
  304. package/src/flow/cards.ts +239 -0
  305. package/src/graph-runner/graph-tick.ts +4 -0
  306. package/src/graph-runner/runner.ts +30 -5
  307. package/src/harness/harness-cli-command.ts +259 -38
  308. package/src/harness/harness-incidents.ts +45 -12
  309. package/src/harness/harness-pod-dispatch.ts +102 -11
  310. package/src/harness/harness-queue-child.ts +3 -3
  311. package/src/harness/harness-queue.ts +583 -65
  312. package/src/harness/helper-repair.ts +87 -0
  313. package/src/harness/helper-scan.ts +151 -0
  314. package/src/harness/launch-stamp.ts +236 -0
  315. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  316. package/src/harness/ledger-write-guard.ts +35 -0
  317. package/src/harness/model-input-observation.ts +37 -0
  318. package/src/harness/pod-memory-policy.ts +15 -0
  319. package/src/harness/repository-name.ts +55 -0
  320. package/src/hq/fence-audit.ts +90 -0
  321. package/src/hq/hq.ts +63 -5
  322. package/src/hq/lease.ts +4 -4
  323. package/src/hq/move-plan.ts +126 -0
  324. package/src/hq/promote.ts +332 -0
  325. package/src/index.ts +159 -27
  326. package/src/intake-plane/check.ts +63 -0
  327. package/src/intake-plane/digest.ts +37 -15
  328. package/src/intake-plane/front-classifier.ts +6 -0
  329. package/src/intake-plane/items.ts +3 -2
  330. package/src/intake-plane/lens.ts +177 -0
  331. package/src/intake-plane/linear-wish.ts +39 -11
  332. package/src/intake-plane/route.ts +145 -1
  333. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  334. package/src/intake-plane/wish-card.ts +53 -2
  335. package/src/intake-plane/wish-folder.ts +5 -2
  336. package/src/intake-plane/wish-reply.ts +72 -0
  337. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  338. package/src/launch-head/seat-goal-queue.ts +198 -0
  339. package/src/lessons/lesson-import.ts +106 -0
  340. package/src/lessons/lesson-ledger.ts +206 -0
  341. package/src/loops/activity.ts +203 -0
  342. package/src/loops/budget.ts +63 -0
  343. package/src/loops/checker.ts +5 -3
  344. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  345. package/src/loops/loop-cli.ts +36 -4
  346. package/src/loops/neighbors.ts +40 -0
  347. package/src/loops/observe.ts +10 -0
  348. package/src/loops/orchestrator/author-depth.ts +220 -0
  349. package/src/loops/orchestrator/author-input-check.ts +82 -0
  350. package/src/loops/orchestrator/author-ledger.ts +232 -0
  351. package/src/loops/orchestrator/finish-rate.ts +175 -0
  352. package/src/loops/orchestrator/overlap.ts +51 -12
  353. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  354. package/src/loops/orchestrator/seat-share.ts +56 -0
  355. package/src/loops/orchestrator/tick.ts +309 -0
  356. package/src/loops/orchestrator/traffic.ts +134 -13
  357. package/src/loops/registry.ts +535 -9
  358. package/src/loops/status-rows.ts +118 -0
  359. package/src/loops/verdict.ts +17 -5
  360. package/src/maturity/bot-slash-catalog.ts +66 -0
  361. package/src/maturity/feature-maturity.ts +9 -5
  362. package/src/maturity/tui-slash-availability.ts +31 -0
  363. package/src/mss/logging/log-store.ts +35 -4
  364. package/src/nexus/api/dashboard.ts +6 -1
  365. package/src/nexus/api/http-server.ts +33 -3
  366. package/src/nexus/api/loop-edges.ts +173 -0
  367. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  368. package/src/nexus/api/schedules-read.ts +18 -1
  369. package/src/nexus/api/seat-requests.ts +79 -56
  370. package/src/nexus/api/task-cards-api.ts +44 -5
  371. package/src/nexus/api/workflows.ts +2 -1
  372. package/src/prompt-enhance/enhance.ts +6 -1
  373. package/src/release-loop/auto-start.ts +51 -0
  374. package/src/release-loop/checklist.ts +22 -4
  375. package/src/release-loop/feature-store.ts +71 -32
  376. package/src/release-loop/landing-freeze.ts +121 -0
  377. package/src/release-loop/merged-pr-checklist.ts +125 -0
  378. package/src/release-loop/placement.ts +302 -0
  379. package/src/release-loop/release-schedule.ts +3 -1
  380. package/src/seat-dispatch/ceo-commands.ts +2 -2
  381. package/src/seat-dispatch/seat-ask.ts +16 -3
  382. package/src/seat-dispatch/seat-questions.ts +58 -0
  383. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  384. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  385. package/src/seat-loop/seat-loop.ts +531 -40
  386. package/src/self-dev/dev-pipeline.ts +14 -6
  387. package/src/self-dev/draft-sweep.ts +162 -6
  388. package/src/self-dev/draft-triage-rules.ts +25 -8
  389. package/src/self-dev/orchestrate.ts +15 -1
  390. package/src/self-dev/review-provider-fallback.ts +6 -1
  391. package/src/self-dev/run-store.ts +73 -1
  392. package/src/self-dev/run-supervisor.ts +57 -2
  393. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  394. package/src/self-implement/budget-gate.ts +29 -0
  395. package/src/self-implement/expose-gate.ts +47 -0
  396. package/src/self-implement/frozen-merges.ts +295 -0
  397. package/src/self-implement/gate-cli.ts +10 -4
  398. package/src/self-implement/gate-scope.ts +46 -9
  399. package/src/self-implement/goal-archive.ts +222 -0
  400. package/src/self-implement/goal-author.ts +43 -5
  401. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  402. package/src/self-implement/goal-lookup.ts +147 -0
  403. package/src/self-implement/heal-intake.ts +197 -0
  404. package/src/self-implement/host-regate.ts +32 -2
  405. package/src/self-implement/l8-merge-queue.ts +132 -0
  406. package/src/self-implement/orchestrator.ts +299 -48
  407. package/src/self-implement/pipeline-shape.ts +1 -1
  408. package/src/self-implement/revert-guard.ts +97 -0
  409. package/src/self-implement/run-ledger.ts +3 -1
  410. package/src/self-implement/run-status-mapping.ts +2 -0
  411. package/src/self-implement/seams.ts +29 -9
  412. package/src/self-implement/self-implement-cli.ts +1 -0
  413. package/src/self-implement/speed-claims.ts +338 -0
  414. package/src/session/index.ts +2 -0
  415. package/src/steward/triage.ts +74 -3
  416. package/src/steward/working-backwards.ts +4 -1
  417. package/src/task-cards/card-cli.ts +10 -2
  418. package/src/task-cards/card-store.ts +10 -6
  419. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  420. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  421. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  422. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  424. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
  426. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  427. package/src/telegram-commands.ts +27 -3
  428. package/src/telegram-loops-command.ts +44 -0
  429. package/src/telegram.ts +25 -4
  430. package/src/user-config.ts +146 -18
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/workflow-runtime/nodes/bash.ts +9 -1
  433. package/src/workflow-runtime/storage.ts +31 -13
  434. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  445. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  449. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  523. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  524. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -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,15 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { Database } from 'bun:sqlite';
3
+
4
+ const [dbPath, releasedMarker] = process.argv.slice(2);
5
+ if (!dbPath || !releasedMarker) throw new Error('lock fixture requires database path and marker');
6
+ const db = new Database(dbPath);
7
+ try {
8
+ db.exec('BEGIN IMMEDIATE');
9
+ console.log('LOCKED');
10
+ await Bun.sleep(500);
11
+ writeFileSync(releasedMarker, 'released');
12
+ db.exec('COMMIT');
13
+ } finally {
14
+ db.close();
15
+ }
@@ -0,0 +1,198 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Database } from 'bun:sqlite';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import type { QueueSeat } from '../harness/harness-queue.js';
7
+
8
+ export type SeatGoalStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'expired';
9
+ export type SeatGoal = {
10
+ id: string;
11
+ seat: QueueSeat;
12
+ goal: string;
13
+ prerequisites: string[];
14
+ notBefore?: string;
15
+ endAt?: string;
16
+ queuedAt: string;
17
+ status: SeatGoalStatus;
18
+ startedAt?: string;
19
+ finishedAt?: string;
20
+ };
21
+ export type SeatGoalInput = {
22
+ id?: string;
23
+ seat: QueueSeat;
24
+ goal: string;
25
+ prerequisites?: string[];
26
+ notBefore?: string;
27
+ endAt?: string;
28
+ };
29
+ export type SeatGoalQueueOptions = { root?: string; now?: () => Date };
30
+ export type SeatGoalScheduleOptions = SeatGoalQueueOptions & {
31
+ maxConcurrent: number;
32
+ seatCaps: Partial<Record<QueueSeat, number>>;
33
+ launch: (goal: SeatGoal) => Promise<void> | void;
34
+ };
35
+ export type SeatGoalScheduleResult = { outcome: 'started' | 'waiting' | 'empty'; goal?: SeatGoal; reason: string };
36
+
37
+ const SEATS = new Set(['OP', 'TC', 'MK', 'UX']);
38
+ const TERMINAL = new Set<SeatGoalStatus>(['succeeded', 'failed', 'expired']);
39
+
40
+ export function seatGoalQueuePath(root = effectiveInstanceRoot()): string {
41
+ return join(root, 'launch-head', 'seat-goals.sqlite');
42
+ }
43
+
44
+ function openQueue(root: string): Database {
45
+ const path = seatGoalQueuePath(root);
46
+ mkdirSync(join(root, 'launch-head'), { recursive: true, mode: 0o700 });
47
+ const db = new Database(path, { create: true, strict: true });
48
+ db.exec('PRAGMA busy_timeout = 30000');
49
+ db.exec(`CREATE TABLE IF NOT EXISTS seat_goals (
50
+ id TEXT PRIMARY KEY, seat TEXT NOT NULL, goal TEXT NOT NULL, prerequisites TEXT NOT NULL,
51
+ not_before TEXT, end_at TEXT, queued_at TEXT NOT NULL, status TEXT NOT NULL,
52
+ started_at TEXT, finished_at TEXT
53
+ )`);
54
+ return db;
55
+ }
56
+
57
+ type GoalRow = {
58
+ id: string; seat: QueueSeat; goal: string; prerequisites: string;
59
+ not_before: string | null; end_at: string | null; queued_at: string;
60
+ status: SeatGoalStatus; started_at: string | null; finished_at: string | null;
61
+ };
62
+
63
+ function toGoal(row: GoalRow): SeatGoal {
64
+ return {
65
+ id: row.id, seat: row.seat, goal: row.goal, prerequisites: JSON.parse(row.prerequisites) as string[],
66
+ queuedAt: row.queued_at, status: row.status,
67
+ ...(row.not_before ? { notBefore: row.not_before } : {}),
68
+ ...(row.end_at ? { endAt: row.end_at } : {}),
69
+ ...(row.started_at ? { startedAt: row.started_at } : {}),
70
+ ...(row.finished_at ? { finishedAt: row.finished_at } : {}),
71
+ };
72
+ }
73
+
74
+ function rows(db: Database): SeatGoal[] {
75
+ return (db.query('SELECT * FROM seat_goals ORDER BY queued_at, rowid').all() as GoalRow[]).map(toGoal);
76
+ }
77
+
78
+ function instant(value: string, field: string): number {
79
+ const time = Date.parse(value);
80
+ if (!Number.isFinite(time) || new Date(time).toISOString() !== value) throw new Error(`seat goal queue: invalid ${field}`);
81
+ return time;
82
+ }
83
+
84
+ function clock(now?: () => Date): Date {
85
+ const date = now?.() ?? new Date();
86
+ if (!Number.isFinite(date.getTime())) throw new Error('seat goal queue: invalid clock');
87
+ return date;
88
+ }
89
+
90
+ function cap(value: number, field: string): number {
91
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`seat goal queue: invalid ${field}`);
92
+ return value;
93
+ }
94
+
95
+ /** The launcher owns this store; producers enqueue only, never start a goal. */
96
+ export function enqueueSeatGoal(input: SeatGoalInput, options: SeatGoalQueueOptions = {}): SeatGoal {
97
+ if (!SEATS.has(input.seat)) throw new Error('seat goal queue: invalid seat');
98
+ if (!input.goal?.trim()) throw new Error('seat goal queue: empty goal');
99
+ const id = input.id ?? `sg-${randomUUID()}`;
100
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/.test(id)) throw new Error('seat goal queue: invalid id');
101
+ const prerequisites = input.prerequisites ?? [];
102
+ if (!Array.isArray(prerequisites) || prerequisites.some((key) => typeof key !== 'string' || !key.trim())
103
+ || new Set(prerequisites).size !== prerequisites.length || prerequisites.includes(id)) {
104
+ throw new Error('seat goal queue: invalid prerequisites');
105
+ }
106
+ const start = input.notBefore === undefined ? undefined : instant(input.notBefore, 'notBefore');
107
+ const end = input.endAt === undefined ? undefined : instant(input.endAt, 'endAt');
108
+ if (start !== undefined && end !== undefined && end <= start) throw new Error('seat goal queue: endAt must follow notBefore');
109
+ const queuedAt = clock(options.now).toISOString();
110
+ if (end !== undefined && end <= Date.parse(queuedAt)) throw new Error('seat goal queue: endAt has passed');
111
+ const db = openQueue(options.root ?? effectiveInstanceRoot());
112
+ try {
113
+ db.exec('BEGIN IMMEDIATE');
114
+ try {
115
+ const all = rows(db);
116
+ if (all.some((row) => row.id === id)) throw new Error(`seat goal queue: duplicate id ${id}`);
117
+ for (const key of prerequisites) {
118
+ if (!all.some((row) => row.id === key)) throw new Error(`seat goal queue: missing prerequisite ${key}`);
119
+ }
120
+ db.query(`INSERT INTO seat_goals (id, seat, goal, prerequisites, not_before, end_at, queued_at, status)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'queued')`).run(id, input.seat, input.goal, JSON.stringify(prerequisites),
122
+ input.notBefore ?? null, input.endAt ?? null, queuedAt);
123
+ db.exec('COMMIT');
124
+ return { id, seat: input.seat, goal: input.goal, prerequisites: [...prerequisites], queuedAt, status: 'queued',
125
+ ...(input.notBefore ? { notBefore: input.notBefore } : {}), ...(input.endAt ? { endAt: input.endAt } : {}) };
126
+ } catch (error) { db.exec('ROLLBACK'); throw error; }
127
+ } finally { db.close(); }
128
+ }
129
+
130
+ export function listSeatGoals(options: SeatGoalQueueOptions = {}): SeatGoal[] {
131
+ const db = openQueue(options.root ?? effectiveInstanceRoot());
132
+ try { return rows(db); } finally { db.close(); }
133
+ }
134
+
135
+ /** Completion is an explicit launcher receipt: an uncertain or crashed launch keeps its slot reserved. */
136
+ export function finishSeatGoal(id: string, status: 'succeeded' | 'failed', options: SeatGoalQueueOptions = {}): SeatGoal {
137
+ const db = openQueue(options.root ?? effectiveInstanceRoot());
138
+ try {
139
+ const finishedAt = clock(options.now).toISOString();
140
+ const result = db.query("UPDATE seat_goals SET status = ?, finished_at = ? WHERE id = ? AND status = 'running'")
141
+ .run(status, finishedAt, id);
142
+ if (result.changes !== 1) throw new Error(`seat goal queue: goal is not running: ${id}`);
143
+ return toGoal(db.query('SELECT * FROM seat_goals WHERE id = ?').get(id) as GoalRow);
144
+ } finally { db.close(); }
145
+ }
146
+
147
+ /** One tick claims at most one eligible goal atomically across launcher processes. */
148
+ export async function scheduleSeatGoals(options: SeatGoalScheduleOptions): Promise<SeatGoalScheduleResult> {
149
+ const totalLimit = cap(options.maxConcurrent, 'maxConcurrent');
150
+ for (const [seat, value] of Object.entries(options.seatCaps)) {
151
+ if (!SEATS.has(seat) || value === undefined) throw new Error('seat goal queue: invalid seatCaps');
152
+ cap(value, `seatCaps.${seat}`);
153
+ }
154
+ const db = openQueue(options.root ?? effectiveInstanceRoot());
155
+ let selected: SeatGoal | undefined;
156
+ let reason = 'empty';
157
+ try {
158
+ db.exec('BEGIN IMMEDIATE');
159
+ try {
160
+ const at = clock(options.now);
161
+ const all = rows(db);
162
+ const byId = new Map(all.map((goal) => [goal.id, goal]));
163
+ for (const goal of all) {
164
+ if (goal.status !== 'queued') continue;
165
+ const failed = goal.prerequisites.some((id) => {
166
+ const prerequisite = byId.get(id);
167
+ return !prerequisite || (TERMINAL.has(prerequisite.status) && prerequisite.status !== 'succeeded');
168
+ });
169
+ if (failed || (goal.endAt && Date.parse(goal.endAt) <= at.getTime())) {
170
+ db.query("UPDATE seat_goals SET status = 'expired', finished_at = ? WHERE id = ? AND status = 'queued'")
171
+ .run(at.toISOString(), goal.id);
172
+ goal.status = 'expired';
173
+ goal.finishedAt = at.toISOString();
174
+ }
175
+ }
176
+ const running = all.filter((goal) => goal.status === 'running');
177
+ for (const goal of all) {
178
+ if (goal.status !== 'queued') continue;
179
+ if (goal.notBefore && Date.parse(goal.notBefore) > at.getTime()) { reason = 'not-before'; continue; }
180
+ if (goal.prerequisites.some((id) => byId.get(id)?.status !== 'succeeded')) { reason = 'prerequisites'; continue; }
181
+ if (running.length >= totalLimit) { reason = 'global-cap'; continue; }
182
+ if (running.filter((row) => row.seat === goal.seat).length >= (options.seatCaps[goal.seat] ?? totalLimit)) {
183
+ reason = 'seat-cap'; continue;
184
+ }
185
+ const startedAt = at.toISOString();
186
+ selected = { ...goal, status: 'running', startedAt };
187
+ db.query("UPDATE seat_goals SET status = 'running', started_at = ? WHERE id = ? AND status = 'queued'")
188
+ .run(startedAt, goal.id);
189
+ break;
190
+ }
191
+ db.exec('COMMIT');
192
+ if (!selected) return { outcome: all.some((goal) => goal.status === 'queued') ? 'waiting' : 'empty', reason };
193
+ } catch (error) { db.exec('ROLLBACK'); throw error; }
194
+ } finally { db.close(); }
195
+ // Keep the claim on an uncertain launch. Only a positive completion receipt can release it.
196
+ await options.launch(selected);
197
+ return { outcome: 'started', goal: selected, reason: 'launched' };
198
+ }
@@ -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
+ }