elanous 0.2.14 → 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 (293) 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/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -28,7 +28,7 @@ export interface LoopEntry {
28
28
  title: string;
29
29
  description: string | null;
30
30
  owner?: string | null;
31
- ownerSource?: 'header' | 'config' | 'seat' | 'default';
31
+ ownerSource?: 'header' | 'config' | 'seat' | 'seat-arg' | 'default';
32
32
  mode?: string | null;
33
33
  file: string;
34
34
  trigger: { cron: string | null; events: string[] };
@@ -46,6 +46,8 @@ export interface LoopRegistryOptions {
46
46
  /** An already-inventoried registry; keeps focused tests isolated. */
47
47
  schedules?: ScheduleRow[];
48
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;
49
51
  }
50
52
 
51
53
  function graphFiles(dir: string): string[] {
@@ -136,29 +138,43 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
136
138
  }
137
139
 
138
140
  type OwnerSource = NonNullable<LoopEntry['ownerSource']>;
139
- function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string): { owner: string; ownerSource: OwnerSource } {
141
+ function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string, seatSource: OwnerSource = 'seat'): { owner: string; ownerSource: OwnerSource } {
140
142
  if (typeof header === 'string' && /^(OP|TC|MK|UX)$/.test(header)) return { owner: header, ownerSource: 'header' };
141
143
  const owners = config.loops?.owners;
142
144
  const configured = owners && (Object.hasOwn(owners, id) ? owners[id] : Object.hasOwn(owners, title) ? owners[title] : undefined);
143
145
  if (configured) return { owner: configured, ownerSource: 'config' };
144
- if (seat) return { owner: seat, ownerSource: 'seat' };
146
+ if (seat) return { owner: seat, ownerSource: seatSource };
145
147
  return { owner: config.loops?.defaultOwner ?? 'OP', ownerSource: 'default' };
146
148
  }
147
149
 
148
- /** An append redirection on the installed command is a run estimate, not a success record. */
149
- function cronLogMtime(command: string): string | undefined {
150
- const match = command.match(/(?:^|\s)>>\s*(?:'([^']+)'|"([^"]+)"|([^\s;&|]+))/);
151
- if (!match) return undefined;
152
- const path = (match[1] ?? match[2] ?? match[3]!).replace(/^~(?=\/)/, homedir()).replace(/^\$HOME(?=\/)/, homedir());
153
- const cd = command.match(/^cd\s+(?:'([^']+)'|"([^"]+)"|([^\s;&|]+))\s*&&/);
154
- const cwd = (cd?.[1] ?? cd?.[2] ?? cd?.[3] ?? homedir()).replace(/^~(?=\/)/, homedir()).replace(/^\$HOME(?=\/)/, homedir());
155
- try {
156
- const stat = statSync(resolve(cwd, path));
157
- return stat.isFile() ? stat.mtime.toISOString() : undefined;
158
- } catch (error) {
159
- debug.log('loops.registry', 'cron-log-unavailable', { path: resolve(cwd, path), error: String(error) });
160
- return undefined;
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
+ }
161
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 };
162
178
  }
163
179
 
164
180
  /** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
@@ -213,7 +229,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
213
229
  }
214
230
 
215
231
  export interface AllLoopEntry extends CheckEntry {
216
- kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator';
232
+ kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator' | 'unregistered';
217
233
  title: string;
218
234
  cron?: string;
219
235
  command?: string;
@@ -268,16 +284,22 @@ function elanousActionStart(args: LaunchWord[]): number {
268
284
  }
269
285
 
270
286
  /** Include every launched segment of a wrapper's quoted shell payload, in execution order. */
271
- function launchedCommands(command: string, onFence?: (role: string) => void): LaunchWord[][] {
272
- const launches: LaunchWord[][] = [];
273
- const last = launchedCommand(command, onFence, words => { if (words.length) launches.push(words); });
274
- if (last.length) launches.push(last);
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 } : {}) }); });
275
290
  return launches;
276
291
  }
277
292
 
293
+ function launchedCommands(command: string): LaunchWord[][] {
294
+ return launchedCommandsWithFence(command).map(({ words }) => words);
295
+ }
296
+
278
297
  /** Only executable-prefix wrappers are stripped; a quoted argument mentioning a loop is not a launch. */
279
- function launchedCommand(command: string, onFence?: (role: string) => void, onLaunch?: (words: LaunchWord[]) => void): LaunchWord[] {
298
+ function launchedCommand(command: string, onFence?: (role: string) => void,
299
+ onLaunch?: (words: LaunchWord[], fenceRole?: string) => void, inheritedFenceRole?: string): LaunchWord[] {
280
300
  const words = launchedWords(command);
301
+ let activeFenceRole = inheritedFenceRole;
302
+ const markFence = (role: string) => { activeFenceRole = role; onFence?.(role); };
281
303
  const wrappers: Record<string, { values: readonly string[]; positionals: number }> = {
282
304
  env: { values: ['-u', '--unset', '-C', '--chdir', '-S', '--split-string'], positionals: 0 },
283
305
  flock: { values: ['-w', '--wait', '-E', '--conflict-exit-code'], positionals: 1 },
@@ -303,7 +325,7 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
303
325
  for (let i = 2; i < separator; i++) {
304
326
  const role = args[i]?.text === '--role' && i + 1 < separator ? args[i + 1]?.text
305
327
  : args[i]?.text.startsWith('--role=') ? args[i]!.text.slice('--role='.length) : undefined;
306
- if (role) { onFence?.(role); break; }
328
+ if (role) { markFence(role); break; }
307
329
  }
308
330
  offset = words.length - args.length + separator + 1;
309
331
  continue;
@@ -313,20 +335,19 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
313
335
  if (words[commandAt]?.text === '--role' && !words[commandAt]?.quoted) {
314
336
  const role = words[commandAt + 1]?.text;
315
337
  if (role && FENCE_ROLES.includes(role as typeof FENCE_ROLES[number])) {
316
- onFence?.(role);
338
+ markFence(role);
317
339
  commandAt += words[commandAt + 2]?.text === '--' ? 3 : 2;
318
340
  }
319
341
  } else if (FENCE_ROLES.includes(words[commandAt]?.text as typeof FENCE_ROLES[number]) && !words[commandAt]?.quoted) {
320
- onFence?.(words[commandAt]!.text);
342
+ markFence(words[commandAt]!.text);
321
343
  commandAt++;
322
344
  }
323
345
  if (words[commandAt]?.quoted && words[commandAt]!.text.includes(' ')) {
324
346
  const parts = shellSegments(words[commandAt]!.text);
325
347
  for (const part of parts.slice(0, -1)) {
326
- const launch = launchedCommand(part, onFence, onLaunch);
327
- if (launch.length) onLaunch?.(launch);
348
+ launchedCommand(part, onFence, onLaunch, activeFenceRole);
328
349
  }
329
- return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
350
+ return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
330
351
  }
331
352
  offset = commandAt;
332
353
  continue;
@@ -348,15 +369,16 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
348
369
  // The payload is its own shell line: each segment may launch a loop.
349
370
  const parts = shellSegments(payload);
350
371
  for (const part of parts.slice(0, -1)) {
351
- const launch = launchedCommand(part, onFence, onLaunch);
352
- if (launch.length) onLaunch?.(launch);
372
+ launchedCommand(part, onFence, onLaunch, activeFenceRole);
353
373
  }
354
- return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
374
+ return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
355
375
  }
356
376
  if (index >= words.length) break;
357
377
  offset = index;
358
378
  }
359
- return words.slice(offset);
379
+ const launch = words.slice(offset);
380
+ if (launch.length) onLaunch?.(launch, activeFenceRole);
381
+ return launch;
360
382
  }
361
383
 
362
384
  /** Split a shell line at top-level `&&`, `||`, `;` and `|` only — separators inside quotes stay in their word. */
@@ -408,6 +430,15 @@ function launchedScript(words: LaunchWord[]): { script: string; args: LaunchWord
408
430
  return { script: words[offset]?.text ?? '', args: words.slice(offset + 1) };
409
431
  }
410
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
+
411
442
  /** Recognize direct .sh execution or sh/bash/zsh with option flags and a script or -c payload. */
412
443
  function isCronShellCommand(command: string): boolean {
413
444
  return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
@@ -444,13 +475,31 @@ function launchedSeat(command: string): string | undefined {
444
475
  return undefined;
445
476
  }
446
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
+
447
495
  function launchedOrchestrator(command: string): boolean {
448
496
  return shellSegments(command).some(segment => launchedCommands(segment).some(words =>
449
497
  /(?:^|\/)(?:\w+-)?orchestrator(?:\.[\w-]+)?$/i.test(launchedScript(words).script)));
450
498
  }
451
499
 
452
- /** A launched two-word elanous action, not a mention in an argument or a read-only CLI query. */
500
+ /** Name only recognizable CLI entry points. Unknown actions remain visible via the unregistered cron warning. */
453
501
  function elanousCronName(command: string): string | undefined {
502
+ const known = new Set(['hq', 'harness', 'card', 'loop', 'graph', 'config', 'logs', 'where', '--version']);
454
503
  for (const segment of shellSegments(command)) {
455
504
  for (const words of launchedCommands(segment)) {
456
505
  const { script, args } = launchedScript(words);
@@ -458,15 +507,9 @@ function elanousCronName(command: string): string | undefined {
458
507
  const start = elanousActionStart(args);
459
508
  const first = args[start];
460
509
  const second = args[start + 1];
461
- if (!first || !second || first.quoted || second.quoted || first.text.startsWith('-') || second.text.startsWith('-')) continue;
462
- const actions: Record<string, readonly string[]> = {
463
- hq: ['heartbeat', 'arbiter-check'],
464
- harness: ['queue'],
465
- card: ['intake-scan'],
466
- loop: ['status', 'list'],
467
- };
468
- if (actions[first.text]?.includes(second.text)
469
- && (first.text !== 'harness' || args[start + 2]?.text === 'tick')) return `${first.text} ${second.text}`;
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;
470
513
  }
471
514
  }
472
515
  return undefined;
@@ -476,12 +519,20 @@ function elanousCronName(command: string): string | undefined {
476
519
  function namedCronIds(rows: ScheduleRow[], graphClaimed: Set<string>): Map<string, string> {
477
520
  const counts = new Map<string, number>();
478
521
  const ids = new Map<string, string>();
479
- for (const row of rows) {
480
- if (graphClaimed.has(row.id) || row.source !== 'crontab' || row.run_via !== 'crontab' || row.disabled_reason === 'vanished') continue;
481
- const command = row.command ?? '';
482
- if (launchedSeat(command) || launchedOrchestrator(command) || isCronShellCommand(command)) continue;
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 => {
483
528
  const name = elanousCronName(command);
484
- if (!name) continue;
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 ?? '')!;
485
536
  const base = `elanous:${name.replace(/\s+/g, '-')}`;
486
537
  const count = (counts.get(base) ?? 0) + 1;
487
538
  counts.set(base, count);
@@ -552,32 +603,47 @@ export function listAllLoops(opts: LoopRegistryOptions = {}): AllLoopEntry[] {
552
603
  if (claimed.has(row.id)) continue;
553
604
  const command = row.command ?? '';
554
605
  const seat = launchedSeat(command);
606
+ const seatArg = seatLoopCommand(command).seat;
555
607
  const orchestrator = launchedOrchestrator(command);
556
608
  const shell = isCronShellCommand(command);
557
609
  const namedId = namedIds.get(row.id);
558
610
  const fenceRole = cronFenceRole(command);
559
611
  const fencedGitPush = fenceRole === 'git-push' && fenceCommandTitle(command, fenceRole) === 'git push (hq-fence git-push)';
560
- const kind: AllLoopEntry['kind'] = seat ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
612
+ const kind: AllLoopEntry['kind'] = seat || seatArg ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
561
613
  if (kind === 'cron-shell' && !shell && !namedId && !fencedGitPush) continue;
562
614
  const cron = row.cron;
563
615
  const period = cronPeriod(cron, now);
564
616
  const dueAt = cron ? dueBefore(cron, now) : null;
565
- const id = seat ? `${seat.toLowerCase()}-seat:${row.id}` : namedId ?? row.id;
566
- const title = seat ? `${seat} seat` : row.note || (fenceRole ? fenceCommandTitle(command, fenceRole) : undefined) || (namedId ? elanousCronName(command) : undefined) || row.name;
567
- const logMtime = kind === 'cron-shell' && command ? cronLogMtime(command) : undefined;
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 });
568
625
  all.push({ kind, id, title,
569
- ...resolveOwner(id, title, config, undefined, seat), ...(fenceRole ? { fenceRole } : {}),
626
+ ...resolveOwner(id, title, config, undefined, seatArg ?? seat, seatArg ? 'seat-arg' : 'seat'), ...(fenceRole ? { fenceRole } : {}),
570
627
  host: hostname(), observationCategory: row.category,
571
628
  enabled: !!row.enabled, registered: true,
572
629
  ...(cron ? { cron } : {}), ...(command ? { command } : {}),
573
630
  ...periodFields(period),
574
631
  ...(dueAt ? { dueAt } : {}),
575
- ...(logMtime ? { lastRunAt: logMtime, evidence: 'log-mtime' as const }
576
- : row.last_run && Number.isFinite(Date.parse(row.last_run)) ? { lastRunAt: row.last_run } : {}),
632
+ ...(picked === 'log-mtime' ? { lastRunAt: logMtime, evidence: picked }
633
+ : picked === 'registry' ? { lastRunAt: recorded, evidence: picked } : {}),
577
634
  ...(row.last_status ? { lastStatus: row.last_status } : {}),
578
635
  recentStatuses: row.last_status ? [row.last_status] : [] });
579
636
  }
580
- const bySource: Record<OwnerSource, number> = { header: 0, config: 0, seat: 0, default: 0 };
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 };
581
647
  for (const loop of all) bySource[loop.ownerSource!]++;
582
648
  debug.log('loop.check', 'ownership', { total: all.length, bySource });
583
649
  debug.log('loops.registry', 'list-all', { count: all.length });
@@ -595,13 +661,28 @@ export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistr
595
661
  const rows = schedules(opts);
596
662
  const graphClaimed = new Set([...graphJobs.values()].flat());
597
663
  const namedRows = new Map([...namedCronIds(rows, graphClaimed)].map(([rowId, name]) => [name, rowId]));
598
- const claimed = new Set(entries.flatMap(entry => entry.kind === 'graph'
664
+ const accounted = entries.flatMap(entry => entry.kind === 'graph'
599
665
  ? graphJobs.get(entry.id) ?? []
600
- : [entry.kind === 'seat' ? entry.id.slice(entry.id.indexOf(':') + 1)
601
- : namedRows.get(entry.id) ?? entry.id]));
602
- return rows.filter(row => row.source === 'crontab' && row.run_via === 'crontab'
603
- && row.disabled_reason !== 'vanished' && !claimed.has(row.id)
604
- && looksElanousRelated(row.command ?? ''));
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;
605
686
  }
606
687
 
607
688
  export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
@@ -45,6 +45,7 @@ export const FEATURE_MATURITY = {
45
45
  '/ops/release': { pwa: 'ops' },
46
46
  '/ops/seats': { pwa: 'ops' },
47
47
  '/ops/checklist': { pwa: 'ops' },
48
+ '/ceo': { pwa: 'ops' },
48
49
  '/dashboard': { pwa: 'ops' },
49
50
  '/bots': { pwa: 'ops' },
50
51
  '/botlab': { pwa: 'ops' },
@@ -55,7 +56,7 @@ export const FEATURE_MATURITY = {
55
56
  },
56
57
  // Canonical CLI command names only; aliases inherit their command's grade.
57
58
  cliRoot: {
58
- pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta',
59
+ pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', 'agent-env': 'beta',
59
60
  where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
60
61
  pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
61
62
  research: 'beta', release: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
@@ -0,0 +1,31 @@
1
+ import { SLASH_COMMANDS, type SlashCommand } from '../chat/index.js';
2
+ import { FEATURE_MATURITY, type Maturity } from './feature-maturity.js';
3
+
4
+ export interface TuiSlashAvailability {
5
+ name: string;
6
+ maturity: Maturity;
7
+ telegram: boolean;
8
+ discord: boolean;
9
+ }
10
+
11
+ /** Project canonical TUI commands onto the maturity map and bot-surface grade maps.
12
+ * An absent bot grade means unavailable; a missing TUI grade is a registry error.
13
+ */
14
+ export function deriveTuiSlashAvailability(
15
+ commands: readonly Pick<SlashCommand, 'name'>[] = SLASH_COMMANDS,
16
+ grades: {
17
+ tuiSlash: Readonly<Record<string, Maturity>>;
18
+ telegramCommand: Readonly<Record<string, Maturity>>;
19
+ discordCommand: Readonly<Record<string, Maturity>>;
20
+ } = FEATURE_MATURITY,
21
+ ): TuiSlashAvailability[] {
22
+ return commands.map(({ name }) => {
23
+ if (!Object.hasOwn(grades.tuiSlash, name)) throw new Error(`TUI slash command /${name} has no maturity grade`);
24
+ return {
25
+ name,
26
+ maturity: grades.tuiSlash[name]!,
27
+ telegram: Object.hasOwn(grades.telegramCommand, name),
28
+ discord: Object.hasOwn(grades.discordCommand, name),
29
+ };
30
+ });
31
+ }
@@ -270,23 +270,54 @@ export class LogStore {
270
270
  this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level, ts_ms)');
271
271
  this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_surface ON logs(surface, ts_ms)');
272
272
  this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_category ON logs(category, ts_ms)');
273
+ this.db.run(`CREATE TABLE IF NOT EXISTS pod_log_imports (
274
+ job TEXT NOT NULL,
275
+ line INTEGER NOT NULL,
276
+ PRIMARY KEY (job, line)
277
+ )`);
278
+ }
279
+
280
+ /** Insert each returned Pod line at most once per job, atomically with its receipt. */
281
+ insertPodBatch(job: string, rows: Array<{ rec: LogRecord; surface: string; line: number }>): number {
282
+ if (rows.length === 0) return 0;
283
+ const receipt = this.db.prepare('INSERT OR IGNORE INTO pod_log_imports (job, line) VALUES (?, ?)');
284
+ return this.db.transaction(() => {
285
+ const unseen = rows.filter(({ line }) => receipt.run(job, line).changes > 0);
286
+ this.insertBatch(unseen, { preserveRows: true });
287
+ return unseen.length;
288
+ })();
273
289
  }
274
290
 
275
291
  /** 배치 insert(단일 트랜잭션). 실패는 throw — fail-soft 는 호출측(StoreSink)
276
292
  * 책임(ops-log 의 recordOpsEvent/Safe 분업 동형). */
277
- insertBatch(rows: Array<{ rec: LogRecord; surface: string }>): void {
293
+ insertBatch(rows: Array<{ rec: LogRecord; surface: string }>, opts: { preserveRows?: boolean } = {}): void {
278
294
  if (rows.length === 0) return;
279
295
  const stmt = this.db.prepare(
280
296
  `INSERT INTO logs (ts, ts_ms, level, instance, host_id, surface, category, event, session_id, trace_id, data)
281
297
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
282
298
  );
299
+ const decisionRow = this.db.prepare(`SELECT id FROM logs WHERE instance = ? AND category = 'loop.steward' AND event = 'decision'
300
+ AND json_valid(data) AND json_extract(data, '$.runId') = ? ORDER BY id DESC LIMIT 1`);
301
+ const updateDecision = this.db.prepare(`UPDATE logs SET ts = ?, ts_ms = ?, level = ?, surface = ?, data = ? WHERE id = ?`);
283
302
  const insertAll = this.db.transaction((batch: Array<{ rec: LogRecord; surface: string }>) => {
284
303
  for (const { rec, surface } of batch) {
285
304
  const tsMs = Date.parse(rec.ts);
305
+ const timestamp = Number.isFinite(tsMs) ? tsMs : Date.now();
306
+ const level = deriveLogLevel(rec);
307
+ const data = rec.data !== undefined ? JSON.stringify(rec.data) : null;
308
+ const runId = rec.category === 'loop.steward' && rec.event === 'decision' &&
309
+ rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
310
+ ? (rec.data as Record<string, unknown>).runId : undefined;
311
+ const existing = !opts.preserveRows && typeof runId === 'string' && runId
312
+ ? decisionRow.get(this.instance, runId) as { id: number } | undefined : undefined;
313
+ if (existing) {
314
+ updateDecision.run(rec.ts, timestamp, level, surface, data, existing.id);
315
+ continue;
316
+ }
286
317
  stmt.run(
287
318
  rec.ts,
288
- Number.isFinite(tsMs) ? tsMs : Date.now(),
289
- deriveLogLevel(rec),
319
+ timestamp,
320
+ level,
290
321
  this.instance,
291
322
  rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
292
323
  && typeof (rec.data as Record<string, unknown>).hostId === 'string'
@@ -297,7 +328,7 @@ export class LogStore {
297
328
  rec.event,
298
329
  (rec as { session_id?: string }).session_id ?? null,
299
330
  rec.trace_id ?? null,
300
- rec.data !== undefined ? JSON.stringify(rec.data) : null,
331
+ data,
301
332
  );
302
333
  }
303
334
  });
@@ -162,6 +162,7 @@ import {
162
162
  } from './vault-api.js';
163
163
  import { handleBuildsGet, parseBuildsPath } from './builds-api.js';
164
164
  import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEventsGet, handleHarnessRunScreenGet, handleHarnessRunsGet, handleHarnessStopPost } from './harness-api.js';
165
+ import { handleLoopEdgesGet } from './loop-edges.js';
165
166
  import { handleFabricPlans, type FabricPlansRouteOpts } from './fabric-plans.js';
166
167
  import { dispatchPersonaRoute } from './personas.js';
167
168
  import { handleMe } from './operator.js';
@@ -1295,6 +1296,15 @@ export async function routeRequest(
1295
1296
  return handleDevices(req, opts.devices);
1296
1297
  }
1297
1298
 
1299
+ if (pathname === '/v1/loops/edges' && method === 'GET') {
1300
+ if (!opts.metaApi?.bearerToken) return jsonResponse({ error: 'unauthorized' }, 401);
1301
+ const offered = req.headers.get('authorization');
1302
+ if (!offered?.startsWith('Bearer ') || !compareTokenConstTime(offered.slice('Bearer '.length).trim(), opts.metaApi.bearerToken)) {
1303
+ return jsonResponse({ error: 'unauthorized' }, 401);
1304
+ }
1305
+ return handleLoopEdgesGet(req);
1306
+ }
1307
+
1298
1308
  if (pathname === '/v1/harness/runs' && method === 'GET') {
1299
1309
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
1300
1310
  if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
@@ -2882,20 +2892,24 @@ export async function routeRequest(
2882
2892
  if (result) return result;
2883
2893
  }
2884
2894
  if (pathname === '/v1/context/now') {
2885
- if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2886
- const audiences = url.searchParams.getAll('audience');
2887
- if (audiences.length > 1) return jsonResponse({ error: 'invalid_audience' }, 400);
2888
- const requested = audiences[0];
2889
- if (requested !== undefined && requested !== 'operator' && requested !== 'user' && requested !== 'public-demo') {
2890
- return jsonResponse({ error: 'invalid_audience' }, 400);
2891
- }
2892
- const forced = getUserConfig().nexus?.demoMode === true;
2893
- const audience: ContextNowAudience = forced ? 'public-demo' : requested ?? 'operator';
2894
- const answer = contextNow({ audience, ...(url.searchParams.has('topic') ? { topic: url.searchParams.get('topic') ?? '' } : {}) }, opts.contextNowDeps);
2895
- debug.log('context.now', 'served', { audience, hidden: answer.hiddenCount ?? 0, forced });
2896
- return jsonResponse(url.searchParams.get('format') === 'voice'
2897
- ? { text: renderVoiceNow(answer, audience), audience, hiddenCount: answer.hiddenCount ?? 0 }
2898
- : { ...answer, audience, hiddenCount: answer.hiddenCount ?? 0 });
2895
+ try {
2896
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2897
+ const audiences = url.searchParams.getAll('audience');
2898
+ if (audiences.length > 1) return jsonResponse({ error: 'invalid_audience' }, 400);
2899
+ const requested = audiences[0];
2900
+ if (requested !== undefined && requested !== 'operator' && requested !== 'user' && requested !== 'public-demo') {
2901
+ return jsonResponse({ error: 'invalid_audience' }, 400);
2902
+ }
2903
+ const forced = getUserConfig().nexus?.demoMode === true;
2904
+ const audience: ContextNowAudience = forced ? 'public-demo' : requested ?? 'operator';
2905
+ const answer = contextNow({ audience, ...(url.searchParams.has('topic') ? { topic: url.searchParams.get('topic') ?? '' } : {}) }, opts.contextNowDeps);
2906
+ debug.log('context.now', 'served', { audience, hidden: answer.hiddenCount ?? 0, forced });
2907
+ return jsonResponse(url.searchParams.get('format') === 'voice'
2908
+ ? { text: renderVoiceNow(answer, audience), audience, hiddenCount: answer.hiddenCount ?? 0 }
2909
+ : { ...answer, audience, hiddenCount: answer.hiddenCount ?? 0 });
2910
+ } catch {
2911
+ return jsonResponse({ error: 'context-now-failed' }, 500);
2912
+ }
2899
2913
  }
2900
2914
  // §6.3 (2026-05-09) — context URL fetch (showroom).
2901
2915
  // POST /v1/context/fetch-url