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
@@ -1,5 +1,7 @@
1
1
  import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { hostname } from 'node:os';
3
5
  import { parse as parseYaml } from 'yaml';
4
6
  import type { Database } from 'bun:sqlite';
5
7
  import { debug } from '../debug/log.js';
@@ -8,7 +10,10 @@ import { cronMatches } from '../domains/cron-match.js';
8
10
  import { resolveTimeZone } from '../time/format.js';
9
11
  import { inventoryCrontab, listSchedules, openSchedulesDb, type ScheduleRow } from '../domains/schedule-registry.js';
10
12
  import { effectiveInstanceRoot } from '../instance/resolve.js';
13
+ import { getUserConfig, type UserConfig } from '../user-config.js';
11
14
  import { runGraph, type GraphRunState } from '../graph-runner/runner.js';
15
+ import { FENCE_ROLES } from '../hq/hq.js';
16
+ import { dueBefore, type CheckEntry } from './checker.js';
12
17
 
13
18
  export interface LoopRun {
14
19
  at: string;
@@ -22,6 +27,9 @@ export interface LoopEntry {
22
27
  id: string;
23
28
  title: string;
24
29
  description: string | null;
30
+ owner?: string | null;
31
+ ownerSource?: 'header' | 'config' | 'seat' | 'seat-arg' | 'default';
32
+ mode?: string | null;
25
33
  file: string;
26
34
  trigger: { cron: string | null; events: string[] };
27
35
  jobs: Array<{ id: string; cron: string | null; enabled: boolean; entryScript?: boolean }>;
@@ -33,9 +41,13 @@ export interface LoopRegistryOptions {
33
41
  root?: string;
34
42
  stateRoot?: string;
35
43
  now?: Date;
44
+ /** Inject a parsed config for isolated inventory checks. */
45
+ config?: UserConfig;
36
46
  /** An already-inventoried registry; keeps focused tests isolated. */
37
47
  schedules?: ScheduleRow[];
38
48
  scheduleAction?: (action: 'create' | 'enable' | 'disable', args: Record<string, unknown>) => Promise<unknown>;
49
+ /** Home used to expand `~`/`$HOME` in cron redirects; tests inject a temp dir instead of writing the real HOME. */
50
+ home?: string;
39
51
  }
40
52
 
41
53
  function graphFiles(dir: string): string[] {
@@ -55,10 +67,16 @@ const LOOP_MARKER = /#\s*elanous-loop=([A-Za-z0-9][\w.-]*)\s*$/;
55
67
  export function graphJobMatches(command: string | null, file: string, root: string, graphId?: string): boolean {
56
68
  if (!command) return false;
57
69
  if (graphId && command.match(LOOP_MARKER)?.[1] === graphId) return true;
58
- const invocation = /(?:^|&&\s*|;\s*)(?:(?:\S*\/)?(?:bun|node)\s+(?:\S*\/)?bin\/elanous\.mjs|(?:\S*\/)?elanous)\s+(?:--test(?:=\S+|\s+\S+)\s+)?graph\s+run\s+(['"]?)([^\s'";&|]+)\1(?=\s|$|[;&|])/g;
59
- for (const match of command.matchAll(invocation)) {
60
- const arg = match[2]!;
61
- if (resolve(root, arg) === file || (isAbsolute(arg) && resolve(arg) === file)) return true;
70
+ for (const segment of shellSegments(command)) {
71
+ for (const words of launchedCommands(segment)) {
72
+ const { script, args } = launchedScript(words);
73
+ if (!isElanousExecutable(script)) continue;
74
+ const graph = args[0]?.text === '--test' ? (args[1]?.text === 'graph' ? 1 : 2)
75
+ : args[0]?.text.startsWith('--test=') ? 1 : 0;
76
+ if (args[graph]?.text !== 'graph' || args[graph + 1]?.text !== 'run' || !args[graph + 2]) continue;
77
+ const arg = args[graph + 2]!.text;
78
+ if (resolve(root, arg) === file || (isAbsolute(arg) && resolve(arg) === file)) return true;
79
+ }
62
80
  }
63
81
  return false;
64
82
  }
@@ -66,9 +84,16 @@ export function graphJobMatches(command: string | null, file: string, root: stri
66
84
  /** `cron_entry` identifies the script actually launched by cron, not text in another command's arguments. */
67
85
  function cronEntryMatches(command: string | null, entry: string): boolean {
68
86
  if (!command) return false;
69
- const invocation = command.match(/^(?:cd\s+\S+\s*&&\s*)?(?:(?:\S*\/)?(?:bun|node)\s+\S*cron-run\.ts\s+--schedule-id\s+\S+\s+--shell\s+)?(?:(?:\S*\/)?(?:bash|zsh|sh)\s+)?(['"]?)([^\s'";&|]+\.sh)\1(?=\s|$|[;&|])/);
70
- const script = invocation?.[2];
71
- return !!script && (script === entry || script.endsWith(`/${entry}`));
87
+ return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
88
+ let index = 0;
89
+ if (/(?:^|\/)(?:bun|node)$/.test(words[0]?.text ?? '') && /(?:^|\/)cron-run\.ts$/.test(words[1]?.text ?? '')) {
90
+ index = words.findIndex(word => word.text === '--shell') + 1;
91
+ if (index === 0) return false;
92
+ }
93
+ if (/(?:^|\/)(?:sh|bash|zsh)$/.test(words[index]?.text ?? '')) index++;
94
+ const script = words[index]?.text;
95
+ return !!script && /\.sh$/.test(script) && (script === entry || script.endsWith(`/${entry}`));
96
+ }));
72
97
  }
73
98
 
74
99
  function recentRuns(id: string, stateRoot: string, count = 5): LoopRun[] {
@@ -112,6 +137,46 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
112
137
  } finally { db.close(); }
113
138
  }
114
139
 
140
+ type OwnerSource = NonNullable<LoopEntry['ownerSource']>;
141
+ function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string, seatSource: OwnerSource = 'seat'): { owner: string; ownerSource: OwnerSource } {
142
+ if (typeof header === 'string' && /^(OP|TC|MK|UX)$/.test(header)) return { owner: header, ownerSource: 'header' };
143
+ const owners = config.loops?.owners;
144
+ const configured = owners && (Object.hasOwn(owners, id) ? owners[id] : Object.hasOwn(owners, title) ? owners[title] : undefined);
145
+ if (configured) return { owner: configured, ownerSource: 'config' };
146
+ if (seat) return { owner: seat, ownerSource: seatSource };
147
+ return { owner: config.loops?.defaultOwner ?? 'OP', ownerSource: 'default' };
148
+ }
149
+
150
+ /** Output redirects in the installed cron line (including a quoted hq-fence payload) estimate runs, not success. */
151
+ function cronLogMtime(command: string, home: string = homedir()): { at?: string; targets: number } {
152
+ const expandHome = (path: string) => path.replace(/^~(?=\/)/, home).replace(/^\$HOME(?=\/)/, home);
153
+ const operand = String.raw`(?:'([^']+)'|"([^"]+)"|([^\s;|&'"<>]+))`;
154
+ const cdPattern = new RegExp(String.raw`(?:^|[\s'"])cd\s+${operand}\s*&&`, 'g');
155
+ const redirectPattern = new RegExp(String.raw`(?:[12]|&)?>{1,2}\s*${operand}`, 'g');
156
+ const cds: Array<{ index: number; cwd: string }> = [];
157
+ for (const match of command.matchAll(cdPattern)) {
158
+ const path = expandHome(match[1] ?? match[2] ?? match[3]!);
159
+ cds.push({ index: match.index, cwd: resolve(cds.at(-1)?.cwd ?? home, path) });
160
+ }
161
+ let latest: number | undefined;
162
+ let targets = 0;
163
+ for (const match of command.matchAll(redirectPattern)) {
164
+ const path = expandHome(match[1] ?? match[2] ?? match[3]!);
165
+ if (path === '/dev/null') continue;
166
+ const cwd = [...cds].reverse().find(cd => cd.index < match.index)?.cwd ?? home;
167
+ const target = resolve(cwd, path);
168
+ targets++;
169
+ try {
170
+ const stat = statSync(target);
171
+ if (stat.isFile()) latest = Math.max(latest ?? -Infinity, stat.mtimeMs);
172
+ } catch (error) {
173
+ debug.log('loops.registry', 'cron-log-unavailable', { path: target, error: String(error) });
174
+ }
175
+ }
176
+ if (latest === undefined) debug.log('loops.registry', 'cron-log-unavailable', { command, targets, reason: 'no-output-files' });
177
+ return { at: latest === undefined ? undefined : new Date(latest).toISOString(), targets };
178
+ }
179
+
115
180
  /** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
116
181
  * the installed package when there is one, else this checkout. Never the caller's cwd: a loop started
117
182
  * from a disposable worktree would schedule a cron into a directory that is about to disappear (🅣 09-29). */
@@ -125,6 +190,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
125
190
  const root = resolve(opts.root ?? defaultLoopRoot());
126
191
  const stateRoot = opts.stateRoot ?? effectiveInstanceRoot();
127
192
  const rows = schedules(opts);
193
+ const config = opts.config ?? getUserConfig();
128
194
  const loops: LoopEntry[] = [];
129
195
  for (const file of graphFiles(join(root, 'graphs'))) {
130
196
  let doc: Record<string, unknown> | null;
@@ -149,8 +215,11 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
149
215
  ...(entryRows.includes(row) ? { entryScript: true } : {}) }));
150
216
  const active = matched.filter(row => !!row.enabled && !!row.cron);
151
217
  const next = active.map(row => nextLoopFire(row.cron!, opts.now ?? new Date())).filter((v): v is string => v !== null).sort()[0] ?? null;
152
- const entry: LoopEntry = { id: doc.graph_id, title: typeof header?.title === 'string' ? header.title : doc.graph_id,
153
- description: typeof header?.description === 'string' ? header.description : null, file: relative(root, file),
218
+ const title = typeof header?.title === 'string' ? header.title : doc.graph_id;
219
+ const entry: LoopEntry = { id: doc.graph_id, title,
220
+ description: typeof header?.description === 'string' ? header.description : null,
221
+ ...resolveOwner(doc.graph_id, title, config, header?.owner),
222
+ mode: typeof header?.mode === 'string' ? header.mode : null, file: relative(root, file),
154
223
  trigger: { cron: declaredCron, events }, jobs, enabled: matched.some(row => !!row.enabled),
155
224
  lastRun: recentRuns(doc.graph_id, stateRoot, 1)[0] ?? null, nextRun: next };
156
225
  loops.push(entry);
@@ -159,6 +228,463 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
159
228
  return loops;
160
229
  }
161
230
 
231
+ export interface AllLoopEntry extends CheckEntry {
232
+ kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator' | 'unregistered';
233
+ title: string;
234
+ cron?: string;
235
+ command?: string;
236
+ /** True when the cron's fire gaps vary (e.g. `0 9,17 * * *`); `expectEveryMinutes` is then the longest gap. */
237
+ cronIrregular?: boolean;
238
+ fenceRole?: string;
239
+ host?: string;
240
+ observationCategory?: string;
241
+ }
242
+
243
+ /** Inspect only words of the launched command. Quoted arguments are kept whole, not searched for flags. */
244
+ function launchedWords(command: string): Array<{ text: string; quoted: boolean }> {
245
+ const launched = command.replace(/^cd\s+(?:'[^']*'|"[^"]*"|\S+)\s*&&\s*/, '');
246
+ const words: Array<{ text: string; quoted: boolean }> = [];
247
+ let text = '';
248
+ let quoted = false;
249
+ let quote: string | null = null;
250
+ for (let i = 0; i < launched.length; i++) {
251
+ const ch = launched[i]!;
252
+ if (quote) {
253
+ if (ch === quote) quote = null;
254
+ else if (ch === '\\' && quote === '"' && i + 1 < launched.length) text += launched[++i];
255
+ else text += ch;
256
+ } else if (ch === "'" || ch === '"') { quote = ch; quoted = true; }
257
+ else if (ch === '\\' && i + 1 < launched.length) { text += launched[++i]; }
258
+ else if (ch === ';' || ch === '|' || ch === '&') break;
259
+ else if (/\s/.test(ch)) {
260
+ if (text || quoted) { words.push({ text, quoted }); text = ''; quoted = false; }
261
+ } else text += ch;
262
+ }
263
+ if (text || quoted) words.push({ text, quoted });
264
+ return words;
265
+ }
266
+
267
+ type LaunchWord = { text: string; quoted: boolean };
268
+
269
+ function isElanousExecutable(script: string): boolean {
270
+ return /^(?:eln|elanous|elanous\.mjs)$/.test(script.split('/').pop() ?? '');
271
+ }
272
+
273
+ /** Skip only leading global CLI options, leaving the action words untouched. */
274
+ function elanousActionStart(args: LaunchWord[]): number {
275
+ let start = 0;
276
+ while (args[start] && !args[start]!.quoted) {
277
+ const option = args[start]!.text;
278
+ if (option === '--test' || option.startsWith('--test=')) { start++; continue; }
279
+ if (option === '--config-dir' || option === '--state-dir') { start += 2; continue; }
280
+ if (option.startsWith('--config-dir=') || option.startsWith('--state-dir=')) { start++; continue; }
281
+ break;
282
+ }
283
+ return start;
284
+ }
285
+
286
+ /** Include every launched segment of a wrapper's quoted shell payload, in execution order. */
287
+ function launchedCommandsWithFence(command: string): Array<{ words: LaunchWord[]; fenceRole?: string }> {
288
+ const launches: Array<{ words: LaunchWord[]; fenceRole?: string }> = [];
289
+ launchedCommand(command, undefined, (words, fenceRole) => { if (words.length) launches.push({ words, ...(fenceRole ? { fenceRole } : {}) }); });
290
+ return launches;
291
+ }
292
+
293
+ function launchedCommands(command: string): LaunchWord[][] {
294
+ return launchedCommandsWithFence(command).map(({ words }) => words);
295
+ }
296
+
297
+ /** Only executable-prefix wrappers are stripped; a quoted argument mentioning a loop is not a launch. */
298
+ function launchedCommand(command: string, onFence?: (role: string) => void,
299
+ onLaunch?: (words: LaunchWord[], fenceRole?: string) => void, inheritedFenceRole?: string): LaunchWord[] {
300
+ const words = launchedWords(command);
301
+ let activeFenceRole = inheritedFenceRole;
302
+ const markFence = (role: string) => { activeFenceRole = role; onFence?.(role); };
303
+ const wrappers: Record<string, { values: readonly string[]; positionals: number }> = {
304
+ env: { values: ['-u', '--unset', '-C', '--chdir', '-S', '--split-string'], positionals: 0 },
305
+ flock: { values: ['-w', '--wait', '-E', '--conflict-exit-code'], positionals: 1 },
306
+ nice: { values: ['-n', '--adjustment'], positionals: 0 },
307
+ timeout: { values: ['-s', '--signal', '-k', '--kill-after'], positionals: 1 },
308
+ 'hq-fence': { values: [], positionals: 0 },
309
+ };
310
+ let offset = 0;
311
+ while (offset < words.length) {
312
+ if (/(?:^|\/)(?:bun|node)$/.test(words[offset]?.text ?? '')
313
+ && /(?:^|\/)cron-run\.ts$/.test(words[offset + 1]?.text ?? '')
314
+ && words[offset + 2]?.text === '--schedule-id' && words[offset + 4]?.text === '--shell') {
315
+ offset += 5;
316
+ continue;
317
+ }
318
+ const name = words[offset]!.text.split('/').pop()!;
319
+ const spec = wrappers[name];
320
+ if (!spec || words[offset]!.quoted) {
321
+ const { script, args } = launchedScript(words.slice(offset));
322
+ if (!isElanousExecutable(script) || args[0]?.text !== 'hq' || args[1]?.text !== 'fence') break;
323
+ const separator = args.findIndex(word => word.text === '--' && !word.quoted);
324
+ if (separator < 0) break;
325
+ for (let i = 2; i < separator; i++) {
326
+ const role = args[i]?.text === '--role' && i + 1 < separator ? args[i + 1]?.text
327
+ : args[i]?.text.startsWith('--role=') ? args[i]!.text.slice('--role='.length) : undefined;
328
+ if (role) { markFence(role); break; }
329
+ }
330
+ offset = words.length - args.length + separator + 1;
331
+ continue;
332
+ }
333
+ if (name === 'hq-fence') {
334
+ let commandAt = offset + 1;
335
+ if (words[commandAt]?.text === '--role' && !words[commandAt]?.quoted) {
336
+ const role = words[commandAt + 1]?.text;
337
+ if (role && FENCE_ROLES.includes(role as typeof FENCE_ROLES[number])) {
338
+ markFence(role);
339
+ commandAt += words[commandAt + 2]?.text === '--' ? 3 : 2;
340
+ }
341
+ } else if (FENCE_ROLES.includes(words[commandAt]?.text as typeof FENCE_ROLES[number]) && !words[commandAt]?.quoted) {
342
+ markFence(words[commandAt]!.text);
343
+ commandAt++;
344
+ }
345
+ if (words[commandAt]?.quoted && words[commandAt]!.text.includes(' ')) {
346
+ const parts = shellSegments(words[commandAt]!.text);
347
+ for (const part of parts.slice(0, -1)) {
348
+ launchedCommand(part, onFence, onLaunch, activeFenceRole);
349
+ }
350
+ return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
351
+ }
352
+ offset = commandAt;
353
+ continue;
354
+ }
355
+ let index = offset + 1;
356
+ while (index < words.length) {
357
+ const word = words[index]!;
358
+ if (word.text === '--' && !word.quoted) { index++; break; }
359
+ if (name === 'env' && /^[A-Za-z_][A-Za-z_0-9]*=/.test(word.text)) { index++; continue; }
360
+ if (name === 'nice' && /^-\d+$/.test(word.text) && !word.quoted) { index++; continue; }
361
+ if (!word.text.startsWith('-') || word.quoted) break;
362
+ const flag = word.text.split('=')[0]!;
363
+ index += spec.values.includes(flag) && !word.text.includes('=') ? 2 : 1;
364
+ }
365
+ index += spec.positionals;
366
+ if (name === 'flock' && words[index]?.text === '-c' && !words[index]?.quoted) {
367
+ const payload = words[index + 1]?.text;
368
+ if (!payload) return [];
369
+ // The payload is its own shell line: each segment may launch a loop.
370
+ const parts = shellSegments(payload);
371
+ for (const part of parts.slice(0, -1)) {
372
+ launchedCommand(part, onFence, onLaunch, activeFenceRole);
373
+ }
374
+ return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
375
+ }
376
+ if (index >= words.length) break;
377
+ offset = index;
378
+ }
379
+ const launch = words.slice(offset);
380
+ if (launch.length) onLaunch?.(launch, activeFenceRole);
381
+ return launch;
382
+ }
383
+
384
+ /** Split a shell line at top-level `&&`, `||`, `;` and `|` only — separators inside quotes stay in their word. */
385
+ export function shellSegments(command: string): string[] {
386
+ const segments: string[] = [];
387
+ let current = '';
388
+ let quote: '"' | "'" | null = null;
389
+ for (let i = 0; i < command.length; i++) {
390
+ const ch = command[i]!;
391
+ if (quote) {
392
+ current += ch;
393
+ if (ch === '\\' && quote === '"' && i + 1 < command.length) { current += command[++i]; continue; }
394
+ if (ch === quote) quote = null;
395
+ continue;
396
+ }
397
+ if (ch === '\\' && i + 1 < command.length) { current += ch + command[++i]; continue; }
398
+ if (ch === '"' || ch === "'") { quote = ch; current += ch; continue; }
399
+ const two = command.slice(i, i + 2);
400
+ if (two === '&&' || two === '||') { segments.push(current); current = ''; i++; continue; }
401
+ if (ch === ';' || ch === '|' || ch === '\n') { segments.push(current); current = ''; continue; }
402
+ current += ch;
403
+ }
404
+ segments.push(current);
405
+ return segments.map(segment => segment.trim()).filter(Boolean);
406
+ }
407
+
408
+ function cronFenceRole(command: string): string | undefined {
409
+ for (const segment of shellSegments(command)) {
410
+ let role: string | undefined;
411
+ launchedCommand(segment, value => { role = value; });
412
+ if (role) return role;
413
+ }
414
+ return undefined;
415
+ }
416
+
417
+ function fenceCommandTitle(command: string, role: string): string | undefined {
418
+ const launches = shellSegments(command).flatMap(segment => launchedCommands(segment));
419
+ const inner = launches.at(-1);
420
+ if (!inner?.length) return undefined;
421
+ const { script, args } = launchedScript(inner);
422
+ const executable = script.split('/').pop();
423
+ const action = elanousCronName(command) ?? (executable === 'git' ? [executable, args[0]?.text].filter(Boolean).join(' ')
424
+ : /^(?:sh|bash|zsh)$/.test(executable ?? '') ? args[0]?.text.split('/').pop() : executable);
425
+ return action ? `${action} (hq-fence ${role})` : undefined;
426
+ }
427
+
428
+ function launchedScript(words: LaunchWord[]): { script: string; args: LaunchWord[] } {
429
+ const offset = /^(?:.*\/)?(?:bun|node)$/.test(words[0]?.text ?? '') ? 1 : 0;
430
+ return { script: words[offset]?.text ?? '', args: words.slice(offset + 1) };
431
+ }
432
+
433
+ /** Return each launched action with its own fence role; one fenced segment cannot cover a sibling. */
434
+ export function cronLaunchedActionsWithFence(command: string): Array<{ script: string; args: string[]; fenceRole?: string }> {
435
+ return shellSegments(command).flatMap(segment => launchedCommandsWithFence(segment).map(({ words, fenceRole }) => {
436
+ const { script, args } = launchedScript(words);
437
+ return { script, args: args.slice(isElanousExecutable(script) ? elanousActionStart(args) : 0).map(word => word.text),
438
+ ...(fenceRole ? { fenceRole } : {}) };
439
+ }));
440
+ }
441
+
442
+ /** Recognize direct .sh execution or sh/bash/zsh with option flags and a script or -c payload. */
443
+ function isCronShellCommand(command: string): boolean {
444
+ return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
445
+ const executable = words[0]?.text ?? '';
446
+ if (/\.sh$/.test(executable)) return true;
447
+ if (!/(?:^|\/)(?:sh|bash|zsh)$/.test(executable)) return false;
448
+ let index = 1;
449
+ let inline = false;
450
+ while (words[index] && /^-[a-zA-Z]+$/.test(words[index]!.text) && !words[index]!.quoted) {
451
+ inline ||= words[index]!.text.includes('c');
452
+ index++;
453
+ }
454
+ if (words[index]?.text === '--') index++;
455
+ return !!words[index] && (inline || /\.sh$/.test(words[index]!.text));
456
+ }));
457
+ }
458
+
459
+ function launchedSeat(command: string): string | undefined {
460
+ for (const segment of shellSegments(command)) {
461
+ for (const words of launchedCommands(segment)) {
462
+ const { script, args } = launchedScript(words);
463
+ const named = script.match(/(?:^|\/)(op|tc|mk|ux)-seat(?:\.[\w-]+)?$/i)?.[1];
464
+ if (named) return named.toUpperCase();
465
+ if (!/(?:^|\/)seat-loop(?:\.[\w-]+)?$/i.test(script)) continue;
466
+ for (let i = 0; i < args.length; i++) {
467
+ const arg = args[i]!;
468
+ if (arg.quoted) continue;
469
+ const seat = arg.text === '--seat' ? (!args[i + 1]?.quoted ? args[i + 1]?.text : undefined)
470
+ : arg.text.startsWith('--seat=') ? arg.text.slice('--seat='.length) : undefined;
471
+ if (seat && /^(OP|TC|MK|UX)$/i.test(seat)) return seat.toUpperCase();
472
+ }
473
+ }
474
+ }
475
+ return undefined;
476
+ }
477
+
478
+ function seatLoopCommand(command: string): { seat?: string; matched: boolean } {
479
+ for (const segment of shellSegments(command)) {
480
+ for (const words of launchedCommands(segment)) {
481
+ const { script, args } = launchedScript(words);
482
+ if (!isElanousExecutable(script)) continue;
483
+ const action = args.slice(elanousActionStart(args));
484
+ if (action[0]?.quoted || action[0]?.text !== 'seat' || action[1]?.quoted || action[1]?.text !== 'loop') continue;
485
+ if (!action.slice(2).some(word => !word.quoted && word.text === '--once')) continue;
486
+ const seatAt = action.findIndex((word, index) => index >= 2 && !word.quoted && word.text === '--seat');
487
+ const seat = seatAt >= 0 && !action[seatAt + 1]?.quoted ? action[seatAt + 1]?.text
488
+ : action.slice(2).find(word => !word.quoted && word.text.startsWith('--seat='))?.text.slice('--seat='.length);
489
+ return { matched: true, ...(seat && /^(OP|TC|MK|UX)$/.test(seat) ? { seat } : {}) };
490
+ }
491
+ }
492
+ return { matched: false };
493
+ }
494
+
495
+ function launchedOrchestrator(command: string): boolean {
496
+ return shellSegments(command).some(segment => launchedCommands(segment).some(words =>
497
+ /(?:^|\/)(?:\w+-)?orchestrator(?:\.[\w-]+)?$/i.test(launchedScript(words).script)));
498
+ }
499
+
500
+ /** Name only recognizable CLI entry points. Unknown actions remain visible via the unregistered cron warning. */
501
+ function elanousCronName(command: string): string | undefined {
502
+ const known = new Set(['hq', 'harness', 'card', 'loop', 'graph', 'config', 'logs', 'where', '--version']);
503
+ for (const segment of shellSegments(command)) {
504
+ for (const words of launchedCommands(segment)) {
505
+ const { script, args } = launchedScript(words);
506
+ if (!isElanousExecutable(script)) continue;
507
+ const start = elanousActionStart(args);
508
+ const first = args[start];
509
+ const second = args[start + 1];
510
+ if (!first || first.quoted || !first.text || !known.has(first.text)) continue;
511
+ return second && !second.quoted && !second.text.startsWith('-') && !/^(?:\d*>|\d*>>|<|<<|&>|\|)$/.test(second.text)
512
+ ? `${first.text} ${second.text}` : first.text;
513
+ }
514
+ }
515
+ return undefined;
516
+ }
517
+
518
+ /** Number only unclaimed named jobs, in installed schedule order, for inventory and warning lookup alike. */
519
+ function namedCronIds(rows: ScheduleRow[], graphClaimed: Set<string>): Map<string, string> {
520
+ const counts = new Map<string, number>();
521
+ const ids = new Map<string, string>();
522
+ const candidates = rows.filter(row => !graphClaimed.has(row.id) && row.source === 'crontab'
523
+ && row.run_via === 'crontab' && row.disabled_reason !== 'vanished'
524
+ && !launchedSeat(row.command ?? '') && !seatLoopCommand(row.command ?? '').matched && !launchedOrchestrator(row.command ?? '')
525
+ && !isCronShellCommand(row.command ?? '') && !!elanousCronName(row.command ?? ''));
526
+ // Reserve the existing owner-key slots before assigning newly recognized commands.
527
+ const established = (command: string): boolean => {
528
+ const name = elanousCronName(command);
529
+ if (!name) return false;
530
+ if (name === 'harness queue') return /\bqueue\s+tick\b/.test(command);
531
+ return ['hq heartbeat', 'hq arbiter-check', 'card intake-scan', 'loop status', 'loop list'].includes(name);
532
+ };
533
+ for (const row of [...candidates.filter(row => established(row.command ?? '')),
534
+ ...candidates.filter(row => !established(row.command ?? ''))]) {
535
+ const name = elanousCronName(row.command ?? '')!;
536
+ const base = `elanous:${name.replace(/\s+/g, '-')}`;
537
+ const count = (counts.get(base) ?? 0) + 1;
538
+ counts.set(base, count);
539
+ ids.set(row.id, `${base}${count > 1 ? `-${count}` : ''}`);
540
+ }
541
+ return ids;
542
+ }
543
+
544
+ /** Gaps between consecutive actual fires, rather than a guess based on cron text. A cron whose gaps vary
545
+ * (e.g. `0 9,17 * * *` = 8h then 16h) is irregular: report the longest gap so «late» never fires early. */
546
+ const CRON_PERIOD_SAMPLES = 8;
547
+ function cronPeriod(cron: string | null, now: Date): { minutes: number; irregular: boolean } | undefined {
548
+ if (!cron) return undefined;
549
+ const fires: number[] = [];
550
+ let at = now;
551
+ for (let i = 0; i <= CRON_PERIOD_SAMPLES; i++) {
552
+ const next = nextLoopFire(cron, at);
553
+ if (!next) break;
554
+ fires.push(Date.parse(next));
555
+ at = new Date(next);
556
+ }
557
+ if (fires.length < 2) return undefined;
558
+ const gaps = fires.slice(1).map((fire, i) => (fire - fires[i]!) / 60_000);
559
+ const longest = Math.max(...gaps);
560
+ return { minutes: longest, irregular: gaps.some(gap => gap !== gaps[0]) };
561
+ }
562
+
563
+ function periodFields(period: { minutes: number; irregular: boolean } | undefined): Pick<AllLoopEntry, 'expectEveryMinutes' | 'cronIrregular'> {
564
+ if (!period) return {};
565
+ return { expectEveryMinutes: period.minutes, ...(period.irregular ? { cronIrregular: true } : {}) };
566
+ }
567
+
568
+ /** Inventory all installed loop agents without changing the graph-only `listLoops` contract. */
569
+ export function listAllLoops(opts: LoopRegistryOptions = {}): AllLoopEntry[] {
570
+ const now = opts.now ?? new Date();
571
+ const config = opts.config ?? getUserConfig();
572
+ const rows = schedules(opts).filter(row => row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
573
+ const graphOpts = { ...opts, now, schedules: rows };
574
+ const graphLoops = listLoops(graphOpts);
575
+ const claimed = new Set<string>();
576
+ const all: AllLoopEntry[] = graphLoops.map(loop => {
577
+ for (const job of loop.jobs) claimed.add(job.id);
578
+ // The installed job's cron is what actually runs — a disabled install still beats the declared trigger.
579
+ const cron = loop.jobs.find(job => job.enabled && job.cron)?.cron ?? loop.jobs.find(job => job.cron)?.cron ?? loop.trigger.cron;
580
+ const runs = recentRuns(loop.id, opts.stateRoot ?? effectiveInstanceRoot());
581
+ const lastRun = runs[0] ?? null;
582
+ const period = cronPeriod(cron, now);
583
+ const dueAt = cron ? dueBefore(cron, now) : null;
584
+ const installed = rows.filter(row => loop.jobs.some(item => item.id === row.id));
585
+ const fenceRole = installed.map(row => cronFenceRole(row.command ?? '')).find((role): role is string => !!role);
586
+ const graphDoc = parseYaml(readFileSync(join(resolve(opts.root ?? defaultLoopRoot()), loop.file), 'utf8')) as Record<string, unknown> | null;
587
+ const header = graphDoc?.loop && typeof graphDoc.loop === 'object' && !Array.isArray(graphDoc.loop)
588
+ ? graphDoc.loop as Record<string, unknown> : {};
589
+ const declaredHost = typeof header.host === 'string' && header.host ? header.host : undefined;
590
+ const declaredCategory = typeof header.observationCategory === 'string' && header.observationCategory ? header.observationCategory : undefined;
591
+ return { kind: 'graph', id: loop.id, title: loop.title, owner: loop.owner ?? undefined, ownerSource: loop.ownerSource,
592
+ ...(declaredHost || installed.length ? { host: declaredHost ?? hostname() } : {}),
593
+ observationCategory: declaredCategory ?? 'graph.runner',
594
+ ...(fenceRole ? { fenceRole } : {}),
595
+ mode: loop.mode ?? undefined, enabled: loop.enabled, registered: loop.jobs.length > 0,
596
+ ...(cron ? { cron } : {}), ...periodFields(period),
597
+ ...(dueAt ? { dueAt } : {}),
598
+ ...(lastRun ? { lastRunAt: lastRun.at, lastStatus: lastRun.status } : {}),
599
+ recentStatuses: runs.map(run => run.status) };
600
+ });
601
+ const namedIds = namedCronIds(rows, claimed);
602
+ for (const row of rows) {
603
+ if (claimed.has(row.id)) continue;
604
+ const command = row.command ?? '';
605
+ const seat = launchedSeat(command);
606
+ const seatArg = seatLoopCommand(command).seat;
607
+ const orchestrator = launchedOrchestrator(command);
608
+ const shell = isCronShellCommand(command);
609
+ const namedId = namedIds.get(row.id);
610
+ const fenceRole = cronFenceRole(command);
611
+ const fencedGitPush = fenceRole === 'git-push' && fenceCommandTitle(command, fenceRole) === 'git push (hq-fence git-push)';
612
+ const kind: AllLoopEntry['kind'] = seat || seatArg ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
613
+ if (kind === 'cron-shell' && !shell && !namedId && !fencedGitPush) continue;
614
+ const cron = row.cron;
615
+ const period = cronPeriod(cron, now);
616
+ const dueAt = cron ? dueBefore(cron, now) : null;
617
+ const id = seatArg ? `elanous:seat-loop:${seatArg}` : seat ? `${seat.toLowerCase()}-seat:${row.id}` : namedId ?? row.id;
618
+ if (seatArg && all.some(entry => entry.id === id)) continue;
619
+ const title = seatArg ? `${seatArg} seat loop` : seat ? `${seat} seat` : row.note || (fenceRole ? fenceCommandTitle(command, fenceRole) : undefined) || (namedId ? elanousCronName(command) : undefined) || row.name;
620
+ const { at: logMtime, targets } = (kind === 'cron-shell' || seatArg) && command ? cronLogMtime(command, opts.home) : { at: undefined, targets: 0 };
621
+ const recorded = row.last_run && Number.isFinite(Date.parse(row.last_run)) ? row.last_run : undefined;
622
+ const picked = logMtime && (!recorded || Date.parse(logMtime) >= Date.parse(recorded)) ? 'log-mtime'
623
+ : recorded ? 'registry' : 'none';
624
+ debug.log('loops.registry', 'cron-last-run', { id, targets, picked });
625
+ all.push({ kind, id, title,
626
+ ...resolveOwner(id, title, config, undefined, seatArg ?? seat, seatArg ? 'seat-arg' : 'seat'), ...(fenceRole ? { fenceRole } : {}),
627
+ host: hostname(), observationCategory: row.category,
628
+ enabled: !!row.enabled, registered: true,
629
+ ...(cron ? { cron } : {}), ...(command ? { command } : {}),
630
+ ...periodFields(period),
631
+ ...(dueAt ? { dueAt } : {}),
632
+ ...(picked === 'log-mtime' ? { lastRunAt: logMtime, evidence: picked }
633
+ : picked === 'registry' ? { lastRunAt: recorded, evidence: picked } : {}),
634
+ ...(row.last_status ? { lastStatus: row.last_status } : {}),
635
+ recentStatuses: row.last_status ? [row.last_status] : [] });
636
+ }
637
+ const warnings = unregisteredCronLoops(all, graphOpts);
638
+ for (const row of warnings) {
639
+ const title = `미등록: ${row.command ?? row.id}`;
640
+ all.push({ kind: 'unregistered', id: row.id, title,
641
+ ...resolveOwner(row.id, title, config), host: hostname(), observationCategory: row.category,
642
+ enabled: !!row.enabled, registered: false,
643
+ ...(row.cron ? { cron: row.cron } : {}), ...(row.command ? { command: row.command } : {}),
644
+ ...periodFields(cronPeriod(row.cron, now)), recentStatuses: [] });
645
+ }
646
+ const bySource: Record<OwnerSource, number> = { header: 0, config: 0, seat: 0, 'seat-arg': 0, default: 0 };
647
+ for (const loop of all) bySource[loop.ownerSource!]++;
648
+ debug.log('loop.check', 'ownership', { total: all.length, bySource });
649
+ debug.log('loops.registry', 'list-all', { count: all.length });
650
+ return all;
651
+ }
652
+
653
+ /** An unclaimed cron line with any elanous marker must surface as an unregistered warning. */
654
+ function looksElanousRelated(command: string): boolean {
655
+ return /(?:^|[^\w.-])(?:elanous(?:\.mjs)?|eln|hq-fence)(?=$|[^\w.-])|(?:^|\/)\.elanous(?:\/|$)/i.test(command);
656
+ }
657
+
658
+ /** Crontab elanous loop launches not represented by any graph, seat, shell or orchestrator row. */
659
+ export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistryOptions = {}): ScheduleRow[] {
660
+ const graphJobs = new Map(listLoops(opts).map(loop => [loop.id, loop.jobs.map(job => job.id)]));
661
+ const rows = schedules(opts);
662
+ const graphClaimed = new Set([...graphJobs.values()].flat());
663
+ const namedRows = new Map([...namedCronIds(rows, graphClaimed)].map(([rowId, name]) => [name, rowId]));
664
+ const accounted = entries.flatMap(entry => entry.kind === 'graph'
665
+ ? graphJobs.get(entry.id) ?? []
666
+ : entry.kind === 'unregistered' ? []
667
+ : [entry.kind === 'seat' && entry.id.startsWith('elanous:seat-loop:')
668
+ ? rows.find(row => row.command === entry.command && row.cron === entry.cron)?.id ?? entry.id
669
+ : entry.kind === 'seat' ? entry.id.slice(entry.id.indexOf(':') + 1)
670
+ : namedRows.get(entry.id) ?? entry.id]);
671
+ const claimed = new Set(accounted);
672
+ const related = rows.filter(row => row.source === 'crontab' && row.run_via === 'crontab'
673
+ && row.disabled_reason !== 'vanished' && row.disabled_reason !== 'manual'
674
+ && !!row.cron && looksElanousRelated(row.command ?? ''));
675
+ const warnings = related.filter(row => !claimed.has(row.id));
676
+ const relatedIds = new Set(related.map(row => row.id));
677
+ const registeredCount = accounted.filter(id => relatedIds.has(id)).length;
678
+ const counts = new Map<string, number>();
679
+ for (const id of accounted) counts.set(id, (counts.get(id) ?? 0) + 1);
680
+ const lines = related.filter(row => (counts.get(row.id) ?? 0) > 1);
681
+ if (related.length !== registeredCount + warnings.length || lines.length) {
682
+ debug.log('loops.registry', 'unaccounted', { lines: lines.map(row => row.raw ?? row.command ?? row.id) });
683
+ for (const row of lines) if (!warnings.includes(row)) warnings.push(row);
684
+ }
685
+ return warnings;
686
+ }
687
+
162
688
  export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
163
689
  const loop = listLoops(opts).find(entry => entry.id === id);
164
690
  if (!loop) throw new Error(`loop not found: ${id}`);