elanous 0.2.14 → 0.2.16

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 (350) 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/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -0,0 +1,173 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { listCoordEvents } from '../../context-bus/coord-events.js';
4
+ import { CardStore, cardIndexPath, type TaskCard } from '../../task-cards/card-store.js';
5
+ import { loadRunLedger, resolveFederatedRunLedgerDirectories, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
6
+ import { listLoops } from '../../loops/registry.js';
7
+ import { listSubSeatCharters } from './sub-seat-charters.js';
8
+ import { SEATS } from './ops-seats.js';
9
+ import { jsonResponse } from './json-response.js';
10
+
11
+ export interface LoopEdge {
12
+ at: string;
13
+ kind: 'request' | 'decision' | 'report' | 'card' | 'run';
14
+ from: string;
15
+ to: string;
16
+ ref: string;
17
+ }
18
+
19
+ export interface LoopEdgesDeps {
20
+ listCoord?: typeof listCoordEvents;
21
+ cardRoot?: string;
22
+ ledgerDir?: string;
23
+ listCards?: () => TaskCard[];
24
+ listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string }>;
25
+ listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
26
+ seatIds?: () => string[];
27
+ }
28
+
29
+ const ID = /^[a-zA-Z0-9_-]{1,128}$/;
30
+ const SURFACES = new Set(['telegram', 'pwa', 'linear', 'tui', 'discord', 'api', 'cli', 'voice', 'folder', 'wish']);
31
+ const KINDS: Record<string, LoopEdge['kind']> = { '요청': 'request', '결정': 'decision', '보고': 'report' };
32
+
33
+ function seat(value: unknown, seats: ReadonlySet<string>): value is string {
34
+ return typeof value === 'string' && seats.has(value);
35
+ }
36
+ function surface(value: unknown): value is string {
37
+ return typeof value === 'string' && SURFACES.has(value);
38
+ }
39
+ function object(value: unknown): Record<string, unknown> | null {
40
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
41
+ }
42
+ function parse(content: string): Record<string, unknown> | null {
43
+ try { return object(JSON.parse(content) as unknown); } catch { return null; }
44
+ }
45
+ function inWindow(at: string, since: number, now: number): boolean {
46
+ const ms = Date.parse(at);
47
+ return Number.isFinite(ms) && ms >= since && ms <= now;
48
+ }
49
+
50
+ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySet<string>): LoopEdge[] {
51
+ if (!card.goalId.startsWith('wish:') || !ID.test(card.id)) return [];
52
+ const node = `card:${card.id}`;
53
+ const ref = card.id;
54
+ const edges: LoopEdge[] = [];
55
+ const intake = card.sections.find(s => s.key === 'intake:wish:0');
56
+ const intakeData = intake && parse(intake.content);
57
+ if (intake && surface(intakeData?.source) && inWindow(intake.createdAt, since, now)) {
58
+ edges.push({ at: intake.createdAt, kind: 'card', from: `surface:${intakeData.source}`, to: node, ref });
59
+ }
60
+ const split = card.sections.find(s => s.key === 'flow:split:0');
61
+ const splitData = split && parse(split.content);
62
+ const cells = Array.isArray(splitData?.cells) ? splitData.cells : [];
63
+ const owners = new Map<string, string>();
64
+ for (const item of cells) {
65
+ const cell = object(item);
66
+ if (typeof cell?.id === 'string' && ID.test(cell.id) && seat(cell.owner, seats)) owners.set(cell.id, cell.owner);
67
+ }
68
+ const placedOwners = new Set<string>();
69
+ for (const section of card.sections) {
70
+ if (!section.key.startsWith('flow:placed:')) continue;
71
+ const cellId = section.key.slice('flow:placed:'.length);
72
+ const saved = parse(section.content);
73
+ const placement = object(saved?.decision);
74
+ const owner = owners.get(cellId);
75
+ if (!owner || !placement || placement.id !== cellId || !ID.test(cellId)
76
+ || typeof placement.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(placement.version)) continue;
77
+ placedOwners.add(owner);
78
+ if (inWindow(section.createdAt, since, now)) {
79
+ const cellNode = `loop:${cellId}`;
80
+ edges.push({ at: section.createdAt, kind: 'card', from: node, to: cellNode, ref });
81
+ edges.push({ at: section.createdAt, kind: 'card', from: cellNode, to: owner, ref });
82
+ }
83
+ }
84
+ const reply = card.sections.find(s => s.key === 'intake:reply:0');
85
+ const replyData = reply && parse(reply.content);
86
+ // The destination section alone is not a send; flow:result records a completed card flow.
87
+ const result = card.sections.find(s => s.key === 'flow:result:0');
88
+ if (result && reply && surface(replyData?.surface) && inWindow(result.createdAt, since, now)) {
89
+ for (const owner of placedOwners) {
90
+ edges.push({ at: result.createdAt, kind: 'card', from: owner, to: `surface:${replyData.surface}`, ref });
91
+ }
92
+ }
93
+ return edges;
94
+ }
95
+
96
+ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string }> {
97
+ if (!existsSync(dir)) return [];
98
+ const result: Array<{ runId: string; at: string; seat: string }> = [];
99
+ let names: string[];
100
+ try { names = readdirSync(dir); } catch { return []; }
101
+ for (const name of names) {
102
+ if (!/^run-[a-zA-Z0-9-]+\.jsonl$/.test(name)) continue;
103
+ const path = join(dir, name);
104
+ try { if (!statSync(path).isFile() || statSync(path).size > 5 * 1024 * 1024) continue; }
105
+ catch { continue; }
106
+ const runId = name.slice(0, -6);
107
+ let entries: RunLedgerEntry[] | null;
108
+ try { entries = loadRunLedger(runId, dir); } catch { continue; }
109
+ const start = entries?.find(entry => entry.event === 'start' && entry.runId === runId);
110
+ const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
111
+ const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
112
+ ?? start?.data.originAgent ?? start?.data.controller;
113
+ if (start?.timestamp && start.timestamp >= since && seat(launchSeat, seats)) {
114
+ result.push({ runId, at: start.timestamp, seat: launchSeat });
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+
120
+ /** GET only; project an allowlist of fields from the three ledgers. Never serialize ledger records. */
121
+ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
122
+ const params = new URL(req.url).searchParams;
123
+ const rawSince = params.get('since');
124
+ const since = rawSince === null ? Date.now() - 60 * 60 * 1000 : Date.parse(rawSince);
125
+ const rawLimit = params.get('limit');
126
+ const limit = rawLimit === null ? 100 : Number(rawLimit);
127
+ const now = Date.now();
128
+ if (rawSince !== null && !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(rawSince)
129
+ || !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
130
+ return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>' }, 400);
131
+ }
132
+ const sinceIso = new Date(since).toISOString();
133
+ const seats = new Set((deps.seatIds ?? (() => [...SEATS.map(s => s.seat), ...listSubSeatCharters().map(s => s.id)]))());
134
+ const edges: LoopEdge[] = [];
135
+ for (const event of (deps.listCoord ?? listCoordEvents)({ since: sinceIso, channelOnly: true })) {
136
+ if (!seat(event.refs.seat, seats) || !inWindow(event.at, since, now)) continue;
137
+ const kind = KINDS[event.refs.kind ?? ''];
138
+ if (!kind || !Array.isArray(event.refs.recipients)) continue;
139
+ const ref = typeof event.refs.url === 'string' && /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/(?:pull|issues)\/\d+#issuecomment-\d+$/.test(event.refs.url)
140
+ ? event.refs.url : event.id;
141
+ if (!ID.test(ref) && !ref.startsWith('https://github.com/')) continue;
142
+ for (const recipient of new Set(event.refs.recipients)) {
143
+ if (seat(recipient, seats)) edges.push({ at: event.at, kind, from: event.refs.seat, to: recipient, ref });
144
+ }
145
+ }
146
+ if (deps.listCards) {
147
+ for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats));
148
+ } else {
149
+ const root = deps.cardRoot;
150
+ if (existsSync(cardIndexPath(root))) {
151
+ const store = new CardStore(root, true);
152
+ try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats)); }
153
+ finally { store.close(); }
154
+ }
155
+ }
156
+ const readStarts = deps.listRunStarts ?? ((from: string) => {
157
+ const owners = new Map<string, string>();
158
+ try {
159
+ for (const entry of (deps.listLoopOwners ?? (() => listLoops()))()) {
160
+ if (seat(entry.owner, seats)) owners.set(entry.id, entry.owner);
161
+ }
162
+ } catch { /* A registry outage does not erase explicit launch-seat evidence. */ }
163
+ return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
164
+ .flatMap(dir => runStarts(from, dir, owners, seats));
165
+ });
166
+ for (const run of readStarts(sinceIso)) {
167
+ if (seat(run.seat, seats) && ID.test(run.runId) && inWindow(run.at, since, now)) {
168
+ edges.push({ at: run.at, kind: 'run', from: run.seat, to: `loop:${run.runId}`, ref: run.runId });
169
+ }
170
+ }
171
+ edges.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
172
+ return jsonResponse({ edges: edges.slice(0, limit) });
173
+ }
@@ -15,6 +15,7 @@ import { tasksDbPath } from '../../task-orchestrator/paths.js';
15
15
  import { jsonResponse } from './http-server.js';
16
16
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
17
17
  import { listScheduleRuns } from '../../domains/schedule-runs.js';
18
+ import { listLoops } from '../../loops/registry.js';
18
19
 
19
20
  export interface SchedulesReadDeps {
20
21
  rows?: () => ScheduleRow[];
@@ -24,6 +25,7 @@ export interface SchedulesReadDeps {
24
25
  logsPath?: () => string;
25
26
  /** 발화 이력 원천(`schedule_runs`) — 기본은 schedules.db 를 읽기 전용으로 연다. */
26
27
  schedulesDbPath?: () => string;
28
+ loopOwners?: () => Array<{ id: string; title: string; owner?: string | null; enabled: boolean; lastRun: { at: string; status: string } | null; jobs: Array<{ id: string }> }>;
27
29
  now?: () => Date;
28
30
  }
29
31
 
@@ -201,6 +203,17 @@ export function handleSchedulesList(req: Request, opts: MetaApiOpts, deps: Sched
201
203
  if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
202
204
  const cards = readSchedulesInventory(deps);
203
205
  const schedules = new URL(req.url).searchParams.get('includeOff') === '1' ? cards : cards.filter(c => c.state !== 'off');
206
+ if (new URL(req.url).searchParams.get('includeOwners') === '1') {
207
+ try {
208
+ const owners = (deps.loopOwners ?? (() => listLoops({ schedules: registryRows() })))().map(loop => ({ id: loop.id, title: redactSecretText(loop.title), owner: loop.owner ?? null,
209
+ enabled: loop.enabled, lastRun: loop.lastRun ? { at: loop.lastRun.at, status: loop.lastRun.status } : null,
210
+ jobs: loop.jobs.map(job => job.id) }));
211
+ return jsonResponse({ schedules, count: schedules.length, owners }, 200);
212
+ } catch {
213
+ // The ordinary inventory stays readable; an opted-in map must not mistake an outage for unowned loops.
214
+ return jsonResponse({ schedules, count: schedules.length, owners: null }, 200);
215
+ }
216
+ }
204
217
  return jsonResponse({ schedules, count: schedules.length }, 200);
205
218
  }
206
219
 
@@ -6,6 +6,7 @@ import { resolveAttachmentPath } from '../../boot/attachment-store.js';
6
6
  import { runGh } from '../../decisions/decision-cards.js';
7
7
  import { openMsgStore, canonicalSeatId } from '../../msg/msg-store.js';
8
8
  import { dispatchCeoTask, type CeoCommandDeps } from '../../seat-dispatch/ceo-commands.js';
9
+ import { appendSeatRequestRows, withSeatRequestLedgerLock } from '../../seat-dispatch/seat-request-ledger.js';
9
10
  import { acknowledgeSeatAnswers, askSeat, deliverSeatAnswers, parseSeatAsk, type SeatAskDeps } from '../../seat-dispatch/seat-ask.js';
10
11
  import { getUserConfig } from '../../user-config.js';
11
12
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
@@ -36,10 +37,12 @@ interface PendingRequest {
36
37
  queuedAt: string;
37
38
  ref: string;
38
39
  attachments?: string[];
40
+ reason?: string;
41
+ closedAt?: string;
39
42
  }
40
43
 
41
44
  interface RejectedRequest extends Omit<PendingRequest, 'status'> {
42
- status: 'rejected';
45
+ status: 'rejected' | 'done';
43
46
  }
44
47
 
45
48
  type RecordEntry = (SeatRequestItem & { key: string }) | PendingRequest | RejectedRequest;
@@ -73,19 +76,7 @@ function file(root: string): string {
73
76
  }
74
77
 
75
78
  function append(path: string, entry: RecordEntry): void {
76
- mkdirSync(dirname(path), { recursive: true });
77
- // Atomically replace the journal so a failed write cannot leave a truncated JSON line.
78
- const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
79
- const temp = `${path}.${randomUUID()}.tmp`;
80
- try {
81
- writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`);
82
- const fd = openSync(temp, 'r');
83
- try { fsyncSync(fd); } finally { closeSync(fd); }
84
- renameSync(temp, path);
85
- } catch (error) {
86
- try { unlinkSync(temp); } catch { /* no temp file to remove */ }
87
- throw error;
88
- }
79
+ appendSeatRequestRows(path, [entry]);
89
80
  }
90
81
 
91
82
  function records(path: string): Map<string, RecordEntry> {
@@ -100,7 +91,7 @@ function records(path: string): Map<string, RecordEntry> {
100
91
  if (!line) continue;
101
92
  const entry = JSON.parse(line) as RecordEntry;
102
93
  if (typeof entry.key !== 'string' || typeof entry.seat !== 'string' || typeof entry.text !== 'string'
103
- || typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected'].includes(entry.status)
94
+ || typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected', 'done'].includes(entry.status)
104
95
  || (entry.status === 'queued' && typeof entry.receiptId !== 'string')
105
96
  || (entry.status !== 'queued' && typeof entry.ref !== 'string')) {
106
97
  throw new Error('invalid seat request journal');
@@ -121,7 +112,7 @@ function records(path: string): Map<string, RecordEntry> {
121
112
  throw new Error('invalid seat request outcome');
122
113
  }
123
114
  const current = entries.get(item.key);
124
- if (current?.status === 'queued') continue;
115
+ if (current?.status === 'queued' || current?.status === 'done' || current?.closedAt) continue;
125
116
  if (current?.status === 'pending') {
126
117
  if (item.status === 'rejected' && current.ref !== item.ref) continue;
127
118
  if (item.status === 'queued' && current.queuedAt !== item.queuedAt) continue;
@@ -135,27 +126,32 @@ function outcomeFile(path: string, key: string): string {
135
126
  return join(dirname(path), 'outcomes', `${key}.json`);
136
127
  }
137
128
 
138
- function persistOutcome(path: string, entry: RecordEntry, write: typeof append): void {
139
- try {
140
- write(path, entry);
141
- const sidecar = outcomeFile(path, entry.key);
142
- if (existsSync(sidecar)) unlinkSync(sidecar);
143
- } catch {
144
- // The primary journal may be unavailable while the outcome is already committed
145
- // to the intake queue. Keep a durable sidecar rather than a process-local receipt.
146
- const target = outcomeFile(path, entry.key);
147
- mkdirSync(dirname(target), { recursive: true });
148
- const temp = `${target}.${randomUUID()}.tmp`;
129
+ function persistOutcome(path: string, entry: RecordEntry, write: typeof append): RecordEntry {
130
+ return withSeatRequestLedgerLock(path, () => {
131
+ const current = records(path).get(entry.key);
132
+ if (current && ('closedAt' in current || current.status === 'done')) return current;
133
+ if (current?.status === 'queued') return current;
149
134
  try {
150
- writeFileSync(temp, JSON.stringify(entry));
151
- const fd = openSync(temp, 'r');
152
- try { fsyncSync(fd); } finally { closeSync(fd); }
153
- renameSync(temp, target);
154
- } catch (error) {
155
- try { unlinkSync(temp); } catch { /* no temp file */ }
156
- throw error;
135
+ write(path, entry);
136
+ const sidecar = outcomeFile(path, entry.key);
137
+ if (existsSync(sidecar)) unlinkSync(sidecar);
138
+ } catch {
139
+ // A committed external delivery still needs a durable outcome if the journal write failed.
140
+ const target = outcomeFile(path, entry.key);
141
+ mkdirSync(dirname(target), { recursive: true });
142
+ const temp = `${target}.${randomUUID()}.tmp`;
143
+ try {
144
+ writeFileSync(temp, JSON.stringify(entry));
145
+ const fd = openSync(temp, 'r');
146
+ try { fsyncSync(fd); } finally { closeSync(fd); }
147
+ renameSync(temp, target);
148
+ } catch (error) {
149
+ try { unlinkSync(temp); } catch { /* no temp file */ }
150
+ throw error;
151
+ }
157
152
  }
158
- }
153
+ return entry;
154
+ });
159
155
  }
160
156
 
161
157
  function receipt(item: SeatRequestItem): Response {
@@ -298,13 +294,18 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
298
294
  const gate = new Promise<void>((resolve) => { start = resolve; });
299
295
  const task = (async (): Promise<Response> => {
300
296
  await gate;
301
- let existing: RecordEntry | undefined;
302
- try { existing = records(path).get(key); }
297
+ // A settled request answers from the journal before attachments are resolved — a retry
298
+ // must return its receipt even when the uploaded file is gone. The locked re-check below still guards races.
299
+ let settled: RecordEntry | undefined;
300
+ try { settled = records(path).get(key); }
303
301
  catch { return unavailable(); }
304
- if (existing) {
305
- if (existing.seat !== seat.id || existing.text !== text
306
- || JSON.stringify(existing.attachments ?? []) !== JSON.stringify(attachmentIds)) return jsonResponse({ error: 'idempotency-conflict' }, 409);
307
- if (existing.status === 'queued') return receipt(existing);
302
+ if (settled) {
303
+ if (settled.seat !== seat.id || settled.text !== text
304
+ || JSON.stringify(settled.attachments ?? []) !== JSON.stringify(attachmentIds)) return jsonResponse({ error: 'idempotency-conflict' }, 409);
305
+ if ('closedAt' in settled || settled.status === 'done') {
306
+ return jsonResponse({ error: 'request-closed', status: settled.status, reason: 'reason' in settled ? settled.reason : undefined }, 409);
307
+ }
308
+ if (settled.status === 'queued') return receipt(settled);
308
309
  }
309
310
  const attachments: Array<{ name: string; path: string }> = [];
310
311
  for (const id of attachmentIds) {
@@ -316,18 +317,36 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
316
317
  const metadata = (input.attachments as Array<{ name?: unknown }>)[attachments.length];
317
318
  attachments.push({ name: typeof metadata?.name === 'string' && metadata.name.trim() ? metadata.name : basename(resolved), path: resolved });
318
319
  }
319
- const intent: PendingRequest = existing?.status === 'pending'
320
- ? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}`,
321
- ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
322
- if (existing?.status !== 'pending') {
323
- try {
324
- (deps.append ?? append)(path, intent);
325
- if (existing?.status === 'rejected') {
326
- const sidecar = outcomeFile(path, key);
327
- if (existsSync(sidecar)) unlinkSync(sidecar);
320
+ let initial: { intent?: PendingRequest; response?: Response };
321
+ try {
322
+ initial = withSeatRequestLedgerLock(path, () => {
323
+ const existing = records(path).get(key);
324
+ if (existing) {
325
+ if (existing.seat !== seat.id || existing.text !== text
326
+ || JSON.stringify(existing.attachments ?? []) !== JSON.stringify(attachmentIds)) {
327
+ return { response: jsonResponse({ error: 'idempotency-conflict' }, 409) };
328
+ }
329
+ if ('closedAt' in existing || existing.status === 'done') {
330
+ return { response: jsonResponse({ error: 'request-closed', status: existing.status,
331
+ reason: 'reason' in existing ? existing.reason : undefined }, 409) };
332
+ }
333
+ if (existing.status === 'queued') return { response: receipt(existing) };
328
334
  }
329
- } catch { return unavailable(); }
330
- }
335
+ const intent: PendingRequest = existing?.status === 'pending'
336
+ ? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}`,
337
+ ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
338
+ if (existing?.status !== 'pending') {
339
+ (deps.append ?? append)(path, intent);
340
+ if (existing?.status === 'rejected') {
341
+ const sidecar = outcomeFile(path, key);
342
+ if (existsSync(sidecar)) unlinkSync(sidecar);
343
+ }
344
+ }
345
+ return { intent };
346
+ });
347
+ } catch { return unavailable(); }
348
+ if (initial.response) return initial.response;
349
+ const intent = initial.intent!;
331
350
  const ceoSeat = canonicalSeatId(seat.id);
332
351
  if (['OP', 'TC', 'MK', 'UX'].includes(ceoSeat)) {
333
352
  let result: Awaited<ReturnType<typeof dispatchCeoTask>>;
@@ -342,10 +361,12 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
342
361
  ...(attachmentIds.length ? { attachments: attachmentIds } : {}), channel: result.channel,
343
362
  ...(result.channelError ? { channelError: result.channelError } : {}),
344
363
  };
345
- try { persistOutcome(path, item, deps.append ?? append); }
364
+ let persisted: RecordEntry;
365
+ try { persisted = persistOutcome(path, item, deps.append ?? append); }
346
366
  catch { return unavailable(); }
367
+ if (persisted.status !== 'queued') return jsonResponse({ error: 'request-closed', status: persisted.status, reason: persisted.reason }, 409);
347
368
  publishInsideEvent({ kind: 'seat', seat: item.seat, receiptId: item.receiptId, status: item.status, ts: item.queuedAt });
348
- return receipt(item);
369
+ return receipt(persisted);
349
370
  }
350
371
  let result: Awaited<ReturnType<typeof submitIntakeWork>>;
351
372
  try {
@@ -363,11 +384,13 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
363
384
  }
364
385
  const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt,
365
386
  status: 'queued', ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
366
- try { persistOutcome(path, item, deps.append ?? append); }
387
+ let persisted: RecordEntry;
388
+ try { persisted = persistOutcome(path, item, deps.append ?? append); }
367
389
  catch { return unavailable(); }
390
+ if (persisted.status !== 'queued') return jsonResponse({ error: 'request-closed', status: persisted.status, reason: persisted.reason }, 409);
368
391
  debug.log('seat-address.pwa', 'enqueued', { seat: seat.id, receiptId: item.receiptId });
369
392
  publishInsideEvent({ kind: 'seat', seat: item.seat, receiptId: item.receiptId, status: item.status, ts: item.queuedAt });
370
- return receipt(item);
393
+ return receipt(persisted);
371
394
  })();
372
395
  inFlight.set(lock, { payload, task });
373
396
  start();
@@ -0,0 +1,75 @@
1
+ // Launch-time application of the overload gate. Kept out of dev-cli.ts so a
2
+ // unit test can import it without the dev-cli module graph (yaml, typescript).
3
+
4
+ import type { ChildLlmSelection } from '../agent/run-context.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { resolveGrokCredential } from '../grok/credential.js';
7
+ import {
8
+ applyOverloadFailoverToChild,
9
+ readLlmCallOutcomes,
10
+ type OverloadFailoverLaunchDecision,
11
+ } from './overload-failover.js';
12
+
13
+ const CODEX_NAMES = new Set(['codex', 'openai-codex']);
14
+
15
+ export function codexProviderNamed(provider: string | undefined): boolean {
16
+ if (!provider?.trim()) return false;
17
+ return CODEX_NAMES.has(provider.trim().toLowerCase());
18
+ }
19
+
20
+ let launchOverloadOutcomesForTesting: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined;
21
+
22
+ /** Test seam. `undefined` restores the real log store. */
23
+ export function setLaunchOverloadOutcomesForTesting(
24
+ reader: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined,
25
+ ): void {
26
+ launchOverloadOutcomesForTesting = reader;
27
+ }
28
+
29
+ export interface LaunchOverloadFlags {
30
+ provider?: string;
31
+ model?: string;
32
+ /** Pinned childLlm config naming codex. A flag is handled via `provider`. */
33
+ codexPinned?: boolean;
34
+ /** Test seam. Default: a grok credential resolves on this host. */
35
+ grokAvailable?: boolean;
36
+ }
37
+
38
+ /** Explicit codex stays. Otherwise k `llm.call`/`outcome` overload rows move
39
+ * the unpinned child to grok before spawn. */
40
+ export function applyLaunchOverloadFailover(
41
+ selection: ChildLlmSelection | undefined,
42
+ flags: LaunchOverloadFlags = {},
43
+ readOutcomes?: () => ReturnType<typeof readLlmCallOutcomes>,
44
+ env: NodeJS.ProcessEnv = process.env,
45
+ ): ChildLlmSelection | undefined {
46
+ // Test processes never read this machine's operational logs.db — results must not depend on the host's history.
47
+ const inTest = env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME);
48
+ const reader = launchOverloadOutcomesForTesting ?? readOutcomes ?? (inTest ? () => [] : () => readLlmCallOutcomes());
49
+ let outcomes: ReturnType<typeof readLlmCallOutcomes> = [];
50
+ try { outcomes = reader(); } catch { outcomes = []; }
51
+ const explicit = codexProviderNamed(flags.provider) || flags.codexPinned === true;
52
+ let grokAvailable = flags.grokAvailable;
53
+ if (grokAvailable === undefined && outcomes.length > 0 && !inTest) {
54
+ // Only resolve when a switch is possible — a host without grok credentials must stay on codex. Tests inject it.
55
+ try { grokAvailable = resolveGrokCredential() !== null; } catch { grokAvailable = false; }
56
+ }
57
+ const { selection: next, decision } = applyOverloadFailoverToChild(selection, {
58
+ codexExplicit: explicit,
59
+ outcomes,
60
+ ...(grokAvailable !== undefined ? { grokAvailable } : {}),
61
+ defaultProvider: selection?.provider ?? 'openai-codex',
62
+ });
63
+ if (decision.switched && next?.provider === 'grok' && next.model) {
64
+ debug.log('self-dev', 'overload-failover', {
65
+ from: selection?.provider ?? 'openai-codex',
66
+ to: next.provider,
67
+ model: next.model,
68
+ why: decision.why,
69
+ }, { level: 'warn' });
70
+ return { provider: next.provider, model: next.model, source: 'config' };
71
+ }
72
+ return selection;
73
+ }
74
+
75
+ export type { OverloadFailoverLaunchDecision };
@@ -0,0 +1,137 @@
1
+ // Launch-time codex → grok switch driven only by `llm.call` / `outcome`.
2
+ //
3
+ // #24256 read `llm.response` / `llm.stream`. Those rows do not carry
4
+ // provider + status + kind, so a launch that consulted them never saw
5
+ // overload. This module refuses every other category.
6
+ //
7
+ // Boundaries:
8
+ // • an explicit codex child (flag or pinned config) never switches (#24183)
9
+ // • k consecutive codex overload/5xx/rate-limit outcomes switch the next
10
+ // child to grok before that child is spawned
11
+ // • k later successful outcomes restore codex
12
+ // • a 400 (kind `other`) never counts
13
+
14
+ import { logsDbPath, LogStore, type LogStoreRow } from '../mss/logging/log-store.js';
15
+ import {
16
+ codexOverloadStreakReached,
17
+ overloadRecovered,
18
+ OVERLOAD_FAILOVER_STREAK,
19
+ type LlmCallOutcome,
20
+ type LlmCallOutcomeKind,
21
+ } from '../session-runtime/retry-policy.js';
22
+ import { lookupLlmTierSpec } from '../model-tier/llm-tier-map.js';
23
+
24
+ export const LLM_CALL_CATEGORY = 'llm.call';
25
+ export const LLM_CALL_EVENT = 'outcome';
26
+
27
+ const KINDS = new Set<LlmCallOutcomeKind>(['ok', 'overloaded', '5xx', 'rate-limit', 'other']);
28
+
29
+ export interface OverloadFailoverLaunchInput {
30
+ /** True when the launch named codex (flag or pinned childLlm). */
31
+ readonly codexExplicit: boolean;
32
+ /** Provider the launch would have used before this gate. */
33
+ readonly provider: string;
34
+ readonly model?: string;
35
+ readonly outcomes: readonly LlmCallOutcome[];
36
+ readonly streak?: number;
37
+ /** grok may be selected. Unknown is not a reason to stay on a hot codex. */
38
+ readonly grokAvailable?: boolean;
39
+ }
40
+
41
+ export interface OverloadFailoverLaunchDecision {
42
+ readonly provider: string;
43
+ readonly model?: string;
44
+ readonly switched: boolean;
45
+ readonly why: 'explicit-codex' | 'not-codex' | 'streak' | 'recovered' | 'below-streak' | 'grok-unavailable';
46
+ }
47
+
48
+ export function parseLlmCallOutcome(row: Pick<LogStoreRow, 'category' | 'event' | 'data'>): LlmCallOutcome | undefined {
49
+ if (row.category !== LLM_CALL_CATEGORY || row.event !== LLM_CALL_EVENT || !row.data) return undefined;
50
+ let parsed: unknown;
51
+ try { parsed = JSON.parse(row.data); } catch { return undefined; }
52
+ if (!parsed || typeof parsed !== 'object') return undefined;
53
+ const rec = parsed as { provider?: unknown; status?: unknown; kind?: unknown };
54
+ if (typeof rec.provider !== 'string' || rec.provider.length === 0) return undefined;
55
+ if (typeof rec.status !== 'number' || !Number.isFinite(rec.status)) return undefined;
56
+ if (typeof rec.kind !== 'string' || !KINDS.has(rec.kind as LlmCallOutcomeKind)) return undefined;
57
+ return { provider: rec.provider, status: rec.status, kind: rec.kind as LlmCallOutcomeKind };
58
+ }
59
+
60
+ /** Newest-last. Rows that are not `llm.call`/`outcome` are dropped, never guessed. */
61
+ export function outcomesFromCallRows(rows: readonly (Pick<LogStoreRow, 'category' | 'event' | 'data' | 'ts_ms'> & { id?: number })[]): LlmCallOutcome[] {
62
+ // The store returns newest first; calls in the same millisecond keep that order unless ties break on row id.
63
+ return [...rows]
64
+ .filter((row) => row.category === LLM_CALL_CATEGORY && row.event === LLM_CALL_EVENT)
65
+ .sort((a, b) => a.ts_ms - b.ts_ms || (a.id ?? 0) - (b.id ?? 0))
66
+ .flatMap((row) => {
67
+ const parsed = parseLlmCallOutcome(row);
68
+ return parsed ? [parsed] : [];
69
+ });
70
+ }
71
+
72
+ export function readLlmCallOutcomes(opts: {
73
+ dbPath?: string;
74
+ sinceMs?: number;
75
+ limit?: number;
76
+ open?: (path: string) => Pick<LogStore, 'query' | 'close'>;
77
+ } = {}): LlmCallOutcome[] {
78
+ const path = opts.dbPath ?? logsDbPath();
79
+ const open = opts.open ?? ((dbPath: string) => LogStore.openReadOnly(dbPath));
80
+ let store: Pick<LogStore, 'query' | 'close'> | undefined;
81
+ try {
82
+ store = open(path);
83
+ const rows = store.query({
84
+ exactCategories: [LLM_CALL_CATEGORY],
85
+ events: [LLM_CALL_EVENT],
86
+ ...(opts.sinceMs !== undefined ? { sinceMs: opts.sinceMs } : {}),
87
+ limit: opts.limit ?? 50,
88
+ });
89
+ return outcomesFromCallRows(rows);
90
+ } catch {
91
+ return [];
92
+ } finally {
93
+ try { store?.close(); } catch { /* a missing store is "no sample", not a launch failure */ }
94
+ }
95
+ }
96
+
97
+ export function decideOverloadFailoverLaunch(input: OverloadFailoverLaunchInput): OverloadFailoverLaunchDecision {
98
+ const streak = input.streak ?? OVERLOAD_FAILOVER_STREAK;
99
+ const base = { provider: input.provider, ...(input.model !== undefined ? { model: input.model } : {}) };
100
+ if (input.codexExplicit) return { ...base, switched: false, why: 'explicit-codex' };
101
+ const onCodex = input.provider === 'openai-codex' || input.provider === 'codex';
102
+ if (!onCodex) return { ...base, switched: false, why: 'not-codex' };
103
+ if (overloadRecovered(input.outcomes, streak)) return { ...base, switched: false, why: 'recovered' };
104
+ if (!codexOverloadStreakReached(input.outcomes, streak)) return { ...base, switched: false, why: 'below-streak' };
105
+ if (input.grokAvailable === false) return { ...base, switched: false, why: 'grok-unavailable' };
106
+ return {
107
+ provider: 'grok',
108
+ model: lookupLlmTierSpec('grok', 'better').model,
109
+ switched: true,
110
+ why: 'streak',
111
+ };
112
+ }
113
+
114
+ /** Apply the gate to a not-yet-pinned child selection. Explicit codex is left untouched. */
115
+ export function applyOverloadFailoverToChild<T extends { provider: string; model: string; source: 'flag' | 'config' }>(
116
+ selection: T | undefined,
117
+ opts: {
118
+ codexExplicit: boolean;
119
+ outcomes: readonly LlmCallOutcome[];
120
+ grokAvailable?: boolean;
121
+ defaultProvider?: string;
122
+ },
123
+ ): { selection: T | { provider: string; model: string; source: 'config' } | undefined; decision: OverloadFailoverLaunchDecision } {
124
+ const provider = selection?.provider ?? opts.defaultProvider ?? 'openai-codex';
125
+ const decision = decideOverloadFailoverLaunch({
126
+ codexExplicit: opts.codexExplicit,
127
+ provider,
128
+ ...(selection?.model !== undefined ? { model: selection.model } : {}),
129
+ outcomes: opts.outcomes,
130
+ grokAvailable: opts.grokAvailable,
131
+ });
132
+ if (!decision.switched || !decision.model) return { selection, decision };
133
+ return {
134
+ selection: { provider: decision.provider, model: decision.model, source: 'config' },
135
+ decision,
136
+ };
137
+ }
@@ -99,6 +99,8 @@ export interface EnhanceOpts {
99
99
  reasoningEffort?: 'low' | 'medium' | 'high';
100
100
  /** 산출물 유형 힌트(선택) — 예: 'PPT 발표덱', 'PLAN 문서'. 제약 문구 조정. */
101
101
  deliverableHint?: string;
102
+ /** Optional usage observer for the caller's authoring phase; missing usage stays unknown. */
103
+ onUsage?: (usage: LLMUsage) => void;
102
104
  /** 인핸싱 끄기(순수 verbatim). true 면 원문만 fenced 로 감싸 반환. */
103
105
  disabled?: boolean;
104
106
  /** Ask 원문 밖 스캐폴드에만 렌더할, 저작기가 관측한 디렉터리 실물 수. */
@@ -368,7 +370,10 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
368
370
  const rawOut = await streamLLM(messages, () => {}, {
369
371
  model,
370
372
  reasoningEffort: opts.reasoningEffort ?? 'high',
371
- onUsage: (usage) => logLlmUsage(model, usage),
373
+ onUsage: (usage) => {
374
+ logLlmUsage(model, usage);
375
+ try { opts.onUsage?.(usage); } catch { /* usage observation must not change enhancement */ }
376
+ },
372
377
  });
373
378
  const parsed = parseEnhanceJson(rawOut);
374
379
  if (parsed) {