elanous 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (534) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,203 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { Database } from 'bun:sqlite';
4
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
5
+ import { LogStore, type LogStoreRow } from '../mss/logging/log-store.js';
6
+ import { checkLoops } from './checker.js';
7
+ import { defaultLoopRoot, listAllLoops, type AllLoopEntry } from './registry.js';
8
+ import type { ScheduleRow } from '../domains/schedule-registry.js';
9
+
10
+ export interface ActivityNode {
11
+ id: string;
12
+ kind: AllLoopEntry['kind'] | 'seat' | 'card' | 'event';
13
+ title: string;
14
+ state: string | null;
15
+ reason: string | null;
16
+ lastAt: string | null;
17
+ events: number;
18
+ }
19
+ export interface ActivityEdge {
20
+ from: string;
21
+ to: string;
22
+ kind: 'delegation' | 'request' | 'reply' | 'card';
23
+ count: number;
24
+ lastAt: string;
25
+ }
26
+ export interface LoopActivityResult {
27
+ since: string;
28
+ until: string;
29
+ nodes: ActivityNode[];
30
+ edges: ActivityEdge[];
31
+ }
32
+ export interface LoopActivityDeps {
33
+ listAllLoops?: (opts: { root: string; stateRoot: string; now: Date; schedules: ScheduleRow[] }) => AllLoopEntry[];
34
+ queryEvents?: (root: string, sinceMs: number, untilMs: number) => LogStoreRow[];
35
+ readJournal?: (root: string, name: string) => readonly Record<string, unknown>[];
36
+ readCards?: (root: string) => readonly { id: string; createdAt: string; sections: readonly { owner: string; createdAt: string }[] }[];
37
+ }
38
+
39
+ function readSchedules(root: string): ScheduleRow[] {
40
+ const path = join(root, 'schedules.db');
41
+ if (!existsSync(path)) return [];
42
+ const db = new Database(path, { readonly: true });
43
+ try {
44
+ if (!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schedule_registry'").get()) return [];
45
+ const columns = (db.query('PRAGMA table_info(schedule_registry)').all() as Array<{ name: string }>).map(row => row.name);
46
+ if (!columns.includes('run_via')) return [];
47
+ return db.query('SELECT * FROM schedule_registry').all() as ScheduleRow[];
48
+ } finally { db.close(); }
49
+ }
50
+
51
+ function readEvents(root: string, sinceMs: number, untilMs: number): LogStoreRow[] {
52
+ const path = join(root, 'logs', 'logs.db');
53
+ if (!existsSync(path)) return [];
54
+ const store = LogStore.openReadOnly(path);
55
+ try { return store.queryAll({ categories: ['loop.', 'seat.loop'], sinceMs, untilMs }); }
56
+ finally { store.close(); }
57
+ }
58
+
59
+ function parseObject(value: string): Record<string, unknown> {
60
+ try {
61
+ const parsed: unknown = JSON.parse(value);
62
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
63
+ } catch { return {}; }
64
+ }
65
+
66
+ function readJournal(root: string, name: string): Record<string, unknown>[] {
67
+ const path = join(root, 'seat-requests', name);
68
+ return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(parseObject) : [];
69
+ }
70
+
71
+ function readCards(root: string): { id: string; createdAt: string; sections: { owner: string; createdAt: string }[] }[] {
72
+ const dir = join(root, 'task-cards');
73
+ if (!existsSync(dir)) return [];
74
+ return readdirSync(dir).filter(file => file.endsWith('.jsonl')).flatMap(file => {
75
+ const entries = readFileSync(join(dir, file), 'utf8').split('\n').filter(Boolean).map(parseObject);
76
+ const created = entries.find(row => row.type === 'created');
77
+ if (!created || typeof created.id !== 'string' || typeof created.createdAt !== 'string') return [];
78
+ return [{ id: created.id, createdAt: created.createdAt,
79
+ sections: entries.filter(row => row.type === 'section' && typeof row.owner === 'string' && typeof row.createdAt === 'string')
80
+ .map(row => ({ owner: row.owner as string, createdAt: row.createdAt as string })) }];
81
+ });
82
+ }
83
+
84
+ function text(value: unknown): string | null {
85
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
86
+ }
87
+
88
+ function loopId(value: unknown): string | null {
89
+ const id = text(value);
90
+ if (!id) return null;
91
+ const seat = id.toUpperCase();
92
+ if (seat === 'OP' || seat === 'TC' || seat === 'UX') return `${seat.toLowerCase()}-seat`;
93
+ if (seat === 'MK' || seat === 'MK-SEAT') return 'cmo-seat';
94
+ return id;
95
+ }
96
+
97
+ function cardNodeId(value: unknown): string | null {
98
+ const id = text(value);
99
+ return id ? (id.startsWith('card:') ? id : `card:${id}`) : null;
100
+ }
101
+
102
+ /** Read-only registry/checker snapshot plus observed edges in the inclusive time window. */
103
+ export function loopActivity({ since, until, root = effectiveInstanceRoot(), deps = {} }: {
104
+ since: string | Date;
105
+ until?: string | Date;
106
+ root?: string;
107
+ deps?: LoopActivityDeps;
108
+ }): LoopActivityResult {
109
+ const end = until === undefined ? new Date() : new Date(until);
110
+ const duration = typeof since === 'string' ? /^(\d+)([mhd])$/.exec(since) : null;
111
+ const durationMs = duration ? Number(duration[1]) * ({ m: 60_000, h: 3_600_000, d: 86_400_000 }[duration[2]!] ?? NaN) : NaN;
112
+ const start = duration ? new Date(end.getTime() - durationMs) : new Date(since);
113
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start > end) throw new Error('invalid loop activity window');
114
+ const inventory = (deps.listAllLoops ?? listAllLoops)({ root: deps.listAllLoops ? root : defaultLoopRoot(), stateRoot: root, now: end, schedules: deps.listAllLoops ? [] : readSchedules(root) });
115
+ const checked = checkLoops(inventory, end);
116
+ const byId = new Map(inventory.map(entry => [entry.id, entry]));
117
+ const nodes = new Map<string, ActivityNode>(checked.map(entry => [entry.id, {
118
+ id: entry.id, kind: byId.get(entry.id)?.kind ?? 'graph',
119
+ title: byId.get(entry.id)?.title ?? entry.id,
120
+ state: entry.state, reason: entry.reason, lastAt: entry.lastRunAt ?? null, events: 0,
121
+ }]));
122
+ const edges = new Map<string, ActivityEdge>();
123
+ const ensure = (id: string, kind: ActivityNode['kind'] = 'event'): ActivityNode => {
124
+ let node = nodes.get(id);
125
+ if (!node) {
126
+ node = { id, kind, title: id, state: null, reason: null, lastAt: null, events: 0 };
127
+ nodes.set(id, node);
128
+ }
129
+ return node;
130
+ };
131
+ const inWindow = (value: unknown): string | null => {
132
+ const ms = typeof value === 'string' ? Date.parse(value) : NaN;
133
+ return Number.isFinite(ms) && ms >= start.getTime() && ms <= end.getTime() ? new Date(ms).toISOString() : null;
134
+ };
135
+ const touch = (id: string, at: string, kind: ActivityNode['kind'] = 'event'): void => {
136
+ const node = ensure(id, kind);
137
+ node.events++;
138
+ if (!node.lastAt || at > node.lastAt) node.lastAt = at;
139
+ };
140
+ const connect = (from: string, to: string, kind: ActivityEdge['kind'], at: string): void => {
141
+ if (from === to) return;
142
+ ensure(from, from.startsWith('card:') ? 'card' : 'event');
143
+ ensure(to, to.startsWith('card:') ? 'card' : 'event');
144
+ const key = JSON.stringify([from, to, kind]);
145
+ const edge = edges.get(key);
146
+ if (edge) { edge.count++; if (at > edge.lastAt) edge.lastAt = at; }
147
+ else edges.set(key, { from, to, kind, count: 1, lastAt: at });
148
+ };
149
+ for (const row of (deps.queryEvents ?? readEvents)(root, start.getTime(), end.getTime())) {
150
+ const timestamp = Number.isFinite(row.ts_ms) ? row.ts_ms : Date.parse(row.ts);
151
+ if (!Number.isFinite(timestamp) || timestamp < start.getTime() || timestamp > end.getTime()) continue;
152
+ const data = parseObject(row.data ?? '');
153
+ const actor = row.category.startsWith('loop.') && !['loop.checker', 'loop.observe', 'loop.registry', 'loop.neighbors', 'loop.package'].includes(row.category)
154
+ ? loopId(row.category.slice('loop.'.length)) : row.category === 'seat.loop' ? loopId(data.seat) : null;
155
+ if (!actor) continue;
156
+ const at = new Date(timestamp).toISOString();
157
+ touch(actor, at);
158
+ if (row.event === 'exchange' && text(data.reason) === 'delegation') {
159
+ const to = loopId(data.to ?? data.targetLoopId);
160
+ if (to) connect(loopId(data.from) ?? actor, to, 'delegation', at);
161
+ } else if (row.category === 'loop.orchestrator' && row.event === 'exchange' && data.reason === 'queued') {
162
+ const to = loopId(data.targetLoopId);
163
+ if (to) connect(actor, to, 'delegation', at);
164
+ } else if (row.event === 'exchange' && (data.reason === 'request' || data.reason === 'reply' || data.reason === 'card')) {
165
+ const to = data.reason === 'card' && data.cardId != null
166
+ ? cardNodeId(data.cardId) : loopId(data.to ?? data.targetLoopId);
167
+ if (to) connect(loopId(data.from) ?? actor, to, data.reason, at);
168
+ }
169
+ }
170
+ for (const item of (deps.readJournal ?? readJournal)(root, 'requests.jsonl')) {
171
+ const at = inWindow(item.queuedAt);
172
+ const to = loopId(item.loopId ?? item.seat);
173
+ if (!at || !to) continue;
174
+ touch(to, at, 'seat');
175
+ const from = loopId(item.from) ?? (item.source === 'orchestrator' || item.source === 'orchestrator-traffic' ? 'orchestrator'
176
+ : item.source === 'loop-checker' ? 'checker' : null);
177
+ if (from) connect(from, to, 'request', at);
178
+ }
179
+ for (const item of (deps.readJournal ?? readJournal)(root, 'replies.jsonl')) {
180
+ const at = inWindow(item.repliedAt ?? item.createdAt);
181
+ const from = loopId(item.from ?? item.seat);
182
+ if (!at || !from) continue;
183
+ touch(from, at, 'seat');
184
+ const to = loopId(item.to);
185
+ if (to) connect(from, to, 'reply', at);
186
+ }
187
+ for (const card of (deps.readCards ?? readCards)(root)) {
188
+ const id = cardNodeId(card.id);
189
+ if (!id) continue;
190
+ const created = inWindow(card.createdAt);
191
+ if (created) touch(id, created, 'card');
192
+ for (const section of card.sections) {
193
+ const at = inWindow(section.createdAt);
194
+ const owner = loopId(section.owner);
195
+ if (!at || !owner) continue;
196
+ touch(id, at, 'card');
197
+ connect(owner, id, 'card', at);
198
+ }
199
+ }
200
+ return { since: start.toISOString(), until: end.toISOString(),
201
+ nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
202
+ edges: [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind)) };
203
+ }
@@ -0,0 +1,32 @@
1
+ import { ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../user-config.js';
2
+
3
+ export interface SpawnBudgetInput {
4
+ seat: OrchestratorSeat;
5
+ /** Current running count for this seat; null/undefined means the ledger could not be measured. */
6
+ running: number | null | undefined;
7
+ /** Configured loops.orchestrator.seatCaps. Missing seats use the orchestrator defaults. */
8
+ caps?: Partial<Record<OrchestratorSeat, number>>;
9
+ /** Release-gate seat limits. Missing seats use the gate default of four. */
10
+ gate?: Partial<Record<OrchestratorSeat, number>>;
11
+ }
12
+
13
+ export interface SpawnBudgetDecision {
14
+ allow: boolean;
15
+ reason: 'within-cap' | 'seat-cap' | 'unknown-running';
16
+ cap: number;
17
+ }
18
+
19
+ /** A release gate can lower, but never raise, the configured per-seat cap. */
20
+ export function decideSpawn({ seat, running, caps, gate }: SpawnBudgetInput): SpawnBudgetDecision {
21
+ const configured = caps?.[seat] ?? ORCHESTRATOR_DEFAULTS.seatCaps[seat];
22
+ const gated = gate?.[seat] ?? 4;
23
+ const cap = Number.isSafeInteger(configured) && configured >= 0 && Number.isSafeInteger(gated) && gated >= 0
24
+ ? Math.min(configured, gated)
25
+ : 0;
26
+ if (!Number.isSafeInteger(running) || running === null || running === undefined || running < 0) {
27
+ return { allow: false, reason: 'unknown-running', cap };
28
+ }
29
+ return running < cap
30
+ ? { allow: true, reason: 'within-cap', cap }
31
+ : { allow: false, reason: 'seat-cap', cap };
32
+ }
@@ -0,0 +1,180 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { Database } from 'bun:sqlite';
5
+ import { debug } from '../debug/log.js';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import * as registry from './registry.js';
8
+ import { cronMatches } from '../domains/cron-match.js';
9
+ import { resolveTimeZone } from '../time/format.js';
10
+ import type { LoopEntry, LoopRun } from './registry.js';
11
+
12
+ type LoopState = 'off' | 'unregistered' | 'failing' | 'late' | 'unknown' | 'alive';
13
+ export type LoopScope = 'graph-only' | 'registry';
14
+ export interface CheckEntry {
15
+ id: string;
16
+ owner?: string;
17
+ ownerSource?: 'header' | 'config' | 'seat' | 'default';
18
+ evidence?: 'log-mtime';
19
+ mode?: string;
20
+ expectEveryMinutes?: number;
21
+ /** The scheduled fire before the latest one (cron loops). A run older than this missed two scheduled fires. */
22
+ dueAt?: string;
23
+ lastRunAt?: string;
24
+ lastStatus?: string;
25
+ recentStatuses?: string[];
26
+ enabled: boolean;
27
+ registered: boolean;
28
+ }
29
+ export interface CheckResult extends CheckEntry {
30
+ state: LoopState;
31
+ reason: string;
32
+ }
33
+ export const LOOP_STATES = ['alive', 'late', 'failing', 'off', 'unregistered', 'unknown'] as const;
34
+ export type LoopCounts = Record<LoopState, number>;
35
+
36
+ export function checkLoops(entries: CheckEntry[], now: Date, opts: { failureStatuses?: readonly string[] } = {}): CheckResult[] {
37
+ const failures = opts.failureStatuses ?? ['failed'];
38
+ return entries.map(entry => {
39
+ let state: LoopState;
40
+ let reason: string;
41
+ const age = entry.lastRunAt ? now.getTime() - Date.parse(entry.lastRunAt) : NaN;
42
+ if (!entry.enabled) { state = 'off'; reason = 'disabled'; }
43
+ else if (!entry.registered) { state = 'unregistered'; reason = 'not registered'; }
44
+ else if (entry.recentStatuses?.length && entry.recentStatuses.length >= 3
45
+ && entry.recentStatuses.slice(0, 3).every(status => failures.includes(status))) {
46
+ state = 'failing'; reason = 'last 3 runs failed';
47
+ } else if (Number.isFinite(age) && entry.dueAt && Number.isFinite(Date.parse(entry.dueAt))) {
48
+ // Judge against the real schedule, not a fixed interval: overnight or weekend gaps are not lateness.
49
+ if (Date.parse(entry.lastRunAt!) < Date.parse(entry.dueAt)) { state = 'late'; reason = `missed the scheduled runs since ${entry.dueAt}`; }
50
+ else { state = 'alive'; reason = 'ran at or after the previous scheduled fire'; }
51
+ } else if (Number.isFinite(age) && entry.expectEveryMinutes !== undefined
52
+ && entry.expectEveryMinutes > 0 && age > entry.expectEveryMinutes * 2 * 60_000) {
53
+ state = 'late'; reason = `last run exceeds twice the expected ${entry.expectEveryMinutes}m interval`;
54
+ } else if (!entry.lastRunAt || !Number.isFinite(age)) { state = 'unknown'; reason = 'no valid run recorded'; }
55
+ else if (entry.expectEveryMinutes === undefined || !Number.isFinite(entry.expectEveryMinutes) || entry.expectEveryMinutes <= 0) {
56
+ state = 'unknown'; reason = 'expected interval unknown';
57
+ } else { state = 'alive'; reason = 'within expected interval'; }
58
+ return { ...entry, state, reason };
59
+ });
60
+ }
61
+
62
+ export function countLoopStates(results: CheckResult[]): LoopCounts {
63
+ const counts: LoopCounts = { alive: 0, late: 0, failing: 0, off: 0, unregistered: 0, unknown: 0 };
64
+ for (const result of results) counts[result.state]++;
65
+ return counts;
66
+ }
67
+
68
+ /** Latest cron fire at or before `at` (minute resolution, scans back up to one year). */
69
+ export function previousLoopFire(cron: string, at: Date, timeZone: string = resolveTimeZone().timeZone): string | null {
70
+ if (cron.trim().split(/\s+/).length !== 5) return null;
71
+ let minute = Math.floor(at.getTime() / 60000) * 60000;
72
+ for (let i = 0; i < 527040; i++, minute -= 60000) {
73
+ if (cronMatches(cron, new Date(minute), { timeZone })) return new Date(minute).toISOString();
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /** The fire before the latest scheduled fire — a run older than this has missed two scheduled fires. */
79
+ export function dueBefore(cron: string, now: Date, timeZone?: string): string | null {
80
+ const latest = previousLoopFire(cron, now, timeZone);
81
+ return latest ? previousLoopFire(cron, new Date(Date.parse(latest) - 60000), timeZone) : null;
82
+ }
83
+
84
+ export interface RegistryAdapterDeps {
85
+ listLoops?: () => LoopEntry[];
86
+ listAllLoops?: () => CheckEntry[];
87
+ statusForLoop?: (id: string) => { recentRuns: Pick<LoopRun, 'status'>[] };
88
+ now?: Date;
89
+ timeZone?: string;
90
+ }
91
+
92
+ export function entriesFromRegistry(deps: RegistryAdapterDeps = {}): { entries: CheckEntry[]; scope: LoopScope } {
93
+ // An explicitly injected graph inventory remains the checker test/adapter fallback.
94
+ const all = deps.listAllLoops ?? (deps.listLoops ? undefined : registry.listAllLoops);
95
+ if (typeof all === 'function') return { entries: all(), scope: 'registry' };
96
+ const now = deps.now ?? new Date();
97
+ return { scope: 'graph-only', entries: (deps.listLoops ?? registry.listLoops)().map(loop => {
98
+ const first = loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, now) : null;
99
+ const second = first && loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, new Date(first)) : null;
100
+ const interval = first && second ? (Date.parse(second) - Date.parse(first)) / 60_000 : undefined;
101
+ const statuses = loop.lastRun
102
+ ? deps.statusForLoop ? deps.statusForLoop(loop.id).recentRuns.map(run => run.status)
103
+ : deps.listLoops ? [loop.lastRun.status]
104
+ : registry.loopStatus(loop.id).recentRuns.map(run => run.status)
105
+ : [];
106
+ const dueAt = loop.trigger.cron ? dueBefore(loop.trigger.cron, now, deps.timeZone) : null;
107
+ return { id: loop.id, owner: loop.owner ?? undefined, ownerSource: loop.ownerSource, enabled: loop.enabled, registered: true,
108
+ ...(interval !== undefined ? { expectEveryMinutes: interval } : {}),
109
+ ...(dueAt ? { dueAt } : {}),
110
+ ...(loop.lastRun ? { lastRunAt: loop.lastRun.at, lastStatus: loop.lastRun.status } : {}),
111
+ recentStatuses: statuses };
112
+ }) };
113
+ }
114
+
115
+ export interface CheckerDeps {
116
+ root?: string;
117
+ now?: Date;
118
+ }
119
+
120
+ /** Appends only new daily keys to the seat request journal; never rewrites existing lines. */
121
+ export function notifyOwners(results: CheckResult[], deps: CheckerDeps = {}): number {
122
+ const eligible = results.filter(result => result.owner && (result.state === 'late' || result.state === 'failing'));
123
+ if (!eligible.length) return 0;
124
+ const now = deps.now ?? new Date();
125
+ const path = join(deps.root ?? effectiveInstanceRoot(), 'seat-requests', 'requests.jsonl');
126
+ mkdirSync(dirname(path), { recursive: true });
127
+ // SQLite's OS locks are released when a process dies, including after SIGKILL.
128
+ // The journal itself remains append-only; this database only serializes check + append.
129
+ const lock = new Database(`${path}.loop-checker.lock.sqlite`);
130
+ try {
131
+ lock.exec('PRAGMA busy_timeout = 10000');
132
+ lock.exec('BEGIN EXCLUSIVE');
133
+ try {
134
+ const existing = new Set<string>();
135
+ if (existsSync(path)) {
136
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
137
+ if (line.trim()) {
138
+ const row = JSON.parse(line) as { key?: string };
139
+ if (row.key) existing.add(row.key);
140
+ }
141
+ }
142
+ }
143
+ let added = 0;
144
+ for (const result of eligible) {
145
+ const key = `loopcheck:${result.id}:${now.toISOString().slice(0, 10)}:${result.state}`;
146
+ if (existing.has(key)) continue;
147
+ const row = { key, receiptId: key, seat: result.owner, text: `${result.id} ${result.state} — 마지막 ${result.lastRunAt ?? '없음'} · ${result.reason}`,
148
+ status: 'queued', queuedAt: now.toISOString(), source: 'loop-checker' };
149
+ appendFileSync(path, JSON.stringify(row) + '\n');
150
+ existing.add(key);
151
+ added++;
152
+ }
153
+ lock.exec('COMMIT');
154
+ return added;
155
+ } catch (error) {
156
+ lock.exec('ROLLBACK');
157
+ throw error;
158
+ }
159
+ } finally { lock.close(); }
160
+ }
161
+
162
+ /** Persist the previous comparison only for explicit all-status checks. Notification is separate. */
163
+ export function observeLoopCheck(results: CheckResult[], scope: LoopScope, deps: CheckerDeps = {}): LoopCounts {
164
+ const path = join(deps.root ?? effectiveInstanceRoot(), 'loop', 'checker', 'last.json');
165
+ let previous: Record<string, LoopState> = {};
166
+ try { previous = JSON.parse(readFileSync(path, 'utf8')) as Record<string, LoopState>; }
167
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
168
+ const current: Record<string, LoopState> = {};
169
+ for (const { id, state } of results) {
170
+ current[id] = state;
171
+ if (previous[id] && previous[id] !== state) debug.log('loop.checker', 'state-change', { id, from: previous[id], to: state });
172
+ }
173
+ const counts = countLoopStates(results);
174
+ debug.log('loop.checker', 'checked', { counts, scope });
175
+ mkdirSync(dirname(path), { recursive: true });
176
+ const tmp = `${path}.${randomUUID()}.tmp`;
177
+ try { writeFileSync(tmp, JSON.stringify(current)); renameSync(tmp, path); }
178
+ catch (error) { if (existsSync(tmp)) unlinkSync(tmp); throw error; }
179
+ return counts;
180
+ }
@@ -2,7 +2,7 @@ id: orchestrator
2
2
  version: 1.0.0
3
3
  kind: orchestrator
4
4
  role: docs/roles/OP.md
5
- graph: graphs/operate-loop.yaml
5
+ graph: graphs/orchestrator/orchestrator.yaml
6
6
  orchestration:
7
7
  windows:
8
8
  - {at: '08:00', delegateTo: [op-seat, cmo-seat, tc-seat, ux-seat], fromLedger: release/checklist-and-seat-requests}
@@ -1,6 +1,10 @@
1
1
  import type { Command } from 'commander';
2
2
  import { writeStdoutJson } from '../cli/stdout-json.js';
3
- import { listLoops, loopStatus, runLoop, setLoopEnabled } from './registry.js';
3
+ import { listAllLoops, listLoops, loopStatus, runLoop, setLoopEnabled, unregisteredCronLoops, type AllLoopEntry, type LoopRegistryOptions } from './registry.js';
4
+ import { checkLoops, countLoopStates, entriesFromRegistry, LOOP_STATES, notifyOwners, observeLoopCheck, type RegistryAdapterDeps } from './checker.js';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import { verifyLoopPackage } from './package/verify.js';
7
+ import { loopActivity } from './activity.js';
4
8
 
5
9
  async function output(value: unknown, json: boolean): Promise<void> {
6
10
  if (json) await writeStdoutJson(JSON.stringify(value) + '\n');
@@ -14,12 +18,62 @@ function handle(action: () => Promise<void> | void): Promise<void> {
14
18
  });
15
19
  }
16
20
 
17
- export function registerLoopCommands(program: Command): void {
21
+ function allLoopTable(entries: AllLoopEntry[]): void {
22
+ console.log('id\t층\t주인 자리\t모드\t기대 주기\t호스트\t울타리 역할\t관측 카테고리\t마지막 실행');
23
+ for (const entry of entries) {
24
+ const interval = entry.expectEveryMinutes === undefined ? '모름'
25
+ : entry.cronIrregular ? `불규칙 · 최대 ${entry.expectEveryMinutes}분` : `${entry.expectEveryMinutes}분`;
26
+ console.log([entry.id, entry.kind, entry.owner ?? '모름', entry.mode ?? '모름', interval,
27
+ entry.host ?? '모름', entry.fenceRole ?? '모름', entry.observationCategory ?? '모름', entry.lastRunAt ?? '모름'].join('\t'));
28
+ }
29
+ }
30
+
31
+ export function registerLoopCommands(program: Command, checkerDeps: RegistryAdapterDeps & { root?: string; registryOptions?: LoopRegistryOptions } = {}): void {
18
32
  const loop = program.command('loop').description('Inspect and control graph-backed loop agents');
19
- loop.command('list').option('--json', 'Print JSON')
20
- .action((opts: { json?: boolean }) => handle(() => output(listLoops(), !!opts.json)));
21
- loop.command('status <id>').option('--json', 'Print JSON')
22
- .action((id: string, opts: { json?: boolean }) => handle(() => output(loopStatus(id), !!opts.json)));
33
+ loop.command('list').option('--all', 'Show the full loop registry').option('--json', 'Print JSON')
34
+ .action((opts: { all?: boolean; json?: boolean }) => handle(async () => {
35
+ if (!opts.all) { await output(listLoops(checkerDeps.registryOptions), !!opts.json); return; }
36
+ const registryOpts = checkerDeps.registryOptions ?? {};
37
+ const entries = listAllLoops(registryOpts);
38
+ if (opts.json) { await output(entries, true); return; }
39
+ allLoopTable(entries);
40
+ const missing = unregisteredCronLoops(entries, registryOpts);
41
+ if (missing.length) console.log(`미등록 ${missing.length}: ${missing.map(row => `${row.id} (${row.command})`).join(', ')}`);
42
+ }));
43
+ loop.command('activity').description('Read loop and seat activity in a time window (default: last 24h)')
44
+ .option('--since <duration-or-date>', 'Start of window: duration (e.g. 24h) or ISO date', '24h')
45
+ .option('--json', 'Print JSON')
46
+ .action((opts: { since: string; json?: boolean }) => handle(async () => {
47
+ const activity = loopActivity({ since: opts.since, root: checkerDeps.root ?? effectiveInstanceRoot() });
48
+ if (opts.json) { await output(activity, true); return; }
49
+ console.log(`Loop activity ${activity.since} → ${activity.until}`);
50
+ console.log(`${activity.nodes.length} nodes · ${activity.edges.length} edges`);
51
+ console.log('Nodes (id · state · events · last activity)');
52
+ for (const node of activity.nodes) console.log(`${node.id} · ${node.state ?? '—'} · ${node.events} · ${node.lastAt ?? '—'}`);
53
+ console.log('Edges (from → to · kind · count · last activity)');
54
+ for (const edge of activity.edges) console.log(`${edge.from} → ${edge.to} · ${edge.kind} · ${edge.count} · ${edge.lastAt}`);
55
+ }));
56
+ loop.command('status [id]').option('--all', 'Check all registered loops')
57
+ .option('--json', 'Print JSON').option('--notify', 'Queue late/failing loops for their owners')
58
+ .action((id: string | undefined, opts: { all?: boolean; json?: boolean; notify?: boolean }) => handle(async () => {
59
+ if (!opts.all) {
60
+ if (!id) throw new Error('loop status requires an id or --all');
61
+ await output(loopStatus(id), !!opts.json);
62
+ return;
63
+ }
64
+ if (id) throw new Error('loop status --all does not take an id');
65
+ const now = checkerDeps.now ?? new Date();
66
+ const root = checkerDeps.root ?? effectiveInstanceRoot();
67
+ const { entries, scope } = entriesFromRegistry({ ...checkerDeps, now });
68
+ const results = checkLoops(entries, now);
69
+ const counts = countLoopStates(results);
70
+ observeLoopCheck(results, scope, { root, now });
71
+ if (opts.notify) notifyOwners(results, { root, now });
72
+ if (opts.json) { await output({ results, counts, scope }, true); return; }
73
+ console.log('id\tstate\tlast run\texpected interval\towner\treason');
74
+ for (const result of results) console.log(`${result.id}\t${result.state}\t${result.lastRunAt ?? '—'}\t${result.expectEveryMinutes === undefined ? '—' : `${result.expectEveryMinutes}m`}\t${result.owner ?? '—'}\t${result.reason}`);
75
+ console.log(`${LOOP_STATES.map(state => `${state} ${counts[state]}`).join(' · ')} · scope ${scope}`);
76
+ }));
23
77
  for (const [name, enabled] of [['start', true], ['stop', false]] as const) {
24
78
  loop.command(`${name} <id>`).option('--yes', 'Apply the crontab change (otherwise preview only)')
25
79
  .option('--json', 'Print JSON')
@@ -34,4 +88,16 @@ export function registerLoopCommands(program: Command): void {
34
88
  await output(state, !!opts.json);
35
89
  if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
36
90
  }));
91
+ loop.command('package').description('Inspect a loop package without installing it')
92
+ .command('verify <dir>').option('--json', 'Print JSON')
93
+ .action((dir: string, opts: { json?: boolean }) => handle(async () => {
94
+ const result = verifyLoopPackage(dir);
95
+ if (opts.json) await output(result, true);
96
+ else {
97
+ if (result.ok) console.log('OK');
98
+ else for (const error of result.errors) console.log(error);
99
+ console.log(`loop-package ${String(result.merged?.id ?? '?')} ${String(result.merged?.version ?? '?')} ${result.ok ? 'ok' : 'refused'} errors=${result.errors.length}`);
100
+ }
101
+ if (!result.ok) process.exitCode = 1;
102
+ }));
37
103
  }
@@ -0,0 +1,40 @@
1
+ import { debug } from '../debug/log.js';
2
+
3
+ /** The neighbor fields validated by src/loops/contract/validate.ts. */
4
+ export interface Neighbor {
5
+ id: string;
6
+ exchange: ('status' | 'request' | 'decision' | 'blocked')[];
7
+ heartbeat: { everyMinutes: number; missedTicks: number };
8
+ onAbsent: { action: 'delegate' | 'defer' | 'escalate'; delegateTo: string };
9
+ }
10
+
11
+ export interface NeighborJudgment {
12
+ state: 'present' | 'absent' | 'unknown';
13
+ action?: Neighbor['onAbsent'];
14
+ reason: string;
15
+ }
16
+
17
+ /** lastSeenAt is the latest ACK of a heartbeat or envelope, not the last send attempt. */
18
+ export function judgeNeighbor({ neighbor, lastSeenAt, lastSendError, now }: {
19
+ neighbor: Neighbor;
20
+ lastSeenAt?: string | null;
21
+ lastSendError?: unknown;
22
+ now: Date;
23
+ }): NeighborJudgment {
24
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
25
+ let result: NeighborJudgment;
26
+ if (lastSendError != null) {
27
+ result = { state: 'unknown', reason: 'send-failed' };
28
+ } else if (!Number.isFinite(seen) || !Number.isFinite(now.getTime()) || seen > now.getTime()) {
29
+ result = { state: 'unknown', reason: 'last-ack-unknown' };
30
+ } else if (now.getTime() - seen >= neighbor.heartbeat.everyMinutes * neighbor.heartbeat.missedTicks * 60_000) {
31
+ result = { state: 'absent', action: neighbor.onAbsent, reason: 'heartbeat-missed' };
32
+ } else {
33
+ result = { state: 'present', reason: 'within-heartbeat-window' };
34
+ }
35
+ // The validated neighbor entry does not carry its owning loop id; do not invent one.
36
+ debug.log('loop.neighbors', 'judged', {
37
+ loop: null, neighbor: neighbor.id, state: result.state, action: result.action, reason: result.reason,
38
+ });
39
+ return result;
40
+ }
@@ -0,0 +1,59 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { recordFailureEvent } from '../self-implement/heal-intake.js';
3
+
4
+ /** Event vocabulary of the loop-agent manifest observability contract. */
5
+ export const LOOP_EVENTS = [
6
+ 'tick', 'posture-change', 'resolution-change', 'grounding', 'heartbeat',
7
+ 'absent', 'exchange', 'spawn', 'reap', 'promote', 'hitl',
8
+ ] as const;
9
+
10
+ const LOOP_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
11
+ const REQUIRED_CONTEXT = ['runId', 'profile', 'reason', 'sourceRef'] as const;
12
+
13
+ function validId(id: unknown): id is string {
14
+ return typeof id === 'string' && LOOP_ID.test(id);
15
+ }
16
+
17
+ function reject(reason: 'invalid-loop-id' | 'invalid-event' | 'invalid-data'): void {
18
+ // Never include untrusted identifiers, event names or payloads in a log category.
19
+ debug.log('loop.observe', 'rejected', { reason });
20
+ }
21
+
22
+ /** Category matches observability.category in the loop-agent manifest. */
23
+ export function loopCategory(id: string): string {
24
+ if (!validId(id)) {
25
+ reject('invalid-loop-id');
26
+ throw new Error('invalid loop id');
27
+ }
28
+ return `loop.${id}`;
29
+ }
30
+
31
+ /** Emit an RFC loop event, retaining absent context as a measurable list. */
32
+ export function loopEvent(loopId: string, event: string, data: Record<string, unknown>): void {
33
+ if (!validId(loopId)) {
34
+ reject('invalid-loop-id');
35
+ return;
36
+ }
37
+ if (typeof event !== 'string' || !LOOP_EVENTS.some(name => name === event)) {
38
+ reject('invalid-event');
39
+ return;
40
+ }
41
+ if (data === null || typeof data !== 'object' || Array.isArray(data)
42
+ || Object.getPrototypeOf(data) !== Object.prototype && Object.getPrototypeOf(data) !== null
43
+ || (Object.hasOwn(data, 'loopId') && data.loopId !== loopId)) {
44
+ reject('invalid-data');
45
+ return;
46
+ }
47
+ const missingRequired = REQUIRED_CONTEXT.filter(field =>
48
+ typeof data[field] !== 'string' || !(data[field] as string).trim());
49
+ debug.log(`loop.${loopId}`, event, { ...data, loopId, missingRequired });
50
+ if (event === 'tick' && (data.outcome === 'failed' || data.status === 'failed')) {
51
+ try {
52
+ recordFailureEvent({ source: 'loop-tick', kind: event, ref: loopId,
53
+ summary: `Loop ${loopId} tick failed`, at: new Date().toISOString() });
54
+ } catch (error) {
55
+ try { debug.log('heal.intake', 'record-failed', { source: 'loop-tick', kind: event, ref: loopId, error: String(error) }, { level: 'error' }); }
56
+ catch { /* Heal reporting must not change the tick outcome. */ }
57
+ }
58
+ }
59
+ }