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,133 @@
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { Database } from 'bun:sqlite';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ export type SeatRequestStatus = 'pending' | 'queued' | 'rejected' | 'done';
7
+ export type SeatRequestRow = {
8
+ key: string;
9
+ seat: string;
10
+ status: SeatRequestStatus;
11
+ text: string;
12
+ queuedAt: string;
13
+ reason?: string;
14
+ closedAt?: string;
15
+ [field: string]: unknown;
16
+ };
17
+
18
+ export type SeatRequestFilters = { seat?: string; status?: SeatRequestStatus };
19
+ export type CloseSeatRequestsOptions = {
20
+ reason: string;
21
+ status?: 'rejected' | 'done';
22
+ /** Minimum age in milliseconds, measured from queuedAt; only strictly older requests qualify. */
23
+ olderThan?: number;
24
+ now?: Date;
25
+ dryRun?: boolean;
26
+ };
27
+
28
+ const ledgerPath = (root: string): string => join(root, 'seat-requests', 'requests.jsonl');
29
+
30
+ /** SQLite releases its transaction lock on process death, including SIGKILL. */
31
+ export function withSeatRequestLedgerLock<T>(path: string, work: () => T): T {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ const db = new Database(`${path}.lock.sqlite`);
34
+ try {
35
+ db.exec('PRAGMA busy_timeout = 10000');
36
+ db.exec('BEGIN IMMEDIATE');
37
+ try {
38
+ const result = work();
39
+ db.exec('COMMIT');
40
+ return result;
41
+ } catch (error) {
42
+ db.exec('ROLLBACK');
43
+ throw error;
44
+ }
45
+ } finally { db.close(); }
46
+ }
47
+
48
+ /** Called under the journal transaction lock; never expose a partial JSON line. */
49
+ export function appendSeatRequestRows<T>(path: string, rows: readonly T[],
50
+ write: (fd: number, data: Buffer, offset: number, length: number) => number = writeSync): void {
51
+ if (!rows.length) return;
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ const prior = existsSync(path) ? readFileSync(path, 'utf8') : '';
54
+ if (prior && !prior.endsWith('\n')) throw new Error('invalid seat request journal terminator');
55
+ const data = Buffer.from(prior + rows.map((row) => `${JSON.stringify(row)}\n`).join(''));
56
+ const temp = `${path}.${randomUUID()}.tmp`;
57
+ let fd: number | undefined;
58
+ try {
59
+ fd = openSync(temp, 'wx', 0o600);
60
+ let offset = 0;
61
+ while (offset < data.length) {
62
+ const count = write(fd, data, offset, data.length - offset);
63
+ if (count <= 0) throw new Error('seat request journal write stalled');
64
+ offset += count;
65
+ }
66
+ fsyncSync(fd);
67
+ closeSync(fd);
68
+ fd = undefined;
69
+ renameSync(temp, path);
70
+ const directory = openSync(dirname(path), 'r');
71
+ try { fsyncSync(directory); } finally { closeSync(directory); }
72
+ } catch (error) {
73
+ if (fd !== undefined) closeSync(fd);
74
+ try { unlinkSync(temp); } catch { /* temp was not created or already renamed */ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ function latestRows(text: string): Map<string, SeatRequestRow> {
80
+ const latest = new Map<string, SeatRequestRow>();
81
+ for (const line of text.split('\n')) {
82
+ if (!line.trim()) continue;
83
+ const row: unknown = JSON.parse(line);
84
+ if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('invalid seat request row');
85
+ const request = row as Record<string, unknown>;
86
+ if (typeof request.key !== 'string' || !request.key || typeof request.seat !== 'string' || !request.seat
87
+ || !['pending', 'queued', 'rejected', 'done'].includes(String(request.status))
88
+ || typeof request.text !== 'string' || typeof request.queuedAt !== 'string') {
89
+ throw new Error('invalid seat request row');
90
+ }
91
+ latest.set(request.key, request as SeatRequestRow);
92
+ }
93
+ return latest;
94
+ }
95
+
96
+ /** Filters apply to the latest row, never to a historical row superseded by a closing event. */
97
+ export function listSeatRequests(root: string, filters: SeatRequestFilters = {}): SeatRequestRow[] {
98
+ const path = ledgerPath(root);
99
+ return [...latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '').values()].filter((row) =>
100
+ (filters.seat === undefined || row.seat === filters.seat)
101
+ && (filters.status === undefined || row.status === filters.status));
102
+ }
103
+
104
+ /** Append at most one terminal event for each distinct, currently open request key. */
105
+ export function closeSeatRequests(root: string, keys: readonly string[], options: CloseSeatRequestsOptions): SeatRequestRow[] {
106
+ const reason = options.reason?.trim();
107
+ if (!reason) throw new Error('seat request close requires a reason');
108
+ const status = options.status ?? 'rejected';
109
+ if (status !== 'rejected' && status !== 'done') throw new Error('invalid seat request close status');
110
+ if (options.olderThan !== undefined && (!Number.isSafeInteger(options.olderThan) || options.olderThan < 0)) {
111
+ throw new Error('invalid seat request older-than duration');
112
+ }
113
+ const now = options.now ?? new Date();
114
+ if (!Number.isFinite(now.getTime())) throw new Error('invalid seat request close time');
115
+ const path = ledgerPath(root);
116
+ const close = (): SeatRequestRow[] => {
117
+ const latest = latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '');
118
+ const closed = [...new Set(keys)].flatMap((key) => {
119
+ const row = latest.get(key);
120
+ if (!row || (row.status !== 'pending' && row.status !== 'queued')) return [];
121
+ if (options.olderThan !== undefined) {
122
+ const queuedAt = Date.parse(row.queuedAt);
123
+ if (!Number.isFinite(queuedAt) || now.getTime() - queuedAt <= options.olderThan) return [];
124
+ }
125
+ return [{ ...row, status, reason, closedAt: now.toISOString(),
126
+ ...(row.status === 'queued' && typeof row.ref !== 'string' && typeof row.receiptId === 'string'
127
+ ? { ref: row.receiptId } : {}) } satisfies SeatRequestRow];
128
+ });
129
+ if (!options.dryRun) appendSeatRequestRows(path, closed);
130
+ return closed;
131
+ };
132
+ return options.dryRun ? close() : withSeatRequestLedgerLock(path, close);
133
+ }
@@ -21,6 +21,7 @@ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePerson
21
21
  import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
22
22
  import { PersonaRegistry } from '../persona/registry.js';
23
23
  import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
24
+ import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
24
25
  import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
25
26
  import { queryRunningRuns } from '../self-implement/running-runs.js';
26
27
  import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
@@ -42,8 +43,14 @@ export type TcCandidate =
42
43
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
43
44
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
44
45
  export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
45
- export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
46
- export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
46
+ export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
47
+ export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
48
+
49
+ export function seatLoopTickLine(result: SeatLoopResult): string {
50
+ const suffix = (result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason
51
+ ? ` (live-safe 요청 · 이유: ${result.modeDowngradeReason})` : '';
52
+ return `seat loop ${result.seat}: ${result.status}${'runId' in result && result.runId ? ` ${result.runId}` : ''}${suffix}`;
53
+ }
47
54
  export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
48
55
  export type SeatDeps = {
49
56
  personaConfig?: PersonaLoopConfig;
@@ -77,6 +84,8 @@ export type SeatDeps = {
77
84
  stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
78
85
  /** Open cards from the decision ledger; injected only for isolated ledger tests. */
79
86
  pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
87
+ /** Running-run observation for OP cutoff; incomplete observations cannot establish that a run is absent. */
88
+ cutoffRuns?: (runIds: readonly string[]) => Pick<ReturnType<typeof queryRunningRuns>, 'completeness' | 'entries' | 'pty'>;
80
89
  /** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
81
90
  pullRequests?: () => Promise<TcPullRequest[]>;
82
91
  ledgerFiles?: (directory: string) => string[];
@@ -200,6 +209,10 @@ async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<Person
200
209
 
201
210
  export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
202
211
  const result = await runSeatLoopOnce(seat, deps);
212
+ if ((result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason) {
213
+ try { debug.log('seat-loop', 'mode-downgraded', { seat, requested: 'live-safe', effective: result.status, reason: result.modeDowngradeReason }); }
214
+ catch { /* logging cannot change execution */ }
215
+ }
203
216
  try { await runPersonaShadowOnce(deps); }
204
217
  catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
205
218
  try {
@@ -301,8 +314,18 @@ function decisionRef(seat: string, item: SeatItem): string {
301
314
  return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
302
315
  }
303
316
 
317
+ // The decision ledger counts Unicode characters and sentence boundaries before it stores a card.
318
+ function decisionSummary(text: string): string {
319
+ const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?。!?])\s+/u).slice(0, 2).join(' ');
320
+ return Array.from(sentences).slice(0, 240).join('').trim();
321
+ }
322
+
323
+ function decisionSourceRef(seat: string, item: SeatItem): string {
324
+ return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
325
+ }
326
+
304
327
  function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
305
- const ref = decisionRef(seat, item);
328
+ const ref = decisionSourceRef(seat, item);
306
329
  return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
307
330
  }
308
331
 
@@ -456,6 +479,62 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
456
479
  return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
457
480
  }
458
481
 
482
+ const OP_CUTOFF_WINDOW_MS = 2 * 60 * 60_000;
483
+ const RUN_REFERENCE = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
484
+
485
+ function checkOpLandingCutoff(deps: SeatDeps, now: Date, path: string, stateRoot: string): SeatEntry | undefined {
486
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
487
+ const shipped = releasedVersion(releaseRoot);
488
+ const schedule = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
489
+ .filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && (!shipped || versionOrder(row.version, shipped) > 0))
490
+ .sort((a, b) => versionOrder(a.version, b.version))[0];
491
+ if (!schedule?.landBy) return;
492
+ const untilLanding = Date.parse(schedule.landBy) - now.getTime();
493
+ if (!Number.isFinite(untilLanding) || untilLanding < 0 || untilLanding > OP_CUTOFF_WINDOW_MS) return;
494
+ const append = deps.append ?? defaultAppend;
495
+ let items: ReturnType<NonNullable<SeatDeps['checklistItems']>>;
496
+ try { items = (deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items))(schedule.version); }
497
+ catch (error) {
498
+ const entry: SeatEntry = { seat: 'OP', ts: now.toISOString(), at: now.toISOString(), status: 'shadow',
499
+ reason: `체크리스트 못 읽음: ${schedule.version} (${String(error).slice(0, 200)})`,
500
+ modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' };
501
+ append(path, entry);
502
+ return entry;
503
+ }
504
+ const yellow = items.filter((item) => item.status === 'yellow');
505
+ const runIds = [...new Set(yellow.flatMap((item) => [...(item.evidence ?? '').matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase())))];
506
+ let running = new Set<string>();
507
+ if (runIds.length) {
508
+ try {
509
+ const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.op-cutoff' })))(runIds);
510
+ if (observed.completeness !== 'complete' || observed.pty.unreadable.length) {
511
+ observe('op-cutoff-runs-unreadable', { version: schedule.version });
512
+ return;
513
+ }
514
+ running = new Set(observed.entries.filter((row) => row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown')
515
+ .map((row) => row.runId.toLowerCase()));
516
+ } catch (error) {
517
+ observe('op-cutoff-runs-unreadable', { version: schedule.version, error: String(error).slice(0, 200) });
518
+ return;
519
+ }
520
+ }
521
+ const candidates = yellow.filter((item) => ![...(item.evidence ?? '').matchAll(RUN_REFERENCE)]
522
+ .some(([id]) => running.has(id.toLowerCase())))
523
+ .sort((a, b) => a.id.localeCompare(b.id))
524
+ .map(({ id, title, owner }) => ({ id, title, owner: owner ?? '미배정' }));
525
+ try { debug.log('seat-loop', 'op-cutoff-check', { version: schedule.version, landBy: schedule.landBy, candidates }); }
526
+ catch { /* Observation cannot change a request. */ }
527
+ if (!candidates.length) return;
528
+ const key = `seat-loop:op-cutoff:${schedule.version}:${seatDay(now)}`;
529
+ const requestPath = join(stateRoot, 'seat-requests', 'requests.jsonl');
530
+ withSeatRequestLedgerLock(requestPath, () => {
531
+ if (listSeatRequests(stateRoot).some((row) => row.key === key)) return;
532
+ appendSeatRequestRows(requestPath, [{ key, seat: 'OP', status: 'pending' as const, queuedAt: now.toISOString(),
533
+ text: `${schedule.version} 착지 마감 ${schedule.landBy} 전 노랑 칸 점검 — 다음 판으로 옮길지 OP 판단:\n${candidates.map((item) => `${item.id} · ${item.title} · 주인 ${item.owner}`).join('\n')}`,
534
+ version: schedule.version, landBy: schedule.landBy, candidates }]);
535
+ });
536
+ }
537
+
459
538
  function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
460
539
  const releaseRoot = deps.root ?? releaseLedgerRoot();
461
540
  const shipped = releasedVersion(releaseRoot);
@@ -827,10 +906,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
827
906
  export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
828
907
  if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
829
908
  const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
830
- if (config.mode === 'off' || !(config.seats ?? ['MK']).includes(seat)) {
909
+ if (config.mode === 'off') {
831
910
  observe('skipped-off', { seat });
832
911
  return { seat, status: 'skipped-off' };
833
912
  }
913
+ if (!(config.seats ?? ['MK']).includes(seat)) {
914
+ // An excluded seat does not read inputs or run the shadow loop; never report it as shadow.
915
+ observe('skipped-off', { seat });
916
+ return { seat, status: 'skipped-off',
917
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: '자리 목록에 없음 (loops.seat.seats)' } : {}) };
918
+ }
834
919
  const now = (deps.now ?? (() => new Date()))();
835
920
  const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
836
921
  const directory = dirname(path);
@@ -928,6 +1013,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
928
1013
  return entry;
929
1014
  }
930
1015
  }
1016
+ let tcDowngrade: SeatEntry | undefined;
931
1017
  if (seat === 'TC') {
932
1018
  const candidates = await tcCandidates(deps, now);
933
1019
  for (const candidate of candidates) {
@@ -938,19 +1024,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
938
1024
  && row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
939
1025
  if (!inLedger) {
940
1026
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1027
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC 타 자리 결함 질문은 shadow 기록만 구현' } : {}),
941
1028
  item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
942
1029
  action: 'seat-question', inquiry: { to: candidate.to, question } };
943
1030
  (deps.append ?? defaultAppend)(path, entry);
1031
+ tcDowngrade ??= entry;
944
1032
  }
945
1033
  if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
946
1034
  continue;
947
1035
  }
948
1036
  if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
949
- const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
1037
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1038
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC PR 판정은 shadow 기록만 구현' } : {}) };
950
1039
  (deps.append ?? defaultAppend)(path, entry);
1040
+ tcDowngrade ??= entry;
951
1041
  observe('tc-shadow-candidate', { seat, candidate });
952
1042
  }
953
1043
  }
1044
+ if (seat === 'OP' && config.mode === 'live-safe') {
1045
+ const unreadable = checkOpLandingCutoff(deps, now, path, stateRoot);
1046
+ if (unreadable) return unreadable;
1047
+ }
954
1048
  if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
955
1049
  !hasSeatAnswer(stateRoot, question)
956
1050
  && !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
@@ -987,7 +1081,8 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
987
1081
  if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
988
1082
  }
989
1083
  }
990
- const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate }));
1084
+ const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
1085
+ ...(config.mode === 'live-safe' ? { modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' } : {}) }));
991
1086
  for (const entry of entries) {
992
1087
  const candidate = entry.candidate!;
993
1088
  const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
@@ -1003,7 +1098,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1003
1098
  }
1004
1099
  return entries[0]!;
1005
1100
  }
1101
+ const failedDecisions = new Set<string>();
1102
+ let lastDecisionFailure: SeatEntry | undefined;
1103
+ while (true) {
1006
1104
  const inputs = await gatherSeatInputs(seat, deps, ledger);
1105
+ inputs.requests = inputs.requests.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
1106
+ inputs.checklist = inputs.checklist.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
1007
1107
  const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
1008
1108
  const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
1009
1109
  const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
@@ -1085,6 +1185,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1085
1185
  entry.action = 'decision';
1086
1186
  }
1087
1187
  if (questionsMode !== 'on') {
1188
+ if (config.mode === 'live-safe') entry.modeDowngradeReason = '질문 전달 선행 조건 미충족 (questions shadow)';
1088
1189
  if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
1089
1190
  observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
1090
1191
  }
@@ -1192,9 +1293,11 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1192
1293
  if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
1193
1294
  observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
1194
1295
  }
1296
+ if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
1195
1297
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
1196
1298
  ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
1197
1299
  if (config.mode === 'shadow' || !item) {
1300
+ if (!item && lastDecisionFailure) return lastDecisionFailure;
1198
1301
  append(path, entry);
1199
1302
  if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
1200
1303
  return entry;
@@ -1282,11 +1385,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1282
1385
  append(path, { ...entry, status: 'attempting' });
1283
1386
  try {
1284
1387
  if (action.kind === 'decision') {
1285
- const title = `${seat}: ${action.text.replace(/[\r\n]+/g, ' ')}`;
1388
+ const title = `${seat}: ${decisionSummary(action.text)}`;
1389
+ const source = decisionSourceRef(seat, item);
1286
1390
  const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
1287
- '--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1391
+ '--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
1392
+ '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1288
1393
  '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
1289
- '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--json']);
1394
+ '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1395
+ '--agent', 'seat-loop', '--ref', source, '--json']);
1290
1396
  const decision: unknown = JSON.parse(raised.trim());
1291
1397
  if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
1292
1398
  throw new Error('decisions raise returned no decision id');
@@ -1332,9 +1438,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1332
1438
  observe('launched', { seat, item, runId });
1333
1439
  return entry;
1334
1440
  } catch (error) {
1335
- append(path, { ...entry, status: 'outcome-unknown' });
1441
+ const failure: SeatEntry = { ...entry, status: 'outcome-unknown' };
1442
+ append(path, failure);
1443
+ if (action.kind === 'decision') {
1444
+ lastDecisionFailure = failure;
1445
+ const source = decisionSourceRef(seat, item);
1446
+ try { debug.log('seat-loop', 'decision-raise-failed', { source, error: String(error) }); }
1447
+ catch { /* A failed observation cannot interrupt the remaining items. */ }
1448
+ failedDecisions.add(itemKey(item));
1449
+ continue;
1450
+ }
1336
1451
  throw error;
1337
1452
  }
1453
+ }
1338
1454
  } catch (error) {
1339
1455
  turnError = error;
1340
1456
  throw error;
@@ -35,6 +35,7 @@ import type { DocumentReferenceStatus } from '../self-implement/self-implement-r
35
35
  import type { SelfDevGoal, OrchestrateSelfDevOptions, SelfDevJobResult } from './orchestrate.js';
36
36
  import { launchCapabilityObservation, resolveLaunchCapabilities } from './launch-capabilities.js';
37
37
  import { lookupEntrance, type EntranceId } from './entrance-registry.js';
38
+ import { resolveLaunchStamp } from '../harness/launch-stamp.js';
38
39
  import type { IngestionEntry } from '../agent-substrate/execution/ingestion-policy.js';
39
40
  import type { ReviewerContextItem } from '../agent-substrate/pr-reviewer.js';
40
41
  import { resolveRunIdentity, type RunIdSource } from '../harness/harness-space.js';
@@ -42,7 +43,7 @@ import { addSelfDevRunParticipant, processBirthId, saveSelfDevRun } from './run-
42
43
  import { getUserConfig, resolveRoleLlm } from '../user-config.js';
43
44
  import { reviewReasoningEffort } from '../model-tier/review-effort.js';
44
45
  import { getProvider, inferProviderFromModel } from '../llm.js';
45
- import { buildReviewProviderAttempts, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
46
+ import { buildReviewProviderAttempts, defaultReviewFallbackModel, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
46
47
  import type { DefaultSeamsOptions } from '../self-implement/seams.js';
47
48
  import { harnessTargetOptions, resolveHarnessTarget, revalidateHarnessTarget, type HarnessTargetResolution } from '../self-implement/harness-target-options.js';
48
49
  import { provisionRepository, type RepoProvisionResult } from '../self-implement/repo-provision.js';
@@ -609,6 +610,7 @@ export function toSelfImplementOptions(text: string, plan: ResolvedDevPlan, seam
609
610
  ...(s.correlationId !== undefined ? { correlationId: s.correlationId } : {}),
610
611
  ...(s.branchName ? { branchName: s.branchName } : {}),
611
612
  ...(s.maxReworkRounds !== undefined ? { maxReworkRounds: s.maxReworkRounds } : {}),
613
+ launch: resolveLaunchStamp({ entrance: plan.entrance, queueId: process.env.ELANOUS_HARNESS_QUEUE_LAUNCH }),
612
614
  };
613
615
  }
614
616
 
@@ -1093,10 +1095,12 @@ export async function buildDefaultSelfImplementSeams(
1093
1095
  provider: inferProviderFromModel(model) ? getProvider(model) : undefined,
1094
1096
  label,
1095
1097
  });
1098
+ const primaryReviewAttempt = reviewAttemptForModel(reviewModel, 'primary');
1096
1099
  const reviewAttempts = buildReviewProviderAttempts(
1097
- reviewAttemptForModel(reviewModel, 'primary'),
1098
- reviewFallbackModels.map((model) => reviewAttemptForModel(model, model)),
1099
- );
1100
+ primaryReviewAttempt,
1101
+ [...reviewFallbackModels, defaultReviewFallbackModel(primaryReviewAttempt.provider?.name)]
1102
+ .map((model) => reviewAttemptForModel(model, model)),
1103
+ ).slice(0, 2);
1100
1104
  const defaultApiReview = async (prompt: string): Promise<string> => {
1101
1105
  const r = await runReviewWithFallback(
1102
1106
  reviewAttempts,
@@ -1,6 +1,6 @@
1
1
  import { CLAIM_IDLE_HOURS, PR_LABELS, STALLED_DRAFT_HOURS } from '../github/pr-labels.js';
2
2
  import { debug } from '../debug/log.js';
3
- import { decideDraft, type DraftTriagePr } from './draft-triage-rules.js';
3
+ import { decideDraft, sameGoalPr, type DraftTriagePr } from './draft-triage-rules.js';
4
4
 
5
5
  export interface SweepDraft extends DraftTriagePr {
6
6
  readonly title: string;
@@ -33,6 +33,18 @@ export interface DraftSweepAdapters {
33
33
  getLatestFileChanges?(draft: SweepDraft, repository: string): Promise<Readonly<Record<string, string>> | undefined>;
34
34
  /** Optional merge-time file inventory for the existing all-files-landed decision. */
35
35
  getPrFiles?(repository: string, number: number): Promise<readonly string[] | undefined>;
36
+ /**
37
+ * Review and gate posture for the daily count. Omitted means unknown — not harvestable and not blocked.
38
+ * `review: 'pass'` is a review PASS; `mustFix` means must-fix remains; `gate: 'pass'` is a green gate.
39
+ */
40
+ getReviewGate?(draft: SweepDraft, repository: string): Promise<SweepReviewGate | undefined>;
41
+ }
42
+
43
+ /** Review PASS and gate posture used only by the daily old-draft count. */
44
+ export interface SweepReviewGate {
45
+ readonly review?: 'pass' | 'fail';
46
+ readonly mustFix?: boolean;
47
+ readonly gate?: 'pass' | 'fail';
36
48
  }
37
49
 
38
50
  export interface DraftSweepOptions {
@@ -67,9 +79,23 @@ export interface DraftSweepResult {
67
79
  claimed?: number;
68
80
  /** Running claims older than the idle window, including those kept by liveness. */
69
81
  claimExpired?: number;
82
+ /**
83
+ * One line per sweep: harness drafts older than 24h, split into closable (a same-goal landing exists),
84
+ * harvestable (review PASS and gate pass), and blocked (must-fix remains).
85
+ */
86
+ daily?: DraftSweepDaily;
70
87
  error?: string;
71
88
  }
72
89
 
90
+ /** Daily old-draft census. `date` is the sweep clock's UTC calendar day. */
91
+ export interface DraftSweepDaily {
92
+ readonly date: string;
93
+ readonly over24h: number;
94
+ readonly closable: number;
95
+ readonly harvestable: number;
96
+ readonly blocked: number;
97
+ }
98
+
73
99
  const stateLabels = PR_LABELS.filter((label) => label.axis === 'state');
74
100
  const label = (action: 'mark-stalled' | 'close', name?: string): string => {
75
101
  const match = PR_LABELS.find((entry) => entry.axis === 'state' && entry.sweep.action === action && (name === undefined || entry.name.endsWith(name)));
@@ -115,7 +141,7 @@ export async function supersedeDraftsOnMerge(input: {
115
141
  const mergedFiles = await adapters.getPrFiles?.(repository, merged.number);
116
142
  const landed = { ...merged, changedFiles: mergedFiles };
117
143
  for (const draft of drafts) {
118
- if (draft.number !== merged.number && !liveBranches.has(draft.branch)
144
+ if (draft.number !== merged.number && draft.branch.startsWith('self-impl/') && !liveBranches.has(draft.branch)
119
145
  && Date.parse(draft.createdAt) < Date.parse(merged.mergedAt)) {
120
146
  statuses.set(draft.number, await adapters.getRunStatus(draft, repository));
121
147
  if (draft.changedFiles?.length && adapters.getLatestFileChanges) {
@@ -125,7 +151,7 @@ export async function supersedeDraftsOnMerge(input: {
125
151
  }
126
152
  for (const listedDraft of drafts) {
127
153
  const draft = { ...listedDraft, latestFileChanges: latestChanges.get(listedDraft.number) };
128
- if (draft.number === merged.number || liveBranches.has(draft.branch)
154
+ if (draft.number === merged.number || !draft.branch.startsWith('self-impl/') || liveBranches.has(draft.branch)
129
155
  || Date.parse(draft.createdAt) >= Date.parse(merged.mergedAt)) {
130
156
  kept.push(draft.number);
131
157
  continue;
@@ -134,7 +160,6 @@ export async function supersedeDraftsOnMerge(input: {
134
160
  mergedTwins: [landed], liveBranches, ageHours: NaN });
135
161
  if (decision.action !== 'close' || !decision.reason.startsWith(`superseded-by #${merged.number}`)
136
162
  || PR_LABELS.filter((entry) => entry.axis === 'state' && draft.labels.includes(entry.name)).length > 1
137
- || (draft.labels.length === 0 && !draft.branch.startsWith('self-impl/'))
138
163
  || closeAttempts >= DRAFT_SWEEP_CLOSE_CAP) {
139
164
  kept.push(draft.number);
140
165
  continue;
@@ -144,7 +169,8 @@ export async function supersedeDraftsOnMerge(input: {
144
169
  try {
145
170
  // Label first: a failed label write must never strand a closed, unlabelled PR outside open-draft sweeps.
146
171
  if (!draft.labels.includes(supersededLabel)) await adapters.setLabels(repository, draft.number, { add: supersededLabel, remove });
147
- await adapters.closeDraft(repository, draft.number, `Draft sweep: superseded-by #${merged.number}. Branch preserved.`);
172
+ await adapters.closeDraft(repository, draft.number,
173
+ `Draft sweep: superseded-by #${merged.number} (https://github.com/${repository}/pull/${merged.number}). Branch preserved.`);
148
174
  closed.push(draft.number);
149
175
  } catch (failure) {
150
176
  kept.push(draft.number);
@@ -171,6 +197,46 @@ async function collectPages<T>(fetch: (page: number, perPage: number) => Promise
171
197
  throw new Error('PR listing exceeded pagination limit');
172
198
  }
173
199
 
200
+ /** First line of a failure, with no stack — the draft-cleanup loop log shows this line as-is. */
201
+ export function sweepFailureReason(error: unknown): string {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ const line = message.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0 && !/^\s*at\s/.test(part));
204
+ return line && line.length > 0 ? line : 'sweep failed';
205
+ }
206
+
207
+ function harnessDraft(draft: SweepDraft): boolean {
208
+ return draft.branch.startsWith('self-impl/') || draft.labels.some((name) => name.startsWith('elanous:'));
209
+ }
210
+
211
+ /**
212
+ * Census of harness drafts older than 24h. Same-goal landing uses `sameGoalPr` unchanged.
213
+ * A draft counts as closable when a same-goal merged PR exists, harvestable when review PASS and the gate passed,
214
+ * and blocked when must-fix remains. Unknown review/gate is neither harvestable nor blocked.
215
+ */
216
+ export function draftSweepDaily(
217
+ drafts: readonly SweepDraft[],
218
+ merged: readonly SweepMergedPr[],
219
+ now: Date,
220
+ reviewGate: ReadonlyMap<number, SweepReviewGate | undefined>,
221
+ ): DraftSweepDaily {
222
+ const cutoff = now.getTime() - STALLED_DRAFT_HOURS * 3_600_000;
223
+ let over24h = 0;
224
+ let closable = 0;
225
+ let harvestable = 0;
226
+ let blocked = 0;
227
+ for (const draft of drafts) {
228
+ if (!harnessDraft(draft)) continue;
229
+ const created = Date.parse(draft.createdAt);
230
+ if (!Number.isFinite(created) || created > cutoff) continue;
231
+ over24h += 1;
232
+ if (merged.some((pr) => pr.number !== draft.number && sameGoalPr(draft, pr))) closable += 1;
233
+ const posture = reviewGate.get(draft.number);
234
+ if (posture?.mustFix === true) blocked += 1;
235
+ else if (posture?.review === 'pass' && posture.gate === 'pass') harvestable += 1;
236
+ }
237
+ return { date: now.toISOString().slice(0, 10), over24h, closable, harvestable, blocked };
238
+ }
239
+
174
240
  /** A failed inventory or liveness lookup is never treated as proof that a draft can be closed. */
175
241
  export async function runDraftSweep({ repository, adapters, apply = false, now = new Date() }: DraftSweepOptions): Promise<DraftSweepResult> {
176
242
  const result: DraftSweepResult = { repository, apply, complete: false, entries: [], counts: {} };
@@ -181,10 +247,12 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
181
247
  };
182
248
  const finish = (): DraftSweepResult => {
183
249
  debug.log('drafts.cleanup', 'summary', result.counts);
250
+ if (result.daily) debug.log('harness.drafts', 'daily', result.daily);
251
+ if (!result.complete && result.error) debug.log('drafts.cleanup', 'failed', { reason: sweepFailureReason(result.error) });
184
252
  return result;
185
253
  };
186
- let drafts: SweepDraft[];
187
- let merged: SweepMergedPr[];
254
+ let drafts: SweepDraft[] = [];
255
+ let merged: SweepMergedPr[] = [];
188
256
  let liveBranches: ReadonlySet<string> | undefined;
189
257
  try {
190
258
  [drafts, merged, liveBranches] = await Promise.all([
@@ -196,7 +264,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
196
264
  if (drafts.some((pr) => !Number.isInteger(pr.number) || !pr.branch || !Array.isArray(pr.labels) || !Number.isFinite(Date.parse(pr.createdAt)))
197
265
  || merged.some((pr) => !Number.isInteger(pr.number) || !pr.branch)) throw new Error('Invalid PR inventory');
198
266
  } catch (error) {
199
- result.error = String(error);
267
+ result.error = sweepFailureReason(error);
268
+ if (drafts.length > 0) result.daily = draftSweepDaily(drafts, merged, now, new Map());
200
269
  return finish();
201
270
  }
202
271
  let statuses: Map<number, string | undefined>;
@@ -218,9 +287,19 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
218
287
  }
219
288
  }
220
289
  } catch (error) {
221
- result.error = String(error);
290
+ result.error = sweepFailureReason(error);
222
291
  return finish();
223
292
  }
293
+ let reviewGate = new Map<number, SweepReviewGate | undefined>();
294
+ if (adapters.getReviewGate) {
295
+ try {
296
+ for (const draft of drafts) reviewGate.set(draft.number, await adapters.getReviewGate(draft, repository));
297
+ } catch (error) {
298
+ result.error = sweepFailureReason(error);
299
+ return finish();
300
+ }
301
+ }
302
+ result.daily = draftSweepDaily(drafts, merged, now, reviewGate);
224
303
  result.complete = true;
225
304
  let closes = 0;
226
305
  let unobserved = 0;
@@ -330,7 +409,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
330
409
  if (action === 'close') {
331
410
  await adapters.closeDraft(repository, draft.number, decision.reason === 'stale-unobserved'
332
411
  ? `Draft sweep: run unobserved — no run record reachable here, no host worktree for this branch, idle ≥${STALLED_DRAFT_HOURS}h. Closed as stalled. Branch preserved; reopen to restore.`
333
- : `Draft sweep: ${decision.reason}. Branch preserved.`);
412
+ : `Draft sweep: ${decision.reason}${decision.reason.startsWith('superseded-by #')
413
+ ? ` (https://github.com/${repository}/pull/${decision.reason.slice('superseded-by #'.length).match(/^\d+/)![0]})` : ''}. Branch preserved.`);
334
414
  }
335
415
  entry.applied = labelsChanged || action === 'close';
336
416
  } catch (error) {