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
@@ -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();
@@ -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) {
@@ -15,10 +15,12 @@ export function selectScheduledCut(schedules: readonly ReleaseSchedule[], now: D
15
15
  .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt) || a.version.localeCompare(b.version))[0] ?? null;
16
16
  }
17
17
 
18
+ export type AutoStartDeferral = { deferred: true; reason: Exclude<ReleaseReadiness, { ready: true }>['reason'] | 'frozen' };
19
+
18
20
  export type AutoStartResult =
19
21
  | { status: 'no-cut' }
20
22
  | { status: 'planned'; version: string; cutAt: string }
21
- | { status: 'skipped'; version: string; reason: Exclude<ReleaseReadiness, { ready: true }>['reason'] }
23
+ | { status: 'skipped'; version: string; reason: AutoStartDeferral['reason'] }
22
24
  | { status: 'started'; version: string; cutAt: string };
23
25
 
24
26
  export interface AutoStartOptions {
@@ -28,7 +30,7 @@ export interface AutoStartOptions {
28
30
  ledgerRoot?: string;
29
31
  schedules?: (root: string) => ReleaseSchedule[];
30
32
  readiness?: (version: string, ledgerRoot: string, now: Date) => ReleaseReadiness;
31
- launch?: (version: string) => void | Promise<void>;
33
+ launch?: (version: string) => void | AutoStartDeferral | Promise<void | AutoStartDeferral>;
32
34
  }
33
35
 
34
36
  /** Select from the release schedule; the existing `release run --if-ready` owns the launch-time lock and guards. */
@@ -43,6 +45,7 @@ export async function autoStartScheduledRelease(options: AutoStartOptions): Prom
43
45
  const readiness = (options.readiness ?? ((v, ledgerRoot, at) => releaseReadiness(v, { ledgerRoot, now: () => at })))(version, root, now);
44
46
  if (!readiness.ready) return { status: 'skipped', version, reason: readiness.reason };
45
47
  if (!options.launch) throw new Error('auto-start apply requires a launch callback');
46
- await options.launch(version);
48
+ const launched = await options.launch(version);
49
+ if (launched?.deferred) return { status: 'skipped', version, reason: launched.reason };
47
50
  return { status: 'started', version, cutAt };
48
51
  }
@@ -16,6 +16,8 @@ export interface ChecklistItem {
16
16
  priority?: 'P0' | 'P1' | 'P2';
17
17
  predecessors?: string[];
18
18
  deadlineVersion?: string;
19
+ ceoMinutes?: number;
20
+ ceoDate?: string;
19
21
  evidence?: string;
20
22
  disposition?: ChecklistDisposition;
21
23
  kind?: ChecklistKind;
@@ -96,7 +98,12 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
96
98
  debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
97
99
  }
98
100
 
99
- export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
101
+ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }): void {
102
+ if (input.ceoMinutes !== undefined && (!Number.isSafeInteger(input.ceoMinutes) || input.ceoMinutes < 0)) throw new CliUserError('대표 손 분량은 0 이상의 정수 분이어야 한다');
103
+ if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
104
+ }
105
+
106
+ export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
100
107
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
101
108
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
102
109
  if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
@@ -104,6 +111,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
104
111
  if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
105
112
  if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
106
113
  if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
114
+ validateCeoLoad(input);
107
115
  return mutate(v, (data, otherItems) => {
108
116
  if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
109
117
  const collisions = otherItems(input.id);
@@ -114,14 +122,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
114
122
  }
115
123
  const by = process.env.ELANOUS_TRACK || 'cli';
116
124
  const at = new Date().toISOString();
117
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), updatedAt: at, updatedBy: by };
125
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), updatedAt: at, updatedBy: by };
118
126
  data.items.push(item);
119
127
  change(data, item.id, 'add', null, item, by, at);
120
128
  return true;
121
129
  });
122
130
  }
123
131
 
124
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, by: string): Checklist {
132
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, by: string): Checklist {
125
133
  return mutate(v, (data) => {
126
134
  const item = data.items.find((i) => i.id === id);
127
135
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
@@ -131,13 +139,14 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
131
139
  if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
132
140
  if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
133
141
  if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
142
+ validateCeoLoad(patch);
134
143
  if (patch.owner !== undefined) {
135
144
  parseOwner(patch.owner);
136
145
  if (item.owner && patch.owner !== item.owner) {
137
146
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
138
147
  }
139
148
  }
140
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
149
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
141
150
  if (fields.length === 0) return false;
142
151
  const at = new Date().toISOString();
143
152
  for (const field of fields) {